claude-code-workflow 6.3.9 → 6.3.11

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 (91) hide show
  1. package/.claude/CLAUDE.md +1 -1
  2. package/.claude/agents/issue-plan-agent.md +21 -15
  3. package/.claude/agents/issue-queue-agent.md +114 -87
  4. package/.claude/commands/issue/discover.md +427 -0
  5. package/.claude/commands/issue/execute.md +195 -363
  6. package/.claude/commands/issue/new.md +13 -1
  7. package/.claude/commands/issue/plan.md +55 -32
  8. package/.claude/commands/issue/queue.md +145 -71
  9. package/.claude/commands/workflow/init.md +75 -29
  10. package/.claude/commands/workflow/lite-fix.md +8 -0
  11. package/.claude/commands/workflow/lite-plan.md +8 -0
  12. package/.claude/commands/workflow/review-module-cycle.md +4 -0
  13. package/.claude/commands/workflow/review-session-cycle.md +4 -0
  14. package/.claude/commands/workflow/review.md +4 -4
  15. package/.claude/commands/workflow/session/solidify.md +299 -0
  16. package/.claude/commands/workflow/session/start.md +10 -7
  17. package/.claude/commands/workflow/tools/context-gather.md +17 -10
  18. package/.claude/skills/software-manual/SKILL.md +184 -0
  19. package/.claude/skills/software-manual/phases/01-requirements-discovery.md +162 -0
  20. package/.claude/skills/software-manual/phases/02-project-exploration.md +101 -0
  21. package/.claude/skills/software-manual/phases/02.5-api-extraction.md +161 -0
  22. package/.claude/skills/software-manual/phases/03-parallel-analysis.md +183 -0
  23. package/.claude/skills/software-manual/phases/03.5-consolidation.md +82 -0
  24. package/.claude/skills/software-manual/phases/04-screenshot-capture.md +89 -0
  25. package/.claude/skills/software-manual/phases/05-html-assembly.md +132 -0
  26. package/.claude/skills/software-manual/phases/06-iterative-refinement.md +259 -0
  27. package/.claude/skills/software-manual/scripts/api-extractor.md +245 -0
  28. package/.claude/skills/software-manual/scripts/bundle-libraries.md +85 -0
  29. package/.claude/skills/software-manual/scripts/extract_apis.py +270 -0
  30. package/.claude/skills/software-manual/scripts/screenshot-helper.md +447 -0
  31. package/.claude/skills/software-manual/scripts/swagger-runner.md +419 -0
  32. package/.claude/skills/software-manual/scripts/typedoc-runner.md +357 -0
  33. package/.claude/skills/software-manual/specs/html-template.md +325 -0
  34. package/.claude/skills/software-manual/specs/quality-standards.md +253 -0
  35. package/.claude/skills/software-manual/specs/writing-style.md +298 -0
  36. package/.claude/skills/software-manual/templates/css/wiki-base.css +788 -0
  37. package/.claude/skills/software-manual/templates/css/wiki-dark.css +278 -0
  38. package/.claude/skills/software-manual/templates/tiddlywiki-shell.html +327 -0
  39. package/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json +219 -0
  40. package/.claude/workflows/cli-templates/schemas/discovery-state-schema.json +125 -0
  41. package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +168 -74
  42. package/.claude/workflows/cli-templates/schemas/queue-schema.json +225 -108
  43. package/.claude/workflows/cli-templates/schemas/solution-schema.json +6 -28
  44. package/.claude/workflows/context-tools.md +17 -25
  45. package/.codex/AGENTS.md +10 -5
  46. package/.codex/prompts/issue-execute.md +174 -84
  47. package/.codex/prompts/issue-plan.md +106 -0
  48. package/.codex/prompts/issue-queue.md +225 -0
  49. package/ccw/dist/cli.d.ts.map +1 -1
  50. package/ccw/dist/cli.js +1 -0
  51. package/ccw/dist/cli.js.map +1 -1
  52. package/ccw/dist/commands/issue.d.ts.map +1 -1
  53. package/ccw/dist/commands/issue.js +443 -123
  54. package/ccw/dist/commands/issue.js.map +1 -1
  55. package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
  56. package/ccw/dist/core/dashboard-generator.js +4 -1
  57. package/ccw/dist/core/dashboard-generator.js.map +1 -1
  58. package/ccw/dist/core/data-aggregator.d.ts +32 -0
  59. package/ccw/dist/core/data-aggregator.d.ts.map +1 -1
  60. package/ccw/dist/core/data-aggregator.js +55 -11
  61. package/ccw/dist/core/data-aggregator.js.map +1 -1
  62. package/ccw/dist/core/routes/discovery-routes.d.ts +37 -0
  63. package/ccw/dist/core/routes/discovery-routes.d.ts.map +1 -0
  64. package/ccw/dist/core/routes/discovery-routes.js +514 -0
  65. package/ccw/dist/core/routes/discovery-routes.js.map +1 -0
  66. package/ccw/dist/core/server.d.ts.map +1 -1
  67. package/ccw/dist/core/server.js +9 -1
  68. package/ccw/dist/core/server.js.map +1 -1
  69. package/ccw/dist/tools/codex-lens.d.ts +12 -1
  70. package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
  71. package/ccw/dist/tools/codex-lens.js +56 -7
  72. package/ccw/dist/tools/codex-lens.js.map +1 -1
  73. package/ccw/src/cli.ts +1 -0
  74. package/ccw/src/commands/issue.ts +498 -158
  75. package/ccw/src/core/dashboard-generator.ts +4 -1
  76. package/ccw/src/core/data-aggregator.ts +94 -11
  77. package/ccw/src/core/routes/discovery-routes.ts +607 -0
  78. package/ccw/src/core/server.ts +9 -1
  79. package/ccw/src/templates/dashboard-css/34-discovery.css +783 -0
  80. package/ccw/src/templates/dashboard-js/components/cli-status.js +1 -78
  81. package/ccw/src/templates/dashboard-js/components/navigation.js +8 -0
  82. package/ccw/src/templates/dashboard-js/i18n.js +140 -4
  83. package/ccw/src/templates/dashboard-js/views/cli-manager.js +0 -18
  84. package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +13 -3
  85. package/ccw/src/templates/dashboard-js/views/issue-discovery.js +730 -0
  86. package/ccw/src/templates/dashboard-js/views/issue-manager.js +57 -26
  87. package/ccw/src/templates/dashboard-js/views/project-overview.js +153 -0
  88. package/ccw/src/templates/dashboard.html +5 -0
  89. package/ccw/src/tools/codex-lens.ts +75 -9
  90. package/package.json +1 -1
  91. 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
  }
