claude-code-workflow 6.3.9 → 6.3.10

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 (29) hide show
  1. package/.claude/CLAUDE.md +1 -1
  2. package/.claude/agents/issue-plan-agent.md +3 -10
  3. package/.claude/agents/issue-queue-agent.md +103 -83
  4. package/.claude/commands/issue/execute.md +195 -363
  5. package/.claude/commands/issue/plan.md +9 -2
  6. package/.claude/commands/issue/queue.md +130 -66
  7. package/.claude/commands/workflow/init.md +75 -29
  8. package/.claude/commands/workflow/lite-fix.md +8 -0
  9. package/.claude/commands/workflow/lite-plan.md +8 -0
  10. package/.claude/commands/workflow/review-module-cycle.md +4 -0
  11. package/.claude/commands/workflow/review-session-cycle.md +4 -0
  12. package/.claude/commands/workflow/review.md +4 -4
  13. package/.claude/commands/workflow/session/solidify.md +299 -0
  14. package/.claude/commands/workflow/session/start.md +10 -7
  15. package/.claude/commands/workflow/tools/context-gather.md +17 -10
  16. package/.claude/workflows/cli-templates/schemas/queue-schema.json +225 -108
  17. package/.claude/workflows/cli-templates/schemas/solution-schema.json +6 -28
  18. package/.claude/workflows/context-tools.md +17 -25
  19. package/.codex/AGENTS.md +10 -5
  20. package/ccw/dist/commands/issue.d.ts.map +1 -1
  21. package/ccw/dist/commands/issue.js +348 -115
  22. package/ccw/dist/commands/issue.js.map +1 -1
  23. package/ccw/src/commands/issue.ts +392 -149
  24. package/ccw/src/templates/dashboard-js/components/cli-status.js +1 -78
  25. package/ccw/src/templates/dashboard-js/i18n.js +0 -4
  26. package/ccw/src/templates/dashboard-js/views/cli-manager.js +0 -18
  27. package/ccw/src/templates/dashboard-js/views/issue-manager.js +57 -26
  28. package/package.json +1 -1
  29. package/.claude/workflows/context-tools-ace.md +0 -105
