claude-code-workflow 6.3.7 → 6.3.9

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 (30) hide show
  1. package/.claude/agents/issue-plan-agent.md +166 -755
  2. package/.claude/agents/issue-queue-agent.md +143 -618
  3. package/.claude/commands/issue/execute.md +55 -26
  4. package/.claude/commands/issue/manage.md +37 -789
  5. package/.claude/commands/issue/new.md +9 -42
  6. package/.claude/commands/issue/plan.md +132 -288
  7. package/.claude/commands/issue/queue.md +99 -159
  8. package/.claude/skills/issue-manage/SKILL.md +244 -0
  9. package/.claude/workflows/cli-templates/schemas/queue-schema.json +3 -3
  10. package/.claude/workflows/cli-templates/schemas/solution-schema.json +70 -3
  11. package/.codex/prompts/issue-execute.md +11 -11
  12. package/ccw/dist/cli.d.ts.map +1 -1
  13. package/ccw/dist/cli.js +1 -0
  14. package/ccw/dist/cli.js.map +1 -1
  15. package/ccw/dist/commands/issue.d.ts +1 -0
  16. package/ccw/dist/commands/issue.d.ts.map +1 -1
  17. package/ccw/dist/commands/issue.js +86 -70
  18. package/ccw/dist/commands/issue.js.map +1 -1
  19. package/ccw/dist/core/routes/issue-routes.d.ts +3 -1
  20. package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -1
  21. package/ccw/dist/core/routes/issue-routes.js +98 -18
  22. package/ccw/dist/core/routes/issue-routes.js.map +1 -1
  23. package/ccw/src/cli.ts +1 -0
  24. package/ccw/src/commands/issue.ts +130 -78
  25. package/ccw/src/core/routes/issue-routes.ts +110 -18
  26. package/ccw/src/templates/dashboard-css/32-issue-manager.css +310 -1
  27. package/ccw/src/templates/dashboard-js/views/issue-manager.js +266 -14
  28. package/package.json +1 -1
  29. package/.claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json +0 -136
  30. package/.claude/workflows/cli-templates/schemas/solutions-jsonl-schema.json +0 -125
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import chalk from 'chalk';
8
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
9
9
  import { join, resolve } from 'path';
10
10
 
11
11
  // Handle EPIPE errors gracefully
