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.
Files changed (111) hide show
  1. package/.claude/agents/issue-plan-agent.md +859 -0
  2. package/.claude/agents/issue-queue-agent.md +702 -0
  3. package/.claude/commands/issue/execute.md +453 -0
  4. package/.claude/commands/issue/manage.md +865 -0
  5. package/.claude/commands/issue/new.md +484 -0
  6. package/.claude/commands/issue/plan.md +421 -0
  7. package/.claude/commands/issue/queue.md +354 -0
  8. package/.claude/commands/{clean.md → workflow/clean.md} +5 -5
  9. package/.claude/commands/workflow/docs/analyze.md +1467 -0
  10. package/.claude/commands/workflow/docs/copyright.md +1265 -0
  11. package/.claude/commands/workflow/execute.md +0 -1
  12. package/.claude/commands/workflow/tools/conflict-resolution.md +76 -240
  13. package/.claude/commands/workflow/tools/context-gather.md +0 -2
  14. package/.claude/commands/workflow/tools/task-generate-agent.md +81 -8
  15. package/.claude/commands/workflow/tools/task-generate-tdd.md +0 -9
  16. package/.claude/commands/workflow/tools/test-context-gather.md +2 -3
  17. package/.claude/commands/workflow/tools/test-task-generate.md +0 -2
  18. package/.claude/skills/_shared/mermaid-utils.md +584 -0
  19. package/.claude/skills/command-guide/reference/agents/action-planning-agent.md +0 -2
  20. package/.claude/skills/command-guide/reference/commands/workflow/execute.md +1 -1
  21. package/.claude/skills/command-guide/reference/commands/workflow/tools/context-gather.md +1 -2
  22. package/.claude/skills/command-guide/reference/commands/workflow/tools/task-generate-tdd.md +1 -8
  23. package/.claude/skills/command-guide/reference/commands/workflow/tools/test-context-gather.md +1 -4
  24. package/.claude/skills/command-guide/reference/commands/workflow/tools/test-task-generate.md +0 -2
  25. package/.claude/skills/copyright-docs/SKILL.md +132 -0
  26. package/.claude/skills/copyright-docs/phases/01-metadata-collection.md +78 -0
  27. package/.claude/skills/copyright-docs/phases/01.5-project-exploration.md +150 -0
  28. package/.claude/skills/copyright-docs/phases/02-deep-analysis.md +664 -0
  29. package/.claude/skills/copyright-docs/phases/02.5-consolidation.md +192 -0
  30. package/.claude/skills/copyright-docs/phases/04-document-assembly.md +261 -0
  31. package/.claude/skills/copyright-docs/phases/05-compliance-refinement.md +192 -0
  32. package/.claude/skills/copyright-docs/specs/cpcc-requirements.md +121 -0
  33. package/.claude/skills/copyright-docs/templates/agent-base.md +200 -0
  34. package/.claude/skills/project-analyze/SKILL.md +162 -0
  35. package/.claude/skills/project-analyze/phases/01-requirements-discovery.md +79 -0
  36. package/.claude/skills/project-analyze/phases/02-project-exploration.md +176 -0
  37. package/.claude/skills/project-analyze/phases/03-deep-analysis.md +854 -0
  38. package/.claude/skills/project-analyze/phases/03.5-consolidation.md +233 -0
  39. package/.claude/skills/project-analyze/phases/04-report-generation.md +217 -0
  40. package/.claude/skills/project-analyze/phases/05-iterative-refinement.md +124 -0
  41. package/.claude/skills/project-analyze/specs/quality-standards.md +115 -0
  42. package/.claude/skills/project-analyze/specs/writing-style.md +152 -0
  43. package/.claude/workflows/cli-templates/schemas/conflict-resolution-schema.json +79 -65
  44. package/.claude/workflows/cli-templates/schemas/issue-task-jsonl-schema.json +136 -0
  45. package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +74 -0
  46. package/.claude/workflows/cli-templates/schemas/queue-schema.json +136 -0
  47. package/.claude/workflows/cli-templates/schemas/registry-schema.json +94 -0
  48. package/.claude/workflows/cli-templates/schemas/solution-schema.json +120 -0
  49. package/.claude/workflows/cli-templates/schemas/solutions-jsonl-schema.json +125 -0
  50. package/.codex/prompts/issue-execute.md +266 -0
  51. package/README.md +11 -1
  52. package/ccw/dist/cli.d.ts.map +1 -1
  53. package/ccw/dist/cli.js +25 -0
  54. package/ccw/dist/cli.js.map +1 -1
  55. package/ccw/dist/commands/cli.d.ts.map +1 -1
  56. package/ccw/dist/commands/cli.js +46 -8
  57. package/ccw/dist/commands/cli.js.map +1 -1
  58. package/ccw/dist/commands/issue.d.ts +21 -0
  59. package/ccw/dist/commands/issue.d.ts.map +1 -0
  60. package/ccw/dist/commands/issue.js +895 -0
  61. package/ccw/dist/commands/issue.js.map +1 -0
  62. package/ccw/dist/core/dashboard-generator-patch.js +1 -0
  63. package/ccw/dist/core/dashboard-generator-patch.js.map +1 -1
  64. package/ccw/dist/core/routes/cli-routes.js +2 -2
  65. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  66. package/ccw/dist/core/routes/issue-routes.d.ts +34 -0
  67. package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -0
  68. package/ccw/dist/core/routes/issue-routes.js +487 -0
  69. package/ccw/dist/core/routes/issue-routes.js.map +1 -0
  70. package/ccw/dist/core/server.d.ts.map +1 -1
  71. package/ccw/dist/core/server.js +17 -2
  72. package/ccw/dist/core/server.js.map +1 -1
  73. package/ccw/dist/tools/claude-cli-tools.d.ts +7 -3
  74. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
  75. package/ccw/dist/tools/claude-cli-tools.js +31 -17
  76. package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
  77. package/ccw/dist/tools/smart-search.d.ts +25 -0
  78. package/ccw/dist/tools/smart-search.d.ts.map +1 -1
  79. package/ccw/dist/tools/smart-search.js +121 -17
  80. package/ccw/dist/tools/smart-search.js.map +1 -1
  81. package/ccw/src/cli.ts +26 -0
  82. package/ccw/src/commands/cli.ts +49 -7
  83. package/ccw/src/commands/issue.ts +1184 -0
  84. package/ccw/src/core/dashboard-generator-patch.ts +1 -0
  85. package/ccw/src/core/routes/cli-routes.ts +3 -3
  86. package/ccw/src/core/routes/issue-routes.ts +559 -0
  87. package/ccw/src/core/server.ts +17 -2
  88. package/ccw/src/templates/dashboard-css/32-issue-manager.css +2544 -0
  89. package/ccw/src/templates/dashboard-css/33-cli-stream-viewer.css +467 -0
  90. package/ccw/src/templates/dashboard-js/components/cli-history.js +40 -13
  91. package/ccw/src/templates/dashboard-js/components/cli-status.js +26 -2
  92. package/ccw/src/templates/dashboard-js/components/cli-stream-viewer.js +461 -0
  93. package/ccw/src/templates/dashboard-js/components/navigation.js +8 -0
  94. package/ccw/src/templates/dashboard-js/components/notifications.js +16 -0
  95. package/ccw/src/templates/dashboard-js/i18n.js +290 -2
  96. package/ccw/src/templates/dashboard-js/views/cli-manager.js +5 -0
  97. package/ccw/src/templates/dashboard-js/views/history.js +19 -4
  98. package/ccw/src/templates/dashboard-js/views/hook-manager.js +11 -5
  99. package/ccw/src/templates/dashboard-js/views/issue-manager.js +1546 -0
  100. package/ccw/src/templates/dashboard.html +55 -0
  101. package/ccw/src/tools/claude-cli-tools.ts +37 -20
  102. package/ccw/src/tools/smart-search.ts +157 -16
  103. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  104. package/codex-lens/src/codexlens/config.py +5 -0
  105. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  106. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  107. package/codex-lens/src/codexlens/search/hybrid_search.py +144 -11
  108. package/codex-lens/src/codexlens/search/ranking.py +267 -1
  109. package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
  110. package/codex-lens/src/codexlens/semantic/chunker.py +55 -10
  111. 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 "ace"', status: 400 };
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
+ }
@@ -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;