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