Files
Claude-Code-Workflow/.claude/commands/issue/new.md
catlog22 0d82c9fa03 refactor(issue): simplify extended_context and add follow-up questions
- Replace discovery_context with minimal extended_context (3 fields: location, suggested_fix, notes)
- Add Phase 4.5: intelligent follow-up questions for missing issue fields
- Fix JSONL append to ensure proper newline handling
- Update schema and plan.md to use new extended_context structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 11:33:59 +08:00

18 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 (Closed-Loop)

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' | 'discovery';  // 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

  // Extended context (minimal, for planning hints)
  extended_context?: {
    location?: string;           // file:line when specific line matters
    suggested_fix?: string;      // Suggested remediation
    notes?: string;              // Additional notes (user clarifications or discovery hints)
  };

  // Closed-loop requirements (guide plan generation)
  lifecycle_requirements: {
    test_strategy: 'unit' | 'integration' | 'e2e' | 'manual' | 'auto';
    regression_scope: 'affected' | 'related' | 'full';  // Which tests to run
    acceptance_type: 'automated' | 'manual' | 'both';   // How to verify
    commit_strategy: 'per-task' | 'squash' | 'atomic';  // Commit granularity
  };

  // Metadata
  bound_solution_id: null;
  solution_count: 0;
  created_at: string;
  updated_at: string;
}

Lifecycle Requirements

The lifecycle_requirements field guides downstream commands (/issue:plan, /issue:execute):

Field Options Purpose
test_strategy unit, integration, e2e, manual, auto Which test types to generate
regression_scope affected, related, full Which tests to run for regression
acceptance_type automated, manual, both How to verify completion
commit_strategy per-task, squash, atomic Commit granularity

Note

: Task structure (SolutionTask) is defined in /issue:plan - see .claude/commands/issue/plan.md

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: Lifecycle Configuration

// Ask for lifecycle requirements (or use smart defaults)
const lifecycleAnswer = AskUserQuestion({
  questions: [
    {
      question: 'Test strategy for this issue?',
      header: 'Test',
      multiSelect: false,
      options: [
        { label: 'auto', description: 'Auto-detect based on affected files (Recommended)' },
        { label: 'unit', description: 'Unit tests only' },
        { label: 'integration', description: 'Integration tests' },
        { label: 'e2e', description: 'End-to-end tests' },
        { label: 'manual', description: 'Manual testing only' }
      ]
    },
    {
      question: 'Regression scope?',
      header: 'Regression',
      multiSelect: false,
      options: [
        { label: 'affected', description: 'Only affected module tests (Recommended)' },
        { label: 'related', description: 'Affected + dependent modules' },
        { label: 'full', description: 'Full test suite' }
      ]
    },
    {
      question: 'Commit strategy?',
      header: 'Commit',
      multiSelect: false,
      options: [
        { label: 'per-task', description: 'One commit per task (Recommended)' },
        { label: 'atomic', description: 'Single commit for entire issue' },
        { label: 'squash', description: 'Squash at the end' }
      ]
    }
  ]
});

const lifecycle = {
  test_strategy: lifecycleAnswer.test || 'auto',
  regression_scope: lifecycleAnswer.regression || 'affected',
  acceptance_type: 'automated',
  commit_strategy: lifecycleAnswer.commit || 'per-task'
};

issueData.lifecycle_requirements = lifecycle;

Phase 4.5: Follow-up Questions (Intelligent Suggestions)

// Analyze parsed data and suggest follow-up questions for missing/unclear fields
const suggestions = [];

// Check for missing critical fields
if (!issueData.expected_behavior) {
  suggestions.push({
    field: 'expected_behavior',
    question: 'What is the expected behavior?',
    hint: 'Describe what should happen when working correctly'
  });
}

if (!issueData.actual_behavior && issueData.source !== 'discovery') {
  suggestions.push({
    field: 'actual_behavior',
    question: 'What is the actual behavior?',
    hint: 'Describe what currently happens (error message, wrong output, etc.)'
  });
}

if (!issueData.affected_components?.length) {
  suggestions.push({
    field: 'affected_components',
    question: 'Which files or modules are affected?',
    hint: 'e.g., src/auth/login.ts, src/api/users.ts'
  });
}

if (!issueData.reproduction_steps?.length && issueData.source === 'text') {
  suggestions.push({
    field: 'reproduction_steps',
    question: 'How can this issue be reproduced?',
    hint: 'Step-by-step instructions to trigger the issue'
  });
}

// Ask follow-up questions if any suggestions exist
let userNotes = '';
if (suggestions.length > 0) {
  console.log(`
## Suggested Clarifications

The following information would help with issue resolution:
${suggestions.map((s, i) => `${i+1}. **${s.question}** - ${s.hint}`).join('\n')}
`);

  const followUpAnswer = AskUserQuestion({
    questions: [{
      question: 'Would you like to provide additional details?',
      header: 'Clarify',
      multiSelect: false,
      options: [
        { label: 'Add details', description: 'Provide clarifications for suggested questions' },
        { label: 'Skip', description: 'Continue without additional details (Recommended)' }
      ]
    }]
  });

  if (followUpAnswer.includes('Add details')) {
    // Collect additional notes via "Other" input
    const notesAnswer = AskUserQuestion({
      questions: [{
        question: 'Enter additional details (address any of the suggested questions):',
        header: 'Details',
        multiSelect: false,
        options: [
          { label: 'Use template', description: 'Expected: ... | Actual: ... | Files: ...' }
        ]
      }]
    });

    if (notesAnswer.customText) {
      userNotes = notesAnswer.customText;

      // Parse structured input if provided
      const expectedMatch = userNotes.match(/expected:\s*([^|]+)/i);
      const actualMatch = userNotes.match(/actual:\s*([^|]+)/i);
      const filesMatch = userNotes.match(/files?:\s*([^|]+)/i);

      if (expectedMatch && !issueData.expected_behavior) {
        issueData.expected_behavior = expectedMatch[1].trim();
      }
      if (actualMatch && !issueData.actual_behavior) {
        issueData.actual_behavior = actualMatch[1].trim();
      }
      if (filesMatch && !issueData.affected_components?.length) {
        issueData.affected_components = filesMatch[1].split(/[,\s]+/).filter(f => f.includes('/') || f.includes('.'));
      }
    }
  }
}

// Store user notes in extended context
issueData.extended_context = {
  ...(issueData.extended_context || {}),
  notes: userNotes || null
};

Phase 5: 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` : ''}

### Lifecycle Configuration
- **Test Strategy**: ${lifecycle.test_strategy}
- **Regression Scope**: ${lifecycle.regression_scope}
- **Commit Strategy**: ${lifecycle.commit_strategy}
`);

// 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 6: 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 || [],

  // Extended context (universal)
  extended_context: issueData.extended_context || null,

  // Closed-loop lifecycle requirements
  lifecycle_requirements: issueData.lifecycle_requirements || {
    test_strategy: 'auto',
    regression_scope: 'affected',
    acceptance_type: 'automated',
    commit_strategy: 'per-task'
  },

  // 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 with proper newline handling
const issuesPath = '.workflow/issues/issues.jsonl';
const jsonLine = JSON.stringify(newIssue);
// Ensure file ends with newline before appending, then append with trailing newline
Bash(`[ -s "${issuesPath}" ] && [ -n "$(tail -c 1 "${issuesPath}")" ] && printf '\\n' >> "${issuesPath}"; printf '%s\\n' '${jsonLine}' >> "${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