scripts/doctor.ps1

#!/usr/bin/env pwsh
<#
.SYNOPSIS
    Scan a deployed .bot project for health issues.

.DESCRIPTION
    Checks output hygiene, settings integrity, stale locks, orphaned worktrees,
    dependencies, task queue health, and theme config. Designed to run from the
    dotbot CLI: `dotbot doctor`

.PARAMETER BotRoot
    Path to the .bot directory (default: ./.bot)
#>

param(
    [string]$BotRoot = (Join-Path (Get-Location) ".bot")
)

$ErrorActionPreference = "Continue"

# Import platform functions for themed output
$PlatformFunctionsModule = Join-Path $PSScriptRoot "Platform-Functions.psm1"
if (-not (Test-Path $PlatformFunctionsModule)) {
    Write-Error "Required module not found: $PlatformFunctionsModule"
    exit 1
}
Import-Module $PlatformFunctionsModule -Force -ErrorAction Stop

# Counters
$passes  = 0
$warns   = 0
$errors  = 0

function Write-Check {
    param([string]$Label, [string]$Result, [ValidateSet('Pass','Warn','Fail')]$Status)
    switch ($Status) {
        'Pass' {
            $script:passes++
            Write-Success "$Label — $Result"
        }
        'Warn' {
            $script:warns++
            Write-DotbotWarning "$Label — $Result"
        }
        'Fail' {
            $script:errors++
            Write-DotbotError "$Label — $Result"
        }
    }
}

# ═══════════════════════════════════════════════════════════════════
# HEADER
# ═══════════════════════════════════════════════════════════════════

$ver = $env:DOTBOT_VERSION ?? 'unknown'
Write-DotbotBanner -Title "D O T B O T D O C T O R v$ver" -Subtitle "Project: $(Split-Path (Split-Path $BotRoot -Parent) -Leaf)"

if (-not (Test-Path $BotRoot)) {
    Write-DotbotError ".bot directory not found at: $BotRoot"
    Write-DotbotWarning "Run 'dotbot init' first."
    exit 2
}

# ═══════════════════════════════════════════════════════════════════
# 1. DEPENDENCIES
# ═══════════════════════════════════════════════════════════════════

Write-DotbotSection -Title "DEPENDENCIES"

# git
if (Get-Command git -ErrorAction SilentlyContinue) {
    Write-Check "git" "found" Pass
} else {
    Write-Check "git" "not found on PATH" Fail
}

# Provider CLI (claude or openai)
$providerFound = $false
foreach ($exe in @('claude', 'claude.exe', 'openai')) {
    if (Get-Command $exe -ErrorAction SilentlyContinue) {
        Write-Check "Provider CLI" "$exe found" Pass
        $providerFound = $true
        break
    }
}
if (-not $providerFound) {
    Write-Check "Provider CLI" "no provider CLI found (claude/openai)" Fail
}

# powershell-yaml
if (Get-Module -ListAvailable powershell-yaml -ErrorAction SilentlyContinue) {
    Write-Check "powershell-yaml" "installed" Pass
} else {
    Write-Check "powershell-yaml" "missing — Install-Module powershell-yaml -Scope CurrentUser" Warn
}

Write-BlankLine

# ═══════════════════════════════════════════════════════════════════
# 2. SETTINGS INTEGRITY
# ═══════════════════════════════════════════════════════════════════

Write-DotbotSection -Title "SETTINGS"

$settingsPath = Join-Path $BotRoot "settings\settings.default.json"
if (Test-Path $settingsPath) {
    try {
        $settings = Get-Content $settingsPath -Raw | ConvertFrom-Json
        if ($settings.execution -and $settings.analysis) {
            Write-Check "settings.default.json" "valid, has execution + analysis" Pass
        } else {
            Write-Check "settings.default.json" "missing execution or analysis keys" Warn
        }
    } catch {
        Write-Check "settings.default.json" "invalid JSON: $_" Fail
    }
} else {
    Write-Check "settings.default.json" "not found" Fail
}

# Theme config
$themeDefault = Join-Path $BotRoot "settings\theme.default.json"
if (Test-Path $themeDefault) {
    try {
        Get-Content $themeDefault -Raw | ConvertFrom-Json | Out-Null
        Write-Check "theme.default.json" "valid" Pass
    } catch {
        Write-Check "theme.default.json" "invalid JSON" Warn
    }
} else {
    Write-Check "theme.default.json" "not found (will use fallback)" Warn
}

Write-BlankLine

# ═══════════════════════════════════════════════════════════════════
# 3. STALE PROCESS LOCKS
# ═══════════════════════════════════════════════════════════════════

Write-DotbotSection -Title "PROCESS LOCKS"

$controlDir = Join-Path $BotRoot ".control"
$lockFiles = @()
if (Test-Path $controlDir) {
    $lockFiles = @(Get-ChildItem $controlDir -Filter "launch-*.lock" -File -ErrorAction SilentlyContinue)
}

if ($lockFiles.Count -eq 0) {
    Write-Check "Process locks" "none (clean)" Pass
} else {
    $staleCount = 0
    foreach ($lf in $lockFiles) {
        $pidStr = (Get-Content $lf.FullName -Raw -ErrorAction SilentlyContinue)?.Trim()
        if ($pidStr -and $pidStr -match '^\d+$') {
            try {
                Get-Process -Id ([int]$pidStr) -ErrorAction Stop | Out-Null
                # Process still running — OK
            } catch {
                $staleCount++
                Write-Check "Lock: $($lf.Name)" "PID $pidStr no longer running (stale)" Warn
            }
        }
    }
    if ($staleCount -eq 0) {
        Write-Check "Process locks" "$($lockFiles.Count) active (all PIDs alive)" Pass
    }
}

