diff --git a/.gitignore b/.gitignore index d01d04e9..35bc3586 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,4 @@ debug_*.py # Claude Code local settings (user-specific permissions) .claude/settings.local.json +.aider* diff --git a/Makefile b/Makefile index f905c200..7105a883 100644 --- a/Makefile +++ b/Makefile @@ -37,9 +37,13 @@ help: @echo " add-diary-entry - Add new entry to ProjectDiary.md (requires Claude Code)" @echo "" @echo "Issue Management:" - @echo " list-issues - Show all gitea issues with status and priority" - @echo " list-open-issues - Show only open issues (active backlog)" - @echo " show-issue NUM=X - Show detailed view of specific issue" + @echo " list-issues - Show all gitea issues with status and priority" + @echo " list-open-issues - Show only open issues (active backlog)" + @echo " show-issue NUM=X - Show detailed view of specific issue" + @echo " issues-get - Export compact issue index to ISSUES.index" + @echo " issues-csv - Export issues as CSV for spreadsheet processing" + @echo " issues-json - Export issues as JSON for programmatic processing" + @echo " issues-high - Export only high/critical priority issues" @echo "" @echo "Test-Driven Development:" @echo " test-from-issue NUM=X - Generate test skeleton from issue (requires Claude Code)" @@ -247,6 +251,40 @@ show-issue: $(VENV)/bin/activate list-open-issues: $(VENV)/bin/activate @PYTHONPATH=. $(VENV_PYTHON) tddai_cli.py list-open-issues +# Export compact issue index to ISSUES.index file (TSV format) +issues-get: $(VENV)/bin/activate + @echo "📋 Fetching issue index from gitea..." + @PYTHONPATH=. $(VENV_PYTHON) tddai_cli.py issue-index --format tsv --sort number > ISSUES.index + @echo "✅ Issue index exported to ISSUES.index (TSV format)" + @echo "📄 File contents:" + @cat ISSUES.index + +# Export issues as CSV for spreadsheet processing +issues-csv: $(VENV)/bin/activate + @echo "📊 Exporting issues as CSV..." + @PYTHONPATH=. $(VENV_PYTHON) tddai_cli.py issue-index --format csv --sort priority --include-state > ISSUES.csv + @echo "✅ Issues exported to ISSUES.csv" + @wc -l ISSUES.csv | awk '{print "📄 Total entries:", $$1-1, "(excluding header)"}' + +# Export issues as JSON for programmatic processing +issues-json: $(VENV)/bin/activate + @echo "🔧 Exporting issues as JSON..." + @PYTHONPATH=. $(VENV_PYTHON) tddai_cli.py issue-index --format json --sort priority > ISSUES.json + @echo "✅ Issues exported to ISSUES.json" + @echo "📄 Sample entry:" + @head -20 ISSUES.json + +# Export only high and critical priority issues +issues-high: $(VENV)/bin/activate + @echo "🚨 Exporting high priority issues..." + @echo "High priority issues:" > ISSUES.high.txt + @PYTHONPATH=. $(VENV_PYTHON) tddai_cli.py issue-index --format tsv --filter-priority high --sort number >> ISSUES.high.txt + @echo "" >> ISSUES.high.txt + @echo "Critical priority issues:" >> ISSUES.high.txt + @PYTHONPATH=. $(VENV_PYTHON) tddai_cli.py issue-index --format tsv --filter-priority critical --sort number >> ISSUES.high.txt + @echo "✅ High priority issues exported to ISSUES.high.txt" + @cat ISSUES.high.txt + # Generate test skeleton from gitea issue (requires Claude Code) test-from-issue: @if [ -z "$(NUM)" ]; then \ diff --git a/tddai_cli.py b/tddai_cli.py index bb6ece89..76c3ffbe 100644 --- a/tddai_cli.py +++ b/tddai_cli.py @@ -679,6 +679,123 @@ def assign_issue_to_milestone(issue_number: int, milestone_id: int): sys.exit(1) +def issue_index(format_type="tsv", sort_by="number", filter_state=None, filter_priority=None, include_state=False): + """Output compact index of all issues with ID, title, and priority for efficient parsing. + + Args: + format_type: Output format (tsv, csv, json, fields) + sort_by: Sort by field (number, title, priority, state, created, updated) + filter_state: Filter by state (open, closed, all) - None means all + filter_priority: Filter by priority (low, medium, high, critical, none) - None means all + include_state: Include state column in output + """ + try: + fetcher = IssueFetcher() + project_mgr = ProjectManager() + from tddai.config import get_config + import json + + config = get_config() + + issues = fetcher.fetch_issues() + if not issues: + return + + # Collect full issue data with additional fields + issue_data = [] + for issue in issues: + # Get priority and state from labels + priority = "none" + state = "none" + try: + # Use ProjectManager's API call method + issue_url = f"{config.issues_api_url}/{issue.number}" + detailed_issue = project_mgr._make_api_call('GET', issue_url) + labels = detailed_issue.get('labels', []) + priority_labels = [l['name'] for l in labels if l['name'].startswith('priority:')] + state_labels = [l['name'] for l in labels if l['name'].startswith('status:')] + + if priority_labels: + priority = priority_labels[0].replace('priority:', '') + if state_labels: + state = state_labels[0].replace('status:', '') + except: + pass # Keep defaults if API call fails + + issue_info = { + 'number': issue.number, + 'title': issue.title.replace('\t', ' ').replace('\n', ' '), # Clean for TSV + 'priority': priority, + 'state': issue.state, # open/closed from basic data + 'status': state, # detailed status from labels + 'created': issue.created_at.strftime('%Y-%m-%d'), + 'updated': issue.updated_at.strftime('%Y-%m-%d') + } + issue_data.append(issue_info) + + # Apply filters + if filter_state: + if filter_state == "open": + issue_data = [i for i in issue_data if i['state'] == 'open'] + elif filter_state == "closed": + issue_data = [i for i in issue_data if i['state'] == 'closed'] + + if filter_priority: + issue_data = [i for i in issue_data if i['priority'] == filter_priority] + + # Sort issues + sort_key_map = { + 'number': lambda x: x['number'], + 'title': lambda x: x['title'].lower(), + 'priority': lambda x: {'critical': 4, 'high': 3, 'medium': 2, 'low': 1, 'none': 0}[x['priority']], + 'state': lambda x: x['state'], + 'created': lambda x: x['created'], + 'updated': lambda x: x['updated'] + } + + if sort_by in sort_key_map: + issue_data.sort(key=sort_key_map[sort_by], reverse=(sort_by in ['number', 'priority', 'created', 'updated'])) + + # Output in requested format + if format_type == "json": + print(json.dumps(issue_data, indent=2)) + elif format_type == "csv": + # CSV header + if include_state: + print("number,title,priority,state,created,updated") + for issue in issue_data: + title = issue['title'].replace('"', '""') # Escape quotes + print(f'{issue["number"]},"{title}",{issue["priority"]},{issue["state"]},{issue["created"]},{issue["updated"]}') + else: + print("number,title,priority,created,updated") + for issue in issue_data: + title = issue['title'].replace('"', '""') + print(f'{issue["number"]},"{title}",{issue["priority"]},{issue["created"]},{issue["updated"]}') + elif format_type == "fields": + # Space-separated fields for easy awk processing + if include_state: + print("NUMBER TITLE PRIORITY STATE CREATED UPDATED") + for issue in issue_data: + title = issue['title'].replace(' ', '_') + print(f'{issue["number"]} {title} {issue["priority"]} {issue["state"]} {issue["created"]} {issue["updated"]}') + else: + print("NUMBER TITLE PRIORITY CREATED UPDATED") + for issue in issue_data: + title = issue['title'].replace(' ', '_') + print(f'{issue["number"]} {title} {issue["priority"]} {issue["created"]} {issue["updated"]}') + else: # Default TSV + if include_state: + for issue in issue_data: + print(f'{issue["number"]}\t{issue["title"]}\t{issue["priority"]}\t{issue["state"]}\t{issue["created"]}\t{issue["updated"]}') + else: + for issue in issue_data: + print(f'{issue["number"]}\t{issue["title"]}\t{issue["priority"]}\t{issue["created"]}\t{issue["updated"]}') + + except TddaiError as e: + print(f"❌ Error: {e}", file=sys.stderr) + sys.exit(1) + + def project_overview(): """Show project management overview.""" try: @@ -718,6 +835,18 @@ def main(): subparsers.add_parser('list-issues', help='List all issues') subparsers.add_parser('list-open-issues', help='List open issues') + index_parser = subparsers.add_parser('issue-index', help='Output compact issue index for Unix processing') + index_parser.add_argument('--format', choices=['tsv', 'csv', 'json', 'fields'], default='tsv', + help='Output format (default: tsv)') + index_parser.add_argument('--sort', choices=['number', 'title', 'priority', 'state', 'created', 'updated'], + default='number', help='Sort by field (default: number)') + index_parser.add_argument('--filter-state', choices=['open', 'closed'], + help='Filter by issue state') + index_parser.add_argument('--filter-priority', choices=['low', 'medium', 'high', 'critical', 'none'], + help='Filter by priority level') + index_parser.add_argument('--include-state', action='store_true', + help='Include state column in output') + show_parser = subparsers.add_parser('show-issue', help='Show issue details') show_parser.add_argument('issue_number', type=int, help='Issue number') @@ -783,6 +912,14 @@ def main(): list_issues() elif args.command == 'list-open-issues': list_open_issues() + elif args.command == 'issue-index': + issue_index( + format_type=args.format, + sort_by=args.sort, + filter_state=args.filter_state, + filter_priority=args.filter_priority, + include_state=args.include_state + ) elif args.command == 'show-issue': show_issue(args.issue_number) elif args.command == 'analyze-coverage':