Skip to content

Nova AI Automation Architecture

Complete technical documentation with diagrams showing how the automation system works.

Table of Contents

  1. System Overview
  2. Architecture Diagrams
  3. Component Details
  4. Code Walkthrough
  5. Data Flow
  6. Integration Points

System Overview

Nova AI automation consists of 3 layers:

  1. Audit Layer - Detects issues via scheduled scans
  2. Automation Layer - Fixes issues automatically
  3. Quality Gate Layer - Prevents regressions
graph TB
    subgraph "Layer 1: Audit"
        A[Scheduler Daemon] --> B[Audit Runner]
        B --> C[6 Audit Modules]
        C --> D[Audit Report JSON]
    end

    subgraph "Layer 2: Automation"
        D --> E[Issue Creator]
        E --> F[Meta-Issues]
        D --> G[Auto-Fixer]
        G --> H[Auto-Fix PR]
    end

    subgraph "Layer 3: Quality Gates"
        I[GitHub Actions] --> J[Stale PR Closer]
        I --> K[Auto-Merge]
        I --> L[Performance Check]
        I --> M[Dead Code Scan]
        I --> N[Bug Predictor]
        I --> O[Dependency Updater]
    end

    F --> I
    H --> I

    style A fill:#e1f5ff
    style E fill:#fff4e1
    style I fill:#ffe1e1

Architecture Diagrams

1. End-to-End Automation Flow

sequenceDiagram
    participant Scheduler as Scheduler Daemon<br/>(APScheduler)
    participant Runner as Batch Audit Runner
    participant Modules as 6 Audit Modules
    participant Creator as Issue Creator<br/>(Meta-Issues)
    participant GH as GitHub API
    participant Actions as GitHub Actions
    participant AutoFix as Auto-Fixer

    Note over Scheduler: Daily 9 AM UTC
    Scheduler->>Runner: run_daily_audit()
    Runner->>Modules: Parallel execution
    Modules-->>Runner: Findings (JSON)
    Runner->>Creator: create_meta_issues.py
    Creator->>Creator: Group by category
    Creator->>GH: Create 10-15 meta-issues
    GH-->>Creator: Issue URLs

    Note over Scheduler: Nightly 2 AM UTC
    Scheduler->>Runner: run_auto_fix_audit()
    Runner->>Modules: Scan for LOW severity
    Modules-->>Runner: Fixable findings
    Runner->>AutoFix: apply_fixes()
    AutoFix->>AutoFix: black, isort, ruff
    AutoFix->>GH: Create PR with fixes

    Note over Actions: On PR events
    Actions->>Actions: performance-check.yml
    Actions->>Actions: auto-merge.yml
    Actions->>Actions: bug-prediction.yml

2. Scheduler Architecture

graph LR
    subgraph "Scheduler Process"
        A[audit/scheduler.py] --> B[ScheduleDaemon<br/>APScheduler]
        B --> C[ScheduleLoader<br/>YAML Config]
    end

    subgraph "Configuration"
        C --> D[SCHEDULER_CONFIG.yml]
        D --> E[4 Cron Jobs]
    end

    subgraph "Handlers"
        E --> F[run_daily_audit<br/>9 AM]
        E --> G[run_hourly_audit<br/>Every hour]
        E --> H[run_auto_fix<br/>2 AM]
        E --> I[run_weekly_report<br/>Monday]
    end

    subgraph "Execution"
        F --> J[BatchAuditRunner]
        G --> J
        H --> J
        I --> J
    end

    style A fill:#e1f5ff
    style D fill:#fff4e1
    style J fill:#e1ffe1

3. Audit Module Pipeline

graph TD
    A[BatchAuditRunner] --> B{Parallel Mode?}
    B -->|Yes| C[ThreadPoolExecutor<br/>6 workers]
    B -->|No| D[Sequential]

    C --> E1[Tests Module]
    C --> E2[Skills Module]
    C --> E3[Code Quality]
    C --> E4[MCP Module]
    C --> E5[Docs Module]
    C --> E6[Dependencies]

    D --> E1
    D --> E2
    D --> E3
    D --> E4
    D --> E5
    D --> E6

    E1 --> F[Aggregate Results]
    E2 --> F
    E3 --> F
    E4 --> F
    E5 --> F
    E6 --> F

    F --> G[AuditReport<br/>JSON + Markdown]

    style A fill:#e1f5ff
    style C fill:#ffe1e1
    style G fill:#e1ffe1

4. Meta-Issue Creation Flow