Write-BlankLine

# ═══════════════════════════════════════════════════════════════════
# 4. ORPHANED WORKTREES
# ═══════════════════════════════════════════════════════════════════

Write-DotbotSection -Title "WORKTREES"

$wtMapPath = Join-Path $controlDir "worktree-map.json"
if (Test-Path $wtMapPath) {
    try {
        $wtMap = Get-Content $wtMapPath -Raw | ConvertFrom-Json
        $orphaned = 0
        foreach ($prop in $wtMap.PSObject.Properties) {
            $wtPath = $prop.Value.worktree_path
            if ($wtPath -and -not (Test-Path $wtPath)) {
                $orphaned++
                Write-Check "Worktree: $($prop.Name)" "path missing: $wtPath" Warn
            }
        }
        if ($orphaned -eq 0) {
            $total = @($wtMap.PSObject.Properties).Count
            Write-Check "Worktree map" "$total entries, all paths valid" Pass
        }
    } catch {
        Write-Check "worktree-map.json" "invalid JSON" Warn
    }
} else {
    Write-Check "Worktree map" "no map file (clean)" Pass
}

Write-BlankLine

# ═══════════════════════════════════════════════════════════════════
# 5. TASK QUEUE HEALTH
# ═══════════════════════════════════════════════════════════════════

Write-DotbotSection -Title "TASK QUEUE"

$tasksDir = Join-Path $BotRoot "workspace\tasks"
if (Test-Path $tasksDir) {
    $badJson = 0
    $missingId = 0
    $totalTasks = 0
    foreach ($dir in (Get-ChildItem $tasksDir -Directory -ErrorAction SilentlyContinue)) {
        foreach ($f in (Get-ChildItem $dir.FullName -Filter "*.json" -File -ErrorAction SilentlyContinue)) {
            if ($f.Name.StartsWith('_')) { continue }
            $totalTasks++
            try {
                $task = Get-Content $f.FullName -Raw | ConvertFrom-Json
                if (-not $task.id -or -not $task.name) { $missingId++ }
            } catch {
                $badJson++
            }
        }
    }
    if ($badJson -gt 0) {
        Write-Check "Task files" "$badJson files with invalid JSON" Fail
    }
    if ($missingId -gt 0) {
        Write-Check "Task files" "$missingId files missing id or name" Warn
    }
    if ($badJson -eq 0 -and $missingId -eq 0) {
        Write-Check "Task queue" "$totalTasks tasks, all valid" Pass
    }
} else {
    Write-Check "Task queue" "no tasks directory" Pass
}

Write-BlankLine

# ═══════════════════════════════════════════════════════════════════
# 6. OUTPUT HYGIENE
# ═══════════════════════════════════════════════════════════════════

Write-DotbotSection -Title "OUTPUT HYGIENE"

$scanDirs = @()
$scriptsDir = Join-Path $BotRoot "scripts"
if (Test-Path $scriptsDir) { $scanDirs += $scriptsDir }
$wfDir = Join-Path $BotRoot "workflows"
if (Test-Path $wfDir) {
    Get-ChildItem $wfDir -Directory | ForEach-Object {
        $wfScripts = Join-Path $_.FullName "scripts"
        if (Test-Path $wfScripts) { $scanDirs += $wfScripts }
    }
}

$writeHostCount = 0
$consoleErrorCount = 0
$findings = @()

foreach ($dir in $scanDirs) {
    Get-ChildItem $dir -Filter "*.ps1" -File -Recurse | ForEach-Object {
        $relPath = $_.FullName.Replace($BotRoot, '.bot')
        $lineNum = 0
        Get-Content $_.FullName | ForEach-Object {
            $lineNum++
            if ($_ -match '^\s*Write-Host\s' -and $_ -notmatch '#.*Write-Host') {
                $writeHostCount++
                if ($findings.Count -lt 10) {
                    $findings += "$relPath`:$lineNum Write-Host"
                }
            }
            if ($_ -match '\[Console\]::Error\.Write') {
                $consoleErrorCount++
                if ($findings.Count -lt 10) {
                    $findings += "$relPath`:$lineNum [Console]::Error.Write"
                }
            }
        }
    }
}

if ($writeHostCount -eq 0 -and $consoleErrorCount -eq 0) {
    Write-Check "Output hygiene" "all scripts use themed output" Pass
} else {
    if ($writeHostCount -gt 0) {
        Write-Check "Write-Host usage" "$writeHostCount occurrence(s) — use Write-BotLog or theme helpers instead" Warn
    }
    if ($consoleErrorCount -gt 0) {
        Write-Check "Console.Error traces" "$consoleErrorCount occurrence(s) — remove debug traces" Warn
    }
    foreach ($f in $findings) {
        Write-DotbotCommand "$f"
    }
}

Write-BlankLine

# ═══════════════════════════════════════════════════════════════════
# SUMMARY
# ═══════════════════════════════════════════════════════════════════

Write-DotbotCommand "────────────────────────────────────────────"
Write-BlankLine
$summary = "$passes passed, $warns warnings, $errors errors"
if ($errors -gt 0) {
    Write-DotbotError $summary
} elseif ($warns -gt 0) {
    Write-DotbotWarning $summary
} else {
    Write-Success $summary
}
Write-BlankLine

if ($errors -gt 0) { exit 2 }
elseif ($warns -gt 0) { exit 1 }
else { exit 0 }