From a81fb4a29aa0894b6825e83a4049b7f4f81cd122 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 17:55:16 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 15 ++ README.md | 3 + commands/project-report.md | 14 ++ plugin.lock.json | 89 ++++++++++ skills/project-status-report/SKILL.md | 106 ++++++++++++ .../project-status-report/scripts/__init__.py | 1 + .../project-status-report/scripts/conftest.py | 7 + .../scripts/git_analysis.py | 156 ++++++++++++++++++ .../scripts/health_check.py | 116 +++++++++++++ .../project-status-report/scripts/report.py | 115 +++++++++++++ .../scripts/test_git_analysis.py | 33 ++++ .../scripts/test_health_check.py | 16 ++ .../scripts/test_report.py | 13 ++ .../scripts/test_work_items.py | 18 ++ .../scripts/work_items.py | 122 ++++++++++++++ 15 files changed, 824 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 commands/project-report.md create mode 100644 plugin.lock.json create mode 100644 skills/project-status-report/SKILL.md create mode 100755 skills/project-status-report/scripts/__init__.py create mode 100755 skills/project-status-report/scripts/conftest.py create mode 100755 skills/project-status-report/scripts/git_analysis.py create mode 100755 skills/project-status-report/scripts/health_check.py create mode 100755 skills/project-status-report/scripts/report.py create mode 100755 skills/project-status-report/scripts/test_git_analysis.py create mode 100755 skills/project-status-report/scripts/test_health_check.py create mode 100755 skills/project-status-report/scripts/test_report.py create mode 100755 skills/project-status-report/scripts/test_work_items.py create mode 100755 skills/project-status-report/scripts/work_items.py diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..dc224ff --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "name": "project-status-report", + "description": "Generate comprehensive project health and status reports for rapid developer onboarding", + "version": "1.0.0", + "author": { + "name": "AnthemFlynn", + "email": "AnthemFlynn@users.noreply.github.com" + }, + "skills": [ + "./skills" + ], + "commands": [ + "./commands" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9365af4 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# project-status-report + +Generate comprehensive project health and status reports for rapid developer onboarding diff --git a/commands/project-report.md b/commands/project-report.md new file mode 100644 index 0000000..42529d7 --- /dev/null +++ b/commands/project-report.md @@ -0,0 +1,14 @@ +--- +description: Generate comprehensive project status and health report +--- + +Execute the project-status-report skill to generate a comprehensive health report. + +Run the report generator and display the full project status including: +- Health indicators (tests, linting, coverage) +- Git status (branch, commits, sync) +- Recent session summary +- Open work items (objectives, TODOs, FIXMEs) +- Suggested next actions + +Use this command anytime you need project overview or are starting work after context switch. diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..8f31ff1 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,89 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:AnthemFlynn/ccmp:plugins/project-status-report", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "c23703b8f027b6cab099c314d723d8facc66f1f0", + "treeHash": "9ab8b5c057db8f2ea18185c9ba12449fd7dea06bc0e40406cbfd0a5c3d6fe5c6", + "generatedAt": "2025-11-28T10:24:52.389982Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "project-status-report", + "description": "Generate comprehensive project health and status reports for rapid developer onboarding", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "a9abddba4fce303fcd41d88af399dbe6d31d329068ed96bea859b52976fe9cc9" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "6fd7322ea4bd2ca7b914c1972557add0877d0d23991b3931d96bbcc95185b59e" + }, + { + "path": "commands/project-report.md", + "sha256": "59722fa6cdddd4f1cf0a937b247632c1e95b64f9a50a0768258c2c5f825b7d19" + }, + { + "path": "skills/project-status-report/SKILL.md", + "sha256": "70c2e74d32cd20060cfff378278b7266e9b172256540458613b73bc3f81e5283" + }, + { + "path": "skills/project-status-report/scripts/work_items.py", + "sha256": "06a778781c59147055c96ae20bf7a1f59ae0414b8a71f3130b0f5b89cc3d8977" + }, + { + "path": "skills/project-status-report/scripts/conftest.py", + "sha256": "065926bf9d33df613c4c0610cc1616338959f13bc462b490d55945014d807bca" + }, + { + "path": "skills/project-status-report/scripts/test_git_analysis.py", + "sha256": "a3c42caaa00a329902c106d537c385342491250764a0369d4411d9cb6f24b175" + }, + { + "path": "skills/project-status-report/scripts/test_health_check.py", + "sha256": "bd2341c62a6573b3fd89897122865d3b1d350f6f7c4b407ba4870495f799de96" + }, + { + "path": "skills/project-status-report/scripts/__init__.py", + "sha256": "d4729ac998b91c9e1384a07463fbfc06ebae2af5d524c529c5880b50c478f939" + }, + { + "path": "skills/project-status-report/scripts/test_report.py", + "sha256": "6f7276f2147dd037f22c048e9633346116d6f4cb1d6f43850b84ebd475ec4fdb" + }, + { + "path": "skills/project-status-report/scripts/health_check.py", + "sha256": "24c60c37a5c91e1bc7af326f6bd1c84528c63a14c32eae176a0d760990108b0c" + }, + { + "path": "skills/project-status-report/scripts/test_work_items.py", + "sha256": "db70b89130cd1a6b5ebb1625d2018411274bfc572028eb792e5ef05270c0f3b6" + }, + { + "path": "skills/project-status-report/scripts/git_analysis.py", + "sha256": "640216f49442b121717e97c8470b93bac2bcfc974f933ccf71835068a639cf25" + }, + { + "path": "skills/project-status-report/scripts/report.py", + "sha256": "62232753ecc3c8e4fbd8285c532fb32a9799e619f811797e6d79115b0cd70fd6" + } + ], + "dirSha256": "9ab8b5c057db8f2ea18185c9ba12449fd7dea06bc0e40406cbfd0a5c3d6fe5c6" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/project-status-report/SKILL.md b/skills/project-status-report/SKILL.md new file mode 100644 index 0000000..496f899 --- /dev/null +++ b/skills/project-status-report/SKILL.md @@ -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 diff --git a/skills/project-status-report/scripts/__init__.py b/skills/project-status-report/scripts/__init__.py new file mode 100755 index 0000000..0539e1f --- /dev/null +++ b/skills/project-status-report/scripts/__init__.py @@ -0,0 +1 @@ +# Empty file to make this directory a Python package diff --git a/skills/project-status-report/scripts/conftest.py b/skills/project-status-report/scripts/conftest.py new file mode 100755 index 0000000..d4bf834 --- /dev/null +++ b/skills/project-status-report/scripts/conftest.py @@ -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)) diff --git a/skills/project-status-report/scripts/git_analysis.py b/skills/project-status-report/scripts/git_analysis.py new file mode 100755 index 0000000..c288384 --- /dev/null +++ b/skills/project-status-report/scripts/git_analysis.py @@ -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) diff --git a/skills/project-status-report/scripts/health_check.py b/skills/project-status-report/scripts/health_check.py new file mode 100755 index 0000000..7717eea --- /dev/null +++ b/skills/project-status-report/scripts/health_check.py @@ -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) diff --git a/skills/project-status-report/scripts/report.py b/skills/project-status-report/scripts/report.py new file mode 100755 index 0000000..374b2cf --- /dev/null +++ b/skills/project-status-report/scripts/report.py @@ -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()) diff --git a/skills/project-status-report/scripts/test_git_analysis.py b/skills/project-status-report/scripts/test_git_analysis.py new file mode 100755 index 0000000..3d34f3b --- /dev/null +++ b/skills/project-status-report/scripts/test_git_analysis.py @@ -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 diff --git a/skills/project-status-report/scripts/test_health_check.py b/skills/project-status-report/scripts/test_health_check.py new file mode 100755 index 0000000..a57e644 --- /dev/null +++ b/skills/project-status-report/scripts/test_health_check.py @@ -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 diff --git a/skills/project-status-report/scripts/test_report.py b/skills/project-status-report/scripts/test_report.py new file mode 100755 index 0000000..7d4d15f --- /dev/null +++ b/skills/project-status-report/scripts/test_report.py @@ -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 diff --git a/skills/project-status-report/scripts/test_work_items.py b/skills/project-status-report/scripts/test_work_items.py new file mode 100755 index 0000000..5b72e35 --- /dev/null +++ b/skills/project-status-report/scripts/test_work_items.py @@ -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) diff --git a/skills/project-status-report/scripts/work_items.py b/skills/project-status-report/scripts/work_items.py new file mode 100755 index 0000000..2263d30 --- /dev/null +++ b/skills/project-status-report/scripts/work_items.py @@ -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)