Skip to content

Commit 8f31033

Browse files
author
catlog22
committed
feat(issue-management): Implement interactive issue management command with CRUD operations
- Added `/issue:manage` command for interactive issue management via CLI. - Implemented features for listing, viewing, editing, deleting, and bulk operations on issues. - Integrated GitHub issue fetching and text description parsing for issue creation. - Enhanced user experience with menu-driven interface and structured output. - Created helper functions for parsing user input and managing issue data. - Added error handling and related command references for better usability. feat(issue-creation): Introduce structured issue creation from GitHub URL or text description - Added `/issue:new` command to create structured issues from GitHub issues or text descriptions. - Implemented parsing logic for extracting key elements from issue descriptions. - Integrated user confirmation for issue creation with options to edit title and priority. - Ensured proper writing of issues to `.workflow/issues/issues.jsonl` with metadata. - Included examples and error handling for various input scenarios.
1 parent 0157e36 commit 8f31033

File tree

9 files changed

+2359
-37
lines changed

9 files changed

+2359
-37
lines changed

.claude/commands/issue/manage.md

Lines changed: 865 additions & 0 deletions
Large diffs are not rendered by default.

.claude/commands/issue/new.md

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
---
2+
name: new
3+
description: Create structured issue from GitHub URL or text description, extracting key elements into issues.jsonl
4+
argument-hint: "<github-url | text-description> [--priority 1-5] [--labels label1,label2]"
5+
allowed-tools: TodoWrite(*), Bash(*), Read(*), Write(*), WebFetch(*), AskUserQuestion(*)
6+
---
7+
8+
# Issue New Command (/issue:new)
9+
10+
## Overview
11+
12+
Creates a new structured issue from either:
13+
1. **GitHub Issue URL** - Fetches and parses issue content via `gh` CLI
14+
2. **Text Description** - Parses natural language into structured fields
15+
16+
Outputs a well-formed issue entry to `.workflow/issues/issues.jsonl`.
17+
18+
## Issue Structure
19+
20+
```typescript
21+
interface Issue {
22+
id: string; // GH-123 or ISS-YYYYMMDD-HHMMSS
23+
title: string; // Issue title (clear, concise)
24+
status: 'registered'; // Initial status
25+
priority: number; // 1 (critical) to 5 (low)
26+
context: string; // Problem description
27+
source: 'github' | 'text'; // Input source type
28+
source_url?: string; // GitHub URL if applicable
29+
labels?: string[]; // Categorization labels
30+
31+
// Structured extraction
32+
problem_statement: string; // What is the problem?
33+
expected_behavior?: string; // What should happen?
34+
actual_behavior?: string; // What actually happens?
35+
affected_components?: string[];// Files/modules affected
36+
reproduction_steps?: string[]; // Steps to reproduce
37+
38+
// Metadata
39+
bound_solution_id: null;
40+
solution_count: 0;
41+
created_at: string;
42+
updated_at: string;
43+
}
44+
```
45+
46+
## Usage
47+
48+
```bash
49+
# From GitHub URL
50+
/issue:new https://github.com/owner/repo/issues/123
51+
52+
# From text description
53+
/issue:new "Login fails when password contains special characters. Expected: successful login. Actual: 500 error. Affects src/auth/*"
54+
55+
# With options
56+
/issue:new <url-or-text> --priority 2 --labels "bug,auth"
57+
```
58+
59+
## Implementation
60+
61+
### Phase 1: Input Detection
62+
63+
```javascript
64+
const input = userInput.trim();
65+
const flags = parseFlags(userInput); // --priority, --labels
66+
67+
// Detect input type
68+
const isGitHubUrl = input.match(/github\.com\/[\w-]+\/[\w-]+\/issues\/\d+/);
69+
const isGitHubShort = input.match(/^#(\d+)$/); // #123 format
70+
71+
let issueData = {};
72+
73+
if (isGitHubUrl || isGitHubShort) {
74+
// GitHub issue - fetch via gh CLI
75+
issueData = await fetchGitHubIssue(input);
76+
} else {
77+
// Text description - parse structure
78+
issueData = await parseTextDescription(input);
79+
}
80+
```
81+
82+
### Phase 2: GitHub Issue Fetching
83+
84+
```javascript
85+
async function fetchGitHubIssue(urlOrNumber) {
86+
let issueRef;
87+
88+
if (urlOrNumber.startsWith('http')) {
89+
// Extract owner/repo/number from URL
90+
const match = urlOrNumber.match(/github\.com\/([\w-]+)\/([\w-]+)\/issues\/(\d+)/);
91+
if (!match) throw new Error('Invalid GitHub URL');
92+
issueRef = `${match[1]}/${match[2]}#${match[3]}`;
93+
} else {
94+
// #123 format - use current repo
95+
issueRef = urlOrNumber.replace('#', '');
96+
}
97+
98+
// Fetch via gh CLI
99+
const result = Bash(`gh issue view ${issueRef} --json number,title,body,labels,state,url`);
100+
const ghIssue = JSON.parse(result);
101+
102+
// Parse body for structure
103+
const parsed = parseIssueBody(ghIssue.body);
104+
105+
return {
106+
id: `GH-${ghIssue.number}`,
107+
title: ghIssue.title,
108+
source: 'github',
109+
source_url: ghIssue.url,
110+
labels: ghIssue.labels.map(l => l.name),
111+
context: ghIssue.body,
112+
...parsed
113+
};
114+
}
115+
116+
function parseIssueBody(body) {
117+
// Extract structured sections from markdown body
118+
const sections = {};
119+
120+
// Problem/Description
121+
const problemMatch = body.match(/##?\s*(problem|description|issue)[:\s]*([\s\S]*?)(?=##|$)/i);
122+
if (problemMatch) sections.problem_statement = problemMatch[2].trim();
123+
124+
// Expected behavior
125+
const expectedMatch = body.match(/##?\s*(expected|should)[:\s]*([\s\S]*?)(?=##|$)/i);
126+
if (expectedMatch) sections.expected_behavior = expectedMatch[2].trim();
127+
128+
// Actual behavior
129+
const actualMatch = body.match(/##?\s*(actual|current)[:\s]*([\s\S]*?)(?=##|$)/i);
130+
if (actualMatch) sections.actual_behavior = actualMatch[2].trim();
131+
132+
// Steps to reproduce
133+
const stepsMatch = body.match(/##?\s*(steps|reproduce)[:\s]*([\s\S]*?)(?=##|$)/i);
134+
if (stepsMatch) {
135+
const stepsText = stepsMatch[2].trim();
136+
sections.reproduction_steps = stepsText
137+
.split('\n')
138+
.filter(line => line.match(/^\s*[\d\-\*]/))
139+
.map(line => line.replace(/^\s*[\d\.\-\*]\s*/, '').trim());
140+
}
141+
142+
// Affected components (from file references)
143+
const fileMatches = body.match(/`[^`]*\.(ts|js|tsx|jsx|py|go|rs)[^`]*`/g);
144+
if (fileMatches) {
145+
sections.affected_components = [...new Set(fileMatches.map(f => f.replace(/`/g, '')))];
146+
}
147+
148+
// Fallback: use entire body as problem statement
149+
if (!sections.problem_statement) {
150+
sections.problem_statement = body.substring(0, 500);
151+
}
152+
153+
return sections;
154+
}
155+
```
156+
157+
### Phase 3: Text Description Parsing
158+
159+
```javascript
160+
async function parseTextDescription(text) {
161+
// Generate unique ID
162+
const id = `ISS-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`;
163+
164+
// Extract structured elements using patterns
165+
const result = {
166+
id,
167+
source: 'text',
168+
title: '',
169+
problem_statement: '',
170+
expected_behavior: null,
171+
actual_behavior: null,
172+
affected_components: [],
173+
reproduction_steps: []
174+
};
175+
176+
// Pattern: "Title. Description. Expected: X. Actual: Y. Affects: files"
177+
const sentences = text.split(/\.(?=\s|$)/);
178+
179+
// First sentence as title
180+
result.title = sentences[0]?.trim() || 'Untitled Issue';
181+
182+
// Look for keywords
183+
for (const sentence of sentences) {
184+
const s = sentence.trim();
185+
186+
if (s.match(/^expected:?\s*/i)) {
187+
result.expected_behavior = s.replace(/^expected:?\s*/i, '');
188+
} else if (s.match(/^actual:?\s*/i)) {
189+
result.actual_behavior = s.replace(/^actual:?\s*/i, '');
190+
} else if (s.match(/^affects?:?\s*/i)) {
191+
const components = s.replace(/^affects?:?\s*/i, '').split(/[,\s]+/);
192+
result.affected_components = components.filter(c => c.includes('/') || c.includes('.'));
193+
} else if (s.match(/^steps?:?\s*/i)) {
194+
result.reproduction_steps = s.replace(/^steps?:?\s*/i, '').split(/[,;]/);
195+
} else if (!result.problem_statement && s.length > 10) {
196+
result.problem_statement = s;
197+
}
198+
}
199+
200+
// Fallback problem statement
201+
if (!result.problem_statement) {
202+
result.problem_statement = text.substring(0, 300);
203+
}
204+
205+
return result;
206+
}
207+
```
208+
209+
### Phase 4: User Confirmation
210+
211+
```javascript
212+
// Show parsed data and ask for confirmation
213+
console.log(`
214+
## Parsed Issue
215+
216+
**ID**: ${issueData.id}
217+
**Title**: ${issueData.title}
218+
**Source**: ${issueData.source}${issueData.source_url ? ` (${issueData.source_url})` : ''}
219+
220+
### Problem Statement
221+
${issueData.problem_statement}
222+
223+
${issueData.expected_behavior ? `### Expected Behavior\n${issueData.expected_behavior}\n` : ''}
224+
${issueData.actual_behavior ? `### Actual Behavior\n${issueData.actual_behavior}\n` : ''}
225+
${issueData.affected_components?.length ? `### Affected Components\n${issueData.affected_components.map(c => `- ${c}`).join('\n')}\n` : ''}
226+
${issueData.reproduction_steps?.length ? `### Reproduction Steps\n${issueData.reproduction_steps.map((s, i) => `${i+1}. ${s}`).join('\n')}\n` : ''}
227+
`);
228+
229+
// Ask user to confirm or edit
230+
const answer = AskUserQuestion({
231+
questions: [{
232+
question: 'Create this issue?',
233+
header: 'Confirm',
234+
multiSelect: false,
235+
options: [
236+
{ label: 'Create', description: 'Save issue to issues.jsonl' },
237+
{ label: 'Edit Title', description: 'Modify the issue title' },
238+
{ label: 'Edit Priority', description: 'Change priority (1-5)' },
239+
{ label: 'Cancel', description: 'Discard and exit' }
240+
]
241+
}]
242+
});
243+
244+
if (answer.includes('Cancel')) {
245+
console.log('Issue creation cancelled.');
246+
return;
247+
}
248+
249+
if (answer.includes('Edit Title')) {
250+
const titleAnswer = AskUserQuestion({
251+
questions: [{
252+
question: 'Enter new title:',
253+
header: 'Title',
254+
multiSelect: false,
255+
options: [
256+
{ label: issueData.title.substring(0, 40), description: 'Keep current' }
257+
]
258+
}]
259+
});
260+
// Handle custom input via "Other"
261+
if (titleAnswer.customText) {
262+
issueData.title = titleAnswer.customText;
263+
}
264+
}
265+
```
266+
267+
### Phase 5: Write to JSONL
268+
269+
```javascript
270+
// Construct final issue object
271+
const priority = flags.priority ? parseInt(flags.priority) : 3;
272+
const labels = flags.labels ? flags.labels.split(',').map(l => l.trim()) : [];
273+
274+
const newIssue = {
275+
id: issueData.id,
276+
title: issueData.title,
277+
status: 'registered',
278+
priority,
279+
context: issueData.problem_statement,
280+
source: issueData.source,
281+
source_url: issueData.source_url || null,
282+
labels: [...(issueData.labels || []), ...labels],
283+
284+
// Structured fields
285+
problem_statement: issueData.problem_statement,
286+
expected_behavior: issueData.expected_behavior || null,
287+
actual_behavior: issueData.actual_behavior || null,
288+
affected_components: issueData.affected_components || [],
289+
reproduction_steps: issueData.reproduction_steps || [],
290+
291+
// Metadata
292+
bound_solution_id: null,
293+
solution_count: 0,
294+
created_at: new Date().toISOString(),
295+
updated_at: new Date().toISOString()
296+
};
297+
298+
// Ensure directory exists
299+
Bash('mkdir -p .workflow/issues');
300+
301+
// Append to issues.jsonl
302+
const issuesPath = '.workflow/issues/issues.jsonl';
303+
Bash(`echo '${JSON.stringify(newIssue)}' >> "${issuesPath}"`);
304+
305+
console.log(`
306+
## Issue Created
307+
308+
**ID**: ${newIssue.id}
309+
**Title**: ${newIssue.title}
310+
**Priority**: ${newIssue.priority}
311+
**Labels**: ${newIssue.labels.join(', ') || 'none'}
312+
**Source**: ${newIssue.source}
313+
314+
### Next Steps
315+
1. Plan solution: \`/issue:plan ${newIssue.id}\`
316+
2. View details: \`ccw issue status ${newIssue.id}\`
317+
3. Manage issues: \`/issue:manage\`
318+
`);
319+
```
320+
321+
## Examples
322+
323+
### GitHub Issue
324+
325+
```bash
326+
/issue:new https://github.com/myorg/myrepo/issues/42 --priority 2
327+
328+
# Output:
329+
## Issue Created
330+
**ID**: GH-42
331+
**Title**: Fix memory leak in WebSocket handler
332+
**Priority**: 2
333+
**Labels**: bug, performance
334+
**Source**: github (https://github.com/myorg/myrepo/issues/42)
335+
```
336+
337+
### Text Description
338+
339+
```bash
340+
/issue:new "API rate limiting not working. Expected: 429 after 100 requests. Actual: No limit. Affects src/middleware/rate-limit.ts"
341+
342+
# Output:
343+
## Issue Created
344+
**ID**: ISS-20251227-142530
345+
**Title**: API rate limiting not working
346+
**Priority**: 3
347+
**Labels**: none
348+
**Source**: text
349+
```
350+
351+
## Error Handling
352+
353+
| Error | Resolution |
354+
|-------|------------|
355+
| Invalid GitHub URL | Show format hint, ask for correction |
356+
| gh CLI not available | Fall back to WebFetch for public issues |
357+
| Empty description | Prompt user for required fields |
358+
| Duplicate issue ID | Auto-increment or suggest merge |
359+
| Parse failure | Show raw input, ask for manual structuring |
360+
361+
## Related Commands
362+
363+
- `/issue:plan` - Plan solution for issue
364+
- `/issue:manage` - Interactive issue management
365+
- `ccw issue list` - List all issues
366+
- `ccw issue status <id>` - View issue details

ccw/src/commands/issue.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,9 +663,11 @@ async function queueAction(subAction: string | undefined, issueId: string | unde
663663
for (const item of queue.queue) {
664664
const statusColor = {
665665
'pending': chalk.gray,
666+
'ready': chalk.cyan,
666667
'executing': chalk.yellow,
667668
'completed': chalk.green,
668-
'failed': chalk.red
669+
'failed': chalk.red,
670+
'blocked': chalk.magenta
669671
}[item.status] || chalk.white;
670672

671673
console.log(

ccw/src/core/dashboard-generator-patch.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const MODULE_FILES = [
2121
'dashboard-js/components/tabs-other.js',
2222
'dashboard-js/components/carousel.js',
2323
'dashboard-js/components/notifications.js',
24+
'dashboard-js/components/cli-stream-viewer.js',
2425
'dashboard-js/components/global-notifications.js',
2526
'dashboard-js/components/cli-status.js',
2627
'dashboard-js/components/cli-history.js',

0 commit comments

Comments
 (0)