/ src / cli / agent-manager.js
agent-manager.js
  1  #!/usr/bin/env node
  2  
  3  /**
  4   * Agent Manager CLI
  5   *
  6   * Commands:
  7   *   npm run agent:list              - List agents and status
  8   *   npm run agent:tasks             - Show pending tasks
  9   *   npm run agent:tasks --assigned-to developer - Filter by agent
 10   *   npm run agent:logs              - View recent execution logs
 11   *   npm run agent:logs --task-id 123 - View logs for specific task
 12   *   npm run agent:create            - Create task manually
 13   *   npm run agent:workflow          - Trigger a workflow
 14   *   npm run agent:stats             - Show agent statistics
 15   */
 16  
 17  import { run, getOne, getAll } from '../utils/db.js';
 18  import { createBugFixWorkflow } from '../agents/workflows/bug-fix.js';
 19  import { createFeatureWorkflow } from '../agents/workflows/feature.js';
 20  import { createRefactorWorkflow } from '../agents/workflows/refactor.js';
 21  import { getAgentStats } from '../agents/runner.js';
 22  import { createAgentTask } from '../agents/utils/task-manager.js';
 23  
 24  /**
 25   * List all agents and their current status
 26   */
 27  async function listAgents() {
 28    const agents = await getAll(
 29      `SELECT
 30        agent_name,
 31        status,
 32        last_active,
 33        current_task_id,
 34        metrics_json
 35      FROM tel.agent_state
 36      ORDER BY agent_name`
 37    );
 38  
 39    console.log('\nπŸ“‹ Agent Status\n');
 40    console.log('Agent           Status      Last Active         Current Task');
 41    console.log('─'.repeat(70));
 42  
 43    for (const agent of agents) {
 44      const metrics = agent.metrics_json ? JSON.parse(agent.metrics_json) : {};
 45      const taskInfo = agent.current_task_id ? `#${agent.current_task_id}` : '-';
 46      const statusIcon = agent.status === 'working' ? '🟒' : agent.status === 'blocked' ? 'πŸ”΄' : 'βšͺ';
 47  
 48      console.log(
 49        `${agent.agent_name.padEnd(15)} ${statusIcon} ${agent.status.padEnd(10)} ${agent.last_active || 'Never'} ${taskInfo}`
 50      );
 51  
 52      if (metrics.circuit_breaker_triggered_at) {
 53        console.log(`  ⚠️  Circuit breaker triggered at ${metrics.circuit_breaker_triggered_at}`);
 54      }
 55    }
 56  
 57    console.log('');
 58  }
 59  
 60  /**
 61   * Show pending tasks
 62   *
 63   * @param {Object} options - Filter options
 64   */
 65  async function showTasks(options = {}) {
 66    let query = `
 67      SELECT
 68        id,
 69        task_type,
 70        assigned_to,
 71        status,
 72        priority,
 73        created_at,
 74        started_at,
 75        retry_count,
 76        error_message
 77      FROM tel.agent_tasks
 78    `;
 79  
 80    const conditions = [];
 81    const params = [];
 82    let paramIdx = 1;
 83  
 84    if (options.assignedTo) {
 85      conditions.push(`assigned_to = $${paramIdx++}`);
 86      params.push(options.assignedTo);
 87    }
 88  
 89    if (options.status) {
 90      conditions.push(`status = $${paramIdx++}`);
 91      params.push(options.status);
 92    } else {
 93      conditions.push("status IN ('pending', 'running', 'blocked')");
 94    }
 95  
 96    if (conditions.length > 0) {
 97      query += ` WHERE ${conditions.join(' AND ')}`;
 98    }
 99  
100    query += ' ORDER BY priority DESC, created_at ASC LIMIT 50';
101  
102    const tasks = await getAll(query, params.length ? params : undefined);
103  
104    console.log(`\nπŸ“‹ Tasks (${tasks.length})\n`);
105    console.log('ID    Type                  Agent       Status     Priority  Created');
106    console.log('─'.repeat(80));
107  
108    for (const task of tasks) {
109      const statusIcon =
110        task.status === 'running'
111          ? 'πŸ”„'
112          : task.status === 'blocked'
113            ? 'πŸ”΄'
114            : task.status === 'pending'
115              ? '⏳'
116              : 'βœ…';
117      const retryInfo = task.retry_count > 0 ? ` (retry ${task.retry_count}/3)` : '';
118  
119      console.log(
120        `${task.id.toString().padEnd(5)} ${task.task_type.padEnd(20)} ${task.assigned_to.padEnd(11)} ${statusIcon} ${task.status.padEnd(10)} ${task.priority}         ${task.created_at}${retryInfo}`
121      );
122  
123      if (task.error_message && task.status === 'blocked') {
124        console.log(`  ⚠️  ${task.error_message.substring(0, 70)}`);
125      }
126    }
127  
128    console.log('');
129  }
130  
131  /**
132   * Show agent execution logs
133   *
134   * @param {Object} options - Filter options
135   */
136  async function showLogs(options = {}) {
137    let query = `
138      SELECT
139        id,
140        task_id,
141        agent_name,
142        log_level,
143        message,
144        created_at
145      FROM tel.agent_logs
146    `;
147  
148    const conditions = [];
149    const params = [];
150    let paramIdx = 1;
151  
152    if (options.taskId) {
153      conditions.push(`task_id = $${paramIdx++}`);
154      params.push(options.taskId);
155    }
156  
157    if (options.agentName) {
158      conditions.push(`agent_name = $${paramIdx++}`);
159      params.push(options.agentName);
160    }
161  
162    if (options.level) {
163      conditions.push(`log_level = $${paramIdx++}`);
164      params.push(options.level);
165    }
166  
167    if (conditions.length > 0) {
168      query += ` WHERE ${conditions.join(' AND ')}`;
169    }
170  
171    query += ' ORDER BY created_at DESC LIMIT 100';
172  
173    const logs = await getAll(query, params.length ? params : undefined);
174  
175    console.log(`\nπŸ“‹ Agent Logs (${logs.length})\n`);
176    console.log('Time                Agent       Level   Task  Message');
177    console.log('─'.repeat(100));
178  
179    for (const log of logs) {
180      const levelIcon =
181        log.log_level === 'error'
182          ? '❌'
183          : log.log_level === 'warn'
184            ? '⚠️ '
185            : log.log_level === 'info'
186              ? 'ℹ️ '
187              : '  ';
188      const taskInfo = log.task_id ? `#${log.task_id}` : '-    ';
189  
190      console.log(
191        `${log.created_at} ${log.agent_name.padEnd(11)} ${levelIcon} ${log.log_level.padEnd(5)} ${taskInfo} ${log.message.substring(0, 60)}`
192      );
193    }
194  
195    console.log('');
196  }
197  
198  /**
199   * Create a new agent task manually
200   *
201   * @param {Object} options - Task options
202   */
203  async function createTask(options) {
204    const { agent, task, context, priority = 5 } = options;
205  
206    if (!agent || !task) {
207      console.error('❌ Error: --agent and --task are required');
208      console.log(
209        '\nUsage: npm run agent:create -- --agent developer --task fix_bug --context \'{"error":"..."}\''
210      );
211      console.log('\nValid agents: developer, qa, security, architect, triage, monitor');
212      console.log('Valid task types vary by agent (see agent documentation)');
213      process.exit(1);
214    }
215  
216    let contextObj = {};
217    if (context) {
218      try {
219        contextObj = JSON.parse(context);
220      } catch (err) {
221        console.error('❌ Error: Invalid JSON in --context');
222        process.exit(1);
223      }
224    }
225  
226    try {
227      const taskId = await createAgentTask({
228        task_type: task,
229        assigned_to: agent,
230        created_by: 'cli',
231        priority,
232        context: contextObj,
233      });
234  
235      console.log(`βœ… Task created: #${taskId}`);
236      console.log(`   Agent: ${agent}`);
237      console.log(`   Type: ${task}`);
238      console.log(`   Priority: ${priority}`);
239    } catch (err) {
240      console.error(`❌ Error creating task: ${err.message}`);
241      process.exit(1);
242    }
243  }
244  
245  /**
246   * Trigger a workflow
247   *
248   * @param {Object} options - Workflow options
249   */
250  async function triggerWorkflow(options) {
251    const { workflow, description, error, file, requirements } = options;
252  
253    if (!workflow) {
254      console.error('❌ Error: --workflow is required');
255      console.log(
256        '\nUsage: npm run agent:workflow -- --workflow bug-fix --error "..." --stack "..."'
257      );
258      console.log(
259        '       npm run agent:workflow -- --workflow feature --description "..." --requirements \'["req1","req2"]\''
260      );
261      console.log('       npm run agent:workflow -- --workflow refactor --file "..." --reason "..."');
262      process.exit(1);
263    }
264  
265    try {
266      let workflowId;
267  
268      if (workflow === 'bug-fix') {
269        if (!error) {
270          console.error('❌ Error: --error is required for bug-fix workflow');
271          process.exit(1);
272        }
273        workflowId = await createBugFixWorkflow(
274          error,
275          options.stack || '',
276          options.stage || 'unknown',
277          options.frequency || 1
278        );
279        console.log(`βœ… Bug fix workflow created: #${workflowId}`);
280      } else if (workflow === 'feature') {
281        if (!description) {
282          console.error('❌ Error: --description is required for feature workflow');
283          process.exit(1);
284        }
285        const reqs = requirements ? JSON.parse(requirements) : [];
286        workflowId = await createFeatureWorkflow(description, reqs);
287        console.log(`βœ… Feature workflow created: #${workflowId}`);
288      } else if (workflow === 'refactor') {
289        if (!file) {
290          console.error('❌ Error: --file is required for refactor workflow');
291          process.exit(1);
292        }
293        workflowId = await createRefactorWorkflow(file, options.reason || 'Manual refactor request');
294        console.log(`βœ… Refactor workflow created: #${workflowId}`);
295      } else {
296        console.error(`❌ Error: Unknown workflow type: ${workflow}`);
297        console.log('Valid workflows: bug-fix, feature, refactor');
298        process.exit(1);
299      }
300  
301      console.log(`\nTo monitor progress: npm run agent:tasks`);
302      console.log(`To view logs: npm run agent:logs --task-id ${workflowId}`);
303    } catch (err) {
304      console.error(`❌ Error creating workflow: ${err.message}`);
305      process.exit(1);
306    }
307  }
308  
309  /**
310   * Show tasks awaiting approval
311   *
312   * @param {Object} options - Filter options
313   */
314  async function showApprovals(options = {}) {
315    let query = `
316      SELECT
317        id,
318        task_type,
319        assigned_to,
320        status,
321        priority,
322        created_at,
323        result_json
324      FROM tel.agent_tasks
325      WHERE status IN ('awaiting_po_approval', 'awaiting_architect_approval')
326    `;
327  
328    const params = [];
329  
330    if (options.status) {
331      query += ` AND status = $1`;
332      params.push(options.status);
333    }
334  
335    query += ' ORDER BY priority DESC, created_at ASC';
336  
337    const tasks = await getAll(query, params.length ? params : undefined);
338  
339    console.log(`\nπŸ“‹ Tasks Awaiting Approval (${tasks.length})\n`);
340    console.log('ID    Type                  Status                        Priority  Created');
341    console.log('─'.repeat(95));
342  
343    for (const task of tasks) {
344      const result = task.result_json ? JSON.parse(task.result_json) : {};
345      const proposal = result.design_proposal || result.implementation_plan || {};
346  
347      console.log(
348        `${task.id.toString().padEnd(5)} ${task.task_type.padEnd(20)} ${task.status.padEnd(28)} ${task.priority}         ${task.created_at}`
349      );
350  
351      if (proposal.summary) {
352        console.log(`  πŸ“ ${proposal.summary.substring(0, 80)}`);
353      }
354    }
355  
356    console.log('');
357    console.log(
358      'To approve: npm run agent:approve -- --task-id <id> --reviewer "Name" --decision approved'
359    );
360    console.log(
361      'To reject:  npm run agent:approve -- --task-id <id> --reviewer "Name" --decision rejected --notes "Reason"'
362    );
363    console.log('');
364  }
365  
366  /**
367   * Approve or reject a task
368   *
369   * @param {Object} options - Approval options
370   */
371  async function approveTask(options) {
372    const { taskId, reviewer, decision, notes = '', conditions = '' } = options;
373  
374    if (!taskId || !reviewer || !decision) {
375      console.error('❌ Error: --task-id, --reviewer, and --decision are required');
376      console.log(
377        '\nUsage: npm run agent:approve -- --task-id 123 --reviewer "Jason" --decision approved'
378      );
379      console.log(
380        '       npm run agent:approve -- --task-id 123 --reviewer "Jason" --decision rejected --notes "Too complex"'
381      );
382      console.log('\nValid decisions: approved, approved_with_conditions, rejected');
383      process.exit(1);
384    }
385  
386    const validDecisions = ['approved', 'approved_with_conditions', 'rejected'];
387    if (!validDecisions.includes(decision)) {
388      console.error(`❌ Error: Invalid decision: ${decision}`);
389      console.log(`Valid decisions: ${validDecisions.join(', ')}`);
390      process.exit(1);
391    }
392  
393    // Get task
394    const task = await getOne('SELECT * FROM tel.agent_tasks WHERE id = $1', [taskId]);
395    if (!task) {
396      console.error(`❌ Error: Task ${taskId} not found`);
397      process.exit(1);
398    }
399  
400    if (!['awaiting_po_approval', 'awaiting_architect_approval'].includes(task.status)) {
401      console.error(`❌ Error: Task ${taskId} is not awaiting approval (status: ${task.status})`);
402      process.exit(1);
403    }
404  
405    // Create approval metadata
406    const approvalData = {
407      decision,
408      reviewer,
409      timestamp: new Date().toISOString(),
410      notes,
411      conditions: conditions ? conditions.split(',').map(c => c.trim()) : [],
412    };
413  
414    try {
415      // Update task with approval
416      await run(
417        `UPDATE tel.agent_tasks
418         SET
419           status = $1,
420           reviewed_by = $2,
421           approval_json = $3,
422           completed_at = NOW()
423         WHERE id = $4`,
424        [
425          decision === 'rejected' ? 'failed' : 'completed',
426          reviewer,
427          JSON.stringify(approvalData),
428          taskId,
429        ]
430      );
431  
432      console.log(`βœ… Task ${taskId} ${decision === 'rejected' ? 'rejected' : 'approved'}`);
433      console.log(`   Reviewer: ${reviewer}`);
434      console.log(`   Decision: ${decision}`);
435      if (notes) {
436        console.log(`   Notes: ${notes}`);
437      }
438  
439      // If approved and is design_proposal, create implementation_plan task
440      if (decision === 'approved' && task.task_type === 'design_proposal') {
441        const result = task.result_json ? JSON.parse(task.result_json) : {};
442        const designProposal = result.design_proposal;
443  
444        if (designProposal) {
445          const planTaskId = await createAgentTask({
446            task_type: 'implementation_plan',
447            assigned_to: 'developer',
448            created_by: 'cli',
449            priority: task.priority,
450            parent_task_id: taskId,
451            context: {
452              design_proposal: designProposal,
453            },
454          });
455  
456          console.log(`   Created implementation_plan task: #${planTaskId}`);
457        }
458      }
459  
460      // If approved and is implementation_plan via technical_review, update original task
461      if (decision === 'approved' && task.task_type === 'technical_review') {
462        const context = task.context_json ? JSON.parse(task.context_json) : {};
463        const originalTaskId = context.original_task_id;
464  
465        if (originalTaskId) {
466          await run(
467            `UPDATE tel.agent_tasks
468             SET status = 'pending'
469             WHERE id = $1`,
470            [originalTaskId]
471          );
472  
473          console.log(`   Updated original task #${originalTaskId} to pending`);
474        }
475      }
476    } catch (err) {
477      console.error(`❌ Error approving task: ${err.message}`);
478      process.exit(1);
479    }
480  }
481  
482  /**
483   * Show workflow status for a task tree
484   *
485   * @param {Object} options - Options
486   */
487  async function showWorkflowStatus(options) {
488    const { workflowId } = options;
489  
490    if (!workflowId) {
491      console.error('❌ Error: --workflow-id is required');
492      console.log('\nUsage: npm run agent:workflow:status -- --workflow-id 42');
493      process.exit(1);
494    }
495  
496    // Get root task and all descendants
497    const tasks = await getAll(
498      `WITH RECURSIVE task_tree AS (
499        SELECT id, task_type, assigned_to, status, priority, created_at, parent_task_id, 0 as depth
500        FROM tel.agent_tasks
501        WHERE id = $1
502        UNION ALL
503        SELECT t.id, t.task_type, t.assigned_to, t.status, t.priority, t.created_at, t.parent_task_id, tt.depth + 1
504        FROM tel.agent_tasks t
505        JOIN task_tree tt ON t.parent_task_id = tt.id
506      )
507      SELECT * FROM task_tree
508      ORDER BY depth, created_at`,
509      [workflowId]
510    );
511  
512    if (tasks.length === 0) {
513      console.error(`❌ Error: Workflow ${workflowId} not found`);
514      process.exit(1);
515    }
516  
517    console.log(`\nπŸ“‹ Workflow Status: Task #${workflowId}\n`);
518    console.log('ID    Type                  Agent       Status                        Created');
519    console.log('─'.repeat(95));
520  
521    for (const task of tasks) {
522      const indent = '  '.repeat(task.depth);
523      const statusIcon =
524        task.status === 'completed'
525          ? 'βœ…'
526          : task.status === 'running'
527            ? 'πŸ”„'
528            : task.status === 'awaiting_po_approval'
529              ? '⏳'
530              : task.status === 'awaiting_architect_approval'
531                ? '⏳'
532                : task.status === 'failed'
533                  ? '❌'
534                  : '⏸️ ';
535  
536      console.log(
537        `${indent}${task.id.toString().padEnd(5)} ${task.task_type.padEnd(20)} ${task.assigned_to.padEnd(11)} ${statusIcon} ${task.status.padEnd(28)} ${task.created_at}`
538      );
539    }
540  
541    console.log('');
542  }
543  
544  /**
545   * Show agent statistics
546   */
547  async function showStats() {
548    const stats = getAgentStats();
549  
550    console.log('\nπŸ“Š Agent Statistics (Last 24 Hours)\n');
551    console.log('Agent           Total  Completed  Failed  Blocked  Success Rate  Avg Time');
552    console.log('─'.repeat(85));
553  
554    for (const agent of stats.agents) {
555      const successRate = `${(agent.success_rate * 100).toFixed(1)}%`;
556      const avgTime = agent.avg_completion_time_minutes
557        ? `${agent.avg_completion_time_minutes.toFixed(1)}m`
558        : '-';
559  
560      console.log(
561        `${agent.agent.padEnd(15)} ${agent.total.toString().padEnd(6)} ${agent.completed.toString().padEnd(10)} ${agent.failed.toString().padEnd(7)} ${agent.blocked.toString().padEnd(8)} ${successRate.padEnd(13)} ${avgTime}`
562      );
563    }
564  
565    console.log('─'.repeat(85));
566    console.log(
567      `${'TOTAL'.padEnd(15)} ${stats.overall.total.toString().padEnd(6)} ${stats.overall.completed.toString().padEnd(10)} ${stats.overall.failed.toString().padEnd(7)} ${''.padEnd(8)} ${`${(stats.overall.success_rate * 100).toFixed(1)}%`.padEnd(13)}`
568    );
569  
570    console.log('');
571  
572    // Show circuit breaker status
573    const blocked = await getAll(
574      `SELECT agent_name, metrics_json
575       FROM tel.agent_state
576       WHERE status = 'blocked'`
577    );
578  
579    if (blocked.length > 0) {
580      console.log('⚠️  Circuit Breakers Triggered:\n');
581      for (const agent of blocked) {
582        const metrics = JSON.parse(agent.metrics_json);
583        console.log(
584          `   ${agent.agent_name}: Failure rate ${(metrics.failure_rate * 100).toFixed(1)}%`
585        );
586        console.log(`   Triggered at: ${metrics.circuit_breaker_triggered_at}`);
587      }
588      console.log('');
589    }
590  }
591  
592  /**
593   * Parse CLI arguments
594   */
595  function parseArgs() {
596    const args = process.argv.slice(2);
597    const command = args[0];
598    const options = {};
599  
600    for (let i = 1; i < args.length; i++) {
601      if (args[i].startsWith('--')) {
602        const key = args[i].substring(2);
603        const value = args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true;
604        options[key] = value;
605        if (value !== true) i++;
606      }
607    }
608  
609    return { command, options };
610  }
611  
612  /**
613   * Main CLI entry point
614   */
615  async function main() {
616    const { command, options } = parseArgs();
617  
618    switch (command) {
619      case 'list':
620        await listAgents();
621        break;
622  
623      case 'tasks':
624        await showTasks({
625          assignedTo: options['assigned-to'],
626          status: options.status,
627        });
628        break;
629  
630      case 'logs':
631        await showLogs({
632          taskId: options['task-id'] ? parseInt(options['task-id']) : null,
633          agentName: options['agent-name'],
634          level: options.level,
635        });
636        break;
637  
638      case 'create':
639        await createTask(options);
640        break;
641  
642      case 'workflow':
643        await triggerWorkflow(options);
644        break;
645  
646      case 'approvals':
647        await showApprovals({
648          status: options.status,
649        });
650        break;
651  
652      case 'approve':
653        await approveTask({
654          taskId: options['task-id'] ? parseInt(options['task-id']) : null,
655          reviewer: options.reviewer,
656          decision: options.decision,
657          notes: options.notes || '',
658          conditions: options.conditions || '',
659        });
660        break;
661  
662      case 'workflow-status':
663        await showWorkflowStatus({
664          workflowId: options['workflow-id'] ? parseInt(options['workflow-id']) : null,
665        });
666        break;
667  
668      case 'stats':
669        await showStats();
670        break;
671  
672      default:
673        console.log(`
674  Agent Manager CLI
675  
676  Commands:
677    list                    List agents and status
678    tasks                   Show pending tasks
679      --assigned-to <agent>   Filter by agent
680      --status <status>       Filter by status
681    logs                    View recent execution logs
682      --task-id <id>          Filter by task ID
683      --agent-name <agent>    Filter by agent
684      --level <level>         Filter by log level
685    create                  Create task manually
686      --agent <agent>         Agent to assign to (required)
687      --task <type>           Task type (required)
688      --context <json>        Context JSON (optional)
689      --priority <1-10>       Priority (optional, default: 5)
690    workflow                Trigger a workflow
691      --workflow <type>       bug-fix|feature|refactor (required)
692      For bug-fix:
693        --error <message>     Error message (required)
694        --stack <trace>       Stack trace (optional)
695        --stage <stage>       Pipeline stage (optional)
696      For feature:
697        --description <desc>  Feature description (required)
698        --requirements <json> Requirements array (optional)
699      For refactor:
700        --file <path>         File to refactor (required)
701        --reason <reason>     Reason for refactoring (optional)
702    approvals               Show tasks awaiting approval
703      --status <status>       Filter by approval status (optional)
704    approve                 Approve or reject a task
705      --task-id <id>          Task ID (required)
706      --reviewer <name>       Reviewer name (required)
707      --decision <decision>   approved|approved_with_conditions|rejected (required)
708      --notes <text>          Approval notes (optional)
709      --conditions <list>     Comma-separated conditions (optional)
710    workflow-status         Show workflow task tree
711      --workflow-id <id>      Root task ID (required)
712    stats                   Show agent statistics
713  
714  Examples:
715    npm run agent:list
716    npm run agent:tasks -- --assigned-to developer
717    npm run agent:logs -- --task-id 123
718    npm run agent:create -- --agent developer --task fix_bug --context '{"error":"..."}'
719    npm run agent:workflow -- --workflow bug-fix --error "..." --stage scoring
720    npm run agent:approvals
721    npm run agent:approve -- --task-id 42 --reviewer "Jason" --decision approved
722    npm run agent:approve -- --task-id 42 --reviewer "Jason" --decision rejected --notes "Too complex"
723    npm run agent:workflow:status -- --workflow-id 42
724    npm run agent:stats
725        `);
726        break;
727    }
728  }
729  
730  main().catch(err => {
731    console.error('❌ Fatal error:', err.message);
732    process.exit(1);
733  });