|
| 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 |
0 commit comments