@@ -29,6 +29,18 @@ interface Issue {
29
29
  source?: string;
30
30
  source_url?: string;
31
31
  labels?: string[];
32
+ // Agent workflow fields
33
+ affected_components?: string[];
34
+ lifecycle_requirements?: {
35
+ test_strategy?: 'unit' | 'integration' | 'e2e' | 'auto';
36
+ regression_scope?: 'full' | 'related' | 'affected';
37
+ commit_strategy?: 'per-task' | 'atomic' | 'squash';
38
+ };
39
+ problem_statement?: string;
40
+ expected_behavior?: string;
41
+ actual_behavior?: string;
42
+ reproduction_steps?: string[];
43
+ // Timestamps
32
44
  created_at: string;
33
45
  updated_at: string;
34
46
  planned_at?: string;
@@ -100,31 +112,48 @@ interface Solution {
100
112
  }
101
113
 
102
114
  interface QueueItem {
103
- queue_id: string;
115
+ item_id: string; // Task item ID in queue: T-1, T-2, ... (formerly queue_id)
104
116
  issue_id: string;
105
117
  solution_id: string;
106
118
  task_id: string;
119
+ title?: string;
107
120
  status: 'pending' | 'ready' | 'executing' | 'completed' | 'failed' | 'blocked';
108
121
  execution_order: number;
109
122
  execution_group: string;
110
123
  depends_on: string[];
111
124
  semantic_priority: number;
112
125
  assigned_executor: 'codex' | 'gemini' | 'agent';
113
- queued_at: string;
114
126
  started_at?: string;
115
127
  completed_at?: string;
116
128
  result?: Record<string, any>;
117
129
  failure_reason?: string;
118
130
  }
119
131
 
132
+ interface QueueConflict {
133
+ type: 'file_conflict' | 'dependency_conflict' | 'resource_conflict';
134
+ tasks: string[]; // Item IDs involved in conflict
135
+ file?: string; // Conflicting file path
136
+ resolution: 'sequential' | 'merge' | 'manual';
137
+ resolution_order?: string[];
138
+ rationale?: string;
139
+ resolved: boolean;
140
+ }
141
+
142
+ interface ExecutionGroup {
143
+ id: string; // Group ID: P1, S1, etc.
144
+ type: 'parallel' | 'sequential';
145
+ task_count: number;
146
+ tasks: string[]; // Item IDs in this group
147
+ }
148
+
120
149
  interface Queue {
121
- id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS
150
+ id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS (derived from filename)
122
151
  name?: string; // Optional queue name
123
152
  status: 'active' | 'completed' | 'archived' | 'failed';
124
153
  issue_ids: string[]; // Issues in this queue
125
- queue: QueueItem[];
126
- conflicts: any[];
127
- execution_groups?: any[];
154
+ tasks: QueueItem[]; // Task items (formerly 'queue')
155
+ conflicts: QueueConflict[];
156
+ execution_groups?: ExecutionGroup[];
128
157
  _metadata: {
129
158
  version: string;
130
159
  total_tasks: number;
@@ -132,13 +161,13 @@ interface Queue {
132
161
  executing_count: number;
133
162
  completed_count: number;
134
163
  failed_count: number;
135
- created_at: string;
136
164
  updated_at: string;
137
165
  };
138
166
  }
139
167
 
140
168
  interface QueueIndex {
141
169
  active_queue_id: string | null;
170
+ active_item_id: string | null;
142
171
  queues: {
143
172
  id: string;
144
173
  status: string;
@@ -162,6 +191,7 @@ interface IssueOptions {
162
191
  json?: boolean;
163
192
  force?: boolean;
164
193
  fail?: boolean;
194
+ ids?: boolean; // List only IDs (one per line)
165
195
  }
166
196
 
167
197
  const ISSUES_DIR = '.workflow/issues';
@@ -278,7 +308,7 @@ function ensureQueuesDir(): void {
278
308
  function readQueueIndex(): QueueIndex {
279
309
  const path = join(getQueuesDir(), 'index.json');
280
310
  if (!existsSync(path)) {
281
- return { active_queue_id: null, queues: [] };
311
+ return { active_queue_id: null, active_item_id: null, queues: [] };
282
312
  }
283
313
  return JSON.parse(readFileSync(path, 'utf-8'));
284
314
  }
@@ -319,16 +349,15 @@ function createEmptyQueue(): Queue {
319
349
  id: generateQueueFileId(),
320
350
  status: 'active',
321
351
  issue_ids: [],
322
- queue: [],
352
+ tasks: [],
323
353
  conflicts: [],
324
354
  _metadata: {
325
- version: '2.0',
355
+ version: '2.1',
326
356
  total_tasks: 0,
327
357
  pending_count: 0,
328
358
  executing_count: 0,
329
359
  completed_count: 0,
330
360
  failed_count: 0,
331
- created_at: new Date().toISOString(),
332
361
  updated_at: new Date().toISOString()
333
362
  }
334
363
  };
@@ -338,11 +367,11 @@ function writeQueue(queue: Queue): void {
338
367
  ensureQueuesDir();
339
368
 
340
369
  // Update metadata counts
341
- queue._metadata.total_tasks = queue.queue.length;
342
- queue._metadata.pending_count = queue.queue.filter(q => q.status === 'pending').length;
343
- queue._metadata.executing_count = queue.queue.filter(q => q.status === 'executing').length;
344
- queue._metadata.completed_count = queue.queue.filter(q => q.status === 'completed').length;
345
- queue._metadata.failed_count = queue.queue.filter(q => q.status === 'failed').length;
370
+ queue._metadata.total_tasks = queue.tasks.length;
371
+ queue._metadata.pending_count = queue.tasks.filter(q => q.status === 'pending').length;
372
+ queue._metadata.executing_count = queue.tasks.filter(q => q.status === 'executing').length;
373
+ queue._metadata.completed_count = queue.tasks.filter(q => q.status === 'completed').length;
374
+ queue._metadata.failed_count = queue.tasks.filter(q => q.status === 'failed').length;
346
375
  queue._metadata.updated_at = new Date().toISOString();
347
376
 
348
377
  // Write queue file
@@ -359,7 +388,7 @@ function writeQueue(queue: Queue): void {
359
388
  issue_ids: queue.issue_ids,
360
389
  total_tasks: queue._metadata.total_tasks,
361
390
  completed_tasks: queue._metadata.completed_count,
362
- created_at: queue._metadata.created_at,
391
+ created_at: queue.id.replace('QUE-', '').replace(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/, '$1-$2-$3T$4:$5:$6Z'), // Derive from ID
363
392
  completed_at: queue.status === 'completed' ? new Date().toISOString() : undefined
364
393
  };
365
394
 
@@ -377,11 +406,11 @@ function writeQueue(queue: Queue): void {
377
406
  }
378
407
 
379
408
  function generateQueueItemId(queue: Queue): string {
380
- const maxNum = queue.queue.reduce((max, q) => {
381
- const match = q.queue_id.match(/^Q-(\d+)$/);
409
+ const maxNum = queue.tasks.reduce((max, q) => {
410
+ const match = q.item_id.match(/^T-(\d+)$/);
382
411
  return match ? Math.max(max, parseInt(match[1])) : max;
383
412
  }, 0);
384
- return `Q-${String(maxNum + 1).padStart(3, '0')}`;
413
+ return `T-${maxNum + 1}`;
385
414
  }
386
415
 
387
416
  // ============ Commands ============
@@ -429,7 +458,19 @@ async function initAction(issueId: string | undefined, options: IssueOptions): P
429
458
  async function listAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
430
459
  if (!issueId) {
431
460
  // List all issues
432
- const issues = readIssues();
461
+ let issues = readIssues();
462
+
463
+ // Filter by status if specified
464
+ if (options.status) {
465
+ const statuses = options.status.split(',').map(s => s.trim());
466
+ issues = issues.filter(i => statuses.includes(i.status));
467
+ }
468
+
469
+ // IDs only mode (one per line, for scripting)
470
+ if (options.ids) {
471
+ issues.forEach(i => console.log(i.id));
472
+ return;
473
+ }
433
474
 
434
475
  if (options.json) {
435
476
  console.log(JSON.stringify(issues, null, 2));
@@ -807,7 +848,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
807
848
  // Archive current queue
808
849
  if (subAction === 'archive') {
809
850
  const queue = readActiveQueue();
810
- if (!queue.id || queue.queue.length === 0) {
851
+ if (!queue.id || queue.tasks.length === 0) {
811
852
  console.log(chalk.yellow('No active queue to archive'));
812
853
  return;
813
854
  }
@@ -823,6 +864,31 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
823
864
  return;
824
865
  }
825
866
 
867
+ // Delete queue from history
868
+ if ((subAction === 'clear' || subAction === 'delete') && issueId) {
869
+ const queueId = issueId; // issueId is actually queue ID here
870
+ const queuePath = join(getQueuesDir(), `${queueId}.json`);
871
+
872
+ if (!existsSync(queuePath)) {
873
+ console.error(chalk.red(`Queue "${queueId}" not found`));
874
+ process.exit(1);
875
+ }
876
+
877
+ // Remove from index
878
+ const index = readQueueIndex();
879
+ index.queues = index.queues.filter(q => q.id !== queueId);
880
+ if (index.active_queue_id === queueId) {
881
+ index.active_queue_id = null;
882
+ }
883
+ writeQueueIndex(index);
884
+
885
+ // Delete queue file
886
+ unlinkSync(queuePath);
887
+
888
+ console.log(chalk.green(`✓ Deleted queue ${queueId}`));
889
+ return;
890
+ }
891
+
826
892
  // Add issue tasks to queue
827
893
  if (subAction === 'add' && issueId) {
828
894
  const issue = findIssue(issueId);
@@ -840,7 +906,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
840
906
 
841
907
  // Get or create active queue (create new if current is completed/archived)
842
908
  let queue = readActiveQueue();
843
- const isNewQueue = queue.queue.length === 0 || queue.status !== 'active';
909
+ const isNewQueue = queue.tasks.length === 0 || queue.status !== 'active';
844
910
 
845
911
  if (queue.status !== 'active') {
846
912
  // Create new queue if current is not active
@@ -854,24 +920,23 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
854
920
 
855
921
  let added = 0;
856
922
  for (const task of solution.tasks) {
857
- const exists = queue.queue.some(q => q.issue_id === issueId && q.task_id === task.id);
923
+ const exists = queue.tasks.some(q => q.issue_id === issueId && q.task_id === task.id);
858
924
  if (exists) continue;
859
925
 
860
- queue.queue.push({
861
- queue_id: generateQueueItemId(queue),
926
+ queue.tasks.push({
927
+ item_id: generateQueueItemId(queue),
862
928
  issue_id: issueId,
863
929
  solution_id: solution.id,
864
930
  task_id: task.id,
865
931
  status: 'pending',
866
- execution_order: queue.queue.length + 1,
932
+ execution_order: queue.tasks.length + 1,
867
933
  execution_group: 'P1',
868
934
  depends_on: task.depends_on.map(dep => {
869
- const depItem = queue.queue.find(q => q.task_id === dep && q.issue_id === issueId);
870
- return depItem?.queue_id || dep;
935
+ const depItem = queue.tasks.find(q => q.task_id === dep && q.issue_id === issueId);
936
+ return depItem?.item_id || dep;
871
937
  }),
872
938
  semantic_priority: 0.5,
873
- assigned_executor: task.executor === 'auto' ? 'codex' : task.executor as any,
874
- queued_at: new Date().toISOString()
939
+ assigned_executor: task.executor === 'auto' ? 'codex' : task.executor as any
875
940
  });
876
941
  added++;
877
942
  }
@@ -896,7 +961,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
896
961
 
897
962
  console.log(chalk.bold.cyan('\nActive Queue\n'));
898
963
 
899
- if (!queue.id || queue.queue.length === 0) {
964
+ if (!queue.id || queue.tasks.length === 0) {
900
965
  console.log(chalk.yellow('No active queue'));
901
966
  console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
902
967
  console.log(chalk.gray('Or list history: ccw issue queue list'));
@@ -911,7 +976,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
911
976
  console.log(chalk.gray('QueueID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
912
977
  console.log(chalk.gray('-'.repeat(60)));
913
978
 
914
- for (const item of queue.queue) {
979
+ for (const item of queue.tasks) {
915
980
  const statusColor = {
916
981
  'pending': chalk.gray,
917
982
  'ready': chalk.cyan,
@@ -922,7 +987,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
922
987
  }[item.status] || chalk.white;
923
988
 
924
989
  console.log(
925
- item.queue_id.padEnd(10) +
990
+ item.item_id.padEnd(10) +
926
991
  item.issue_id.substring(0, 13).padEnd(15) +
927
992
  item.task_id.padEnd(8) +
928
993
  statusColor(item.status.padEnd(12)) +
@@ -938,13 +1003,13 @@ async function nextAction(options: IssueOptions): Promise<void> {
938
1003
  const queue = readActiveQueue();
939
1004
 
940
1005
  // Priority 1: Resume executing tasks (interrupted/crashed)
941
- const executingTasks = queue.queue.filter(item => item.status === 'executing');
1006
+ const executingTasks = queue.tasks.filter(item => item.status === 'executing');
942
1007
 
943
1008
  // Priority 2: Find pending tasks with satisfied dependencies
944
- const pendingTasks = queue.queue.filter(item => {
1009
+ const pendingTasks = queue.tasks.filter(item => {
945
1010
  if (item.status !== 'pending') return false;
946
1011
  return item.depends_on.every(depId => {
947
- const dep = queue.queue.find(q => q.queue_id === depId);
1012
+ const dep = queue.tasks.find(q => q.item_id === depId);
948
1013
  return !dep || dep.status === 'completed';
949
1014
  });
950
1015
  });
@@ -977,25 +1042,25 @@ async function nextAction(options: IssueOptions): Promise<void> {
977
1042
 
978
1043
  // Only update status if not already executing (new task)
979
1044
  if (!isResume) {
980
- const idx = queue.queue.findIndex(q => q.queue_id === nextItem.queue_id);
981
- queue.queue[idx].status = 'executing';
982
- queue.queue[idx].started_at = new Date().toISOString();
1045
+ const idx = queue.tasks.findIndex(q => q.item_id === nextItem.item_id);
1046
+ queue.tasks[idx].status = 'executing';
1047
+ queue.tasks[idx].started_at = new Date().toISOString();
983
1048
  writeQueue(queue);
984
1049
  updateIssue(nextItem.issue_id, { status: 'executing' });
985
1050
  }
986
1051
 
987
1052
  // Calculate queue stats for context
988
1053
  const stats = {
989
- total: queue.queue.length,
990
- completed: queue.queue.filter(q => q.status === 'completed').length,
991
- failed: queue.queue.filter(q => q.status === 'failed').length,
1054
+ total: queue.tasks.length,
1055
+ completed: queue.tasks.filter(q => q.status === 'completed').length,
1056
+ failed: queue.tasks.filter(q => q.status === 'failed').length,
992
1057
  executing: executingTasks.length,
993
1058
  pending: pendingTasks.length
994
1059
  };
995
1060
  const remaining = stats.pending + stats.executing;
996
1061
 
997
1062
  console.log(JSON.stringify({
998
- queue_id: nextItem.queue_id,
1063
+ item_id: nextItem.item_id,
999
1064
  issue_id: nextItem.issue_id,
1000
1065
  solution_id: nextItem.solution_id,
1001
1066
  task: taskDef,
@@ -1026,7 +1091,7 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
1026
1091
  }
1027
1092
 
1028
1093
  const queue = readActiveQueue();
1029
- const idx = queue.queue.findIndex(q => q.queue_id === queueId);
1094
+ const idx = queue.tasks.findIndex(q => q.item_id === queueId);
1030
1095
 
1031
1096
  if (idx === -1) {
1032
1097
  console.error(chalk.red(`Queue item "${queueId}" not found`));
@@ -1034,22 +1099,22 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
1034
1099
  }
1035
1100
 
1036
1101
  const isFail = options.fail;
1037
- queue.queue[idx].status = isFail ? 'failed' : 'completed';
1038
- queue.queue[idx].completed_at = new Date().toISOString();
1102
+ queue.tasks[idx].status = isFail ? 'failed' : 'completed';
1103
+ queue.tasks[idx].completed_at = new Date().toISOString();
1039
1104
 
1040
1105
  if (isFail) {
1041
- queue.queue[idx].failure_reason = options.reason || 'Unknown failure';
1106
+ queue.tasks[idx].failure_reason = options.reason || 'Unknown failure';
1042
1107
  } else if (options.result) {
1043
1108
  try {
1044
- queue.queue[idx].result = JSON.parse(options.result);
1109
+ queue.tasks[idx].result = JSON.parse(options.result);
1045
1110
  } catch {
1046
1111
  console.warn(chalk.yellow('Warning: Could not parse result JSON'));
1047
1112
  }
1048
1113
  }
1049
1114
 
1050
1115
  // Check if all issue tasks are complete
1051
- const issueId = queue.queue[idx].issue_id;
1052
- const issueTasks = queue.queue.filter(q => q.issue_id === issueId);
1116
+ const issueId = queue.tasks[idx].issue_id;
1117
+ const issueTasks = queue.tasks.filter(q => q.issue_id === issueId);
1053
1118
  const allIssueComplete = issueTasks.every(q => q.status === 'completed');
1054
1119
  const anyIssueFailed = issueTasks.some(q => q.status === 'failed');
1055
1120
 
@@ -1065,13 +1130,13 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
1065
1130
  }
1066
1131
 
1067
1132
  // Check if entire queue is complete
1068
- const allQueueComplete = queue.queue.every(q => q.status === 'completed');
1069
- const anyQueueFailed = queue.queue.some(q => q.status === 'failed');
1133
+ const allQueueComplete = queue.tasks.every(q => q.status === 'completed');
1134
+ const anyQueueFailed = queue.tasks.some(q => q.status === 'failed');
1070
1135
 
1071
1136
  if (allQueueComplete) {
1072
1137
  queue.status = 'completed';
1073
1138
  console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all tasks done)`));
1074
- } else if (anyQueueFailed && queue.queue.every(q => q.status === 'completed' || q.status === 'failed')) {
1139
+ } else if (anyQueueFailed && queue.tasks.every(q => q.status === 'completed' || q.status === 'failed')) {
1075
1140
  queue.status = 'failed';
1076
1141
  console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed tasks`));
1077
1142
  }
@@ -1080,24 +1145,20 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
1080
1145
  }
1081
1146
 
1082
1147
  /**
1083
- * retry - Retry failed tasks, or reset stuck executing tasks (--force)
1148
+ * retry - Reset failed tasks to pending for re-execution
1084
1149
  */
1085
1150
  async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
1086
1151
  const queue = readActiveQueue();
1087
1152
 
1088
- if (!queue.id || queue.queue.length === 0) {
1153
+ if (!queue.id || queue.tasks.length === 0) {
1089
1154
  console.log(chalk.yellow('No active queue'));
1090
1155
  return;
1091
1156
  }
1092
1157
 
1093
1158
  let updated = 0;
1094
1159
 
1095
- // Check for stuck executing tasks (started > 30 min ago with no completion)
1096
- const stuckThreshold = 30 * 60 * 1000; // 30 minutes
1097
- const now = Date.now();
1098
-
1099
- for (const item of queue.queue) {
1100
- // Retry failed tasks
1160
+ for (const item of queue.tasks) {
1161
+ // Retry failed tasks only
1101
1162
  if (item.status === 'failed') {
1102
1163
  if (!issueId || item.issue_id === issueId) {
1103
1164
  item.status = 'pending';
@@ -1107,23 +1168,11 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
1107
1168
  updated++;
1108
1169
  }
1109
1170
  }
1110
- // Reset stuck executing tasks (optional: use --force or --reset-stuck)
1111
- else if (item.status === 'executing' && options.force) {
1112
- const startedAt = item.started_at ? new Date(item.started_at).getTime() : 0;
1113
- if (now - startedAt > stuckThreshold) {
1114
- if (!issueId || item.issue_id === issueId) {
1115
- console.log(chalk.yellow(`Resetting stuck task: ${item.queue_id} (started ${Math.round((now - startedAt) / 60000)} min ago)`));
1116
- item.status = 'pending';
1117
- item.started_at = undefined;
1118
- updated++;
1119
- }
1120
- }
1121
- }
1122
1171
  }
1123
1172
 
1124
1173
  if (updated === 0) {
1125
- console.log(chalk.yellow('No failed/stuck tasks to retry'));
1126
- console.log(chalk.gray('Use --force to reset stuck executing tasks (>30 min)'));
1174
+ console.log(chalk.yellow('No failed tasks to retry'));
1175
+ console.log(chalk.gray('Note: Interrupted (executing) tasks are auto-resumed by "ccw issue next"'));
1127
1176
  return;
1128
1177
  }
1129
1178
 
@@ -1204,7 +1253,8 @@ export async function issueCommand(
1204
1253
  console.log(chalk.gray(' queue add <issue-id> Add issue to active queue (or create new)'));
1205
1254
  console.log(chalk.gray(' queue switch <queue-id> Switch active queue'));
1206
1255
  console.log(chalk.gray(' queue archive Archive current queue'));
1207
- console.log(chalk.gray(' retry [issue-id] [--force] Retry failed/stuck tasks'));
1256
+ console.log(chalk.gray(' queue delete <queue-id> Delete queue from history'));
1257
+ console.log(chalk.gray(' retry [issue-id] Retry failed tasks'));
1208
1258
  console.log();
1209
1259
  console.log(chalk.bold('Execution Endpoints:'));
1210
1260
  console.log(chalk.gray(' next Get next ready task (JSON)'));
@@ -1213,6 +1263,8 @@ export async function issueCommand(
1213
1263
  console.log();
1214
1264
  console.log(chalk.bold('Options:'));
1215
1265
  console.log(chalk.gray(' --title <title> Issue/task title'));
1266
+ console.log(chalk.gray(' --status <status> Filter by status (comma-separated)'));
1267
+ console.log(chalk.gray(' --ids List only IDs (one per line)'));
1216
1268
  console.log(chalk.gray(' --solution <path> Solution JSON file'));
1217
1269
  console.log(chalk.gray(' --result <json> Execution result'));
1218
1270
  console.log(chalk.gray(' --reason <text> Failure reason'));
@@ -5,7 +5,9 @@
5
5
  * Storage Structure:
6
6
  * .workflow/issues/
7
7
  * ├── issues.jsonl # All issues (one per line)
8
- * ├── queue.json # Execution queue
8
+ * ├── queues/ # Queue history directory
9
+ * │ ├── index.json # Queue index (active + history)
10
+ * │ └── {queue-id}.json # Individual queue files
9
11
  * └── solutions/
10
12
  * ├── {issue-id}.jsonl # Solutions for issue (one per line)
11
13
  * └── ...
@@ -102,12 +104,12 @@ function readQueue(issuesDir: string) {
102
104
  }
103
105
  }
104
106
 
105
- return { queue: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } };
107
+ return { tasks: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } };
106
108
  }
107
109
 
108
110
  function writeQueue(issuesDir: string, queue: any) {
109
111
  if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
110
- queue._metadata = { ...queue._metadata, updated_at: new Date().toISOString(), total_tasks: queue.queue?.length || 0 };
112
+ queue._metadata = { ...queue._metadata, updated_at: new Date().toISOString(), total_tasks: queue.tasks?.length || 0 };
111
113
 
112
114
  // Check if using new multi-queue structure
113
115
  const queuesDir = join(issuesDir, 'queues');
@@ -123,8 +125,8 @@ function writeQueue(issuesDir: string, queue: any) {
123
125
  const index = JSON.parse(readFileSync(indexPath, 'utf8'));
124
126
  const queueEntry = index.queues?.find((q: any) => q.id === queue.id);
125
127
  if (queueEntry) {
126
- queueEntry.total_tasks = queue.queue?.length || 0;
127
- queueEntry.completed_tasks = queue.queue?.filter((i: any) => i.status === 'completed').length || 0;
128
+ queueEntry.total_tasks = queue.tasks?.length || 0;
129
+ queueEntry.completed_tasks = queue.tasks?.filter((i: any) => i.status === 'completed').length || 0;
128
130
  writeFileSync(indexPath, JSON.stringify(index, null, 2));
129
131
  }
130
132
  } catch {
@@ -151,15 +153,29 @@ function getIssueDetail(issuesDir: string, issueId: string) {
151
153
  }
152
154
 
153
155
  function enrichIssues(issues: any[], issuesDir: string) {
154
- return issues.map(issue => ({
155
- ...issue,
156
- solution_count: readSolutionsJsonl(issuesDir, issue.id).length
157
- }));
156
+ return issues.map(issue => {
157
+ const solutions = readSolutionsJsonl(issuesDir, issue.id);
158
+ let taskCount = 0;
159
+
160
+ // Get task count from bound solution
161
+ if (issue.bound_solution_id) {
162
+ const boundSol = solutions.find(s => s.id === issue.bound_solution_id);
163
+ if (boundSol?.tasks) {
164
+ taskCount = boundSol.tasks.length;
165
+ }
166
+ }
167
+
168
+ return {
169
+ ...issue,
170
+ solution_count: solutions.length,
171
+ task_count: taskCount
172
+ };
173
+ });
158
174
  }
159
175
 
160
176
  function groupQueueByExecutionGroup(queue: any) {
161
177
  const groups: { [key: string]: any[] } = {};
162
- for (const item of queue.queue || []) {
178
+ for (const item of queue.tasks || []) {
163
179
  const groupId = item.execution_group || 'ungrouped';
164
180
  if (!groups[groupId]) groups[groupId] = [];
165
181
  groups[groupId].push(item);
@@ -171,7 +187,7 @@ function groupQueueByExecutionGroup(queue: any) {
171
187
  id,
172
188
  type: id.startsWith('P') ? 'parallel' : id.startsWith('S') ? 'sequential' : 'unknown',
173
189
  task_count: items.length,
174
- tasks: items.map(i => i.queue_id)
190
+ tasks: items.map(i => i.item_id)
175
191
  })).sort((a, b) => {
176
192
  const aFirst = groups[a.id]?.[0]?.execution_order || 0;
177
193
  const bFirst = groups[b.id]?.[0]?.execution_order || 0;
@@ -220,6 +236,82 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
220
236
  return true;
221
237
  }
222
238
 
239
+ // GET /api/queue/history - Get queue history (all queues from index)
240
+ if (pathname === '/api/queue/history' && req.method === 'GET') {
241
+ const queuesDir = join(issuesDir, 'queues');
242
+ const indexPath = join(queuesDir, 'index.json');
243
+
244
+ if (!existsSync(indexPath)) {
245
+ res.writeHead(200, { 'Content-Type': 'application/json' });
246
+ res.end(JSON.stringify({ queues: [], active_queue_id: null }));
247
+ return true;
248
+ }
249
+
250
+ try {
251
+ const index = JSON.parse(readFileSync(indexPath, 'utf8'));
252
+ res.writeHead(200, { 'Content-Type': 'application/json' });
253
+ res.end(JSON.stringify(index));
254
+ } catch {
255
+ res.writeHead(200, { 'Content-Type': 'application/json' });
256
+ res.end(JSON.stringify({ queues: [], active_queue_id: null }));
257
+ }
258
+ return true;
259
+ }
260
+
261
+ // GET /api/queue/:id - Get specific queue by ID
262
+ const queueDetailMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
263
+ if (queueDetailMatch && req.method === 'GET' && queueDetailMatch[1] !== 'history' && queueDetailMatch[1] !== 'reorder') {
264
+ const queueId = queueDetailMatch[1];
265
+ const queuesDir = join(issuesDir, 'queues');
266
+ const queueFilePath = join(queuesDir, `${queueId}.json`);
267
+
268
+ if (!existsSync(queueFilePath)) {
269
+ res.writeHead(404, { 'Content-Type': 'application/json' });
270
+ res.end(JSON.stringify({ error: `Queue ${queueId} not found` }));
271
+ return true;
272
+ }
273
+
274
+ try {
275
+ const queue = JSON.parse(readFileSync(queueFilePath, 'utf8'));
276
+ res.writeHead(200, { 'Content-Type': 'application/json' });
277
+ res.end(JSON.stringify(groupQueueByExecutionGroup(queue)));
278
+ } catch {
279
+ res.writeHead(500, { 'Content-Type': 'application/json' });
280
+ res.end(JSON.stringify({ error: 'Failed to read queue' }));
281
+ }
282
+ return true;
283
+ }
284
+
285
+ // POST /api/queue/switch - Switch active queue
286
+ if (pathname === '/api/queue/switch' && req.method === 'POST') {
287
+ handlePostRequest(req, res, async (body: any) => {
288
+ const { queueId } = body;
289
+ if (!queueId) return { error: 'queueId required' };
290
+
291
+ const queuesDir = join(issuesDir, 'queues');
292
+ const indexPath = join(queuesDir, 'index.json');
293
+ const queueFilePath = join(queuesDir, `${queueId}.json`);
294
+
295
+ if (!existsSync(queueFilePath)) {
296
+ return { error: `Queue ${queueId} not found` };
297
+ }
298
+
299
+ try {
300
+ const index = existsSync(indexPath)
301
+ ? JSON.parse(readFileSync(indexPath, 'utf8'))
302
+ : { active_queue_id: null, queues: [] };
303
+
304
+ index.active_queue_id = queueId;
305
+ writeFileSync(indexPath, JSON.stringify(index, null, 2));
306
+
307
+ return { success: true, active_queue_id: queueId };
308
+ } catch (err) {
309
+ return { error: 'Failed to switch queue' };
310
+ }
311
+ });
312
+ return true;
313
+ }
314
+
223
315
  // POST /api/queue/reorder - Reorder queue items
224
316
  if (pathname === '/api/queue/reorder' && req.method === 'POST') {
225
317
  handlePostRequest(req, res, async (body: any) => {
@@ -229,20 +321,20 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
229
321
  }
230
322
 
231
323
  const queue = readQueue(issuesDir);
232
- const groupItems = queue.queue.filter((item: any) => item.execution_group === groupId);
233
- const otherItems = queue.queue.filter((item: any) => item.execution_group !== groupId);
324
+ const groupItems = queue.tasks.filter((item: any) => item.execution_group === groupId);
325
+ const otherItems = queue.tasks.filter((item: any) => item.execution_group !== groupId);
234
326
 
235
327
  if (groupItems.length === 0) return { error: `No items in group ${groupId}` };
236
328
 
237
- const groupQueueIds = new Set(groupItems.map((i: any) => i.queue_id));
238
- if (groupQueueIds.size !== new Set(newOrder).size) {
329
+ const groupItemIds = new Set(groupItems.map((i: any) => i.item_id));
330
+ if (groupItemIds.size !== new Set(newOrder).size) {
239
331
  return { error: 'newOrder must contain all group items' };
240
332
  }
241
333
  for (const id of newOrder) {
242
- if (!groupQueueIds.has(id)) return { error: `Invalid queue_id: ${id}` };
334
+ if (!groupItemIds.has(id)) return { error: `Invalid item_id: ${id}` };
243
335
  }
244
336
 
245
- const itemMap = new Map(groupItems.map((i: any) => [i.queue_id, i]));
337
+ const itemMap = new Map(groupItems.map((i: any) => [i.item_id, i]));
246
338
  const reorderedItems = newOrder.map((qid: string, idx: number) => ({ ...itemMap.get(qid), _idx: idx }));
247
339
  const newQueue = [...otherItems, ...reorderedItems].sort((a, b) => {
248
340
  const aGroup = parseInt(a.execution_group?.match(/\d+/)?.[0] || '999');
@@ -255,7 +347,7 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
255
347
  });
256
348
 
257
349
  newQueue.forEach((item, idx) => { item.execution_order = idx + 1; delete item._idx; });
258
- queue.queue = newQueue;
350
+ queue.tasks = newQueue;
259
351
  writeQueue(issuesDir, queue);
260
352
 
261
353
  return { success: true, groupId, reordered: newOrder.length };