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 });