Initial commit: ProjectTracker infrastructure

Multi-repository state tracker and coordinator for managing 18+ Git projects.

Core Components:
- tools/scan.sh: Bash script to scan all repos, pull updates, detect conflicts
- tools/analyze.py: Python generator for human-readable summaries
- tools/config.json: Repository tracking configuration
- README.md: Complete documentation

Features:
- State tracking (uncommitted changes, new commits, conflicts)
- Activity monitoring (3-week lookback window)
- Automatic pulling with conflict detection
- Categorization (META, CONSTANT, WIP, CONCEPT, PAUSE, DONE)
- Summary generation with prioritization

analyze.py created in Session 7 - Cycle 2 (ClaudeSelf external exploration).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
StillHammer 2026-01-19 01:14:28 +07:00
commit 13234b0e8b
6 changed files with 726 additions and 0 deletions

16
.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
# Environment variables (NEVER commit tokens/secrets)
.env
# Planning outputs (generated files)
.planning/state.json
.planning/scan.log
.planning/conflicts.txt
.planning/summary.md
# System files
.DS_Store
Thumbs.db
# Temporary files
tmpclaude-*
*.tmp

196
README.md Normal file
View File

@ -0,0 +1,196 @@
# ProjectTracker
**Multi-repository state tracker and coordinator for managing multiple Git projects.**
ProjectTracker scans all tracked repositories, detects changes, pulls updates, identifies conflicts, and generates human-readable summaries of the entire ecosystem state.
## Purpose
Managing 15+ active Git repositories manually is tedious and error-prone. ProjectTracker automates:
- **State Tracking**: Which repos have new commits, uncommitted changes, conflicts
- **Activity Monitoring**: Which projects are active vs dormant
- **Conflict Detection**: Automatic merge conflict identification
- **Categorization**: Organize projects by status (WIP, CONSTANT, PAUSE, etc.)
- **Reporting**: Generate readable summaries for quick ecosystem overview
## Structure
```
ProjectTracker/
├── tools/
│ ├── config.json # Repository configuration and tracking settings
│ ├── scan.sh # Main scanner script (bash)
│ └── analyze.py # Summary generator (Python)
├── projects/
│ ├── META/ # Meta-projects (planning, infrastructure)
│ ├── CONSTANT/ # Ongoing/maintenance projects
│ ├── WIP/ # Active development projects
│ ├── CONCEPT/ # Early-stage ideas
│ ├── PAUSE/ # Paused projects
│ └── DONE/ # Completed projects
├── .planning/ # Generated state files (auto-created)
│ ├── state.json # Machine-readable repo state
│ ├── summary.md # Human-readable summary
│ ├── scan.log # Detailed scan logs
│ └── conflicts.txt # List of repos with conflicts
├── .env # Gitea credentials and configuration
└── README.md # This file
```
## Configuration
### 1. Environment Variables (`.env`)
```bash
GITEA_URL=https://git.etheryale.com
GITEA_TOKEN=your_token_here
```
**Note**: Use `wget` instead of `curl` for API calls due to proxy compatibility.
### 2. Repository Tracking (`tools/config.json`)
Define which repos to track, exclude, and categorize:
```json
{
"repos_root": "C:/Users/alexi/Documents/projects",
"tracked_repos": ["civjdr", "ChineseClass", "seogeneratorserver", ...],
"excluded_repos": ["couple-repo", "vba-mcp-demo", ...],
"project_mapping": {
"civjdr": "projects/CONSTANT/civjdr.md",
"ChineseClass": "projects/CONSTANT/ChineseClass.md",
...
}
}
```
**Scan Options**:
- `auto_pull`: Automatically pull updates (default: true)
- `commits_lookback_days`: Activity window for "recent" commits (default: 21)
- `detect_uncommitted_changes`: Check for unstaged/uncommitted files (default: true)
## Usage
### Full Scan and Summary
```bash
cd ProjectTracker
bash tools/scan.sh
```
This will:
1. Fetch updates from all tracked repos
2. Pull changes (if `auto_pull: true`)
3. Detect conflicts, uncommitted changes, ahead/behind status
4. Generate `.planning/state.json` with full repo state
5. Run `analyze.py` to generate `.planning/summary.md`
### Generate Summary Only
If you already have a `state.json` and want to regenerate the summary:
```bash
python tools/analyze.py
```
### View Summary
```bash
cat .planning/summary.md
```
## Output Files
### state.json (Machine-Readable)
JSON snapshot of all repos with detailed metrics:
```json
{
"last_scan": "2026-01-18T14:30:00+00:00",
"repos": {
"civjdr": {
"last_commit": "abc1234",
"branch": "main",
"commits_3w": 8,
"new_commits_since_last_scan": 2,
"has_uncommitted_changes": false,
"has_conflict": false,
"needs_attention": true,
...
}
},
"stats": {
"total_repos": 18,
"active_3w": 12,
"needs_attention": 5,
"conflicts": 1
}
}
```
### summary.md (Human-Readable)
Markdown report with:
- **Global Stats**: Total repos, active count, attention needed, conflicts
- **Needs Attention**: Prioritized list (conflicts → uncommitted → new commits → behind)
- **By Category**: Repos organized by status (META, CONSTANT, WIP, etc.)
- **Recent Activity**: Commit counts for active repos (last 3 weeks)
Example:
```markdown
## ⚠️ Needs Attention
- **seogeneratorserver** (develop) - ❌ CONFLICT | 📝 7 uncommitted | 🆕 5 new
Last: Update SEO templates (3 days ago)
- **ChineseClass** (main) - 📝 3 uncommitted | 🆕 2 new
Last: Add vocabulary list (2 days ago)
```
## Project Categorization
Projects are organized by development status:
- **META**: Infrastructure, planning, coordination projects
- **CONSTANT**: Ongoing/maintenance projects (no end date)
- **WIP**: Active development (work in progress)
- **CONCEPT**: Early-stage ideas, prototypes
- **PAUSE**: Temporarily paused projects
- **DONE**: Completed projects (archived)
Each project has a corresponding `.md` file in `projects/<CATEGORY>/` with:
- Description
- Current status
- Repository URL
- Next steps
- Notes
## Workflow
1. **Daily/Weekly**: Run `scan.sh` to check ecosystem state
2. **Review**: Check `summary.md` for repos needing attention
3. **Act**: Address conflicts, commit changes, pull updates
4. **Repeat**: Keep repos synchronized and tracked
## Requirements
- **Bash**: For `scan.sh` (Git Bash on Windows, native on Linux/Mac)
- **Python 3.7+**: For `analyze.py`
- **jq**: JSON processor for bash script parsing
- **Git**: Obviously :)
## Notes
- Scanner automatically fetches before pulling
- Conflicts are never auto-resolved (manual intervention required)
- `state.json` tracks previous scan state to detect "new" commits
- All timestamps use ISO 8601 format
- UTF-8 encoding support for emoji indicators on all platforms
---
**Created**: 2026-01-18 (Cycle 2 - External Exploration)
**Purpose**: Coordinate multi-project ecosystem efficiently
**Maintained**: Active