graph TB
    A[Audit Report JSON] --> B[create_meta_issues.py]
    B --> C{Group Findings}

    C --> D[Missing Purpose<br/>Sections]
    C --> E[Missing Usage<br/>Sections]
    C --> F[Missing Examples<br/>Sections]
    C --> G[Test Coverage<br/>Gaps]
    C --> H[Circular<br/>Dependencies]
    C --> I[Dead Code]
    C --> J[TODO Markers]

    D --> K[Generate Issue Body<br/>with Checklist]
    E --> K
    F --> K
    G --> K
    H --> K
    I --> K
    J --> K

    K --> L{Create via gh CLI}
    L --> M[Meta-Issue #485<br/>Purpose 73 items]
    L --> N[Meta-Issue #486<br/>Examples 75 items]
    L --> O[Meta-Issue #487<br/>Usage 73 items]

    style A fill:#e1f5ff
    style C fill:#fff4e1
    style M fill:#e1ffe1
    style N fill:#e1ffe1
    style O fill:#e1ffe1

5. GitHub Actions Quality Gates

graph LR
    subgraph "PR Events"
        A[pull_request<br/>opened/synchronize] --> B[performance-check.yml]
        A --> C[bug-prediction.yml]
        D[pull_request_review<br/>approved] --> E[auto-merge.yml]
    end

    subgraph "Scheduled Events"
        F[schedule<br/>weekly] --> G[stale-pr-closer.yml]
        F --> H[dead-code-check.yml]
        F --> I[dependency-updates.yml]
    end

    subgraph "Scripts"
        B --> J[scripts/performance_checker.py]
        C --> K[scripts/predict_pr_bugs.py]
        E --> L[scripts/auto_merge_approved.py]
        G --> M[scripts/close_stale_prs.py]
        H --> N[scripts/dead_code_detector.py]
        I --> O[scripts/dependency_updater.py]
    end

    style A fill:#e1f5ff
    style F fill:#fff4e1
    style J fill:#e1ffe1

6. Auto-Merge Safety Flow

flowchart TD
    A[PR Review: Approved] --> B{Has auto-merge<br/>label?}
    B -->|No| Z[Skip]
    B -->|Yes| C{Check Approvals}

    C -->|< 1 approval| Z
    C -->|≥ 1 approval| D{Check CI Status}

    D -->|Failed| Z
    D -->|Pending| W[Wait]
    D -->|Success| E{Check Conflicts}

    E -->|Has conflicts| Z
    E -->|Clean| F{Check Required<br/>Reviews}

    F -->|Changes requested| Z
    F -->|All approved| G[Merge PR<br/>Squash method]

    G --> H[Delete Branch]
    H --> I[Post Comment]

    style A fill:#e1f5ff
    style G fill:#90EE90
    style Z fill:#FFB6C1
    style W fill:#FFE4B5

7. Performance Regression Detection

sequenceDiagram
    participant PR as Pull Request
    participant Action as GitHub Action
    participant Checker as PerformanceChecker
    participant Baseline as main branch
    participant Report as Artifacts

    PR->>Action: push event
    Action->>Baseline: Checkout & benchmark
    Baseline-->>Action: baseline_results.json

    Action->>PR: Checkout PR branch
    Action->>Checker: Run benchmarks
    Checker->>Checker: bench_orchestrator.py
    Checker->>Checker: bench_tools.py
    Checker->>Checker: bench_kb.py
    Checker-->>Action: pr_results.json

    Action->>Checker: compare_results()
    Checker->>Checker: Calculate % change

    alt >25% slower
        Checker-->>Action: ❌ CRITICAL regression
        Action->>PR: Block merge
    else 10-25% slower
        Checker-->>Action: ⚠️ WARNING regression
        Action->>PR: Comment warning
    else <10% slower
        Checker-->>Action: ✅ PASS
        Action->>PR: Comment success
    end

    Action->>Report: Upload artifacts

8. ML Bug Prediction Pipeline

graph TB
    subgraph "Training Phase (One-time)"
        A[Historical PRs<br/>from GitHub] --> B[Feature Extraction]
        B --> C[16 Features]
        C --> D[Random Forest<br/>Classifier]
        D --> E[model.pkl<br/>scaler.pkl]
    end

    subgraph "Prediction Phase (Every PR)"
        F[New PR] --> G[Extract Features]
        G --> H[16 Features]
        H --> I[Load Model]
        I --> J{Predict<br/>Bug Probability}
    end

    subgraph "Features"
        C --> K[Complexity<br/>cyclomatic, cognitive]
        C --> L[Size<br/>files, lines, net]
        C --> M[Coverage<br/>delta, tests]
        C --> N[Author<br/>PR count, bug rate]
        C --> O[Metadata<br/>breaking, weekend]
    end

    J -->|>70%| P[HIGH Risk<br/>🔴]
    J -->|40-70%| Q[MEDIUM Risk<br/>🟡]
    J -->|<40%| R[LOW Risk<br/>🟢]

    P --> S[Post Comment<br/>with recommendations]
    Q --> S
    R --> S

    style E fill:#e1f5ff
    style J fill:#fff4e1
    style P fill:#FFB6C1
    style Q fill:#FFE4B5
    style R fill:#90EE90

9. Dead Code Detection Flow

graph LR
    A[Weekly Schedule] --> B[dead-code-check.yml]
    B --> C[DeadCodeDetector]
    C --> D[Run Vulture]
    D --> E[Raw Findings]

    E --> F{Filter False<br/>Positives}
    F --> G[Check Whitelist<br/>.vulture-whitelist.py]
    F --> H[Detect Public APIs<br/>__init__.py exports]
    F --> I[Detect Tests<br/>test_*.py]
    F --> J[Detect Callbacks<br/>handlers, fixtures]

    G --> K[True Dead Code]
    H --> K
    I --> K
    J --> K

    K --> L{Confidence<br/>≥ 80%?}
    L -->|Yes| M[Create Issue]
    L -->|No| N[Skip]

    M --> O[GitHub Issue<br/>with file:line]

    style A fill:#e1f5ff
    style K fill:#fff4e1
    style O fill:#e1ffe1

10. Dependency Update Workflow

sequenceDiagram
    participant Schedule as Weekly Schedule
    participant Action as dependency-updates.yml
    participant Dependabot as Dependabot
    participant Custom as DependencyChecker
    participant GH as GitHub

    Note over Schedule: Monday 9 AM UTC
    Schedule->>Action: Trigger workflow

    par Dependabot PRs
        Action->>Dependabot: Check config
        Dependabot->>GH: Create PRs<br/>patch updates
    and Custom Updates
        Action->>Custom: Check outdated
        Custom->>Custom: pip list --outdated
        Custom->>Custom: npm outdated
        Custom->>Custom: Group by semver
        Custom->>GH: Create grouped PR<br/>minor + major
    end

    GH->>Action: PRs created
    Action->>Action: Run tests

    alt Tests pass
        Action->>GH: Auto-merge patch<br/>if has auto-merge label
    else Tests fail
        Action->>GH: Request review
    end

Component Details

Scheduler Daemon

File: audit/scheduler.py (90 lines)

Purpose: Run automated audits on schedule using APScheduler

Key Components: - ScheduleDaemon - Manages APScheduler lifecycle - ScheduleLoader - Loads YAML config and registers jobs - Healthcheck endpoint via is_running()

Configuration: audit/SCHEDULER_CONFIG.yml

schedules:
  - name: daily_full_audit
    enabled: true
    schedule: "0 9 * * *"  # 9 AM UTC
    handler: audit.handlers.run_daily_audit
    max_retries: 3
    kwargs:
      create_issues: true
      severity_threshold: "high"

Schedules: 1. Daily Full Audit (9 AM UTC) - All 6 modules, creates issues 2. Hourly Fast Audit (every hour) - Tests + MCP only 3. Nightly Auto-Fix (2 AM UTC) - Auto-fix LOW severity 4. Weekly Report (Monday 9 AM) - Email summary (disabled)


Batch Audit Runner

File: audit/runner.py

Purpose: Execute multiple audit modules in parallel

Key Methods: - run() - Main entry point, runs all enabled modules - _run_module() - Execute single module with error handling - _aggregate_results() - Combine findings from all modules

Modules: 1. TestsModule - pytest coverage, failures, missing tests 2. SkillsModule - SKILL.md validation, required sections 3. CodeQualityModule - ruff, black, complexity 4. MCPModule - Server health, tool availability 5. DocsModule - README, docstrings, broken links 6. DependenciesModule - Outdated, security vulnerabilities

Parallelism:

if parallel:
    with ThreadPoolExecutor(max_workers=6) as executor:
        futures = [
            executor.submit(self._run_module, module)
            for module in modules
        ]
        results = [f.result() for f in futures]


Issue Creation Strategies

Individual Issues (Current)

File: scripts/create_audit_issues.py

Problem: Creates 300+ issues (one per finding)

Used by: audit/handlers.py:run_daily_audit()

subprocess.run([
    "python", "scripts/create_audit_issues.py",
    str(json_path), "--create",
    "--min-severity", severity_threshold,
])

Meta-Issues (New, Not Integrated)

File: scripts/create_meta_issues.py

Solution: Groups findings into 10-15 meta-issues with checklists

Not yet used by: Scheduler (needs integration)

Grouping Logic:

def group_findings_by_category(findings):
    groups = defaultdict(list)
    for finding in findings:
        title = finding["title"].lower()
        if "missing" in title and "purpose" in title:
            groups["Missing Purpose Sections"].append(finding)
        elif "missing" in title and "usage" in title:
            groups["Missing Usage Sections"].append(finding)
        # ... more categories
    return groups

Output Example:

## 📋 Bulk Audit Tracking: Missing Purpose Sections

**Total Items:** 73
**Severity:** HIGH

## ✅ Progress Tracker

- [ ] `auto-context-prime/SKILL.md`
- [ ] `brainstorming/SKILL.md`
- [ ] `coding-standards/SKILL.md`
... (70 more)


Auto-Fixer

File: audit/auto_fixer.py (not yet created, referenced in handlers)

Purpose: Automatically fix LOW/INFO severity issues

Fixers: - black - Code formatting (PEP 8) - isort - Import sorting - ruff --fix - Safe lint fixes only

Safety: - Only fixes LOW and INFO severity - Creates PR for human review - Never commits directly to main - Max 20 files per run (prevents huge PRs)

Flow: 1. Run audit to get fixable findings 2. Apply automated fixers (black, isort, ruff) 3. Create branch: auto-fix/audit-{date} 4. Commit changes 5. Create PR with: - Title: [Auto-fix] Audit findings - {date} - Labels: audit, auto-fix, low-priority - Body: List of fixes applied


GitHub Actions Automations

1. Stale PR Closer

File: scripts/close_stale_prs.py (339 lines) Workflow: .github/workflows/stale-pr-closer.yml Schedule: Weekly (Sunday 9 AM UTC)

Logic:

STALE_WARNING_DAYS = 21
STALE_CLOSE_DAYS = 30
STALE_LABEL = "stale"
KEEP_OPEN_LABEL = "keep-open"

def mark_stale(pr):
    if pr.days_inactive >= STALE_WARNING_DAYS:
        pr.add_label(STALE_LABEL)
        pr.comment("⚠️ Stale PR - will close in 9 days")

def close_stale(pr):
    if pr.days_inactive >= STALE_CLOSE_DAYS:
        if KEEP_OPEN_LABEL not in pr.labels:
            pr.close()
            pr.comment("🔒 Closed due to inactivity")

Tests: 22 comprehensive tests - Mocked PyGithub objects - Test all edge cases (keep-open label, draft PRs, etc.)


2. Auto-Merge Approved PRs

File: scripts/auto_merge_approved.py (526 lines) Workflow: .github/workflows/auto-merge.yml Trigger: pull_request_review (approved), check_suite (completed)

Safety Checks:

class AutoMergeService:
    def check_pr_ready_for_merge(self, pr):
        # 1. Has auto-merge label?
        if "auto-merge" not in pr.labels:
            return MergeDecision(can_merge=False, reason="No label")

        # 2. Has approvals?
        if approvals < 1:
            return MergeDecision(can_merge=False, reason="No approvals")

        # 3. No changes requested?
        if has_changes_requested:
            return MergeDecision(can_merge=False, reason="Changes requested")

        # 4. CI passing?
        if not ci_passing:
            return MergeDecision(can_merge=False, reason="CI failing")

        # 5. No merge conflicts?
        if pr.mergeable_state == "dirty":
            return MergeDecision(can_merge=False, reason="Conflicts")

        return MergeDecision(can_merge=True)

Merge Method: Squash (default)

Post-Merge: - Delete head branch - Post comment with merge info - Update project boards

Tests: 54 tests (48 unit + 6 integration)


3. Dependency Updates

Files: - scripts/dependency_updater.py (600+ lines) - .github/dependabot.yml - .github/workflows/dependency-updates.yml

Dual Strategy:

  1. Dependabot (patch updates)

    updates:
      - package-ecosystem: "pip"
        schedule:
          interval: "weekly"
        groups:
          patch:
            patterns: ["*"]
            update-types: ["patch"]
    

  2. Custom Updater (grouped minor/major)

    class DependencyChecker:
        def check_pip_outdated(self):
            result = subprocess.run(
                ["pip", "list", "--outdated", "--format=json"]
            )
            deps = json.loads(result.stdout)
    
            # Group by semver
            groups = {"patch": [], "minor": [], "major": []}
            for dep in deps:
                change_type = classify_semver(
                    dep["version"], dep["latest_version"]
                )
                groups[change_type].append(dep)
    
            # Create grouped PRs
            if groups["minor"]:
                create_pr(title="Update minor dependencies", deps=groups["minor"])
            if groups["major"]:
                create_pr(title="Update major dependencies", deps=groups["major"])
    

Benefits: - Patch: Auto-merged (safe) - Minor: Grouped PR (easier review) - Major: Separate PR (requires careful review)

Tests: 20 tests (pip + npm support)


4. Performance Regression Detection

File: scripts/performance_checker.py (547 lines) Workflow: .github/workflows/performance-check.yml Trigger: pull_request (opened, synchronize)

Benchmarks:

# tests/benchmarks/bench_orchestrator.py
def bench_spawn_agent():
    """Benchmark agent spawning."""
    start = time.time()
    orchestrator.spawn_agent("implementer", "task")
    duration = time.time() - start
    return {"duration": duration, "metric": "agent_spawn"}

# tests/benchmarks/bench_tools.py
def bench_kb_search():
    """Benchmark KB semantic search."""
    start = time.time()
    kb_client.search("authentication patterns", limit=10)
    duration = time.time() - start
    return {"duration": duration, "metric": "kb_search"}

Comparison Logic:

class PerformanceChecker:
    THRESHOLD_WARNING = 10.0   # % slower
    THRESHOLD_ERROR = 25.0     # % slower

    def compare_results(self, pr_results, baseline_results):
        results = []
        for metric in pr_results:
            baseline = baseline_results[metric]
            pr_value = pr_results[metric]

            change_pct = (pr_value - baseline) / baseline * 100

            if change_pct > self.THRESHOLD_ERROR:
                status = "CRITICAL"
            elif change_pct > self.THRESHOLD_WARNING:
                status = "WARNING"
            else:
                status = "PASS"

            results.append({
                "metric": metric,
                "baseline": baseline,
                "pr_value": pr_value,
                "change_pct": change_pct,
                "status": status,
            })

        return results

Artifacts: - baseline_results.json - From main branch - pr_results.json - From PR branch - comparison_report.json - Detailed comparison

Tests: 20 tests


5. Dead Code Elimination

File: scripts/dead_code_detector.py (670 lines) Workflow: .github/workflows/dead-code-check.yml Schedule: Weekly (Sunday 2 AM UTC)

Tool: Vulture (static analysis for Python)

False Positive Filtering:

class DeadCodeDetector:
    PUBLIC_API_PATTERNS = [
        r"^__init__\.py$",      # Module exports
        r"test_.*\.py$",         # Test files
        r"conftest\.py$",        # Pytest fixtures
    ]

    CALLBACK_PATTERNS = [
        r"^on_",                 # Event handlers
        r"^handle_",             # Handlers
        r"^_.*_callback$",       # Callbacks
        r"^setup_",              # Setup functions
        r"^teardown_",           # Teardown functions
    ]

    def is_false_positive(self, finding):
        # 1. Check whitelist
        if finding.file in self.whitelist:
            return True

        # 2. Check public API
        if any(re.match(p, finding.file) for p in self.PUBLIC_API_PATTERNS):
            return True

        # 3. Check callbacks
        if any(re.match(p, finding.name) for p in self.CALLBACK_PATTERNS):
            return True

        # 4. Check __all__ exports
        if finding.name in self.get_module_exports(finding.file):
            return True

        return False

Whitelist: .vulture-whitelist.py

# Known false positives (used dynamically)
orchestrator_main  # CLI entry point
skill_handler      # Loaded by plugin system
mcp_server_tool    # Registered via decorator

Confidence Threshold: 80% (adjustable)

Tests: 25 tests


6. ML Bug Prediction

Files: - src/orchestrator/ml/bug_predictor.py (592 lines) - scripts/train_bug_model.py (155 lines) - scripts/predict_pr_bugs.py (104 lines) - .github/workflows/bug-prediction.yml

Trigger: pull_request (opened, synchronize)

16 ML Features:

Category Features Description
Complexity cyclomatic_complexity McCabe complexity
cognitive_complexity Human readability
max_nesting_depth Deepest nesting
Size files_changed Number of files
lines_added Added lines
lines_deleted Deleted lines
net_lines Added - deleted
Testing coverage_delta Coverage change
tests_added New tests
tests_modified Modified tests
Author author_pr_count Total PRs
author_bug_rate Historical bugs
time_since_last_pr Days since last
Metadata has_breaking_changes Breaking tag
modifies_critical_files Core files
is_weekend_commit Sat/Sun

Training:

# scripts/train_bug_model.py
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler

# Load historical PRs from GitHub
prs = github.get_repo().get_pulls(state="closed")

# Extract features
X = []
y = []
for pr in prs:
    features = FeatureExtractor.extract_from_pr(pr)
    X.append(features)

    # Label: has linked bug issues?
    has_bugs = any("bug" in issue.labels for issue in pr.linked_issues)
    y.append(1 if has_bugs else 0)

# Train model
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

model = RandomForestClassifier(n_estimators=100, max_depth=10)
model.fit(X_scaled, y)

# Save
joblib.dump(model, "models/bug_predictor_v1.pkl")
joblib.dump(scaler, "models/scaler_v1.pkl")

Prediction:

# scripts/predict_pr_bugs.py
class BugPredictor:
    HIGH_RISK_THRESHOLD = 0.70
    MEDIUM_RISK_THRESHOLD = 0.40

    def predict(self, pr_data):
        features = FeatureExtractor.extract_from_pr(pr_data)
        X = self.scaler.transform([features])

        bug_probability = self.model.predict_proba(X)[0][1]

        if bug_probability >= self.HIGH_RISK_THRESHOLD:
            risk_level = "HIGH"
            recommendations = [
                "Add comprehensive unit tests",
                "Request code review from senior engineer",
                "Consider breaking into smaller PRs",
            ]
        elif bug_probability >= self.MEDIUM_RISK_THRESHOLD:
            risk_level = "MEDIUM"
            recommendations = [
                "Add edge case tests",
                "Review error handling",
            ]
        else:
            risk_level = "LOW"
            recommendations = ["Looks good! 🎉"]

        return BugPrediction(
            probability=bug_probability,
            risk_level=risk_level,
            recommendations=recommendations,
            features_used=features,
        )

Comment Example:

## 🤖 ML Bug Prediction

**Risk Level:** 🔴 **HIGH** (72%)

**Recommendations:**
- Add comprehensive unit tests (currently 0 tests added)
- Request code review from senior engineer
- Consider breaking into smaller PRs (15 files changed)

**Contributing Factors:**
- High cyclomatic complexity (avg: 12.3)
- Large PR (15 files, +450 lines)
- Modifies critical files: `orchestrator.py`, `scheduler.py`
- No tests added

**Feature Importance:**
- Cyclomatic complexity: 0.23
- Files changed: 0.19
- Tests added: 0.18

Tests: 34 tests (24 unit + 7 integration + 3 e2e)


Code Walkthrough

Complete Scheduler Flow

Let me walk through exactly what happens when the scheduler runs:

Step 1: Scheduler Starts

File: audit/scheduler.py:48-89

def main() -> None:
    """Main entry point for audit scheduler."""
    global _daemon_instance

    logger.info("Starting audit scheduler daemon")

    # 1. Load YAML configuration
    config_file = Path(__file__).parent / "SCHEDULER_CONFIG.yml"
    if not config_file.exists():
        logger.error(f"Configuration file not found: {config_file}")
        sys.exit(1)

    # 2. Initialize scheduler daemon with APScheduler
    config_loader = ScheduleLoader(config_file)
    daemon = ScheduleDaemon(config_loader, reload_interval=30)
    _daemon_instance = daemon

    # 3. Start daemon (registers cron jobs)
    try:
        daemon.start()
        logger.info("Audit scheduler started successfully")
        logger.info("Press Ctrl+C to stop")

        # 4. Keep running (daemon loop)
        while True:
            time.sleep(1)

    except KeyboardInterrupt:
        logger.info("Received interrupt signal, shutting down...")
        daemon.stop()
        logger.info("Audit scheduler stopped")

What daemon.start() does (src/orchestrator/scheduler/daemon.py):

class ScheduleDaemon:
    def start(self):
        """Start the scheduler and register all jobs."""
        if self.is_running:
            logger.warning("Scheduler already running")
            return

        # 1. Load schedules from YAML
        schedules = self.config_loader.load_schedules()

        # 2. Register each schedule as APScheduler job
        for schedule in schedules:
            if not schedule.enabled:
                continue

            # Parse cron expression
            trigger = CronTrigger.from_crontab(schedule.schedule)

            # Import handler function
            module_name, func_name = schedule.handler.rsplit(".", 1)
            module = importlib.import_module(module_name)
            handler_func = getattr(module, func_name)

            # Register job
            self.scheduler.add_job(
                func=handler_func,
                trigger=trigger,
                args=schedule.args,
                kwargs=schedule.kwargs,
                id=schedule.name,
                max_instances=1,
                misfire_grace_time=300,  # 5 min grace period
            )

            logger.info(f"Registered job: {schedule.name} ({schedule.schedule})")

        # 3. Start APScheduler
        self.scheduler.start()
        self._running = True

        logger.info(f"Scheduler started with {len(schedules)} jobs")

Step 2: Cron Trigger (9 AM UTC)

APScheduler fires the job at 9 AM UTC:

# Inside APScheduler internals
job = self.get_job("daily_full_audit")
job.func(*job.args, **job.kwargs)  # Calls audit.handlers.run_daily_audit()

Step 3: Audit Handler Execution

File: audit/handlers.py:43-146

def run_daily_audit(
    mode: str = "daily",
    create_issues: bool = True,
    send_notifications: bool = True,
    severity_threshold: str = "medium",
) -> dict[str, Any]:
    """Run daily full audit with all 6 modules."""

    logger.info(f"Starting {mode} audit")
    project_root = Path.cwd()

    # 1. Create audit runner
    import asyncio
    runner = BatchAuditRunner(
        project_root=project_root,
        mode=mode,
        parallel=True,              # ← Run modules in parallel
        severity_filter=None,       # ← Get all findings
    )

    # 2. Run audit (blocking call)
    report = asyncio.run(runner.run())

    # 3. Save reports to disk
    output_dir = project_root / "audit" / "reports"
    output_dir.mkdir(parents=True, exist_ok=True)

    timestamp_str = report.timestamp.strftime("%Y%m%d_%H%M%S")
    json_path = output_dir / f"audit_{mode}_{timestamp_str}.json"
    md_path = output_dir / f"audit_{mode}_{timestamp_str}.md"

    JSONFormatter.save(report, json_path)
    MarkdownFormatter.save(report, md_path)

    logger.info(f"Reports saved: {json_path}, {md_path}")

    # 4. Create GitHub issues (CURRENT: individual issues)
    issues_created = 0
    if create_issues:
        try:
            result = subprocess.run(
                [
                    "python",
                    "scripts/create_audit_issues.py",  # ← OLD SCRIPT
                    str(json_path),
                    "--create",
                    "--min-severity",
                    severity_threshold,
                ],
                capture_output=True,
                text=True,
                timeout=300,
            )

            if result.returncode == 0:
                logger.info("GitHub issues created successfully")
                # Parse output to count issues
                for line in result.stdout.split("\n"):
                    if "Issues to create:" in line:
                        issues_created = int(line.split(":")[-1].strip())
            else:
                logger.error(f"Failed to create issues: {result.stderr}")
        except Exception as e:
            logger.error(f"Error creating issues: {e}")

    # 5. Send notifications
    if send_notifications and report.total_findings > 0:
        try:
            from audit.notifier import send_notification

            critical_count = report.findings_by_severity.get("critical", 0)
            high_count = report.findings_by_severity.get("high", 0)

            if critical_count > 0 or high_count > 0:
                send_notification(
                    title=f"Audit {mode}: {critical_count} CRITICAL, {high_count} HIGH",
                    message=f"Total findings: {report.total_findings}\n"
                            f"Report: {md_path}",
                    severity="critical" if critical_count > 0 else "high",
                )
        except Exception as e:
            logger.error(f"Error sending notification: {e}")

    # 6. Return summary
    return {
        "success": True,
        "mode": mode,
        "total_findings": report.total_findings,
        "findings_by_severity": report.findings_by_severity,
        "issues_created": issues_created,
        "reports": {
            "json": str(json_path),
            "markdown": str(md_path),
        },
    }

Step 4: Batch Audit Runner

File: audit/runner.py

class BatchAuditRunner:
    def __init__(
        self,
        project_root: Path,
        mode: str = "daily",
        parallel: bool = True,
        severity_filter: str | None = None,
    ):
        self.project_root = project_root
        self.mode = mode
        self.parallel = parallel
        self.severity_filter = severity_filter

        # Initialize all 6 modules
        self.modules = [
            TestsModule(project_root),
            SkillsModule(project_root),
            CodeQualityModule(project_root),
            MCPModule(project_root),
            DocsModule(project_root),
            DependenciesModule(project_root),
        ]

    async def run(self) -> AuditReport:
        """Run all modules and aggregate results."""
        logger.info(f"Running {self.mode} audit (parallel={self.parallel})")

        start_time = time.time()

        # 1. Run modules
        if self.parallel:
            results = await self._run_parallel()
        else:
            results = await self._run_sequential()

        # 2. Aggregate findings
        all_findings = []
        module_results = {}

        for module, findings in results.items():
            module_results[module] = {
                "count": len(findings),
                "findings": findings,
            }
            all_findings.extend(findings)

        # 3. Filter by severity
        if self.severity_filter:
            all_findings = [
                f for f in all_findings
                if f.severity.value >= FindingSeverity[self.severity_filter.upper()].value
            ]

        # 4. Group by severity
        by_severity = defaultdict(int)
        for finding in all_findings:
            by_severity[finding.severity.name.lower()] += 1

        # 5. Create report
        report = AuditReport(
            timestamp=datetime.now(),
            mode=self.mode,
            total_findings=len(all_findings),
            findings_by_severity=dict(by_severity),
            modules=module_results,
            duration=time.time() - start_time,
        )

        logger.info(f"Audit complete: {len(all_findings)} findings in {report.duration:.2f}s")

        return report

    async def _run_parallel(self) -> dict:
        """Run modules in parallel using ThreadPoolExecutor."""
        results = {}

        with ThreadPoolExecutor(max_workers=6) as executor:
            # Submit all module tasks
            futures = {
                executor.submit(self._run_module, module): module
                for module in self.modules
            }

            # Collect results as they complete
            for future in as_completed(futures):
                module = futures[future]
                try:
                    findings = future.result()
                    results[module.name] = findings
                except Exception as e:
                    logger.error(f"Module {module.name} failed: {e}")
                    results[module.name] = []

        return results

    def _run_module(self, module) -> list:
        """Execute a single audit module."""
        logger.info(f"Running module: {module.name}")

        try:
            findings = module.run()
            logger.info(f"Module {module.name}: {len(findings)} findings")
            return findings
        except Exception as e:
            logger.error(f"Module {module.name} error: {e}", exc_info=True)
            return []

Step 5: Module Execution Example

File: audit/modules/skills.py

class SkillsModule(AuditModule):
    """Audit SKILL.md files for completeness."""

    REQUIRED_SECTIONS = [
        "Purpose",
        "Usage",
        "Examples",
    ]

    def run(self) -> list[Finding]:
        """Check all SKILL.md files."""
        findings = []

        # 1. Find all SKILL.md files
        skill_files = list(self.project_root.glob("skills/*/SKILL.md"))
        skill_files.extend(self.project_root.glob(".claude/skills/*/SKILL.md"))

        logger.info(f"Found {len(skill_files)} skill files")

        # 2. Check each file
        for skill_file in skill_files:
            content = skill_file.read_text()

            # 3. Check for required sections
            for section in self.REQUIRED_SECTIONS:
                if f"## {section}" not in content:
                    findings.append(Finding(
                        title=f"Missing required section: '{section}'",
                        description=f"SKILL.md is missing the '{section}' section",
                        severity=FindingSeverity.HIGH,
                        category="documentation",
                        file=str(skill_file),
                        line=None,
                        recommendation=f"Add a '## {section}' section to the file",
                    ))

            # 4. Check for empty sections
            for section in self.REQUIRED_SECTIONS:
                pattern = rf"## {section}\s*\n\s*##"
                if re.search(pattern, content):
                    findings.append(Finding(
                        title=f"Empty section: '{section}'",
                        description=f"The '{section}' section is empty",
                        severity=FindingSeverity.MEDIUM,
                        category="documentation",
                        file=str(skill_file),
                        recommendation=f"Add content to the '{section}' section",
                    ))

        return findings

Step 6: Issue Creation (Current)

File: scripts/create_audit_issues.py

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("report", type=Path)
    parser.add_argument("--create", action="store_true")
    parser.add_argument("--min-severity", default="medium")
    args = parser.parse_args()

    # 1. Load audit report
    with open(args.report) as f:
        report = json.load(f)

    # 2. Extract findings
    all_findings = []
    for module in report["modules"].values():
        all_findings.extend(module["findings"])

    # 3. Filter by severity
    severity_order = ["critical", "high", "medium", "low", "info"]
    min_index = severity_order.index(args.min_severity)

    filtered = [
        f for f in all_findings
        if severity_order.index(f["severity"].lower()) <= min_index
    ]

    print(f"Issues to create: {len(filtered)}")

    # 4. Create individual issues (PROBLEM: too many!)
    if args.create:
        for finding in filtered:
            title = f"[{finding['severity'].upper()}] {finding['title']}"
            body = f"""## Description
{finding['description']}

## File
`{finding['file']}`

## Recommendation
{finding['recommendation']}

## Severity
{finding['severity']}

## Category
{finding['category']}

---
Auto-generated by audit on {datetime.now()}
"""

            subprocess.run([
                "gh", "issue", "create",
                "--title", title,
                "--body", body,
                "--label", finding["category"],
                "--label", f"severity-{finding['severity']}",
            ])

            print(f"Created issue: {title}")

Problem: This creates 300+ issues!

Step 7: Issue Creation (New - Meta-Issues)

File: scripts/create_meta_issues.py:68-209

def generate_meta_issue_body(
    category: str,
    findings: list[dict],
    report_path: Path,
    max_items: int = 100
) -> str:
    """Generate markdown body for a meta-issue."""

    severity = findings[0].get("severity", "HIGH").upper()

    body = f"""## 📋 Bulk Audit Tracking: {category}

**Severity:** {severity}
**Total Items:** {len(findings)}
**Auto-generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
**Source Report:** `{report_path.name}`

---

## 🎯 Overview

This meta-issue tracks all audit findings related to **{category.lower()}**.
Each item in the checklist below represents a specific file that needs attention.
Check off items as they are completed.

---

## ✅ Progress Tracker

"""

    # Add checklist (GitHub renders with progress bar)
    limited_findings = findings[:max_items]
    for finding in limited_findings:
        file_path = finding.get("file", "Unknown")

        # Extract parent_dir/file.md for readability
        if file_path != "Unknown":
            file_name = Path(file_path).name
            parent_dir = Path(file_path).parent.name
            display_name = f"`{parent_dir}/{file_name}`"
        else:
            display_name = file_path

        body += f"- [ ] {display_name}\n"

    if len(findings) > max_items:
        remaining = len(findings) - max_items
        body += f"\n... and {remaining} more items (see full report)\n"

    body += """

---

## 🔧 How to Fix

1. **Pick an item** from the checklist above
2. **Open the file** in your editor
3. **Make the required changes**
4. **Check off the item** (edit: `- [ ]` → `- [x]`)
5. **No PR needed** for docs (edit directly on `dev`)

## 📖 Recommendation

"""

    # Category-specific recommendations
    if "Purpose" in category:
        body += """Add a `## Purpose` section:

```markdown
## Purpose

Explain what this skill does and why it exists. 1-2 sentences.
""" elif "Usage" in category: body += """Add a ## Usage section:

## Usage

Explain when and how to use this skill.
""" elif "Examples" in category: body += """Add an ## Examples section:

## Examples

### Example 1: [Scenario]
\\```
Description
\\```
"""

return body

def create_meta_issue( title: str, body: str, labels: list[str], dry_run: bool = True ) -> str | None: """Create a GitHub meta-issue via gh CLI."""

if dry_run:
    print(f"[DRY RUN] Would create: {title}")
    return None

try:
    cmd = ["gh", "issue", "create", "--title", title, "--body", body]
    for label in labels:
        cmd.extend(["--label", label])

    result = subprocess.run(cmd, capture_output=True, text=True, check=True)

    issue_url = result.stdout.strip()
    print(f"✅ Created: {title} → {issue_url}")
    return issue_url

except subprocess.CalledProcessError as e:
    print(f"❌ Failed: {title}\n   {e.stderr}")
    return None

``` Benefits: - 73 individual issues → 1 meta-issue with 73 checkboxes - GitHub shows progress bar automatically - Easy to see what's done at a glance - Can assign multiple people to same meta-issue


Data Flow

Complete Audit → Issue → Fix → Merge Flow

mermaid graph TB subgraph "Phase 1: Detection" A[Scheduler: 9 AM UTC] --> B[BatchAuditRunner] B --> C1[Tests Module] B --> C2[Skills Module] B --> C3[Code Quality] B --> C4[MCP Module] B --> C5[Docs Module] B --> C6[Dependencies] C1 --> D[Aggregate Results] C2 --> D C3 --> D C4 --> D C5 --> D C6 --> D D --> E[Save Report<br/>JSON + Markdown] end subgraph "Phase 2: Triage" E --> F{create_issues?} F -->|Yes| G[create_meta_issues.py] G --> H[Group by Category] H --> I[Meta-Issue #485<br/>Purpose 73 items] H --> J[Meta-Issue #486<br/>Examples 75 items] H --> K[Meta-Issue #487<br/>Usage 73 items] end subgraph "Phase 3: Auto-Fix" E --> L{auto_fix?} L -->|Yes| M[AutoFixer] M --> N[Apply: black, isort, ruff] N --> O[Create PR<br/>auto-fix/audit-DATE] end subgraph "Phase 4: Human Work" I --> P[Developer checks off items] J --> P K --> P O --> Q[Developer reviews PR] end subgraph "Phase 5: Quality Gates" P --> R[Create PR] Q --> R R --> S[GitHub Actions] S --> T[performance-check.yml] S --> U[bug-prediction.yml] T --> V{Regression?} U --> W{High risk?} V -->|No| X[auto-merge.yml] W -->|No| X X --> Y{Approved + CI pass?} Y -->|Yes| Z[Auto-Merge] end Z --> AA[Delete Branch] AA --> AB[Update Meta-Issue] style A fill:#e1f5ff style G fill:#fff4e1 style M fill:#ffe1e1 style Z fill:#90EE90


Integration Points

How Everything Connects

graph TB
    subgraph "Audit System"
        A[APScheduler Daemon<br/>audit/scheduler.py]
        B[Handlers<br/>audit/handlers.py]
        C[BatchAuditRunner<br/>audit/runner.py]
        D[6 Modules<br/>audit/modules/]
    end

    subgraph "Issue Management"
        E[Meta-Issue Creator<br/>scripts/create_meta_issues.py]
        F[Individual Issue Creator<br/>scripts/create_audit_issues.py]
        G[Batch Fixer<br/>scripts/fix_skill_docs_batched.py]
    end

    subgraph "GitHub Actions"
        H[Stale PR Closer<br/>.github/workflows/stale-pr-closer.yml]
        I[Auto-Merge<br/>.github/workflows/auto-merge.yml]
        J[Performance Check<br/>.github/workflows/performance-check.yml]
        K[Bug Prediction<br/>.github/workflows/bug-prediction.yml]
        L[Dead Code Scan<br/>.github/workflows/dead-code-check.yml]
        M[Dependency Updates<br/>.github/workflows/dependency-updates.yml]
    end

    subgraph "Scripts"
        N[close_stale_prs.py]
        O[auto_merge_approved.py]
        P[performance_checker.py]
        Q[predict_pr_bugs.py]
        R[dead_code_detector.py]
        S[dependency_updater.py]
    end

    subgraph "ML System"
        T[Bug Predictor<br/>src/orchestrator/ml/bug_predictor.py]
        U[Model Training<br/>scripts/train_bug_model.py]
        V[Trained Model<br/>models/bug_predictor_v1.pkl]
    end

    A --> B
    B --> C
    C --> D

    D --> E
    D --> F
    E --> G

    B -.->|calls| F
    B -.->|should call| E

    H --> N
    I --> O
    J --> P
    K --> Q
    L --> R
    M --> S

    Q --> T
    U --> V
    T --> V

    style A fill:#e1f5ff
    style E fill:#fff4e1
    style H fill:#ffe1e1
    style T fill:#e1ffe1

Data Format Flow

graph LR
    A[Python Objects<br/>Finding, AuditReport] --> B[JSON Formatter<br/>JSONFormatter.save]
    B --> C[audit_daily_20251221.json]

    A --> D[Markdown Formatter<br/>MarkdownFormatter.save]
    D --> E[audit_daily_20251221.md]

    C --> F[create_meta_issues.py]
    F --> G[GitHub API<br/>gh issue create]
    G --> H[Meta-Issue #485]

    C --> I[fix_skill_docs_batched.py]
    I --> J[Git Commits<br/>batches of 15]

    E --> K[Email Notifier<br/>send_email_report]
    K --> L[Stakeholders]

    style A fill:#e1f5ff
    style C fill:#fff4e1
    style H fill:#90EE90

Summary

What We Built

  1. Audit Layer (Detection)
  2. Scheduler daemon with APScheduler
  3. 6 parallel audit modules
  4. JSON + Markdown reports

  5. Automation Layer (Fixing)

  6. Meta-issue batching (10-15 issues vs 300+)
  7. Auto-fixer for LOW severity
  8. Batched doc fixes (15 files per commit)

  9. Quality Gate Layer (Prevention)

  10. Stale PR auto-closer (21-day warning, 30-day close)
  11. Auto-merge with safety checks
  12. Performance regression detection (10% warn, 25% block)
  13. Dead code elimination (Vulture + false positive filtering)
  14. ML bug prediction (16 features, Random Forest)
  15. Dependency updates (Dependabot + custom grouping)

Integration Status

Complete: - All 6 automation implementations (#166-171) - Comprehensive test suites (175+ tests total) - Documentation for each component

⚠️ Pending: - Wire GitHub Actions workflows to .github/workflows/ - Integrate meta-issue creator into scheduler - Train initial ML bug prediction model - Re-run failed agent #164 (CD Pipeline)

Next Steps

  1. Update audit/handlers.py:91-100 to call create_meta_issues.py
  2. Copy workflow YAML files to .github/workflows/
  3. Train ML model: python scripts/train_bug_model.py
  4. Enable workflows in SCHEDULER_CONFIG.yml
  5. Test end-to-end flow

Total Lines of Code: ~4,500 lines Test Coverage: 90%+ across all modules Automation Impact: 300+ manual issues → 10-15 trackable meta-issues