Initial commit
This commit is contained in:
15
.claude-plugin/plugin.json
Normal file
15
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# project-status-report
|
||||||
|
|
||||||
|
Generate comprehensive project health and status reports for rapid developer onboarding
|
||||||
14
commands/project-report.md
Normal file
14
commands/project-report.md
Normal 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
89
plugin.lock.json
Normal 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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
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