claude-code-workflow 6.3.7 → 6.3.8

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.
@@ -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,17 +112,17 @@ 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>;
@@ -118,11 +130,11 @@ interface QueueItem {
118
130
  }
119
131
 
120
132
  interface Queue {
121
- id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS
133
+ id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS (derived from filename)
122
134
  name?: string; // Optional queue name
123
135
  status: 'active' | 'completed' | 'archived' | 'failed';
124
136
  issue_ids: string[]; // Issues in this queue
125
- queue: QueueItem[];
137
+ tasks: QueueItem[]; // Task items (formerly 'queue')
126
138
  conflicts: any[];
127
139
  execution_groups?: any[];
128
140
  _metadata: {
@@ -132,13 +144,13 @@ interface Queue {
132
144
  executing_count: number;
133
145
  completed_count: number;
134
146
  failed_count: number;
135
- created_at: string;
136
147
  updated_at: string;
137
148
  };
138
149
  }
139
150
 
140
151
  interface QueueIndex {
141
152
  active_queue_id: string | null;
153
+ active_item_id: string | null;
142
154
  queues: {
143
155
  id: string;
144
156
  status: string;
@@ -162,6 +174,7 @@ interface IssueOptions {
162
174
  json?: boolean;
163
175
  force?: boolean;
164
176
  fail?: boolean;
177
+ ids?: boolean; // List only IDs (one per line)
165
178
  }
166
179
 
167
180
  const ISSUES_DIR = '.workflow/issues';
@@ -278,7 +291,7 @@ function ensureQueuesDir(): void {
278
291
  function readQueueIndex(): QueueIndex {
279
292
  const path = join(getQueuesDir(), 'index.json');
280
293
  if (!existsSync(path)) {
281
- return { active_queue_id: null, queues: [] };
294
+ return { active_queue_id: null, active_item_id: null, queues: [] };
282
295
  }
283
296
  return JSON.parse(readFileSync(path, 'utf-8'));
284
297
  }
@@ -319,16 +332,15 @@ function createEmptyQueue(): Queue {
319
332
  id: generateQueueFileId(),
320
333
  status: 'active',
321
334
  issue_ids: [],
322
- queue: [],
335
+ tasks: [],
323
336
  conflicts: [],
324
337
  _metadata: {
325
- version: '2.0',
338
+ version: '2.1',
326
339
  total_tasks: 0,
327
340
  pending_count: 0,
328
341
  executing_count: 0,
329
342
  completed_count: 0,
330
343
  failed_count: 0,
331
- created_at: new Date().toISOString(),
332
344
  updated_at: new Date().toISOString()
333
345
  }
334
346
  };
@@ -338,11 +350,11 @@ function writeQueue(queue: Queue): void {
338
350
  ensureQueuesDir();
339
351
 
340
352
  // 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;
353
+ queue._metadata.total_tasks = queue.tasks.length;
354
+ queue._metadata.pending_count = queue.tasks.filter(q => q.status === 'pending').length;
355
+ queue._metadata.executing_count = queue.tasks.filter(q => q.status === 'executing').length;
356
+ queue._metadata.completed_count = queue.tasks.filter(q => q.status === 'completed').length;
357
+ queue._metadata.failed_count = queue.tasks.filter(q => q.status === 'failed').length;
346
358
  queue._metadata.updated_at = new Date().toISOString();
347
359
 
348
360
  // Write queue file
@@ -359,7 +371,7 @@ function writeQueue(queue: Queue): void {
359
371
  issue_ids: queue.issue_ids,
360
372
  total_tasks: queue._metadata.total_tasks,
361
373
  completed_tasks: queue._metadata.completed_count,
362
- created_at: queue._metadata.created_at,
374
+ 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
375
  completed_at: queue.status === 'completed' ? new Date().toISOString() : undefined
364
376
  };
365
377
 
@@ -377,11 +389,11 @@ function writeQueue(queue: Queue): void {
377
389
  }
378
390
 
379
391
  function generateQueueItemId(queue: Queue): string {
380
- const maxNum = queue.queue.reduce((max, q) => {
381
- const match = q.queue_id.match(/^Q-(\d+)$/);
392
+ const maxNum = queue.tasks.reduce((max, q) => {
393
+ const match = q.item_id.match(/^T-(\d+)$/);
382
394
  return match ? Math.max(max, parseInt(match[1])) : max;
383
395
  }, 0);
384
- return `Q-${String(maxNum + 1).padStart(3, '0')}`;
396
+ return `T-${maxNum + 1}`;
385
397
  }
386
398
 
387
399
  // ============ Commands ============
@@ -429,7 +441,19 @@ async function initAction(issueId: string | undefined, options: IssueOptions): P
429
441
  async function listAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
430
442
  if (!issueId) {
431
443
  // List all issues
432
- const issues = readIssues();
444
+ let issues = readIssues();
445
+
446
+ // Filter by status if specified
447
+ if (options.status) {
448
+ const statuses = options.status.split(',').map(s => s.trim());
449
+ issues = issues.filter(i => statuses.includes(i.status));
450
+ }
451
+
452
+ // IDs only mode (one per line, for scripting)
453
+ if (options.ids) {
454
+ issues.forEach(i => console.log(i.id));
455
+ return;
456
+ }
433
457
 
434
458
  if (options.json) {
435
459
  console.log(JSON.stringify(issues, null, 2));
@@ -807,7 +831,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
807
831
  // Archive current queue
808
832
  if (subAction === 'archive') {
809
833
  const queue = readActiveQueue();
810
- if (!queue.id || queue.queue.length === 0) {
834
+ if (!queue.id || queue.tasks.length === 0) {
811
835
  console.log(chalk.yellow('No active queue to archive'));
812
836
  return;
813
837
  }
@@ -823,6 +847,31 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
823
847
  return;
824
848
  }
825
849
 
850
+ // Delete queue from history
851
+ if ((subAction === 'clear' || subAction === 'delete') && issueId) {
852
+ const queueId = issueId; // issueId is actually queue ID here
853
+ const queuePath = join(getQueuesDir(), `${queueId}.json`);
854
+
855
+ if (!existsSync(queuePath)) {
856
+ console.error(chalk.red(`Queue "${queueId}" not found`));
857
+ process.exit(1);
858
+ }
859
+
860
+ // Remove from index
861
+ const index = readQueueIndex();
862
+ index.queues = index.queues.filter(q => q.id !== queueId);
863
+ if (index.active_queue_id === queueId) {
864
+ index.active_queue_id = null;
865
+ }
866
+ writeQueueIndex(index);
867
+
868
+ // Delete queue file
869
+ unlinkSync(queuePath);
870
+
871
+ console.log(chalk.green(`✓ Deleted queue ${queueId}`));
872
+ return;
873
+ }
874
+
826
875
  // Add issue tasks to queue
827
876
  if (subAction === 'add' && issueId) {
828
877
  const issue = findIssue(issueId);
@@ -840,7 +889,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
840
889
 
841
890
  // Get or create active queue (create new if current is completed/archived)
842
891
  let queue = readActiveQueue();
843
- const isNewQueue = queue.queue.length === 0 || queue.status !== 'active';
892
+ const isNewQueue = queue.tasks.length === 0 || queue.status !== 'active';
844
893
 
845
894
  if (queue.status !== 'active') {
846
895
  // Create new queue if current is not active
@@ -854,24 +903,23 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
854
903
 
855
904
  let added = 0;
856
905
  for (const task of solution.tasks) {
857
- const exists = queue.queue.some(q => q.issue_id === issueId && q.task_id === task.id);
906
+ const exists = queue.tasks.some(q => q.issue_id === issueId && q.task_id === task.id);
858
907
  if (exists) continue;
859
908
 
860
- queue.queue.push({
861
- queue_id: generateQueueItemId(queue),
909
+ queue.tasks.push({
910
+ item_id: generateQueueItemId(queue),
862
911
  issue_id: issueId,
863
912
  solution_id: solution.id,
864
913
  task_id: task.id,
865
914
  status: 'pending',
866
- execution_order: queue.queue.length + 1,
915
+ execution_order: queue.tasks.length + 1,
867
916
  execution_group: 'P1',
868
917
  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;
918
+ const depItem = queue.tasks.find(q => q.task_id === dep && q.issue_id === issueId);
919
+ return depItem?.item_id || dep;
871
920
  }),
872
921
  semantic_priority: 0.5,
873
- assigned_executor: task.executor === 'auto' ? 'codex' : task.executor as any,
874
- queued_at: new Date().toISOString()
922
+ assigned_executor: task.executor === 'auto' ? 'codex' : task.executor as any
875
923
  });
876
924
  added++;
877
925
  }
@@ -896,7 +944,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
896
944
 
897
945
  console.log(chalk.bold.cyan('\nActive Queue\n'));
898
946
 
899
- if (!queue.id || queue.queue.length === 0) {
947
+ if (!queue.id || queue.tasks.length === 0) {
900
948
  console.log(chalk.yellow('No active queue'));
901
949
  console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
902
950
  console.log(chalk.gray('Or list history: ccw issue queue list'));
@@ -911,7 +959,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
911
959
  console.log(chalk.gray('QueueID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
912
960
  console.log(chalk.gray('-'.repeat(60)));
913
961
 
914
- for (const item of queue.queue) {
962
+ for (const item of queue.tasks) {
915
963
  const statusColor = {
916
964
  'pending': chalk.gray,
917
965
  'ready': chalk.cyan,
@@ -922,7 +970,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
922
970
  }[item.status] || chalk.white;
923
971
 
924
972
  console.log(
925
- item.queue_id.padEnd(10) +
973
+ item.item_id.padEnd(10) +
926
974
  item.issue_id.substring(0, 13).padEnd(15) +
927
975
  item.task_id.padEnd(8) +
928
976
  statusColor(item.status.padEnd(12)) +
@@ -938,13 +986,13 @@ async function nextAction(options: IssueOptions): Promise<void> {
938
986
  const queue = readActiveQueue();
939
987
 
940
988
  // Priority 1: Resume executing tasks (interrupted/crashed)
941
- const executingTasks = queue.queue.filter(item => item.status === 'executing');
989
+ const executingTasks = queue.tasks.filter(item => item.status === 'executing');
942
990
 
943
991
  // Priority 2: Find pending tasks with satisfied dependencies
944
- const pendingTasks = queue.queue.filter(item => {
992
+ const pendingTasks = queue.tasks.filter(item => {
945
993
  if (item.status !== 'pending') return false;
946
994
  return item.depends_on.every(depId => {
947
- const dep = queue.queue.find(q => q.queue_id === depId);
995
+ const dep = queue.tasks.find(q => q.item_id === depId);
948
996
  return !dep || dep.status === 'completed';
949
997
  });
950
998
  });
@@ -977,25 +1025,25 @@ async function nextAction(options: IssueOptions): Promise<void> {
977
1025
 
978
1026
  // Only update status if not already executing (new task)
979
1027
  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();
1028
+ const idx = queue.tasks.findIndex(q => q.item_id === nextItem.item_id);
1029
+ queue.tasks[idx].status = 'executing';
1030
+ queue.tasks[idx].started_at = new Date().toISOString();
983
1031
  writeQueue(queue);
984
1032
  updateIssue(nextItem.issue_id, { status: 'executing' });
985
1033
  }
986
1034
 
987
1035
  // Calculate queue stats for context
988
1036
  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,
1037
+ total: queue.tasks.length,
1038
+ completed: queue.tasks.filter(q => q.status === 'completed').length,
1039
+ failed: queue.tasks.filter(q => q.status === 'failed').length,
992
1040
  executing: executingTasks.length,
993
1041
  pending: pendingTasks.length
994
1042
  };
995
1043
  const remaining = stats.pending + stats.executing;
996
1044
 
997
1045
  console.log(JSON.stringify({
998
- queue_id: nextItem.queue_id,
1046
+ item_id: nextItem.item_id,
999
1047
  issue_id: nextItem.issue_id,
1000
1048
  solution_id: nextItem.solution_id,
1001
1049
  task: taskDef,
@@ -1026,7 +1074,7 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
1026
1074
  }
1027
1075
 
1028
1076
  const queue = readActiveQueue();
1029
- const idx = queue.queue.findIndex(q => q.queue_id === queueId);
1077
+ const idx = queue.tasks.findIndex(q => q.item_id === queueId);
1030
1078
 
1031
1079
  if (idx === -1) {
1032
1080
  console.error(chalk.red(`Queue item "${queueId}" not found`));
@@ -1034,22 +1082,22 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
1034
1082
  }
1035
1083
 
1036
1084
  const isFail = options.fail;
1037
- queue.queue[idx].status = isFail ? 'failed' : 'completed';
1038
- queue.queue[idx].completed_at = new Date().toISOString();
1085
+ queue.tasks[idx].status = isFail ? 'failed' : 'completed';
1086
+ queue.tasks[idx].completed_at = new Date().toISOString();
1039
1087
 
1040
1088
  if (isFail) {
1041
- queue.queue[idx].failure_reason = options.reason || 'Unknown failure';
1089
+ queue.tasks[idx].failure_reason = options.reason || 'Unknown failure';
1042
1090
  } else if (options.result) {
1043
1091
  try {
1044
- queue.queue[idx].result = JSON.parse(options.result);
1092
+ queue.tasks[idx].result = JSON.parse(options.result);
1045
1093
  } catch {
1046
1094
  console.warn(chalk.yellow('Warning: Could not parse result JSON'));
1047
1095
  }
1048
1096
  }
1049
1097
 
1050
1098
  // 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);
1099
+ const issueId = queue.tasks[idx].issue_id;
1100
+ const issueTasks = queue.tasks.filter(q => q.issue_id === issueId);
1053
1101
  const allIssueComplete = issueTasks.every(q => q.status === 'completed');
1054
1102
  const anyIssueFailed = issueTasks.some(q => q.status === 'failed');
1055
1103
 
@@ -1065,13 +1113,13 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
1065
1113
  }
1066
1114
 
1067
1115
  // 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');
1116
+ const allQueueComplete = queue.tasks.every(q => q.status === 'completed');
1117
+ const anyQueueFailed = queue.tasks.some(q => q.status === 'failed');
1070
1118
 
1071
1119
  if (allQueueComplete) {
1072
1120
  queue.status = 'completed';
1073
1121
  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')) {
1122
+ } else if (anyQueueFailed && queue.tasks.every(q => q.status === 'completed' || q.status === 'failed')) {
1075
1123
  queue.status = 'failed';
1076
1124
  console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed tasks`));
1077
1125
  }
@@ -1080,24 +1128,20 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
1080
1128
  }
1081
1129
 
1082
1130
  /**
1083
- * retry - Retry failed tasks, or reset stuck executing tasks (--force)
1131
+ * retry - Reset failed tasks to pending for re-execution
1084
1132
  */
1085
1133
  async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
1086
1134
  const queue = readActiveQueue();
1087
1135
 
1088
- if (!queue.id || queue.queue.length === 0) {
1136
+ if (!queue.id || queue.tasks.length === 0) {
1089
1137
  console.log(chalk.yellow('No active queue'));
1090
1138
  return;
1091
1139
  }
1092
1140
 
1093
1141
  let updated = 0;
1094
1142
 
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
1143
+ for (const item of queue.tasks) {
1144
+ // Retry failed tasks only
1101
1145
  if (item.status === 'failed') {
1102
1146
  if (!issueId || item.issue_id === issueId) {
1103
1147
  item.status = 'pending';
@@ -1107,23 +1151,11 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
1107
1151
  updated++;
1108
1152
  }
1109
1153
  }
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
1154
  }
1123
1155
 
1124
1156
  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)'));
1157
+ console.log(chalk.yellow('No failed tasks to retry'));
1158
+ console.log(chalk.gray('Note: Interrupted (executing) tasks are auto-resumed by "ccw issue next"'));
1127
1159
  return;
1128
1160
  }
1129
1161
 
@@ -1204,7 +1236,8 @@ export async function issueCommand(
1204
1236
  console.log(chalk.gray(' queue add <issue-id> Add issue to active queue (or create new)'));
1205
1237
  console.log(chalk.gray(' queue switch <queue-id> Switch active queue'));
1206
1238
  console.log(chalk.gray(' queue archive Archive current queue'));
1207
- console.log(chalk.gray(' retry [issue-id] [--force] Retry failed/stuck tasks'));
1239
+ console.log(chalk.gray(' queue delete <queue-id> Delete queue from history'));
1240
+ console.log(chalk.gray(' retry [issue-id] Retry failed tasks'));
1208
1241
  console.log();
1209
1242
  console.log(chalk.bold('Execution Endpoints:'));
1210
1243
  console.log(chalk.gray(' next Get next ready task (JSON)'));
@@ -1213,6 +1246,8 @@ export async function issueCommand(
1213
1246
  console.log();
1214
1247
  console.log(chalk.bold('Options:'));
1215
1248
  console.log(chalk.gray(' --title <title> Issue/task title'));
1249
+ console.log(chalk.gray(' --status <status> Filter by status (comma-separated)'));
1250
+ console.log(chalk.gray(' --ids List only IDs (one per line)'));
1216
1251
  console.log(chalk.gray(' --solution <path> Solution JSON file'));
1217
1252
  console.log(chalk.gray(' --result <json> Execution result'));
1218
1253
  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;
@@ -229,20 +245,20 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
229
245
  }
230
246
 
231
247
  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);
248
+ const groupItems = queue.tasks.filter((item: any) => item.execution_group === groupId);
249
+ const otherItems = queue.tasks.filter((item: any) => item.execution_group !== groupId);
234
250
 
235
251
  if (groupItems.length === 0) return { error: `No items in group ${groupId}` };
236
252
 
237
- const groupQueueIds = new Set(groupItems.map((i: any) => i.queue_id));
238
- if (groupQueueIds.size !== new Set(newOrder).size) {
253
+ const groupItemIds = new Set(groupItems.map((i: any) => i.item_id));
254
+ if (groupItemIds.size !== new Set(newOrder).size) {
239
255
  return { error: 'newOrder must contain all group items' };
240
256
  }
241
257
  for (const id of newOrder) {
242
- if (!groupQueueIds.has(id)) return { error: `Invalid queue_id: ${id}` };
258
+ if (!groupItemIds.has(id)) return { error: `Invalid item_id: ${id}` };
243
259
  }
244
260
 
245
- const itemMap = new Map(groupItems.map((i: any) => [i.queue_id, i]));
261
+ const itemMap = new Map(groupItems.map((i: any) => [i.item_id, i]));
246
262
  const reorderedItems = newOrder.map((qid: string, idx: number) => ({ ...itemMap.get(qid), _idx: idx }));
247
263
  const newQueue = [...otherItems, ...reorderedItems].sort((a, b) => {
248
264
  const aGroup = parseInt(a.execution_group?.match(/\d+/)?.[0] || '999');
@@ -255,7 +271,7 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
255
271
  });
256
272
 
257
273
  newQueue.forEach((item, idx) => { item.execution_order = idx + 1; delete item._idx; });
258
- queue.queue = newQueue;
274
+ queue.tasks = newQueue;
259
275
  writeQueue(issuesDir, queue);
260
276
 
261
277
  return { success: true, groupId, reordered: newOrder.length };
@@ -6,7 +6,7 @@
6
6
  // ========== Issue State ==========
7
7
  var issueData = {
8
8
  issues: [],
9
- queue: { queue: [], conflicts: [], execution_groups: [], grouped_items: {} },
9
+ queue: { tasks: [], conflicts: [], execution_groups: [], grouped_items: {} },
10
10
  selectedIssue: null,
11
11
  selectedSolution: null,
12
12
  selectedSolutionIssueId: null,
@@ -65,7 +65,7 @@ async function loadQueueData() {
65
65
  issueData.queue = await response.json();
66
66
  } catch (err) {
67
67
  console.error('Failed to load queue:', err);
68
- issueData.queue = { queue: [], conflicts: [], execution_groups: [], grouped_items: {} };
68
+ issueData.queue = { tasks: [], conflicts: [], execution_groups: [], grouped_items: {} };
69
69
  }
70
70
  }
71
71
 
@@ -360,7 +360,7 @@ function filterIssuesByStatus(status) {
360
360
  // ========== Queue Section ==========
361
361
  function renderQueueSection() {
362
362
  const queue = issueData.queue;
363
- const queueItems = queue.queue || [];
363
+ const queueItems = queue.tasks || [];
364
364
  const metadata = queue._metadata || {};
365
365
 
366
366
  // Check if queue is empty
@@ -530,10 +530,10 @@ function renderQueueItem(item, index, total) {
530
530
  return `
531
531
  <div class="queue-item ${statusColors[item.status] || ''}"
532
532
  draggable="true"
533
- data-queue-id="${item.queue_id}"
533
+ data-item-id="${item.item_id}"
534
534
  data-group-id="${item.execution_group}"
535
- onclick="openQueueItemDetail('${item.queue_id}')">
536
- <span class="queue-item-id font-mono text-xs">${item.queue_id}</span>
535
+ onclick="openQueueItemDetail('${item.item_id}')">
536
+ <span class="queue-item-id font-mono text-xs">${item.item_id}</span>
537
537
  <span class="queue-item-issue text-xs text-muted-foreground">${item.issue_id}</span>
538
538
  <span class="queue-item-task text-sm">${item.task_id}</span>
539
539
  <span class="queue-item-priority" style="opacity: ${item.semantic_priority || 0.5}">
@@ -586,12 +586,12 @@ function handleIssueDragStart(e) {
586
586
  const item = e.target.closest('.queue-item');
587
587
  if (!item) return;
588
588
 
589
- issueDragState.dragging = item.dataset.queueId;
589
+ issueDragState.dragging = item.dataset.itemId;
590
590
  issueDragState.groupId = item.dataset.groupId;
591
591
 
592
592
  item.classList.add('dragging');
593
593
  e.dataTransfer.effectAllowed = 'move';
594
- e.dataTransfer.setData('text/plain', item.dataset.queueId);
594
+ e.dataTransfer.setData('text/plain', item.dataset.itemId);
595
595
  }
596
596
 
597
597
  function handleIssueDragEnd(e) {
@@ -610,7 +610,7 @@ function handleIssueDragOver(e) {
610
610
  e.preventDefault();
611
611
 
612
612
  const target = e.target.closest('.queue-item');
613
- if (!target || target.dataset.queueId === issueDragState.dragging) return;
613
+ if (!target || target.dataset.itemId === issueDragState.dragging) return;
614
614
 
615
615
  // Only allow drag within same group
616
616
  if (target.dataset.groupId !== issueDragState.groupId) {
@@ -635,7 +635,7 @@ function handleIssueDrop(e) {
635
635
 
636
636
  // Get new order
637
637
  const items = Array.from(container.querySelectorAll('.queue-item'));
638
- const draggedItem = items.find(i => i.dataset.queueId === issueDragState.dragging);
638
+ const draggedItem = items.find(i => i.dataset.itemId === issueDragState.dragging);
639
639
  const targetIndex = items.indexOf(target);
640
640
  const draggedIndex = items.indexOf(draggedItem);
641
641
 
@@ -649,7 +649,7 @@ function handleIssueDrop(e) {
649
649
  }
650
650
 
651
651
  // Get new order and save
652
- const newOrder = Array.from(container.querySelectorAll('.queue-item')).map(i => i.dataset.queueId);
652
+ const newOrder = Array.from(container.querySelectorAll('.queue-item')).map(i => i.dataset.itemId);
653
653
  saveQueueOrder(issueDragState.groupId, newOrder);
654
654
  }
655
655
 
@@ -767,7 +767,7 @@ function renderIssueDetailPanel(issue) {
767
767
  <div class="flex items-center justify-between">
768
768
  <span class="font-mono text-sm">${task.id}</span>
769
769
  <select class="task-status-select" onchange="updateTaskStatus('${issue.id}', '${task.id}', this.value)">
770
- ${['pending', 'ready', 'in_progress', 'completed', 'failed', 'paused', 'skipped'].map(s =>
770
+ ${['pending', 'ready', 'executing', 'completed', 'failed', 'blocked', 'paused', 'skipped'].map(s =>
771
771
  `<option value="${s}" ${task.status === s ? 'selected' : ''}>${s}</option>`
772
772
  ).join('')}
773
773
  </select>
@@ -1145,8 +1145,8 @@ function escapeHtml(text) {
1145
1145
  return div.innerHTML;
1146
1146
  }
1147
1147
 
1148
- function openQueueItemDetail(queueId) {
1149
- const item = issueData.queue.queue?.find(q => q.queue_id === queueId);
1148
+ function openQueueItemDetail(itemId) {
1149
+ const item = issueData.queue.tasks?.find(q => q.item_id === itemId);
1150
1150
  if (item) {
1151
1151
  openIssueDetail(item.issue_id);
1152
1152
  }