Files
Claude-Code-Workflow/.claude/commands/issue/new.md
catlog22 8f310339df 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.
2025-12-27 10:20:34 +08:00

11 KiB

name, description, argument-hint, allowed-tools
name description argument-hint allowed-tools
new Create structured issue from GitHub URL or text description, extracting key elements into issues.jsonl <github-url | text-description> [--priority 1-5] [--labels label1,label2] TodoWrite(*), Bash(*), Read(*), Write(*), WebFetch(*), AskUserQuestion(*)

Issue New Command (/issue:new)

Overview

Creates a new structured issue from either:

  1. GitHub Issue URL - Fetches and parses issue content via gh CLI
  2. Text Description - Parses natural language into structured fields

Outputs a well-formed issue entry to .workflow/issues/issues.jsonl.

Issue Structure

interface Issue {
  id: string;                    // GH-123 or ISS-YYYYMMDD-HHMMSS
  title: string;                 // Issue title (clear, concise)
  status: 'registered';          // Initial status
  priority: number;              // 1 (critical) to 5 (low)
  context: string;               // Problem description
  source: 'github' | 'text';     // Input source type
  source_url?: string;           // GitHub URL if applicable
  labels?: string[];             // Categorization labels
  
  // Structured extraction
  problem_statement: string;     // What is the problem?
  expected_behavior?: string;    // What should happen?
  actual_behavior?: string;      // What actually happens?
  affected_components?: string[];// Files/modules affected
  reproduction_steps?: string[]; // Steps to reproduce
  
  // Metadata
  bound_solution_id: null;
  solution_count: 0;
  created_at: string;
  updated_at: string;
}

Usage

# From GitHub URL
/issue:new https://github.com/owner/repo/issues/123

# From text description
/issue:new "Login fails when password contains special characters. Expected: successful login. Actual: 500 error. Affects src/auth/*"

# With options
/issue:new <url-or-text> --priority 2 --labels "bug,auth"

Implementation

Phase 1: Input Detection

const input = userInput.trim();
const flags = parseFlags(userInput);  // --priority, --labels

