Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:55:16 +08:00
commit a81fb4a29a
15 changed files with 824 additions and 0 deletions

View File

@@ -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"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# project-status-report
Generate comprehensive project health and status reports for rapid developer onboarding

View File

@@ -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.

89
plugin.lock.json Normal file
View File

@@ -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": []
}
}

View 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

View File

@@ -0,0 +1 @@
# Empty file to make this directory a Python package

View 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))

View 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)

View 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)

View 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())

View 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

View 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

View 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

View 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)

View 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)