claude-code-workflow 6.3.7 → 6.3.9
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 +166 -755
- package/.claude/agents/issue-queue-agent.md +143 -618
- 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 +132 -288
- package/.claude/commands/issue/queue.md +99 -159
- package/.claude/skills/issue-manage/SKILL.md +244 -0
- package/.claude/workflows/cli-templates/schemas/queue-schema.json +3 -3
- package/.claude/workflows/cli-templates/schemas/solution-schema.json +70 -3
- 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 +98 -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 +130 -78
- package/ccw/src/core/routes/issue-routes.ts +110 -18
- package/ccw/src/templates/dashboard-css/32-issue-manager.css +310 -1
- package/ccw/src/templates/dashboard-js/views/issue-manager.js +266 -14
- package/package.json +1 -1
- package/.claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json +0 -136
- package/.claude/workflows/cli-templates/schemas/solutions-jsonl-schema.json +0 -125
|
@@ -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,31 +112,48 @@ 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>;
|
|
117
129
|
failure_reason?: string;
|
|
118
130
|
}
|
|
119
131
|
|
|
132
|
+
interface QueueConflict {
|
|
133
|
+
type: 'file_conflict' | 'dependency_conflict' | 'resource_conflict';
|
|
134
|
+
tasks: string[]; // Item IDs involved in conflict
|
|
135
|
+
file?: string; // Conflicting file path
|
|
136
|
+
resolution: 'sequential' | 'merge' | 'manual';
|
|
137
|
+
resolution_order?: string[];
|
|
138
|
+
rationale?: string;
|
|
139
|
+
resolved: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface ExecutionGroup {
|
|
143
|
+
id: string; // Group ID: P1, S1, etc.
|
|
144
|
+
type: 'parallel' | 'sequential';
|
|
145
|
+
task_count: number;
|
|
146
|
+
tasks: string[]; // Item IDs in this group
|
|
147
|
+
}
|
|
148
|
+
|
|
120
149
|
interface Queue {
|
|
121
|
-
id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS
|
|
150
|
+
id: string; // Queue unique ID: QUE-YYYYMMDD-HHMMSS (derived from filename)
|
|
122
151
|
name?: string; // Optional queue name
|
|
123
152
|
status: 'active' | 'completed' | 'archived' | 'failed';
|
|
124
153
|
issue_ids: string[]; // Issues in this queue
|
|
125
|
-
|
|
126
|
-
conflicts:
|
|
127
|
-
execution_groups?:
|
|
154
|
+
tasks: QueueItem[]; // Task items (formerly 'queue')
|
|
155
|
+
conflicts: QueueConflict[];
|
|
156
|
+
execution_groups?: ExecutionGroup[];
|
|
128
157
|
_metadata: {
|
|
129
158
|
version: string;
|
|
130
159
|
total_tasks: number;
|
|
@@ -132,13 +161,13 @@ interface Queue {
|
|
|
132
161
|
executing_count: number;
|
|
133
162
|
completed_count: number;
|
|
134
163
|
failed_count: number;
|
|
135
|
-
created_at: string;
|
|
136
164
|
updated_at: string;
|
|
137
165
|
};
|
|
138
166
|
}
|
|
139
167
|
|
|
140
168
|
interface QueueIndex {
|
|
141
169
|
active_queue_id: string | null;
|
|
170
|
+
active_item_id: string | null;
|
|
142
171
|
queues: {
|
|
143
172
|
id: string;
|
|
144
173
|
status: string;
|
|
@@ -162,6 +191,7 @@ interface IssueOptions {
|
|
|
162
191
|
json?: boolean;
|
|
163
192
|
force?: boolean;
|
|
164
193
|
fail?: boolean;
|
|
194
|
+
ids?: boolean; // List only IDs (one per line)
|
|
165
195
|
}
|
|
166
196
|
|
|
167
197
|
const ISSUES_DIR = '.workflow/issues';
|
|
@@ -278,7 +308,7 @@ function ensureQueuesDir(): void {
|
|
|
278
308
|
function readQueueIndex(): QueueIndex {
|
|
279
309
|
const path = join(getQueuesDir(), 'index.json');
|
|
280
310
|
if (!existsSync(path)) {
|
|
281
|
-
return { active_queue_id: null, queues: [] };
|
|
311
|
+
return { active_queue_id: null, active_item_id: null, queues: [] };
|
|
282
312
|
}
|
|
283
313
|
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
284
314
|
}
|
|
@@ -319,16 +349,15 @@ function createEmptyQueue(): Queue {
|
|
|
319
349
|
id: generateQueueFileId(),
|
|
320
350
|
status: 'active',
|
|
321
351
|
issue_ids: [],
|
|
322
|
-
|
|
352
|
+
tasks: [],
|
|
323
353
|
conflicts: [],
|
|
324
354
|
_metadata: {
|
|
325
|
-
version: '2.
|
|
355
|
+
version: '2.1',
|
|
326
356
|
total_tasks: 0,
|
|
327
357
|
pending_count: 0,
|
|
328
358
|
executing_count: 0,
|
|
329
359
|
completed_count: 0,
|
|
330
360
|
failed_count: 0,
|
|
331
|
-
created_at: new Date().toISOString(),
|
|
332
361
|
updated_at: new Date().toISOString()
|
|
333
362
|
}
|
|
334
363
|
};
|
|
@@ -338,11 +367,11 @@ function writeQueue(queue: Queue): void {
|
|
|
338
367
|
ensureQueuesDir();
|
|
339
368
|
|
|
340
369
|
// 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.
|
|
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;
|
|
346
375
|
queue._metadata.updated_at = new Date().toISOString();
|
|
347
376
|
|
|
348
377
|
// Write queue file
|
|
@@ -359,7 +388,7 @@ function writeQueue(queue: Queue): void {
|
|
|
359
388
|
issue_ids: queue.issue_ids,
|
|
360
389
|
total_tasks: queue._metadata.total_tasks,
|
|
361
390
|
completed_tasks: queue._metadata.completed_count,
|
|
362
|
-
created_at: queue.
|
|
391
|
+
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
392
|
completed_at: queue.status === 'completed' ? new Date().toISOString() : undefined
|
|
364
393
|
};
|
|
365
394
|
|
|
@@ -377,11 +406,11 @@ function writeQueue(queue: Queue): void {
|
|
|
377
406
|
}
|
|
378
407
|
|
|
379
408
|
function generateQueueItemId(queue: Queue): string {
|
|
380
|
-
const maxNum = queue.
|
|
381
|
-
const match = q.
|
|
409
|
+
const maxNum = queue.tasks.reduce((max, q) => {
|
|
410
|
+
const match = q.item_id.match(/^T-(\d+)$/);
|
|
382
411
|
return match ? Math.max(max, parseInt(match[1])) : max;
|
|
383
412
|
}, 0);
|
|
384
|
-
return `
|
|
413
|
+
return `T-${maxNum + 1}`;
|
|
385
414
|
}
|
|
386
415
|
|
|
387
416
|
// ============ Commands ============
|
|
@@ -429,7 +458,19 @@ async function initAction(issueId: string | undefined, options: IssueOptions): P
|
|
|
429
458
|
async function listAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
|
|
430
459
|
if (!issueId) {
|
|
431
460
|
// List all issues
|
|
432
|
-
|
|
461
|
+
let issues = readIssues();
|
|
462
|
+
|
|
463
|
+
// Filter by status if specified
|
|
464
|
+
if (options.status) {
|
|
465
|
+
const statuses = options.status.split(',').map(s => s.trim());
|
|
466
|
+
issues = issues.filter(i => statuses.includes(i.status));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// IDs only mode (one per line, for scripting)
|
|
470
|
+
if (options.ids) {
|
|
471
|
+
issues.forEach(i => console.log(i.id));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
433
474
|
|
|
434
475
|
if (options.json) {
|
|
435
476
|
console.log(JSON.stringify(issues, null, 2));
|
|
@@ -807,7 +848,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
|
|
807
848
|
// Archive current queue
|
|
808
849
|
if (subAction === 'archive') {
|
|
809
850
|
const queue = readActiveQueue();
|
|
810
|
-
if (!queue.id || queue.
|
|
851
|
+
if (!queue.id || queue.tasks.length === 0) {
|
|
811
852
|
console.log(chalk.yellow('No active queue to archive'));
|
|
812
853
|
return;
|
|
813
854
|
}
|
|
@@ -823,6 +864,31 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
|
|
823
864
|
return;
|
|
824
865
|
}
|
|
825
866
|
|
|
867
|
+
// Delete queue from history
|
|
868
|
+
if ((subAction === 'clear' || subAction === 'delete') && issueId) {
|
|
869
|
+
const queueId = issueId; // issueId is actually queue ID here
|
|
870
|
+
const queuePath = join(getQueuesDir(), `${queueId}.json`);
|
|
871
|
+
|
|
872
|
+
if (!existsSync(queuePath)) {
|
|
873
|
+
console.error(chalk.red(`Queue "${queueId}" not found`));
|
|
874
|
+
process.exit(1);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Remove from index
|
|
878
|
+
const index = readQueueIndex();
|
|
879
|
+
index.queues = index.queues.filter(q => q.id !== queueId);
|
|
880
|
+
if (index.active_queue_id === queueId) {
|
|
881
|
+
index.active_queue_id = null;
|
|
882
|
+
}
|
|
883
|
+
writeQueueIndex(index);
|
|
884
|
+
|
|
885
|
+
// Delete queue file
|
|
886
|
+
unlinkSync(queuePath);
|
|
887
|
+
|
|
888
|
+
console.log(chalk.green(`✓ Deleted queue ${queueId}`));
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
826
892
|
// Add issue tasks to queue
|
|
827
893
|
if (subAction === 'add' && issueId) {
|
|
828
894
|
const issue = findIssue(issueId);
|
|
@@ -840,7 +906,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
|
|
840
906
|
|
|
841
907
|
// Get or create active queue (create new if current is completed/archived)
|
|
842
908
|
let queue = readActiveQueue();
|
|
843
|
-
const isNewQueue = queue.
|
|
909
|
+
const isNewQueue = queue.tasks.length === 0 || queue.status !== 'active';
|
|
844
910
|
|
|
845
911
|
if (queue.status !== 'active') {
|
|
846
912
|
// Create new queue if current is not active
|
|
@@ -854,24 +920,23 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
|
|
854
920
|
|
|
855
921
|
let added = 0;
|
|
856
922
|
for (const task of solution.tasks) {
|
|
857
|
-
const exists = queue.
|
|
923
|
+
const exists = queue.tasks.some(q => q.issue_id === issueId && q.task_id === task.id);
|
|
858
924
|
if (exists) continue;
|
|
859
925
|
|
|
860
|
-
queue.
|
|
861
|
-
|
|
926
|
+
queue.tasks.push({
|
|
927
|
+
item_id: generateQueueItemId(queue),
|
|
862
928
|
issue_id: issueId,
|
|
863
929
|
solution_id: solution.id,
|
|
864
930
|
task_id: task.id,
|
|
865
931
|
status: 'pending',
|
|
866
|
-
execution_order: queue.
|
|
932
|
+
execution_order: queue.tasks.length + 1,
|
|
867
933
|
execution_group: 'P1',
|
|
868
934
|
depends_on: task.depends_on.map(dep => {
|
|
869
|
-
const depItem = queue.
|
|
870
|
-
return depItem?.
|
|
935
|
+
const depItem = queue.tasks.find(q => q.task_id === dep && q.issue_id === issueId);
|
|
936
|
+
return depItem?.item_id || dep;
|
|
871
937
|
}),
|
|
872
938
|
semantic_priority: 0.5,
|
|
873
|
-
assigned_executor: task.executor === 'auto' ? 'codex' : task.executor as any
|
|
874
|
-
queued_at: new Date().toISOString()
|
|
939
|
+
assigned_executor: task.executor === 'auto' ? 'codex' : task.executor as any
|
|
875
940
|
});
|
|
876
941
|
added++;
|
|
877
942
|
}
|
|
@@ -896,7 +961,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
|
|
896
961
|
|
|
897
962
|
console.log(chalk.bold.cyan('\nActive Queue\n'));
|
|
898
963
|
|
|
899
|
-
if (!queue.id || queue.
|
|
964
|
+
if (!queue.id || queue.tasks.length === 0) {
|
|
900
965
|
console.log(chalk.yellow('No active queue'));
|
|
901
966
|
console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
|
|
902
967
|
console.log(chalk.gray('Or list history: ccw issue queue list'));
|
|
@@ -911,7 +976,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
|
|
911
976
|
console.log(chalk.gray('QueueID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
|
|
912
977
|
console.log(chalk.gray('-'.repeat(60)));
|
|
913
978
|
|
|
914
|
-
for (const item of queue.
|
|
979
|
+
for (const item of queue.tasks) {
|
|
915
980
|
const statusColor = {
|
|
916
981
|
'pending': chalk.gray,
|
|
917
982
|
'ready': chalk.cyan,
|
|
@@ -922,7 +987,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
|
|
922
987
|
}[item.status] || chalk.white;
|
|
923
988
|
|
|
924
989
|
console.log(
|
|
925
|
-
item.
|
|
990
|
+
item.item_id.padEnd(10) +
|
|
926
991
|
item.issue_id.substring(0, 13).padEnd(15) +
|
|
927
992
|
item.task_id.padEnd(8) +
|
|
928
993
|
statusColor(item.status.padEnd(12)) +
|
|
@@ -938,13 +1003,13 @@ async function nextAction(options: IssueOptions): Promise<void> {
|
|
|
938
1003
|
const queue = readActiveQueue();
|
|
939
1004
|
|
|
940
1005
|
// Priority 1: Resume executing tasks (interrupted/crashed)
|
|
941
|
-
const executingTasks = queue.
|
|
1006
|
+
const executingTasks = queue.tasks.filter(item => item.status === 'executing');
|
|
942
1007
|
|
|
943
1008
|
// Priority 2: Find pending tasks with satisfied dependencies
|
|
944
|
-
const pendingTasks = queue.
|
|
1009
|
+
const pendingTasks = queue.tasks.filter(item => {
|
|
945
1010
|
if (item.status !== 'pending') return false;
|
|
946
1011
|
return item.depends_on.every(depId => {
|
|
947
|
-
const dep = queue.
|
|
1012
|
+
const dep = queue.tasks.find(q => q.item_id === depId);
|
|
948
1013
|
return !dep || dep.status === 'completed';
|
|
949
1014
|
});
|
|
950
1015
|
});
|
|
@@ -977,25 +1042,25 @@ async function nextAction(options: IssueOptions): Promise<void> {
|
|
|
977
1042
|
|
|
978
1043
|
// Only update status if not already executing (new task)
|
|
979
1044
|
if (!isResume) {
|
|
980
|
-
const idx = queue.
|
|
981
|
-
queue.
|
|
982
|
-
queue.
|
|
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();
|
|
983
1048
|
writeQueue(queue);
|
|
984
1049
|
updateIssue(nextItem.issue_id, { status: 'executing' });
|
|
985
1050
|
}
|
|
986
1051
|
|
|
987
1052
|
// Calculate queue stats for context
|
|
988
1053
|
const stats = {
|
|
989
|
-
total: queue.
|
|
990
|
-
completed: queue.
|
|
991
|
-
failed: queue.
|
|
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,
|
|
992
1057
|
executing: executingTasks.length,
|
|
993
1058
|
pending: pendingTasks.length
|
|
994
1059
|
};
|
|
995
1060
|
const remaining = stats.pending + stats.executing;
|
|
996
1061
|
|
|
997
1062
|
console.log(JSON.stringify({
|
|
998
|
-
|
|
1063
|
+
item_id: nextItem.item_id,
|
|
999
1064
|
issue_id: nextItem.issue_id,
|
|
1000
1065
|
solution_id: nextItem.solution_id,
|
|
1001
1066
|
task: taskDef,
|
|
@@ -1026,7 +1091,7 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
|
|
|
1026
1091
|
}
|
|
1027
1092
|
|
|
1028
1093
|
const queue = readActiveQueue();
|
|
1029
|
-
const idx = queue.
|
|
1094
|
+
const idx = queue.tasks.findIndex(q => q.item_id === queueId);
|
|
1030
1095
|
|
|
1031
1096
|
if (idx === -1) {
|
|
1032
1097
|
console.error(chalk.red(`Queue item "${queueId}" not found`));
|
|
@@ -1034,22 +1099,22 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
|
|
|
1034
1099
|
}
|
|
1035
1100
|
|
|
1036
1101
|
const isFail = options.fail;
|
|
1037
|
-
queue.
|
|
1038
|
-
queue.
|
|
1102
|
+
queue.tasks[idx].status = isFail ? 'failed' : 'completed';
|
|
1103
|
+
queue.tasks[idx].completed_at = new Date().toISOString();
|
|
1039
1104
|
|
|
1040
1105
|
if (isFail) {
|
|
1041
|
-
queue.
|
|
1106
|
+
queue.tasks[idx].failure_reason = options.reason || 'Unknown failure';
|
|
1042
1107
|
} else if (options.result) {
|
|
1043
1108
|
try {
|
|
1044
|
-
queue.
|
|
1109
|
+
queue.tasks[idx].result = JSON.parse(options.result);
|
|
1045
1110
|
} catch {
|
|
1046
1111
|
console.warn(chalk.yellow('Warning: Could not parse result JSON'));
|
|
1047
1112
|
}
|
|
1048
1113
|
}
|
|
1049
1114
|
|
|
1050
1115
|
// Check if all issue tasks are complete
|
|
1051
|
-
const issueId = queue.
|
|
1052
|
-
const issueTasks = queue.
|
|
1116
|
+
const issueId = queue.tasks[idx].issue_id;
|
|
1117
|
+
const issueTasks = queue.tasks.filter(q => q.issue_id === issueId);
|
|
1053
1118
|
const allIssueComplete = issueTasks.every(q => q.status === 'completed');
|
|
1054
1119
|
const anyIssueFailed = issueTasks.some(q => q.status === 'failed');
|
|
1055
1120
|
|
|
@@ -1065,13 +1130,13 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
|
|
|
1065
1130
|
}
|
|
1066
1131
|
|
|
1067
1132
|
// Check if entire queue is complete
|
|
1068
|
-
const allQueueComplete = queue.
|
|
1069
|
-
const anyQueueFailed = queue.
|
|
1133
|
+
const allQueueComplete = queue.tasks.every(q => q.status === 'completed');
|
|
1134
|
+
const anyQueueFailed = queue.tasks.some(q => q.status === 'failed');
|
|
1070
1135
|
|
|
1071
1136
|
if (allQueueComplete) {
|
|
1072
1137
|
queue.status = 'completed';
|
|
1073
1138
|
console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all tasks done)`));
|
|
1074
|
-
} else if (anyQueueFailed && queue.
|
|
1139
|
+
} else if (anyQueueFailed && queue.tasks.every(q => q.status === 'completed' || q.status === 'failed')) {
|
|
1075
1140
|
queue.status = 'failed';
|
|
1076
1141
|
console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed tasks`));
|
|
1077
1142
|
}
|
|
@@ -1080,24 +1145,20 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
|
|
|
1080
1145
|
}
|
|
1081
1146
|
|
|
1082
1147
|
/**
|
|
1083
|
-
* retry -
|
|
1148
|
+
* retry - Reset failed tasks to pending for re-execution
|
|
1084
1149
|
*/
|
|
1085
1150
|
async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
|
|
1086
1151
|
const queue = readActiveQueue();
|
|
1087
1152
|
|
|
1088
|
-
if (!queue.id || queue.
|
|
1153
|
+
if (!queue.id || queue.tasks.length === 0) {
|
|
1089
1154
|
console.log(chalk.yellow('No active queue'));
|
|
1090
1155
|
return;
|
|
1091
1156
|
}
|
|
1092
1157
|
|
|
1093
1158
|
let updated = 0;
|
|
1094
1159
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
const now = Date.now();
|
|
1098
|
-
|
|
1099
|
-
for (const item of queue.queue) {
|
|
1100
|
-
// Retry failed tasks
|
|
1160
|
+
for (const item of queue.tasks) {
|
|
1161
|
+
// Retry failed tasks only
|
|
1101
1162
|
if (item.status === 'failed') {
|
|
1102
1163
|
if (!issueId || item.issue_id === issueId) {
|
|
1103
1164
|
item.status = 'pending';
|
|
@@ -1107,23 +1168,11 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
|
|
|
1107
1168
|
updated++;
|
|
1108
1169
|
}
|
|
1109
1170
|
}
|
|
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
1171
|
}
|
|
1123
1172
|
|
|
1124
1173
|
if (updated === 0) {
|
|
1125
|
-
console.log(chalk.yellow('No failed
|
|
1126
|
-
console.log(chalk.gray('
|
|
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"'));
|
|
1127
1176
|
return;
|
|
1128
1177
|
}
|
|
1129
1178
|
|
|
@@ -1204,7 +1253,8 @@ export async function issueCommand(
|
|
|
1204
1253
|
console.log(chalk.gray(' queue add <issue-id> Add issue to active queue (or create new)'));
|
|
1205
1254
|
console.log(chalk.gray(' queue switch <queue-id> Switch active queue'));
|
|
1206
1255
|
console.log(chalk.gray(' queue archive Archive current queue'));
|
|
1207
|
-
console.log(chalk.gray('
|
|
1256
|
+
console.log(chalk.gray(' queue delete <queue-id> Delete queue from history'));
|
|
1257
|
+
console.log(chalk.gray(' retry [issue-id] Retry failed tasks'));
|
|
1208
1258
|
console.log();
|
|
1209
1259
|
console.log(chalk.bold('Execution Endpoints:'));
|
|
1210
1260
|
console.log(chalk.gray(' next Get next ready task (JSON)'));
|
|
@@ -1213,6 +1263,8 @@ export async function issueCommand(
|
|
|
1213
1263
|
console.log();
|
|
1214
1264
|
console.log(chalk.bold('Options:'));
|
|
1215
1265
|
console.log(chalk.gray(' --title <title> Issue/task title'));
|
|
1266
|
+
console.log(chalk.gray(' --status <status> Filter by status (comma-separated)'));
|
|
1267
|
+
console.log(chalk.gray(' --ids List only IDs (one per line)'));
|
|
1216
1268
|
console.log(chalk.gray(' --solution <path> Solution JSON file'));
|
|
1217
1269
|
console.log(chalk.gray(' --result <json> Execution result'));
|
|
1218
1270
|
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;
|
|
@@ -220,6 +236,82 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
220
236
|
return true;
|
|
221
237
|
}
|
|
222
238
|
|
|
239
|
+
// GET /api/queue/history - Get queue history (all queues from index)
|
|
240
|
+
if (pathname === '/api/queue/history' && req.method === 'GET') {
|
|
241
|
+
const queuesDir = join(issuesDir, 'queues');
|
|
242
|
+
const indexPath = join(queuesDir, 'index.json');
|
|
243
|
+
|
|
244
|
+
if (!existsSync(indexPath)) {
|
|
245
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
246
|
+
res.end(JSON.stringify({ queues: [], active_queue_id: null }));
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
252
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
253
|
+
res.end(JSON.stringify(index));
|
|
254
|
+
} catch {
|
|
255
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
256
|
+
res.end(JSON.stringify({ queues: [], active_queue_id: null }));
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// GET /api/queue/:id - Get specific queue by ID
|
|
262
|
+
const queueDetailMatch = pathname.match(/^\/api\/queue\/([^/]+)$/);
|
|
263
|
+
if (queueDetailMatch && req.method === 'GET' && queueDetailMatch[1] !== 'history' && queueDetailMatch[1] !== 'reorder') {
|
|
264
|
+
const queueId = queueDetailMatch[1];
|
|
265
|
+
const queuesDir = join(issuesDir, 'queues');
|
|
266
|
+
const queueFilePath = join(queuesDir, `${queueId}.json`);
|
|
267
|
+
|
|
268
|
+
if (!existsSync(queueFilePath)) {
|
|
269
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
270
|
+
res.end(JSON.stringify({ error: `Queue ${queueId} not found` }));
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const queue = JSON.parse(readFileSync(queueFilePath, 'utf8'));
|
|
276
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
277
|
+
res.end(JSON.stringify(groupQueueByExecutionGroup(queue)));
|
|
278
|
+
} catch {
|
|
279
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
280
|
+
res.end(JSON.stringify({ error: 'Failed to read queue' }));
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// POST /api/queue/switch - Switch active queue
|
|
286
|
+
if (pathname === '/api/queue/switch' && req.method === 'POST') {
|
|
287
|
+
handlePostRequest(req, res, async (body: any) => {
|
|
288
|
+
const { queueId } = body;
|
|
289
|
+
if (!queueId) return { error: 'queueId required' };
|
|
290
|
+
|
|
291
|
+
const queuesDir = join(issuesDir, 'queues');
|
|
292
|
+
const indexPath = join(queuesDir, 'index.json');
|
|
293
|
+
const queueFilePath = join(queuesDir, `${queueId}.json`);
|
|
294
|
+
|
|
295
|
+
if (!existsSync(queueFilePath)) {
|
|
296
|
+
return { error: `Queue ${queueId} not found` };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const index = existsSync(indexPath)
|
|
301
|
+
? JSON.parse(readFileSync(indexPath, 'utf8'))
|
|
302
|
+
: { active_queue_id: null, queues: [] };
|
|
303
|
+
|
|
304
|
+
index.active_queue_id = queueId;
|
|
305
|
+
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
306
|
+
|
|
307
|
+
return { success: true, active_queue_id: queueId };
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return { error: 'Failed to switch queue' };
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
223
315
|
// POST /api/queue/reorder - Reorder queue items
|
|
224
316
|
if (pathname === '/api/queue/reorder' && req.method === 'POST') {
|
|
225
317
|
handlePostRequest(req, res, async (body: any) => {
|
|
@@ -229,20 +321,20 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
229
321
|
}
|
|
230
322
|
|
|
231
323
|
const queue = readQueue(issuesDir);
|
|
232
|
-
const groupItems = queue.
|
|
233
|
-
const otherItems = queue.
|
|
324
|
+
const groupItems = queue.tasks.filter((item: any) => item.execution_group === groupId);
|
|
325
|
+
const otherItems = queue.tasks.filter((item: any) => item.execution_group !== groupId);
|
|
234
326
|
|
|
235
327
|
if (groupItems.length === 0) return { error: `No items in group ${groupId}` };
|
|
236
328
|
|
|
237
|
-
const
|
|
238
|
-
if (
|
|
329
|
+
const groupItemIds = new Set(groupItems.map((i: any) => i.item_id));
|
|
330
|
+
if (groupItemIds.size !== new Set(newOrder).size) {
|
|
239
331
|
return { error: 'newOrder must contain all group items' };
|
|
240
332
|
}
|
|
241
333
|
for (const id of newOrder) {
|
|
242
|
-
if (!
|
|
334
|
+
if (!groupItemIds.has(id)) return { error: `Invalid item_id: ${id}` };
|
|
243
335
|
}
|
|
244
336
|
|
|
245
|
-
const itemMap = new Map(groupItems.map((i: any) => [i.
|
|
337
|
+
const itemMap = new Map(groupItems.map((i: any) => [i.item_id, i]));
|
|
246
338
|
const reorderedItems = newOrder.map((qid: string, idx: number) => ({ ...itemMap.get(qid), _idx: idx }));
|
|
247
339
|
const newQueue = [...otherItems, ...reorderedItems].sort((a, b) => {
|
|
248
340
|
const aGroup = parseInt(a.execution_group?.match(/\d+/)?.[0] || '999');
|
|
@@ -255,7 +347,7 @@ export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
255
347
|
});
|
|
256
348
|
|
|
257
349
|
newQueue.forEach((item, idx) => { item.execution_order = idx + 1; delete item._idx; });
|
|
258
|
-
queue.
|
|
350
|
+
queue.tasks = newQueue;
|
|
259
351
|
writeQueue(issuesDir, queue);
|
|
260
352
|
|
|
261
353
|
return { success: true, groupId, reordered: newOrder.length };
|