claude-code-workflow 6.3.4 → 6.3.6
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 +859 -0
- package/.claude/agents/issue-queue-agent.md +702 -0
- package/.claude/commands/issue/execute.md +453 -0
- package/.claude/commands/issue/manage.md +865 -0
- package/.claude/commands/issue/new.md +484 -0
- package/.claude/commands/issue/plan.md +421 -0
- package/.claude/commands/issue/queue.md +354 -0
- package/.claude/commands/{clean.md → workflow/clean.md} +5 -5
- package/.claude/commands/workflow/docs/analyze.md +1467 -0
- package/.claude/commands/workflow/docs/copyright.md +1265 -0
- package/.claude/commands/workflow/execute.md +0 -1
- package/.claude/commands/workflow/tools/conflict-resolution.md +76 -240
- package/.claude/commands/workflow/tools/context-gather.md +0 -2
- package/.claude/commands/workflow/tools/task-generate-agent.md +81 -8
- package/.claude/commands/workflow/tools/task-generate-tdd.md +0 -9
- package/.claude/commands/workflow/tools/test-context-gather.md +2 -3
- package/.claude/commands/workflow/tools/test-task-generate.md +0 -2
- package/.claude/skills/_shared/mermaid-utils.md +584 -0
- package/.claude/skills/command-guide/reference/agents/action-planning-agent.md +0 -2
- package/.claude/skills/command-guide/reference/commands/workflow/execute.md +1 -1
- package/.claude/skills/command-guide/reference/commands/workflow/tools/context-gather.md +1 -2
- package/.claude/skills/command-guide/reference/commands/workflow/tools/task-generate-tdd.md +1 -8
- package/.claude/skills/command-guide/reference/commands/workflow/tools/test-context-gather.md +1 -4
- package/.claude/skills/command-guide/reference/commands/workflow/tools/test-task-generate.md +0 -2
- package/.claude/skills/copyright-docs/SKILL.md +132 -0
- package/.claude/skills/copyright-docs/phases/01-metadata-collection.md +78 -0
- package/.claude/skills/copyright-docs/phases/01.5-project-exploration.md +150 -0
- package/.claude/skills/copyright-docs/phases/02-deep-analysis.md +664 -0
- package/.claude/skills/copyright-docs/phases/02.5-consolidation.md +192 -0
- package/.claude/skills/copyright-docs/phases/04-document-assembly.md +261 -0
- package/.claude/skills/copyright-docs/phases/05-compliance-refinement.md +192 -0
- package/.claude/skills/copyright-docs/specs/cpcc-requirements.md +121 -0
- package/.claude/skills/copyright-docs/templates/agent-base.md +200 -0
- package/.claude/skills/project-analyze/SKILL.md +162 -0
- package/.claude/skills/project-analyze/phases/01-requirements-discovery.md +79 -0
- package/.claude/skills/project-analyze/phases/02-project-exploration.md +176 -0
- package/.claude/skills/project-analyze/phases/03-deep-analysis.md +854 -0
- package/.claude/skills/project-analyze/phases/03.5-consolidation.md +233 -0
- package/.claude/skills/project-analyze/phases/04-report-generation.md +217 -0
- package/.claude/skills/project-analyze/phases/05-iterative-refinement.md +124 -0
- package/.claude/skills/project-analyze/specs/quality-standards.md +115 -0
- package/.claude/skills/project-analyze/specs/writing-style.md +152 -0
- package/.claude/workflows/cli-templates/schemas/conflict-resolution-schema.json +79 -65
- package/.claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json +136 -0
- package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +74 -0
- package/.claude/workflows/cli-templates/schemas/queue-schema.json +136 -0
- package/.claude/workflows/cli-templates/schemas/registry-schema.json +94 -0
- package/.claude/workflows/cli-templates/schemas/solution-schema.json +120 -0
- package/.claude/workflows/cli-templates/schemas/solutions-jsonl-schema.json +125 -0
- package/.codex/prompts/issue-execute.md +266 -0
- package/README.md +11 -1
- package/ccw/dist/cli.d.ts.map +1 -1
- package/ccw/dist/cli.js +25 -0
- package/ccw/dist/cli.js.map +1 -1
- package/ccw/dist/commands/cli.d.ts.map +1 -1
- package/ccw/dist/commands/cli.js +46 -8
- package/ccw/dist/commands/cli.js.map +1 -1
- package/ccw/dist/commands/issue.d.ts +21 -0
- package/ccw/dist/commands/issue.d.ts.map +1 -0
- package/ccw/dist/commands/issue.js +895 -0
- package/ccw/dist/commands/issue.js.map +1 -0
- package/ccw/dist/core/dashboard-generator-patch.js +1 -0
- package/ccw/dist/core/dashboard-generator-patch.js.map +1 -1
- package/ccw/dist/core/routes/cli-routes.js +2 -2
- package/ccw/dist/core/routes/cli-routes.js.map +1 -1
- package/ccw/dist/core/routes/issue-routes.d.ts +34 -0
- package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/issue-routes.js +487 -0
- package/ccw/dist/core/routes/issue-routes.js.map +1 -0
- package/ccw/dist/core/server.d.ts.map +1 -1
- package/ccw/dist/core/server.js +17 -2
- package/ccw/dist/core/server.js.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.d.ts +7 -3
- package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.js +31 -17
- package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
- package/ccw/dist/tools/smart-search.d.ts +25 -0
- package/ccw/dist/tools/smart-search.d.ts.map +1 -1
- package/ccw/dist/tools/smart-search.js +121 -17
- package/ccw/dist/tools/smart-search.js.map +1 -1
- package/ccw/src/cli.ts +26 -0
- package/ccw/src/commands/cli.ts +49 -7
- package/ccw/src/commands/issue.ts +1184 -0
- package/ccw/src/core/dashboard-generator-patch.ts +1 -0
- package/ccw/src/core/routes/cli-routes.ts +3 -3
- package/ccw/src/core/routes/issue-routes.ts +559 -0
- package/ccw/src/core/server.ts +17 -2
- package/ccw/src/templates/dashboard-css/32-issue-manager.css +2544 -0
- package/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +467 -0
- package/ccw/src/templates/dashboard-js/components/cli-history.js +40 -13
- package/ccw/src/templates/dashboard-js/components/cli-status.js +26 -2
- package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +461 -0
- package/ccw/src/templates/dashboard-js/components/navigation.js +8 -0
- package/ccw/src/templates/dashboard-js/components/notifications.js +16 -0
- package/ccw/src/templates/dashboard-js/i18n.js +290 -2
- package/ccw/src/templates/dashboard-js/views/cli-manager.js +5 -0
- package/ccw/src/templates/dashboard-js/views/history.js +19 -4
- package/ccw/src/templates/dashboard-js/views/hook-manager.js +11 -5
- package/ccw/src/templates/dashboard-js/views/issue-manager.js +1546 -0
- package/ccw/src/templates/dashboard.html +55 -0
- package/ccw/src/tools/claude-cli-tools.ts +37 -20
- package/ccw/src/tools/smart-search.ts +157 -16
- package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/config.py +5 -0
- package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/hybrid_search.py +144 -11
- package/codex-lens/src/codexlens/search/ranking.py +267 -1
- package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/chunker.py +55 -10
- package/package.json +2 -2
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue Command - Unified JSONL storage with CLI & API compatibility
|
|
3
|
+
* Storage: issues.jsonl + solutions/{issue-id}.jsonl + queue.json
|
|
4
|
+
* Commands: init, list, status, task, bind, queue, next, done, retry
|
|
5
|
+
*/
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
8
|
+
import { join, resolve } from 'path';
|
|
9
|
+
// Handle EPIPE errors gracefully
|
|
10
|
+
process.stdout.on('error', (err) => {
|
|
11
|
+
if (err.code === 'EPIPE') {
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
throw err;
|
|
15
|
+
});
|
|
16
|
+
const ISSUES_DIR = '.workflow/issues';
|
|
17
|
+
// ============ Storage Layer (JSONL) ============
|
|
18
|
+
function getProjectRoot() {
|
|
19
|
+
let dir = process.cwd();
|
|
20
|
+
while (dir !== resolve(dir, '..')) {
|
|
21
|
+
if (existsSync(join(dir, '.workflow')) || existsSync(join(dir, '.git'))) {
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
dir = resolve(dir, '..');
|
|
25
|
+
}
|
|
26
|
+
return process.cwd();
|
|
27
|
+
}
|
|
28
|
+
function getIssuesDir() {
|
|
29
|
+
return join(getProjectRoot(), ISSUES_DIR);
|
|
30
|
+
}
|
|
31
|
+
function ensureIssuesDir() {
|
|
32
|
+
const dir = getIssuesDir();
|
|
33
|
+
if (!existsSync(dir)) {
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ============ Issues JSONL ============
|
|
38
|
+
function readIssues() {
|
|
39
|
+
const path = join(getIssuesDir(), 'issues.jsonl');
|
|
40
|
+
if (!existsSync(path))
|
|
41
|
+
return [];
|
|
42
|
+
try {
|
|
43
|
+
return readFileSync(path, 'utf-8')
|
|
44
|
+
.split('\n')
|
|
45
|
+
.filter(line => line.trim())
|
|
46
|
+
.map(line => JSON.parse(line));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function writeIssues(issues) {
|
|
53
|
+
ensureIssuesDir();
|
|
54
|
+
const path = join(getIssuesDir(), 'issues.jsonl');
|
|
55
|
+
writeFileSync(path, issues.map(i => JSON.stringify(i)).join('\n'), 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
function findIssue(issueId) {
|
|
58
|
+
return readIssues().find(i => i.id === issueId);
|
|
59
|
+
}
|
|
60
|
+
function updateIssue(issueId, updates) {
|
|
61
|
+
const issues = readIssues();
|
|
62
|
+
const idx = issues.findIndex(i => i.id === issueId);
|
|
63
|
+
if (idx === -1)
|
|
64
|
+
return false;
|
|
65
|
+
issues[idx] = { ...issues[idx], ...updates, updated_at: new Date().toISOString() };
|
|
66
|
+
writeIssues(issues);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
// ============ Solutions JSONL ============
|
|
70
|
+
function getSolutionsPath(issueId) {
|
|
71
|
+
return join(getIssuesDir(), 'solutions', `${issueId}.jsonl`);
|
|
72
|
+
}
|
|
73
|
+
function readSolutions(issueId) {
|
|
74
|
+
const path = getSolutionsPath(issueId);
|
|
75
|
+
if (!existsSync(path))
|
|
76
|
+
return [];
|
|
77
|
+
try {
|
|
78
|
+
return readFileSync(path, 'utf-8')
|
|
79
|
+
.split('\n')
|
|
80
|
+
.filter(line => line.trim())
|
|
81
|
+
.map(line => JSON.parse(line));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function writeSolutions(issueId, solutions) {
|
|
88
|
+
const dir = join(getIssuesDir(), 'solutions');
|
|
89
|
+
if (!existsSync(dir))
|
|
90
|
+
mkdirSync(dir, { recursive: true });
|
|
91
|
+
writeFileSync(getSolutionsPath(issueId), solutions.map(s => JSON.stringify(s)).join('\n'), 'utf-8');
|
|
92
|
+
}
|
|
93
|
+
function findSolution(issueId, solutionId) {
|
|
94
|
+
return readSolutions(issueId).find(s => s.id === solutionId);
|
|
95
|
+
}
|
|
96
|
+
function getBoundSolution(issueId) {
|
|
97
|
+
return readSolutions(issueId).find(s => s.is_bound);
|
|
98
|
+
}
|
|
99
|
+
function generateSolutionId() {
|
|
100
|
+
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
101
|
+
return `SOL-${ts}`;
|
|
102
|
+
}
|
|
103
|
+
// ============ Queue Management (Multi-Queue) ============
|
|
104
|
+
function getQueuesDir() {
|
|
105
|
+
return join(getIssuesDir(), 'queues');
|
|
106
|
+
}
|
|
107
|
+
function ensureQueuesDir() {
|
|
108
|
+
const dir = getQueuesDir();
|
|
109
|
+
if (!existsSync(dir)) {
|
|
110
|
+
mkdirSync(dir, { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function readQueueIndex() {
|
|
114
|
+
const path = join(getQueuesDir(), 'index.json');
|
|
115
|
+
if (!existsSync(path)) {
|
|
116
|
+
return { active_queue_id: null, queues: [] };
|
|
117
|
+
}
|
|
118
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
119
|
+
}
|
|
120
|
+
function writeQueueIndex(index) {
|
|
121
|
+
ensureQueuesDir();
|
|
122
|
+
writeFileSync(join(getQueuesDir(), 'index.json'), JSON.stringify(index, null, 2), 'utf-8');
|
|
123
|
+
}
|
|
124
|
+
function generateQueueFileId() {
|
|
125
|
+
const now = new Date();
|
|
126
|
+
const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
127
|
+
return `QUE-${ts}`;
|
|
128
|
+
}
|
|
129
|
+
function readQueue(queueId) {
|
|
130
|
+
const index = readQueueIndex();
|
|
131
|
+
const targetId = queueId || index.active_queue_id;
|
|
132
|
+
if (!targetId)
|
|
133
|
+
return null;
|
|
134
|
+
const path = join(getQueuesDir(), `${targetId}.json`);
|
|
135
|
+
if (!existsSync(path))
|
|
136
|
+
return null;
|
|
137
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
138
|
+
}
|
|
139
|
+
function readActiveQueue() {
|
|
140
|
+
const queue = readQueue();
|
|
141
|
+
if (queue)
|
|
142
|
+
return queue;
|
|
143
|
+
// Return empty queue structure if no active queue
|
|
144
|
+
return createEmptyQueue();
|
|
145
|
+
}
|
|
146
|
+
function createEmptyQueue() {
|
|
147
|
+
return {
|
|
148
|
+
id: generateQueueFileId(),
|
|
149
|
+
status: 'active',
|
|
150
|
+
issue_ids: [],
|
|
151
|
+
queue: [],
|
|
152
|
+
conflicts: [],
|
|
153
|
+
_metadata: {
|
|
154
|
+
version: '2.0',
|
|
155
|
+
total_tasks: 0,
|
|
156
|
+
pending_count: 0,
|
|
157
|
+
executing_count: 0,
|
|
158
|
+
completed_count: 0,
|
|
159
|
+
failed_count: 0,
|
|
160
|
+
created_at: new Date().toISOString(),
|
|
161
|
+
updated_at: new Date().toISOString()
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function writeQueue(queue) {
|
|
166
|
+
ensureQueuesDir();
|
|
167
|
+
// Update metadata counts
|
|
168
|
+
queue._metadata.total_tasks = queue.queue.length;
|
|
169
|
+
queue._metadata.pending_count = queue.queue.filter(q => q.status === 'pending').length;
|
|
170
|
+
queue._metadata.executing_count = queue.queue.filter(q => q.status === 'executing').length;
|
|
171
|
+
queue._metadata.completed_count = queue.queue.filter(q => q.status === 'completed').length;
|
|
172
|
+
queue._metadata.failed_count = queue.queue.filter(q => q.status === 'failed').length;
|
|
173
|
+
queue._metadata.updated_at = new Date().toISOString();
|
|
174
|
+
// Write queue file
|
|
175
|
+
const path = join(getQueuesDir(), `${queue.id}.json`);
|
|
176
|
+
writeFileSync(path, JSON.stringify(queue, null, 2), 'utf-8');
|
|
177
|
+
// Update index
|
|
178
|
+
const index = readQueueIndex();
|
|
179
|
+
const existingIdx = index.queues.findIndex(q => q.id === queue.id);
|
|
180
|
+
const indexEntry = {
|
|
181
|
+
id: queue.id,
|
|
182
|
+
status: queue.status,
|
|
183
|
+
issue_ids: queue.issue_ids,
|
|
184
|
+
total_tasks: queue._metadata.total_tasks,
|
|
185
|
+
completed_tasks: queue._metadata.completed_count,
|
|
186
|
+
created_at: queue._metadata.created_at,
|
|
187
|
+
completed_at: queue.status === 'completed' ? new Date().toISOString() : undefined
|
|
188
|
+
};
|
|
189
|
+
if (existingIdx >= 0) {
|
|
190
|
+
index.queues[existingIdx] = indexEntry;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
index.queues.unshift(indexEntry);
|
|
194
|
+
}
|
|
195
|
+
if (queue.status === 'active') {
|
|
196
|
+
index.active_queue_id = queue.id;
|
|
197
|
+
}
|
|
198
|
+
writeQueueIndex(index);
|
|
199
|
+
}
|
|
200
|
+
function generateQueueItemId(queue) {
|
|
201
|
+
const maxNum = queue.queue.reduce((max, q) => {
|
|
202
|
+
const match = q.queue_id.match(/^Q-(\d+)$/);
|
|
203
|
+
return match ? Math.max(max, parseInt(match[1])) : max;
|
|
204
|
+
}, 0);
|
|
205
|
+
return `Q-${String(maxNum + 1).padStart(3, '0')}`;
|
|
206
|
+
}
|
|
207
|
+
// ============ Commands ============
|
|
208
|
+
/**
|
|
209
|
+
* init - Initialize a new issue
|
|
210
|
+
*/
|
|
211
|
+
async function initAction(issueId, options) {
|
|
212
|
+
if (!issueId) {
|
|
213
|
+
console.error(chalk.red('Issue ID is required'));
|
|
214
|
+
console.error(chalk.gray('Usage: ccw issue init <issue-id> [--title "..."]'));
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
const existing = findIssue(issueId);
|
|
218
|
+
if (existing && !options.force) {
|
|
219
|
+
console.error(chalk.red(`Issue "${issueId}" already exists`));
|
|
220
|
+
console.error(chalk.gray('Use --force to reinitialize'));
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
const issues = readIssues().filter(i => i.id !== issueId);
|
|
224
|
+
const newIssue = {
|
|
225
|
+
id: issueId,
|
|
226
|
+
title: options.title || issueId,
|
|
227
|
+
status: 'registered',
|
|
228
|
+
priority: options.priority ? parseInt(options.priority) : 3,
|
|
229
|
+
context: options.description || '',
|
|
230
|
+
bound_solution_id: null,
|
|
231
|
+
solution_count: 0,
|
|
232
|
+
created_at: new Date().toISOString(),
|
|
233
|
+
updated_at: new Date().toISOString()
|
|
234
|
+
};
|
|
235
|
+
issues.push(newIssue);
|
|
236
|
+
writeIssues(issues);
|
|
237
|
+
console.log(chalk.green(`✓ Issue "${issueId}" initialized`));
|
|
238
|
+
console.log(chalk.gray(` Next: ccw issue task ${issueId} --title "Task title"`));
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* list - List issues or tasks
|
|
242
|
+
*/
|
|
243
|
+
async function listAction(issueId, options) {
|
|
244
|
+
if (!issueId) {
|
|
245
|
+
// List all issues
|
|
246
|
+
const issues = readIssues();
|
|
247
|
+
if (options.json) {
|
|
248
|
+
console.log(JSON.stringify(issues, null, 2));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (issues.length === 0) {
|
|
252
|
+
console.log(chalk.yellow('No issues found'));
|
|
253
|
+
console.log(chalk.gray('Create one with: ccw issue init <issue-id>'));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
console.log(chalk.bold.cyan('\nIssues\n'));
|
|
257
|
+
console.log(chalk.gray('ID'.padEnd(20) + 'Status'.padEnd(15) + 'Solutions'.padEnd(12) + 'Title'));
|
|
258
|
+
console.log(chalk.gray('-'.repeat(70)));
|
|
259
|
+
for (const issue of issues) {
|
|
260
|
+
const statusColor = {
|
|
261
|
+
'registered': chalk.gray,
|
|
262
|
+
'planning': chalk.blue,
|
|
263
|
+
'planned': chalk.cyan,
|
|
264
|
+
'queued': chalk.yellow,
|
|
265
|
+
'executing': chalk.yellow,
|
|
266
|
+
'completed': chalk.green,
|
|
267
|
+
'failed': chalk.red,
|
|
268
|
+
'paused': chalk.magenta
|
|
269
|
+
}[issue.status] || chalk.white;
|
|
270
|
+
const bound = issue.bound_solution_id ? `[${issue.bound_solution_id}]` : `${issue.solution_count}`;
|
|
271
|
+
console.log(issue.id.padEnd(20) +
|
|
272
|
+
statusColor(issue.status.padEnd(15)) +
|
|
273
|
+
bound.padEnd(12) +
|
|
274
|
+
(issue.title || '').substring(0, 30));
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// List tasks in bound solution
|
|
279
|
+
const issue = findIssue(issueId);
|
|
280
|
+
if (!issue) {
|
|
281
|
+
console.error(chalk.red(`Issue "${issueId}" not found`));
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
const solution = getBoundSolution(issueId);
|
|
285
|
+
const tasks = solution?.tasks || [];
|
|
286
|
+
if (options.json) {
|
|
287
|
+
console.log(JSON.stringify({ issue, solution, tasks }, null, 2));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
console.log(chalk.bold.cyan(`\nIssue: ${issueId}\n`));
|
|
291
|
+
console.log(`Title: ${issue.title}`);
|
|
292
|
+
console.log(`Status: ${issue.status}`);
|
|
293
|
+
console.log(`Bound: ${issue.bound_solution_id || 'none'}`);
|
|
294
|
+
console.log();
|
|
295
|
+
if (tasks.length === 0) {
|
|
296
|
+
console.log(chalk.yellow('No tasks (bind a solution first)'));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
console.log(chalk.gray('ID'.padEnd(8) + 'Action'.padEnd(12) + 'Scope'.padEnd(20) + 'Title'));
|
|
300
|
+
console.log(chalk.gray('-'.repeat(70)));
|
|
301
|
+
for (const task of tasks) {
|
|
302
|
+
console.log(task.id.padEnd(8) +
|
|
303
|
+
task.action.padEnd(12) +
|
|
304
|
+
task.scope.substring(0, 18).padEnd(20) +
|
|
305
|
+
task.title.substring(0, 30));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* status - Show detailed status
|
|
310
|
+
*/
|
|
311
|
+
async function statusAction(issueId, options) {
|
|
312
|
+
if (!issueId) {
|
|
313
|
+
// Show queue status
|
|
314
|
+
const queue = readActiveQueue();
|
|
315
|
+
const issues = readIssues();
|
|
316
|
+
const index = readQueueIndex();
|
|
317
|
+
if (options.json) {
|
|
318
|
+
console.log(JSON.stringify({ queue: queue._metadata, issues: issues.length, queues: index.queues.length }, null, 2));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
console.log(chalk.bold.cyan('\nSystem Status\n'));
|
|
322
|
+
console.log(`Issues: ${issues.length}`);
|
|
323
|
+
console.log(`Queues: ${index.queues.length} (Active: ${index.active_queue_id || 'none'})`);
|
|
324
|
+
console.log(`Active Queue: ${queue._metadata.total_tasks} tasks`);
|
|
325
|
+
console.log(` Pending: ${queue._metadata.pending_count}`);
|
|
326
|
+
console.log(` Executing: ${queue._metadata.executing_count}`);
|
|
327
|
+
console.log(` Completed: ${queue._metadata.completed_count}`);
|
|
328
|
+
console.log(` Failed: ${queue._metadata.failed_count}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const issue = findIssue(issueId);
|
|
332
|
+
if (!issue) {
|
|
333
|
+
console.error(chalk.red(`Issue "${issueId}" not found`));
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
const solutions = readSolutions(issueId);
|
|
337
|
+
const boundSol = solutions.find(s => s.is_bound);
|
|
338
|
+
if (options.json) {
|
|
339
|
+
console.log(JSON.stringify({ issue, solutions, bound: boundSol }, null, 2));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
console.log(chalk.bold.cyan(`\nIssue: ${issueId}\n`));
|
|
343
|
+
console.log(`Title: ${issue.title}`);
|
|
344
|
+
console.log(`Status: ${issue.status}`);
|
|
345
|
+
console.log(`Priority: ${issue.priority}`);
|
|
346
|
+
console.log(`Created: ${issue.created_at}`);
|
|
347
|
+
console.log(`Updated: ${issue.updated_at}`);
|
|
348
|
+
if (issue.context) {
|
|
349
|
+
console.log();
|
|
350
|
+
console.log(chalk.bold('Context:'));
|
|
351
|
+
console.log(issue.context.substring(0, 200));
|
|
352
|
+
}
|
|
353
|
+
console.log();
|
|
354
|
+
console.log(chalk.bold(`Solutions (${solutions.length}):`));
|
|
355
|
+
for (const sol of solutions) {
|
|
356
|
+
const marker = sol.is_bound ? chalk.green('◉') : chalk.gray('○');
|
|
357
|
+
console.log(` ${marker} ${sol.id}: ${sol.tasks.length} tasks`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* task - Add or update task (simplified - mainly for manual task management)
|
|
362
|
+
*/
|
|
363
|
+
async function taskAction(issueId, taskId, options) {
|
|
364
|
+
if (!issueId) {
|
|
365
|
+
console.error(chalk.red('Issue ID is required'));
|
|
366
|
+
console.error(chalk.gray('Usage: ccw issue task <issue-id> [task-id] --title "..."'));
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
const issue = findIssue(issueId);
|
|
370
|
+
if (!issue) {
|
|
371
|
+
console.error(chalk.red(`Issue "${issueId}" not found`));
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
const solutions = readSolutions(issueId);
|
|
375
|
+
let boundIdx = solutions.findIndex(s => s.is_bound);
|
|
376
|
+
// Create default solution if none bound
|
|
377
|
+
if (boundIdx === -1) {
|
|
378
|
+
const newSol = {
|
|
379
|
+
id: generateSolutionId(),
|
|
380
|
+
description: 'Manual tasks',
|
|
381
|
+
tasks: [],
|
|
382
|
+
is_bound: true,
|
|
383
|
+
created_at: new Date().toISOString(),
|
|
384
|
+
bound_at: new Date().toISOString()
|
|
385
|
+
};
|
|
386
|
+
solutions.push(newSol);
|
|
387
|
+
boundIdx = solutions.length - 1;
|
|
388
|
+
updateIssue(issueId, { bound_solution_id: newSol.id, status: 'planned' });
|
|
389
|
+
}
|
|
390
|
+
const solution = solutions[boundIdx];
|
|
391
|
+
if (taskId) {
|
|
392
|
+
// Update existing task
|
|
393
|
+
const taskIdx = solution.tasks.findIndex(t => t.id === taskId);
|
|
394
|
+
if (taskIdx === -1) {
|
|
395
|
+
console.error(chalk.red(`Task "${taskId}" not found`));
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
if (options.title)
|
|
399
|
+
solution.tasks[taskIdx].title = options.title;
|
|
400
|
+
if (options.status)
|
|
401
|
+
solution.tasks[taskIdx].status = options.status;
|
|
402
|
+
if (options.executor)
|
|
403
|
+
solution.tasks[taskIdx].executor = options.executor;
|
|
404
|
+
writeSolutions(issueId, solutions);
|
|
405
|
+
console.log(chalk.green(`✓ Task ${taskId} updated`));
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
// Add new task
|
|
409
|
+
if (!options.title) {
|
|
410
|
+
console.error(chalk.red('Task title is required (--title)'));
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
const newTaskId = `T${solution.tasks.length + 1}`;
|
|
414
|
+
const newTask = {
|
|
415
|
+
id: newTaskId,
|
|
416
|
+
title: options.title,
|
|
417
|
+
scope: '',
|
|
418
|
+
action: 'Implement',
|
|
419
|
+
description: options.description || options.title,
|
|
420
|
+
implementation: [],
|
|
421
|
+
test: {
|
|
422
|
+
unit: [],
|
|
423
|
+
commands: ['npm test']
|
|
424
|
+
},
|
|
425
|
+
regression: ['npm test'],
|
|
426
|
+
acceptance: {
|
|
427
|
+
criteria: ['Task completed successfully'],
|
|
428
|
+
verification: ['Manual verification']
|
|
429
|
+
},
|
|
430
|
+
commit: {
|
|
431
|
+
type: 'feat',
|
|
432
|
+
scope: 'core',
|
|
433
|
+
message_template: `feat(core): ${options.title}`
|
|
434
|
+
},
|
|
435
|
+
depends_on: [],
|
|
436
|
+
executor: options.executor || 'auto'
|
|
437
|
+
};
|
|
438
|
+
solution.tasks.push(newTask);
|
|
439
|
+
writeSolutions(issueId, solutions);
|
|
440
|
+
console.log(chalk.green(`✓ Task ${newTaskId} added to ${issueId}`));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* bind - Register and/or bind a solution
|
|
445
|
+
*/
|
|
446
|
+
async function bindAction(issueId, solutionId, options) {
|
|
447
|
+
if (!issueId) {
|
|
448
|
+
console.error(chalk.red('Issue ID is required'));
|
|
449
|
+
console.error(chalk.gray('Usage: ccw issue bind <issue-id> [solution-id] [--solution <path>]'));
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
const issue = findIssue(issueId);
|
|
453
|
+
if (!issue) {
|
|
454
|
+
console.error(chalk.red(`Issue "${issueId}" not found`));
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
let solutions = readSolutions(issueId);
|
|
458
|
+
// Register new solution from file if provided
|
|
459
|
+
if (options.solution) {
|
|
460
|
+
try {
|
|
461
|
+
const content = readFileSync(options.solution, 'utf-8');
|
|
462
|
+
const data = JSON.parse(content);
|
|
463
|
+
const newSol = {
|
|
464
|
+
id: solutionId || generateSolutionId(),
|
|
465
|
+
description: data.description || data.approach_name || 'Imported solution',
|
|
466
|
+
tasks: data.tasks || [],
|
|
467
|
+
exploration_context: data.exploration_context,
|
|
468
|
+
analysis: data.analysis,
|
|
469
|
+
score: data.score,
|
|
470
|
+
is_bound: false,
|
|
471
|
+
created_at: new Date().toISOString()
|
|
472
|
+
};
|
|
473
|
+
solutions.push(newSol);
|
|
474
|
+
solutionId = newSol.id;
|
|
475
|
+
console.log(chalk.green(`✓ Solution ${solutionId} registered (${newSol.tasks.length} tasks)`));
|
|
476
|
+
}
|
|
477
|
+
catch (e) {
|
|
478
|
+
console.error(chalk.red(`Failed to read solution file: ${options.solution}`));
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (!solutionId) {
|
|
483
|
+
// List available solutions
|
|
484
|
+
if (solutions.length === 0) {
|
|
485
|
+
console.log(chalk.yellow('No solutions available'));
|
|
486
|
+
console.log(chalk.gray('Register one: ccw issue bind <issue-id> --solution <path>'));
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
console.log(chalk.bold.cyan(`\nSolutions for ${issueId}:\n`));
|
|
490
|
+
for (const sol of solutions) {
|
|
491
|
+
const marker = sol.is_bound ? chalk.green('◉') : chalk.gray('○');
|
|
492
|
+
console.log(` ${marker} ${sol.id}: ${sol.tasks.length} tasks - ${sol.description || ''}`);
|
|
493
|
+
}
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// Bind the specified solution
|
|
497
|
+
const solIdx = solutions.findIndex(s => s.id === solutionId);
|
|
498
|
+
if (solIdx === -1) {
|
|
499
|
+
console.error(chalk.red(`Solution "${solutionId}" not found`));
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
// Unbind all, bind selected
|
|
503
|
+
solutions = solutions.map(s => ({ ...s, is_bound: false }));
|
|
504
|
+
solutions[solIdx].is_bound = true;
|
|
505
|
+
solutions[solIdx].bound_at = new Date().toISOString();
|
|
506
|
+
writeSolutions(issueId, solutions);
|
|
507
|
+
updateIssue(issueId, {
|
|
508
|
+
bound_solution_id: solutionId,
|
|
509
|
+
solution_count: solutions.length,
|
|
510
|
+
status: 'planned',
|
|
511
|
+
planned_at: new Date().toISOString()
|
|
512
|
+
});
|
|
513
|
+
console.log(chalk.green(`✓ Solution ${solutionId} bound to ${issueId}`));
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* queue - Queue management (list / add / history)
|
|
517
|
+
*/
|
|
518
|
+
async function queueAction(subAction, issueId, options) {
|
|
519
|
+
// List all queues (history)
|
|
520
|
+
if (subAction === 'list' || subAction === 'history') {
|
|
521
|
+
const index = readQueueIndex();
|
|
522
|
+
if (options.json) {
|
|
523
|
+
console.log(JSON.stringify(index, null, 2));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
console.log(chalk.bold.cyan('\nQueue History\n'));
|
|
527
|
+
console.log(chalk.gray(`Active: ${index.active_queue_id || 'none'}`));
|
|
528
|
+
console.log();
|
|
529
|
+
if (index.queues.length === 0) {
|
|
530
|
+
console.log(chalk.yellow('No queues found'));
|
|
531
|
+
console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
console.log(chalk.gray('ID'.padEnd(22) + 'Status'.padEnd(12) + 'Tasks'.padEnd(10) + 'Issues'));
|
|
535
|
+
console.log(chalk.gray('-'.repeat(70)));
|
|
536
|
+
for (const q of index.queues) {
|
|
537
|
+
const statusColor = {
|
|
538
|
+
'active': chalk.green,
|
|
539
|
+
'completed': chalk.cyan,
|
|
540
|
+
'archived': chalk.gray,
|
|
541
|
+
'failed': chalk.red
|
|
542
|
+
}[q.status] || chalk.white;
|
|
543
|
+
const marker = q.id === index.active_queue_id ? '→ ' : ' ';
|
|
544
|
+
console.log(marker +
|
|
545
|
+
q.id.padEnd(20) +
|
|
546
|
+
statusColor(q.status.padEnd(12)) +
|
|
547
|
+
`${q.completed_tasks}/${q.total_tasks}`.padEnd(10) +
|
|
548
|
+
q.issue_ids.join(', '));
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
// Switch active queue
|
|
553
|
+
if (subAction === 'switch' && issueId) {
|
|
554
|
+
const queueId = issueId; // issueId is actually queue ID here
|
|
555
|
+
const targetQueue = readQueue(queueId);
|
|
556
|
+
if (!targetQueue) {
|
|
557
|
+
console.error(chalk.red(`Queue "${queueId}" not found`));
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
const index = readQueueIndex();
|
|
561
|
+
index.active_queue_id = queueId;
|
|
562
|
+
writeQueueIndex(index);
|
|
563
|
+
console.log(chalk.green(`✓ Switched to queue ${queueId}`));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
// Archive current queue
|
|
567
|
+
if (subAction === 'archive') {
|
|
568
|
+
const queue = readActiveQueue();
|
|
569
|
+
if (!queue.id || queue.queue.length === 0) {
|
|
570
|
+
console.log(chalk.yellow('No active queue to archive'));
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
queue.status = 'archived';
|
|
574
|
+
writeQueue(queue);
|
|
575
|
+
const index = readQueueIndex();
|
|
576
|
+
index.active_queue_id = null;
|
|
577
|
+
writeQueueIndex(index);
|
|
578
|
+
console.log(chalk.green(`✓ Archived queue ${queue.id}`));
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
// Add issue tasks to queue
|
|
582
|
+
if (subAction === 'add' && issueId) {
|
|
583
|
+
const issue = findIssue(issueId);
|
|
584
|
+
if (!issue) {
|
|
585
|
+
console.error(chalk.red(`Issue "${issueId}" not found`));
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
const solution = getBoundSolution(issueId);
|
|
589
|
+
if (!solution) {
|
|
590
|
+
console.error(chalk.red(`No bound solution for "${issueId}"`));
|
|
591
|
+
console.error(chalk.gray('First bind a solution: ccw issue bind <issue-id> <solution-id>'));
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
// Get or create active queue (create new if current is completed/archived)
|
|
595
|
+
let queue = readActiveQueue();
|
|
596
|
+
const isNewQueue = queue.queue.length === 0 || queue.status !== 'active';
|
|
597
|
+
if (queue.status !== 'active') {
|
|
598
|
+
// Create new queue if current is not active
|
|
599
|
+
queue = createEmptyQueue();
|
|
600
|
+
}
|
|
601
|
+
// Add issue to queue's issue list
|
|
602
|
+
if (!queue.issue_ids.includes(issueId)) {
|
|
603
|
+
queue.issue_ids.push(issueId);
|
|
604
|
+
}
|
|
605
|
+
let added = 0;
|
|
606
|
+
for (const task of solution.tasks) {
|
|
607
|
+
const exists = queue.queue.some(q => q.issue_id === issueId && q.task_id === task.id);
|
|
608
|
+
if (exists)
|
|
609
|
+
continue;
|
|
610
|
+
queue.queue.push({
|
|
611
|
+
queue_id: generateQueueItemId(queue),
|
|
612
|
+
issue_id: issueId,
|
|
613
|
+
solution_id: solution.id,
|
|
614
|
+
task_id: task.id,
|
|
615
|
+
status: 'pending',
|
|
616
|
+
execution_order: queue.queue.length + 1,
|
|
617
|
+
execution_group: 'P1',
|
|
618
|
+
depends_on: task.depends_on.map(dep => {
|
|
619
|
+
const depItem = queue.queue.find(q => q.task_id === dep && q.issue_id === issueId);
|
|
620
|
+
return depItem?.queue_id || dep;
|
|
621
|
+
}),
|
|
622
|
+
semantic_priority: 0.5,
|
|
623
|
+
assigned_executor: task.executor === 'auto' ? 'codex' : task.executor,
|
|
624
|
+
queued_at: new Date().toISOString()
|
|
625
|
+
});
|
|
626
|
+
added++;
|
|
627
|
+
}
|
|
628
|
+
writeQueue(queue);
|
|
629
|
+
updateIssue(issueId, { status: 'queued', queued_at: new Date().toISOString() });
|
|
630
|
+
if (isNewQueue) {
|
|
631
|
+
console.log(chalk.green(`✓ Created queue ${queue.id}`));
|
|
632
|
+
}
|
|
633
|
+
console.log(chalk.green(`✓ Added ${added} tasks from ${solution.id}`));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
// Show current queue
|
|
637
|
+
const queue = readActiveQueue();
|
|
638
|
+
if (options.json) {
|
|
639
|
+
console.log(JSON.stringify(queue, null, 2));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
console.log(chalk.bold.cyan('\nActive Queue\n'));
|
|
643
|
+
if (!queue.id || queue.queue.length === 0) {
|
|
644
|
+
console.log(chalk.yellow('No active queue'));
|
|
645
|
+
console.log(chalk.gray('Create one: ccw issue queue add <issue-id>'));
|
|
646
|
+
console.log(chalk.gray('Or list history: ccw issue queue list'));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
console.log(chalk.gray(`Queue: ${queue.id}`));
|
|
650
|
+
console.log(chalk.gray(`Issues: ${queue.issue_ids.join(', ')}`));
|
|
651
|
+
console.log(chalk.gray(`Total: ${queue._metadata.total_tasks} | Pending: ${queue._metadata.pending_count} | Executing: ${queue._metadata.executing_count} | Completed: ${queue._metadata.completed_count}`));
|
|
652
|
+
console.log();
|
|
653
|
+
console.log(chalk.gray('QueueID'.padEnd(10) + 'Issue'.padEnd(15) + 'Task'.padEnd(8) + 'Status'.padEnd(12) + 'Executor'));
|
|
654
|
+
console.log(chalk.gray('-'.repeat(60)));
|
|
655
|
+
for (const item of queue.queue) {
|
|
656
|
+
const statusColor = {
|
|
657
|
+
'pending': chalk.gray,
|
|
658
|
+
'ready': chalk.cyan,
|
|
659
|
+
'executing': chalk.yellow,
|
|
660
|
+
'completed': chalk.green,
|
|
661
|
+
'failed': chalk.red,
|
|
662
|
+
'blocked': chalk.magenta
|
|
663
|
+
}[item.status] || chalk.white;
|
|
664
|
+
console.log(item.queue_id.padEnd(10) +
|
|
665
|
+
item.issue_id.substring(0, 13).padEnd(15) +
|
|
666
|
+
item.task_id.padEnd(8) +
|
|
667
|
+
statusColor(item.status.padEnd(12)) +
|
|
668
|
+
item.assigned_executor);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* next - Get next ready task for execution (JSON output)
|
|
673
|
+
*/
|
|
674
|
+
async function nextAction(options) {
|
|
675
|
+
const queue = readActiveQueue();
|
|
676
|
+
// Find ready tasks
|
|
677
|
+
const readyTasks = queue.queue.filter(item => {
|
|
678
|
+
if (item.status !== 'pending')
|
|
679
|
+
return false;
|
|
680
|
+
return item.depends_on.every(depId => {
|
|
681
|
+
const dep = queue.queue.find(q => q.queue_id === depId);
|
|
682
|
+
return !dep || dep.status === 'completed';
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
if (readyTasks.length === 0) {
|
|
686
|
+
console.log(JSON.stringify({
|
|
687
|
+
status: 'empty',
|
|
688
|
+
message: 'No ready tasks',
|
|
689
|
+
queue_status: queue._metadata
|
|
690
|
+
}, null, 2));
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
// Sort by execution order
|
|
694
|
+
readyTasks.sort((a, b) => a.execution_order - b.execution_order);
|
|
695
|
+
const nextItem = readyTasks[0];
|
|
696
|
+
// Load task definition
|
|
697
|
+
const solution = findSolution(nextItem.issue_id, nextItem.solution_id);
|
|
698
|
+
const taskDef = solution?.tasks.find(t => t.id === nextItem.task_id);
|
|
699
|
+
if (!taskDef) {
|
|
700
|
+
console.log(JSON.stringify({ status: 'error', message: 'Task definition not found' }));
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
// Mark as executing
|
|
704
|
+
const idx = queue.queue.findIndex(q => q.queue_id === nextItem.queue_id);
|
|
705
|
+
queue.queue[idx].status = 'executing';
|
|
706
|
+
queue.queue[idx].started_at = new Date().toISOString();
|
|
707
|
+
writeQueue(queue);
|
|
708
|
+
// Update issue status
|
|
709
|
+
updateIssue(nextItem.issue_id, { status: 'executing' });
|
|
710
|
+
console.log(JSON.stringify({
|
|
711
|
+
queue_id: nextItem.queue_id,
|
|
712
|
+
issue_id: nextItem.issue_id,
|
|
713
|
+
solution_id: nextItem.solution_id,
|
|
714
|
+
task: taskDef,
|
|
715
|
+
context: solution?.exploration_context || {},
|
|
716
|
+
execution_hints: {
|
|
717
|
+
executor: nextItem.assigned_executor,
|
|
718
|
+
estimated_minutes: taskDef.estimated_minutes || 30
|
|
719
|
+
}
|
|
720
|
+
}, null, 2));
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* done - Mark task completed or failed
|
|
724
|
+
*/
|
|
725
|
+
async function doneAction(queueId, options) {
|
|
726
|
+
if (!queueId) {
|
|
727
|
+
console.error(chalk.red('Queue ID is required'));
|
|
728
|
+
console.error(chalk.gray('Usage: ccw issue done <queue-id> [--fail] [--reason "..."]'));
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
const queue = readActiveQueue();
|
|
732
|
+
const idx = queue.queue.findIndex(q => q.queue_id === queueId);
|
|
733
|
+
if (idx === -1) {
|
|
734
|
+
console.error(chalk.red(`Queue item "${queueId}" not found`));
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
const isFail = options.fail;
|
|
738
|
+
queue.queue[idx].status = isFail ? 'failed' : 'completed';
|
|
739
|
+
queue.queue[idx].completed_at = new Date().toISOString();
|
|
740
|
+
if (isFail) {
|
|
741
|
+
queue.queue[idx].failure_reason = options.reason || 'Unknown failure';
|
|
742
|
+
}
|
|
743
|
+
else if (options.result) {
|
|
744
|
+
try {
|
|
745
|
+
queue.queue[idx].result = JSON.parse(options.result);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
console.warn(chalk.yellow('Warning: Could not parse result JSON'));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// Check if all issue tasks are complete
|
|
752
|
+
const issueId = queue.queue[idx].issue_id;
|
|
753
|
+
const issueTasks = queue.queue.filter(q => q.issue_id === issueId);
|
|
754
|
+
const allIssueComplete = issueTasks.every(q => q.status === 'completed');
|
|
755
|
+
const anyIssueFailed = issueTasks.some(q => q.status === 'failed');
|
|
756
|
+
if (allIssueComplete) {
|
|
757
|
+
updateIssue(issueId, { status: 'completed', completed_at: new Date().toISOString() });
|
|
758
|
+
console.log(chalk.green(`✓ ${queueId} completed`));
|
|
759
|
+
console.log(chalk.green(`✓ Issue ${issueId} completed (all tasks done)`));
|
|
760
|
+
}
|
|
761
|
+
else if (anyIssueFailed) {
|
|
762
|
+
updateIssue(issueId, { status: 'failed' });
|
|
763
|
+
console.log(chalk.red(`✗ ${queueId} failed`));
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
console.log(isFail ? chalk.red(`✗ ${queueId} failed`) : chalk.green(`✓ ${queueId} completed`));
|
|
767
|
+
}
|
|
768
|
+
// Check if entire queue is complete
|
|
769
|
+
const allQueueComplete = queue.queue.every(q => q.status === 'completed');
|
|
770
|
+
const anyQueueFailed = queue.queue.some(q => q.status === 'failed');
|
|
771
|
+
if (allQueueComplete) {
|
|
772
|
+
queue.status = 'completed';
|
|
773
|
+
console.log(chalk.green(`\n✓ Queue ${queue.id} completed (all tasks done)`));
|
|
774
|
+
}
|
|
775
|
+
else if (anyQueueFailed && queue.queue.every(q => q.status === 'completed' || q.status === 'failed')) {
|
|
776
|
+
queue.status = 'failed';
|
|
777
|
+
console.log(chalk.yellow(`\n⚠ Queue ${queue.id} has failed tasks`));
|
|
778
|
+
}
|
|
779
|
+
writeQueue(queue);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* retry - Retry failed tasks
|
|
783
|
+
*/
|
|
784
|
+
async function retryAction(issueId, options) {
|
|
785
|
+
const queue = readActiveQueue();
|
|
786
|
+
if (!queue.id || queue.queue.length === 0) {
|
|
787
|
+
console.log(chalk.yellow('No active queue'));
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
let updated = 0;
|
|
791
|
+
for (const item of queue.queue) {
|
|
792
|
+
if (item.status === 'failed') {
|
|
793
|
+
if (!issueId || item.issue_id === issueId) {
|
|
794
|
+
item.status = 'pending';
|
|
795
|
+
item.failure_reason = undefined;
|
|
796
|
+
item.started_at = undefined;
|
|
797
|
+
item.completed_at = undefined;
|
|
798
|
+
updated++;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (updated === 0) {
|
|
803
|
+
console.log(chalk.yellow('No failed tasks to retry'));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
// Reset queue status if it was failed
|
|
807
|
+
if (queue.status === 'failed') {
|
|
808
|
+
queue.status = 'active';
|
|
809
|
+
}
|
|
810
|
+
writeQueue(queue);
|
|
811
|
+
if (issueId) {
|
|
812
|
+
updateIssue(issueId, { status: 'queued' });
|
|
813
|
+
}
|
|
814
|
+
console.log(chalk.green(`✓ Reset ${updated} task(s) to pending`));
|
|
815
|
+
}
|
|
816
|
+
// ============ Main Entry ============
|
|
817
|
+
export async function issueCommand(subcommand, args, options) {
|
|
818
|
+
const argsArray = Array.isArray(args) ? args : (args ? [args] : []);
|
|
819
|
+
switch (subcommand) {
|
|
820
|
+
case 'init':
|
|
821
|
+
await initAction(argsArray[0], options);
|
|
822
|
+
break;
|
|
823
|
+
case 'list':
|
|
824
|
+
await listAction(argsArray[0], options);
|
|
825
|
+
break;
|
|
826
|
+
case 'status':
|
|
827
|
+
await statusAction(argsArray[0], options);
|
|
828
|
+
break;
|
|
829
|
+
case 'task':
|
|
830
|
+
await taskAction(argsArray[0], argsArray[1], options);
|
|
831
|
+
break;
|
|
832
|
+
case 'bind':
|
|
833
|
+
await bindAction(argsArray[0], argsArray[1], options);
|
|
834
|
+
break;
|
|
835
|
+
case 'queue':
|
|
836
|
+
await queueAction(argsArray[0], argsArray[1], options);
|
|
837
|
+
break;
|
|
838
|
+
case 'next':
|
|
839
|
+
await nextAction(options);
|
|
840
|
+
break;
|
|
841
|
+
case 'done':
|
|
842
|
+
await doneAction(argsArray[0], options);
|
|
843
|
+
break;
|
|
844
|
+
case 'retry':
|
|
845
|
+
await retryAction(argsArray[0], options);
|
|
846
|
+
break;
|
|
847
|
+
// Legacy aliases
|
|
848
|
+
case 'register':
|
|
849
|
+
console.log(chalk.yellow('Deprecated: use "ccw issue bind <issue-id> --solution <path>"'));
|
|
850
|
+
await bindAction(argsArray[0], undefined, options);
|
|
851
|
+
break;
|
|
852
|
+
case 'complete':
|
|
853
|
+
await doneAction(argsArray[0], options);
|
|
854
|
+
break;
|
|
855
|
+
case 'fail':
|
|
856
|
+
await doneAction(argsArray[0], { ...options, fail: true });
|
|
857
|
+
break;
|
|
858
|
+
default:
|
|
859
|
+
console.log(chalk.bold.cyan('\nCCW Issue Management (v3.0 - Multi-Queue + Lifecycle)\n'));
|
|
860
|
+
console.log(chalk.bold('Core Commands:'));
|
|
861
|
+
console.log(chalk.gray(' init <issue-id> Initialize new issue'));
|
|
862
|
+
console.log(chalk.gray(' list [issue-id] List issues or tasks'));
|
|
863
|
+
console.log(chalk.gray(' status [issue-id] Show detailed status'));
|
|
864
|
+
console.log(chalk.gray(' task <issue-id> [task-id] Add or update task'));
|
|
865
|
+
console.log(chalk.gray(' bind <issue-id> [sol-id] Bind solution (--solution <path> to register)'));
|
|
866
|
+
console.log();
|
|
867
|
+
console.log(chalk.bold('Queue Commands:'));
|
|
868
|
+
console.log(chalk.gray(' queue Show active queue'));
|
|
869
|
+
console.log(chalk.gray(' queue list List all queues (history)'));
|
|
870
|
+
console.log(chalk.gray(' queue add <issue-id> Add issue to active queue (or create new)'));
|
|
871
|
+
console.log(chalk.gray(' queue switch <queue-id> Switch active queue'));
|
|
872
|
+
console.log(chalk.gray(' queue archive Archive current queue'));
|
|
873
|
+
console.log(chalk.gray(' retry [issue-id] Retry failed tasks'));
|
|
874
|
+
console.log();
|
|
875
|
+
console.log(chalk.bold('Execution Endpoints:'));
|
|
876
|
+
console.log(chalk.gray(' next Get next ready task (JSON)'));
|
|
877
|
+
console.log(chalk.gray(' done <queue-id> Mark task completed'));
|
|
878
|
+
console.log(chalk.gray(' done <queue-id> --fail Mark task failed'));
|
|
879
|
+
console.log();
|
|
880
|
+
console.log(chalk.bold('Options:'));
|
|
881
|
+
console.log(chalk.gray(' --title <title> Issue/task title'));
|
|
882
|
+
console.log(chalk.gray(' --solution <path> Solution JSON file'));
|
|
883
|
+
console.log(chalk.gray(' --result <json> Execution result'));
|
|
884
|
+
console.log(chalk.gray(' --reason <text> Failure reason'));
|
|
885
|
+
console.log(chalk.gray(' --json JSON output'));
|
|
886
|
+
console.log(chalk.gray(' --force Force operation'));
|
|
887
|
+
console.log();
|
|
888
|
+
console.log(chalk.bold('Storage:'));
|
|
889
|
+
console.log(chalk.gray(' .workflow/issues/issues.jsonl All issues'));
|
|
890
|
+
console.log(chalk.gray(' .workflow/issues/solutions/*.jsonl Solutions per issue'));
|
|
891
|
+
console.log(chalk.gray(' .workflow/issues/queues/ Queue files (multi-queue)'));
|
|
892
|
+
console.log(chalk.gray(' .workflow/issues/queues/index.json Queue index'));
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
//# sourceMappingURL=issue.js.map
|