// Detect input type
const isGitHubUrl = input.match(/github\.com\/[\w-]+\/[\w-]+\/issues\/\d+/);
const isGitHubShort = input.match(/^#(\d+)$/);  // #123 format

let issueData = {};

if (isGitHubUrl || isGitHubShort) {
  // GitHub issue - fetch via gh CLI
  issueData = await fetchGitHubIssue(input);
} else {
  // Text description - parse structure
  issueData = await parseTextDescription(input);
}

Phase 2: GitHub Issue Fetching

async function fetchGitHubIssue(urlOrNumber) {
  let issueRef;
  
  if (urlOrNumber.startsWith('http')) {
    // Extract owner/repo/number from URL
    const match = urlOrNumber.match(/github\.com\/([\w-]+)\/([\w-]+)\/issues\/(\d+)/);
    if (!match) throw new Error('Invalid GitHub URL');
    issueRef = `${match[1]}/${match[2]}#${match[3]}`;
  } else {
    // #123 format - use current repo
    issueRef = urlOrNumber.replace('#', '');
  }
  
  // Fetch via gh CLI
  const result = Bash(`gh issue view ${issueRef} --json number,title,body,labels,state,url`);
  const ghIssue = JSON.parse(result);
  
  // Parse body for structure
  const parsed = parseIssueBody(ghIssue.body);
  
  return {
    id: `GH-${ghIssue.number}`,
    title: ghIssue.title,
    source: 'github',
    source_url: ghIssue.url,
    labels: ghIssue.labels.map(l => l.name),
    context: ghIssue.body,
    ...parsed
  };
}

function parseIssueBody(body) {
  // Extract structured sections from markdown body
  const sections = {};
  
  // Problem/Description
  const problemMatch = body.match(/##?\s*(problem|description|issue)[:\s]*([\s\S]*?)(?=##|$)/i);
  if (problemMatch) sections.problem_statement = problemMatch[2].trim();
  
  // Expected behavior
  const expectedMatch = body.match(/##?\s*(expected|should)[:\s]*([\s\S]*?)(?=##|$)/i);
  if (expectedMatch) sections.expected_behavior = expectedMatch[2].trim();
  
  // Actual behavior
  const actualMatch = body.match(/##?\s*(actual|current)[:\s]*([\s\S]*?)(?=##|$)/i);
  if (actualMatch) sections.actual_behavior = actualMatch[2].trim();
  
  // Steps to reproduce
  const stepsMatch = body.match(/##?\s*(steps|reproduce)[:\s]*([\s\S]*?)(?=##|$)/i);
  if (stepsMatch) {
    const stepsText = stepsMatch[2].trim();
    sections.reproduction_steps = stepsText
      .split('\n')
      .filter(line => line.match(/^\s*[\d\-\*]/))
      .map(line => line.replace(/^\s*[\d\.\-\*]\s*/, '').trim());
  }
  
  // Affected components (from file references)
  const fileMatches = body.match(/`[^`]*\.(ts|js|tsx|jsx|py|go|rs)[^`]*`/g);
  if (fileMatches) {
    sections.affected_components = [...new Set(fileMatches.map(f => f.replace(/`/g, '')))];
  }
  
  // Fallback: use entire body as problem statement
  if (!sections.problem_statement) {
    sections.problem_statement = body.substring(0, 500);
  }
  
  return sections;
}

Phase 3: Text Description Parsing

async function parseTextDescription(text) {
  // Generate unique ID
  const id = `ISS-${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}`;
  
  // Extract structured elements using patterns
  const result = {
    id,
    source: 'text',
    title: '',
    problem_statement: '',
    expected_behavior: null,
    actual_behavior: null,
    affected_components: [],
    reproduction_steps: []
  };
  
  // Pattern: "Title. Description. Expected: X. Actual: Y. Affects: files"
  const sentences = text.split(/\.(?=\s|$)/);
  
  // First sentence as title
  result.title = sentences[0]?.trim() || 'Untitled Issue';
  
  // Look for keywords
  for (const sentence of sentences) {
    const s = sentence.trim();
    
    if (s.match(/^expected:?\s*/i)) {
      result.expected_behavior = s.replace(/^expected:?\s*/i, '');
    } else if (s.match(/^actual:?\s*/i)) {
      result.actual_behavior = s.replace(/^actual:?\s*/i, '');
    } else if (s.match(/^affects?:?\s*/i)) {
      const components = s.replace(/^affects?:?\s*/i, '').split(/[,\s]+/);
      result.affected_components = components.filter(c => c.includes('/') || c.includes('.'));
    } else if (s.match(/^steps?:?\s*/i)) {
      result.reproduction_steps = s.replace(/^steps?:?\s*/i, '').split(/[,;]/);
    } else if (!result.problem_statement && s.length > 10) {
      result.problem_statement = s;
    }
  }
  
  // Fallback problem statement
  if (!result.problem_statement) {
    result.problem_statement = text.substring(0, 300);
  }
  
  return result;
}

Phase 4: User Confirmation

// Show parsed data and ask for confirmation
console.log(`
## Parsed Issue

**ID**: ${issueData.id}
**Title**: ${issueData.title}
**Source**: ${issueData.source}${issueData.source_url ? ` (${issueData.source_url})` : ''}

### Problem Statement
${issueData.problem_statement}

${issueData.expected_behavior ? `### Expected Behavior\n${issueData.expected_behavior}\n` : ''}
${issueData.actual_behavior ? `### Actual Behavior\n${issueData.actual_behavior}\n` : ''}
${issueData.affected_components?.length ? `### Affected Components\n${issueData.affected_components.map(c => `- ${c}`).join('\n')}\n` : ''}
${issueData.reproduction_steps?.length ? `### Reproduction Steps\n${issueData.reproduction_steps.map((s, i) => `${i+1}. ${s}`).join('\n')}\n` : ''}
`);

// Ask user to confirm or edit
const answer = AskUserQuestion({
  questions: [{
    question: 'Create this issue?',
    header: 'Confirm',
    multiSelect: false,
    options: [
      { label: 'Create', description: 'Save issue to issues.jsonl' },
      { label: 'Edit Title', description: 'Modify the issue title' },
      { label: 'Edit Priority', description: 'Change priority (1-5)' },
      { label: 'Cancel', description: 'Discard and exit' }
    ]
  }]
});

if (answer.includes('Cancel')) {
  console.log('Issue creation cancelled.');
  return;
}

if (answer.includes('Edit Title')) {
  const titleAnswer = AskUserQuestion({
    questions: [{
      question: 'Enter new title:',
      header: 'Title',
      multiSelect: false,
      options: [
        { label: issueData.title.substring(0, 40), description: 'Keep current' }
      ]
    }]
  });
  // Handle custom input via "Other"
  if (titleAnswer.customText) {
    issueData.title = titleAnswer.customText;
  }
}

Phase 5: Write to JSONL

// Construct final issue object
const priority = flags.priority ? parseInt(flags.priority) : 3;
const labels = flags.labels ? flags.labels.split(',').map(l => l.trim()) : [];

const newIssue = {
  id: issueData.id,
  title: issueData.title,
  status: 'registered',
  priority,
  context: issueData.problem_statement,
  source: issueData.source,
  source_url: issueData.source_url || null,
  labels: [...(issueData.labels || []), ...labels],
  
  // Structured fields
  problem_statement: issueData.problem_statement,
  expected_behavior: issueData.expected_behavior || null,
  actual_behavior: issueData.actual_behavior || null,
  affected_components: issueData.affected_components || [],
  reproduction_steps: issueData.reproduction_steps || [],
  
  // Metadata
  bound_solution_id: null,
  solution_count: 0,
  created_at: new Date().toISOString(),
  updated_at: new Date().toISOString()
};

// Ensure directory exists
Bash('mkdir -p .workflow/issues');

// Append to issues.jsonl
const issuesPath = '.workflow/issues/issues.jsonl';
Bash(`echo '${JSON.stringify(newIssue)}' >> "${issuesPath}"`);

console.log(`
## Issue Created

**ID**: ${newIssue.id}
**Title**: ${newIssue.title}
**Priority**: ${newIssue.priority}
**Labels**: ${newIssue.labels.join(', ') || 'none'}
**Source**: ${newIssue.source}

### Next Steps
1. Plan solution: \`/issue:plan ${newIssue.id}\`
2. View details: \`ccw issue status ${newIssue.id}\`
3. Manage issues: \`/issue:manage\`
`);

Examples

GitHub Issue

/issue:new https://github.com/myorg/myrepo/issues/42 --priority 2

# Output:
## Issue Created
**ID**: GH-42
**Title**: Fix memory leak in WebSocket handler
**Priority**: 2
**Labels**: bug, performance
**Source**: github (https://github.com/myorg/myrepo/issues/42)

Text Description

/issue:new "API rate limiting not working. Expected: 429 after 100 requests. Actual: No limit. Affects src/middleware/rate-limit.ts"

# Output:
## Issue Created
**ID**: ISS-20251227-142530
**Title**: API rate limiting not working
**Priority**: 3
**Labels**: none
**Source**: text

Error Handling

Error Resolution
Invalid GitHub URL Show format hint, ask for correction
gh CLI not available Fall back to WebFetch for public issues
Empty description Prompt user for required fields
Duplicate issue ID Auto-increment or suggest merge
Parse failure Show raw input, ask for manual structuring
  • /issue:plan - Plan solution for issue
  • /issue:manage - Interactive issue management
  • ccw issue list - List all issues
  • ccw issue status <id> - View issue details