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
|
@@ -21,6 +21,7 @@ const MODULE_FILES = [
|
|
|
21
21
|
'dashboard-js/components/tabs-other.js',
|
|
22
22
|
'dashboard-js/components/carousel.js',
|
|
23
23
|
'dashboard-js/components/notifications.js',
|
|
24
|
+
'dashboard-js/components/cli-stream-viewer.js',
|
|
24
25
|
'dashboard-js/components/global-notifications.js',
|
|
25
26
|
'dashboard-js/components/cli-status.js',
|
|
26
27
|
'dashboard-js/components/cli-history.js',
|
|
@@ -769,9 +769,9 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
|
|
769
769
|
if (pathname === '/api/cli/code-index-mcp' && req.method === 'PUT') {
|
|
770
770
|
handlePostRequest(req, res, async (body: unknown) => {
|
|
771
771
|
try {
|
|
772
|
-
const { provider } = body as { provider: 'codexlens' | 'ace' };
|
|
773
|
-
if (!provider || !['codexlens', 'ace'].includes(provider)) {
|
|
774
|
-
return { error: 'Invalid provider. Must be "codexlens" or "
|
|
772
|
+
const { provider } = body as { provider: 'codexlens' | 'ace' | 'none' };
|
|
773
|
+
if (!provider || !['codexlens', 'ace', 'none'].includes(provider)) {
|
|
774
|
+
return { error: 'Invalid provider. Must be "codexlens", "ace", or "none"', status: 400 };
|
|
775
775
|
}
|
|
776
776
|
|
|
777
777
|
const result = updateCodeIndexMcp(initialPath, provider);
|
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Issue Routes Module (Optimized - Flat JSONL Storage)
|
|
4
|
+
*
|
|
5
|
+
* Storage Structure:
|
|
6
|
+
* .workflow/issues/
|
|
7
|
+
* ├── issues.jsonl # All issues (one per line)
|
|
8
|
+
* ├── queue.json # Execution queue
|
|
9
|
+
* └── solutions/
|
|
10
|
+
* ├── {issue-id}.jsonl # Solutions for issue (one per line)
|
|
11
|
+
* └── ...
|
|
12
|
+
*
|
|
13
|
+
* API Endpoints (8 total):
|
|
14
|
+
* - GET /api/issues - List all issues
|
|
15
|
+
* - POST /api/issues - Create new issue
|
|
16
|
+
* - GET /api/issues/:id - Get issue detail
|
|
17
|
+
* - PATCH /api/issues/:id - Update issue (includes binding logic)
|
|
18
|
+
* - DELETE /api/issues/:id - Delete issue
|
|
19
|
+
* - POST /api/issues/:id/solutions - Add solution
|
|
20
|
+
* - PATCH /api/issues/:id/tasks/:taskId - Update task
|
|
21
|
+
* - GET /api/queue - Get execution queue
|
|
22
|
+
* - POST /api/queue/reorder - Reorder queue items
|
|
23
|
+
*/
|
|
24
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
25
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
26
|
+
import { join } from 'path';
|
|
27
|
+
|
|
28
|
+
export interface RouteContext {
|
|
29
|
+
pathname: string;
|
|
30
|
+
url: URL;
|
|
31
|
+
req: IncomingMessage;
|
|
32
|
+
res: ServerResponse;
|
|
33
|
+
initialPath: string;
|
|
34
|
+
handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise<any>) => void;
|
|
35
|
+
broadcastToClients: (data: unknown) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ========== JSONL Helper Functions ==========
|
|
39
|
+
|
|
40
|
+
function readIssuesJsonl(issuesDir: string): any[] {
|
|
41
|
+
const issuesPath = join(issuesDir, 'issues.jsonl');
|
|
42
|
+
if (!existsSync(issuesPath)) return [];
|
|
43
|
+
try {
|
|
44
|
+
const content = readFileSync(issuesPath, 'utf8');
|
|
45
|
+
return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function writeIssuesJsonl(issuesDir: string, issues: any[]) {
|
|
52
|
+
if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
|
|
53
|
+
const issuesPath = join(issuesDir, 'issues.jsonl');
|
|
54
|
+
writeFileSync(issuesPath, issues.map(i => JSON.stringify(i)).join('\n'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readSolutionsJsonl(issuesDir: string, issueId: string): any[] {
|
|
58
|
+
const solutionsPath = join(issuesDir, 'solutions', `${issueId}.jsonl`);
|
|
59
|
+
if (!existsSync(solutionsPath)) return [];
|
|
60
|
+
try {
|
|
61
|
+
const content = readFileSync(solutionsPath, 'utf8');
|
|
62
|
+
return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line));
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeSolutionsJsonl(issuesDir: string, issueId: string, solutions: any[]) {
|
|
69
|
+
const solutionsDir = join(issuesDir, 'solutions');
|
|
70
|
+
if (!existsSync(solutionsDir)) mkdirSync(solutionsDir, { recursive: true });
|
|
71
|
+
writeFileSync(join(solutionsDir, `${issueId}.jsonl`), solutions.map(s => JSON.stringify(s)).join('\n'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readQueue(issuesDir: string) {
|
|
75
|
+
// Try new multi-queue structure first
|
|
76
|
+
const queuesDir = join(issuesDir, 'queues');
|
|
77
|
+
const indexPath = join(queuesDir, 'index.json');
|
|
78
|
+
|
|
79
|
+
if (existsSync(indexPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
82
|
+
const activeQueueId = index.active_queue_id;
|
|
83
|
+
|
|
84
|
+
if (activeQueueId) {
|
|
85
|
+
const queueFilePath = join(queuesDir, `${activeQueueId}.json`);
|
|
86
|
+
if (existsSync(queueFilePath)) {
|
|
87
|
+
return JSON.parse(readFileSync(queueFilePath, 'utf8'));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Fall through to legacy check
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Fallback to legacy queue.json
|
|
96
|
+
const legacyQueuePath = join(issuesDir, 'queue.json');
|
|
97
|
+
if (existsSync(legacyQueuePath)) {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(readFileSync(legacyQueuePath, 'utf8'));
|
|
100
|
+
} catch {
|
|
101
|
+
// Return empty queue
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { queue: [], conflicts: [], execution_groups: [], _metadata: { version: '1.0', total_tasks: 0 } };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writeQueue(issuesDir: string, queue: any) {
|
|
109
|
+
if (!existsSync(issuesDir)) mkdirSync(issuesDir, { recursive: true });
|
|
110
|
+
queue._metadata = { ...queue._metadata, updated_at: new Date().toISOString(), total_tasks: queue.queue?.length || 0 };
|
|
111
|
+
|
|
112
|
+
// Check if using new multi-queue structure
|
|
113
|
+
const queuesDir = join(issuesDir, 'queues');
|
|
114
|
+
const indexPath = join(queuesDir, 'index.json');
|
|
115
|
+
|
|
116
|
+
if (existsSync(indexPath) && queue.id) {
|
|
117
|
+
// Write to new structure
|
|
118
|
+
const queueFilePath = join(queuesDir, `${queue.id}.json`);
|
|
119
|
+
writeFileSync(queueFilePath, JSON.stringify(queue, null, 2));
|
|
120
|
+
|
|
121
|
+
// Update index metadata
|
|
122
|
+
try {
|
|
123
|
+
const index = JSON.parse(readFileSync(indexPath, 'utf8'));
|
|
124
|
+
const queueEntry = index.queues?.find((q: any) => q.id === queue.id);
|
|
125
|
+
if (queueEntry) {
|
|
126
|
+
queueEntry.total_tasks = queue.queue?.length || 0;
|
|
127
|
+
queueEntry.completed_tasks = queue.queue?.filter((i: any) => i.status === 'completed').length || 0;
|
|
128
|
+
writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Ignore index update errors
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
// Fallback to legacy queue.json
|
|
135
|
+
writeFileSync(join(issuesDir, 'queue.json'), JSON.stringify(queue, null, 2));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function getIssueDetail(issuesDir: string, issueId: string) {
|
|
140
|
+
const issues = readIssuesJsonl(issuesDir);
|
|
141
|
+
const issue = issues.find(i => i.id === issueId);
|
|
142
|
+
if (!issue) return null;
|
|
143
|
+
|
|
144
|
+
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
|
145
|
+
let tasks: any[] = [];
|
|
146
|
+
if (issue.bound_solution_id) {
|
|
147
|
+
const boundSol = solutions.find(s => s.id === issue.bound_solution_id);
|
|
148
|
+
if (boundSol?.tasks) tasks = boundSol.tasks;
|
|
149
|
+
}
|
|
150
|
+
return { ...issue, solutions, tasks };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function enrichIssues(issues: any[], issuesDir: string) {
|
|
154
|
+
return issues.map(issue => ({
|
|
155
|
+
...issue,
|
|
156
|
+
solution_count: readSolutionsJsonl(issuesDir, issue.id).length
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function groupQueueByExecutionGroup(queue: any) {
|
|
161
|
+
const groups: { [key: string]: any[] } = {};
|
|
162
|
+
for (const item of queue.queue || []) {
|
|
163
|
+
const groupId = item.execution_group || 'ungrouped';
|
|
164
|
+
if (!groups[groupId]) groups[groupId] = [];
|
|
165
|
+
groups[groupId].push(item);
|
|
166
|
+
}
|
|
167
|
+
for (const groupId of Object.keys(groups)) {
|
|
168
|
+
groups[groupId].sort((a, b) => (a.execution_order || 0) - (b.execution_order || 0));
|
|
169
|
+
}
|
|
170
|
+
const executionGroups = Object.entries(groups).map(([id, items]) => ({
|
|
171
|
+
id,
|
|
172
|
+
type: id.startsWith('P') ? 'parallel' : id.startsWith('S') ? 'sequential' : 'unknown',
|
|
173
|
+
task_count: items.length,
|
|
174
|
+
tasks: items.map(i => i.queue_id)
|
|
175
|
+
})).sort((a, b) => {
|
|
176
|
+
const aFirst = groups[a.id]?.[0]?.execution_order || 0;
|
|
177
|
+
const bFirst = groups[b.id]?.[0]?.execution_order || 0;
|
|
178
|
+
return aFirst - bFirst;
|
|
179
|
+
});
|
|
180
|
+
return { ...queue, execution_groups: executionGroups, grouped_items: groups };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Bind solution to issue with proper side effects
|
|
185
|
+
*/
|
|
186
|
+
function bindSolutionToIssue(issuesDir: string, issueId: string, solutionId: string, issues: any[], issueIndex: number) {
|
|
187
|
+
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
|
188
|
+
const solIndex = solutions.findIndex(s => s.id === solutionId);
|
|
189
|
+
|
|
190
|
+
if (solIndex === -1) return { error: `Solution ${solutionId} not found` };
|
|
191
|
+
|
|
192
|
+
// Unbind all, bind new
|
|
193
|
+
solutions.forEach(s => { s.is_bound = false; });
|
|
194
|
+
solutions[solIndex].is_bound = true;
|
|
195
|
+
solutions[solIndex].bound_at = new Date().toISOString();
|
|
196
|
+
writeSolutionsJsonl(issuesDir, issueId, solutions);
|
|
197
|
+
|
|
198
|
+
// Update issue
|
|
199
|
+
issues[issueIndex].bound_solution_id = solutionId;
|
|
200
|
+
issues[issueIndex].status = 'planned';
|
|
201
|
+
issues[issueIndex].planned_at = new Date().toISOString();
|
|
202
|
+
|
|
203
|
+
return { success: true, bound: solutionId };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ========== Route Handler ==========
|
|
207
|
+
|
|
208
|
+
export async function handleIssueRoutes(ctx: RouteContext): Promise<boolean> {
|
|
209
|
+
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
|
|
210
|
+
const projectPath = url.searchParams.get('path') || initialPath;
|
|
211
|
+
const issuesDir = join(projectPath, '.workflow', 'issues');
|
|
212
|
+
|
|
213
|
+
// ===== Queue Routes (top-level /api/queue) =====
|
|
214
|
+
|
|
215
|
+
// GET /api/queue - Get execution queue
|
|
216
|
+
if (pathname === '/api/queue' && req.method === 'GET') {
|
|
217
|
+
const queue = groupQueueByExecutionGroup(readQueue(issuesDir));
|
|
218
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
219
|
+
res.end(JSON.stringify(queue));
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// POST /api/queue/reorder - Reorder queue items
|
|
224
|
+
if (pathname === '/api/queue/reorder' && req.method === 'POST') {
|
|
225
|
+
handlePostRequest(req, res, async (body: any) => {
|
|
226
|
+
const { groupId, newOrder } = body;
|
|
227
|
+
if (!groupId || !Array.isArray(newOrder)) {
|
|
228
|
+
return { error: 'groupId and newOrder (array) required' };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const queue = readQueue(issuesDir);
|
|
232
|
+
const groupItems = queue.queue.filter((item: any) => item.execution_group === groupId);
|
|
233
|
+
const otherItems = queue.queue.filter((item: any) => item.execution_group !== groupId);
|
|
234
|
+
|
|
235
|
+
if (groupItems.length === 0) return { error: `No items in group ${groupId}` };
|
|
236
|
+
|
|
237
|
+
const groupQueueIds = new Set(groupItems.map((i: any) => i.queue_id));
|
|
238
|
+
if (groupQueueIds.size !== new Set(newOrder).size) {
|
|
239
|
+
return { error: 'newOrder must contain all group items' };
|
|
240
|
+
}
|
|
241
|
+
for (const id of newOrder) {
|
|
242
|
+
if (!groupQueueIds.has(id)) return { error: `Invalid queue_id: ${id}` };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const itemMap = new Map(groupItems.map((i: any) => [i.queue_id, i]));
|
|
246
|
+
const reorderedItems = newOrder.map((qid: string, idx: number) => ({ ...itemMap.get(qid), _idx: idx }));
|
|
247
|
+
const newQueue = [...otherItems, ...reorderedItems].sort((a, b) => {
|
|
248
|
+
const aGroup = parseInt(a.execution_group?.match(/\d+/)?.[0] || '999');
|
|
249
|
+
const bGroup = parseInt(b.execution_group?.match(/\d+/)?.[0] || '999');
|
|
250
|
+
if (aGroup !== bGroup) return aGroup - bGroup;
|
|
251
|
+
if (a.execution_group === b.execution_group) {
|
|
252
|
+
return (a._idx ?? a.execution_order ?? 999) - (b._idx ?? b.execution_order ?? 999);
|
|
253
|
+
}
|
|
254
|
+
return (a.execution_order || 0) - (b.execution_order || 0);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
newQueue.forEach((item, idx) => { item.execution_order = idx + 1; delete item._idx; });
|
|
258
|
+
queue.queue = newQueue;
|
|
259
|
+
writeQueue(issuesDir, queue);
|
|
260
|
+
|
|
261
|
+
return { success: true, groupId, reordered: newOrder.length };
|
|
262
|
+
});
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Legacy: GET /api/issues/queue (backward compat)
|
|
267
|
+
if (pathname === '/api/issues/queue' && req.method === 'GET') {
|
|
268
|
+
const queue = groupQueueByExecutionGroup(readQueue(issuesDir));
|
|
269
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
270
|
+
res.end(JSON.stringify(queue));
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ===== Issue Routes =====
|
|
275
|
+
|
|
276
|
+
// GET /api/issues - List all issues
|
|
277
|
+
if (pathname === '/api/issues' && req.method === 'GET') {
|
|
278
|
+
const issues = enrichIssues(readIssuesJsonl(issuesDir), issuesDir);
|
|
279
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
280
|
+
res.end(JSON.stringify({
|
|
281
|
+
issues,
|
|
282
|
+
_metadata: { version: '2.0', storage: 'jsonl', total_issues: issues.length, last_updated: new Date().toISOString() }
|
|
283
|
+
}));
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// POST /api/issues - Create issue
|
|
288
|
+
if (pathname === '/api/issues' && req.method === 'POST') {
|
|
289
|
+
handlePostRequest(req, res, async (body: any) => {
|
|
290
|
+
if (!body.id || !body.title) return { error: 'id and title required' };
|
|
291
|
+
|
|
292
|
+
const issues = readIssuesJsonl(issuesDir);
|
|
293
|
+
if (issues.find(i => i.id === body.id)) return { error: `Issue ${body.id} exists` };
|
|
294
|
+
|
|
295
|
+
const newIssue = {
|
|
296
|
+
id: body.id,
|
|
297
|
+
title: body.title,
|
|
298
|
+
status: body.status || 'registered',
|
|
299
|
+
priority: body.priority || 3,
|
|
300
|
+
context: body.context || '',
|
|
301
|
+
source: body.source || 'text',
|
|
302
|
+
source_url: body.source_url || null,
|
|
303
|
+
labels: body.labels || [],
|
|
304
|
+
created_at: new Date().toISOString(),
|
|
305
|
+
updated_at: new Date().toISOString()
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
issues.push(newIssue);
|
|
309
|
+
writeIssuesJsonl(issuesDir, issues);
|
|
310
|
+
return { success: true, issue: newIssue };
|
|
311
|
+
});
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// GET /api/issues/:id - Get issue detail
|
|
316
|
+
const detailMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
|
|
317
|
+
if (detailMatch && req.method === 'GET') {
|
|
318
|
+
const issueId = decodeURIComponent(detailMatch[1]);
|
|
319
|
+
if (issueId === 'queue') return false;
|
|
320
|
+
|
|
321
|
+
const detail = getIssueDetail(issuesDir, issueId);
|
|
322
|
+
if (!detail) {
|
|
323
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
324
|
+
res.end(JSON.stringify({ error: 'Issue not found' }));
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
328
|
+
res.end(JSON.stringify(detail));
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// PATCH /api/issues/:id - Update issue (with binding support)
|
|
333
|
+
const updateMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
|
|
334
|
+
if (updateMatch && req.method === 'PATCH') {
|
|
335
|
+
const issueId = decodeURIComponent(updateMatch[1]);
|
|
336
|
+
if (issueId === 'queue') return false;
|
|
337
|
+
|
|
338
|
+
handlePostRequest(req, res, async (body: any) => {
|
|
339
|
+
const issues = readIssuesJsonl(issuesDir);
|
|
340
|
+
const issueIndex = issues.findIndex(i => i.id === issueId);
|
|
341
|
+
if (issueIndex === -1) return { error: 'Issue not found' };
|
|
342
|
+
|
|
343
|
+
const updates: string[] = [];
|
|
344
|
+
|
|
345
|
+
// Handle binding if bound_solution_id provided
|
|
346
|
+
if (body.bound_solution_id !== undefined) {
|
|
347
|
+
if (body.bound_solution_id) {
|
|
348
|
+
const bindResult = bindSolutionToIssue(issuesDir, issueId, body.bound_solution_id, issues, issueIndex);
|
|
349
|
+
if (bindResult.error) return bindResult;
|
|
350
|
+
updates.push('bound_solution_id');
|
|
351
|
+
} else {
|
|
352
|
+
// Unbind
|
|
353
|
+
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
|
354
|
+
solutions.forEach(s => { s.is_bound = false; });
|
|
355
|
+
writeSolutionsJsonl(issuesDir, issueId, solutions);
|
|
356
|
+
issues[issueIndex].bound_solution_id = null;
|
|
357
|
+
updates.push('bound_solution_id (unbound)');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Update other fields
|
|
362
|
+
for (const field of ['title', 'context', 'status', 'priority', 'labels']) {
|
|
363
|
+
if (body[field] !== undefined) {
|
|
364
|
+
issues[issueIndex][field] = body[field];
|
|
365
|
+
updates.push(field);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
issues[issueIndex].updated_at = new Date().toISOString();
|
|
370
|
+
writeIssuesJsonl(issuesDir, issues);
|
|
371
|
+
return { success: true, issueId, updated: updates };
|
|
372
|
+
});
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// DELETE /api/issues/:id
|
|
377
|
+
const deleteMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
|
|
378
|
+
if (deleteMatch && req.method === 'DELETE') {
|
|
379
|
+
const issueId = decodeURIComponent(deleteMatch[1]);
|
|
380
|
+
|
|
381
|
+
const issues = readIssuesJsonl(issuesDir);
|
|
382
|
+
const filtered = issues.filter(i => i.id !== issueId);
|
|
383
|
+
if (filtered.length === issues.length) {
|
|
384
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
385
|
+
res.end(JSON.stringify({ error: 'Issue not found' }));
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
writeIssuesJsonl(issuesDir, filtered);
|
|
390
|
+
|
|
391
|
+
// Clean up solutions file
|
|
392
|
+
const solPath = join(issuesDir, 'solutions', `${issueId}.jsonl`);
|
|
393
|
+
if (existsSync(solPath)) {
|
|
394
|
+
try { unlinkSync(solPath); } catch {}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
398
|
+
res.end(JSON.stringify({ success: true, issueId }));
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// POST /api/issues/:id/solutions - Add solution
|
|
403
|
+
const addSolMatch = pathname.match(/^\/api\/issues\/([^/]+)\/solutions$/);
|
|
404
|
+
if (addSolMatch && req.method === 'POST') {
|
|
405
|
+
const issueId = decodeURIComponent(addSolMatch[1]);
|
|
406
|
+
|
|
407
|
+
handlePostRequest(req, res, async (body: any) => {
|
|
408
|
+
if (!body.id || !body.tasks) return { error: 'id and tasks required' };
|
|
409
|
+
|
|
410
|
+
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
|
411
|
+
if (solutions.find(s => s.id === body.id)) return { error: `Solution ${body.id} exists` };
|
|
412
|
+
|
|
413
|
+
const newSolution = {
|
|
414
|
+
id: body.id,
|
|
415
|
+
description: body.description || '',
|
|
416
|
+
tasks: body.tasks,
|
|
417
|
+
exploration_context: body.exploration_context || {},
|
|
418
|
+
analysis: body.analysis || {},
|
|
419
|
+
score: body.score || 0,
|
|
420
|
+
is_bound: false,
|
|
421
|
+
created_at: new Date().toISOString()
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
solutions.push(newSolution);
|
|
425
|
+
writeSolutionsJsonl(issuesDir, issueId, solutions);
|
|
426
|
+
|
|
427
|
+
// Update issue solution_count
|
|
428
|
+
const issues = readIssuesJsonl(issuesDir);
|
|
429
|
+
const idx = issues.findIndex(i => i.id === issueId);
|
|
430
|
+
if (idx !== -1) {
|
|
431
|
+
issues[idx].solution_count = solutions.length;
|
|
432
|
+
issues[idx].updated_at = new Date().toISOString();
|
|
433
|
+
writeIssuesJsonl(issuesDir, issues);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return { success: true, solution: newSolution };
|
|
437
|
+
});
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// PATCH /api/issues/:id/tasks/:taskId - Update task
|
|
442
|
+
const taskMatch = pathname.match(/^\/api\/issues\/([^/]+)\/tasks\/([^/]+)$/);
|
|
443
|
+
if (taskMatch && req.method === 'PATCH') {
|
|
444
|
+
const issueId = decodeURIComponent(taskMatch[1]);
|
|
445
|
+
const taskId = decodeURIComponent(taskMatch[2]);
|
|
446
|
+
|
|
447
|
+
handlePostRequest(req, res, async (body: any) => {
|
|
448
|
+
const issues = readIssuesJsonl(issuesDir);
|
|
449
|
+
const issue = issues.find(i => i.id === issueId);
|
|
450
|
+
if (!issue?.bound_solution_id) return { error: 'Issue or bound solution not found' };
|
|
451
|
+
|
|
452
|
+
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
|
453
|
+
const solIdx = solutions.findIndex(s => s.id === issue.bound_solution_id);
|
|
454
|
+
if (solIdx === -1) return { error: 'Bound solution not found' };
|
|
455
|
+
|
|
456
|
+
const taskIdx = solutions[solIdx].tasks?.findIndex((t: any) => t.id === taskId);
|
|
457
|
+
if (taskIdx === -1 || taskIdx === undefined) return { error: 'Task not found' };
|
|
458
|
+
|
|
459
|
+
const updates: string[] = [];
|
|
460
|
+
for (const field of ['status', 'priority', 'result', 'error']) {
|
|
461
|
+
if (body[field] !== undefined) {
|
|
462
|
+
solutions[solIdx].tasks[taskIdx][field] = body[field];
|
|
463
|
+
updates.push(field);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
solutions[solIdx].tasks[taskIdx].updated_at = new Date().toISOString();
|
|
467
|
+
writeSolutionsJsonl(issuesDir, issueId, solutions);
|
|
468
|
+
|
|
469
|
+
return { success: true, issueId, taskId, updated: updates };
|
|
470
|
+
});
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Legacy: PUT /api/issues/:id/task/:taskId (backward compat)
|
|
475
|
+
const legacyTaskMatch = pathname.match(/^\/api\/issues\/([^/]+)\/task\/([^/]+)$/);
|
|
476
|
+
if (legacyTaskMatch && req.method === 'PUT') {
|
|
477
|
+
const issueId = decodeURIComponent(legacyTaskMatch[1]);
|
|
478
|
+
const taskId = decodeURIComponent(legacyTaskMatch[2]);
|
|
479
|
+
|
|
480
|
+
handlePostRequest(req, res, async (body: any) => {
|
|
481
|
+
const issues = readIssuesJsonl(issuesDir);
|
|
482
|
+
const issue = issues.find(i => i.id === issueId);
|
|
483
|
+
if (!issue?.bound_solution_id) return { error: 'Issue or bound solution not found' };
|
|
484
|
+
|
|
485
|
+
const solutions = readSolutionsJsonl(issuesDir, issueId);
|
|
486
|
+
const solIdx = solutions.findIndex(s => s.id === issue.bound_solution_id);
|
|
487
|
+
if (solIdx === -1) return { error: 'Bound solution not found' };
|
|
488
|
+
|
|
489
|
+
const taskIdx = solutions[solIdx].tasks?.findIndex((t: any) => t.id === taskId);
|
|
490
|
+
if (taskIdx === -1 || taskIdx === undefined) return { error: 'Task not found' };
|
|
491
|
+
|
|
492
|
+
const updates: string[] = [];
|
|
493
|
+
if (body.status !== undefined) { solutions[solIdx].tasks[taskIdx].status = body.status; updates.push('status'); }
|
|
494
|
+
if (body.priority !== undefined) { solutions[solIdx].tasks[taskIdx].priority = body.priority; updates.push('priority'); }
|
|
495
|
+
solutions[solIdx].tasks[taskIdx].updated_at = new Date().toISOString();
|
|
496
|
+
writeSolutionsJsonl(issuesDir, issueId, solutions);
|
|
497
|
+
|
|
498
|
+
return { success: true, issueId, taskId, updated: updates };
|
|
499
|
+
});
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Legacy: PUT /api/issues/:id/bind/:solutionId (backward compat)
|
|
504
|
+
const legacyBindMatch = pathname.match(/^\/api\/issues\/([^/]+)\/bind\/([^/]+)$/);
|
|
505
|
+
if (legacyBindMatch && req.method === 'PUT') {
|
|
506
|
+
const issueId = decodeURIComponent(legacyBindMatch[1]);
|
|
507
|
+
const solutionId = decodeURIComponent(legacyBindMatch[2]);
|
|
508
|
+
|
|
509
|
+
const issues = readIssuesJsonl(issuesDir);
|
|
510
|
+
const issueIndex = issues.findIndex(i => i.id === issueId);
|
|
511
|
+
if (issueIndex === -1) {
|
|
512
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
513
|
+
res.end(JSON.stringify({ error: 'Issue not found' }));
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const result = bindSolutionToIssue(issuesDir, issueId, solutionId, issues, issueIndex);
|
|
518
|
+
if (result.error) {
|
|
519
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
520
|
+
res.end(JSON.stringify(result));
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
issues[issueIndex].updated_at = new Date().toISOString();
|
|
525
|
+
writeIssuesJsonl(issuesDir, issues);
|
|
526
|
+
|
|
527
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
528
|
+
res.end(JSON.stringify({ success: true, issueId, solutionId }));
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Legacy: PUT /api/issues/:id (backward compat for PATCH)
|
|
533
|
+
const legacyUpdateMatch = pathname.match(/^\/api\/issues\/([^/]+)$/);
|
|
534
|
+
if (legacyUpdateMatch && req.method === 'PUT') {
|
|
535
|
+
const issueId = decodeURIComponent(legacyUpdateMatch[1]);
|
|
536
|
+
if (issueId === 'queue') return false;
|
|
537
|
+
|
|
538
|
+
handlePostRequest(req, res, async (body: any) => {
|
|
539
|
+
const issues = readIssuesJsonl(issuesDir);
|
|
540
|
+
const issueIndex = issues.findIndex(i => i.id === issueId);
|
|
541
|
+
if (issueIndex === -1) return { error: 'Issue not found' };
|
|
542
|
+
|
|
543
|
+
const updates: string[] = [];
|
|
544
|
+
for (const field of ['title', 'context', 'status', 'priority', 'bound_solution_id', 'labels']) {
|
|
545
|
+
if (body[field] !== undefined) {
|
|
546
|
+
issues[issueIndex][field] = body[field];
|
|
547
|
+
updates.push(field);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
issues[issueIndex].updated_at = new Date().toISOString();
|
|
552
|
+
writeIssuesJsonl(issuesDir, issues);
|
|
553
|
+
return { success: true, issueId, updated: updates };
|
|
554
|
+
});
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return false;
|
|
559
|
+
}
|
package/ccw/src/core/server.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { handleGraphRoutes } from './routes/graph-routes.js';
|
|
|
17
17
|
import { handleSystemRoutes } from './routes/system-routes.js';
|
|
18
18
|
import { handleFilesRoutes } from './routes/files-routes.js';
|
|
19
19
|
import { handleSkillsRoutes } from './routes/skills-routes.js';
|
|
20
|
+
import { handleIssueRoutes } from './routes/issue-routes.js';
|
|
20
21
|
import { handleRulesRoutes } from './routes/rules-routes.js';
|
|
21
22
|
import { handleSessionRoutes } from './routes/session-routes.js';
|
|
22
23
|
import { handleCcwRoutes } from './routes/ccw-routes.js';
|
|
@@ -86,7 +87,9 @@ const MODULE_CSS_FILES = [
|
|
|
86
87
|
'28-mcp-manager.css',
|
|
87
88
|
'29-help.css',
|
|
88
89
|
'30-core-memory.css',
|
|
89
|
-
'31-api-settings.css'
|
|
90
|
+
'31-api-settings.css',
|
|
91
|
+
'32-issue-manager.css',
|
|
92
|
+
'33-cli-stream-viewer.css'
|
|
90
93
|
];
|
|
91
94
|
|
|
92
95
|
// Modular JS files in dependency order
|
|
@@ -107,6 +110,7 @@ const MODULE_FILES = [
|
|
|
107
110
|
'components/flowchart.js',
|
|
108
111
|
'components/carousel.js',
|
|
109
112
|
'components/notifications.js',
|
|
113
|
+
'components/cli-stream-viewer.js',
|
|
110
114
|
'components/global-notifications.js',
|
|
111
115
|
'components/task-queue-sidebar.js',
|
|
112
116
|
'components/cli-status.js',
|
|
@@ -142,6 +146,7 @@ const MODULE_FILES = [
|
|
|
142
146
|
'views/claude-manager.js',
|
|
143
147
|
'views/api-settings.js',
|
|
144
148
|
'views/help.js',
|
|
149
|
+
'views/issue-manager.js',
|
|
145
150
|
'main.js'
|
|
146
151
|
];
|
|
147
152
|
|
|
@@ -244,7 +249,7 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
|
|
244
249
|
|
|
245
250
|
// CORS headers for API requests
|
|
246
251
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
247
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
252
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
248
253
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
249
254
|
|
|
250
255
|
if (req.method === 'OPTIONS') {
|
|
@@ -340,6 +345,16 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
|
|
340
345
|
if (await handleSkillsRoutes(routeContext)) return;
|
|
341
346
|
}
|
|
342
347
|
|
|
348
|
+
// Queue routes (/api/queue*) - top-level queue API
|
|
349
|
+
if (pathname.startsWith('/api/queue')) {
|
|
350
|
+
if (await handleIssueRoutes(routeContext)) return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Issue routes (/api/issues*)
|
|
354
|
+
if (pathname.startsWith('/api/issues')) {
|
|
355
|
+
if (await handleIssueRoutes(routeContext)) return;
|
|
356
|
+
}
|
|
357
|
+
|
|
343
358
|
// Rules routes (/api/rules*)
|
|
344
359
|
if (pathname.startsWith('/api/rules')) {
|
|
345
360
|
if (await handleRulesRoutes(routeContext)) return;
|