mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: Enhance issue management to support solution-level queues
- Added support for solution-level queues in the issue management system. - Updated interfaces to include solution-specific properties such as `approach`, `task_count`, and `files_touched`. - Modified queue handling to differentiate between task-level and solution-level items. - Adjusted rendering logic in the dashboard to display solutions and their associated tasks correctly. - Enhanced queue statistics and conflict resolution to accommodate the new solution structure. - Updated actions (next, done, retry) to handle both tasks and solutions seamlessly.
This commit is contained in:
@@ -102,6 +102,7 @@ interface SolutionTask {
|
||||
interface Solution {
|
||||
id: string;
|
||||
description?: string;
|
||||
approach?: string; // Solution approach description
|
||||
tasks: SolutionTask[];
|
||||
exploration_context?: Record<string, any>;
|
||||
analysis?: { risk?: string; impact?: string; complexity?: string };
|
||||
@@ -112,10 +113,10 @@ interface Solution {
|
||||
}
|
||||
|
||||
interface QueueItem {
|
||||
item_id: string; // Task item ID in queue: T-1, T-2, ... (formerly queue_id)
|
||||
item_id: string; // Item ID in queue: T-1, T-2, ... (task-level) or S-1, S-2, ... (solution-level)
|
||||
issue_id: string;
|
||||
solution_id: string;
|
||||
task_id: string;
|
||||
task_id?: string; // Only for task-level queues
|
||||
title?: string;
|
||||
status: 'pending' | 'ready' | 'executing' | 'completed' | 'failed' | 'blocked';
|
||||
execution_order: number;
|
||||
@@ -123,6 +124,8 @@ interface QueueItem {
|
||||
depends_on: string[];
|
||||
semantic_priority: number;
|
||||
assigned_executor: 'codex' | 'gemini' | 'agent';
|
||||
task_count?: number; // For solution-level queues
|
||||
files_touched?: string[]; // For solution-level queues
|
||||
started_at?: string;
|
||||
completed_at?: string;
|
||||
result?: Record<string, any>;
|
||||
@@ -142,8 +145,10 @@ interface QueueConflict {
|
||||
interface ExecutionGroup {
|
||||
id: string; // Group ID: P1, S1, etc.
|
||||
type: 'parallel' | 'sequential';
|
||||
task_count: number;
|
||||
tasks: string[]; // Item IDs in this group
|
||||
task_count?: number; // For task-level queues
|
||||
solution_count?: number; // For solution-level queues
|
||||
tasks?: string[]; // Task IDs in this group (task-level)
|
||||
solutions?: string[]; // Solution IDs in this group (solution-level)
|
||||
}
|
||||
|
||||
interface Queue {
|
||||
@@ -151,7 +156,8 @@ interface Queue {
|
||||
name?: string; // Optional queue name
|
||||
status: 'active' | 'completed' | 'archived' | 'failed';
|
||||
issue_ids: string[]; // Issues in this queue
|
||||
tasks: QueueItem[]; // Task items (formerly 'queue')
|
||||
tasks: QueueItem[]; // Task items (task-level queue)
|
||||
solutions?: QueueItem[]; // Solution items (solution-level queue)
|
||||
conflicts: QueueConflict[];
|
||||
execution_groups?: ExecutionGroup[];
|
||||
_metadata: {
|
||||
@@ -172,8 +178,10 @@ interface QueueIndex {
|
||||
id: string;
|
||||
status: string;
|
||||
issue_ids: string[];
|
||||
total_tasks: number;
|
||||
completed_tasks: number;
|
||||
total_tasks?: number; // For task-level queues
|
||||
total_solutions?: number; // For solution-level queues
|
||||
completed_tasks?: number; // For task-level queues
|
||||
completed_solutions?: number; // For solution-level queues
|
||||
created_at: string;
|
||||
completed_at?: string;
|
||||
}[];
|
||||
@@ -845,92 +853,129 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
||||
return;
|
||||
}
|
||||
|
||||
// DAG - Return dependency graph for parallel execution planning
|
||||
// DAG - Return dependency graph for parallel execution planning (solution-level)
|
||||
if (subAction === 'dag') {
|
||||
const queue = readActiveQueue();
|
||||
|
||||
if (!queue.id || queue.tasks.length === 0) {
|
||||
// Support both old (tasks) and new (solutions) queue format
|
||||
const items = queue.solutions || queue.tasks || [];
|
||||
if (!queue.id || items.length === 0) {
|
||||
console.log(JSON.stringify({ error: 'No active queue', nodes: [], edges: [], groups: [] }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Build DAG nodes
|
||||
const completedIds = new Set(queue.tasks.filter(t => t.status === 'completed').map(t => t.item_id));
|
||||
const failedIds = new Set(queue.tasks.filter(t => t.status === 'failed').map(t => t.item_id));
|
||||
// Build DAG nodes (solution-level)
|
||||
const completedIds = new Set(items.filter(t => t.status === 'completed').map(t => t.item_id));
|
||||
const failedIds = new Set(items.filter(t => t.status === 'failed').map(t => t.item_id));
|
||||
|
||||
const nodes = queue.tasks.map(task => ({
|
||||
id: task.item_id,
|
||||
issue_id: task.issue_id,
|
||||
task_id: task.task_id,
|
||||
status: task.status,
|
||||
executor: task.assigned_executor,
|
||||
priority: task.semantic_priority,
|
||||
depends_on: task.depends_on,
|
||||
const nodes = items.map(item => ({
|
||||
id: item.item_id,
|
||||
issue_id: item.issue_id,
|
||||
solution_id: item.solution_id,
|
||||
status: item.status,
|
||||
executor: item.assigned_executor,
|
||||
priority: item.semantic_priority,
|
||||
depends_on: item.depends_on || [],
|
||||
task_count: item.task_count || 1,
|
||||
files_touched: item.files_touched || [],
|
||||
// Calculate if ready (dependencies satisfied)
|
||||
ready: task.status === 'pending' && task.depends_on.every(d => completedIds.has(d)),
|
||||
blocked_by: task.depends_on.filter(d => !completedIds.has(d) && !failedIds.has(d))
|
||||
ready: item.status === 'pending' && (item.depends_on || []).every(d => completedIds.has(d)),
|
||||
blocked_by: (item.depends_on || []).filter(d => !completedIds.has(d) && !failedIds.has(d))
|
||||
}));
|
||||
|
||||
// Build edges for visualization
|
||||
const edges = queue.tasks.flatMap(task =>
|
||||
task.depends_on.map(dep => ({ from: dep, to: task.item_id }))
|
||||
const edges = items.flatMap(item =>
|
||||
(item.depends_on || []).map(dep => ({ from: dep, to: item.item_id }))
|
||||
);
|
||||
|
||||
// Group ready tasks by execution_group for parallel execution
|
||||
const readyTasks = nodes.filter(n => n.ready || n.status === 'executing');
|
||||
// Group ready items by execution_group
|
||||
const readyItems = nodes.filter(n => n.ready || n.status === 'executing');
|
||||
const groups: Record<string, string[]> = {};
|
||||
|
||||
for (const task of queue.tasks) {
|
||||
if (readyTasks.some(r => r.id === task.item_id)) {
|
||||
const group = task.execution_group || 'P1';
|
||||
for (const item of items) {
|
||||
if (readyItems.some(r => r.id === item.item_id)) {
|
||||
const group = item.execution_group || 'P1';
|
||||
if (!groups[group]) groups[group] = [];
|
||||
groups[group].push(task.item_id);
|
||||
groups[group].push(item.item_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate parallel batches (tasks with no dependencies on each other)
|
||||
// Calculate parallel batches - prefer execution_groups from queue if available
|
||||
const parallelBatches: string[][] = [];
|
||||
const remainingReady = new Set(readyTasks.map(t => t.id));
|
||||
const readyItemIds = new Set(readyItems.map(t => t.id));
|
||||
|
||||
while (remainingReady.size > 0) {
|
||||
const batch: string[] = [];
|
||||
const batchFiles = new Set<string>();
|
||||
|
||||
for (const taskId of remainingReady) {
|
||||
const task = queue.tasks.find(t => t.item_id === taskId);
|
||||
if (!task) continue;
|
||||
|
||||
// Check for file conflicts with already-batched tasks
|
||||
const solution = findSolution(task.issue_id, task.solution_id);
|
||||
const taskDef = solution?.tasks.find(t => t.id === task.task_id);
|
||||
const taskFiles = taskDef?.modification_points?.map(mp => mp.file) || [];
|
||||
|
||||
const hasConflict = taskFiles.some(f => batchFiles.has(f));
|
||||
|
||||
if (!hasConflict) {
|
||||
batch.push(taskId);
|
||||
taskFiles.forEach(f => batchFiles.add(f));
|
||||
// Check if queue has pre-assigned execution_groups
|
||||
if (queue.execution_groups && queue.execution_groups.length > 0) {
|
||||
// Use agent-assigned execution groups
|
||||
for (const group of queue.execution_groups) {
|
||||
const groupItems = (group.solutions || group.tasks || [])
|
||||
.filter((id: string) => readyItemIds.has(id));
|
||||
if (groupItems.length > 0) {
|
||||
if (group.type === 'parallel') {
|
||||
// All items in parallel group can run together
|
||||
parallelBatches.push(groupItems);
|
||||
} else {
|
||||
// Sequential group: each item is its own batch
|
||||
for (const itemId of groupItems) {
|
||||
parallelBatches.push([itemId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: calculate parallel batches from file conflicts
|
||||
const remainingReady = new Set(readyItemIds);
|
||||
|
||||
if (batch.length === 0) {
|
||||
// Fallback: take one at a time if all conflict
|
||||
const first = Array.from(remainingReady)[0];
|
||||
batch.push(first);
|
||||
while (remainingReady.size > 0) {
|
||||
const batch: string[] = [];
|
||||
const batchFiles = new Set<string>();
|
||||
|
||||
for (const itemId of Array.from(remainingReady)) {
|
||||
const item = items.find(t => t.item_id === itemId);
|
||||
if (!item) continue;
|
||||
|
||||
// Get all files touched by this solution
|
||||
let solutionFiles: string[] = item.files_touched || [];
|
||||
|
||||
// If not in queue item, fetch from solution definition
|
||||
if (solutionFiles.length === 0) {
|
||||
const solution = findSolution(item.issue_id, item.solution_id);
|
||||
if (solution?.tasks) {
|
||||
for (const task of solution.tasks) {
|
||||
for (const mp of task.modification_points || []) {
|
||||
solutionFiles.push(mp.file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const hasConflict = solutionFiles.some(f => batchFiles.has(f));
|
||||
|
||||
if (!hasConflict) {
|
||||
batch.push(itemId);
|
||||
solutionFiles.forEach(f => batchFiles.add(f));
|
||||
}
|
||||
}
|
||||
|
||||
if (batch.length === 0) {
|
||||
// Fallback: take one at a time if all conflict
|
||||
const first = Array.from(remainingReady)[0];
|
||||
batch.push(first);
|
||||
}
|
||||
|
||||
parallelBatches.push(batch);
|
||||
batch.forEach(id => remainingReady.delete(id));
|
||||
}
|
||||
|
||||
parallelBatches.push(batch);
|
||||
batch.forEach(id => remainingReady.delete(id));
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
queue_id: queue.id,
|
||||
total: nodes.length,
|
||||
ready_count: readyTasks.length,
|
||||
ready_count: readyItems.length,
|
||||
completed_count: completedIds.size,
|
||||
nodes,
|
||||
edges,
|
||||
groups: Object.entries(groups).map(([id, tasks]) => ({ id, tasks })),
|
||||
groups: Object.entries(groups).map(([id, solutions]) => ({ id, solutions })),
|
||||
parallel_batches: parallelBatches,
|
||||
_summary: {
|
||||
can_parallel: parallelBatches[0]?.length || 0,
|
||||
@@ -1084,7 +1129,7 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
||||
console.log(
|
||||
item.item_id.padEnd(10) +
|
||||
item.issue_id.substring(0, 13).padEnd(15) +
|
||||
item.task_id.padEnd(8) +
|
||||
(item.task_id || '-').padEnd(8) +
|
||||
statusColor(item.status.padEnd(12)) +
|
||||
item.assigned_executor
|
||||
);
|
||||
@@ -1097,91 +1142,107 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
||||
*/
|
||||
async function nextAction(itemId: string | undefined, options: IssueOptions): Promise<void> {
|
||||
const queue = readActiveQueue();
|
||||
let nextItem: typeof queue.tasks[0] | undefined;
|
||||
// Support both old (tasks) and new (solutions) queue format
|
||||
const items = queue.solutions || queue.tasks || [];
|
||||
let nextItem: typeof items[0] | undefined;
|
||||
let isResume = false;
|
||||
|
||||
// If specific item_id provided, fetch that task directly
|
||||
// If specific item_id provided, fetch that item directly
|
||||
if (itemId) {
|
||||
nextItem = queue.tasks.find(t => t.item_id === itemId);
|
||||
nextItem = items.find(t => t.item_id === itemId);
|
||||
if (!nextItem) {
|
||||
console.log(JSON.stringify({ status: 'error', message: `Task ${itemId} not found` }));
|
||||
console.log(JSON.stringify({ status: 'error', message: `Item ${itemId} not found` }));
|
||||
return;
|
||||
}
|
||||
if (nextItem.status === 'completed') {
|
||||
console.log(JSON.stringify({ status: 'completed', message: `Task ${itemId} already completed` }));
|
||||
console.log(JSON.stringify({ status: 'completed', message: `Item ${itemId} already completed` }));
|
||||
return;
|
||||
}
|
||||
if (nextItem.status === 'failed') {
|
||||
console.log(JSON.stringify({ status: 'failed', message: `Task ${itemId} failed, use retry to reset` }));
|
||||
console.log(JSON.stringify({ status: 'failed', message: `Item ${itemId} failed, use retry to reset` }));
|
||||
return;
|
||||
}
|
||||
isResume = nextItem.status === 'executing';
|
||||
} else {
|
||||
// Auto-select: Priority 1 - executing, Priority 2 - ready pending
|
||||
const executingTasks = queue.tasks.filter(item => item.status === 'executing');
|
||||
const pendingTasks = queue.tasks.filter(item => {
|
||||
const executingItems = items.filter(item => item.status === 'executing');
|
||||
const pendingItems = items.filter(item => {
|
||||
if (item.status !== 'pending') return false;
|
||||
return item.depends_on.every(depId => {
|
||||
const dep = queue.tasks.find(q => q.item_id === depId);
|
||||
return (item.depends_on || []).every(depId => {
|
||||
const dep = items.find(q => q.item_id === depId);
|
||||
return !dep || dep.status === 'completed';
|
||||
});
|
||||
});
|
||||
|
||||
const readyTasks = [...executingTasks, ...pendingTasks];
|
||||
const readyItems = [...executingItems, ...pendingItems];
|
||||
|
||||
if (readyTasks.length === 0) {
|
||||
if (readyItems.length === 0) {
|
||||
console.log(JSON.stringify({
|
||||
status: 'empty',
|
||||
message: 'No ready tasks',
|
||||
message: 'No ready items',
|
||||
queue_status: queue._metadata
|
||||
}, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
readyTasks.sort((a, b) => a.execution_order - b.execution_order);
|
||||
nextItem = readyTasks[0];
|
||||
readyItems.sort((a, b) => a.execution_order - b.execution_order);
|
||||
nextItem = readyItems[0];
|
||||
isResume = nextItem.status === 'executing';
|
||||
}
|
||||
|
||||
// Load task definition
|
||||
// Load FULL solution with all tasks
|
||||
const solution = findSolution(nextItem.issue_id, nextItem.solution_id);
|
||||
const taskDef = solution?.tasks.find(t => t.id === nextItem.task_id);
|
||||
|
||||
if (!taskDef) {
|
||||
console.log(JSON.stringify({ status: 'error', message: 'Task definition not found' }));
|
||||
if (!solution) {
|
||||
console.log(JSON.stringify({ status: 'error', message: 'Solution not found' }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Only update status if not already executing (new task)
|
||||
// Only update status if not already executing
|
||||
if (!isResume) {
|
||||
const idx = queue.tasks.findIndex(q => q.item_id === nextItem.item_id);
|
||||
queue.tasks[idx].status = 'executing';
|
||||
queue.tasks[idx].started_at = new Date().toISOString();
|
||||
const idx = items.findIndex(q => q.item_id === nextItem.item_id);
|
||||
items[idx].status = 'executing';
|
||||
items[idx].started_at = new Date().toISOString();
|
||||
// Write back to correct array
|
||||
if (queue.solutions) {
|
||||
queue.solutions = items;
|
||||
} else {
|
||||
queue.tasks = items;
|
||||
}
|
||||
writeQueue(queue);
|
||||
updateIssue(nextItem.issue_id, { status: 'executing' });
|
||||
}
|
||||
|
||||
// Calculate queue stats for context
|
||||
// Calculate queue stats
|
||||
const stats = {
|
||||
total: queue.tasks.length,
|
||||
completed: queue.tasks.filter(q => q.status === 'completed').length,
|
||||
failed: queue.tasks.filter(q => q.status === 'failed').length,
|
||||
executing: queue.tasks.filter(q => q.status === 'executing').length,
|
||||
pending: queue.tasks.filter(q => q.status === 'pending').length
|
||||
total: items.length,
|
||||
completed: items.filter(q => q.status === 'completed').length,
|
||||
failed: items.filter(q => q.status === 'failed').length,
|
||||
executing: items.filter(q => q.status === 'executing').length,
|
||||
pending: items.filter(q => q.status === 'pending').length
|
||||
};
|
||||
const remaining = stats.pending + stats.executing;
|
||||
|
||||
// Calculate total estimated time for all tasks
|
||||
const totalMinutes = solution.tasks?.reduce((sum, t) => sum + (t.estimated_minutes || 30), 0) || 30;
|
||||
|
||||
console.log(JSON.stringify({
|
||||
item_id: nextItem.item_id,
|
||||
issue_id: nextItem.issue_id,
|
||||
solution_id: nextItem.solution_id,
|
||||
task: taskDef,
|
||||
context: solution?.exploration_context || {},
|
||||
// Return full solution object with all tasks
|
||||
solution: {
|
||||
id: solution.id,
|
||||
approach: solution.approach,
|
||||
tasks: solution.tasks || [],
|
||||
exploration_context: solution.exploration_context || {}
|
||||
},
|
||||
resumed: isResume,
|
||||
resume_note: isResume ? `Resuming interrupted task (started: ${nextItem.started_at})` : undefined,
|
||||
resume_note: isResume ? `Resuming interrupted item (started: ${nextItem.started_at})` : undefined,
|
||||
execution_hints: {
|
||||
executor: nextItem.assigned_executor,
|
||||
estimated_minutes: taskDef.estimated_minutes || 30
|
||||
task_count: solution.tasks?.length || 0,
|
||||
estimated_minutes: totalMinutes
|
||||
},
|
||||
queue_progress: {
|
||||
completed: stats.completed,
|
||||
@@ -1203,33 +1264,43 @@ async function detailAction(itemId: string | undefined, options: IssueOptions):
|
||||
}
|
||||
|
||||
const queue = readActiveQueue();
|
||||
const queueItem = queue.tasks.find(t => t.item_id === itemId);
|
||||
// Support both old (tasks) and new (solutions) queue format
|
||||
const items = queue.solutions || queue.tasks || [];
|
||||
const queueItem = items.find(t => t.item_id === itemId);
|
||||
|
||||
if (!queueItem) {
|
||||
console.log(JSON.stringify({ status: 'error', message: `Task ${itemId} not found` }));
|
||||
console.log(JSON.stringify({ status: 'error', message: `Item ${itemId} not found` }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Load task definition from solution
|
||||
// Load FULL solution with all tasks
|
||||
const solution = findSolution(queueItem.issue_id, queueItem.solution_id);
|
||||
const taskDef = solution?.tasks.find(t => t.id === queueItem.task_id);
|
||||
|
||||
if (!taskDef) {
|
||||
console.log(JSON.stringify({ status: 'error', message: 'Task definition not found in solution' }));
|
||||
if (!solution) {
|
||||
console.log(JSON.stringify({ status: 'error', message: 'Solution not found' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Return full task info (READ-ONLY - no status update)
|
||||
// Calculate total estimated time for all tasks
|
||||
const totalMinutes = solution.tasks?.reduce((sum, t) => sum + (t.estimated_minutes || 30), 0) || 30;
|
||||
|
||||
// Return FULL SOLUTION with all tasks (READ-ONLY - no status update)
|
||||
console.log(JSON.stringify({
|
||||
item_id: queueItem.item_id,
|
||||
issue_id: queueItem.issue_id,
|
||||
solution_id: queueItem.solution_id,
|
||||
status: queueItem.status,
|
||||
task: taskDef,
|
||||
context: solution?.exploration_context || {},
|
||||
// Return full solution object with all tasks
|
||||
solution: {
|
||||
id: solution.id,
|
||||
approach: solution.approach,
|
||||
tasks: solution.tasks || [],
|
||||
exploration_context: solution.exploration_context || {}
|
||||
},
|
||||
execution_hints: {
|
||||
executor: queueItem.assigned_executor,
|
||||
estimated_minutes: taskDef.estimated_minutes || 30
|
||||
task_count: solution.tasks?.length || 0,
|
||||
estimated_minutes: totalMinutes
|
||||
}
|
||||
}, null, 2));
|
||||
}
|
||||
@@ -1239,13 +1310,15 @@ async function detailAction(itemId: string | undefined, options: IssueOptions):
|
||||
*/
|
||||
async function doneAction(queueId: string | undefined, options: IssueOptions): Promise<void> {
|
||||
if (!queueId) {
|
||||
console.error(chalk.red('Queue ID is required'));
|
||||
console.error(chalk.gray('Usage: ccw issue done <queue-id> [--fail] [--reason "..."]'));
|
||||
console.error(chalk.red('Item ID is required'));
|
||||
console.error(chalk.gray('Usage: ccw issue done <item-id> [--fail] [--reason "..."]'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const queue = readActiveQueue();
|
||||
const idx = queue.tasks.findIndex(q => q.item_id === queueId);
|
||||
// Support both old (tasks) and new (solutions) queue format
|
||||
const items = queue.solutions || queue.tasks || [];
|
||||
const idx = items.findIndex(q => q.item_id === queueId);
|
||||
|
||||
if (idx === -1) {
|
||||
console.error(chalk.red(`Queue item "${queueId}" not found`));
|
||||
@@ -1253,66 +1326,69 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
|
||||
}
|
||||
|
||||
const isFail = options.fail;
|
||||
queue.tasks[idx].status = isFail ? 'failed' : 'completed';
|
||||
queue.tasks[idx].completed_at = new Date().toISOString();
|
||||
items[idx].status = isFail ? 'failed' : 'completed';
|
||||
items[idx].completed_at = new Date().toISOString();
|
||||
|
||||
if (isFail) {
|
||||
queue.tasks[idx].failure_reason = options.reason || 'Unknown failure';
|
||||
items[idx].failure_reason = options.reason || 'Unknown failure';
|
||||
} else if (options.result) {
|
||||
try {
|
||||
queue.tasks[idx].result = JSON.parse(options.result);
|
||||
items[idx].result = JSON.parse(options.result);
|
||||
} catch {
|
||||
console.warn(chalk.yellow('Warning: Could not parse result JSON'));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all issue tasks are complete
|
||||
const issueId = queue.tasks[idx].issue_id;
|
||||
const issueTasks = queue.tasks.filter(q => q.issue_id === issueId);
|
||||
const allIssueComplete = issueTasks.every(q => q.status === 'completed');
|
||||
const anyIssueFailed = issueTasks.some(q => q.status === 'failed');
|
||||
// Update issue status (solution = issue in new model)
|
||||
const issueId = items[idx].issue_id;
|
||||
|
||||
if (allIssueComplete) {
|
||||
updateIssue(issueId, { status: 'completed', completed_at: new Date().toISOString() });
|
||||
console.log(chalk.green(`✓ ${queueId} completed`));
|
||||
console.log(chalk.green(`✓ Issue ${issueId} completed (all tasks done)`));
|
||||
} else if (anyIssueFailed) {
|
||||
if (isFail) {
|
||||
updateIssue(issueId, { status: 'failed' });
|
||||
console.log(chalk.red(`✗ ${queueId} failed`));
|
||||
} else {
|
||||
console.log(isFail ? chalk.red(`✗ ${queueId} failed`) : chalk.green(`✓ ${queueId} completed`));
|
||||
updateIssue(issueId, { status: 'completed', completed_at: new Date().toISOString() });
|
||||
console.log(chalk.green(`✓ ${queueId} completed`));
|
||||
console.log(chalk.green(`✓ Issue ${issueId} completed`));
|
||||
}
|
||||
|
||||
// Check if entire queue is complete
|
||||
const allQueueComplete = queue.tasks.every(q => q.status === 'completed');
|
||||
const anyQueueFailed = queue.tasks.some(q => q.status === 'failed');
|
||||
const allQueueComplete = items.every(q => q.status === 'completed');
|
||||
const anyQueueFailed = items.some(q => q.status === 'failed');
|
||||
|
||||
if (allQueueComplete) {
|
||||
queue.status = 'completed';
|
||||
console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all tasks done)`));
|
||||
} else if (anyQueueFailed && queue.tasks.every(q => q.status === 'completed' || q.status === 'failed')) {
|
||||
console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all solutions done)`));
|
||||
} else if (anyQueueFailed && items.every(q => q.status === 'completed' || q.status === 'failed')) {
|
||||
queue.status = 'failed';
|
||||
console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed tasks`));
|
||||
console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed solutions`));
|
||||
}
|
||||
|
||||
// Write back to queue (update the correct array)
|
||||
if (queue.solutions) {
|
||||
queue.solutions = items;
|
||||
} else {
|
||||
queue.tasks = items;
|
||||
}
|
||||
writeQueue(queue);
|
||||
}
|
||||
|
||||
/**
|
||||
* retry - Reset failed tasks to pending for re-execution
|
||||
* retry - Reset failed items to pending for re-execution
|
||||
*/
|
||||
async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
|
||||
const queue = readActiveQueue();
|
||||
// Support both old (tasks) and new (solutions) queue format
|
||||
const items = queue.solutions || queue.tasks || [];
|
||||
|
||||
if (!queue.id || queue.tasks.length === 0) {
|
||||
if (!queue.id || items.length === 0) {
|
||||
console.log(chalk.yellow('No active queue'));
|
||||
return;
|
||||
}
|
||||
|
||||
let updated = 0;
|
||||
|
||||
for (const item of queue.tasks) {
|
||||
// Retry failed tasks only
|
||||
for (const item of items) {
|
||||
// Retry failed items only
|
||||
if (item.status === 'failed') {
|
||||
if (!issueId || item.issue_id === issueId) {
|
||||
item.status = 'pending';
|
||||
@@ -1325,8 +1401,7 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
|
||||
}
|
||||
|
||||
if (updated === 0) {
|
||||
console.log(chalk.yellow('No failed tasks to retry'));
|
||||
console.log(chalk.gray('Note: Interrupted (executing) tasks are auto-resumed by "ccw issue next"'));
|
||||
console.log(chalk.yellow('No failed items to retry'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1335,13 +1410,19 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
|
||||
queue.status = 'active';
|
||||
}
|
||||
|
||||
// Write back to queue
|
||||
if (queue.solutions) {
|
||||
queue.solutions = items;
|
||||
} else {
|
||||
queue.tasks = items;
|
||||
}
|
||||
writeQueue(queue);
|
||||
|
||||
if (issueId) {
|
||||
updateIssue(issueId, { status: 'queued' });
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ Reset ${updated} task(s) to pending`));
|
||||
console.log(chalk.green(`✓ Reset ${updated} item(s) to pending`));
|
||||
}
|
||||
|
||||
// ============ Main Entry ============
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// ========== Issue State ==========
|
||||
var issueData = {
|
||||
issues: [],
|
||||
queue: { tasks: [], conflicts: [], execution_groups: [], grouped_items: {} },
|
||||
queue: { tasks: [], solutions: [], conflicts: [], execution_groups: [], grouped_items: {} },
|
||||
selectedIssue: null,
|
||||
selectedSolution: null,
|
||||
selectedSolutionIssueId: null,
|
||||
@@ -65,7 +65,7 @@ async function loadQueueData() {
|
||||
issueData.queue = await response.json();
|
||||
} catch (err) {
|
||||
console.error('Failed to load queue:', err);
|
||||
issueData.queue = { tasks: [], conflicts: [], execution_groups: [], grouped_items: {} };
|
||||
issueData.queue = { tasks: [], solutions: [], conflicts: [], execution_groups: [], grouped_items: {} };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +360,9 @@ function filterIssuesByStatus(status) {
|
||||
// ========== Queue Section ==========
|
||||
function renderQueueSection() {
|
||||
const queue = issueData.queue;
|
||||
const queueItems = queue.tasks || [];
|
||||
// Support both solution-level and task-level queues
|
||||
const queueItems = queue.solutions || queue.tasks || [];
|
||||
const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
|
||||
const metadata = queue._metadata || {};
|
||||
|
||||
// Check if queue is empty
|
||||
@@ -443,8 +445,8 @@ function renderQueueSection() {
|
||||
<!-- Queue Stats -->
|
||||
<div class="queue-stats-grid mb-4">
|
||||
<div class="queue-stat-card">
|
||||
<span class="queue-stat-value">${metadata.total_tasks || queueItems.length}</span>
|
||||
<span class="queue-stat-label">${t('issues.totalTasks') || 'Total'}</span>
|
||||
<span class="queue-stat-value">${isSolutionLevel ? (metadata.total_solutions || queueItems.length) : (metadata.total_tasks || queueItems.length)}</span>
|
||||
<span class="queue-stat-label">${isSolutionLevel ? (t('issues.totalSolutions') || 'Solutions') : (t('issues.totalTasks') || 'Total')}</span>
|
||||
</div>
|
||||
<div class="queue-stat-card pending">
|
||||
<span class="queue-stat-value">${metadata.pending_count || queueItems.filter(i => i.status === 'pending').length}</span>
|
||||
@@ -510,6 +512,9 @@ function renderQueueSection() {
|
||||
|
||||
function renderQueueGroup(group, items) {
|
||||
const isParallel = group.type === 'parallel';
|
||||
// Support both solution-level (solution_count) and task-level (task_count)
|
||||
const itemCount = group.solution_count || group.task_count || items.length;
|
||||
const itemLabel = group.solution_count ? 'solutions' : 'tasks';
|
||||
|
||||
return `
|
||||
<div class="queue-group" data-group-id="${group.id}">
|
||||
@@ -518,7 +523,7 @@ function renderQueueGroup(group, items) {
|
||||
<i data-lucide="${isParallel ? 'git-merge' : 'arrow-right'}" class="w-4 h-4"></i>
|
||||
${group.id} (${isParallel ? t('issues.parallelGroup') || 'Parallel' : t('issues.sequentialGroup') || 'Sequential'})
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${group.task_count} tasks</span>
|
||||
<span class="text-sm text-muted-foreground">${itemCount} ${itemLabel}</span>
|
||||
</div>
|
||||
<div class="queue-items ${isParallel ? 'parallel' : 'sequential'}">
|
||||
${items.map((item, idx) => renderQueueItem(item, idx, items.length)).join('')}
|
||||
@@ -537,6 +542,9 @@ function renderQueueItem(item, index, total) {
|
||||
blocked: 'blocked'
|
||||
};
|
||||
|
||||
// Check if this is a solution-level item (has task_count) or task-level (has task_id)
|
||||
const isSolutionItem = item.task_count !== undefined;
|
||||
|
||||
return `
|
||||
<div class="queue-item ${statusColors[item.status] || ''}"
|
||||
draggable="true"
|
||||
@@ -545,7 +553,20 @@ function renderQueueItem(item, index, total) {
|
||||
onclick="openQueueItemDetail('${item.item_id}')">
|
||||
<span class="queue-item-id font-mono text-xs">${item.item_id}</span>
|
||||
<span class="queue-item-issue text-xs text-muted-foreground">${item.issue_id}</span>
|
||||
<span class="queue-item-task text-sm">${item.task_id}</span>
|
||||
${isSolutionItem ? `
|
||||
<span class="queue-item-solution text-sm" title="${item.solution_id || ''}">
|
||||
<i data-lucide="package" class="w-3 h-3 inline mr-1"></i>
|
||||
${item.task_count} ${t('issues.tasks') || 'tasks'}
|
||||
</span>
|
||||
${item.files_touched && item.files_touched.length > 0 ? `
|
||||
<span class="queue-item-files text-xs text-muted-foreground" title="${item.files_touched.join(', ')}">
|
||||
<i data-lucide="file" class="w-3 h-3"></i>
|
||||
${item.files_touched.length}
|
||||
</span>
|
||||
` : ''}
|
||||
` : `
|
||||
<span class="queue-item-task text-sm">${item.task_id || '-'}</span>
|
||||
`}
|
||||
<span class="queue-item-priority" style="opacity: ${item.semantic_priority || 0.5}">
|
||||
<i data-lucide="arrow-up" class="w-3 h-3"></i>
|
||||
</span>
|
||||
@@ -569,9 +590,12 @@ function renderConflictsSection(conflicts) {
|
||||
${conflicts.map(c => `
|
||||
<div class="conflict-item">
|
||||
<span class="conflict-file font-mono text-xs">${c.file}</span>
|
||||
<span class="conflict-tasks text-xs text-muted-foreground">${c.tasks.join(' → ')}</span>
|
||||
<span class="conflict-status ${c.resolved ? 'resolved' : 'pending'}">
|
||||
${c.resolved ? 'Resolved' : 'Pending'}
|
||||
<span class="conflict-items text-xs text-muted-foreground">${(c.solutions || c.tasks || []).join(' → ')}</span>
|
||||
${c.rationale ? `<span class="conflict-rationale text-xs text-muted-foreground" title="${c.rationale}">
|
||||
<i data-lucide="info" class="w-3 h-3"></i>
|
||||
</span>` : ''}
|
||||
<span class="conflict-status ${c.resolved || c.resolution ? 'resolved' : 'pending'}">
|
||||
${c.resolved || c.resolution ? 'Resolved' : 'Pending'}
|
||||
</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
@@ -1156,7 +1180,9 @@ function escapeHtml(text) {
|
||||
}
|
||||
|
||||
function openQueueItemDetail(itemId) {
|
||||
const item = issueData.queue.tasks?.find(q => q.item_id === itemId);
|
||||
// Support both solution-level and task-level queues
|
||||
const items = issueData.queue.solutions || issueData.queue.tasks || [];
|
||||
const item = items.find(q => q.item_id === itemId);
|
||||
if (item) {
|
||||
openIssueDetail(item.issue_id);
|
||||
}
|
||||
@@ -1600,7 +1626,7 @@ async function showQueueHistoryModal() {
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
<i data-lucide="check-circle" class="w-3 h-3 inline"></i>
|
||||
${q.completed_tasks || 0}/${q.total_tasks || 0} tasks
|
||||
${q.completed_solutions || q.completed_tasks || 0}/${q.total_solutions || q.total_tasks || 0} ${q.total_solutions ? 'solutions' : 'tasks'}
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
<i data-lucide="calendar" class="w-3 h-3 inline"></i>
|
||||
@@ -1689,17 +1715,21 @@ async function viewQueueDetail(queueId) {
|
||||
throw new Error(queue.error);
|
||||
}
|
||||
|
||||
const tasks = queue.queue || [];
|
||||
// Support both solution-level and task-level queues
|
||||
const items = queue.solutions || queue.queue || queue.tasks || [];
|
||||
const isSolutionLevel = !!(queue.solutions && queue.solutions.length > 0);
|
||||
const metadata = queue._metadata || {};
|
||||
|
||||
// Group by execution_group
|
||||
const grouped = {};
|
||||
tasks.forEach(task => {
|
||||
const group = task.execution_group || 'ungrouped';
|
||||
items.forEach(item => {
|
||||
const group = item.execution_group || 'ungrouped';
|
||||
if (!grouped[group]) grouped[group] = [];
|
||||
grouped[group].push(task);
|
||||
grouped[group].push(item);
|
||||
});
|
||||
|
||||
const itemLabel = isSolutionLevel ? 'solutions' : 'tasks';
|
||||
|
||||
const detailHtml = `
|
||||
<div class="queue-detail-view">
|
||||
<div class="queue-detail-header mb-4">
|
||||
@@ -1715,40 +1745,41 @@ async function viewQueueDetail(queueId) {
|
||||
|
||||
<div class="queue-detail-stats mb-4">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">${tasks.length}</span>
|
||||
<span class="stat-label">Total</span>
|
||||
<span class="stat-value">${items.length}</span>
|
||||
<span class="stat-label">${isSolutionLevel ? 'Solutions' : 'Total'}</span>
|
||||
</div>
|
||||
<div class="stat-item completed">
|
||||
<span class="stat-value">${tasks.filter(t => t.status === 'completed').length}</span>
|
||||
<span class="stat-value">${items.filter(t => t.status === 'completed').length}</span>
|
||||
<span class="stat-label">Completed</span>
|
||||
</div>
|
||||
<div class="stat-item pending">
|
||||
<span class="stat-value">${tasks.filter(t => t.status === 'pending').length}</span>
|
||||
<span class="stat-value">${items.filter(t => t.status === 'pending').length}</span>
|
||||
<span class="stat-label">Pending</span>
|
||||
</div>
|
||||
<div class="stat-item failed">
|
||||
<span class="stat-value">${tasks.filter(t => t.status === 'failed').length}</span>
|
||||
<span class="stat-value">${items.filter(t => t.status === 'failed').length}</span>
|
||||
<span class="stat-label">Failed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="queue-detail-groups">
|
||||
${Object.entries(grouped).map(([groupId, items]) => `
|
||||
${Object.entries(grouped).map(([groupId, groupItems]) => `
|
||||
<div class="queue-group-section">
|
||||
<div class="queue-group-header">
|
||||
<i data-lucide="folder" class="w-4 h-4"></i>
|
||||
<span>${groupId}</span>
|
||||
<span class="text-xs text-muted-foreground">(${items.length} tasks)</span>
|
||||
<span class="text-xs text-muted-foreground">(${groupItems.length} ${itemLabel})</span>
|
||||
</div>
|
||||
<div class="queue-group-items">
|
||||
${items.map(item => `
|
||||
${groupItems.map(item => `
|
||||
<div class="queue-detail-item ${item.status || ''}">
|
||||
<div class="item-main">
|
||||
<span class="item-id font-mono text-xs">${item.queue_id || item.task_id || 'N/A'}</span>
|
||||
<span class="item-title text-sm">${item.title || item.action || 'Untitled'}</span>
|
||||
<span class="item-id font-mono text-xs">${item.item_id || item.queue_id || item.task_id || 'N/A'}</span>
|
||||
<span class="item-title text-sm">${isSolutionLevel ? (item.task_count + ' tasks') : (item.title || item.action || 'Untitled')}</span>
|
||||
</div>
|
||||
<div class="item-meta">
|
||||
<span class="item-issue text-xs">${item.issue_id || ''}</span>
|
||||
${isSolutionLevel && item.files_touched ? `<span class="item-files text-xs">${item.files_touched.length} files</span>` : ''}
|
||||
<span class="item-status ${item.status || ''}">${item.status || 'unknown'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user