Initial commit
This commit is contained in:
106
skills/project-status-report/SKILL.md
Normal file
106
skills/project-status-report/SKILL.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
name: project-status-report
|
||||
description: Generate comprehensive project health and status reports for rapid developer onboarding. Use when starting sessions, checking project health mid-work, or needing overview of git status, open work items, and suggested next actions.
|
||||
---
|
||||
|
||||
# Project Status Report
|
||||
|
||||
Generate comprehensive project health and status reports for rapid developer onboarding.
|
||||
|
||||
## When to Use
|
||||
|
||||
- **Session start**: Get full project context before deciding what to work on
|
||||
- **Mid-session check**: Quick health check without session overhead
|
||||
- **Context switching**: Rapid re-immersion after days away from project
|
||||
- **Before major changes**: Understand current state before refactoring
|
||||
|
||||
## What It Reports
|
||||
|
||||
### Priority 1: Health Indicators 🏥
|
||||
- Test status (passing/failing)
|
||||
- Linting errors
|
||||
- Coverage metrics
|
||||
- Build status
|
||||
- Context health (from claude-context-manager if available)
|
||||
|
||||
### Priority 2: Git Status 📍
|
||||
- Current branch
|
||||
- Uncommitted changes
|
||||
- Sync status with remote
|
||||
- Active branches (recent activity)
|
||||
|
||||
### Priority 3: Recent Session 📖
|
||||
- Last checkpoint summary
|
||||
- What was accomplished
|
||||
- Where you left off
|
||||
|
||||
### Priority 4: Open Work Items 📋
|
||||
- Session objectives
|
||||
- TODOs in code
|
||||
- FIXMEs in code
|
||||
|
||||
### Priority 5: Backlog 📚
|
||||
- Planned features (if configured)
|
||||
- Technical debt items
|
||||
|
||||
### Priority 6: AI Suggestions 💡
|
||||
- Recommended next actions based on project state
|
||||
- Effort estimates
|
||||
- Priority guidance
|
||||
|
||||
## Usage
|
||||
|
||||
### Standalone
|
||||
|
||||
```bash
|
||||
python scripts/report.py
|
||||
```
|
||||
|
||||
### From Claude Code
|
||||
|
||||
```
|
||||
/project-report
|
||||
```
|
||||
|
||||
### Programmatic
|
||||
|
||||
```python
|
||||
from report import ReportGenerator
|
||||
|
||||
generator = ReportGenerator()
|
||||
report = generator.generate()
|
||||
print(report)
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
Markdown report with sections in priority order. Designed for quick scanning with emojis and clear hierarchy.
|
||||
|
||||
## Integration
|
||||
|
||||
**Used by session-management**: Automatically invoked during `/session-start` to provide onboarding context.
|
||||
|
||||
**Standalone utility**: Can be run independently without session management.
|
||||
|
||||
## Configuration
|
||||
|
||||
No configuration required. Automatically detects:
|
||||
- Git repository
|
||||
- Test frameworks (pytest)
|
||||
- Session state (`.sessions/` directory)
|
||||
- CCMP plugin state (`.ccmp/state.json`)
|
||||
|
||||
## Best Practices
|
||||
|
||||
**Quick check**: Run `/project-report` anytime you need project overview
|
||||
|
||||
**Before work**: Check health indicators before starting new work
|
||||
|
||||
**After context switch**: First command after returning to project
|
||||
|
||||
**Share with team**: Generate report for handoffs or status updates
|
||||
|
||||
## See Also
|
||||
|
||||
- **session-management**: Uses this skill for session start onboarding
|
||||
- **claude-context-manager**: Provides context health metrics
|
||||
1
skills/project-status-report/scripts/__init__.py
Executable file
1
skills/project-status-report/scripts/__init__.py
Executable file
@@ -0,0 +1 @@
|
||||
# Empty file to make this directory a Python package
|
||||
7
skills/project-status-report/scripts/conftest.py
Executable file
7
skills/project-status-report/scripts/conftest.py
Executable file
@@ -0,0 +1,7 @@
|
||||
"""Pytest configuration for project-status-report tests."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add the scripts directory to Python path so tests can import modules
|
||||
scripts_dir = Path(__file__).parent
|
||||
sys.path.insert(0, str(scripts_dir))
|
||||
156
skills/project-status-report/scripts/git_analysis.py
Executable file
156
skills/project-status-report/scripts/git_analysis.py
Executable file
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Git Analysis Module
|
||||
|
||||
Analyzes git repository state for project status reports.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
class GitAnalyzer:
|
||||
"""Analyze git repository state"""
|
||||
|
||||
def __init__(self, repo_path: str = "."):
|
||||
"""Initialize analyzer with repository path"""
|
||||
self.repo_path = Path(repo_path)
|
||||
|
||||
def _run_git(self, args: List[str]) -> Optional[str]:
|
||||
"""Run git command and return output"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git"] + args,
|
||||
cwd=self.repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
def get_current_branch(self) -> Optional[str]:
|
||||
"""Get current git branch name"""
|
||||
return self._run_git(["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
|
||||
def get_uncommitted_changes(self) -> Dict[str, List[str]]:
|
||||
"""Get uncommitted and untracked files"""
|
||||
# Get staged files
|
||||
staged_output = self._run_git(["diff", "--cached", "--name-only"])
|
||||
staged = staged_output.split("\n") if staged_output else []
|
||||
staged = [f for f in staged if f] # Filter empty strings
|
||||
|
||||
# Get modified files
|
||||
modified_output = self._run_git(["diff", "--name-only"])
|
||||
modified = modified_output.split("\n") if modified_output else []
|
||||
modified = [f for f in modified if f] # Filter empty strings
|
||||
|
||||
# Get untracked files
|
||||
untracked_output = self._run_git(["ls-files", "--others", "--exclude-standard"])
|
||||
untracked = untracked_output.split("\n") if untracked_output else []
|
||||
untracked = [f for f in untracked if f]
|
||||
|
||||
return {
|
||||
"staged": staged,
|
||||
"modified": modified,
|
||||
"untracked": untracked
|
||||
}
|
||||
|
||||
def get_active_branches(self, limit: int = 10) -> List[Dict[str, str]]:
|
||||
"""Get branches sorted by recent activity"""
|
||||
# Get all branches with last commit info
|
||||
output = self._run_git([
|
||||
"for-each-ref",
|
||||
"--sort=-committerdate",
|
||||
"--format=%(refname:short)|%(committerdate:relative)|%(subject)",
|
||||
"refs/heads/",
|
||||
f"--count={limit}"
|
||||
])
|
||||
|
||||
if not output:
|
||||
return []
|
||||
|
||||
branches = []
|
||||
for line in output.split("\n"):
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split("|", 2)
|
||||
if len(parts) == 3:
|
||||
branches.append({
|
||||
"name": parts[0],
|
||||
"last_activity": parts[1],
|
||||
"last_commit": parts[2]
|
||||
})
|
||||
|
||||
return branches
|
||||
|
||||
def get_remote_sync_status(self) -> Dict[str, Any]:
|
||||
"""Get sync status with remote"""
|
||||
current_branch = self.get_current_branch()
|
||||
if not current_branch:
|
||||
return {"error": "Not in a git repository"}
|
||||
|
||||
# Get ahead/behind count
|
||||
upstream = self._run_git(["rev-parse", "--abbrev-ref", f"{current_branch}@{{upstream}}"])
|
||||
|
||||
if not upstream:
|
||||
return {"status": "no_upstream"}
|
||||
|
||||
# Get ahead/behind counts
|
||||
ahead_behind = self._run_git(["rev-list", "--left-right", "--count", f"{upstream}...HEAD"])
|
||||
|
||||
if ahead_behind:
|
||||
parts = ahead_behind.split("\t")
|
||||
behind = int(parts[0])
|
||||
ahead = int(parts[1])
|
||||
|
||||
return {
|
||||
"upstream": upstream,
|
||||
"ahead": ahead,
|
||||
"behind": behind
|
||||
}
|
||||
|
||||
return {"status": "unknown"}
|
||||
|
||||
def generate_report(self) -> str:
|
||||
"""Generate git status section of report"""
|
||||
current_branch = self.get_current_branch()
|
||||
changes = self.get_uncommitted_changes()
|
||||
sync_status = self.get_remote_sync_status()
|
||||
active_branches = self.get_active_branches(limit=5)
|
||||
|
||||
lines = []
|
||||
lines.append("## 📍 Git Status")
|
||||
lines.append("")
|
||||
lines.append(f"**Current Branch**: {current_branch or 'Unknown'}")
|
||||
|
||||
# Sync status
|
||||
if "upstream" in sync_status:
|
||||
ahead = sync_status["ahead"]
|
||||
behind = sync_status["behind"]
|
||||
if ahead > 0 and behind > 0:
|
||||
lines.append(f"**Status**: {ahead} commits ahead, {behind} commits behind {sync_status['upstream']}")
|
||||
elif ahead > 0:
|
||||
lines.append(f"**Status**: {ahead} commits ahead of {sync_status['upstream']}")
|
||||
elif behind > 0:
|
||||
lines.append(f"**Status**: {behind} commits behind {sync_status['upstream']}")
|
||||
else:
|
||||
lines.append(f"**Status**: Up to date with {sync_status['upstream']}")
|
||||
|
||||
# Uncommitted changes
|
||||
if changes["staged"]:
|
||||
lines.append(f"**Staged**: {len(changes['staged'])} files")
|
||||
if changes["modified"]:
|
||||
lines.append(f"**Uncommitted**: {len(changes['modified'])} files modified")
|
||||
if changes["untracked"]:
|
||||
lines.append(f"**Untracked**: {len(changes['untracked'])} files")
|
||||
|
||||
lines.append("")
|
||||
lines.append("**Active Branches** (recent):")
|
||||
for branch in active_branches:
|
||||
marker = "(current)" if branch["name"] == current_branch else ""
|
||||
lines.append(f"- {branch['name']} {marker} (last commit: {branch['last_activity']})")
|
||||
|
||||
return "\n".join(lines)
|
||||
116
skills/project-status-report/scripts/health_check.py
Executable file
116
skills/project-status-report/scripts/health_check.py
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Health Check Module
|
||||
|
||||
Checks project health: tests, linting, coverage, build status.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
class HealthChecker:
|
||||
"""Check project health indicators"""
|
||||
|
||||
def __init__(self, project_path: str = "."):
|
||||
"""Initialize checker with project path"""
|
||||
self.project_path = Path(project_path)
|
||||
|
||||
def _run_command(self, cmd: List[str]) -> Optional[subprocess.CompletedProcess]:
|
||||
"""Run command and return result"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=self.project_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
return result
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return None
|
||||
|
||||
def check_tests(self) -> Dict[str, Any]:
|
||||
"""Check test status (pytest)"""
|
||||
result = self._run_command(["pytest", "--collect-only", "-q"])
|
||||
|
||||
if not result:
|
||||
return {"status": "unknown", "reason": "pytest not found"}
|
||||
|
||||
# Check if pytest runs successfully
|
||||
if result.returncode == 0:
|
||||
# Parse output for test count
|
||||
lines = result.stdout.split("\n")
|
||||
for line in lines:
|
||||
if "test" in line.lower():
|
||||
return {"status": "pass", "message": line.strip()}
|
||||
return {"status": "pass"}
|
||||
else:
|
||||
return {"status": "fail", "message": result.stderr[:200]}
|
||||
|
||||
def check_ccmp_context_health(self) -> Optional[Dict[str, Any]]:
|
||||
"""Check context health from .ccmp/state.json"""
|
||||
state_file = self.project_path / ".ccmp" / "state.json"
|
||||
|
||||
if not state_file.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(state_file) as f:
|
||||
state = json.load(f)
|
||||
|
||||
context_state = state.get("claude-context-manager", {})
|
||||
if context_state:
|
||||
return {
|
||||
"health_score": context_state.get("health_score"),
|
||||
"critical_files": context_state.get("critical_files", [])
|
||||
}
|
||||
except (json.JSONDecodeError, IOError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def generate_report(self) -> str:
|
||||
"""Generate health indicators section"""
|
||||
lines = []
|
||||
lines.append("## 🏥 Health Indicators")
|
||||
lines.append("")
|
||||
|
||||
# Test status
|
||||
test_result = self.check_tests()
|
||||
if test_result["status"] == "pass":
|
||||
lines.append(f"✅ Tests: {test_result.get('message', 'Passing')}")
|
||||
elif test_result["status"] == "fail":
|
||||
lines.append(f"❌ Tests: {test_result.get('message', 'Failing')}")
|
||||
else:
|
||||
lines.append("⚠️ Tests: Status unknown")
|
||||
|
||||
# Context health (if available)
|
||||
context_health = self.check_ccmp_context_health()
|
||||
if context_health:
|
||||
score = context_health.get("health_score")
|
||||
critical = context_health.get("critical_files", [])
|
||||
if score is not None:
|
||||
if score >= 80:
|
||||
lines.append(f"✅ Context Health: {score}/100")
|
||||
elif score >= 60:
|
||||
lines.append(f"⚠️ Context Health: {score}/100")
|
||||
else:
|
||||
lines.append(f"❌ Context Health: {score}/100")
|
||||
|
||||
if critical:
|
||||
lines.append(f"⚠️ Context: {len(critical)} files need attention")
|
||||
|
||||
# Summary
|
||||
lines.append("")
|
||||
critical_issues = [line for line in lines if "❌" in line]
|
||||
warnings = [line for line in lines if "⚠️" in line]
|
||||
|
||||
if critical_issues:
|
||||
lines.append(f"**Critical Issues**: {len(critical_issues)}")
|
||||
if warnings:
|
||||
lines.append(f"**Warnings**: {len(warnings)}")
|
||||
|
||||
return "\n".join(lines)
|
||||
115
skills/project-status-report/scripts/report.py
Executable file
115
skills/project-status-report/scripts/report.py
Executable file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Project Status Report Generator
|
||||
|
||||
Main CLI for generating comprehensive project status reports.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from health_check import HealthChecker
|
||||
from git_analysis import GitAnalyzer
|
||||
from work_items import WorkItemsScanner
|
||||
|
||||
|
||||
class ReportGenerator:
|
||||
"""Generate comprehensive project status reports"""
|
||||
|
||||
def __init__(self, project_path: str = "."):
|
||||
"""Initialize generator with project path"""
|
||||
self.project_path = Path(project_path)
|
||||
self.health_checker = HealthChecker(project_path)
|
||||
self.git_analyzer = GitAnalyzer(project_path)
|
||||
self.work_scanner = WorkItemsScanner(project_path)
|
||||
|
||||
def load_recent_session(self) -> str:
|
||||
"""Load recent session summary from checkpoints"""
|
||||
checkpoints_dir = self.project_path / ".sessions" / "checkpoints"
|
||||
|
||||
if not checkpoints_dir.exists():
|
||||
return "**No previous sessions found**"
|
||||
|
||||
# Get most recent checkpoint
|
||||
checkpoints = sorted(checkpoints_dir.glob("*.md"), reverse=True)
|
||||
if not checkpoints:
|
||||
return "**No checkpoints found**"
|
||||
|
||||
latest = checkpoints[0]
|
||||
|
||||
# Read first few lines for summary
|
||||
try:
|
||||
with open(latest) as f:
|
||||
lines = f.readlines()[:15]
|
||||
|
||||
summary = f"**Last Checkpoint**: {latest.stem}\n\n"
|
||||
summary += "".join(lines)
|
||||
return summary
|
||||
except IOError:
|
||||
return "**Error reading checkpoint**"
|
||||
|
||||
def generate(self) -> str:
|
||||
"""Generate complete project status report"""
|
||||
lines = []
|
||||
lines.append("# Project Status Report")
|
||||
lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
lines.append("")
|
||||
|
||||
# Priority 1: Health Indicators
|
||||
lines.append(self.health_checker.generate_report())
|
||||
lines.append("")
|
||||
|
||||
# Priority 2: Git Status
|
||||
lines.append(self.git_analyzer.generate_report())
|
||||
lines.append("")
|
||||
|
||||
# Priority 3: Recent Session
|
||||
lines.append("## 📖 Recent Session")
|
||||
lines.append("")
|
||||
lines.append(self.load_recent_session())
|
||||
lines.append("")
|
||||
|
||||
# Priority 4: Open Work Items
|
||||
lines.append(self.work_scanner.generate_report())
|
||||
lines.append("")
|
||||
|
||||
# Priority 5: Backlog (placeholder)
|
||||
lines.append("## 📚 Backlog")
|
||||
lines.append("")
|
||||
lines.append("*No backlog configured*")
|
||||
lines.append("")
|
||||
|
||||
# Priority 6: AI Suggestions (placeholder)
|
||||
lines.append("## 💡 Suggested Next Actions")
|
||||
lines.append("")
|
||||
lines.append("*AI suggestions will be generated based on above analysis*")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entry point"""
|
||||
parser = argparse.ArgumentParser(description="Generate project status report")
|
||||
parser.add_argument("--path", default=".", help="Project path (default: current directory)")
|
||||
parser.add_argument("--output", help="Output file (default: stdout)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
generator = ReportGenerator(args.path)
|
||||
report = generator.generate()
|
||||
|
||||
if args.output:
|
||||
with open(args.output, 'w') as f:
|
||||
f.write(report)
|
||||
print(f"Report saved to {args.output}")
|
||||
else:
|
||||
print(report)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
33
skills/project-status-report/scripts/test_git_analysis.py
Executable file
33
skills/project-status-report/scripts/test_git_analysis.py
Executable file
@@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
from git_analysis import GitAnalyzer
|
||||
|
||||
def test_get_current_branch():
|
||||
"""Test that we can get current git branch"""
|
||||
analyzer = GitAnalyzer()
|
||||
branch = analyzer.get_current_branch()
|
||||
assert branch is not None
|
||||
assert isinstance(branch, str)
|
||||
assert len(branch) > 0
|
||||
|
||||
def test_get_uncommitted_changes():
|
||||
"""Test detection of uncommitted changes"""
|
||||
analyzer = GitAnalyzer()
|
||||
changes = analyzer.get_uncommitted_changes()
|
||||
assert isinstance(changes, dict)
|
||||
assert "staged" in changes
|
||||
assert "modified" in changes
|
||||
assert "untracked" in changes
|
||||
assert isinstance(changes["staged"], list)
|
||||
assert isinstance(changes["modified"], list)
|
||||
assert isinstance(changes["untracked"], list)
|
||||
|
||||
def test_get_active_branches():
|
||||
"""Test listing active branches with recent activity"""
|
||||
analyzer = GitAnalyzer()
|
||||
branches = analyzer.get_active_branches(limit=5)
|
||||
assert isinstance(branches, list)
|
||||
assert len(branches) <= 5
|
||||
for branch in branches:
|
||||
assert "name" in branch
|
||||
assert "last_commit" in branch
|
||||
assert "last_activity" in branch
|
||||
16
skills/project-status-report/scripts/test_health_check.py
Executable file
16
skills/project-status-report/scripts/test_health_check.py
Executable file
@@ -0,0 +1,16 @@
|
||||
import pytest
|
||||
from health_check import HealthChecker
|
||||
|
||||
def test_check_tests_basic():
|
||||
"""Test that we can check test status"""
|
||||
checker = HealthChecker()
|
||||
result = checker.check_tests()
|
||||
assert "status" in result
|
||||
assert result["status"] in ["pass", "fail", "unknown"]
|
||||
|
||||
def test_generate_report():
|
||||
"""Test health report generation"""
|
||||
checker = HealthChecker()
|
||||
report = checker.generate_report()
|
||||
assert isinstance(report, str)
|
||||
assert "Health Indicators" in report
|
||||
13
skills/project-status-report/scripts/test_report.py
Executable file
13
skills/project-status-report/scripts/test_report.py
Executable file
@@ -0,0 +1,13 @@
|
||||
import pytest
|
||||
from report import ReportGenerator
|
||||
|
||||
def test_generate_full_report():
|
||||
"""Test generating complete project status report"""
|
||||
generator = ReportGenerator()
|
||||
report = generator.generate()
|
||||
|
||||
assert isinstance(report, str)
|
||||
assert "Project Status Report" in report
|
||||
assert "Health Indicators" in report
|
||||
assert "Git Status" in report
|
||||
assert "Open Work Items" in report
|
||||
18
skills/project-status-report/scripts/test_work_items.py
Executable file
18
skills/project-status-report/scripts/test_work_items.py
Executable file
@@ -0,0 +1,18 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from work_items import WorkItemsScanner
|
||||
|
||||
def test_scan_code_markers():
|
||||
"""Test scanning code for TODO/FIXME markers"""
|
||||
scanner = WorkItemsScanner()
|
||||
markers = scanner.scan_code_markers()
|
||||
assert isinstance(markers, dict)
|
||||
assert "todos" in markers
|
||||
assert "fixmes" in markers
|
||||
|
||||
def test_load_session_objectives():
|
||||
"""Test loading objectives from session state"""
|
||||
scanner = WorkItemsScanner()
|
||||
objectives = scanner.load_session_objectives()
|
||||
# Should return list even if file doesn't exist
|
||||
assert isinstance(objectives, list)
|
||||
122
skills/project-status-report/scripts/work_items.py
Executable file
122
skills/project-status-report/scripts/work_items.py
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Work Items Scanner
|
||||
|
||||
Scans code for TODOs, FIXMEs, and loads session objectives.
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class WorkItemsScanner:
|
||||
"""Scan project for work items"""
|
||||
|
||||
def __init__(self, project_path: str = "."):
|
||||
"""Initialize scanner with project path"""
|
||||
self.project_path = Path(project_path)
|
||||
|
||||
def scan_code_markers(self, patterns: List[str] = None) -> Dict[str, List[Dict]]:
|
||||
"""Scan code files for TODO/FIXME markers"""
|
||||
if patterns is None:
|
||||
patterns = ["*.py", "*.js", "*.ts", "*.tsx", "*.java", "*.go", "*.rs"]
|
||||
|
||||
todos = []
|
||||
fixmes = []
|
||||
|
||||
for pattern in patterns:
|
||||
for file_path in self.project_path.rglob(pattern):
|
||||
# Skip test files and node_modules
|
||||
if "test" in str(file_path) or "node_modules" in str(file_path):
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for line_num, line in enumerate(f, 1):
|
||||
# Match TODO: or TODO -
|
||||
if re.search(r'TODO[:\-]', line, re.IGNORECASE):
|
||||
comment = line.strip()
|
||||
todos.append({
|
||||
"file": str(file_path.relative_to(self.project_path)),
|
||||
"line": line_num,
|
||||
"text": comment
|
||||
})
|
||||
# Match FIXME: or FIXME -
|
||||
if re.search(r'FIXME[:\-]', line, re.IGNORECASE):
|
||||
comment = line.strip()
|
||||
fixmes.append({
|
||||
"file": str(file_path.relative_to(self.project_path)),
|
||||
"line": line_num,
|
||||
"text": comment
|
||||
})
|
||||
except (IOError, UnicodeDecodeError):
|
||||
# Skip files we can't read
|
||||
continue
|
||||
|
||||
return {
|
||||
"todos": todos[:20], # Limit to first 20
|
||||
"fixmes": fixmes[:20]
|
||||
}
|
||||
|
||||
def load_session_objectives(self) -> List[Dict]:
|
||||
"""Load objectives from .sessions/state.json"""
|
||||
state_file = self.project_path / ".sessions" / "state.json"
|
||||
|
||||
if not state_file.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(state_file) as f:
|
||||
state = json.load(f)
|
||||
|
||||
objectives = state.get("objectives", [])
|
||||
# Convert to list of dicts if it's a simple list
|
||||
if objectives and isinstance(objectives[0], str):
|
||||
return [{"text": obj, "completed": False} for obj in objectives]
|
||||
|
||||
return objectives
|
||||
except (json.JSONDecodeError, IOError, KeyError):
|
||||
return []
|
||||
|
||||
def generate_report(self) -> str:
|
||||
"""Generate work items section"""
|
||||
lines = []
|
||||
lines.append("## 📋 Open Work Items")
|
||||
lines.append("")
|
||||
|
||||
# Session objectives
|
||||
objectives = self.load_session_objectives()
|
||||
if objectives:
|
||||
lines.append("**Session Objectives**:")
|
||||
for obj in objectives:
|
||||
status = "[x]" if obj.get("completed") else "[ ]"
|
||||
text = obj.get("text", "Unknown objective")
|
||||
lines.append(f"- {status} {text}")
|
||||
lines.append("")
|
||||
|
||||
# Code markers
|
||||
markers = self.scan_code_markers()
|
||||
todos = markers["todos"]
|
||||
fixmes = markers["fixmes"]
|
||||
|
||||
if todos:
|
||||
lines.append("**TODOs in Code**:")
|
||||
for todo in todos[:5]: # Show first 5
|
||||
lines.append(f"- {todo['file']}:{todo['line']} - {todo['text'][:80]}")
|
||||
if len(todos) > 5:
|
||||
lines.append(f"- ... and {len(todos) - 5} more")
|
||||
lines.append("")
|
||||
|
||||
if fixmes:
|
||||
lines.append("**FIXMEs in Code**:")
|
||||
for fixme in fixmes[:5]:
|
||||
lines.append(f"- {fixme['file']}:{fixme['line']} - {fixme['text'][:80]}")
|
||||
if len(fixmes) > 5:
|
||||
lines.append(f"- ... and {len(fixmes) - 5} more")
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"**Summary**: {len(todos)} TODOs, {len(fixmes)} FIXMEs")
|
||||
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user