claude-code-workflow 6.3.37 → 6.3.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/.claude/commands/workflow/lite-execute.md +2 -0
  2. package/.codex/agents/action-planning-agent.md +885 -0
  3. package/.codex/agents/ccw-loop-b-complete.md +227 -0
  4. package/.codex/agents/ccw-loop-b-debug.md +172 -0
  5. package/.codex/agents/ccw-loop-b-develop.md +147 -0
  6. package/.codex/agents/ccw-loop-b-init.md +82 -0
  7. package/.codex/agents/ccw-loop-b-validate.md +204 -0
  8. package/.codex/agents/ccw-loop-executor.md +260 -0
  9. package/.codex/agents/cli-discuss-agent.md +391 -0
  10. package/.codex/agents/cli-execution-agent.md +333 -0
  11. package/.codex/agents/cli-explore-agent.md +186 -0
  12. package/.codex/agents/cli-lite-planning-agent.md +736 -0
  13. package/.codex/agents/cli-planning-agent.md +562 -0
  14. package/.codex/agents/code-developer.md +408 -0
  15. package/.codex/agents/conceptual-planning-agent.md +321 -0
  16. package/.codex/agents/context-search-agent.md +585 -0
  17. package/.codex/agents/debug-explore-agent.md +436 -0
  18. package/.codex/agents/doc-generator.md +334 -0
  19. package/.codex/agents/issue-plan-agent.md +417 -0
  20. package/.codex/agents/issue-queue-agent.md +311 -0
  21. package/.codex/agents/memory-bridge.md +96 -0
  22. package/.codex/agents/test-context-search-agent.md +402 -0
  23. package/.codex/agents/test-fix-agent.md +359 -0
  24. package/.codex/agents/ui-design-agent.md +595 -0
  25. package/.codex/agents/universal-executor.md +135 -0
  26. package/.codex/prompts/clean.md +409 -0
  27. package/.codex/prompts/issue-discover-by-prompt.md +364 -0
  28. package/.codex/prompts/issue-discover.md +261 -0
  29. package/.codex/prompts/issue-execute.md +10 -0
  30. package/.codex/prompts/issue-new.md +285 -0
  31. package/.codex/prompts/issue-plan.md +161 -63
  32. package/.codex/prompts/issue-queue.md +298 -288
  33. package/.codex/prompts/lite-execute.md +627 -133
  34. package/.codex/prompts/lite-fix.md +670 -0
  35. package/.codex/prompts/lite-plan-a.md +337 -0
  36. package/.codex/prompts/lite-plan-b.md +485 -0
  37. package/.codex/prompts/{lite-plan.md → lite-plan-c.md} +601 -469
  38. package/.codex/skills/ccw-loop/README.md +171 -0
  39. package/.codex/skills/ccw-loop/SKILL.md +349 -0
  40. package/.codex/skills/ccw-loop/phases/actions/action-complete.md +269 -0
  41. package/.codex/skills/ccw-loop/phases/actions/action-debug.md +286 -0
  42. package/.codex/skills/ccw-loop/phases/actions/action-develop.md +183 -0
  43. package/.codex/skills/ccw-loop/phases/actions/action-init.md +164 -0
  44. package/.codex/skills/ccw-loop/phases/actions/action-menu.md +205 -0
  45. package/.codex/skills/ccw-loop/phases/actions/action-validate.md +250 -0
  46. package/.codex/skills/ccw-loop/phases/orchestrator.md +416 -0
  47. package/.codex/skills/ccw-loop/phases/state-schema.md +388 -0
  48. package/.codex/skills/ccw-loop/specs/action-catalog.md +182 -0
  49. package/.codex/skills/ccw-loop-b/README.md +301 -0
  50. package/.codex/skills/ccw-loop-b/SKILL.md +322 -0
  51. package/.codex/skills/ccw-loop-b/phases/orchestrator.md +257 -0
  52. package/.codex/skills/ccw-loop-b/phases/state-schema.md +181 -0
  53. package/.codex/skills/ccw-loop-b/specs/action-catalog.md +383 -0
  54. package/.codex/skills/parallel-dev-cycle/README.md +382 -0
  55. package/.codex/skills/parallel-dev-cycle/SKILL.md +512 -0
  56. package/.codex/skills/parallel-dev-cycle/phases/agents/code-developer.md +242 -0
  57. package/.codex/skills/parallel-dev-cycle/phases/agents/exploration-planner.md +285 -0
  58. package/.codex/skills/parallel-dev-cycle/phases/agents/requirements-analyst.md +285 -0
  59. package/.codex/skills/parallel-dev-cycle/phases/agents/validation-archivist.md +381 -0
  60. package/.codex/skills/parallel-dev-cycle/phases/orchestrator.md +696 -0
  61. package/.codex/skills/parallel-dev-cycle/phases/state-schema.md +436 -0
  62. package/.codex/skills/parallel-dev-cycle/specs/communication-optimization.md +423 -0
  63. package/.codex/skills/parallel-dev-cycle/specs/coordination-protocol.md +391 -0
  64. package/.codex/skills/parallel-dev-cycle/specs/versioning-strategy.md +330 -0
  65. package/ccw/dist/cli.d.ts.map +1 -1
  66. package/ccw/dist/cli.js +4 -0
  67. package/ccw/dist/cli.js.map +1 -1
  68. package/ccw/dist/commands/install.d.ts.map +1 -1
  69. package/ccw/dist/commands/install.js +39 -8
  70. package/ccw/dist/commands/install.js.map +1 -1
  71. package/ccw/dist/commands/issue.d.ts +3 -0
  72. package/ccw/dist/commands/issue.d.ts.map +1 -1
  73. package/ccw/dist/commands/issue.js +107 -0
  74. package/ccw/dist/commands/issue.js.map +1 -1
  75. package/ccw/dist/commands/upgrade.js +1 -1
  76. package/ccw/dist/commands/upgrade.js.map +1 -1
  77. package/ccw/dist/config/litellm-api-config-manager.d.ts.map +1 -1
  78. package/ccw/dist/config/litellm-api-config-manager.js +3 -2
  79. package/ccw/dist/config/litellm-api-config-manager.js.map +1 -1
  80. package/ccw/dist/core/memory-embedder-bridge.d.ts.map +1 -1
  81. package/ccw/dist/core/memory-embedder-bridge.js +2 -5
  82. package/ccw/dist/core/memory-embedder-bridge.js.map +1 -1
  83. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  84. package/ccw/dist/core/routes/codexlens/config-handlers.d.ts.map +1 -1
  85. package/ccw/dist/core/routes/codexlens/config-handlers.js +7 -6
  86. package/ccw/dist/core/routes/codexlens/config-handlers.js.map +1 -1
  87. package/ccw/dist/core/routes/codexlens/semantic-handlers.d.ts.map +1 -1
  88. package/ccw/dist/core/routes/codexlens/semantic-handlers.js +2 -2
  89. package/ccw/dist/core/routes/codexlens/semantic-handlers.js.map +1 -1
  90. package/ccw/dist/core/routes/graph-routes.d.ts.map +1 -1
  91. package/ccw/dist/core/routes/graph-routes.js +17 -2
  92. package/ccw/dist/core/routes/graph-routes.js.map +1 -1
  93. package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -1
  94. package/ccw/dist/core/routes/issue-routes.js +280 -33
  95. package/ccw/dist/core/routes/issue-routes.js.map +1 -1
  96. package/ccw/dist/core/routes/loop-v2-routes.d.ts +9 -0
  97. package/ccw/dist/core/routes/loop-v2-routes.d.ts.map +1 -1
  98. package/ccw/dist/core/routes/loop-v2-routes.js +56 -4
  99. package/ccw/dist/core/routes/loop-v2-routes.js.map +1 -1
  100. package/ccw/dist/core/routes/system-routes.d.ts.map +1 -1
  101. package/ccw/dist/core/routes/system-routes.js +3 -2
  102. package/ccw/dist/core/routes/system-routes.js.map +1 -1
  103. package/ccw/dist/core/server.d.ts.map +1 -1
  104. package/ccw/dist/core/server.js +5 -3
  105. package/ccw/dist/core/server.js.map +1 -1
  106. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
  107. package/ccw/dist/tools/claude-cli-tools.js +4 -3
  108. package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
  109. package/ccw/dist/tools/cli-config-manager.d.ts +1 -0
  110. package/ccw/dist/tools/cli-config-manager.d.ts.map +1 -1
  111. package/ccw/dist/tools/cli-config-manager.js +2 -1
  112. package/ccw/dist/tools/cli-config-manager.js.map +1 -1
  113. package/ccw/dist/tools/codex-lens-lsp.d.ts.map +1 -1
  114. package/ccw/dist/tools/codex-lens-lsp.js +2 -5
  115. package/ccw/dist/tools/codex-lens-lsp.js.map +1 -1
  116. package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
  117. package/ccw/dist/tools/codex-lens.js +22 -32
  118. package/ccw/dist/tools/codex-lens.js.map +1 -1
  119. package/ccw/dist/tools/litellm-client.d.ts +6 -0
  120. package/ccw/dist/tools/litellm-client.d.ts.map +1 -1
  121. package/ccw/dist/tools/litellm-client.js +15 -2
  122. package/ccw/dist/tools/litellm-client.js.map +1 -1
  123. package/ccw/dist/tools/loop-task-manager.d.ts +13 -2
  124. package/ccw/dist/tools/loop-task-manager.d.ts.map +1 -1
  125. package/ccw/dist/tools/loop-task-manager.js.map +1 -1
  126. package/ccw/dist/tools/native-session-discovery.d.ts.map +1 -1
  127. package/ccw/dist/tools/native-session-discovery.js +35 -7
  128. package/ccw/dist/tools/native-session-discovery.js.map +1 -1
  129. package/ccw/dist/utils/codexlens-path.d.ts +36 -0
  130. package/ccw/dist/utils/codexlens-path.d.ts.map +1 -0
  131. package/ccw/dist/utils/codexlens-path.js +56 -0
  132. package/ccw/dist/utils/codexlens-path.js.map +1 -0
  133. package/ccw/dist/utils/uv-manager.d.ts.map +1 -1
  134. package/ccw/dist/utils/uv-manager.js +3 -2
  135. package/ccw/dist/utils/uv-manager.js.map +1 -1
  136. package/ccw/src/cli.ts +4 -0
  137. package/ccw/src/commands/install.ts +51 -8
  138. package/ccw/src/commands/issue.ts +119 -0
  139. package/ccw/src/commands/upgrade.ts +1 -1
  140. package/ccw/src/config/litellm-api-config-manager.ts +3 -2
  141. package/ccw/src/core/memory-embedder-bridge.ts +2 -6
  142. package/ccw/src/core/routes/cli-routes.ts +1 -1
  143. package/ccw/src/core/routes/codexlens/config-handlers.ts +7 -6
  144. package/ccw/src/core/routes/codexlens/semantic-handlers.ts +2 -2
  145. package/ccw/src/core/routes/graph-routes.ts +18 -2
  146. package/ccw/src/core/routes/issue-routes.ts +308 -33
  147. package/ccw/src/core/routes/loop-v2-routes.ts +64 -6
  148. package/ccw/src/core/routes/system-routes.ts +3 -2
  149. package/ccw/src/core/server.ts +6 -3
  150. package/ccw/src/templates/dashboard-css/02-session.css +2 -0
  151. package/ccw/src/templates/dashboard-css/04-lite-tasks.css +103 -1
  152. package/ccw/src/templates/dashboard-css/32-issue-manager.css +32 -0
  153. package/ccw/src/templates/dashboard-js/components/cli-history.js +48 -48
  154. package/ccw/src/templates/dashboard-js/components/navigation.js +6 -0
  155. package/ccw/src/templates/dashboard-js/components/notifications.js +6 -0
  156. package/ccw/src/templates/dashboard-js/components/version-check.js +38 -0
  157. package/ccw/src/templates/dashboard-js/i18n.js +126 -0
  158. package/ccw/src/templates/dashboard-js/state.js +2 -0
  159. package/ccw/src/templates/dashboard-js/views/cli-manager.js +1 -1
  160. package/ccw/src/templates/dashboard-js/views/issue-manager.js +183 -1
  161. package/ccw/src/templates/dashboard-js/views/lite-tasks.js +55 -11
  162. package/ccw/src/templates/dashboard-js/views/loop-monitor.js +112 -11
  163. package/ccw/src/templates/dashboard.html +48 -2
  164. package/ccw/src/tools/claude-cli-tools.ts +4 -3
  165. package/ccw/src/tools/cli-config-manager.ts +3 -1
  166. package/ccw/src/tools/codex-lens-lsp.ts +2 -5
  167. package/ccw/src/tools/codex-lens.ts +27 -38
  168. package/ccw/src/tools/litellm-client.ts +16 -2
  169. package/ccw/src/tools/loop-task-manager.ts +13 -2
  170. package/ccw/src/tools/native-session-discovery.ts +38 -7
  171. package/ccw/src/utils/codexlens-path.ts +60 -0
  172. package/ccw/src/utils/uv-manager.ts +3 -2
  173. package/package.json +1 -1
