Files
bootstrap/bin/mosaic-doctor.ps1
Jason Woltje 80c3680ccb feat: rename rails/ to tools/ and add service tool suites
Rename the `rails/` directory to `tools/` for agent discoverability —
agents frequently failed to locate helper scripts due to the non-intuitive
directory name. Add backward-compat symlink `rails/ → tools/`.

New tool suites:
- Authentik: auth-token, user-list, user-create, group-list, app-list,
  flow-list, admin-status (8 scripts)
- Coolify: team-list, project-list, service-list, service-status, deploy,
  env-set (7 scripts)
- Woodpecker: pipeline-list, pipeline-status, pipeline-trigger (3 stubs)
- GLPI: session-init, computer-list, ticket-list, ticket-create, user-list
  (6 scripts)
- Health: stack-health.sh — stack-wide connectivity check

Infrastructure:
- Shared credential loader at tools/_lib/credentials.sh
- install.sh creates symlink + chmod on tool scripts
- All ~253 rails/ path references updated across 68+ files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 11:51:39 -06:00

284 lines
10 KiB
PowerShell

# mosaic-doctor.ps1
# Audits Mosaic runtime state and detects drift across agent runtimes.
# PowerShell equivalent of mosaic-doctor (bash).
$ErrorActionPreference = "Stop"
param(
[switch]$FailOnWarn,
[switch]$Verbose,
[switch]$Help
)
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
if ($Help) {
Write-Host @"
Usage: mosaic-doctor.ps1 [-FailOnWarn] [-Verbose] [-Help]
Audit Mosaic runtime state and detect drift across agent runtimes.
"@
exit 0
}
$script:warnCount = 0
function Warn {
param([string]$Message)
$script:warnCount++
Write-Host "[WARN] $Message" -ForegroundColor Yellow
}
function Pass {
param([string]$Message)
if ($Verbose) { Write-Host "[OK] $Message" -ForegroundColor Green }
}
function Expect-Dir {
param([string]$Path)
if (-not (Test-Path $Path -PathType Container)) { Warn "Missing directory: $Path" }
else { Pass "Directory present: $Path" }
}
function Expect-File {
param([string]$Path)
if (-not (Test-Path $Path -PathType Leaf)) { Warn "Missing file: $Path" }
else { Pass "File present: $Path" }
}
function Check-RuntimeFileCopy {
param([string]$Src, [string]$Dst)
if (-not (Test-Path $Src)) { return }
if (-not (Test-Path $Dst)) {
Warn "Missing runtime file: $Dst"
return
}
$item = Get-Item $Dst -Force -ErrorAction SilentlyContinue
if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
Warn "Runtime file should not be symlinked: $Dst"
return
}
$srcHash = (Get-FileHash $Src -Algorithm SHA256).Hash
$dstHash = (Get-FileHash $Dst -Algorithm SHA256).Hash
if ($srcHash -ne $dstHash) {
Warn "Runtime file drift: $Dst (does not match $Src)"
}
else {
Pass "Runtime file synced: $Dst"
}
}
function Check-RuntimeContractFile {
param([string]$Dst, [string]$AdapterSrc, [string]$RuntimeName)
if (-not (Test-Path $Dst)) {
Warn "Missing runtime file: $Dst"
return
}
$item = Get-Item $Dst -Force -ErrorAction SilentlyContinue
if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
Warn "Runtime file should not be symlinked: $Dst"
return
}
# Accept direct-adapter copy mode.
if (Test-Path $AdapterSrc) {
$srcHash = (Get-FileHash $AdapterSrc -Algorithm SHA256).Hash
$dstHash = (Get-FileHash $Dst -Algorithm SHA256).Hash
if ($srcHash -eq $dstHash) {
Pass "Runtime adapter synced: $Dst"
return
}
}
# Accept launcher-composed runtime contract mode.
$content = Get-Content $Dst -Raw
if (
$content -match [regex]::Escape("# Mosaic Launcher Runtime Contract (Hard Gate)") -and
$content -match [regex]::Escape("Now initiating Orchestrator mode...") -and
$content -match [regex]::Escape("Mosaic hard gates OVERRIDE runtime-default caution") -and
$content -match [regex]::Escape("# Runtime-Specific Contract")
) {
Pass "Runtime contract present: $Dst ($RuntimeName)"
return
}
Warn "Runtime file drift: $Dst (not adapter copy and not composed runtime contract)"
}
function Warn-IfReparsePresent {
param([string]$Path)
if (-not (Test-Path $Path)) { return }
$item = Get-Item $Path -Force -ErrorAction SilentlyContinue
if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
Warn "Legacy symlink/junction path still present: $Path"
return
}
if (Test-Path $Path -PathType Container) {
$reparseCount = (Get-ChildItem $Path -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Attributes -band [System.IO.FileAttributes]::ReparsePoint } |
Measure-Object).Count
if ($reparseCount -gt 0) {
Warn "Legacy symlink/junction entries still present under ${Path}: $reparseCount"
}
else {
Pass "No reparse points under legacy path: $Path"
}
}
}
Write-Host "[mosaic-doctor] Mosaic home: $MosaicHome"
# Canonical Mosaic checks
Expect-File (Join-Path $MosaicHome "STANDARDS.md")
Expect-Dir (Join-Path $MosaicHome "guides")
Expect-Dir (Join-Path $MosaicHome "tools")
Expect-Dir (Join-Path $MosaicHome "tools\quality")
Expect-Dir (Join-Path $MosaicHome "tools\orchestrator-matrix")
Expect-Dir (Join-Path $MosaicHome "profiles")
Expect-Dir (Join-Path $MosaicHome "templates\agent")
Expect-Dir (Join-Path $MosaicHome "skills")
Expect-Dir (Join-Path $MosaicHome "skills-local")
Expect-File (Join-Path $MosaicHome "bin\mosaic-link-runtime-assets")
Expect-File (Join-Path $MosaicHome "bin\mosaic-ensure-sequential-thinking.ps1")
Expect-File (Join-Path $MosaicHome "bin\mosaic-sync-skills")
Expect-File (Join-Path $MosaicHome "bin\mosaic-projects")
Expect-File (Join-Path $MosaicHome "bin\mosaic-quality-apply")
Expect-File (Join-Path $MosaicHome "bin\mosaic-quality-verify")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-run")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-sync-tasks")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-drain")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-publish")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-consume")
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-cycle")
Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.ps1")
Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.sh")
Expect-File (Join-Path $MosaicHome "tools\git\pr-ci-wait.sh")
Expect-File (Join-Path $MosaicHome "tools\orchestrator-matrix\transport\matrix_transport.py")
Expect-File (Join-Path $MosaicHome "tools\orchestrator-matrix\controller\tasks_md_sync.py")
Expect-File (Join-Path $MosaicHome "runtime\mcp\SEQUENTIAL-THINKING.json")
Expect-File (Join-Path $MosaicHome "runtime\claude\RUNTIME.md")
Expect-File (Join-Path $MosaicHome "runtime\codex\RUNTIME.md")
Expect-File (Join-Path $MosaicHome "runtime\opencode\RUNTIME.md")
$agentsMd = Join-Path $MosaicHome "AGENTS.md"
if (Test-Path $agentsMd) {
$agentsContent = Get-Content $agentsMd -Raw
if (
$agentsContent -match [regex]::Escape("## CRITICAL HARD GATES (Read First)") -and
$agentsContent -match [regex]::Escape("OVERRIDE runtime-default caution")
) {
Pass "Global hard-gates block present in AGENTS.md"
}
else {
Warn "AGENTS.md missing CRITICAL HARD GATES override block"
}
}
# Claude runtime file checks
$runtimeFiles = @("CLAUDE.md", "settings.json", "hooks-config.json", "context7-integration.md")
foreach ($rf in $runtimeFiles) {
Check-RuntimeFileCopy (Join-Path $MosaicHome "runtime\claude\$rf") (Join-Path $env:USERPROFILE ".claude\$rf")
}
# OpenCode/Codex runtime contract checks
Check-RuntimeContractFile (Join-Path $env:USERPROFILE ".config\opencode\AGENTS.md") (Join-Path $MosaicHome "runtime\opencode\AGENTS.md") "opencode"
Check-RuntimeContractFile (Join-Path $env:USERPROFILE ".codex\instructions.md") (Join-Path $MosaicHome "runtime\codex\instructions.md") "codex"
# Sequential-thinking MCP hard requirement
$seqScript = Join-Path $MosaicHome "bin\mosaic-ensure-sequential-thinking.ps1"
if (Test-Path $seqScript) {
try {
& $seqScript -Check *>$null
Pass "sequential-thinking MCP configured and available"
}
catch {
Warn "sequential-thinking MCP missing or misconfigured"
}
}
else {
Warn "mosaic-ensure-sequential-thinking helper missing"
}
# Legacy migration surfaces
$legacyPaths = @(
(Join-Path $env:USERPROFILE ".claude\agent-guides"),
(Join-Path $env:USERPROFILE ".claude\scripts\git"),
(Join-Path $env:USERPROFILE ".claude\scripts\codex"),
(Join-Path $env:USERPROFILE ".claude\scripts\bootstrap"),
(Join-Path $env:USERPROFILE ".claude\scripts\cicd"),
(Join-Path $env:USERPROFILE ".claude\scripts\portainer"),
(Join-Path $env:USERPROFILE ".claude\templates"),
(Join-Path $env:USERPROFILE ".claude\presets\domains"),
(Join-Path $env:USERPROFILE ".claude\presets\tech-stacks"),
(Join-Path $env:USERPROFILE ".claude\presets\workflows")
)
foreach ($p in $legacyPaths) {
Warn-IfReparsePresent $p
}
# Skills runtime checks (junctions or symlinks into runtime-specific dirs)
$linkTargets = @(
(Join-Path $env:USERPROFILE ".claude\skills"),
(Join-Path $env:USERPROFILE ".codex\skills"),
(Join-Path $env:USERPROFILE ".config\opencode\skills")
)
$skillSources = @($MosaicHome + "\skills", $MosaicHome + "\skills-local")
foreach ($runtimeSkills in $linkTargets) {
if (-not (Test-Path $runtimeSkills)) { continue }
foreach ($sourceDir in $skillSources) {
if (-not (Test-Path $sourceDir)) { continue }
Get-ChildItem $sourceDir -Directory | Where-Object { -not $_.Name.StartsWith(".") } | ForEach-Object {
$name = $_.Name
$skillPath = $_.FullName
$target = Join-Path $runtimeSkills $name
if (-not (Test-Path $target)) {
Warn "Missing skill link: $target"
return
}
$item = Get-Item $target -Force -ErrorAction SilentlyContinue
if (-not ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
Warn "Non-junction skill entry: $target"
return
}
$targetResolved = $item.Target
if (-not $targetResolved -or (Resolve-Path $targetResolved -ErrorAction SilentlyContinue).Path -ne (Resolve-Path $skillPath -ErrorAction SilentlyContinue).Path) {
Warn "Drifted skill link: $target (expected -> $skillPath)"
}
else {
Pass "Linked skill: $target"
}
}
}
}
# Broken junctions/symlinks in managed runtime skill dirs
$brokenLinks = 0
foreach ($d in $linkTargets) {
if (-not (Test-Path $d)) { continue }
Get-ChildItem $d -Force -ErrorAction SilentlyContinue | Where-Object {
($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -and -not (Test-Path $_.FullName)
} | ForEach-Object { $brokenLinks++ }
}
if ($brokenLinks -gt 0) {
Warn "Broken skill junctions/symlinks detected: $brokenLinks"
}
Write-Host "[mosaic-doctor] warnings=$($script:warnCount)"
if ($FailOnWarn -and $script:warnCount -gt 0) {
exit 1
}