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.
- package/.claude/CLAUDE.md +1 -1
- package/.claude/agents/issue-plan-agent.md +21 -15
- package/.claude/agents/issue-queue-agent.md +114 -87
- package/.claude/commands/issue/discover.md +427 -0
- package/.claude/commands/issue/execute.md +195 -363
- package/.claude/commands/issue/new.md +13 -1
- package/.claude/commands/issue/plan.md +55 -32
- package/.claude/commands/issue/queue.md +145 -71
- package/.claude/commands/workflow/init.md +75 -29
- package/.claude/commands/workflow/lite-fix.md +8 -0
- package/.claude/commands/workflow/lite-plan.md +8 -0
- package/.claude/commands/workflow/review-module-cycle.md +4 -0
- package/.claude/commands/workflow/review-session-cycle.md +4 -0
- package/.claude/commands/workflow/review.md +4 -4
- package/.claude/commands/workflow/session/solidify.md +299 -0
- package/.claude/commands/workflow/session/start.md +10 -7
- package/.claude/commands/workflow/tools/context-gather.md +17 -10
- package/.claude/skills/software-manual/SKILL.md +184 -0
- package/.claude/skills/software-manual/phases/01-requirements-discovery.md +162 -0
- package/.claude/skills/software-manual/phases/02-project-exploration.md +101 -0
- package/.claude/skills/software-manual/phases/02.5-api-extraction.md +161 -0
- package/.claude/skills/software-manual/phases/03-parallel-analysis.md +183 -0
- package/.claude/skills/software-manual/phases/03.5-consolidation.md +82 -0
- package/.claude/skills/software-manual/phases/04-screenshot-capture.md +89 -0
- package/.claude/skills/software-manual/phases/05-html-assembly.md +132 -0
- package/.claude/skills/software-manual/phases/06-iterative-refinement.md +259 -0
- package/.claude/skills/software-manual/scripts/api-extractor.md +245 -0
- package/.claude/skills/software-manual/scripts/bundle-libraries.md +85 -0
- package/.claude/skills/software-manual/scripts/extract_apis.py +270 -0
- package/.claude/skills/software-manual/scripts/screenshot-helper.md +447 -0
- package/.claude/skills/software-manual/scripts/swagger-runner.md +419 -0
- package/.claude/skills/software-manual/scripts/typedoc-runner.md +357 -0
- package/.claude/skills/software-manual/specs/html-template.md +325 -0
- package/.claude/skills/software-manual/specs/quality-standards.md +253 -0
- package/.claude/skills/software-manual/specs/writing-style.md +298 -0
- package/.claude/skills/software-manual/templates/css/wiki-base.css +788 -0
- package/.claude/skills/software-manual/templates/css/wiki-dark.css +278 -0
- package/.claude/skills/software-manual/templates/tiddlywiki-shell.html +327 -0
- package/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json +219 -0
- package/.claude/workflows/cli-templates/schemas/discovery-state-schema.json +125 -0
- package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +168 -74
- package/.claude/workflows/cli-templates/schemas/queue-schema.json +225 -108
- package/.claude/workflows/cli-templates/schemas/solution-schema.json +6 -28
- package/.claude/workflows/context-tools.md +17 -25
- package/.codex/AGENTS.md +10 -5
- package/.codex/prompts/issue-execute.md +174 -84
- package/.codex/prompts/issue-plan.md +106 -0
- package/.codex/prompts/issue-queue.md +225 -0
- package/ccw/dist/cli.d.ts.map +1 -1
- package/ccw/dist/cli.js +1 -0
- package/ccw/dist/cli.js.map +1 -1
- package/ccw/dist/commands/issue.d.ts.map +1 -1
- package/ccw/dist/commands/issue.js +443 -123
- package/ccw/dist/commands/issue.js.map +1 -1
- package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
- package/ccw/dist/core/dashboard-generator.js +4 -1
- package/ccw/dist/core/dashboard-generator.js.map +1 -1
- package/ccw/dist/core/data-aggregator.d.ts +32 -0
- package/ccw/dist/core/data-aggregator.d.ts.map +1 -1
- package/ccw/dist/core/data-aggregator.js +55 -11
- package/ccw/dist/core/data-aggregator.js.map +1 -1
- package/ccw/dist/core/routes/discovery-routes.d.ts +37 -0
- package/ccw/dist/core/routes/discovery-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/discovery-routes.js +514 -0
- package/ccw/dist/core/routes/discovery-routes.js.map +1 -0
- package/ccw/dist/core/server.d.ts.map +1 -1
- package/ccw/dist/core/server.js +9 -1
- package/ccw/dist/core/server.js.map +1 -1
- package/ccw/dist/tools/codex-lens.d.ts +12 -1
- package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
- package/ccw/dist/tools/codex-lens.js +56 -7
- package/ccw/dist/tools/codex-lens.js.map +1 -1
- package/ccw/src/cli.ts +1 -0
- package/ccw/src/commands/issue.ts +498 -158
- package/ccw/src/core/dashboard-generator.ts +4 -1
- package/ccw/src/core/data-aggregator.ts +94 -11
- package/ccw/src/core/routes/discovery-routes.ts +607 -0
- package/ccw/src/core/server.ts +9 -1
- package/ccw/src/templates/dashboard-css/34-discovery.css +783 -0
- package/ccw/src/templates/dashboard-js/components/cli-status.js +1 -78
- package/ccw/src/templates/dashboard-js/components/navigation.js +8 -0
- package/ccw/src/templates/dashboard-js/i18n.js +140 -4
- package/ccw/src/templates/dashboard-js/views/cli-manager.js +0 -18
- package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +13 -3
- package/ccw/src/templates/dashboard-js/views/issue-discovery.js +730 -0
- package/ccw/src/templates/dashboard-js/views/issue-manager.js +57 -26
- package/ccw/src/templates/dashboard-js/views/project-overview.js +153 -0
- package/ccw/src/templates/dashboard.html +5 -0
- package/ccw/src/tools/codex-lens.ts +75 -9
- package/package.json +1 -1
- 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; //
|
|
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
|
|
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
|
|
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
|
|
146
|
-
|
|
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 (
|
|
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
|
|
176
|
-
|
|
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,
|
|
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 =
|
|
371
|
-
queue._metadata.pending_count =
|
|
372
|
-
queue._metadata.executing_count =
|
|
373
|
-
queue._metadata.completed_count =
|
|
374
|
-
queue._metadata.failed_count =
|
|
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
|
-
|
|
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:
|
|
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
|
|
410
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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 ${
|
|
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
|
-
|
|
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: ${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
1018
|
-
const readyTasks = [...executingTasks, ...pendingTasks];
|
|
1273
|
+
const readyItems = [...executingItems, ...pendingItems];
|
|
1019
1274
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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
|
|
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 (!
|
|
1039
|
-
console.log(JSON.stringify({ status: 'error', message: '
|
|
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
|
|
1297
|
+
// Only update status if not already executing
|
|
1044
1298
|
if (!isResume) {
|
|
1045
|
-
const idx =
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
|
1312
|
+
// Calculate queue stats
|
|
1053
1313
|
const stats = {
|
|
1054
|
-
total:
|
|
1055
|
-
completed:
|
|
1056
|
-
failed:
|
|
1057
|
-
executing:
|
|
1058
|
-
pending:
|
|
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
|
-
|
|
1067
|
-
|
|
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
|
|
1337
|
+
resume_note: isResume ? `Resuming interrupted item (started: ${nextItem.started_at})` : undefined,
|
|
1070
1338
|
execution_hints: {
|
|
1071
1339
|
executor: nextItem.assigned_executor,
|
|
1072
|
-
|
|
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('
|
|
1089
|
-
console.error(chalk.gray('Usage: ccw issue done <
|
|
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
|
-
|
|
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
|
-
|
|
1103
|
-
|
|
1425
|
+
items[idx].status = isFail ? 'failed' : 'completed';
|
|
1426
|
+
items[idx].completed_at = new Date().toISOString();
|
|
1104
1427
|
|
|
1105
1428
|
if (isFail) {
|
|
1106
|
-
|
|
1429
|
+
items[idx].failure_reason = options.reason || 'Unknown failure';
|
|
1107
1430
|
} else if (options.result) {
|
|
1108
1431
|
try {
|
|
1109
|
-
|
|
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
|
-
//
|
|
1116
|
-
const issueId =
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
1134
|
-
const anyQueueFailed =
|
|
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
|
|
1139
|
-
} else if (anyQueueFailed &&
|
|
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
|
|
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
|
|
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 ||
|
|
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
|
|
1161
|
-
// Retry failed
|
|
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
|
|
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}
|
|
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
|
|
1261
|
-
console.log(chalk.gray('
|
|
1262
|
-
console.log(chalk.gray(' done <
|
|
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'));
|