@@ -212,20 +212,22 @@ function getIssueDetail(issuesDir: string, issueId: string) {
212
212
  function enrichIssues(issues: any[], issuesDir: string) {
213
213
  return issues.map(issue => {
214
214
  const solutions = readSolutionsJsonl(issuesDir, issue.id);
215
- let taskCount = 0;
215
+ let tasks: any[] = [];
216
216
 
217
- // Get task count from bound solution
217
+ // Get tasks from bound solution
218
218
  if (issue.bound_solution_id) {
219
219
  const boundSol = solutions.find(s => s.id === issue.bound_solution_id);
220
220
  if (boundSol?.tasks) {
221
- taskCount = boundSol.tasks.length;
221
+ tasks = boundSol.tasks;
222
222
  }
223
223
  }
224
224
 
225
225
  return {
226
226
  ...issue,
227
+ solutions, // Add full solutions array
228
+ tasks, // Add full tasks array
227
229
  solution_count: solutions.length,
228
- task_count: taskCount
230
+ task_count: tasks.length
229
231
  };
230
232
  });
231
233
  }
@@ -337,41 +339,58 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
337
339
 
338
340
  const issuesDir = join(projectPath, '.workflow', 'issues');
339
341
 
340
- // ===== Queue Routes (top-level /api/queue) =====
342
+ // ===== Helper: Normalize queue path (supports both /api/queue/* and /api/issues/queue/*) =====
343
+ const normalizeQueuePath = (path: string): string | null => {
344
+ if (path.startsWith('/api/issues/queue')) {
345
+ return path.replace('/api/issues/queue', '/api/queue');
346
+ }
347
+ if (path.startsWith('/api/queue')) {
348
+ return path;
349
+ }
350
+ return null;
351
+ };
341
352
 
342
- // GET /api/queue - Get execution queue
343
- if (pathname === '/api/queue' && req.method === 'GET') {
353
+ const normalizedPath = normalizeQueuePath(pathname);
354
+
355
+ // ===== Queue Routes (supports both /api/queue/* and /api/issues/queue/*) =====
356
+
357
+ // GET /api/queue or /api/issues/queue - Get execution queue
358
+ if ((normalizedPath === '/api/queue') && req.method === 'GET') {
344
359
  const queue = groupQueueByExecutionGroup(readQueue(issuesDir));
345
360
  res.writeHead(200, { 'Content-Type': 'application/json' });
346
361
  res.end(JSON.stringify(queue));
347
362
  return true;
348
363
  }
349
364
 
350
- // GET /api/queue/history - Get queue history (all queues from index)
351
- if (pathname === '/api/queue/history' && req.method === 'GET') {
365
+ // GET /api/queue/history or /api/issues/queue/history - Get queue history (all queues from index)
366
+ if (normalizedPath === '/api/queue/history' && req.method === 'GET') {
352
367
  const queuesDir = join(issuesDir, 'queues');
353
368
  const indexPath = join(queuesDir, 'index.json');
354
369
 
355
370
  if (!existsSync(indexPath)) {
356
371
  res.writeHead(200, { 'Content-Type': 'application/json' });
357
- res.end(JSON.stringify({ queues: [], active_queue_id: null }));
372
+ res.end(JSON.stringify({ queues: [], active_queue_id: null, active_queue_ids: [] }));
358
373
  return true;
359
374
  }
360
375
 
361
376
  try {
362
377
  const index = JSON.parse(readFileSync(indexPath, 'utf8'));
378
+ // Ensure active_queue_ids is always returned for multi-queue support
379
+ if (!index.active_queue_ids) {
380
+ index.active_queue_ids = index.active_queue_id ? [index.active_queue_id] : [];
381
+ }
363
382
  res.writeHead(200, { 'Content-Type': 'application/json' });
364
383
  res.end(JSON.stringify(index));
365
384
  } catch {
366
385
  res.writeHead(200, { 'Content-Type': 'application/json' });
367
- res.end(JSON.stringify({ queues: [], active_queue_id: null }));
386
+ res.end(JSON.stringify({ queues: [], active_queue_id: null, active_queue_ids: [] }));
368
387
  }
369
388
  return true;
370
389
  }
371
390
 
372
- // GET /api/queue/:id - Get specific queue by ID
373
- const queueDetailMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
374
- const reservedQueuePaths = ['history', 'reorder', 'switch', 'deactivate', 'merge'];
391
+ // GET /api/queue/:id or /api/issues/queue/:id - Get specific queue by ID
392
+ const queueDetailMatch = normalizedPath?.match(/^\/api\/queue\/([^/]+)$/);
393
+ const reservedQueuePaths = ['history', 'reorder', 'switch', 'deactivate', 'merge', 'activate'];
375
394
  if (queueDetailMatch && req.method === 'GET' && !reservedQueuePaths.includes(queueDetailMatch[1])) {
376
395
  const queueId = queueDetailMatch[1];
377
396
  const queuesDir = join(issuesDir, 'queues');
@@ -394,8 +413,55 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
394
413
  return true;
395
414
  }
396
415
 
397
- // POST /api/queue/switch - Switch active queue
398
- if (pathname === '/api/queue/switch' && req.method === 'POST') {
416
+ // POST /api/queue/activate or /api/issues/queue/activate - Activate one or more queues (multi-queue support)
417
+ if (normalizedPath === '/api/queue/activate' && req.method === 'POST') {
418
+ handlePostRequest(req, res, async (body: any) => {
419
+ const { queueId, queueIds } = body;
420
+
421
+ // Support both single queueId and array queueIds
422
+ const idsToActivate: string[] = queueIds
423
+ ? (Array.isArray(queueIds) ? queueIds : [queueIds])
424
+ : (queueId ? [queueId] : []);
425
+
426
+ if (idsToActivate.length === 0) {
427
+ return { error: 'queueId or queueIds required' };
428
+ }
429
+
430
+ const queuesDir = join(issuesDir, 'queues');
431
+ const indexPath = join(queuesDir, 'index.json');
432
+
433
+ // Validate all queue IDs exist
434
+ for (const id of idsToActivate) {
435
+ const queueFilePath = join(queuesDir, `${id}.json`);
436
+ if (!existsSync(queueFilePath)) {
437
+ return { error: `Queue ${id} not found` };
438
+ }
439
+ }
440
+
441
+ try {
442
+ const index = existsSync(indexPath)
443
+ ? JSON.parse(readFileSync(indexPath, 'utf8'))
444
+ : { active_queue_id: null, active_queue_ids: [], queues: [] };
445
+
446
+ index.active_queue_ids = idsToActivate;
447
+ index.active_queue_id = idsToActivate[0] || null; // Backward compat
448
+
449
+ writeFileSync(indexPath, JSON.stringify(index, null, 2));
450
+
451
+ return {
452
+ success: true,
453
+ active_queue_ids: idsToActivate,
454
+ active_queue_id: idsToActivate[0] || null // Backward compat
455
+ };
456
+ } catch (err) {
457
+ return { error: 'Failed to activate queue(s)' };
458
+ }
459
+ });
460
+ return true;
461
+ }
462
+
463
+ // POST /api/queue/switch or /api/issues/queue/switch - Switch active queue (legacy, single queue)
464
+ if (normalizedPath === '/api/queue/switch' && req.method === 'POST') {
399
465
  handlePostRequest(req, res, async (body: any) => {
400
466
  const { queueId } = body;
401
467
  if (!queueId) return { error: 'queueId required' };
@@ -411,12 +477,18 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
411
477
  try {
412
478
  const index = existsSync(indexPath)
413
479
  ? JSON.parse(readFileSync(indexPath, 'utf8'))
414
- : { active_queue_id: null, queues: [] };
480
+ : { active_queue_id: null, active_queue_ids: [], queues: [] };
415
481
 
416
482
  index.active_queue_id = queueId;
483
+ index.active_queue_ids = [queueId]; // Also update multi-queue array
484
+
417
485
  writeFileSync(indexPath, JSON.stringify(index, null, 2));
418
486
 
419
- return { success: true, active_queue_id: queueId };
487
+ return {
488
+ success: true,
489
+ active_queue_id: queueId,
490
+ active_queue_ids: [queueId]
491
+ };
420
492
  } catch (err) {
421
493
  return { error: 'Failed to switch queue' };
422
494
  }
@@ -424,22 +496,43 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
424
496
  return true;
425
497
  }
426
498
 
427
- // POST /api/queue/deactivate - Deactivate current queue (set active to null)
428
- if (pathname === '/api/queue/deactivate' && req.method === 'POST') {
499
+ // POST /api/queue/deactivate or /api/issues/queue/deactivate - Deactivate queue(s)
500
+ if (normalizedPath === '/api/queue/deactivate' && req.method === 'POST') {
429
501
  handlePostRequest(req, res, async (body: any) => {
502
+ const { queueId } = body; // Optional: specific queue to deactivate
430
503
  const queuesDir = join(issuesDir, 'queues');
431
504
  const indexPath = join(queuesDir, 'index.json');
432
505
 
433
506
  try {
434
507
  const index = existsSync(indexPath)
435
508
  ? JSON.parse(readFileSync(indexPath, 'utf8'))
436
- : { active_queue_id: null, queues: [] };
509
+ : { active_queue_id: null, active_queue_ids: [], queues: [] };
510
+
511
+ const currentActiveIds = index.active_queue_ids || (index.active_queue_id ? [index.active_queue_id] : []);
512
+ let deactivatedIds: string[] = [];
513
+ let remainingIds: string[] = [];
514
+
515
+ if (queueId) {
516
+ // Deactivate specific queue
517
+ deactivatedIds = currentActiveIds.includes(queueId) ? [queueId] : [];
518
+ remainingIds = currentActiveIds.filter((id: string) => id !== queueId);
519
+ } else {
520
+ // Deactivate all
521
+ deactivatedIds = [...currentActiveIds];
522
+ remainingIds = [];
523
+ }
524
+
525
+ index.active_queue_ids = remainingIds;
526
+ index.active_queue_id = remainingIds[0] || null; // Backward compat
437
527
 
438
- const previousActiveId = index.active_queue_id;
439
- index.active_queue_id = null;
440
528
  writeFileSync(indexPath, JSON.stringify(index, null, 2));
441
529
 
442
- return { success: true, previous_active_id: previousActiveId };
530
+ return {
531
+ success: true,
532
+ deactivated_queue_ids: deactivatedIds,
533
+ active_queue_ids: remainingIds,
534
+ active_queue_id: remainingIds[0] || null // Backward compat
535
+ };
443
536
  } catch (err) {
444
537
  return { error: 'Failed to deactivate queue' };
445
538
  }
@@ -447,8 +540,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
447
540
  return true;
448
541
  }
449
542
 
450
- // POST /api/queue/reorder - Reorder queue items (supports both solutions and tasks)
451
- if (pathname === '/api/queue/reorder' && req.method === 'POST') {
543
+ // POST /api/queue/reorder or /api/issues/queue/reorder - Reorder queue items (supports both solutions and tasks)
544
+ if (normalizedPath === '/api/queue/reorder' && req.method === 'POST') {
452
545
  handlePostRequest(req, res, async (body: any) => {
453
546
  const { groupId, newOrder } = body;
454
547
  if (!groupId || !Array.isArray(newOrder)) {
@@ -499,8 +592,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
499
592
  return true;
500
593
  }
501
594
 
502
- // DELETE /api/queue/:queueId/item/:itemId - Delete item from queue
503
- const queueItemDeleteMatch = pathname.match(/^\/api\/queue\/([^/]+)\/item\/([^/]+)$/);
595
+ // DELETE /api/queue/:queueId/item/:itemId or /api/issues/queue/:queueId/item/:itemId
596
+ const queueItemDeleteMatch = normalizedPath?.match(/^\/api\/queue\/([^/]+)\/item\/([^/]+)$/);
504
597
  if (queueItemDeleteMatch && req.method === 'DELETE') {
505
598
  const queueId = queueItemDeleteMatch[1];
506
599
  const itemId = decodeURIComponent(queueItemDeleteMatch[2]);
@@ -574,8 +667,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
574
667
  return true;
575
668
  }
576
669
 
577
- // DELETE /api/queue/:queueId - Delete entire queue
578
- const queueDeleteMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
670
+ // DELETE /api/queue/:queueId or /api/issues/queue/:queueId - Delete entire queue
671
+ const queueDeleteMatch = normalizedPath?.match(/^\/api\/queue\/([^/]+)$/);
579
672
  if (queueDeleteMatch && req.method === 'DELETE') {
580
673
  const queueId = queueDeleteMatch[1];
581
674
  const queuesDir = join(issuesDir, 'queues');
@@ -616,8 +709,8 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
616
709
  return true;
617
710
  }
618
711
 
619
- // POST /api/queue/merge - Merge source queue into target queue
620
- if (pathname === '/api/queue/merge' && req.method === 'POST') {
712
+ // POST /api/queue/merge or /api/issues/queue/merge - Merge source queue into target queue
713
+ if (normalizedPath === '/api/queue/merge' && req.method === 'POST') {
621
714
  handlePostRequest(req, res, async (body: any) => {
622
715
  const { sourceQueueId, targetQueueId } = body;
623
716
  if (!sourceQueueId || !targetQueueId) {
@@ -755,7 +848,7 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
755
848
  }
756
849
 
757
850
  // POST /api/queue/split - Split items from source queue into a new queue
758
- if (pathname === '/api/queue/split' && req.method === 'POST') {
851
+ if (normalizedPath === '/api/queue/split' && req.method === 'POST') {
759
852
  handlePostRequest(req, res, async (body: any) => {
760
853
  const { sourceQueueId, itemIds } = body;
761
854
  if (!sourceQueueId || !itemIds || !Array.isArray(itemIds) || itemIds.length === 0) {
@@ -992,6 +1085,188 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
992
1085
  return true;
993
1086
  }
994
1087
 
1088
+ // POST /api/issues/pull - Pull issues from GitHub
1089
+ if (pathname === '/api/issues/pull' && req.method === 'POST') {
1090
+ const state = url.searchParams.get('state') || 'open';
1091
+ const limit = parseInt(url.searchParams.get('limit') || '100');
1092
+ const labels = url.searchParams.get('labels') || '';
1093
+ const downloadImages = url.searchParams.get('downloadImages') === 'true';
1094
+
1095
+ try {
1096
+ const { execSync } = await import('child_process');
1097
+ const https = await import('https');
1098
+ const http = await import('http');
1099
+
1100
+ // Check if gh CLI is available
1101
+ try {
1102
+ execSync('gh --version', { stdio: 'ignore', timeout: 5000 });
1103
+ } catch {
1104
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1105
+ res.end(JSON.stringify({ error: 'GitHub CLI (gh) is not installed or not in PATH' }));
1106
+ return true;
1107
+ }
1108
+
1109
+ // Build gh command
1110
+ let ghCommand = `gh issue list --state ${state} --limit ${limit} --json number,title,body,labels,url,state`;
1111
+ if (labels) ghCommand += ` --label "${labels}"`;
1112
+
1113
+ // Execute gh command from project root
1114
+ const ghOutput = execSync(ghCommand, {
1115
+ encoding: 'utf-8',
1116
+ stdio: ['pipe', 'pipe', 'pipe'],
1117
+ timeout: 60000,
1118
+ cwd: issuesDir.replace(/[\\/]\.workflow[\\/]issues$/, '')
1119
+ }).trim();
1120
+
1121
+ if (!ghOutput) {
1122
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1123
+ res.end(JSON.stringify({ imported: 0, updated: 0, skipped: 0, images_downloaded: 0 }));
1124
+ return true;
1125
+ }
1126
+
1127
+ const ghIssues = JSON.parse(ghOutput);
1128
+ const existingIssues = readIssuesJsonl(issuesDir);
1129
+
1130
+ let imported = 0;
1131
+ let skipped = 0;
1132
+ let updated = 0;
1133
+ let imagesDownloaded = 0;
1134
+
1135
+ // Create images directory if needed
1136
+ const imagesDir = join(issuesDir, 'images');
1137
+ if (downloadImages && !existsSync(imagesDir)) {
1138
+ mkdirSync(imagesDir, { recursive: true });
1139
+ }
1140
+
1141
+ // Helper function to download image
1142
+ const downloadImage = async (imageUrl: string, issueNumber: number, imageIndex: number): Promise<string | null> => {
1143
+ return new Promise((resolveDownload) => {
1144
+ try {
1145
+ const ext = imageUrl.match(/\.(png|jpg|jpeg|gif|webp|svg)/i)?.[1] || 'png';
1146
+ const filename = `GH-${issueNumber}-${imageIndex}.${ext}`;
1147
+ const filePath = join(imagesDir, filename);
1148
+
1149
+ // Skip if already downloaded
1150
+ if (existsSync(filePath)) {
1151
+ resolveDownload(`.workflow/issues/images/${filename}`);
1152
+ return;
1153
+ }
1154
+
1155
+ const protocol = imageUrl.startsWith('https') ? https : http;
1156
+ protocol.get(imageUrl, { timeout: 30000 }, (response: any) => {
1157
+ // Handle redirect
1158
+ if (response.statusCode === 301 || response.statusCode === 302) {
1159
+ const redirectUrl = response.headers.location;
1160
+ if (redirectUrl) {
1161
+ downloadImage(redirectUrl, issueNumber, imageIndex).then(resolveDownload);
1162
+ return;
1163
+ }
1164
+ }
1165
+ if (response.statusCode !== 200) {
1166
+ resolveDownload(null);
1167
+ return;
1168
+ }
1169
+
1170
+ const chunks: Buffer[] = [];
1171
+ response.on('data', (chunk: Buffer) => chunks.push(chunk));
1172
+ response.on('end', () => {
1173
+ try {
1174
+ writeFileSync(filePath, Buffer.concat(chunks));
1175
+ resolveDownload(`.workflow/issues/images/${filename}`);
1176
+ } catch {
1177
+ resolveDownload(null);
1178
+ }
1179
+ });
1180
+ response.on('error', () => resolveDownload(null));
1181
+ }).on('error', () => resolveDownload(null));
1182
+ } catch {
1183
+ resolveDownload(null);
1184
+ }
1185
+ });
1186
+ };
1187
+
1188
+ // Process issues
1189
+ for (const ghIssue of ghIssues) {
1190
+ const issueId = `GH-${ghIssue.number}`;
1191
+ const existingIssue = existingIssues.find((i: any) => i.id === issueId);
1192
+
1193
+ let context = ghIssue.body || ghIssue.title;
1194
+
1195
+ // Extract and download images if enabled
1196
+ if (downloadImages && ghIssue.body) {
1197
+ // Find all image URLs in the body
1198
+ const imgPattern = /!\[[^\]]*\]\((https?:\/\/[^)]+)\)|<img[^>]+src=["'](https?:\/\/[^"']+)["']/gi;
1199
+ const imageUrls: string[] = [];
1200
+ let match;
1201
+ while ((match = imgPattern.exec(ghIssue.body)) !== null) {
1202
+ imageUrls.push(match[1] || match[2]);
1203
+ }
1204
+
1205
+ // Download images and build reference list
1206
+ if (imageUrls.length > 0) {
1207
+ const downloadedImages: string[] = [];
1208
+ for (let i = 0; i < imageUrls.length; i++) {
1209
+ const localPath = await downloadImage(imageUrls[i], ghIssue.number, i + 1);
1210
+ if (localPath) {
1211
+ downloadedImages.push(localPath);
1212
+ imagesDownloaded++;
1213
+ }
1214
+ }
1215
+
1216
+ // Append image references to context
1217
+ if (downloadedImages.length > 0) {
1218
+ context += '\n\n---\n**Downloaded Images:**\n';
1219
+ downloadedImages.forEach((path, idx) => {
1220
+ context += `- Image ${idx + 1}: \`${path}\`\n`;
1221
+ });
1222
+ }
1223
+ }
1224
+ }
1225
+
1226
+ // Prepare issue data (truncate context to 2000 chars max)
1227
+ const issueData = {
1228
+ id: issueId,
1229
+ title: ghIssue.title,
1230
+ status: ghIssue.state === 'OPEN' ? 'registered' : 'completed',
1231
+ priority: 3,
1232
+ context: context.substring(0, 2000),
1233
+ source: 'github',
1234
+ source_url: ghIssue.url,
1235
+ tags: ghIssue.labels?.map((l: any) => l.name) || [],
1236
+ created_at: new Date().toISOString(),
1237
+ updated_at: new Date().toISOString()
1238
+ };
1239
+
1240
+ if (existingIssue) {
1241
+ // Update if changed
1242
+ const newStatus = ghIssue.state === 'OPEN' ? 'registered' : 'completed';
1243
+ if (existingIssue.status !== newStatus || existingIssue.title !== ghIssue.title) {
1244
+ existingIssue.title = ghIssue.title;
1245
+ existingIssue.status = newStatus;
1246
+ existingIssue.context = issueData.context;
1247
+ existingIssue.updated_at = new Date().toISOString();
1248
+ updated++;
1249
+ } else {
1250
+ skipped++;
1251
+ }
1252
+ } else {
1253
+ existingIssues.push(issueData);
1254
+ imported++;
1255
+ }
1256
+ }
1257
+
1258
+ // Save all issues
1259
+ writeIssuesJsonl(issuesDir, existingIssues);
1260
+
1261
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1262
+ res.end(JSON.stringify({ imported, updated, skipped, images_downloaded: imagesDownloaded, total: ghIssues.length }));
1263
+ } catch (err: any) {
1264
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1265
+ res.end(JSON.stringify({ error: err.message || 'Failed to pull issues from GitHub' }));
1266
+ }
1267
+ return true;
1268
+ }
1269
+
995
1270
  // GET /api/issues/:id - Get issue detail
996
1271
  const detailMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
997
1272
  if (detailMatch && req.method === 'GET') {
@@ -29,11 +29,45 @@
29
29
 
30
30
  import { join } from 'path';
31
31
  import { randomBytes } from 'crypto';
32
+ import * as os from 'os';
32
33
  import type { RouteContext } from './types.js';
33
34
  import { LoopStatus } from '../../types/loop.js';
34
35
  import type { LoopState } from '../../types/loop.js';
35
36
  import { TaskStorageManager, type TaskCreateRequest, type TaskUpdateRequest, type TaskReorderRequest } from '../../tools/loop-task-manager.js';
36
37
  import { executeCliTool } from '../../tools/cli-executor.js';
38
+ import { loadClaudeCliTools } from '../../tools/claude-cli-tools.js';
39
+
40
+ /**
41
+ * Module-level cache for CLI tools configuration
42
+ * Loaded once at server startup to avoid repeated file I/O
43
+ */
44
+ let cachedEnabledTools: string[] | null = null;
45
+
46
+ /**
47
+ * Initialize CLI tools cache at server startup
48
+ * Should be called once when the server starts
49
+ */
50
+ export function initializeCliToolsCache(): void {
51
+ try {
52
+ const cliToolsConfig = loadClaudeCliTools(os.homedir());
53
+ const enabledTools = Object.entries(cliToolsConfig.tools || {})
54
+ .filter(([_, config]) => config.enabled === true)
55
+ .map(([name]) => name);
56
+ cachedEnabledTools = ['bash', ...enabledTools];
57
+ console.log('[Loop V2] CLI tools cache initialized:', cachedEnabledTools);
58
+ } catch (err) {
59
+ console.error('[Loop V2] Failed to initialize CLI tools cache:', err);
60
+ // Fallback to basic tools if config loading fails
61
+ cachedEnabledTools = ['bash', 'gemini', 'qwen', 'codex', 'claude'];
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Clear CLI tools cache (for testing or config reload)
67
+ */
68
+ export function clearCliToolsCache(): void {
69
+ cachedEnabledTools = null;
70
+ }
37
71
 
38
72
  /**
39
73
  * V2 Loop Create Request
@@ -710,9 +744,17 @@ export async function handleLoopV2Routes(ctx: RouteContext): Promise<boolean> {
710
744
  return { success: false, error: 'tool is required', status: 400 };
711
745
  }
712
746
 
713
- const validTools = ['bash', 'gemini', 'codex', 'qwen', 'claude'];
747
+ // Get enabled tools from cli-tools.json dynamically
748
+ const cliToolsConfig = loadClaudeCliTools(os.homedir());
749
+ const enabledTools = Object.entries(cliToolsConfig.tools || {})
750
+ .filter(([_, config]) => config.enabled === true)
751
+ .map(([name]) => name);
752
+
753
+ // Also allow 'bash' as a special case (built-in tool)
754
+ const validTools = ['bash', ...enabledTools];
755
+
714
756
  if (!validTools.includes(tool)) {
715
- return { success: false, error: `tool must be one of: ${validTools.join(', ')}`, status: 400 };
757
+ return { success: false, error: `tool must be one of enabled tools: ${validTools.join(', ')}`, status: 400 };
716
758
  }
717
759
 
718
760
  if (!mode || typeof mode !== 'string') {
@@ -1303,12 +1345,28 @@ function isValidId(id: string): boolean {
1303
1345
  return true;
1304
1346
  }
1305
1347
 
1348
+ /**
1349
+ * Get enabled tools list from cache
1350
+ * If cache is not initialized, it will load from config (fallback for lazy initialization)
1351
+ */
1352
+ function getEnabledToolsList(): string[] {
1353
+ // Return cached value if available
1354
+ if (cachedEnabledTools) {
1355
+ return cachedEnabledTools;
1356
+ }
1357
+
1358
+ // Fallback: lazy initialization if cache not initialized (shouldn't happen in normal operation)
1359
+ console.warn('[Loop V2] CLI tools cache not initialized, performing lazy load');
1360
+ initializeCliToolsCache();
1361
+ return cachedEnabledTools || ['bash', 'gemini', 'qwen', 'codex', 'claude'];
1362
+ }
1363
+
1306
1364
  /**
1307
1365
  * Map issue tool to loop tool
1308
1366
  */
1309
- function mapIssueToolToLoopTool(tool: any): 'bash' | 'gemini' | 'codex' | 'qwen' | 'claude' | null {
1310
- const validTools = ['bash', 'gemini', 'codex', 'qwen', 'claude'];
1311
- if (validTools.includes(tool)) return tool as any;
1367
+ function mapIssueToolToLoopTool(tool: any): string | null {
1368
+ const validTools = getEnabledToolsList();
1369
+ if (validTools.includes(tool)) return tool;
1312
1370
  // Map aliases
1313
1371
  if (tool === 'ccw') return 'gemini';
1314
1372
  if (tool === 'ai') return 'gemini';
@@ -1343,7 +1401,7 @@ function mapIssueOnError(onError: any): 'continue' | 'pause' | 'fail_fast' | und
1343
1401
  * Validate tool value
1344
1402
  */
1345
1403
  function validateTool(tool: any): boolean {
1346
- const validTools = ['bash', 'gemini', 'codex', 'qwen', 'claude'];
1404
+ const validTools = getEnabledToolsList();
1347
1405
  return validTools.includes(tool);
1348
1406
  }
1349
1407
 
@@ -145,7 +145,7 @@ async function getWorkflowData(projectPath: string): Promise<any> {
145
145
  generatedAt: new Date().toISOString(),
146
146
  activeSessions: [],
147
147
  archivedSessions: [],
148
- liteTasks: { litePlan: [], liteFix: [] },
148
+ liteTasks: { litePlan: [], liteFix: [], multiCliPlan: [] },
149
149
  reviewData: { dimensions: {} },
150
150
  projectOverview: null,
151
151
  statistics: {
@@ -155,7 +155,8 @@ async function getWorkflowData(projectPath: string): Promise<any> {
155
155
  completedTasks: 0,
156
156
  reviewFindings: 0,
157
157
  litePlanCount: 0,
158
- liteFixCount: 0
158
+ liteFixCount: 0,
159
+ multiCliPlanCount: 0
159
160
  },
160
161
  projectPath: normalizePathForDisplay(resolvedPath),
161
162
  recentPaths: getRecentPaths()
@@ -29,7 +29,7 @@ import { handleLiteLLMApiRoutes } from './routes/litellm-api-routes.js';
29
29
  import { handleNavStatusRoutes } from './routes/nav-status-routes.js';
30
30
  import { handleAuthRoutes } from './routes/auth-routes.js';
31
31
  import { handleLoopRoutes } from './routes/loop-routes.js';
32
- import { handleLoopV2Routes } from './routes/loop-v2-routes.js';
32
+ import { handleLoopV2Routes, initializeCliToolsCache } from './routes/loop-v2-routes.js';
33
33
  import { handleTestLoopRoutes } from './routes/test-loop-routes.js';
34
34
  import { handleTaskRoutes } from './routes/task-routes.js';
35
35
 
@@ -383,10 +383,10 @@ function generateServerDashboard(initialPath: string): string {
383
383
  generatedAt: new Date().toISOString(),
384
384
  activeSessions: [],
385
385
  archivedSessions: [],
386
- liteTasks: { litePlan: [], liteFix: [] },
386
+ liteTasks: { litePlan: [], liteFix: [], multiCliPlan: [] },
387
387
  reviewData: { dimensions: {} },
388
388
  projectOverview: null,
389
- statistics: { totalSessions: 0, activeSessions: 0, totalTasks: 0, completedTasks: 0, reviewFindings: 0, litePlanCount: 0, liteFixCount: 0 }
389
+ statistics: { totalSessions: 0, activeSessions: 0, totalTasks: 0, completedTasks: 0, reviewFindings: 0, litePlanCount: 0, liteFixCount: 0, multiCliPlanCount: 0 }
390
390
  };
391
391
 
392
392
  // Replace JS placeholders
@@ -723,6 +723,9 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
723
723
  console.log(`WebSocket endpoint available at ws://${host}:${serverPort}/ws`);
724
724
  console.log(`Hook endpoint available at POST http://${host}:${serverPort}/api/hook`);
725
725
 
726
+ // Initialize CLI tools cache for Loop V2 routes
727
+ initializeCliToolsCache();
728
+
726
729
  // Start periodic cleanup of stale CLI executions (every 2 minutes)
727
730
  const CLEANUP_INTERVAL_MS = 2 * 60 * 1000;
728
731
  const cleanupInterval = setInterval(cleanupStaleExecutions, CLEANUP_INTERVAL_MS);
@@ -464,6 +464,7 @@
464
464
  display: flex;
465
465
  flex-direction: column;
466
466
  gap: 1rem;
467
+ align-items: flex-start;
467
468
  }
468
469
 
469
470
  .btn-back {
@@ -492,6 +493,7 @@
492
493
  .detail-title-row {
493
494
  display: flex;
494
495
  align-items: center;
496
+ justify-content: flex-start;
495
497
  gap: 1rem;
496
498
  flex-wrap: wrap;
497
499
  }