@@ -26,20 +26,6 @@ interface Issue {
26
26
  context: string;
27
27
  bound_solution_id: string | null;
28
28
  solution_count: number;
29
- source?: string;
30
- source_url?: string;
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
29
  // Timestamps
44
30
  created_at: string;
45
31
  updated_at: string;
@@ -85,16 +71,6 @@ interface SolutionTask {
85
71
 
86
72
  depends_on: string[];
87
73
  estimated_minutes?: number;
88
- executor: 'codex' | 'gemini' | 'agent' | 'auto';
89
-
90
- // Lifecycle status tracking
91
- lifecycle_status?: {
92
- implemented: boolean;
93
- tested: boolean;
94
- regression_passed: boolean;
95
- accepted: boolean;
96
- committed: boolean;
97
- };
98
74
  status?: string;
99
75
  priority?: number;
100
76
  }
@@ -102,6 +78,7 @@ interface SolutionTask {
102
78
  interface Solution {
103
79
  id: string;
104
80
  description?: string;
81
+ approach?: string; // Solution approach description
105
82
  tasks: SolutionTask[];
106
83
  exploration_context?: Record<string, any>;
107
84
  analysis?: { risk?: string; impact?: string; complexity?: string };
@@ -112,17 +89,19 @@ interface Solution {
112
89
  }
113
90
 
114
91
  interface QueueItem {
115
- item_id: string; // Task item ID in queue: T-1, T-2, ... (formerly queue_id)
92
+ item_id: string; // Item ID in queue: T-1, T-2, ... (task-level) or S-1, S-2, ... (solution-level)
116
93
  issue_id: string;
117
94
  solution_id: string;
118
- task_id: string;
119
- title?: string;
95
+ task_id?: string; // Only for task-level queues
120
96
  status: 'pending' | 'ready' | 'executing' | 'completed' | 'failed' | 'blocked';
121
97
  execution_order: number;
122
98
  execution_group: string;
123
99
  depends_on: string[];
124
100
  semantic_priority: number;
125
101
  assigned_executor: 'codex' | 'gemini' | 'agent';
102
+ task_count?: number; // For solution-level queues
103
+ files_touched?: string[]; // For solution-level queues
104
+ queued_at?: string;
126
105
  started_at?: string;
127
106
  completed_at?: string;
128
107
  result?: Record<string, any>;
@@ -131,7 +110,8 @@ interface QueueItem {
131
110
 
132
111
  interface QueueConflict {
133
112
  type: 'file_conflict' | 'dependency_conflict' | 'resource_conflict';
134
- tasks: string[]; // Item IDs involved in conflict
113
+ tasks?: string[]; // Task IDs involved (task-level queues)
114
+ solutions?: string[]; // Solution IDs involved (solution-level queues)
135
115
  file?: string; // Conflicting file path
136
116
  resolution: 'sequential' | 'merge' | 'manual';
137
117
  resolution_order?: string[];
@@ -142,8 +122,10 @@ interface QueueConflict {
142
122
  interface ExecutionGroup {
143
123
  id: string; // Group ID: P1, S1, etc.
144
124
  type: 'parallel' | 'sequential';
145
- task_count: number;
146
- tasks: string[]; // Item IDs in this group
125
+ task_count?: number; // For task-level queues
126
+ solution_count?: number; // For solution-level queues
127
+ tasks?: string[]; // Task IDs in this group (task-level)
128
+ solutions?: string[]; // Solution IDs in this group (solution-level)
147
129
  }
148
130
 
149
131
  interface Queue {
@@ -151,7 +133,8 @@ interface Queue {
151
133
  name?: string; // Optional queue name
152
134
  status: 'active' | 'completed' | 'archived' | 'failed';
153
135
  issue_ids: string[]; // Issues in this queue
154
- tasks: QueueItem[]; // Task items (formerly 'queue')
136
+ tasks: QueueItem[]; // Task items (task-level queue)
137
+ solutions?: QueueItem[]; // Solution items (solution-level queue)
155
138
  conflicts: QueueConflict[];
156
139
  execution_groups?: ExecutionGroup[];
157
140
  _metadata: {
@@ -167,13 +150,14 @@ interface Queue {
167
150
 
168
151
  interface QueueIndex {
169
152
  active_queue_id: string | null;
170
- active_item_id: string | null;
171
153
  queues: {
172
154
  id: string;
173
155
  status: string;
174
156
  issue_ids: string[];
175
- total_tasks: number;
176
- completed_tasks: number;
157
+ total_tasks?: number; // For task-level queues
158
+ total_solutions?: number; // For solution-level queues
159
+ completed_tasks?: number; // For task-level queues
160
+ completed_solutions?: number; // For solution-level queues
177
161
  created_at: string;
178
162
  completed_at?: string;
179
163
  }[];
@@ -308,7 +292,7 @@ function ensureQueuesDir(): void {
308
292
  function readQueueIndex(): QueueIndex {
309
293
  const path = join(getQueuesDir(), 'index.json');
310
294
  if (!existsSync(path)) {
311
- return { active_queue_id: null, active_item_id: null, queues: [] };
295
+ return { active_queue_id: null, queues: [] };
312
296
  }
313
297
  return JSON.parse(readFileSync(path, 'utf-8'));
314
298
  }
@@ -405,12 +389,16 @@ function writeQueue(queue: Queue): void {
405
389
  writeQueueIndex(index);
406
390
  }
407
391
 
408
- function generateQueueItemId(queue: Queue): string {
409
- const maxNum = queue.tasks.reduce((max, q) => {
410
- const match = q.item_id.match(/^T-(\d+)$/);
392
+ function generateQueueItemId(queue: Queue, level: 'solution' | 'task' = 'solution'): string {
393
+ const prefix = level === 'solution' ? 'S' : 'T';
394
+ const items = level === 'solution' ? (queue.solutions || []) : (queue.tasks || []);
395
+ const pattern = new RegExp(`^${prefix}-(\\d+)$`);
396
+
397
+ const maxNum = items.reduce((max, q) => {
398
+ const match = q.item_id.match(pattern);
411
399
  return match ? Math.max(max, parseInt(match[1])) : max;
412
400
  }, 0);
413
- return `T-${maxNum + 1}`;
401
+ return `${prefix}-${maxNum + 1}`;
414
402
  }
415
403
 
416
404
  // ============ Commands ============
@@ -657,7 +645,6 @@ async function taskAction(issueId: string | undefined, taskId: string | undefine
657
645
 
658
646
  if (options.title) solution.tasks[taskIdx].title = options.title;
659
647
  if (options.status) solution.tasks[taskIdx].status = options.status;
660
- if (options.executor) solution.tasks[taskIdx].executor = options.executor as any;
661
648
 
662
649
  writeSolutions(issueId, solutions);
663
650
  console.log(chalk.green(`✓ Task ${taskId} updated`));
@@ -690,8 +677,7 @@ async function taskAction(issueId: string | undefined, taskId: string | undefine
690
677
  scope: 'core',
691
678
  message_template: `feat(core): ${options.title}`
692
679
  },
693
- depends_on: [],
694
- executor: (options.executor as any) || 'auto'
680
+ depends_on: []
695
681
  };
696
682
 
697
683
  solution.tasks.push(newTask);
@@ -845,6 +831,138 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
845
831
  return;
846
832
  }
847
833
 
834
+ // DAG - Return dependency graph for parallel execution planning (solution-level)
835
+ if (subAction === 'dag') {
836
+ const queue = readActiveQueue();
837
+
838
+ // Support both old (tasks) and new (solutions) queue format
839
+ const items = queue.solutions || queue.tasks || [];
840
+ if (!queue.id || items.length === 0) {
841
+ console.log(JSON.stringify({ error: 'No active queue', nodes: [], edges: [], groups: [] }));
842
+ return;
843
+ }
844
+
845
+ // Build DAG nodes (solution-level)
846
+ const completedIds = new Set(items.filter(t => t.status === 'completed').map(t => t.item_id));
847
+ const failedIds = new Set(items.filter(t => t.status === 'failed').map(t => t.item_id));
848
+
849
+ const nodes = items.map(item => ({
850
+ id: item.item_id,
851
+ issue_id: item.issue_id,
852
+ solution_id: item.solution_id,
853
+ status: item.status,
854
+ executor: item.assigned_executor,
855
+ priority: item.semantic_priority,
856
+ depends_on: item.depends_on || [],
857
+ task_count: item.task_count || 1,
858
+ files_touched: item.files_touched || [],
859
+ // Calculate if ready (dependencies satisfied)
860
+ ready: item.status === 'pending' && (item.depends_on || []).every(d => completedIds.has(d)),
861
+ blocked_by: (item.depends_on || []).filter(d => !completedIds.has(d) && !failedIds.has(d))
862
+ }));
863
+
864
+ // Build edges for visualization
865
+ const edges = items.flatMap(item =>
866
+ (item.depends_on || []).map(dep => ({ from: dep, to: item.item_id }))
867
+ );
868
+
869
+ // Group ready items by execution_group
870
+ const readyItems = nodes.filter(n => n.ready || n.status === 'executing');
871
+ const groups: Record<string, string[]> = {};
872
+
873
+ for (const item of items) {
874
+ if (readyItems.some(r => r.id === item.item_id)) {
875
+ const group = item.execution_group || 'P1';
876
+ if (!groups[group]) groups[group] = [];
877
+ groups[group].push(item.item_id);
878
+ }
879
+ }
880
+
881
+ // Calculate parallel batches - prefer execution_groups from queue if available
882
+ const parallelBatches: string[][] = [];
883
+ const readyItemIds = new Set(readyItems.map(t => t.id));
884
+
885
+ // Check if queue has pre-assigned execution_groups
886
+ if (queue.execution_groups && queue.execution_groups.length > 0) {
887
+ // Use agent-assigned execution groups
888
+ for (const group of queue.execution_groups) {
889
+ const groupItems = (group.solutions || group.tasks || [])
890
+ .filter((id: string) => readyItemIds.has(id));
891
+ if (groupItems.length > 0) {
892
+ if (group.type === 'parallel') {
893
+ // All items in parallel group can run together
894
+ parallelBatches.push(groupItems);
895
+ } else {
896
+ // Sequential group: each item is its own batch
897
+ for (const itemId of groupItems) {
898
+ parallelBatches.push([itemId]);
899
+ }
900
+ }
901
+ }
902
+ }
903
+ } else {
904
+ // Fallback: calculate parallel batches from file conflicts
905
+ const remainingReady = new Set(readyItemIds);
906
+
907
+ while (remainingReady.size > 0) {
908
+ const batch: string[] = [];
909
+ const batchFiles = new Set<string>();
910
+
911
+ for (const itemId of Array.from(remainingReady)) {
912
+ const item = items.find(t => t.item_id === itemId);
913
+ if (!item) continue;
914
+
915
+ // Get all files touched by this solution
916
+ let solutionFiles: string[] = item.files_touched || [];
917
+
918
+ // If not in queue item, fetch from solution definition
919
+ if (solutionFiles.length === 0) {
920
+ const solution = findSolution(item.issue_id, item.solution_id);
921
+ if (solution?.tasks) {
922
+ for (const task of solution.tasks) {
923
+ for (const mp of task.modification_points || []) {
924
+ solutionFiles.push(mp.file);
925
+ }
926
+ }
927
+ }
928
+ }
929
+
930
+ const hasConflict = solutionFiles.some(f => batchFiles.has(f));
931
+
932
+ if (!hasConflict) {
933
+ batch.push(itemId);
934
+ solutionFiles.forEach(f => batchFiles.add(f));
935
+ }
936
+ }
937
+
938
+ if (batch.length === 0) {
939
+ // Fallback: take one at a time if all conflict
940
+ const first = Array.from(remainingReady)[0];
941
+ batch.push(first);
942
+ }
943
+
944
+ parallelBatches.push(batch);
945
+ batch.forEach(id => remainingReady.delete(id));
946
+ }
947
+ }
948
+
949
+ console.log(JSON.stringify({
950
+ queue_id: queue.id,
951
+ total: nodes.length,
952
+ ready_count: readyItems.length,
953
+ completed_count: completedIds.size,
954
+ nodes,
955
+ edges,
956
+ groups: Object.entries(groups).map(([id, solutions]) => ({ id, solutions })),
957
+ parallel_batches: parallelBatches,
958
+ _summary: {
959
+ can_parallel: parallelBatches[0]?.length || 0,
960
+ batches_needed: parallelBatches.length
961
+ }
962
+ }, null, 2));
963
+ return;
964
+ }
965
+
848
966
  // Archive current queue
849
967
  if (subAction === 'archive') {
850
968
  const queue = readActiveQueue();
@@ -889,7 +1007,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
889
1007
  return;
890
1008
  }
891
1009
 
892
- // Add issue tasks to queue
1010
+ // Add issue solution to queue (solution-level granularity)
893
1011
  if (subAction === 'add' && issueId) {
894
1012
  const issue = findIssue(issueId);
895
1013
  if (!issue) {
@@ -906,48 +1024,61 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
906
1024
 
907
1025
  // Get or create active queue (create new if current is completed/archived)
908
1026
  let queue = readActiveQueue();
909
- const isNewQueue = queue.tasks.length === 0 || queue.status !== 'active';
1027
+ const items = queue.solutions || [];
1028
+ const isNewQueue = items.length === 0 || queue.status !== 'active';
910
1029
 
911
1030
  if (queue.status !== 'active') {
912
1031
  // Create new queue if current is not active
913
1032
  queue = createEmptyQueue();
914
1033
  }
915
1034
 
1035
+ // Ensure solutions array exists
1036
+ if (!queue.solutions) {
1037
+ queue.solutions = [];
1038
+ }
1039
+
1040
+ // Check if solution already in queue
1041
+ const exists = queue.solutions.some(q => q.issue_id === issueId && q.solution_id === solution.id);
1042
+ if (exists) {
1043
+ console.log(chalk.yellow(`Solution ${solution.id} already in queue`));
1044
+ return;
1045
+ }
1046
+
916
1047
  // Add issue to queue's issue list
917
1048
  if (!queue.issue_ids.includes(issueId)) {
918
1049
  queue.issue_ids.push(issueId);
919
1050
  }
920
1051
 
921
- let added = 0;
922
- for (const task of solution.tasks) {
923
- const exists = queue.tasks.some(q => q.issue_id === issueId && q.task_id === task.id);
924
- if (exists) continue;
925
-
926
- queue.tasks.push({
927
- item_id: generateQueueItemId(queue),
928
- issue_id: issueId,
929
- solution_id: solution.id,
930
- task_id: task.id,
931
- status: 'pending',
932
- execution_order: queue.tasks.length + 1,
933
- execution_group: 'P1',
934
- depends_on: task.depends_on.map(dep => {
935
- const depItem = queue.tasks.find(q => q.task_id === dep && q.issue_id === issueId);
936
- return depItem?.item_id || dep;
937
- }),
938
- semantic_priority: 0.5,
939
- assigned_executor: task.executor === 'auto' ? 'codex' : task.executor as any
940
- });
941
- added++;
1052
+ // Collect all files touched by this solution
1053
+ const filesTouched = new Set<string>();
1054
+ for (const task of solution.tasks || []) {
1055
+ for (const mp of task.modification_points || []) {
1056
+ filesTouched.add(mp.file);
1057
+ }
942
1058
  }
943
1059
 
1060
+ // Create solution-level queue item (S-N)
1061
+ queue.solutions.push({
1062
+ item_id: generateQueueItemId(queue, 'solution'),
1063
+ issue_id: issueId,
1064
+ solution_id: solution.id,
1065
+ status: 'pending',
1066
+ execution_order: queue.solutions.length + 1,
1067
+ execution_group: 'P1',
1068
+ depends_on: [],
1069
+ semantic_priority: 0.5,
1070
+ assigned_executor: 'codex',
1071
+ task_count: solution.tasks?.length || 0,
1072
+ files_touched: Array.from(filesTouched)
1073
+ });
1074
+
944
1075
  writeQueue(queue);
945
1076
  updateIssue(issueId, { status: 'queued', queued_at: new Date().toISOString() });
946
1077
 
947
1078
  if (isNewQueue) {
948
1079
  console.log(chalk.green(`✓ Created queue ${queue.id}`));
949
1080
  }
950
- console.log(chalk.green(`✓ Added ${added} tasks from ${solution.id}`));
1081
+ console.log(chalk.green(`✓ Added solution ${solution.id} (${solution.tasks?.length || 0} tasks) to queue`));
951
1082
  return;
952
1083
  }
953
1084
 
@@ -961,7 +1092,11 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
961
1092
 
962
1093
  console.log(chalk.bold.cyan('\nActive Queue\n'));
963
1094
 
964
- if (!queue.id || queue.tasks.length === 0) {
1095
+ // Support both solution-level and task-level queues
1096
+ const items = queue.solutions || queue.tasks || [];
1097
+ const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
1098
+
1099
+ if (!queue.id || items.length === 0) {
965
1100
  console.log(chalk.yellow('No active queue'));
966
1101
  console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
967
1102
  console.log(chalk.gray('Or list history: ccw issue queue list'));
@@ -970,13 +1105,17 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
970
1105
 
971
1106
  console.log(chalk.gray(`Queue: ${queue.id}`));
972
1107
  console.log(chalk.gray(`Issues: ${queue.issue_ids.join(', ')}`));
973
- console.log(chalk.gray(`Total: ${queue._metadata.total_tasks} | Pending: ${queue._metadata.pending_count} | Executing: ${queue._metadata.executing_count} | Completed: ${queue._metadata.completed_count}`));
1108
+ console.log(chalk.gray(`Total: ${items.length} | Pending: ${items.filter(i => i.status === 'pending').length} | Executing: ${items.filter(i => i.status === 'executing').length} | Completed: ${items.filter(i => i.status === 'completed').length}`));
974
1109
  console.log();
975
1110
 
976
- console.log(chalk.gray('QueueID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
1111
+ if (isSolutionLevel) {
1112
+ console.log(chalk.gray('ItemID'.padEnd(10) + 'Issue'.padEnd(15) + 'Tasks'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
1113
+ } else {
1114
+ console.log(chalk.gray('ItemID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
1115
+ }
977
1116
  console.log(chalk.gray('-'.repeat(60)));
978
1117
 
979
- for (const item of queue.tasks) {
1118
+ for (const item of items) {
980
1119
  const statusColor = {
981
1120
  'pending': chalk.gray,
982
1121
  'ready': chalk.cyan,
@@ -986,10 +1125,14 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
986
1125
  'blocked': chalk.magenta
987
1126
  }[item.status] || chalk.white;
988
1127
 
1128
+ const thirdCol = isSolutionLevel
1129
+ ? String(item.task_count || 0).padEnd(8)
1130
+ : (item.task_id || '-').padEnd(8);
1131
+
989
1132
  console.log(
990
1133
  item.item_id.padEnd(10) +
991
1134
  item.issue_id.substring(0, 13).padEnd(15) +
992
- item.task_id.padEnd(8) +
1135
+ thirdCol +
993
1136
  statusColor(item.status.padEnd(12)) +
994
1137
  item.assigned_executor
995
1138
  );
@@ -998,78 +1141,111 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
998
1141
 
999
1142
  /**
1000
1143
  * next - Get next ready task for execution (JSON output)
1144
+ * Accepts optional item_id to fetch a specific task directly
1001
1145
  */
1002
- async function nextAction(options: IssueOptions): Promise<void> {
1146
+ async function nextAction(itemId: string | undefined, options: IssueOptions): Promise<void> {
1003
1147
  const queue = readActiveQueue();
1004
-
1005
- // Priority 1: Resume executing tasks (interrupted/crashed)
1006
- const executingTasks = queue.tasks.filter(item => item.status === 'executing');
1007
-
1008
- // Priority 2: Find pending tasks with satisfied dependencies
1009
- const pendingTasks = queue.tasks.filter(item => {
1010
- if (item.status !== 'pending') return false;
1011
- return item.depends_on.every(depId => {
1012
- const dep = queue.tasks.find(q => q.item_id === depId);
1013
- return !dep || dep.status === 'completed';
1148
+ // Support both old (tasks) and new (solutions) queue format
1149
+ const items = queue.solutions || queue.tasks || [];
1150
+ let nextItem: typeof items[0] | undefined;
1151
+ let isResume = false;
1152
+
1153
+ // If specific item_id provided, fetch that item directly
1154
+ if (itemId) {
1155
+ nextItem = items.find(t => t.item_id === itemId);
1156
+ if (!nextItem) {
1157
+ console.log(JSON.stringify({ status: 'error', message: `Item ${itemId} not found` }));
1158
+ return;
1159
+ }
1160
+ if (nextItem.status === 'completed') {
1161
+ console.log(JSON.stringify({ status: 'completed', message: `Item ${itemId} already completed` }));
1162
+ return;
1163
+ }
1164
+ if (nextItem.status === 'failed') {
1165
+ console.log(JSON.stringify({ status: 'failed', message: `Item ${itemId} failed, use retry to reset` }));
1166
+ return;
1167
+ }
1168
+ isResume = nextItem.status === 'executing';
1169
+ } else {
1170
+ // Auto-select: Priority 1 - executing, Priority 2 - ready pending
1171
+ const executingItems = items.filter(item => item.status === 'executing');
1172
+ const pendingItems = items.filter(item => {
1173
+ if (item.status !== 'pending') return false;
1174
+ return (item.depends_on || []).every(depId => {
1175
+ const dep = items.find(q => q.item_id === depId);
1176
+ return !dep || dep.status === 'completed';
1177
+ });
1014
1178
  });
1015
- });
1016
1179
 
1017
- // Combine: executing first, then pending
1018
- const readyTasks = [...executingTasks, ...pendingTasks];
1180
+ const readyItems = [...executingItems, ...pendingItems];
1019
1181
 
1020
- if (readyTasks.length === 0) {
1021
- console.log(JSON.stringify({
1022
- status: 'empty',
1023
- message: 'No ready tasks',
1024
- queue_status: queue._metadata
1025
- }, null, 2));
1026
- return;
1027
- }
1182
+ if (readyItems.length === 0) {
1183
+ console.log(JSON.stringify({
1184
+ status: 'empty',
1185
+ message: 'No ready items',
1186
+ queue_status: queue._metadata
1187
+ }, null, 2));
1188
+ return;
1189
+ }
1028
1190
 
1029
- // Sort by execution order
1030
- readyTasks.sort((a, b) => a.execution_order - b.execution_order);
1031
- const nextItem = readyTasks[0];
1032
- const isResume = nextItem.status === 'executing';
1191
+ readyItems.sort((a, b) => a.execution_order - b.execution_order);
1192
+ nextItem = readyItems[0];
1193
+ isResume = nextItem.status === 'executing';
1194
+ }
1033
1195
 
1034
- // Load task definition
1196
+ // Load FULL solution with all tasks
1035
1197
  const solution = findSolution(nextItem.issue_id, nextItem.solution_id);
1036
- const taskDef = solution?.tasks.find(t => t.id === nextItem.task_id);
1037
1198
 
1038
- if (!taskDef) {
1039
- console.log(JSON.stringify({ status: 'error', message: 'Task definition not found' }));
1199
+ if (!solution) {
1200
+ console.log(JSON.stringify({ status: 'error', message: 'Solution not found' }));
1040
1201
  process.exit(1);
1041
1202
  }
1042
1203
 
1043
- // Only update status if not already executing (new task)
1204
+ // Only update status if not already executing
1044
1205
  if (!isResume) {
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();
1206
+ const idx = items.findIndex(q => q.item_id === nextItem.item_id);
1207
+ items[idx].status = 'executing';
1208
+ items[idx].started_at = new Date().toISOString();
1209
+ // Write back to correct array
1210
+ if (queue.solutions) {
1211
+ queue.solutions = items;
1212
+ } else {
1213
+ queue.tasks = items;
1214
+ }
1048
1215
  writeQueue(queue);
1049
1216
  updateIssue(nextItem.issue_id, { status: 'executing' });
1050
1217
  }
1051
1218
 
1052
- // Calculate queue stats for context
1219
+ // Calculate queue stats
1053
1220
  const stats = {
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,
1057
- executing: executingTasks.length,
1058
- pending: pendingTasks.length
1221
+ total: items.length,
1222
+ completed: items.filter(q => q.status === 'completed').length,
1223
+ failed: items.filter(q => q.status === 'failed').length,
1224
+ executing: items.filter(q => q.status === 'executing').length,
1225
+ pending: items.filter(q => q.status === 'pending').length
1059
1226
  };
1060
1227
  const remaining = stats.pending + stats.executing;
1061
1228
 
1229
+ // Calculate total estimated time for all tasks
1230
+ const totalMinutes = solution.tasks?.reduce((sum, t) => sum + (t.estimated_minutes || 30), 0) || 30;
1231
+
1062
1232
  console.log(JSON.stringify({
1063
1233
  item_id: nextItem.item_id,
1064
1234
  issue_id: nextItem.issue_id,
1065
1235
  solution_id: nextItem.solution_id,
1066
- task: taskDef,
1067
- context: solution?.exploration_context || {},
1236
+ // Return full solution object with all tasks
1237
+ solution: {
1238
+ id: solution.id,
1239
+ approach: solution.approach,
1240
+ tasks: solution.tasks || [],
1241
+ exploration_context: solution.exploration_context || {}
1242
+ },
1068
1243
  resumed: isResume,
1069
- resume_note: isResume ? `Resuming interrupted task (started: ${nextItem.started_at})` : undefined,
1244
+ resume_note: isResume ? `Resuming interrupted item (started: ${nextItem.started_at})` : undefined,
1070
1245
  execution_hints: {
1071
1246
  executor: nextItem.assigned_executor,
1072
- estimated_minutes: taskDef.estimated_minutes || 30
1247
+ task_count: solution.tasks?.length || 0,
1248
+ estimated_minutes: totalMinutes
1073
1249
  },
1074
1250
  queue_progress: {
1075
1251
  completed: stats.completed,
@@ -1080,18 +1256,72 @@ async function nextAction(options: IssueOptions): Promise<void> {
1080
1256
  }, null, 2));
1081
1257
  }
1082
1258
 
1259
+ /**
1260
+ * detail - Get task details by item_id (READ-ONLY, does NOT change status)
1261
+ * Used for parallel execution: orchestrator gets dag, then dispatches with detail <id>
1262
+ */
1263
+ async function detailAction(itemId: string | undefined, options: IssueOptions): Promise<void> {
1264
+ if (!itemId) {
1265
+ console.log(JSON.stringify({ status: 'error', message: 'item_id is required' }));
1266
+ return;
1267
+ }
1268
+
1269
+ const queue = readActiveQueue();
1270
+ // Support both old (tasks) and new (solutions) queue format
1271
+ const items = queue.solutions || queue.tasks || [];
1272
+ const queueItem = items.find(t => t.item_id === itemId);
1273
+
1274
+ if (!queueItem) {
1275
+ console.log(JSON.stringify({ status: 'error', message: `Item ${itemId} not found` }));
1276
+ return;
1277
+ }
1278
+
1279
+ // Load FULL solution with all tasks
1280
+ const solution = findSolution(queueItem.issue_id, queueItem.solution_id);
1281
+
1282
+ if (!solution) {
1283
+ console.log(JSON.stringify({ status: 'error', message: 'Solution not found' }));
1284
+ return;
1285
+ }
1286
+
1287
+ // Calculate total estimated time for all tasks
1288
+ const totalMinutes = solution.tasks?.reduce((sum, t) => sum + (t.estimated_minutes || 30), 0) || 30;
1289
+
1290
+ // Return FULL SOLUTION with all tasks (READ-ONLY - no status update)
1291
+ console.log(JSON.stringify({
1292
+ item_id: queueItem.item_id,
1293
+ issue_id: queueItem.issue_id,
1294
+ solution_id: queueItem.solution_id,
1295
+ status: queueItem.status,
1296
+ // Return full solution object with all tasks
1297
+ solution: {
1298
+ id: solution.id,
1299
+ approach: solution.approach,
1300
+ tasks: solution.tasks || [],
1301
+ exploration_context: solution.exploration_context || {}
1302
+ },
1303
+ execution_hints: {
1304
+ executor: queueItem.assigned_executor,
1305
+ task_count: solution.tasks?.length || 0,
1306
+ estimated_minutes: totalMinutes
1307
+ }
1308
+ }, null, 2));
1309
+ }
1310
+
1083
1311
  /**
1084
1312
  * done - Mark task completed or failed
1085
1313
  */
1086
1314
  async function doneAction(queueId: string | undefined, options: IssueOptions): Promise<void> {
1087
1315
  if (!queueId) {
1088
- console.error(chalk.red('Queue ID is required'));
1089
- console.error(chalk.gray('Usage: ccw issue done <queue-id> [--fail] [--reason "..."]'));
1316
+ console.error(chalk.red('Item ID is required'));
1317
+ console.error(chalk.gray('Usage: ccw issue done <item-id> [--fail] [--reason "..."]'));
1090
1318
  process.exit(1);
1091
1319
  }
1092
1320
 
1093
1321
  const queue = readActiveQueue();
1094
- const idx = queue.tasks.findIndex(q => q.item_id === queueId);
1322
+ // Support both old (tasks) and new (solutions) queue format
1323
+ const items = queue.solutions || queue.tasks || [];
1324
+ const idx = items.findIndex(q => q.item_id === queueId);
1095
1325
 
1096
1326
  if (idx === -1) {
1097
1327
  console.error(chalk.red(`Queue item "${queueId}" not found`));
@@ -1099,66 +1329,69 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
1099
1329
  }
1100
1330
 
1101
1331
  const isFail = options.fail;
1102
- queue.tasks[idx].status = isFail ? 'failed' : 'completed';
1103
- queue.tasks[idx].completed_at = new Date().toISOString();
1332
+ items[idx].status = isFail ? 'failed' : 'completed';
1333
+ items[idx].completed_at = new Date().toISOString();
1104
1334
 
1105
1335
  if (isFail) {
1106
- queue.tasks[idx].failure_reason = options.reason || 'Unknown failure';
1336
+ items[idx].failure_reason = options.reason || 'Unknown failure';
1107
1337
  } else if (options.result) {
1108
1338
  try {
1109
- queue.tasks[idx].result = JSON.parse(options.result);
1339
+ items[idx].result = JSON.parse(options.result);
1110
1340
  } catch {
1111
1341
  console.warn(chalk.yellow('Warning: Could not parse result JSON'));
1112
1342
  }
1113
1343
  }
1114
1344
 
1115
- // Check if all issue tasks are complete
1116
- const issueId = queue.tasks[idx].issue_id;
1117
- const issueTasks = queue.tasks.filter(q => q.issue_id === issueId);
1118
- const allIssueComplete = issueTasks.every(q => q.status === 'completed');
1119
- const anyIssueFailed = issueTasks.some(q => q.status === 'failed');
1345
+ // Update issue status (solution = issue in new model)
1346
+ const issueId = items[idx].issue_id;
1120
1347
 
1121
- if (allIssueComplete) {
1122
- updateIssue(issueId, { status: 'completed', completed_at: new Date().toISOString() });
1123
- console.log(chalk.green(`✓ ${queueId} completed`));
1124
- console.log(chalk.green(`✓ Issue ${issueId} completed (all tasks done)`));
1125
- } else if (anyIssueFailed) {
1348
+ if (isFail) {
1126
1349
  updateIssue(issueId, { status: 'failed' });
1127
1350
  console.log(chalk.red(`✗ ${queueId} failed`));
1128
1351
  } else {
1129
- console.log(isFail ? chalk.red(`✗ ${queueId} failed`) : chalk.green(`✓ ${queueId} completed`));
1352
+ updateIssue(issueId, { status: 'completed', completed_at: new Date().toISOString() });
1353
+ console.log(chalk.green(`✓ ${queueId} completed`));
1354
+ console.log(chalk.green(`✓ Issue ${issueId} completed`));
1130
1355
  }
1131
1356
 
1132
1357
  // Check if entire queue is complete
1133
- const allQueueComplete = queue.tasks.every(q => q.status === 'completed');
1134
- const anyQueueFailed = queue.tasks.some(q => q.status === 'failed');
1358
+ const allQueueComplete = items.every(q => q.status === 'completed');
1359
+ const anyQueueFailed = items.some(q => q.status === 'failed');
1135
1360
 
1136
1361
  if (allQueueComplete) {
1137
1362
  queue.status = 'completed';
1138
- console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all tasks done)`));
1139
- } else if (anyQueueFailed && queue.tasks.every(q => q.status === 'completed' || q.status === 'failed')) {
1363
+ console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all solutions done)`));
1364
+ } else if (anyQueueFailed && items.every(q => q.status === 'completed' || q.status === 'failed')) {
1140
1365
  queue.status = 'failed';
1141
- console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed tasks`));
1366
+ console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed solutions`));
1142
1367
  }
1143
1368
 
1369
+ // Write back to queue (update the correct array)
1370
+ if (queue.solutions) {
1371
+ queue.solutions = items;
1372
+ } else {
1373
+ queue.tasks = items;
1374
+ }
1144
1375
  writeQueue(queue);
1145
1376
  }
1146
1377
 
1147
1378
  /**
1148
- * retry - Reset failed tasks to pending for re-execution
1379
+ * retry - Reset failed items to pending for re-execution
1149
1380
  */
1150
1381
  async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
1151
1382
  const queue = readActiveQueue();
1383
+ // Support both old (tasks) and new (solutions) queue format
1384
+ const items = queue.solutions || queue.tasks || [];
1152
1385
 
1153
- if (!queue.id || queue.tasks.length === 0) {
1386
+ if (!queue.id || items.length === 0) {
1154
1387
  console.log(chalk.yellow('No active queue'));
1155
1388
  return;
1156
1389
  }
1157
1390
 
1158
1391
  let updated = 0;
1159
1392
 
1160
- for (const item of queue.tasks) {
1161
- // Retry failed tasks only
1393
+ for (const item of items) {
1394
+ // Retry failed items only
1162
1395
  if (item.status === 'failed') {
1163
1396
  if (!issueId || item.issue_id === issueId) {
1164
1397
  item.status = 'pending';
@@ -1171,8 +1404,7 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
1171
1404
  }
1172
1405
 
1173
1406
  if (updated === 0) {
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"'));
1407
+ console.log(chalk.yellow('No failed items to retry'));
1176
1408
  return;
1177
1409
  }
1178
1410
 
@@ -1181,13 +1413,19 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
1181
1413
  queue.status = 'active';
1182
1414
  }
1183
1415
 
1416
+ // Write back to queue
1417
+ if (queue.solutions) {
1418
+ queue.solutions = items;
1419
+ } else {
1420
+ queue.tasks = items;
1421
+ }
1184
1422
  writeQueue(queue);
1185
1423
 
1186
1424
  if (issueId) {
1187
1425
  updateIssue(issueId, { status: 'queued' });
1188
1426
  }
1189
1427
 
1190
- console.log(chalk.green(`✓ Reset ${updated} task(s) to pending`));
1428
+ console.log(chalk.green(`✓ Reset ${updated} item(s) to pending`));
1191
1429
  }
1192
1430
 
1193
1431
  // ============ Main Entry ============
@@ -1219,7 +1457,10 @@ export async function issueCommand(
1219
1457
  await queueAction(argsArray[0], argsArray[1], options);
1220
1458
  break;
1221
1459
  case 'next':
1222
- await nextAction(options);
1460
+ await nextAction(argsArray[0], options);
1461
+ break;
1462
+ case 'detail':
1463
+ await detailAction(argsArray[0], options);
1223
1464
  break;
1224
1465
  case 'done':
1225
1466
  await doneAction(argsArray[0], options);
@@ -1252,14 +1493,16 @@ export async function issueCommand(
1252
1493
  console.log(chalk.gray(' queue list List all queues (history)'));
1253
1494
  console.log(chalk.gray(' queue add <issue-id> Add issue to active queue (or create new)'));
1254
1495
  console.log(chalk.gray(' queue switch <queue-id> Switch active queue'));
1496
+ console.log(chalk.gray(' queue dag Get dependency graph (JSON) for parallel execution'));
1255
1497
  console.log(chalk.gray(' queue archive Archive current queue'));
1256
1498
  console.log(chalk.gray(' queue delete <queue-id> Delete queue from history'));
1257
1499
  console.log(chalk.gray(' retry [issue-id] Retry failed tasks'));
1258
1500
  console.log();
1259
1501
  console.log(chalk.bold('Execution Endpoints:'));
1260
- console.log(chalk.gray(' next Get next ready task (JSON)'));
1261
- console.log(chalk.gray(' done <queue-id> Mark task completed'));
1262
- console.log(chalk.gray(' done <queue-id> --fail Mark task failed'));
1502
+ console.log(chalk.gray(' next [item-id] Get & mark task executing (JSON)'));
1503
+ console.log(chalk.gray(' detail <item-id> Get task details (READ-ONLY, for parallel)'));
1504
+ console.log(chalk.gray(' done <item-id> Mark task completed'));
1505
+ console.log(chalk.gray(' done <item-id> --fail Mark task failed'));
1263
1506
  console.log();
1264
1507
  console.log(chalk.bold('Options:'));
1265
1508
  console.log(chalk.gray(' --title <title> Issue/task title'));