View File

@ -0,0 +1,30 @@
# WrittingFantasy
**Statut:** WIP (Work In Progress)
**Type:** Projet d'écriture fantasy
**Repository:** git@git.etheryale.com:StillHammer/WrittingFantasy.git
## Description
Projet d'écriture fantasy avec worldbuilding complet, personnages, et arcs narratifs.
## Structure
- `worldbuilding/` - Univers, géographie, magie, cultures
- `characters/` - Fiches personnages
- `plots/` - Intrigues et arcs narratifs
- `chapters/` - Chapitres et brouillons
- `notes/` - Notes et idées
## Prochaines Étapes
1. Créer le repository sur Gitea (git.etheryale.com)
2. Pusher le commit initial
3. Développer le worldbuilding de base
4. Créer les fiches des personnages principaux
5. Écrire les premiers chapitres
## Notes
- Repository créé localement le 2025-12-17
- En attente de création sur Gitea (serveur API injoignable temporairement)

207
tools/analyze.py Normal file
View File

@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""
ProjectTracker - Summary Generator
Reads state.json and generates human-readable summary.md
"""
import json
import sys
import os
from pathlib import Path
from typing import Dict, List, Tuple
# Force UTF-8 encoding for Windows console (emoji support)
os.environ['PYTHONIOENCODING'] = 'utf-8'
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8')
# Constants
SCRIPT_DIR = Path(__file__).parent
PROJECT_TRACKER = SCRIPT_DIR.parent
STATE_FILE = PROJECT_TRACKER / ".planning" / "state.json"
CONFIG_FILE = SCRIPT_DIR / "config.json"
SUMMARY_FILE = PROJECT_TRACKER / ".planning" / "summary.md"
# Categories order for display
CATEGORIES_ORDER = ["META", "CONSTANT", "WIP", "CONCEPT", "PAUSE", "DONE"]
def load_json(path: Path) -> dict:
"""Load JSON file"""
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def get_category(project_file: str) -> str:
"""Extract category from project file path
Example: projects/WIP/repo.md -> WIP
"""
if not project_file:
return "UNCATEGORIZED"
parts = project_file.split('/')
if len(parts) >= 2 and parts[0] == "projects":
return parts[1]
return "UNCATEGORIZED"
def format_repo_status(repo_name: str, data: Dict) -> str:
"""Format single repo status line with indicators"""
indicators = []
# Priority order: conflict > uncommitted > new commits > behind
if data.get("has_conflict"):
indicators.append("❌ CONFLICT")
if data.get("has_uncommitted_changes"):
count = data.get("uncommitted_count", 0)
indicators.append(f"📝 {count} uncommitted")
if data.get("new_commits_since_last_scan", 0) > 0:
count = data["new_commits_since_last_scan"]
indicators.append(f"🆕 {count} new")
if data.get("behind", 0) > 0:
count = data["behind"]
indicators.append(f"⬆️ {count} behind")
status = " | ".join(indicators) if indicators else ""
# Format output
branch = data.get("branch", "unknown")
msg = data.get("last_commit_msg", "No commits")
date = data.get("last_commit_date", "unknown")
return f"- **{repo_name}** ({branch}) - {status}\n Last: {msg} ({date})"
def categorize_repos(repos: Dict, config: Dict) -> Dict[str, List[Tuple[str, Dict]]]:
"""Organize repos by category"""
categorized = {cat: [] for cat in CATEGORIES_ORDER}
categorized["UNCATEGORIZED"] = []
for repo_name, data in repos.items():
category = get_category(data.get("project_file", ""))
if category in categorized:
categorized[category].append((repo_name, data))
else:
categorized["UNCATEGORIZED"].append((repo_name, data))
return categorized
def generate_summary():
"""Main function: generate summary.md from state.json"""
# Load data
state = load_json(STATE_FILE)
config = load_json(CONFIG_FILE)
repos = state["repos"]
stats = state["stats"]
last_scan = state["last_scan"]
# Categorize
categorized = categorize_repos(repos, config)
# Start building markdown
md = []
md.append("# ProjectTracker - Summary\n")
md.append(f"**Last Scan:** {last_scan}\n")
md.append("---\n")
# Global Stats
md.append("## 📊 Global Stats\n")
md.append(f"- **Total Repos:** {stats['total_repos']}")
md.append(f"- **Active (3w):** {stats['active_3w']}")
md.append(f"- **Needs Attention:** {stats['needs_attention']}")
md.append(f"- **Conflicts:** {stats['conflicts']}\n")
# Needs Attention Section
attention_repos = [
(name, data) for name, data in repos.items()
if data.get("needs_attention")
]
if attention_repos:
md.append("## ⚠️ Needs Attention\n")
# Sort by priority: conflicts > uncommitted > new commits > behind
attention_repos.sort(key=lambda x: (
-int(x[1].get("has_conflict", False)),
-int(x[1].get("has_uncommitted_changes", False)),
-x[1].get("new_commits_since_last_scan", 0),
-x[1].get("behind", 0)
))
for repo_name, data in attention_repos:
md.append(format_repo_status(repo_name, data))
md.append("")
# By Category Section
md.append("## 📂 By Category\n")
for category in CATEGORIES_ORDER:
repos_in_cat = categorized[category]
if repos_in_cat:
md.append(f"### {category} ({len(repos_in_cat)})\n")
for repo_name, data in sorted(repos_in_cat):
md.append(format_repo_status(repo_name, data))
md.append("")
# Uncategorized (if any)
if categorized["UNCATEGORIZED"]:
count = len(categorized["UNCATEGORIZED"])
md.append(f"### UNCATEGORIZED ({count})\n")
for repo_name, data in sorted(categorized["UNCATEGORIZED"]):
md.append(format_repo_status(repo_name, data))
md.append("")
# Recent Activity Section
active_repos = [
(name, data) for name, data in repos.items()
if data.get("commits_3w", 0) > 0
]
if active_repos:
md.append("## 🔥 Recent Activity (3 weeks)\n")
# Sort by commit count (descending)
active_repos.sort(key=lambda x: -x[1]["commits_3w"])
for repo_name, data in active_repos:
commits = data["commits_3w"]
plural = 's' if commits > 1 else ''
md.append(f"- **{repo_name}**: {commits} commit{plural}")
md.append("")
# Footer
md.append("---\n")
md.append("*Generated by ProjectTracker/tools/analyze.py*")
# Write summary
summary_content = "\n".join(md)
SUMMARY_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(SUMMARY_FILE, 'w', encoding='utf-8') as f:
f.write(summary_content)
# Console output
print(f"✅ Summary generated: {SUMMARY_FILE}")
print(f"📊 {stats['total_repos']} repos scanned, {stats['needs_attention']} need attention")
if __name__ == "__main__":
try:
generate_summary()
except FileNotFoundError as e:
print(f"❌ Error: File not found - {e}")
print("💡 Run scan.sh first to generate state.json")
exit(1)
except json.JSONDecodeError as e:
print(f"❌ Error: Invalid JSON - {e}")
exit(1)
except Exception as e:
print(f"❌ Unexpected error: {e}")
import traceback
traceback.print_exc()
exit(1)

69
tools/config.json Normal file
View File

@ -0,0 +1,69 @@
{
"version": "1.0.0",
"repos_root": "C:/Users/alexi/Documents/projects",
"tracked_repos": [
"aissia",
"groveengine",
"AlwaysOnRecorder",
"civjdr",
"ChineseClass",
"Class_generator",
"confluent",
"maicivy",
"mobilecommand",
"secondvoice",
"seogeneratorserver",
"vba-mcp-monorepo",
"vba-mcp-server",
"videotoMP3Transcriptor",
"warfactoryracine",
"wechat-homework-bot",
"freelance_planning",
"WrittingFantasy"
],
"excluded_repos": [
"couple-repo",
"knowledgemapperrepo",
"vba-mcp-demo",
"ProjectTracker"
],
"project_mapping": {
"aissia": "projects/WIP/aissia.md",
"groveengine": "projects/WIP/groveengine.md",
"AlwaysOnRecorder": "projects/WIP/AlwaysOnRecorder.md",
"civjdr": "projects/CONSTANT/civjdr.md",
"ChineseClass": "projects/CONSTANT/ChineseClass.md",
"Class_generator": "projects/CONSTANT/Class_generator.md",
"confluent": "projects/WIP/confluent.md",
"maicivy": "projects/WIP/maicivy.md",
"mobilecommand": "projects/CONCEPT/mobilecommand.md",
"secondvoice": "projects/PAUSE/secondvoice.md",
"seogeneratorserver": "projects/WIP/seogeneratorserver.md",
"vba-mcp-monorepo": "projects/WIP/vba-mcp-monorepo.md",
"vba-mcp-server": "projects/WIP/vba-mcp-server.md",
"videotoMP3Transcriptor": "projects/DONE/videotoMP3Transcriptor.md",
"warfactoryracine": "projects/PAUSE/warfactoryracine.md",
"wechat-homework-bot": "projects/WIP/wechat-homework-bot.md",
"freelance_planning": "projects/META/freelance_planning.md",
"WrittingFantasy": "projects/WIP/WrittingFantasy.md"
},
"scan_options": {
"auto_pull": true,
"fetch_before_pull": true,
"check_ahead_behind": true,
"commits_lookback_days": 21,
"detect_uncommitted_changes": true,
"timeout_per_repo_seconds": 30
},
"output_options": {
"state_file": ".planning/state.json",
"log_file": ".planning/scan.log",
"conflicts_file": ".planning/conflicts.txt",
"summary_file": ".planning/summary.md"
}
}

208
tools/scan.sh Normal file
View File

@ -0,0 +1,208 @@
#!/bin/bash
# ProjectTracker - Multi-repo scan script
# Scans all tracked repos, pulls updates, generates state.json
set -euo pipefail
# Paths
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_TRACKER="$(cd "$SCRIPT_DIR/.." && pwd)"
CONFIG_FILE="$SCRIPT_DIR/config.json"
# Check jq availability
if ! command -v jq &> /dev/null; then
echo "ERROR: jq is required but not installed."
echo "Install: https://stedolan.github.io/jq/download/"
exit 1
fi
# Load config
REPOS_ROOT=$(jq -r '.repos_root' "$CONFIG_FILE")
LOOKBACK_DAYS=$(jq -r '.scan_options.commits_lookback_days' "$CONFIG_FILE")
AUTO_PULL=$(jq -r '.scan_options.auto_pull' "$CONFIG_FILE")
# Output files
STATE_FILE="$PROJECT_TRACKER/.planning/state.json"
LOG_FILE="$PROJECT_TRACKER/.planning/scan.log"
CONFLICTS_FILE="$PROJECT_TRACKER/.planning/conflicts.txt"
# Ensure .planning exists
mkdir -p "$PROJECT_TRACKER/.planning"
# Init log
echo "===== Scan started: $(date -Iseconds) =====" > "$LOG_FILE"
> "$CONFLICTS_FILE"
echo "🔍 ProjectTracker - Multi-repo scan"
echo "Repos root: $REPOS_ROOT"
echo ""
# Read tracked repos into array
mapfile -t TRACKED_REPOS < <(jq -r '.tracked_repos[]' "$CONFIG_FILE")
# Start building state.json
cat > "$STATE_FILE" << 'EOF_HEADER'
{
"last_scan": "TIMESTAMP_PLACEHOLDER",
"repos": {
EOF_HEADER
FIRST_REPO=true
CONFLICT_COUNT=0
SCANNED_COUNT=0
ATTENTION_COUNT=0
# Scan each repo
for repo in "${TRACKED_REPOS[@]}"; do
REPO_PATH="$REPOS_ROOT/$repo"
# Check if repo exists
if [ ! -d "$REPO_PATH/.git" ]; then
echo "⚠️ $repo - NOT A GIT REPO (skipping)" | tee -a "$LOG_FILE"
continue
fi
echo -n "📂 $repo... " | tee -a "$LOG_FILE"
cd "$REPO_PATH"
# Fetch origin
git fetch origin &>> "$LOG_FILE" || true
# Pull if enabled
HAS_CONFLICT=false
if [ "$AUTO_PULL" = "true" ]; then
PULL_OUTPUT=$(git pull 2>&1) || true
echo "$PULL_OUTPUT" >> "$LOG_FILE"
if echo "$PULL_OUTPUT" | grep -q "CONFLICT\|Automatic merge failed"; then
HAS_CONFLICT=true
CONFLICT_COUNT=$((CONFLICT_COUNT + 1))
echo "⚠️ CONFLICT: $repo" | tee -a "$CONFLICTS_FILE"
fi
fi
# Collect repo info
CURRENT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
LAST_COMMIT_MSG=$(git log -1 --format="%s" 2>/dev/null || echo "No commits")
LAST_COMMIT_DATE=$(git log -1 --format="%ar" 2>/dev/null || echo "unknown")
# Ahead/behind tracking branch
TRACKING_BRANCH=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || echo "")
if [ -n "$TRACKING_BRANCH" ]; then
AHEAD=$(git rev-list --count HEAD..$TRACKING_BRANCH 2>/dev/null || echo 0)
BEHIND=$(git rev-list --count $TRACKING_BRANCH..HEAD 2>/dev/null || echo 0)
else
AHEAD=0
BEHIND=0
fi
# Uncommitted changes
if git diff-index --quiet HEAD -- 2>/dev/null; then
UNCOMMITTED=false
UNCOMMITTED_COUNT=0
else
UNCOMMITTED=true
UNCOMMITTED_COUNT=$(git status --short 2>/dev/null | wc -l)
fi
# Recent commits
COMMITS_3W=$(git rev-list --count --since="$LOOKBACK_DAYS days ago" HEAD 2>/dev/null || echo 0)
# Load previous commit from existing state (if exists)
PREV_COMMIT=$(jq -r ".repos.\"$repo\".last_commit // \"\"" "$STATE_FILE" 2>/dev/null || echo "")
if [ -n "$PREV_COMMIT" ] && [ "$PREV_COMMIT" != "$CURRENT_COMMIT" ] && [ "$PREV_COMMIT" != "null" ]; then
NEW_COMMITS=$(git rev-list --count $PREV_COMMIT..$CURRENT_COMMIT 2>/dev/null || echo 0)
else
NEW_COMMITS=0
fi
# Determine needs_attention
NEEDS_ATTENTION=false
if [ "$NEW_COMMITS" -gt 0 ] || [ "$UNCOMMITTED" = "true" ] || [ "$BEHIND" -gt 0 ] || [ "$HAS_CONFLICT" = "true" ]; then
NEEDS_ATTENTION=true
ATTENTION_COUNT=$((ATTENTION_COUNT + 1))
fi
# Project file mapping
PROJECT_FILE=$(jq -r ".project_mapping.\"$repo\" // \"\"" "$CONFIG_FILE")
# Add comma if not first repo
if [ "$FIRST_REPO" = false ]; then
echo "," >> "$STATE_FILE"
fi
FIRST_REPO=false
# Write JSON entry (properly escaped)
LAST_COMMIT_MSG_ESCAPED=$(echo "$LAST_COMMIT_MSG" | sed 's/\\/\\\\/g; s/"/\\"/g')
cat >> "$STATE_FILE" << EOF
"$repo": {
"last_commit": "$CURRENT_COMMIT",
"branch": "$CURRENT_BRANCH",
"last_commit_msg": "$LAST_COMMIT_MSG_ESCAPED",
"last_commit_date": "$LAST_COMMIT_DATE",
"commits_3w": $COMMITS_3W,
"new_commits_since_last_scan": $NEW_COMMITS,
"ahead": $AHEAD,
"behind": $BEHIND,
"has_uncommitted_changes": $UNCOMMITTED,
"uncommitted_count": $UNCOMMITTED_COUNT,
"has_conflict": $HAS_CONFLICT,
"needs_attention": $NEEDS_ATTENTION,
"project_file": "$PROJECT_FILE"
}
EOF
SCANNED_COUNT=$((SCANNED_COUNT + 1))
# Status indicator
if [ "$HAS_CONFLICT" = "true" ]; then
echo "❌ CONFLICT"
elif [ "$NEEDS_ATTENTION" = "true" ]; then
echo "⚠️ needs attention"
else
echo "✅"
fi
done
# Close repos object and add stats
cat >> "$STATE_FILE" << EOF
},
"stats": {
"total_repos": $SCANNED_COUNT,
"active_3w": 0,
"needs_attention": $ATTENTION_COUNT,
"conflicts": $CONFLICT_COUNT
}
}
EOF
# Calculate active repos count
ACTIVE_COUNT=$(jq '[.repos[] | select(.commits_3w > 0)] | length' "$STATE_FILE")
jq ".stats.active_3w = $ACTIVE_COUNT" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
# Update timestamp
TIMESTAMP=$(date -Iseconds)
jq ".last_scan = \"$TIMESTAMP\"" "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"
# Summary
echo ""
echo "===== Scan completed: $(date -Iseconds) =====" | tee -a "$LOG_FILE"
echo "✅ Scanned: $SCANNED_COUNT repos" | tee -a "$LOG_FILE"
echo "🔥 Active (3w): $ACTIVE_COUNT repos" | tee -a "$LOG_FILE"
echo "⚠️ Needs attention: $ATTENTION_COUNT repos" | tee -a "$LOG_FILE"
echo "❌ Conflicts: $CONFLICT_COUNT repos" | tee -a "$LOG_FILE"
echo ""
echo "📊 State saved to: $STATE_FILE"
echo "📝 Log saved to: $LOG_FILE"
# Generate summary
if [ -f "$SCRIPT_DIR/analyze.py" ]; then
echo ""
echo "Generating summary..."
python3 "$SCRIPT_DIR/analyze.py"
fi