mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat(issue-queue): Enhance task execution flow with priority handling and stuck task reset option
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: plan
|
name: plan
|
||||||
description: Batch plan issue resolution using issue-plan-agent (explore + plan closed-loop)
|
description: Batch plan issue resolution using issue-plan-agent (explore + plan closed-loop)
|
||||||
argument-hint: "<issue-id>[,<issue-id>,...] [--batch-size 3]"
|
argument-hint: "<issue-id>[,<issue-id>,...] [--batch-size 3] --all-pending"
|
||||||
allowed-tools: TodoWrite(*), Task(*), SlashCommand(*), AskUserQuestion(*), Bash(*), Read(*), Write(*)
|
allowed-tools: TodoWrite(*), Task(*), SlashCommand(*), AskUserQuestion(*), Bash(*), Read(*), Write(*)
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -78,24 +78,49 @@ Phase 4: Summary
|
|||||||
### Phase 1: Issue Loading
|
### Phase 1: Issue Loading
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Parse input
|
// Parse input and flags
|
||||||
const issueIds = userInput.includes(',')
|
|
||||||
? userInput.split(',').map(s => s.trim())
|
|
||||||
: [userInput.trim()];
|
|
||||||
|
|
||||||
// Read issues.jsonl
|
|
||||||
const issuesPath = '.workflow/issues/issues.jsonl';
|
const issuesPath = '.workflow/issues/issues.jsonl';
|
||||||
const allIssues = Bash(`cat "${issuesPath}" 2>/dev/null || echo ''`)
|
const batchSize = flags.batchSize || 3;
|
||||||
.split('\n')
|
|
||||||
.filter(line => line.trim())
|
|
||||||
.map(line => JSON.parse(line));
|
|
||||||
|
|
||||||
// Load and validate issues
|
// Key fields for planning (avoid loading full issue data)
|
||||||
|
const PLAN_FIELDS = 'id,title,status,context,affected_components,lifecycle_requirements,priority,bound_solution_id';
|
||||||
|
|
||||||
|
let issueIds = [];
|
||||||
|
|
||||||
|
if (flags.allPending) {
|
||||||
|
// Use jq to filter pending/registered issues - extract only IDs
|
||||||
|
const pendingIds = Bash(`
|
||||||
|
cat "${issuesPath}" 2>/dev/null | \\
|
||||||
|
jq -r 'select(.status == "pending" or .status == "registered") | .id' 2>/dev/null || echo ''
|
||||||
|
`).trim();
|
||||||
|
|
||||||
|
issueIds = pendingIds ? pendingIds.split('\n').filter(Boolean) : [];
|
||||||
|
|
||||||
|
if (issueIds.length === 0) {
|
||||||
|
console.log('No pending issues found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`Found ${issueIds.length} pending issues`);
|
||||||
|
} else {
|
||||||
|
// Parse comma-separated issue IDs
|
||||||
|
issueIds = userInput.includes(',')
|
||||||
|
? userInput.split(',').map(s => s.trim())
|
||||||
|
: [userInput.trim()];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load issues using jq to extract only key fields
|
||||||
const issues = [];
|
const issues = [];
|
||||||
for (const id of issueIds) {
|
for (const id of issueIds) {
|
||||||
let issue = allIssues.find(i => i.id === id);
|
// Use jq to find issue by ID and extract only needed fields
|
||||||
|
const issueJson = Bash(`
|
||||||
|
cat "${issuesPath}" 2>/dev/null | \\
|
||||||
|
jq -c 'select(.id == "${id}") | {${PLAN_FIELDS}}' 2>/dev/null | head -1
|
||||||
|
`).trim();
|
||||||
|
|
||||||
if (!issue) {
|
let issue;
|
||||||
|
if (issueJson) {
|
||||||
|
issue = JSON.parse(issueJson);
|
||||||
|
} else {
|
||||||
console.log(`Issue ${id} not found. Creating...`);
|
console.log(`Issue ${id} not found. Creating...`);
|
||||||
issue = {
|
issue = {
|
||||||
id,
|
id,
|
||||||
@@ -114,12 +139,13 @@ for (const id of issueIds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Group into batches
|
// Group into batches
|
||||||
const batchSize = flags.batchSize || 3;
|
|
||||||
const batches = [];
|
const batches = [];
|
||||||
for (let i = 0; i < issues.length; i += batchSize) {
|
for (let i = 0; i < issues.length; i += batchSize) {
|
||||||
batches.push(issues.slice(i, i + batchSize));
|
batches.push(issues.slice(i, i + batchSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Processing ${issues.length} issues in ${batches.length} batch(es)`);
|
||||||
|
|
||||||
TodoWrite({
|
TodoWrite({
|
||||||
todos: batches.flatMap((batch, i) => [
|
todos: batches.flatMap((batch, i) => [
|
||||||
{ content: `Plan batch ${i+1}`, status: 'pending', activeForm: `Planning batch ${i+1}` }
|
{ content: `Plan batch ${i+1}`, status: 'pending', activeForm: `Planning batch ${i+1}` }
|
||||||
@@ -130,10 +156,13 @@ TodoWrite({
|
|||||||
### Phase 2: Unified Explore + Plan (issue-plan-agent)
|
### Phase 2: Unified Explore + Plan (issue-plan-agent)
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
// Ensure solutions directory exists
|
||||||
|
Bash(`mkdir -p .workflow/issues/solutions`);
|
||||||
|
|
||||||
for (const [batchIndex, batch] of batches.entries()) {
|
for (const [batchIndex, batch] of batches.entries()) {
|
||||||
updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress');
|
updateTodo(`Plan batch ${batchIndex + 1}`, 'in_progress');
|
||||||
|
|
||||||
// Build issue prompt for agent with lifecycle requirements
|
// Build issue prompt for agent - agent writes solutions directly
|
||||||
const issuePrompt = `
|
const issuePrompt = `
|
||||||
## Issues to Plan (Closed-Loop Tasks Required)
|
## Issues to Plan (Closed-Loop Tasks Required)
|
||||||
|
|
||||||
@@ -152,7 +181,29 @@ ${batch.map((issue, i) => `
|
|||||||
## Project Root
|
## Project Root
|
||||||
${process.cwd()}
|
${process.cwd()}
|
||||||
|
|
||||||
## Requirements - CLOSED-LOOP TASKS
|
## Output Requirements
|
||||||
|
|
||||||
|
**IMPORTANT**: Write solutions DIRECTLY to files, do NOT return full solution content.
|
||||||
|
|
||||||
|
### 1. Write Solution Files
|
||||||
|
For each issue, write solution to: \`.workflow/issues/solutions/{issue-id}.jsonl\`
|
||||||
|
- Append one JSON line per solution
|
||||||
|
- Solution must include all closed-loop task fields (see Solution Format below)
|
||||||
|
|
||||||
|
### 2. Return Summary Only
|
||||||
|
After writing solutions, return ONLY a brief JSON summary:
|
||||||
|
\`\`\`json
|
||||||
|
{
|
||||||
|
"planned": [
|
||||||
|
{ "issue_id": "XXX", "solution_id": "SOL-xxx", "task_count": 3, "description": "Brief description" }
|
||||||
|
],
|
||||||
|
"conflicts": [
|
||||||
|
{ "file": "path/to/file", "issues": ["ID1", "ID2"], "suggested_order": ["ID1", "ID2"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Closed-Loop Task Requirements
|
||||||
|
|
||||||
Each task MUST include ALL lifecycle phases:
|
Each task MUST include ALL lifecycle phases:
|
||||||
|
|
||||||
@@ -168,12 +219,10 @@ Each task MUST include ALL lifecycle phases:
|
|||||||
|
|
||||||
### 3. Regression
|
### 3. Regression
|
||||||
- regression: string[] (commands to run for regression check)
|
- regression: string[] (commands to run for regression check)
|
||||||
- Based on issue's regression_scope setting
|
|
||||||
|
|
||||||
### 4. Acceptance
|
### 4. Acceptance
|
||||||
- acceptance.criteria: string[] (testable acceptance criteria)
|
- acceptance.criteria: string[] (testable acceptance criteria)
|
||||||
- acceptance.verification: string[] (how to verify each criterion)
|
- acceptance.verification: string[] (how to verify each criterion)
|
||||||
- acceptance.manual_checks: string[] (manual checks if needed)
|
|
||||||
|
|
||||||
### 5. Commit
|
### 5. Commit
|
||||||
- commit.type: feat|fix|refactor|test|docs|chore
|
- commit.type: feat|fix|refactor|test|docs|chore
|
||||||
@@ -188,7 +237,7 @@ Each task MUST include ALL lifecycle phases:
|
|||||||
4. Infer commit scope from affected files
|
4. Infer commit scope from affected files
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Launch issue-plan-agent (combines explore + plan)
|
// Launch issue-plan-agent - agent writes solutions directly
|
||||||
const result = Task(
|
const result = Task(
|
||||||
subagent_type="issue-plan-agent",
|
subagent_type="issue-plan-agent",
|
||||||
run_in_background=false,
|
run_in_background=false,
|
||||||
@@ -196,24 +245,18 @@ Each task MUST include ALL lifecycle phases:
|
|||||||
prompt=issuePrompt
|
prompt=issuePrompt
|
||||||
);
|
);
|
||||||
|
|
||||||
// Parse agent output
|
// Parse brief summary from agent
|
||||||
const agentOutput = JSON.parse(result);
|
const summary = JSON.parse(result);
|
||||||
|
|
||||||
// Register solutions for each issue (append to solutions/{issue-id}.jsonl)
|
// Display planning results
|
||||||
for (const item of agentOutput.solutions) {
|
for (const item of summary.planned || []) {
|
||||||
const solutionPath = `.workflow/issues/solutions/${item.issue_id}.jsonl`;
|
console.log(`✓ ${item.issue_id}: ${item.solution_id} (${item.task_count} tasks) - ${item.description}`);
|
||||||
|
|
||||||
// Ensure solutions directory exists
|
|
||||||
Bash(`mkdir -p .workflow/issues/solutions`);
|
|
||||||
|
|
||||||
// Append solution as new line
|
|
||||||
Bash(`echo '${JSON.stringify(item.solution)}' >> "${solutionPath}"`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle conflicts if any
|
// Handle conflicts if any
|
||||||
if (agentOutput.conflicts?.length > 0) {
|
if (summary.conflicts?.length > 0) {
|
||||||
console.log(`\n⚠ File conflicts detected:`);
|
console.log(`\n⚠ File conflicts detected:`);
|
||||||
agentOutput.conflicts.forEach(c => {
|
summary.conflicts.forEach(c => {
|
||||||
console.log(` ${c.file}: ${c.issues.join(', ')} → suggested: ${c.suggested_order.join(' → ')}`);
|
console.log(` ${c.file}: ${c.issues.join(', ')} → suggested: ${c.suggested_order.join(' → ')}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -225,90 +268,86 @@ Each task MUST include ALL lifecycle phases:
|
|||||||
### Phase 3: Solution Binding
|
### Phase 3: Solution Binding
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Re-read issues.jsonl
|
// Collect issues needing user selection (multiple solutions)
|
||||||
let allIssuesUpdated = Bash(`cat "${issuesPath}"`)
|
const needSelection = [];
|
||||||
.split('\n')
|
|
||||||
.filter(line => line.trim())
|
|
||||||
.map(line => JSON.parse(line));
|
|
||||||
|
|
||||||
for (const issue of issues) {
|
for (const issue of issues) {
|
||||||
const solPath = `.workflow/issues/solutions/${issue.id}.jsonl`;
|
const solPath = `.workflow/issues/solutions/${issue.id}.jsonl`;
|
||||||
const solutions = Bash(`cat "${solPath}" 2>/dev/null || echo ''`)
|
|
||||||
.split('\n')
|
|
||||||
.filter(line => line.trim())
|
|
||||||
.map(line => JSON.parse(line));
|
|
||||||
|
|
||||||
if (solutions.length === 0) {
|
// Use jq to count solutions
|
||||||
console.log(`⚠ No solutions for ${issue.id}`);
|
const count = parseInt(Bash(`cat "${solPath}" 2>/dev/null | jq -s 'length' 2>/dev/null || echo '0'`).trim()) || 0;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedSolId;
|
if (count === 0) continue; // No solutions - skip silently (agent already reported)
|
||||||
|
|
||||||
if (solutions.length === 1) {
|
if (count === 1) {
|
||||||
// Auto-bind single solution
|
// Auto-bind single solution
|
||||||
selectedSolId = solutions[0].id;
|
const solId = Bash(`cat "${solPath}" | jq -r '.id' | head -1`).trim();
|
||||||
console.log(`✓ Auto-bound ${selectedSolId} to ${issue.id} (${solutions[0].tasks?.length || 0} tasks)`);
|
bindSolution(issue.id, solId);
|
||||||
} else {
|
} else {
|
||||||
// Multiple solutions - ask user
|
// Multiple solutions - collect for batch selection
|
||||||
const answer = AskUserQuestion({
|
const options = Bash(`cat "${solPath}" | jq -c '{id, description, task_count: (.tasks | length)}'`).trim();
|
||||||
questions: [{
|
needSelection.push({ issue, options: options.split('\n').map(s => JSON.parse(s)) });
|
||||||
question: `Select solution for ${issue.id}:`,
|
|
||||||
header: issue.id,
|
|
||||||
multiSelect: false,
|
|
||||||
options: solutions.map(s => ({
|
|
||||||
label: `${s.id}: ${s.description || 'Solution'}`,
|
|
||||||
description: `${s.tasks?.length || 0} tasks`
|
|
||||||
}))
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedSolId = extractSelectedSolutionId(answer);
|
|
||||||
console.log(`✓ Bound ${selectedSolId} to ${issue.id}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update issue in allIssuesUpdated
|
|
||||||
const issueIndex = allIssuesUpdated.findIndex(i => i.id === issue.id);
|
|
||||||
if (issueIndex !== -1) {
|
|
||||||
allIssuesUpdated[issueIndex].bound_solution_id = selectedSolId;
|
|
||||||
allIssuesUpdated[issueIndex].status = 'planned';
|
|
||||||
allIssuesUpdated[issueIndex].planned_at = new Date().toISOString();
|
|
||||||
allIssuesUpdated[issueIndex].updated_at = new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark solution as bound in solutions file
|
|
||||||
const updatedSolutions = solutions.map(s => ({
|
|
||||||
...s,
|
|
||||||
is_bound: s.id === selectedSolId,
|
|
||||||
bound_at: s.id === selectedSolId ? new Date().toISOString() : s.bound_at
|
|
||||||
}));
|
|
||||||
Write(solPath, updatedSolutions.map(s => JSON.stringify(s)).join('\n'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write updated issues.jsonl
|
// Batch ask user for multiple-solution issues
|
||||||
Write(issuesPath, allIssuesUpdated.map(i => JSON.stringify(i)).join('\n'));
|
if (needSelection.length > 0) {
|
||||||
|
const answer = AskUserQuestion({
|
||||||
|
questions: needSelection.map(({ issue, options }) => ({
|
||||||
|
question: `Select solution for ${issue.id}:`,
|
||||||
|
header: issue.id,
|
||||||
|
multiSelect: false,
|
||||||
|
options: options.map(s => ({
|
||||||
|
label: `${s.id} (${s.task_count} tasks)`,
|
||||||
|
description: s.description || 'Solution'
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bind selected solutions
|
||||||
|
for (const { issue } of needSelection) {
|
||||||
|
const selectedSolId = extractSelectedSolutionId(answer, issue.id);
|
||||||
|
if (selectedSolId) bindSolution(issue.id, selectedSolId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: bind solution to issue
|
||||||
|
function bindSolution(issueId, solutionId) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const solPath = `.workflow/issues/solutions/${issueId}.jsonl`;
|
||||||
|
|
||||||
|
// Update issue status
|
||||||
|
Bash(`
|
||||||
|
tmpfile=$(mktemp) && \\
|
||||||
|
cat "${issuesPath}" | jq -c 'if .id == "${issueId}" then . + {
|
||||||
|
bound_solution_id: "${solutionId}", status: "planned",
|
||||||
|
planned_at: "${now}", updated_at: "${now}"
|
||||||
|
} else . end' > "$tmpfile" && mv "$tmpfile" "${issuesPath}"
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Mark solution as bound
|
||||||
|
Bash(`
|
||||||
|
tmpfile=$(mktemp) && \\
|
||||||
|
cat "${solPath}" | jq -c 'if .id == "${solutionId}" then . + {
|
||||||
|
is_bound: true, bound_at: "${now}"
|
||||||
|
} else . + {is_bound: false} end' > "$tmpfile" && mv "$tmpfile" "${solPath}"
|
||||||
|
`);
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Phase 4: Summary
|
### Phase 4: Summary
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
// Brief summary using jq
|
||||||
|
const stats = Bash(`
|
||||||
|
cat "${issuesPath}" 2>/dev/null | \\
|
||||||
|
jq -s '[.[] | select(.status == "planned")] | length' 2>/dev/null || echo '0'
|
||||||
|
`).trim();
|
||||||
|
|
||||||
console.log(`
|
console.log(`
|
||||||
## Planning Complete
|
## Done: ${issues.length} issues → ${stats} planned
|
||||||
|
|
||||||
**Issues Planned**: ${issues.length}
|
Next: \`/issue:queue\` → \`/issue:execute\`
|
||||||
|
|
||||||
### Bound Solutions
|
|
||||||
${issues.map(i => {
|
|
||||||
const issue = allIssuesUpdated.find(a => a.id === i.id);
|
|
||||||
return issue?.bound_solution_id
|
|
||||||
? `✓ ${i.id}: ${issue.bound_solution_id}`
|
|
||||||
: `○ ${i.id}: No solution bound`;
|
|
||||||
}).join('\n')}
|
|
||||||
|
|
||||||
### Next Steps
|
|
||||||
1. Review: \`ccw issue status <issue-id>\`
|
|
||||||
2. Form queue: \`/issue:queue\`
|
|
||||||
3. Execute: \`/issue:execute\`
|
|
||||||
`);
|
`);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -936,8 +936,11 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
|
|||||||
async function nextAction(options: IssueOptions): Promise<void> {
|
async function nextAction(options: IssueOptions): Promise<void> {
|
||||||
const queue = readActiveQueue();
|
const queue = readActiveQueue();
|
||||||
|
|
||||||
// Find ready tasks
|
// Priority 1: Resume executing tasks (interrupted/crashed)
|
||||||
const readyTasks = queue.queue.filter(item => {
|
const executingTasks = queue.queue.filter(item => item.status === 'executing');
|
||||||
|
|
||||||
|
// Priority 2: Find pending tasks with satisfied dependencies
|
||||||
|
const pendingTasks = queue.queue.filter(item => {
|
||||||
if (item.status !== 'pending') return false;
|
if (item.status !== 'pending') return false;
|
||||||
return item.depends_on.every(depId => {
|
return item.depends_on.every(depId => {
|
||||||
const dep = queue.queue.find(q => q.queue_id === depId);
|
const dep = queue.queue.find(q => q.queue_id === depId);
|
||||||
@@ -945,6 +948,9 @@ async function nextAction(options: IssueOptions): Promise<void> {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Combine: executing first, then pending
|
||||||
|
const readyTasks = [...executingTasks, ...pendingTasks];
|
||||||
|
|
||||||
if (readyTasks.length === 0) {
|
if (readyTasks.length === 0) {
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
status: 'empty',
|
status: 'empty',
|
||||||
@@ -957,6 +963,7 @@ async function nextAction(options: IssueOptions): Promise<void> {
|
|||||||
// Sort by execution order
|
// Sort by execution order
|
||||||
readyTasks.sort((a, b) => a.execution_order - b.execution_order);
|
readyTasks.sort((a, b) => a.execution_order - b.execution_order);
|
||||||
const nextItem = readyTasks[0];
|
const nextItem = readyTasks[0];
|
||||||
|
const isResume = nextItem.status === 'executing';
|
||||||
|
|
||||||
// Load task definition
|
// Load task definition
|
||||||
const solution = findSolution(nextItem.issue_id, nextItem.solution_id);
|
const solution = findSolution(nextItem.issue_id, nextItem.solution_id);
|
||||||
@@ -967,14 +974,24 @@ async function nextAction(options: IssueOptions): Promise<void> {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as executing
|
// Only update status if not already executing (new task)
|
||||||
const idx = queue.queue.findIndex(q => q.queue_id === nextItem.queue_id);
|
if (!isResume) {
|
||||||
queue.queue[idx].status = 'executing';
|
const idx = queue.queue.findIndex(q => q.queue_id === nextItem.queue_id);
|
||||||
queue.queue[idx].started_at = new Date().toISOString();
|
queue.queue[idx].status = 'executing';
|
||||||
writeQueue(queue);
|
queue.queue[idx].started_at = new Date().toISOString();
|
||||||
|
writeQueue(queue);
|
||||||
|
updateIssue(nextItem.issue_id, { status: 'executing' });
|
||||||
|
}
|
||||||
|
|
||||||
// Update issue status
|
// Calculate queue stats for context
|
||||||
updateIssue(nextItem.issue_id, { status: 'executing' });
|
const stats = {
|
||||||
|
total: queue.queue.length,
|
||||||
|
completed: queue.queue.filter(q => q.status === 'completed').length,
|
||||||
|
failed: queue.queue.filter(q => q.status === 'failed').length,
|
||||||
|
executing: executingTasks.length,
|
||||||
|
pending: pendingTasks.length
|
||||||
|
};
|
||||||
|
const remaining = stats.pending + stats.executing;
|
||||||
|
|
||||||
console.log(JSON.stringify({
|
console.log(JSON.stringify({
|
||||||
queue_id: nextItem.queue_id,
|
queue_id: nextItem.queue_id,
|
||||||
@@ -982,9 +999,17 @@ async function nextAction(options: IssueOptions): Promise<void> {
|
|||||||
solution_id: nextItem.solution_id,
|
solution_id: nextItem.solution_id,
|
||||||
task: taskDef,
|
task: taskDef,
|
||||||
context: solution?.exploration_context || {},
|
context: solution?.exploration_context || {},
|
||||||
|
resumed: isResume,
|
||||||
|
resume_note: isResume ? `Resuming interrupted task (started: ${nextItem.started_at})` : undefined,
|
||||||
execution_hints: {
|
execution_hints: {
|
||||||
executor: nextItem.assigned_executor,
|
executor: nextItem.assigned_executor,
|
||||||
estimated_minutes: taskDef.estimated_minutes || 30
|
estimated_minutes: taskDef.estimated_minutes || 30
|
||||||
|
},
|
||||||
|
queue_progress: {
|
||||||
|
completed: stats.completed,
|
||||||
|
remaining: remaining,
|
||||||
|
total: stats.total,
|
||||||
|
progress: `${stats.completed}/${stats.total}`
|
||||||
}
|
}
|
||||||
}, null, 2));
|
}, null, 2));
|
||||||
}
|
}
|
||||||
@@ -1054,7 +1079,7 @@ async function doneAction(queueId: string | undefined, options: IssueOptions): P
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* retry - Retry failed tasks
|
* retry - Retry failed tasks, or reset stuck executing tasks (--force)
|
||||||
*/
|
*/
|
||||||
async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
|
async function retryAction(issueId: string | undefined, options: IssueOptions): Promise<void> {
|
||||||
const queue = readActiveQueue();
|
const queue = readActiveQueue();
|
||||||
@@ -1066,7 +1091,12 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
|
|||||||
|
|
||||||
let updated = 0;
|
let updated = 0;
|
||||||
|
|
||||||
|
// Check for stuck executing tasks (started > 30 min ago with no completion)
|
||||||
|
const stuckThreshold = 30 * 60 * 1000; // 30 minutes
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
for (const item of queue.queue) {
|
for (const item of queue.queue) {
|
||||||
|
// Retry failed tasks
|
||||||
if (item.status === 'failed') {
|
if (item.status === 'failed') {
|
||||||
if (!issueId || item.issue_id === issueId) {
|
if (!issueId || item.issue_id === issueId) {
|
||||||
item.status = 'pending';
|
item.status = 'pending';
|
||||||
@@ -1076,10 +1106,23 @@ async function retryAction(issueId: string | undefined, options: IssueOptions):
|
|||||||
updated++;
|
updated++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Reset stuck executing tasks (optional: use --force or --reset-stuck)
|
||||||
|
else if (item.status === 'executing' && options.force) {
|
||||||
|
const startedAt = item.started_at ? new Date(item.started_at).getTime() : 0;
|
||||||
|
if (now - startedAt > stuckThreshold) {
|
||||||
|
if (!issueId || item.issue_id === issueId) {
|
||||||
|
console.log(chalk.yellow(`Resetting stuck task: ${item.queue_id} (started ${Math.round((now - startedAt) / 60000)} min ago)`));
|
||||||
|
item.status = 'pending';
|
||||||
|
item.started_at = undefined;
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updated === 0) {
|
if (updated === 0) {
|
||||||
console.log(chalk.yellow('No failed tasks to retry'));
|
console.log(chalk.yellow('No failed/stuck tasks to retry'));
|
||||||
|
console.log(chalk.gray('Use --force to reset stuck executing tasks (>30 min)'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1160,7 +1203,7 @@ export async function issueCommand(
|
|||||||
console.log(chalk.gray(' queue add <issue-id> Add issue to active queue (or create new)'));
|
console.log(chalk.gray(' queue add <issue-id> Add issue to active queue (or create new)'));
|
||||||
console.log(chalk.gray(' queue switch <queue-id> Switch active queue'));
|
console.log(chalk.gray(' queue switch <queue-id> Switch active queue'));
|
||||||
console.log(chalk.gray(' queue archive Archive current queue'));
|
console.log(chalk.gray(' queue archive Archive current queue'));
|
||||||
console.log(chalk.gray(' retry [issue-id] Retry failed tasks'));
|
console.log(chalk.gray(' retry [issue-id] [--force] Retry failed/stuck tasks'));
|
||||||
console.log();
|
console.log();
|
||||||
console.log(chalk.bold('Execution Endpoints:'));
|
console.log(chalk.bold('Execution Endpoints:'));
|
||||||
console.log(chalk.gray(' next Get next ready task (JSON)'));
|
console.log(chalk.gray(' next Get next ready task (JSON)'));
|
||||||
|
|||||||
Reference in New Issue
Block a user