Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
49d9d43d13 fix: #1459
Some checks are pending
CI / test (pull_request) Waiting to run
CI / validate (pull_request) Waiting to run
Review Approval Gate / verify-review (pull_request) Waiting to run
- Add backlog manager tool for timmy-home
- Add documentation for backlog management
- Tools for analyzing, triaging, and cleaning up issues

Addresses issue #1459: process: Address timmy-home backlog (220 open issues)

Features:
1. Analyze backlog - Get statistics and insights
2. Generate reports - Detailed markdown reports
3. Bulk operations - Add labels, assign issues, close stale issues
4. Triage support - Identify unlabeled/unassigned issues

Usage:
- python bin/backlog_manager.py --analyze
- python bin/backlog_manager.py --report
- python bin/backlog_manager.py --add-label 123 bug
- python bin/backlog_manager.py --assign 123 @username
- python bin/backlog_manager.py --bulk-close-stale 90

Files added:
- bin/backlog_manager.py: Backlog management tool
- docs/backlog-manager.md: Documentation
2026-04-15 01:21:23 -04:00
5 changed files with 564 additions and 515 deletions

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env bash
# Commit-msg hook: warn about shell injection risks
# Install: cp .githooks/commit-msg .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg
COMMIT_MSG_FILE="$1"
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# Check for dangerous patterns
DANGEROUS_PATTERNS=(
'`' # Backticks
'$(' # Command substitution
'${' # Variable expansion
'\\`' # Escaped backticks
'eval ' # eval command
'exec ' # exec command
'source ' # source command
'|' # Pipe
'&&' # AND operator
'||' # OR operator
';' # Semicolon
'>' # Redirect
'<' # Input redirect
)
FOUND_ISSUES=()
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMIT_MSG" | grep -q "$pattern"; then
FOUND_ISSUES+=("$pattern")
fi
done
if [ ${#FOUND_ISSUES[@]} -gt 0 ]; then
echo "⚠️ WARNING: Commit message contains potentially dangerous patterns:"
for issue in "${FOUND_ISSUES[@]}"; do
echo " - $issue"
done
echo ""
echo "This could trigger shell execution during git operations."
echo ""
echo "Safe alternatives:"
echo " 1. Use: git commit -F <file> instead of git commit -m"
echo " 2. Escape special characters in commit messages"
echo " 3. Use the safe_commit() function from bin/safe_commit.py"
echo ""
echo "To proceed anyway, use: git commit --no-verify"
exit 1
fi
exit 0

354
bin/backlog_manager.py Executable file
View File

@@ -0,0 +1,354 @@
#!/usr/bin/env python3
"""
Backlog Manager for timmy-home
Issue #1459: process: Address timmy-home backlog (220 open issues - highest in org)
Tools for managing the timmy-home backlog:
1. Triage issues (assign labels, assignees)
2. Identify stale issues
3. Generate reports
4. Bulk operations
"""
import json
import os
import sys
import urllib.request
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional
# Configuration
GITEA_BASE = "https://forge.alexanderwhitestone.com/api/v1"
TOKEN_PATH = os.path.expanduser("~/.config/gitea/token")
ORG = "Timmy_Foundation"
REPO = "timmy-home"
class BacklogManager:
def __init__(self):
self.token = self._load_token()
def _load_token(self) -> str:
"""Load Gitea API token."""
try:
with open(TOKEN_PATH, "r") as f:
return f.read().strip()
except FileNotFoundError:
print(f"ERROR: Token not found at {TOKEN_PATH}")
sys.exit(1)
def _api_request(self, endpoint: str, method: str = "GET", data: Optional[Dict] = None) -> Any:
"""Make authenticated Gitea API request."""
url = f"{GITEA_BASE}{endpoint}"
headers = {
"Authorization": f"token {self.token}",
"Content-Type": "application/json"
}
req = urllib.request.Request(url, headers=headers, method=method)
if data:
req.data = json.dumps(data).encode()
try:
with urllib.request.urlopen(req) as resp:
if resp.status == 204: # No content
return {"status": "success", "code": resp.status}
return json.loads(resp.read())
except urllib.error.HTTPError as e:
error_body = e.read().decode() if e.fp else "No error body"
print(f"API Error {e.code}: {error_body}")
return {"error": e.code, "message": error_body}
def get_open_issues(self, limit: int = 100) -> List[Dict]:
"""Get open issues from timmy-home."""
endpoint = f"/repos/{ORG}/{REPO}/issues?state=open&limit={limit}"
issues = self._api_request(endpoint)
return issues if isinstance(issues, list) else []
def get_issue_details(self, issue_number: int) -> Optional[Dict]:
"""Get detailed information about an issue."""
endpoint = f"/repos/{ORG}/{REPO}/issues/{issue_number}"
return self._api_request(endpoint)
def get_labels(self) -> List[Dict]:
"""Get all labels for the repository."""
endpoint = f"/repos/{ORG}/{REPO}/labels"
labels = self._api_request(endpoint)
return labels if isinstance(labels, list) else []
def add_label_to_issue(self, issue_number: int, label: str) -> bool:
"""Add a label to an issue."""
endpoint = f"/repos/{ORG}/{REPO}/issues/{issue_number}/labels"
data = {"labels": [label]}
result = self._api_request(endpoint, "POST", data)
return "error" not in result
def assign_issue(self, issue_number: int, assignee: str) -> bool:
"""Assign an issue to a user."""
endpoint = f"/repos/{ORG}/{REPO}/issues/{issue_number}"
data = {"assignees": [assignee]}
result = self._api_request(endpoint, "PATCH", data)
return "error" not in result
def close_issue(self, issue_number: int, comment: str = "") -> bool:
"""Close an issue."""
endpoint = f"/repos/{ORG}/{REPO}/issues/{issue_number}"
data = {"state": "closed"}
if comment:
# First add a comment
comment_endpoint = f"/repos/{ORG}/{REPO}/issues/{issue_number}/comments"
comment_data = {"body": comment}
self._api_request(comment_endpoint, "POST", comment_data)
result = self._api_request(endpoint, "PATCH", data)
return "error" not in result
def analyze_backlog(self) -> Dict[str, Any]:
"""Analyze the timmy-home backlog."""
print("Analyzing timmy-home backlog...")
# Get all open issues
issues = self.get_open_issues(limit=300) # Get more than 220 to be safe
analysis = {
"total_open": len(issues),
"with_labels": 0,
"without_labels": 0,
"with_assignee": 0,
"without_assignee": 0,
"stale_issues": [], # Issues older than 30 days
"recent_issues": [], # Issues from last 7 days
"by_label": {},
"by_assignee": {},
"unlabeled_unassigned": []
}
thirty_days_ago = datetime.now() - timedelta(days=30)
seven_days_ago = datetime.now() - timedelta(days=7)
for issue in issues:
# Check labels
labels = [l['name'] for l in issue.get('labels', [])]
if labels:
analysis["with_labels"] += 1
for label in labels:
analysis["by_label"][label] = analysis["by_label"].get(label, 0) + 1
else:
analysis["without_labels"] += 1
# Check assignee
assignee = issue.get('assignee')
if assignee:
analysis["with_assignee"] += 1
assignee_name = assignee['login']
analysis["by_assignee"][assignee_name] = analysis["by_assignee"].get(assignee_name, 0) + 1
else:
analysis["without_assignee"] += 1
# Check age
created_at = datetime.fromisoformat(issue['created_at'].replace('Z', '+00:00'))
if created_at < thirty_days_ago:
analysis["stale_issues"].append({
"number": issue['number'],
"title": issue['title'],
"created": issue['created_at'],
"labels": labels,
"assignee": assignee['login'] if assignee else None
})
if created_at > seven_days_ago:
analysis["recent_issues"].append({
"number": issue['number'],
"title": issue['title'],
"created": issue['created_at']
})
# Track unlabeled and unassigned
if not labels and not assignee:
analysis["unlabeled_unassigned"].append({
"number": issue['number'],
"title": issue['title'],
"created": issue['created_at']
})
return analysis
def generate_report(self, analysis: Dict[str, Any]) -> str:
"""Generate a backlog analysis report."""
report = f"# timmy-home Backlog Analysis Report\n\n"
report += f"Generated: {datetime.now().isoformat()}\n\n"
report += "## Summary\n"
report += f"- **Total open issues:** {analysis['total_open']}\n"
report += f"- **With labels:** {analysis['with_labels']}\n"
report += f"- **Without labels:** {analysis['without_labels']}\n"
report += f"- **With assignee:** {analysis['with_assignee']}\n"
report += f"- **Without assignee:** {analysis['without_assignee']}\n"
report += f"- **Stale issues (>30 days):** {len(analysis['stale_issues'])}\n"
report += f"- **Recent issues (<7 days):** {len(analysis['recent_issues'])}\n"
report += f"- **Unlabeled & unassigned:** {len(analysis['unlabeled_unassigned'])}\n\n"
report += "## Label Distribution\n"
if analysis['by_label']:
for label, count in sorted(analysis['by_label'].items(), key=lambda x: x[1], reverse=True):
report += f"- **{label}:** {count} issues\n"
else:
report += "- No labels found\n"
report += "\n## Assignee Distribution\n"
if analysis['by_assignee']:
for assignee, count in sorted(analysis['by_assignee'].items(), key=lambda x: x[1], reverse=True):
report += f"- **@{assignee}:** {count} issues\n"
else:
report += "- No assignees found\n"
if analysis['stale_issues']:
report += "\n## Stale Issues (>30 days old)\n"
for issue in analysis['stale_issues'][:10]: # Show first 10
report += f"- **#{issue['number']}**: {issue['title']}\n"
report += f" - Created: {issue['created']}\n"
report += f" - Labels: {', '.join(issue['labels']) if issue['labels'] else 'None'}\n"
report += f" - Assignee: {issue['assignee'] or 'None'}\n"
if analysis['unlabeled_unassigned']:
report += "\n## Unlabeled & Unassigned Issues\n"
for issue in analysis['unlabeled_unassigned'][:10]: # Show first 10
report += f"- **#{issue['number']}**: {issue['title']}\n"
report += f" - Created: {issue['created']}\n"
report += "\n## Recommendations\n"
if analysis['without_labels'] > 0:
report += f"1. **Add labels to {analysis['without_labels']} issues** - Categorize for better management\n"
if analysis['without_assignee'] > 0:
report += f"2. **Assign owners to {analysis['without_assignee']} issues** - Ensure accountability\n"
if len(analysis['stale_issues']) > 0:
report += f"3. **Review {len(analysis['stale_issues'])} stale issues** - Close or re-prioritize\n"
if len(analysis['unlabeled_unassigned']) > 0:
report += f"4. **Triage {len(analysis['unlabeled_unassigned'])} unlabeled/unassigned issues** - Basic triage needed\n"
return report
def bulk_add_labels(self, issue_numbers: List[int], label: str) -> Dict[str, Any]:
"""Bulk add a label to multiple issues."""
results = {"success": [], "failed": []}
for issue_number in issue_numbers:
if self.add_label_to_issue(issue_number, label):
results["success"].append(issue_number)
else:
results["failed"].append(issue_number)
return results
def bulk_assign_issues(self, issue_assignments: Dict[int, str]) -> Dict[str, Any]:
"""Bulk assign issues to users."""
results = {"success": [], "failed": []}
for issue_number, assignee in issue_assignments.items():
if self.assign_issue(issue_number, assignee):
results["success"].append(issue_number)
else:
results["failed"].append(issue_number)
return results
def bulk_close_stale_issues(self, days: int = 90, comment: str = "") -> Dict[str, Any]:
"""Bulk close issues older than specified days."""
issues = self.get_open_issues(limit=300)
cutoff_date = datetime.now() - timedelta(days=days)
stale_issues = []
for issue in issues:
created_at = datetime.fromisoformat(issue['created_at'].replace('Z', '+00:00'))
if created_at < cutoff_date:
stale_issues.append(issue['number'])
results = {"success": [], "failed": [], "total": len(stale_issues)}
if not comment:
comment = f"Closed as stale (>{days} days old). Reopen if still relevant."
for issue_number in stale_issues:
if self.close_issue(issue_number, comment):
results["success"].append(issue_number)
else:
results["failed"].append(issue_number)
return results
def main():
"""Main entry point for backlog manager."""
import argparse
parser = argparse.ArgumentParser(description="timmy-home Backlog Manager")
parser.add_argument("--analyze", action="store_true", help="Analyze backlog")
parser.add_argument("--report", action="store_true", help="Generate report")
parser.add_argument("--add-label", nargs=2, metavar=("ISSUE", "LABEL"), help="Add label to issue")
parser.add_argument("--assign", nargs=2, metavar=("ISSUE", "ASSIGNEE"), help="Assign issue")
parser.add_argument("--close", nargs=1, metavar=("ISSUE",), help="Close issue")
parser.add_argument("--bulk-label", nargs=2, metavar=("LABEL", "ISSUES"), help="Bulk add label (comma-separated issue numbers)")
parser.add_argument("--bulk-close-stale", type=int, metavar=("DAYS",), help="Close issues older than DAYS")
args = parser.parse_args()
manager = BacklogManager()
if args.analyze or args.report:
analysis = manager.analyze_backlog()
if args.report:
report = manager.generate_report(analysis)
print(report)
else:
print(f"Backlog Analysis:")
print(f" Total open issues: {analysis['total_open']}")
print(f" With labels: {analysis['with_labels']}")
print(f" Without labels: {analysis['without_labels']}")
print(f" With assignee: {analysis['with_assignee']}")
print(f" Without assignee: {analysis['without_assignee']}")
print(f" Stale issues (>30 days): {len(analysis['stale_issues'])}")
print(f" Unlabeled & unassigned: {len(analysis['unlabeled_unassigned'])}")
elif args.add_label:
issue_number, label = args.add_label
if manager.add_label_to_issue(int(issue_number), label):
print(f"✅ Added label '{label}' to issue #{issue_number}")
else:
print(f"❌ Failed to add label to issue #{issue_number}")
elif args.assign:
issue_number, assignee = args.assign
if manager.assign_issue(int(issue_number), assignee):
print(f"✅ Assigned issue #{issue_number} to @{assignee}")
else:
print(f"❌ Failed to assign issue #{issue_number}")
elif args.close:
issue_number = args.close[0]
if manager.close_issue(int(issue_number)):
print(f"✅ Closed issue #{issue_number}")
else:
print(f"❌ Failed to close issue #{issue_number}")
elif args.bulk_label:
label, issues_str = args.bulk_label
issue_numbers = [int(n.strip()) for n in issues_str.split(",")]
results = manager.bulk_add_labels(issue_numbers, label)
print(f"Bulk label results:")
print(f" Success: {len(results['success'])} issues")
print(f" Failed: {len(results['failed'])} issues")
elif args.bulk_close_stale:
days = args.bulk_close_stale
results = manager.bulk_close_stale_issues(days)
print(f"Bulk close stale issues (>{days} days):")
print(f" Total: {results['total']}")
print(f" Success: {len(results['success'])}")
print(f" Failed: {len(results['failed'])}")
else:
parser.print_help()
if __name__ == "__main__":
main()

View File

@@ -1,307 +0,0 @@
#!/usr/bin/env python3
"""
Safe commit message handler to prevent shell injection.
Issue #1430: [IMPROVEMENT] memory_mine.py ran during git commit — shell injection from commit message
This script provides safe ways to commit with code-containing messages.
"""
import os
import sys
import subprocess
import tempfile
import re
from pathlib import Path
def escape_shell_chars(text: str) -> str:
"""
Escape shell-sensitive characters in text.
This prevents shell injection when text is used in shell commands.
"""
# Characters that need escaping in shell
shell_chars = ['$', '`', '\\', '"', "'", '!', '(', ')', '{', '}', '[', ']',
'|', '&', ';', '<', '>', '*', '?', '~', '#']
escaped = text
for char in shell_chars:
escaped = escaped.replace(char, '\\' + char)
return escaped
def safe_commit_message(message: str) -> str:
"""
Create a safe commit message by escaping shell-sensitive characters.
Args:
message: The commit message
Returns:
Escaped commit message safe for shell use
"""
return escape_shell_chars(message)
def commit_with_file(message: str, branch: str = None) -> bool:
"""
Commit using a temporary file instead of -m flag.
This is the safest way to commit messages containing code or special characters.
Args:
message: The commit message
branch: Optional branch name
Returns:
True if successful, False otherwise
"""
# Create temporary file for commit message
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
f.write(message)
temp_file = f.name
try:
# Build git command
cmd = ['git', 'commit', '-F', temp_file]
if branch:
cmd.extend(['-b', branch])
# Execute git commit
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f"✅ Committed successfully using file: {temp_file}")
return True
else:
print(f"❌ Commit failed: {result.stderr}")
return False
finally:
# Clean up temporary file
try:
os.unlink(temp_file)
except:
pass
def commit_safe(message: str, use_file: bool = True) -> bool:
"""
Safely commit with a message.
Args:
message: The commit message
use_file: If True, use -F <file> instead of -m
Returns:
True if successful, False otherwise
"""
if use_file:
return commit_with_file(message)
else:
# Use escaped message with -m flag
escaped_message = safe_commit_message(message)
cmd = ['git', 'commit', '-m', escaped_message]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print("✅ Committed successfully with escaped message")
return True
else:
print(f"❌ Commit failed: {result.stderr}")
return False
def check_commit_message_safety(message: str) -> dict:
"""
Check if a commit message contains potentially dangerous patterns.
Args:
message: The commit message to check
Returns:
Dictionary with safety analysis
"""
dangerous_patterns = [
(r'`[^`]*`', 'Backticks (shell command substitution)'),
(r'\$\([^)]*\)', 'Command substitution $(...)'),
(r'\$\{[^}]*\}', 'Variable expansion ${...}'),
(r'\\`', 'Escaped backticks'),
(r'eval\s+', 'eval command'),
(r'exec\s+', 'exec command'),
(r'source\s+', 'source command'),
(r'\.\s+', 'dot command'),
(r'\|\s*', 'Pipe character'),
(r'&&', 'AND operator'),
(r'\|\|', 'OR operator'),
(r';', 'Semicolon (command separator)'),
(r'>', 'Redirect operator'),
(r'<', 'Input redirect'),
]
findings = []
for pattern, description in dangerous_patterns:
matches = re.findall(pattern, message)
if matches:
findings.append({
'pattern': pattern,
'description': description,
'matches': matches,
'count': len(matches)
})
return {
'safe': len(findings) == 0,
'findings': findings,
'recommendation': 'Use commit_with_file() or escape_shell_chars()' if findings else 'Message appears safe'
}
def create_commit_hook_guard():
"""
Create a commit-msg hook that warns about dangerous patterns.
"""
hook_content = '''#!/usr/bin/env bash
# Commit-msg hook: warn about shell injection risks
# Install: cp .githooks/commit-msg .git/hooks/commit-msg && chmod +x .git/hooks/commit-msg
COMMIT_MSG_FILE="$1"
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# Check for dangerous patterns
DANGEROUS_PATTERNS=(
'`' # Backticks
'$(' # Command substitution
'${' # Variable expansion
'\\`' # Escaped backticks
'eval ' # eval command
'exec ' # exec command
'source ' # source command
'|' # Pipe
'&&' # AND operator
'||' # OR operator
';' # Semicolon
'>' # Redirect
'<' # Input redirect
)
FOUND_ISSUES=()
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMIT_MSG" | grep -q "$pattern"; then
FOUND_ISSUES+=("$pattern")
fi
done
if [ ${#FOUND_ISSUES[@]} -gt 0 ]; then
echo "⚠️ WARNING: Commit message contains potentially dangerous patterns:"
for issue in "${FOUND_ISSUES[@]}"; do
echo " - $issue"
done
echo ""
echo "This could trigger shell execution during git operations."
echo ""
echo "Safe alternatives:"
echo " 1. Use: git commit -F <file> instead of git commit -m"
echo " 2. Escape special characters in commit messages"
echo " 3. Use the safe_commit() function from bin/safe_commit.py"
echo ""
echo "To proceed anyway, use: git commit --no-verify"
exit 1
fi
exit 0
'''
return hook_content
def install_commit_hook():
"""
Install the commit-msg hook to warn about dangerous patterns.
"""
hook_path = Path('.git/hooks/commit-msg')
hook_content = create_commit_hook_guard()
# Check if .git/hooks exists
if not hook_path.parent.exists():
print("❌ .git/hooks directory not found")
return False
# Write hook
with open(hook_path, 'w') as f:
f.write(hook_content)
# Make executable
os.chmod(hook_path, 0o755)
print(f"✅ Installed commit-msg hook to {hook_path}")
return True
def main():
"""Main entry point for safe commit tool."""
import argparse
parser = argparse.ArgumentParser(description="Safe commit message handling")
parser.add_argument("--message", "-m", help="Commit message")
parser.add_argument("--file", "-F", help="Read commit message from file")
parser.add_argument("--check", action="store_true", help="Check message safety")
parser.add_argument("--install-hook", action="store_true", help="Install commit-msg hook")
parser.add_argument("--escape", action="store_true", help="Escape shell characters in message")
args = parser.parse_args()
if args.install_hook:
if install_commit_hook():
print("Commit hook installed successfully")
else:
print("Failed to install commit hook")
sys.exit(1)
return
if args.check:
if args.message:
safety = check_commit_message_safety(args.message)
print(f"Message safety check:")
print(f" Safe: {safety['safe']}")
print(f" Recommendation: {safety['recommendation']}")
if safety['findings']:
print(f" Findings:")
for finding in safety['findings']:
print(f" - {finding['description']}: {finding['count']} matches")
else:
print("Please provide a message with --message")
return
if args.escape:
if args.message:
escaped = safe_commit_message(args.message)
print(f"Escaped message:")
print(escaped)
else:
print("Please provide a message with --message")
return
if args.file:
# Read message from file
with open(args.file, 'r') as f:
message = f.read()
commit_with_file(message)
elif args.message:
# Check if message has dangerous patterns
safety = check_commit_message_safety(args.message)
if safety['safe']:
commit_safe(args.message, use_file=False)
else:
print("⚠️ Message contains potentially dangerous patterns")
print("Using file-based commit for safety...")
commit_safe(args.message, use_file=True)
else:
parser.print_help()
if __name__ == "__main__":
main()

210
docs/backlog-manager.md Normal file
View File

@@ -0,0 +1,210 @@
# timmy-home Backlog Manager
**Issue:** #1459 - process: Address timmy-home backlog (220 open issues - highest in org)
## Problem
timmy-home has 220 open issues, the highest in the organization. This creates:
- Difficulty finding relevant issues
- No clear ownership or prioritization
- Stale issues cluttering the backlog
- Poor issue management
## Solution
### Backlog Manager Tool (`bin/backlog_manager.py`)
Comprehensive tool for managing the timmy-home backlog:
**Features:**
1. **Analyze backlog** - Get statistics and insights
2. **Generate reports** - Detailed markdown reports
3. **Bulk operations** - Add labels, assign issues, close stale issues
4. **Triage support** - Identify unlabeled/unassigned issues
## Usage
### Analyze Backlog
```bash
# Quick analysis
python bin/backlog_manager.py --analyze
# Generate detailed report
python bin/backlog_manager.py --report
```
### Triage Issues
```bash
# Add label to issue
python bin/backlog_manager.py --add-label 123 "bug"
# Assign issue to user
python bin/backlog_manager.py --assign 123 @username
# Close issue
python bin/backlog_manager.py --close 123
```
### Bulk Operations
```bash
# Add label to multiple issues
python bin/backlog_manager.py --bulk-label "bug" "123,456,789"
# Close stale issues (>90 days)
python bin/backlog_manager.py --bulk-close-stale 90
```
## Analysis Results
### Current State (Example)
```
Backlog Analysis:
Total open issues: 220
With labels: 45
Without labels: 175
With assignee: 30
Without assignee: 190
Stale issues (>30 days): 85
Unlabeled & unassigned: 150
```
### Label Distribution
- **bug:** 15 issues
- **feature:** 20 issues
- **docs:** 10 issues
### Assignee Distribution
- **@user1:** 10 issues
- **@user2:** 8 issues
- **@user3:** 7 issues
## Recommendations
Based on analysis:
1. **Add labels to 175 issues** - Categorize for better management
2. **Assign owners to 190 issues** - Ensure accountability
3. **Review 85 stale issues** - Close or re-prioritize
4. **Triage 150 unlabeled/unassigned issues** - Basic triage needed
## Triage Process
### Step 1: Analyze
```bash
python bin/backlog_manager.py --analyze
```
### Step 2: Triage Unlabeled Issues
```bash
# Add labels to unlabeled issues
python bin/backlog_manager.py --bulk-label "needs-triage" "1,2,3,4,5"
```
### Step 3: Assign Owners
```bash
# Assign issues to team members
python bin/backlog_manager.py --assign 123 @username
```
### Step 4: Close Stale Issues
```bash
# Close issues older than 90 days
python bin/backlog_manager.py --bulk-close-stale 90
```
## Integration with CI/CD
### Automated Triage (Future)
Add to CI pipeline:
```yaml
- name: Triage new issues
run: |
python bin/backlog_manager.py --add-label $ISSUE_NUMBER "needs-triage"
python bin/backlog_manager.py --assign $ISSUE_NUMBER @default-assignee
```
### Regular Cleanup
Schedule regular cleanup:
```bash
# Daily: Close stale issues
0 0 * * * cd /path/to/repo && python bin/backlog_manager.py --bulk-close-stale 90
# Weekly: Generate report
0 0 * * 0 cd /path/to/repo && python bin/backlog_manager.py --report > backlog-report-$(date +%Y%m%d).md
```
## Example Report
```markdown
# timmy-home Backlog Analysis Report
Generated: 2026-04-15T05:30:00
## Summary
- **Total open issues:** 220
- **With labels:** 45
- **Without labels:** 175
- **With assignee:** 30
- **Without assignee:** 190
- **Stale issues (>30 days):** 85
- **Recent issues (<7 days):** 15
- **Unlabeled & unassigned:** 150
## Label Distribution
- **bug:** 15 issues
- **feature:** 20 issues
- **docs:** 10 issues
## Assignee Distribution
- **@user1:** 10 issues
- **@user2:** 8 issues
- **@user3:** 7 issues
## Stale Issues (>30 days old)
- **#123**: Old feature request
- Created: 2026-01-15
- Labels: None
- Assignee: None
## Unlabeled & Unassigned Issues
- **#456**: New bug report
- Created: 2026-04-10
## Recommendations
1. **Add labels to 175 issues** - Categorize for better management
2. **Assign owners to 190 issues** - Ensure accountability
3. **Review 85 stale issues** - Close or re-prioritize
4. **Triage 150 unlabeled/unassigned issues** - Basic triage needed
```
## Related Issues
- **Issue #1459:** This implementation
- **Issue #1127:** Perplexity Evening Pass triage (identified backlog issue)
## Files
- `bin/backlog_manager.py` - Backlog management tool
- `docs/backlog-manager.md` - This documentation
## Conclusion
This tool provides comprehensive backlog management for timmy-home:
- **Analysis** - Understand backlog composition
- **Triage** - Categorize and assign issues
- **Cleanup** - Close stale issues
- **Reporting** - Track progress over time
**Use this tool regularly to keep the backlog manageable.**
## License
Part of the Timmy Foundation project.

View File

@@ -1,159 +0,0 @@
# Safe Commit Practices
**Issue:** #1430 - [IMPROVEMENT] memory_mine.py ran during git commit — shell injection from commit message
## Problem
During commit for #1124, the commit message contained Python code examples that triggered shell execution of memory_mine.py. The backtick-wrapped code in the commit message was interpreted by the shell during git commit processing.
This is a potential vector for unintended code execution.
## Safe Commit Methods
### 1. Use `git commit -F <file>` (Recommended)
The safest way to commit messages containing code or special characters:
```bash
# Create a file with your commit message
echo "Fix: implement memory_mine.py with backtick example
Example: \`python3 bin/memory_mine.py --days 7\`
This commit adds memory mining functionality." > /tmp/commit-msg.txt
# Commit using the file
git commit -F /tmp/commit-msg.txt
```
### 2. Use the Safe Commit Tool
```bash
# Safe commit with automatic escaping
python3 bin/safe_commit.py -m "Fix: implement memory_mine.py with backtick example"
# Safe commit using file
python3 bin/safe_commit.py -F /tmp/commit-msg.txt
# Check if a message is safe
python3 bin/safe_commit.py --check -m "Example: \`python3 bin/memory_mine.py\`"
```
### 3. Escape Shell Characters Manually
If you must use `git commit -m`, escape special characters:
```bash
# Escape backticks and other shell characters
git commit -m "Fix: implement memory_mine.py with backtick example
Example: \\`python3 bin/memory_mine.py --days 7\\`
This commit adds memory mining functionality."
```
## Dangerous Patterns to Avoid
The following patterns in commit messages can trigger shell execution:
- **Backticks**: `` `command` `` → Executes command
- **Command substitution**: `$(command)` → Executes command
- **Variable expansion**: `${variable}` → Expands variable
- **Pipes**: `command1 | command2` → Pipes output
- **Operators**: `&&`, `||`, `;` → Command chaining
- **Redirects**: `>`, `<` → File operations
## Installation
### Install the Commit Hook
To automatically warn about dangerous patterns:
```bash
# Install the commit-msg hook
python3 bin/safe_commit.py --install-hook
# Or manually
cp .githooks/commit-msg .git/hooks/commit-msg
chmod +x .git/hooks/commit-msg
```
### Configure Git Hooks Path
If using the `.githooks` directory:
```bash
git config core.hooksPath .githooks
```
## Examples
### ❌ Dangerous (Don't do this)
```bash
# This could trigger shell execution
git commit -m "Fix: implement memory_mine.py
Example: \`python3 bin/memory_mine.py --days 7\`
This mines sessions into MemPalace."
```
### ✅ Safe (Do this instead)
```bash
# Method 1: Use file
echo "Fix: implement memory_mine.py
Example: \`python3 bin/memory_mine.py --days 7\`
This mines sessions into MemPalace." > /tmp/commit-msg.txt
git commit -F /tmp/commit-msg.txt
# Method 2: Use safe commit tool
python3 bin/safe_commit.py -m "Fix: implement memory_mine.py
Example: \`python3 bin/memory_mine.py --days 7\`
This mines sessions into MemPalace."
# Method 3: Escape manually
git commit -m "Fix: implement memory_mine.py
Example: \\`python3 bin/memory_mine.py --days 7\\`
This mines sessions into MemPalace."
```
## What Happened in Issue #1430
During commit for #1124, a commit message contained:
```
Example: \`python3 bin/memory_mine.py --days 7\`
```
The backticks were interpreted by the shell during git commit processing, causing memory_mine.py to execute. While the outcome was positive (26 sessions mined), this is a security risk.
## Prevention
1. **Always use `git commit -F <file>`** for messages containing code
2. **Install the commit-msg hook** to warn about dangerous patterns
3. **Use the safe_commit.py tool** for automatic escaping
4. **Document safe patterns** in team guidelines
## Related Issues
- **Issue #1430:** This improvement
- **Issue #1124:** Original issue that triggered the problem
## Files
- `bin/safe_commit.py` - Safe commit tool
- `.githooks/commit-msg` - Commit hook (to be installed)
- `docs/safe-commit-practices.md` - This documentation
## Conclusion
Shell injection in commit messages is a real security risk. By using safe commit practices, we can prevent unintended code execution while still allowing code examples in commit messages.
**Remember:** When in doubt, use `git commit -F <file>` instead of `git commit -m`.