@@ -366,12 +350,30 @@ function createEmptyQueue(): Queue {
366
350
  function writeQueue(queue: Queue): void {
367
351
  ensureQueuesDir();
368
352
 
353
+ // Support both old (tasks) and new (solutions) queue format
354
+ const items = queue.solutions || queue.tasks || [];
355
+ const isSolutionQueue = !!queue.solutions;
356
+
357
+ // Ensure _metadata exists (support queues with 'metadata' field from external sources)
358
+ if (!queue._metadata) {
359
+ const extMeta = (queue as any).metadata;
360
+ queue._metadata = {
361
+ version: '2.0',
362
+ total_tasks: extMeta?.total_tasks || items.length,
363
+ pending_count: items.filter(q => q.status === 'pending').length,
364
+ executing_count: items.filter(q => q.status === 'executing').length,
365
+ completed_count: items.filter(q => q.status === 'completed').length,
366
+ failed_count: items.filter(q => q.status === 'failed').length,
367
+ updated_at: new Date().toISOString()
368
+ };
369
+ }
370
+
369
371
  // Update metadata counts
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;
372
+ queue._metadata.total_tasks = items.length;
373
+ queue._metadata.pending_count = items.filter(q => q.status === 'pending').length;
374
+ queue._metadata.executing_count = items.filter(q => q.status === 'executing').length;
375
+ queue._metadata.completed_count = items.filter(q => q.status === 'completed').length;
376
+ queue._metadata.failed_count = items.filter(q => q.status === 'failed').length;
375
377
  queue._metadata.updated_at = new Date().toISOString();
376
378
 
377
379
  // Write queue file
@@ -382,16 +384,28 @@ function writeQueue(queue: Queue): void {
382
384
  const index = readQueueIndex();
383
385
  const existingIdx = index.queues.findIndex(q => q.id === queue.id);
384
386
 
385
- const indexEntry = {
387
+ // Derive issue_ids from solutions if not present
388
+ const issueIds = queue.issue_ids || (isSolutionQueue
389
+ ? [...new Set(items.map(item => item.issue_id))]
390
+ : []);
391
+
392
+ const indexEntry: QueueIndex['queues'][0] = {
386
393
  id: queue.id,
387
394
  status: queue.status,
388
- issue_ids: queue.issue_ids,
389
- total_tasks: queue._metadata.total_tasks,
390
- completed_tasks: queue._metadata.completed_count,
395
+ issue_ids: issueIds,
391
396
  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
392
397
  completed_at: queue.status === 'completed' ? new Date().toISOString() : undefined
393
398
  };
394
399
 
400
+ // Add format-specific counts
401
+ if (isSolutionQueue) {
402
+ indexEntry.total_solutions = items.length;
403
+ indexEntry.completed_solutions = queue._metadata.completed_count;
404
+ } else {
405
+ indexEntry.total_tasks = items.length;
406
+ indexEntry.completed_tasks = queue._metadata.completed_count;
407
+ }
408
+
395
409
  if (existingIdx >= 0) {
396
410
  index.queues[existingIdx] = indexEntry;
397
411
  } else {
@@ -405,12 +419,16 @@ function writeQueue(queue: Queue): void {
405
419
  writeQueueIndex(index);
406
420
  }
407
421
 
408
- function generateQueueItemId(queue: Queue): string {
409
- const maxNum = queue.tasks.reduce((max, q) => {
410
- const match = q.item_id.match(/^T-(\d+)$/);
422
+ function generateQueueItemId(queue: Queue, level: 'solution' | 'task' = 'solution'): string {
423
+ const prefix = level === 'solution' ? 'S' : 'T';
424
+ const items = level === 'solution' ? (queue.solutions || []) : (queue.tasks || []);
425
+ const pattern = new RegExp(`^${prefix}-(\\d+)$`);
426
+
427
+ const maxNum = items.reduce((max, q) => {
428
+ const match = q.item_id.match(pattern);
411
429
  return match ? Math.max(max, parseInt(match[1])) : max;
412
430
  }, 0);
413
- return `T-${maxNum + 1}`;
431
+ return `${prefix}-${maxNum + 1}`;
414
432
  }
415
433
 
416
434
  // ============ Commands ============
@@ -657,7 +675,6 @@ async function taskAction(issueId: string | undefined, taskId: string | undefine
657
675
 
658
676
  if (options.title) solution.tasks[taskIdx].title = options.title;
659
677
  if (options.status) solution.tasks[taskIdx].status = options.status;
660
- if (options.executor) solution.tasks[taskIdx].executor = options.executor as any;
661
678
 
662
679
  writeSolutions(issueId, solutions);
663
680
  console.log(chalk.green(`✓ Task ${taskId} updated`));
@@ -690,8 +707,7 @@ async function taskAction(issueId: string | undefined, taskId: string | undefine
690
707
  scope: 'core',
691
708
  message_template: `feat(core): ${options.title}`
692
709
  },
693
- depends_on: [],
694
- executor: (options.executor as any) || 'auto'
710
+ depends_on: []
695
711
  };
696
712
 
697
713
  solution.tasks.push(newTask);
@@ -700,6 +716,69 @@ async function taskAction(issueId: string | undefined, taskId: string | undefine
700
716
  }
701
717
  }
702
718
 
719
+ /**
720
+ * update - Update issue fields (status, priority, title, etc.)
721
+ */
722
+ async function updateAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
723
+ if (!issueId) {
724
+ console.error(chalk.red('Issue ID is required'));
725
+ console.error(chalk.gray('Usage: ccw issue update <issue-id> --status <status> [--priority <n>] [--title "..."]'));
726
+ process.exit(1);
727
+ }
728
+
729
+ const issue = findIssue(issueId);
730
+ if (!issue) {
731
+ console.error(chalk.red(`Issue "${issueId}" not found`));
732
+ process.exit(1);
733
+ }
734
+
735
+ const updates: Partial<Issue> = {};
736
+
737
+ if (options.status) {
738
+ const validStatuses = ['registered', 'planning', 'planned', 'queued', 'executing', 'completed', 'failed', 'paused'];
739
+ if (!validStatuses.includes(options.status)) {
740
+ console.error(chalk.red(`Invalid status: ${options.status}`));
741
+ console.error(chalk.gray(`Valid: ${validStatuses.join(', ')}`));
742
+ process.exit(1);
743
+ }
744
+ updates.status = options.status as Issue['status'];
745
+
746
+ // Auto-set timestamps based on status
747
+ if (options.status === 'planned') updates.planned_at = new Date().toISOString();
748
+ if (options.status === 'queued') updates.queued_at = new Date().toISOString();
749
+ if (options.status === 'completed') updates.completed_at = new Date().toISOString();
750
+ }
751
+
752
+ if (options.priority) {
753
+ updates.priority = parseInt(options.priority);
754
+ }
755
+
756
+ if (options.title) {
757
+ updates.title = options.title;
758
+ }
759
+
760
+ if (options.description) {
761
+ updates.context = options.description;
762
+ }
763
+
764
+ if (Object.keys(updates).length === 0) {
765
+ console.error(chalk.yellow('No updates specified'));
766
+ console.error(chalk.gray('Use --status, --priority, --title, or --description'));
767
+ return;
768
+ }
769
+
770
+ updateIssue(issueId, updates);
771
+
772
+ if (options.json) {
773
+ console.log(JSON.stringify({ success: true, issue_id: issueId, updates }));
774
+ } else {
775
+ console.log(chalk.green(`✓ Issue "${issueId}" updated`));
776
+ Object.entries(updates).forEach(([k, v]) => {
777
+ console.log(chalk.gray(` ${k}: ${v}`));
778
+ });
779
+ }
780
+ }
781
+
703
782
  /**
704
783
  * bind - Register and/or bind a solution
705
784
  */
@@ -845,6 +924,138 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
845
924
  return;
846
925
  }
847
926
 
927
+ // DAG - Return dependency graph for parallel execution planning (solution-level)
928
+ if (subAction === 'dag') {
929
+ const queue = readActiveQueue();
930
+
931
+ // Support both old (tasks) and new (solutions) queue format
932
+ const items = queue.solutions || queue.tasks || [];
933
+ if (!queue.id || items.length === 0) {
934
+ console.log(JSON.stringify({ error: 'No active queue', nodes: [], edges: [], groups: [] }));
935
+ return;
936
+ }
937
+
938
+ // Build DAG nodes (solution-level)
939
+ const completedIds = new Set(items.filter(t => t.status === 'completed').map(t => t.item_id));
940
+ const failedIds = new Set(items.filter(t => t.status === 'failed').map(t => t.item_id));
941
+
942
+ const nodes = items.map(item => ({
943
+ id: item.item_id,
944
+ issue_id: item.issue_id,
945
+ solution_id: item.solution_id,
946
+ status: item.status,
947
+ executor: item.assigned_executor,
948
+ priority: item.semantic_priority,
949
+ depends_on: item.depends_on || [],
950
+ task_count: item.task_count || 1,
951
+ files_touched: item.files_touched || [],
952
+ // Calculate if ready (dependencies satisfied)
953
+ ready: item.status === 'pending' && (item.depends_on || []).every(d => completedIds.has(d)),
954
+ blocked_by: (item.depends_on || []).filter(d => !completedIds.has(d) && !failedIds.has(d))
955
+ }));
956
+
957
+ // Build edges for visualization
958
+ const edges = items.flatMap(item =>
959
+ (item.depends_on || []).map(dep => ({ from: dep, to: item.item_id }))
960
+ );
961
+
962
+ // Group ready items by execution_group
963
+ const readyItems = nodes.filter(n => n.ready || n.status === 'executing');
964
+ const groups: Record<string, string[]> = {};
965
+
966
+ for (const item of items) {
967
+ if (readyItems.some(r => r.id === item.item_id)) {
968
+ const group = item.execution_group || 'P1';
969
+ if (!groups[group]) groups[group] = [];
970
+ groups[group].push(item.item_id);
971
+ }
972
+ }
973
+
974
+ // Calculate parallel batches - prefer execution_groups from queue if available
975
+ const parallelBatches: string[][] = [];
976
+ const readyItemIds = new Set(readyItems.map(t => t.id));
977
+
978
+ // Check if queue has pre-assigned execution_groups
979
+ if (queue.execution_groups && queue.execution_groups.length > 0) {
980
+ // Use agent-assigned execution groups
981
+ for (const group of queue.execution_groups) {
982
+ const groupItems = (group.solutions || group.tasks || [])
983
+ .filter((id: string) => readyItemIds.has(id));
984
+ if (groupItems.length > 0) {
985
+ if (group.type === 'parallel') {
986
+ // All items in parallel group can run together
987
+ parallelBatches.push(groupItems);
988
+ } else {
989
+ // Sequential group: each item is its own batch
990
+ for (const itemId of groupItems) {
991
+ parallelBatches.push([itemId]);
992
+ }
993
+ }
994
+ }
995
+ }
996
+ } else {
997
+ // Fallback: calculate parallel batches from file conflicts
998
+ const remainingReady = new Set(readyItemIds);
999
+
1000
+ while (remainingReady.size > 0) {
1001
+ const batch: string[] = [];
1002
+ const batchFiles = new Set<string>();
1003
+
1004
+ for (const itemId of Array.from(remainingReady)) {
1005
+ const item = items.find(t => t.item_id === itemId);
1006
+ if (!item) continue;
1007
+
1008
+ // Get all files touched by this solution
1009
+ let solutionFiles: string[] = item.files_touched || [];
1010
+
1011
+ // If not in queue item, fetch from solution definition
1012
+ if (solutionFiles.length === 0) {
1013
+ const solution = findSolution(item.issue_id, item.solution_id);
1014
+ if (solution?.tasks) {
1015
+ for (const task of solution.tasks) {
1016
+ for (const mp of task.modification_points || []) {
1017
+ solutionFiles.push(mp.file);
1018
+ }
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+ const hasConflict = solutionFiles.some(f => batchFiles.has(f));
1024
+
1025
+ if (!hasConflict) {
1026
+ batch.push(itemId);
1027
+ solutionFiles.forEach(f => batchFiles.add(f));
1028
+ }
1029
+ }
1030
+
1031
+ if (batch.length === 0) {
1032
+ // Fallback: take one at a time if all conflict
1033
+ const first = Array.from(remainingReady)[0];
1034
+ batch.push(first);
1035
+ }
1036
+
1037
+ parallelBatches.push(batch);
1038
+ batch.forEach(id => remainingReady.delete(id));
1039
+ }
1040
+ }
1041
+
1042
+ console.log(JSON.stringify({
1043
+ queue_id: queue.id,
1044
+ total: nodes.length,
1045
+ ready_count: readyItems.length,
1046
+ completed_count: completedIds.size,
1047
+ nodes,
1048
+ edges,
1049
+ groups: Object.entries(groups).map(([id, solutions]) => ({ id, solutions })),
1050
+ parallel_batches: parallelBatches,
1051
+ _summary: {
1052
+ can_parallel: parallelBatches[0]?.length || 0,
1053
+ batches_needed: parallelBatches.length
1054
+ }
1055
+ }, null, 2));
1056
+ return;
1057
+ }
1058
+
848
1059
  // Archive current queue
849
1060
  if (subAction === 'archive') {
850
1061
  const queue = readActiveQueue();
@@ -889,7 +1100,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
889
1100
  return;
890
1101
  }
891
1102
 
892
- // Add issue tasks to queue
1103
+ // Add issue solution to queue (solution-level granularity)
893
1104
  if (subAction === 'add' && issueId) {
894
1105
  const issue = findIssue(issueId);
895
1106
  if (!issue) {
@@ -906,48 +1117,61 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
906
1117
 
907
1118
  // Get or create active queue (create new if current is completed/archived)
908
1119
  let queue = readActiveQueue();
909
- const isNewQueue = queue.tasks.length === 0 || queue.status !== 'active';
1120
+ const items = queue.solutions || [];
1121
+ const isNewQueue = items.length === 0 || queue.status !== 'active';
910
1122
 
911
1123
  if (queue.status !== 'active') {
912
1124
  // Create new queue if current is not active
913
1125
  queue = createEmptyQueue();
914
1126
  }
915
1127
 
1128
+ // Ensure solutions array exists
1129
+ if (!queue.solutions) {
1130
+ queue.solutions = [];
1131
+ }
1132
+
1133
+ // Check if solution already in queue
1134
+ const exists = queue.solutions.some(q => q.issue_id === issueId && q.solution_id === solution.id);
1135
+ if (exists) {
1136
+ console.log(chalk.yellow(`Solution ${solution.id} already in queue`));
1137
+ return;
1138
+ }
1139
+
916
1140
  // Add issue to queue's issue list
917
1141
  if (!queue.issue_ids.includes(issueId)) {
918
1142
  queue.issue_ids.push(issueId);
919
1143
  }
920
1144
 
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++;
1145
+ // Collect all files touched by this solution
1146
+ const filesTouched = new Set<string>();
1147
+ for (const task of solution.tasks || []) {
1148
+ for (const mp of task.modification_points || []) {
1149
+ filesTouched.add(mp.file);
1150
+ }
942
1151
  }
943
1152
 
1153
+ // Create solution-level queue item (S-N)
1154
+ queue.solutions.push({
1155
+ item_id: generateQueueItemId(queue, 'solution'),
1156
+ issue_id: issueId,
1157
+ solution_id: solution.id,
1158
+ status: 'pending',
1159
+ execution_order: queue.solutions.length + 1,
1160
+ execution_group: 'P1',
1161
+ depends_on: [],
1162
+ semantic_priority: 0.5,
1163
+ assigned_executor: 'codex',
1164
+ task_count: solution.tasks?.length || 0,
1165
+ files_touched: Array.from(filesTouched)
1166
+ });
1167
+
944
1168
  writeQueue(queue);
945
1169
  updateIssue(issueId, { status: 'queued', queued_at: new Date().toISOString() });
946
1170
 
947
1171
  if (isNewQueue) {
948
1172
  console.log(chalk.green(`✓ Created queue ${queue.id}`));
949
1173
  }
950
- console.log(chalk.green(`✓ Added ${added} tasks from ${solution.id}`));
1174
+ console.log(chalk.green(`✓ Added solution ${solution.id} (${solution.tasks?.length || 0} tasks) to queue`));
951
1175
  return;
952
1176
  }
953
1177
 
@@ -961,7 +1185,11 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
961
1185
 
962
1186
  console.log(chalk.bold.cyan('\nActive Queue\n'));
963
1187
 
964
- if (!queue.id || queue.tasks.length === 0) {
1188
+ // Support both solution-level and task-level queues
1189
+ const items = queue.solutions || queue.tasks || [];
1190
+ const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
1191
+
1192
+ if (!queue.id || items.length === 0) {
965
1193
  console.log(chalk.yellow('No active queue'));
966
1194
  console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
967
1195
  console.log(chalk.gray('Or list history: ccw issue queue list'));
@@ -970,13 +1198,17 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
970
1198
 
971
1199
  console.log(chalk.gray(`Queue: ${queue.id}`));
972
1200
  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}`));
1201
+ 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
1202
  console.log();
975
1203
 
976
- console.log(chalk.gray('QueueID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
1204
+ if (isSolutionLevel) {
1205
+ console.log(chalk.gray('ItemID'.padEnd(10) + 'Issue'.padEnd(15) + 'Tasks'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
1206
+ } else {
1207
+ console.log(chalk.gray('ItemID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
1208
+ }
977
1209
  console.log(chalk.gray('-'.repeat(60)));
978
1210
 
979
- for (const item of queue.tasks) {
1211
+ for (const item of items) {
980
1212
  const statusColor = {
981
1213
  'pending': chalk.gray,
982
1214
  'ready': chalk.cyan,
@@ -986,10 +1218,14 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
986
1218
  'blocked': chalk.magenta
987
1219
  }[item.status] || chalk.white;
988
1220
 
1221
+ const thirdCol = isSolutionLevel
1222
+ ? String(item.task_count || 0).padEnd(8)
1223
+ : (item.task_id || '-').padEnd(8);
1224
+
989
1225
  console.log(
990
1226
  item.item_id.padEnd(10) +
991
1227
  item.issue_id.substring(0, 13).padEnd(15) +
992
- item.task_id.padEnd(8) +
1228
+ thirdCol +
993
1229
  statusColor(item.status.padEnd(12)) +
994
1230
  item.assigned_executor
995
1231
  );
@@ -998,78 +1234,111 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
998
1234
 
999
1235
  /**
1000
1236
  * next - Get next ready task for execution (JSON output)
1237
+ * Accepts optional item_id to fetch a specific task directly
1001
1238
  */
1002
- async function nextAction(options: IssueOptions): Promise<void> {
1239
+ async function nextAction(itemId: string | undefined, options: IssueOptions): Promise<void> {
1003
1240
  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';
1241
+ // Support both old (tasks) and new (solutions) queue format
1242
+ const items = queue.solutions || queue.tasks || [];
1243
+ let nextItem: typeof items[0] | undefined;
1244
+ let isResume = false;
1245
+
1246
+ // If specific item_id provided, fetch that item directly
1247
+ if (itemId) {
1248
+ nextItem = items.find(t => t.item_id === itemId);
1249
+ if (!nextItem) {
1250
+ console.log(JSON.stringify({ status: 'error', message: `Item ${itemId} not found` }));
1251
+ return;
1252
+ }
1253
+ if (nextItem.status === 'completed') {
1254
+ console.log(JSON.stringify({ status: 'completed', message: `Item ${itemId} already completed` }));
1255
+ return;
1256
+ }
1257
+ if (nextItem.status === 'failed') {
1258
+ console.log(JSON.stringify({ status: 'failed', message: `Item ${itemId} failed, use retry to reset` }));
1259
+ return;
1260
+ }
1261
+ isResume = nextItem.status === 'executing';
1262
+ } else {
1263
+ // Auto-select: Priority 1 - executing, Priority 2 - ready pending
1264
+ const executingItems = items.filter(item => item.status === 'executing');
1265
+ const pendingItems = items.filter(item => {
1266
+ if (item.status !== 'pending') return false;
1267
+ return (item.depends_on || []).every(depId => {
1268
+ const dep = items.find(q => q.item_id === depId);
1269
+ return !dep || dep.status === 'completed';
1270
+ });
1014
1271
  });
1015
- });
1016
1272
 
1017
- // Combine: executing first, then pending
1018
- const readyTasks = [...executingTasks, ...pendingTasks];
1273
+ const readyItems = [...executingItems, ...pendingItems];
1019
1274
 
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
- }
1275
+ if (readyItems.length === 0) {
1276
+ console.log(JSON.stringify({
1277
+ status: 'empty',
1278
+ message: 'No ready items',
1279
+ queue_status: queue._metadata
1280
+ }, null, 2));
1281
+ return;
1282
+ }
1028
1283
 
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';
1284
+ readyItems.sort((a, b) => a.execution_order - b.execution_order);
1285
+ nextItem = readyItems[0];
1286
+ isResume = nextItem.status === 'executing';
1287
+ }
1033
1288
 
1034
- // Load task definition
1289
+ // Load FULL solution with all tasks
1035
1290
  const solution = findSolution(nextItem.issue_id, nextItem.solution_id);
1036
- const taskDef = solution?.tasks.find(t => t.id === nextItem.task_id);
1037
1291
 
1038
- if (!taskDef) {
1039
- console.log(JSON.stringify({ status: 'error', message: 'Task definition not found' }));
1292
+ if (!solution) {
1293
+ console.log(JSON.stringify({ status: 'error', message: 'Solution not found' }));
1040
1294
  process.exit(1);
1041
1295
  }
1042
1296
 
1043
- // Only update status if not already executing (new task)
1297
+ // Only update status if not already executing
1044
1298
  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();
1299
+ const idx = items.findIndex(q => q.item_id === nextItem.item_id);
1300
+ items[idx].status = 'executing';
1301
+ items[idx].started_at = new Date().toISOString();
1302
+ // Write back to correct array
1303
+ if (queue.solutions) {
1304
+ queue.solutions = items;
1305
+ } else {
1306
+ queue.tasks = items;
1307
+ }
1048
1308
  writeQueue(queue);
1049
1309
  updateIssue(nextItem.issue_id, { status: 'executing' });
1050
1310
  }
1051
1311
 
1052
- // Calculate queue stats for context
1312
+ // Calculate queue stats
1053
1313
  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
1314
+ total: items.length,
1315
+ completed: items.filter(q => q.status === 'completed').length,
1316
+ failed: items.filter(q => q.status === 'failed').length,
1317
+ executing: items.filter(q => q.status === 'executing').length,
1318
+ pending: items.filter(q => q.status === 'pending').length
1059
1319
  };
1060
1320
  const remaining = stats.pending + stats.executing;
1061
1321
 
1322
+ // Calculate total estimated time for all tasks
1323
+ const totalMinutes = solution.tasks?.reduce((sum, t) => sum + (t.estimated_minutes || 30), 0) || 30;
1324
+
1062
1325
  console.log(JSON.stringify({
1063
1326
  item_id: nextItem.item_id,
1064
1327
  issue_id: nextItem.issue_id,
1065
1328
  solution_id: nextItem.solution_id,
1066
- task: taskDef,
1067
- context: solution?.exploration_context || {},
1329
+ // Return full solution object with all tasks
1330
+ solution: {
1331
+ id: solution.id,
1332
+ approach: solution.approach,
1333
+ tasks: solution.tasks || [],
1334
+ exploration_context: solution.exploration_context || {}
1335
+ },
1068
1336
  resumed: isResume,
1069
- resume_note: isResume ? `Resuming interrupted task (started: ${nextItem.started_at})` : undefined,
1337
+ resume_note: isResume ? `Resuming interrupted item (started: ${nextItem.started_at})` : undefined,
1070
1338
  execution_hints: {
1071
1339
  executor: nextItem.assigned_executor,
1072
- estimated_minutes: taskDef.estimated_minutes || 30
1340
+ task_count: solution.tasks?.length || 0,
1341
+ estimated_minutes: totalMinutes
1073
1342
  },
1074
1343
  queue_progress: {
1075
1344
  completed: stats.completed,
@@ -1080,18 +1349,72 @@ async function nextAction(options: IssueOptions): Promise<void> {
1080
1349
  }, null, 2));
1081
1350
  }
1082
1351
 
1352
+ /**
1353
+ * detail - Get task details by item_id (READ-ONLY, does NOT change status)
1354
+ * Used for parallel execution: orchestrator gets dag, then dispatches with detail <id>
1355
+ */
1356
+ async function detailAction(itemId: string | undefined, options: IssueOptions): Promise<void> {
1357
+ if (!itemId) {
1358
+ console.log(JSON.stringify({ status: 'error', message: 'item_id is required' }));
1359
+ return;
1360
+ }
1361
+
1362
+ const queue = readActiveQueue();
1363
+ // Support both old (tasks) and new (solutions) queue format
1364
+ const items = queue.solutions || queue.tasks || [];
1365
+ const queueItem = items.find(t => t.item_id === itemId);
1366
+
1367
+ if (!queueItem) {
1368
+ console.log(JSON.stringify({ status: 'error', message: `Item ${itemId} not found` }));
1369
+ return;
1370
+ }
1371
+
1372
+ // Load FULL solution with all tasks
1373
+ const solution = findSolution(queueItem.issue_id, queueItem.solution_id);
1374
+
1375
+ if (!solution) {
1376
+ console.log(JSON.stringify({ status: 'error', message: 'Solution not found' }));
1377
+ return;
1378
+ }
1379
+
1380
+ // Calculate total estimated time for all tasks
1381
+ const totalMinutes = solution.tasks?.reduce((sum, t) => sum + (t.estimated_minutes || 30), 0) || 30;
1382
+
1383
+ // Return FULL SOLUTION with all tasks (READ-ONLY - no status update)
1384
+ console.log(JSON.stringify({
1385
+ item_id: queueItem.item_id,
1386
+ issue_id: queueItem.issue_id,
1387
+ solution_id: queueItem.solution_id,
1388
+ status: queueItem.status,
1389
+ // Return full solution object with all tasks
1390
+ solution: {
1391
+ id: solution.id,
1392
+ approach: solution.approach,
1393
+ tasks: solution.tasks || [],
1394
+ exploration_context: solution.exploration_context || {}
1395
+ },
1396
+ execution_hints: {
1397
+ executor: queueItem.assigned_executor,
1398
+ task_count: solution.tasks?.length || 0,
1399
+ estimated_minutes: totalMinutes
1400
+ }
1401
+ }, null, 2));
1402
+ }
1403
+
1083
1404
  /**
1084
1405
  * done - Mark task completed or failed
1085
1406
  */
1086
1407
  async function doneAction(queueId: string | undefined, options: IssueOptions): Promise<void> {
1087
1408
  if (!queueId) {
1088
- console.error(chalk.red('Queue ID is required'));
1089
- console.error(chalk.gray('Usage: ccw issue done <queue-id> [--fail] [--reason "..."]'));
1409
+ console.error(chalk.red('Item ID is required'));
1410
+ console.error(chalk.gray('Usage: ccw issue done <item-id> [--fail] [--reason "..."]'));
1090
1411
  process.exit(1);
1091
1412
  }
1092
1413
 
1093
1414
  const queue = readActiveQueue();
1094
- const idx = queue.tasks.findIndex(q => q.item_id === queueId);
1415
+ // Support both old (tasks) and new (solutions) queue format
1416
+ const items = queue.solutions || queue.tasks || [];
1417
+ const idx = items.findIndex(q => q.item_id === queueId);
1095
1418
 
1096
1419
  if (idx === -1) {
1097
1420
  console.error(chalk.red(`Queue item "${queueId}" not found`));
@@ -1099,66 +1422,69 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
1099
1422
  }
1100
1423
 
1101
1424
  const isFail = options.fail;
1102
- queue.tasks[idx].status = isFail ? 'failed' : 'completed';
1103
- queue.tasks[idx].completed_at = new Date().toISOString();
1425
+ items[idx].status = isFail ? 'failed' : 'completed';
1426
+ items[idx].completed_at = new Date().toISOString();
1104
1427
 
1105
1428
  if (isFail) {
1106
- queue.tasks[idx].failure_reason = options.reason || 'Unknown failure';
1429
+ items[idx].failure_reason = options.reason || 'Unknown failure';
1107
1430
  } else if (options.result) {
1108
1431
  try {
1109
- queue.tasks[idx].result = JSON.parse(options.result);
1432
+ items[idx].result = JSON.parse(options.result);
1110
1433
  } catch {
1111
1434
  console.warn(chalk.yellow('Warning: Could not parse result JSON'));
1112
1435
  }
1113
1436
  }
1114
1437
 
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');
1438
+ // Update issue status (solution = issue in new model)
1439
+ const issueId = items[idx].issue_id;
1120
1440
 
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) {
1441
+ if (isFail) {
1126
1442
  updateIssue(issueId, { status: 'failed' });
1127
1443
  console.log(chalk.red(`✗ ${queueId} failed`));
1128
1444
  } else {
1129
- console.log(isFail ? chalk.red(`✗ ${queueId} failed`) : chalk.green(`✓ ${queueId} completed`));
1445
+ updateIssue(issueId, { status: 'completed', completed_at: new Date().toISOString() });
1446
+ console.log(chalk.green(`✓ ${queueId} completed`));
1447
+ console.log(chalk.green(`✓ Issue ${issueId} completed`));
1130
1448
  }
1131
1449
 
1132
1450
  // 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');
1451
+ const allQueueComplete = items.every(q => q.status === 'completed');
1452
+ const anyQueueFailed = items.some(q => q.status === 'failed');
1135
1453
 
1136
1454
  if (allQueueComplete) {
1137
1455
  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')) {
1456
+ console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all solutions done)`));
1457
+ } else if (anyQueueFailed && items.every(q => q.status === 'completed' || q.status === 'failed')) {
1140
1458
  queue.status = 'failed';
1141
- console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed tasks`));
1459
+ console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed solutions`));
1142
1460
  }
1143
1461
 
1462
+ // Write back to queue (update the correct array)
1463
+ if (queue.solutions) {
1464
+ queue.solutions = items;
1465
+ } else {
1466
+ queue.tasks = items;
1467
+ }
1144
1468
  writeQueue(queue);
1145
1469
  }
1146
1470
 
1147
1471
  /**
1148
- * retry - Reset failed tasks to pending for re-execution
1472
+ * retry - Reset failed items to pending for re-execution
1149
1473
  */
1150
1474
  async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
1151
1475
  const queue = readActiveQueue();
1476
+ // Support both old (tasks) and new (solutions) queue format
1477
+ const items = queue.solutions || queue.tasks || [];
1152
1478
 
1153
- if (!queue.id || queue.tasks.length === 0) {
1479
+ if (!queue.id || items.length === 0) {
1154
1480
  console.log(chalk.yellow('No active queue'));
1155
1481
  return;
1156
1482
  }
1157
1483
 
1158
1484
  let updated = 0;
1159
1485
 
1160
- for (const item of queue.tasks) {
1161
- // Retry failed tasks only
1486
+ for (const item of items) {
1487
+ // Retry failed items only
1162
1488
  if (item.status === 'failed') {
1163
1489
  if (!issueId || item.issue_id === issueId) {
1164
1490
  item.status = 'pending';
@@ -1171,8 +1497,7 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
1171
1497
  }
1172
1498
 
1173
1499
  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"'));
1500
+ console.log(chalk.yellow('No failed items to retry'));
1176
1501
  return;
1177
1502
  }
1178
1503
 
@@ -1181,13 +1506,19 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
1181
1506
  queue.status = 'active';
1182
1507
  }
1183
1508
 
1509
+ // Write back to queue
1510
+ if (queue.solutions) {
1511
+ queue.solutions = items;
1512
+ } else {
1513
+ queue.tasks = items;
1514
+ }
1184
1515
  writeQueue(queue);
1185
1516
 
1186
1517
  if (issueId) {
1187
1518
  updateIssue(issueId, { status: 'queued' });
1188
1519
  }
1189
1520
 
1190
- console.log(chalk.green(`✓ Reset ${updated} task(s) to pending`));
1521
+ console.log(chalk.green(`✓ Reset ${updated} item(s) to pending`));
1191
1522
  }
1192
1523
 
1193
1524
  // ============ Main Entry ============
@@ -1215,11 +1546,17 @@ export async function issueCommand(
1215
1546
  case 'bind':
1216
1547
  await bindAction(argsArray[0], argsArray[1], options);
1217
1548
  break;
1549
+ case 'update':
1550
+ await updateAction(argsArray[0], options);
1551
+ break;
1218
1552
  case 'queue':
1219
1553
  await queueAction(argsArray[0], argsArray[1], options);
1220
1554
  break;
1221
1555
  case 'next':
1222
- await nextAction(options);
1556
+ await nextAction(argsArray[0], options);
1557
+ break;
1558
+ case 'detail':
1559
+ await detailAction(argsArray[0], options);
1223
1560
  break;
1224
1561
  case 'done':
1225
1562
  await doneAction(argsArray[0], options);
@@ -1246,20 +1583,23 @@ export async function issueCommand(
1246
1583
  console.log(chalk.gray(' status [issue-id] Show detailed status'));
1247
1584
  console.log(chalk.gray(' task <issue-id> [task-id] Add or update task'));
1248
1585
  console.log(chalk.gray(' bind <issue-id> [sol-id] Bind solution (--solution <path> to register)'));
1586
+ console.log(chalk.gray(' update <issue-id> Update issue (--status, --priority, --title)'));
1249
1587
  console.log();
1250
1588
  console.log(chalk.bold('Queue Commands:'));
1251
1589
  console.log(chalk.gray(' queue Show active queue'));
1252
1590
  console.log(chalk.gray(' queue list List all queues (history)'));
1253
1591
  console.log(chalk.gray(' queue add <issue-id> Add issue to active queue (or create new)'));
1254
1592
  console.log(chalk.gray(' queue switch <queue-id> Switch active queue'));
1593
+ console.log(chalk.gray(' queue dag Get dependency graph (JSON) for parallel execution'));
1255
1594
  console.log(chalk.gray(' queue archive Archive current queue'));
1256
1595
  console.log(chalk.gray(' queue delete <queue-id> Delete queue from history'));
1257
1596
  console.log(chalk.gray(' retry [issue-id] Retry failed tasks'));
1258
1597
  console.log();
1259
1598
  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'));
1599
+ console.log(chalk.gray(' next [item-id] Get & mark task executing (JSON)'));
1600
+ console.log(chalk.gray(' detail <item-id> Get task details (READ-ONLY, for parallel)'));
1601
+ console.log(chalk.gray(' done <item-id> Mark task completed'));
1602
+ console.log(chalk.gray(' done <item-id> --fail Mark task failed'));
1263
1603
  console.log();
1264
1604
  console.log(chalk.bold('Options:'));
1265
1605
  console.log(chalk.gray(' --title <title> Issue/task title'));