<!-- swain-model-hint: sonnet, effort: low -- Doctor Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check. Running the script The script outputs structured JSON with . Each check has , , , and optional . Parse it and present the summary table to the operator. Then use the remediation sections below only for checks that reported or status — do not re-run detection. To auto-fix flat-file artifacts: If the script is unavailable (e.g., symlinks not yet bootstrapped)…

; then\n echo \"invalid: $f (missing frontmatter open)\"\n elif ! sed -n '2,/^---$/p' \"$f\" | tail -1 | grep -q '^---

<!-- swain-model-hint: sonnet, effort: low -- Doctor Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check. Running the script The script outputs structured JSON with . Each check has , , , and optional . Parse it and present the summary table to the operator. Then use the remediation sections below only for checks that reported or status — do not re-run detection. To auto-fix flat-file artifacts: If the script is unavailable (e.g., symlinks not yet bootstrapped)…

; then\n echo \"invalid: $f (missing frontmatter close)\"\n fi\ndone\n```\n\nIf invalid files found → warn: \"Found N ticket(s) with invalid YAML frontmatter. tk may not be able to read these.\"\nIf all valid → silent.\n\n## Step 2 — Detect stale lock files\n\n```bash\nif [ -d .tickets/.locks ]; then\n find .tickets/.locks -type f -mmin +60 2>/dev/null\nfi\n```\n\nIf stale locks found (> 1 hour old) → warn and list files. **Do not auto-delete** — ask the user first.\n\n## Extended tk health checks\n\n### Vendored tk availability\n\n```bash\nTK_BIN=\"$SKILLS_ROOT/swain-do/bin/tk\"\nif [ ! -x \"$TK_BIN\" ]; then\n echo \"warning: vendored tk not found or not executable at $TK_BIN\"\nfi\n```\n\nIf missing → warn: \"Reinstall swain skills to restore it.\"\n\n### Stale lock files (same as Step 2)\n\nCheck `.tickets/.locks/` for files older than 1 hour. Ask before deleting.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1255,"content_sha256":"27139db1e595bcc39529eeec764de9db873deb7670e9bf3b2d8a80a4cef30aaa"},{"filename":"references/tool-availability.md","content":"# Tool Availability\n\nCheck for required and optional external tools. Report results as a table. Classify each finding per ADR-020's three-tier remediation model:\n\n- **Self-heal**: local, idempotent fixes — execute silently with advisory log line\n- **Bundle-offer**: external installs (e.g., `brew install`) — collect into a fix plan presented at scan end, require operator consent before executing\n- **Report-only**: judgment calls — show as warnings, no fix offered\n\n## Required tools\n\nThese tools are needed by multiple skills. If missing, warn the user.\n\n| Tool | Check | Used by | Install hint (macOS) |\n|------|-------|---------|---------------------|\n| `git` | `command -v git` | All skills | Xcode Command Line Tools |\n| `jq` | `command -v jq` | swain-init, swain-teardown, swain-do | `brew install jq` |\n\n## Optional tools\n\nThese tools enable specific features. If missing, note which features are degraded.\n\n| Tool | Check | Used by | Degradation | Install hint (macOS) | Action |\n|------|-------|---------|-------------|---------------------|--------|\n| `tk` | `[ -x \"$SKILLS_ROOT/swain-do/bin/tk\" ]` | swain-do | Task tracking unavailable; status skips task section | Vendored at `swain-do/bin/tk` -- reinstall swain if missing | report-only |\n| `uv` | `command -v uv` | swain-do (plan ingestion) | Plan ingestion unavailable | `brew install uv` | bundle-offer |\n| `gh` | `command -v gh` | swain-roadmap (GitHub issues), swain-release, swain-teardown | Status skips issues section; release can't create GitHub releases | `brew install gh` | bundle-offer |\n| `tmux` | `which tmux` | swain-init | Session tab-naming unavailable outside tmux | `brew install tmux` | bundle-offer |\n| `fswatch` | `command -v fswatch` | swain-design (specwatch live mode) | Live artifact watching unavailable; on-demand `specwatch.sh scan` still works | `brew install fswatch` | bundle-offer |\n| `ssh` | `command -v ssh` | swain-keys, git SSH alias remotes | Project-specific GitHub SSH aliases cannot be used from this runtime | `brew install openssh` | bundle-offer |\n| `rtk` | `command -v rtk` | git-compact (context-window compression) | `git-compact` passes through to raw git — no compression savings | `brew install rtk` | bundle-offer |\n\n## Reporting format\n\nAfter checking all tools, output a summary:\n\n```\nTool availability:\n git .............. ok\n jq ............... ok\n tk ............... ok (vendored)\n uv ............... ok\n gh ............... ok\n tmux ............. ok\n tmux ............. WARN — tmux not found — session tab-naming unavailable. [offer to install]\n fswatch .......... MISSING — live specwatch unavailable. Install: brew install fswatch\n```\n\nOnly flag items that need attention. If all required tools are present, the check is silent except for missing optional tools that meaningfully degrade the experience.\n\n## Remediation (ADR-020)\n\nAfter all checks complete, collect findings by action tier:\n\n1. **Self-heal** findings execute silently during the scan (advisory log line only)\n2. **Bundle-offer** findings are collected into a fix plan presented once at the end:\n\n```\nDoctor found 2 fixable issues:\n\n 1. tmux not installed — session tab-naming unavailable\n Fix: brew install tmux\n\n 2. rtk not installed — git-compact passes through without compression\n Fix: brew install rtk\n\nRun all fixes? [y/N]\n```\n\n3. **Report-only** findings are shown as warnings with no fix offered\n\nOn operator approval, execute each fix command sequentially and report per-item success/failure. On decline, log findings as advisories and continue.\n\nWith `--auto-fix`, self-heal fixes run but bundle-offer fixes are skipped (no operator to consent).\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":3684,"content_sha256":"c5a48b8c6b4fce50c882b1c2c6238206c2af3bb2e462ad9b795e304b4dda41a1"},{"filename":"references/worktree-detection.md","content":"# Stale Worktree Detection\n\nEnumerate all linked worktrees and classify their health. **Skip if the repo has no linked worktrees** (i.e., `git worktree list --porcelain` returns only the main worktree entry) — this check produces no output in a clean repo.\n\n## Detection\n\n```bash\ngit worktree list --porcelain\n```\n\nParse each linked worktree (exclude the main worktree — the first entry in the output):\n\n```bash\ngit worktree list --porcelain | awk '\n /^worktree / { path=$2 }\n /^branch / { branch=$2 }\n /^$/ { if (path != \"\") print path, branch; path=\"\"; branch=\"\" }\n' | tail -n +2\n```\n\nFor each linked worktree:\n\n1. **Orphaned** — directory does not exist on disk (`[ ! -d \"$path\" ]`):\n - WARN: \"Orphaned worktree: `\u003cpath>` (directory missing). Clean up with: `git worktree prune`\"\n\n2. **Stale (merged)** — directory exists and branch is fully merged into `trunk`:\n ```bash\n git merge-base --is-ancestor \"$branch\" origin/trunk\n ```\n - WARN: \"Stale worktree: `\u003cpath>` (branch `\u003cbranch>` already merged into trunk). Safe to remove:\n `git worktree remove \u003cpath> && git branch -d \u003cbranch>`\"\n\n3. **Active (unmerged)** — directory exists and branch has commits not in `trunk`:\n - INFO: \"Active worktree: `\u003cpath>` (branch `\u003cbranch>`, N commits ahead of trunk). Do not remove — work in progress.\"\n\nDo not remove any worktree automatically. All output is advisory.\n\n## Status values\n\n- **ok** — no linked worktrees, or all are active\n- **warning** — one or more stale or orphaned worktrees found (provide cleanup commands per item)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1569,"content_sha256":"7e64200e6032c2aa8c68819c2d9e517268665e864076db42dbba3f24802035a9"},{"filename":"scripts/check-skill-changes.sh","content":"#!/usr/bin/env bash\n# check-skill-changes.sh — Detect non-trivial skill file changes on trunk\n#\n# Scans the last N commits (default: 10) on the current branch for commits\n# that touch skill files with non-trivial diffs.\n#\n# Triviality threshold (all must hold for a commit to be trivial):\n# - Touches exactly 1 skill file\n# - Total diff is ≤5 lines (insertions + deletions)\n# - No structural changes (new sections, frontmatter field adds/removes, version bumps)\n#\n# Exit 0 = clean (no non-trivial skill changes found)\n# Exit 1 = non-trivial skill changes detected (advisory warning emitted)\n#\n# Usage: bash check-skill-changes.sh [--commits N]\n\nset -euo pipefail\n\nCOMMIT_COUNT=10\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --commits) COMMIT_COUNT=\"$2\"; shift 2 ;;\n *) shift ;;\n esac\ndone\n\n# Skill file path patterns\nSKILL_PATHS=\"skills/ .claude/skills/ .agents/skills/\"\n\nfound_issues=()\n\n# Scan recent commits\nwhile IFS= read -r commit_hash; do\n [[ -z \"$commit_hash\" ]] && continue\n\n # Get list of skill files changed in this commit\n skill_files=()\n while IFS= read -r file; do\n [[ -z \"$file\" ]] && continue\n for prefix in $SKILL_PATHS; do\n if [[ \"$file\" == ${prefix}* ]]; then\n skill_files+=(\"$file\")\n break\n fi\n done\n done \u003c \u003c(git diff-tree --no-commit-id --name-only -r \"$commit_hash\" 2>/dev/null)\n\n # Skip commits that don't touch skill files\n [[ ${#skill_files[@]} -eq 0 ]] && continue\n\n # Multi-file skill change is always non-trivial\n if [[ ${#skill_files[@]} -gt 1 ]]; then\n found_issues+=(\"$commit_hash\")\n continue\n fi\n\n # Single skill file — check diff size\n file=\"${skill_files[0]}\"\n diff_stat=$(git diff-tree --no-commit-id --numstat -r \"$commit_hash\" -- \"$file\" 2>/dev/null)\n insertions=$(echo \"$diff_stat\" | awk '{print $1}')\n deletions=$(echo \"$diff_stat\" | awk '{print $2}')\n\n # Handle binary files\n if [[ \"$insertions\" == \"-\" || \"$deletions\" == \"-\" ]]; then\n found_issues+=(\"$commit_hash\")\n continue\n fi\n\n total_lines=$((insertions + deletions))\n\n # Over 5 lines = non-trivial\n if [[ $total_lines -gt 5 ]]; then\n found_issues+=(\"$commit_hash\")\n continue\n fi\n\n # Check for structural changes even in small diffs\n diff_content=$(git diff-tree --no-commit-id -p -r \"$commit_hash\" -- \"$file\" 2>/dev/null)\n\n # Version bump detection (version field change in frontmatter)\n if echo \"$diff_content\" | grep -qE '^\\+.*version:'; then\n found_issues+=(\"$commit_hash\")\n continue\n fi\n\n # New section detection (added ## heading)\n if echo \"$diff_content\" | grep -qE '^\\+##\\s'; then\n found_issues+=(\"$commit_hash\")\n continue\n fi\n\n # Frontmatter field addition/removal\n if echo \"$diff_content\" | grep -qE '^\\+[a-z_-]+:' | head -1; then\n # Check if we're inside frontmatter (between --- markers)\n in_frontmatter=false\n while IFS= read -r line; do\n if [[ \"$line\" == \"---\" || \"$line\" == \"+---\" || \"$line\" == \" ---\" ]]; then\n if $in_frontmatter; then\n in_frontmatter=false\n else\n in_frontmatter=true\n fi\n continue\n fi\n if $in_frontmatter && echo \"$line\" | grep -qE '^\\+[a-z_-]+:'; then\n found_issues+=(\"$commit_hash\")\n break 2\n fi\n done \u003c\u003c\u003c \"$diff_content\"\n fi\n\ndone \u003c \u003c(git log --format='%H' -n \"$COMMIT_COUNT\" 2>/dev/null)\n\n# Report\nif [[ ${#found_issues[@]} -eq 0 ]]; then\n exit 0\nfi\n\nfor commit in \"${found_issues[@]}\"; do\n short=$(git log --format='%h %s' -n 1 \"$commit\" 2>/dev/null)\n echo \"⚠ Trunk commit $short touches skill files with non-trivial changes.\"\n echo \" Skill changes above the triviality threshold should use worktree branches.\"\ndone\nexit 1\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3670,"content_sha256":"9e51742d14bbea4531ed0bd432f623dbf1fab07ce32d9b7d892ce34e8fa6b074"},{"filename":"scripts/crash-debris-lib.sh","content":"#!/usr/bin/env bash\n# crash-debris-lib.sh — standalone crash debris detection functions (SPEC-182)\n#\n# Each function takes a project root path as $1 and prints findings\n# to stdout as tab-separated lines: TYPE\\tSTATUS\\tDETAIL\n#\n# STATUS values: found, clean\n# When STATUS=found, DETAIL contains human-readable description\n#\n# These functions are sourceable by both the pre-runtime script\n# (SPEC-180) and swain-doctor (SPEC-192).\n\n# Check for stale .git/index.lock\n# $1 = project root (must contain .git/ or be a worktree)\ncheck_git_index_lock() {\n local root=\"$1\"\n local git_dir=\"$root/.git\"\n\n # Handle worktree: .git may be a file pointing to the real git dir\n if [[ -f \"$git_dir\" ]]; then\n git_dir=$(sed 's/^gitdir: //' \"$git_dir\")\n # Resolve relative paths\n [[ \"$git_dir\" != /* ]] && git_dir=\"$root/$git_dir\"\n fi\n\n local lock=\"$git_dir/index.lock\"\n if [[ ! -f \"$lock\" ]]; then\n printf \"git_index_lock\\tclean\\n\"\n return\n fi\n\n # Check if creating PID is still alive\n local pid\n pid=$(cat \"$lock\" 2>/dev/null | head -1 | grep -oE '^[0-9]+

<!-- swain-model-hint: sonnet, effort: low -- Doctor Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check. Running the script The script outputs structured JSON with . Each check has , , , and optional . Parse it and present the summary table to the operator. Then use the remediation sections below only for checks that reported or status — do not re-run detection. To auto-fix flat-file artifacts: If the script is unavailable (e.g., symlinks not yet bootstrapped)…

|| echo \"\")\n if [[ -n \"$pid\" ]] && kill -0 \"$pid\" 2>/dev/null; then\n # PID alive — lock is legitimate\n printf \"git_index_lock\\tclean\\tlock held by live PID %s\\n\" \"$pid\"\n return\n fi\n\n printf \"git_index_lock\\tfound\\t%s (owner PID %s not running)\\n\" \"$lock\" \"${pid:-unknown}\"\n}\n\n# Check for interrupted git operations (merge, rebase, cherry-pick)\ncheck_interrupted_git_ops() {\n local root=\"$1\"\n local git_dir=\"$root/.git\"\n\n if [[ -f \"$git_dir\" ]]; then\n git_dir=$(sed 's/^gitdir: //' \"$git_dir\")\n [[ \"$git_dir\" != /* ]] && git_dir=\"$root/$git_dir\"\n fi\n\n local found=()\n\n [[ -f \"$git_dir/MERGE_HEAD\" ]] && found+=(\"interrupted merge (MERGE_HEAD)\")\n [[ -d \"$git_dir/rebase-merge\" ]] && found+=(\"interrupted rebase (rebase-merge/)\")\n [[ -d \"$git_dir/rebase-apply\" ]] && found+=(\"interrupted rebase-apply (rebase-apply/)\")\n [[ -f \"$git_dir/CHERRY_PICK_HEAD\" ]] && found+=(\"interrupted cherry-pick (CHERRY_PICK_HEAD)\")\n\n if [[ ${#found[@]} -eq 0 ]]; then\n printf \"interrupted_git_ops\\tclean\\n\"\n return\n fi\n\n for item in \"${found[@]}\"; do\n printf \"interrupted_git_ops\\tfound\\t%s\\n\" \"$item\"\n done\n}\n\n# Check for stale tk claim locks (dead owner PID or age >1 hour)\ncheck_stale_tk_locks() {\n local root=\"$1\"\n local locks_dir=\"$root/.tickets/.locks\"\n\n if [[ ! -d \"$locks_dir\" ]]; then\n printf \"stale_tk_locks\\tclean\\n\"\n return\n fi\n\n local found=0\n for lock_dir in \"$locks_dir\"/*/; do\n [[ -d \"$lock_dir\" ]] || continue\n local owner_file=\"$lock_dir/owner\"\n local task_id\n task_id=$(basename \"$lock_dir\")\n\n if [[ -f \"$owner_file\" ]]; then\n local pid\n pid=$(cat \"$owner_file\" 2>/dev/null | tr -d '[:space:]')\n if [[ -n \"$pid\" ]] && kill -0 \"$pid\" 2>/dev/null; then\n continue # alive — legitimate lock\n fi\n printf \"stale_tk_locks\\tfound\\ttask %s locked by dead PID %s\\n\" \"$task_id\" \"$pid\"\n else\n # Lock dir exists but no owner file — treat as stale\n printf \"stale_tk_locks\\tfound\\ttask %s lock has no owner file\\n\" \"$task_id\"\n fi\n found=$((found + 1))\n done\n\n [[ $found -eq 0 ]] && printf \"stale_tk_locks\\tclean\\n\"\n}\n\n# Check for dangling worktrees (missing directory or merged branches)\ncheck_dangling_worktrees() {\n local root=\"$1\"\n local found=0\n local in_first=1\n local path=\"\" branch=\"\"\n\n while IFS= read -r line; do\n if [[ \"$line\" == worktree\\ * ]]; then\n path=\"${line#worktree }\"\n elif [[ \"$line\" == branch\\ * ]]; then\n branch=\"${line#branch }\"\n elif [[ -z \"$line\" ]]; then\n if [[ $in_first -eq 1 ]]; then\n in_first=0\n path=\"\"\n branch=\"\"\n continue\n fi\n if [[ -n \"$path\" ]]; then\n if [[ ! -d \"$path\" ]]; then\n printf \"dangling_worktrees\\tfound\\tmissing directory: %s (branch: %s)\\n\" \"$path\" \"${branch:-detached}\"\n found=$((found + 1))\n else\n # Cross-reference with runtime sessions (best-effort)\n local has_live_session=false\n if [[ -d \"$HOME/.claude/sessions\" ]]; then\n for sess in \"$HOME/.claude/sessions\"/*.json; do\n [[ -f \"$sess\" ]] || continue\n local sess_cwd sess_pid\n sess_cwd=$(grep -o '\"cwd\":\"[^\"]*\"' \"$sess\" 2>/dev/null | head -1 | sed 's/\"cwd\":\"//;s/\"$//')\n if [[ \"$sess_cwd\" == \"$path\" ]]; then\n sess_pid=$(grep -o '\"pid\":[0-9]*' \"$sess\" 2>/dev/null | head -1 | sed 's/\"pid\"://')\n if [[ -n \"$sess_pid\" ]] && kill -0 \"$sess_pid\" 2>/dev/null; then\n has_live_session=true\n fi\n fi\n done\n fi\n\n if [[ \"$has_live_session\" == \"true\" ]]; then\n continue\n fi\n\n local wt_status\n wt_status=$(git -C \"$path\" status --porcelain 2>/dev/null | head -5)\n if [[ -n \"$wt_status\" ]]; then\n local change_count\n change_count=$(git -C \"$path\" status --porcelain 2>/dev/null | wc -l | tr -d ' ')\n printf \"dangling_worktrees\\tfound\\tuncommitted changes (%s files) in %s\\n\" \"$change_count\" \"$path\"\n found=$((found + 1))\n fi\n fi\n fi\n path=\"\"\n branch=\"\"\n fi\n done \u003c \u003c(git -C \"$root\" worktree list --porcelain 2>/dev/null; echo \"\")\n\n [[ $found -eq 0 ]] && printf \"dangling_worktrees\\tclean\\n\"\n}\n\n# Check for orphaned MCP servers associated with this project\n# Best-effort: matches process names containing \"mcp\" with cwd matching project root\ncheck_orphaned_mcp() {\n local root=\"$1\"\n local real_root\n real_root=$(cd \"$root\" && pwd -P 2>/dev/null || echo \"$root\")\n local found=0\n\n while IFS= read -r line; do\n [[ -z \"$line\" ]] && continue\n local pid cmd\n pid=$(echo \"$line\" | awk '{print $1}')\n cmd=$(echo \"$line\" | awk '{$1=\"\"; print $0}' | sed 's/^ //')\n\n local proc_cwd=\"\"\n if [[ -d \"/proc/$pid\" ]]; then\n proc_cwd=$(readlink \"/proc/$pid/cwd\" 2>/dev/null || echo \"\")\n else\n proc_cwd=$(lsof -p \"$pid\" -Fn 2>/dev/null | grep '^n/' | head -1 | sed 's/^n//' || echo \"\")\n fi\n\n if [[ \"$proc_cwd\" == \"$real_root\"* ]]; then\n printf \"orphaned_mcp\\tfound\\tPID %s: %s\\n\" \"$pid\" \"$cmd\"\n found=$((found + 1))\n fi\n done \u003c \u003c(ps aux 2>/dev/null | grep -i '[m]cp.*server\\|[m]cp.*gateway' | grep -iv 'docker\\|containerd' | awk '{print $2, $11, $12, $13}' || true)\n\n [[ $found -eq 0 ]] && printf \"orphaned_mcp\\tclean\\n\"\n}\n\n# Run all crash debris checks and return combined results\n# $1 = project root\n# Returns: only \"found\" lines (tab-separated), or nothing if clean (AC5 silent fast path)\ncheck_all_crash_debris() {\n local root=\"$1\"\n local output=\"\"\n\n output+=$(check_git_index_lock \"$root\" 2>/dev/null)\n output+=

<!-- swain-model-hint: sonnet, effort: low -- Doctor Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check. Running the script The script outputs structured JSON with . Each check has , , , and optional . Parse it and present the summary table to the operator. Then use the remediation sections below only for checks that reported or status — do not re-run detection. To auto-fix flat-file artifacts: If the script is unavailable (e.g., symlinks not yet bootstrapped)…

\\n'\n output+=$(check_interrupted_git_ops \"$root\" 2>/dev/null)\n output+=

<!-- swain-model-hint: sonnet, effort: low -- Doctor Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check. Running the script The script outputs structured JSON with . Each check has , , , and optional . Parse it and present the summary table to the operator. Then use the remediation sections below only for checks that reported or status — do not re-run detection. To auto-fix flat-file artifacts: If the script is unavailable (e.g., symlinks not yet bootstrapped)…

\\n'\n output+=$(check_stale_tk_locks \"$root\" 2>/dev/null)\n output+=

<!-- swain-model-hint: sonnet, effort: low -- Doctor Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check. Running the script The script outputs structured JSON with . Each check has , , , and optional . Parse it and present the summary table to the operator. Then use the remediation sections below only for checks that reported or status — do not re-run detection. To auto-fix flat-file artifacts: If the script is unavailable (e.g., symlinks not yet bootstrapped)…

\\n'\n output+=$(check_dangling_worktrees \"$root\" 2>/dev/null)\n output+=

<!-- swain-model-hint: sonnet, effort: low -- Doctor Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check. Running the script The script outputs structured JSON with . Each check has , , , and optional . Parse it and present the summary table to the operator. Then use the remediation sections below only for checks that reported or status — do not re-run detection. To auto-fix flat-file artifacts: If the script is unavailable (e.g., symlinks not yet bootstrapped)…

\\n'\n output+=$(check_orphaned_mcp \"$root\" 2>/dev/null)\n\n # AC5: silent fast path — only emit lines with findings, nothing if clean\n echo \"$output\" | grep 'found' || true\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":7145,"content_sha256":"06d9268df0964e76614f802895759140cdf37735bd7e5c574ca5b00871cc4690"},{"filename":"scripts/migrate-to-trunk-release.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\n# migrate-to-trunk-release.sh\n# One-time migration from single-branch (main) to trunk+release model (SPEC-114, ADR-013).\n# Idempotent — safe to run multiple times. Use --dry-run to preview.\n\nDRY_RUN=false\n[[ \"${1:-}\" == \"--dry-run\" ]] && DRY_RUN=true\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ninfo() { echo \"==> $*\"; }\nwarn() { echo \"WARN: $*\" >&2; }\ndie() { echo \"ERROR: $*\" >&2; exit 1; }\n\nrun() {\n if $DRY_RUN; then\n echo \"[dry-run] $*\"\n else\n info \"Running: $*\"\n \"$@\"\n fi\n}\n\n# ---------------------------------------------------------------------------\n# Prerequisite checks\n# ---------------------------------------------------------------------------\n\ninfo \"Checking prerequisites...\"\n\ncommand -v git >/dev/null 2>&1 || die \"git is not installed\"\ncommand -v gh >/dev/null 2>&1 || die \"gh CLI is not installed\"\n\n# Verify gh is authenticated\ngh auth status >/dev/null 2>&1 || die \"gh CLI is not authenticated — run 'gh auth login' first\"\n\n# Detect remote URL and extract owner/repo\nREMOTE_URL=\"$(git remote get-url origin 2>/dev/null)\" || die \"No 'origin' remote found\"\ninfo \"Remote URL: $REMOTE_URL\"\n\n# Extract owner/repo from SSH or HTTPS URLs\nif [[ \"$REMOTE_URL\" =~ .*:(.+/.+)\\.git$ ]]; then\n OWNER_REPO=\"${BASH_REMATCH[1]}\"\nelif [[ \"$REMOTE_URL\" =~ .*:(.+/.+)$ ]]; then\n OWNER_REPO=\"${BASH_REMATCH[1]}\"\nelif [[ \"$REMOTE_URL\" =~ github\\.com/(.+/.+)\\.git$ ]]; then\n OWNER_REPO=\"${BASH_REMATCH[1]}\"\nelif [[ \"$REMOTE_URL\" =~ github\\.com/(.+/.+)$ ]]; then\n OWNER_REPO=\"${BASH_REMATCH[1]}\"\nelse\n die \"Could not extract owner/repo from remote URL: $REMOTE_URL\"\nfi\ninfo \"GitHub repo: $OWNER_REPO\"\n\n# Must be on main branch (or trunk if re-running)\nCURRENT_BRANCH=\"$(git symbolic-ref --short HEAD 2>/dev/null)\" || die \"Detached HEAD — check out a branch first\"\nif [[ \"$CURRENT_BRANCH\" != \"main\" && \"$CURRENT_BRANCH\" != \"trunk\" ]]; then\n die \"Must be on 'main' (or 'trunk' if re-running). Currently on '$CURRENT_BRANCH'.\"\nfi\n\n# Clean working tree\nif ! git diff --quiet || ! git diff --cached --quiet; then\n die \"Working tree is dirty — commit or stash changes first\"\nfi\n\n# Fetch latest remote state\ninfo \"Fetching latest remote state...\"\ngit fetch origin\n\n# ---------------------------------------------------------------------------\n# Step 1: Rename main → trunk (locally and on remote)\n# ---------------------------------------------------------------------------\n\ninfo \"\"\ninfo \"--- Step 1: Rename main → trunk ---\"\n\nif git show-ref --verify --quiet refs/heads/trunk; then\n info \"Local branch 'trunk' already exists — skipping local rename.\"\nelse\n if git show-ref --verify --quiet refs/heads/main; then\n run git branch -m main trunk\n else\n info \"No local 'main' branch found (already renamed?) — skipping.\"\n fi\nfi\n\n# Push trunk to remote\nif git ls-remote --heads origin trunk | grep -q trunk; then\n info \"Remote branch 'trunk' already exists — skipping push.\"\nelse\n run git push origin trunk\nfi\n\n# ---------------------------------------------------------------------------\n# Step 2: Create release branch from trunk HEAD\n# ---------------------------------------------------------------------------\n\ninfo \"\"\ninfo \"--- Step 2: Create release branch from trunk HEAD ---\"\n\nif git show-ref --verify --quiet refs/heads/release; then\n info \"Local branch 'release' already exists — skipping creation.\"\nelse\n run git branch release trunk\nfi\n\n# Push release to remote\nif git ls-remote --heads origin release | grep -q release; then\n info \"Remote branch 'release' already exists — skipping push.\"\nelse\n run git push origin release\nfi\n\n# ---------------------------------------------------------------------------\n# Step 3: Set release as the default branch on GitHub\n# ---------------------------------------------------------------------------\n\ninfo \"\"\ninfo \"--- Step 3: Set 'release' as the default branch on GitHub ---\"\n\nCURRENT_DEFAULT=\"$(gh api \"repos/$OWNER_REPO\" --jq '.default_branch' 2>/dev/null)\" || true\n\nif [[ \"$CURRENT_DEFAULT\" == \"release\" ]]; then\n info \"Default branch is already 'release' — skipping.\"\nelse\n info \"Current default branch: ${CURRENT_DEFAULT:-unknown}\"\n run gh api -X PATCH \"repos/$OWNER_REPO\" -f default_branch=release\nfi\n\n# ---------------------------------------------------------------------------\n# Step 4: Push both branches (ensure up-to-date)\n# ---------------------------------------------------------------------------\n\ninfo \"\"\ninfo \"--- Step 4: Ensure both branches are pushed ---\"\n\nrun git push origin trunk\nrun git push origin release\n\n# ---------------------------------------------------------------------------\n# Step 5: Delete old main branch on the remote\n# ---------------------------------------------------------------------------\n\ninfo \"\"\ninfo \"--- Step 5: Delete old 'main' branch on remote ---\"\n\nif git ls-remote --heads origin main | grep -q main; then\n run git push origin --delete main\nelse\n info \"Remote branch 'main' does not exist — skipping deletion.\"\nfi\n\n# ---------------------------------------------------------------------------\n# Step 6: Set upstream tracking\n# ---------------------------------------------------------------------------\n\ninfo \"\"\ninfo \"--- Step 6: Set upstream tracking for trunk ---\"\n\nif [[ \"$(git symbolic-ref --short HEAD 2>/dev/null)\" == \"trunk\" ]]; then\n run git branch --set-upstream-to=origin/trunk trunk\nfi\n\n# ---------------------------------------------------------------------------\n# Done\n# ---------------------------------------------------------------------------\n\ninfo \"\"\ninfo \"Migration complete!\"\ninfo \" trunk → origin/trunk (development)\"\ninfo \" release → origin/release (default branch, stable)\"\nif $DRY_RUN; then\n info \"\"\n info \"(This was a dry run — no changes were made.)\"\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5926,"content_sha256":"3815f7fa789225c85fe936da74ca60653ce22ae9220f13a88a505351c7d2aa53"},{"filename":"scripts/ssh-readiness.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\n# ssh-readiness.sh — validate and optionally repair per-project SSH alias wiring\n#\n# Usage:\n# ssh-readiness.sh --check\n# ssh-readiness.sh --repair\n#\n# Exit 0: SSH alias wiring is healthy or not applicable for this repo\n# Exit 1: Remaining issues need operator action\n\nMODE=\"${1:---check}\"\nREPO_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"\n\nissue_count=0\n\nadd_issue() {\n echo \"ISSUE: $*\"\n issue_count=$((issue_count + 1))\n}\n\nadd_note() {\n echo \"NOTE: $*\"\n}\n\nderive_project_from_alias_remote() {\n local remote_url\n remote_url=\"$(git -C \"$REPO_ROOT\" remote get-url origin 2>/dev/null || true)\"\n if [[ \"$remote_url\" =~ ^git@github\\.com-([a-z0-9-]+): ]]; then\n echo \"${BASH_REMATCH[1]}\"\n fi\n}\n\nensure_main_config_include() {\n local ssh_dir=\"$1\" config_dir=\"$2\" main_config=\"$3\"\n mkdir -p \"$ssh_dir\" \"$config_dir\"\n chmod 700 \"$ssh_dir\"\n\n if [[ ! -f \"$main_config\" ]]; then\n printf 'Include config.d/*\\n' > \"$main_config\"\n chmod 600 \"$main_config\"\n add_note \"Created $main_config with Include config.d/*\"\n return 0\n fi\n\n if ! grep -qF \"Include config.d/*\" \"$main_config\" 2>/dev/null; then\n local tmp\n tmp=\"$(mktemp)\"\n printf 'Include config.d/*\\n\\n' > \"$tmp\"\n cat \"$main_config\" >> \"$tmp\"\n mv \"$tmp\" \"$main_config\"\n chmod 600 \"$main_config\"\n add_note \"Updated $main_config to include config.d/*\"\n fi\n}\n\nwrite_alias_config() {\n local alias_file=\"$1\" host_alias=\"$2\" key_path=\"$3\"\n mkdir -p \"$(dirname \"$alias_file\")\"\n cat > \"$alias_file\" \u003c\u003cEOF\n# swain-doctor: per-project SSH config for ${host_alias}\nHost ${host_alias}\n HostName ssh.github.com\n Port 443\n User git\n IdentityFile ${key_path}\n IdentitiesOnly yes\nEOF\n chmod 600 \"$alias_file\"\n add_note \"Created $alias_file\"\n}\n\nread_identity_file() {\n local alias_file=\"$1\"\n awk '/^[[:space:]]*IdentityFile[[:space:]]+/ { print $2; exit }' \"$alias_file\" 2>/dev/null || true\n}\n\nalias_uses_github_443() {\n local alias_file=\"$1\"\n grep -qF \"HostName ssh.github.com\" \"$alias_file\" 2>/dev/null \\\n && grep -qF \"Port 443\" \"$alias_file\" 2>/dev/null\n}\n\nmain() {\n local project host_alias ssh_dir config_dir main_config alias_file default_key alias_key\n\n project=\"$(derive_project_from_alias_remote)\"\n if [[ -z \"$project\" ]]; then\n exit 0\n fi\n\n host_alias=\"github.com-${project}\"\n ssh_dir=\"$HOME/.ssh\"\n config_dir=\"$ssh_dir/config.d\"\n main_config=\"$ssh_dir/config\"\n alias_file=\"$config_dir/${project}.conf\"\n default_key=\"$ssh_dir/${project}_signing\"\n\n if ! command -v ssh >/dev/null 2>&1; then\n add_issue \"ssh client not found on PATH — install OpenSSH client before using ${host_alias}\"\n fi\n\n if [[ \"$MODE\" == \"--repair\" ]]; then\n ensure_main_config_include \"$ssh_dir\" \"$config_dir\" \"$main_config\"\n else\n if [[ ! -f \"$main_config\" ]]; then\n add_issue \"${host_alias} remote requires $main_config with 'Include config.d/*'\"\n elif ! grep -qF \"Include config.d/*\" \"$main_config\" 2>/dev/null; then\n add_issue \"$main_config is missing 'Include config.d/*' for ${host_alias}\"\n fi\n fi\n\n if [[ ! -f \"$alias_file\" ]]; then\n if [[ \"$MODE\" == \"--repair\" && -f \"$default_key\" ]]; then\n write_alias_config \"$alias_file\" \"$host_alias\" \"$default_key\"\n else\n add_issue \"${host_alias} remote is configured but $alias_file is missing. Run swain-keys --provision.\"\n fi\n fi\n\n if [[ -f \"$alias_file\" ]]; then\n if ! grep -qE \"^[[:space:]]*Host[[:space:]]+${host_alias}\\$\" \"$alias_file\" 2>/dev/null; then\n add_issue \"$alias_file does not define Host ${host_alias}\"\n fi\n\n alias_key=\"$(read_identity_file \"$alias_file\")\"\n alias_key=\"${alias_key/#\\~/$HOME}\"\n if [[ -z \"$alias_key\" ]]; then\n add_issue \"$alias_file is missing IdentityFile for ${host_alias}\"\n elif [[ ! -f \"$alias_key\" ]]; then\n add_issue \"${host_alias} points to missing key ${alias_key}. Run swain-keys --provision.\"\n elif ! alias_uses_github_443 \"$alias_file\"; then\n if [[ \"$MODE\" == \"--repair\" ]]; then\n write_alias_config \"$alias_file\" \"$host_alias\" \"$alias_key\"\n else\n add_issue \"$alias_file still targets legacy github.com:22. Re-run swain-keys --provision or doctor repair.\"\n fi\n fi\n elif [[ ! -f \"$default_key\" ]]; then\n add_issue \"${host_alias} remote has no local key at $default_key. Run swain-keys --provision.\"\n fi\n\n if [[ $issue_count -gt 0 ]]; then\n exit 1\n fi\n}\n\nmain\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4423,"content_sha256":"05cbeac12fe8570f40d927248822ffe4af1cf40eff8bd0f6e756120c70497d64"},{"filename":"scripts/swain-doctor.sh","content":"#!/usr/bin/env bash\n# swain-doctor.sh — consolidated health check script (SPEC-192)\n#\n# Runs all swain-doctor checks in a single process with set +e,\n# eliminating the parallel tool-call cascade failure where one\n# erroring check cancels all sibling checks.\n#\n# Output: JSON object with { checks: [...], summary: {...} }\n# Exit: always 0 — findings are reported in the JSON, not the exit code.\n\nREPO_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"\ncd \"$REPO_ROOT\"\n\n# Portable path resolution — works whether installed at skills/ or .agents/skills/\n_src=\"${BASH_SOURCE[0]}\"\nwhile [[ -L \"$_src\" ]]; do\n _dir=\"$(cd \"$(dirname \"$_src\")\" && pwd)\"\n _src=\"$(readlink \"$_src\")\"\n [[ \"$_src\" != /* ]] && _src=\"$_dir/$_src\"\ndone\nSCRIPT_DIR=\"$(cd \"$(dirname \"$_src\")\" && pwd)\"\nSKILL_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nSKILLS_ROOT=\"$(dirname \"$SKILL_DIR\")\"\nLEGACY_SKILLS_LIB=\"$SKILL_DIR/references/legacy-skills-lib.sh\"\n\nif [[ -f \"$LEGACY_SKILLS_LIB\" ]]; then\n # shellcheck disable=SC1090\n source \"$LEGACY_SKILLS_LIB\"\nfi\n\n# Collect results\ndeclare -a CHECKS=()\n\nadd_check() {\n local name=\"$1\"\n local status=\"$2\"\n local message=\"${3:-}\"\n local detail=\"${4:-}\"\n local entry=\"{\\\"name\\\":\\\"$name\\\",\\\"status\\\":\\\"$status\\\"\"\n if [[ -n \"$message\" ]]; then\n # Escape quotes and newlines in message\n message=$(echo \"$message\" | sed 's/\"/\\\\\"/g' | tr '\\n' ' ')\n entry=\"$entry,\\\"message\\\":\\\"$message\\\"\"\n fi\n if [[ -n \"$detail\" ]]; then\n detail=$(echo \"$detail\" | sed 's/\"/\\\\\"/g' | tr '\\n' ' ')\n entry=\"$entry,\\\"detail\\\":\\\"$detail\\\"\"\n fi\n entry=\"$entry}\"\n CHECKS+=(\"$entry\")\n}\n\n# ============================================================\n# CLI flags\n# ============================================================\nFIX_FLAT=false\nfor arg in \"$@\"; do\n case \"$arg\" in\n --fix-flat-artifacts) FIX_FLAT=true ;;\n esac\ndone\n\n# ============================================================\n# Check 1: Governance (SPEC-222: auto-repair stale block when markers present)\n# ============================================================\ncheck_governance() {\n local gov_files\n gov_files=$(grep -l \"swain governance\" CLAUDE.md AGENTS.md .cursor/rules/swain-governance.mdc 2>/dev/null || true)\n\n if [[ -z \"$gov_files\" ]]; then\n add_check \"governance\" \"warning\" \"governance markers not found in any context file\"\n return\n fi\n\n # Freshness check\n local canonical=\"$SKILL_DIR/references/AGENTS.content.md\"\n if [[ ! -f \"$canonical\" ]]; then\n add_check \"governance\" \"ok\" \"governance markers present (canonical source not found for freshness check)\"\n return\n fi\n\n local gov_file\n gov_file=$(echo \"$gov_files\" | head -1)\n extract_gov() { awk '/\u003c!-- swain governance/{f=1;next}/\u003c!-- end swain governance/{f=0}f' \"$1\"; }\n local installed_hash canonical_hash\n installed_hash=$(extract_gov \"$gov_file\" | shasum -a 256 | cut -d' ' -f1)\n canonical_hash=$(extract_gov \"$canonical\" | shasum -a 256 | cut -d' ' -f1)\n\n if [[ \"$installed_hash\" == \"$canonical_hash\" ]]; then\n add_check \"governance\" \"ok\" \"governance current\"\n return\n fi\n\n # Stale — attempt auto-repair if both markers are present\n if grep -q '\u003c!-- swain governance' \"$gov_file\" && grep -q '\u003c!-- end swain governance' \"$gov_file\"; then\n # Write canonical block content to temp file (avoids awk -v newline limitation)\n local tmp_canonical\n tmp_canonical=$(mktemp)\n extract_gov \"$canonical\" > \"$tmp_canonical\"\n awk -v tmpfile=\"$tmp_canonical\" '\n BEGIN{ while ((getline line \u003c tmpfile) > 0) { buf = buf line \"\\n\" } }\n /\u003c!-- swain governance/{print; p=1; printf \"%s\", buf; next}\n /\u003c!-- end swain governance/{p=0}\n !p{print}\n ' \"$gov_file\" > \"${gov_file}.tmp\" && mv -f \"${gov_file}.tmp\" \"$gov_file\"\n rm -f \"$tmp_canonical\"\n add_check \"governance\" \"advisory\" \"governance block updated to match canonical\"\n else\n add_check \"governance\" \"warning\" \"governance block is stale — markers missing, cannot auto-repair\" \"installed=$installed_hash canonical=$canonical_hash\"\n fi\n}\n\n# ============================================================\n# Check 2: Legacy skill cleanup\n# ============================================================\ncheck_legacy_skills() {\n local legacy_json=\"$SKILL_DIR/references/legacy-skills.json\"\n if [[ ! -f \"$legacy_json\" ]] || ! declare -F legacy_skill_entries >/dev/null 2>&1; then\n add_check \"legacy_skills\" \"warning\" \"legacy skill map unavailable — cannot check stale skill directories\"\n return\n fi\n\n local removed=()\n local skipped=()\n local kind old_name replacement base_dir skill_dir replacement_dir\n\n while IFS=

<!-- swain-model-hint: sonnet, effort: low -- Doctor Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check. Running the script The script outputs structured JSON with . Each check has , , , and optional . Parse it and present the summary table to the operator. Then use the remediation sections below only for checks that reported or status — do not re-run detection. To auto-fix flat-file artifacts: If the script is unavailable (e.g., symlinks not yet bootstrapped)…

\\t' read -r kind old_name replacement; do\n [[ -n \"$old_name\" ]] || continue\n for base_dir in \"$REPO_ROOT/.agents/skills\" \"$REPO_ROOT/.claude/skills\"; do\n skill_dir=\"$base_dir/$old_name\"\n [[ -d \"$skill_dir\" ]] || continue\n\n if [[ \"$kind\" == \"renamed\" ]]; then\n replacement_dir=\"$base_dir/$replacement\"\n if [[ ! -d \"$replacement_dir\" ]]; then\n skipped+=(\"$skill_dir (replacement missing: $replacement)\")\n continue\n fi\n fi\n\n if ! legacy_skill_matches_fingerprint \"$skill_dir\" \"$legacy_json\"; then\n skipped+=(\"$skill_dir (no swain fingerprint)\")\n continue\n fi\n\n rm -rf \"$skill_dir\"\n if [[ \"$kind\" == \"renamed\" ]]; then\n removed+=(\"$skill_dir -> $replacement\")\n else\n removed+=(\"$skill_dir (absorbed by $replacement)\")\n fi\n done\n done \u003c \u003c(legacy_skill_entries \"$legacy_json\")\n\n if [[ ${#removed[@]} -eq 0 && ${#skipped[@]} -eq 0 ]]; then\n add_check \"legacy_skills\" \"ok\" \"no legacy skill directories found\"\n return\n fi\n\n local detail=\"\"\n if [[ ${#removed[@]} -gt 0 ]]; then\n detail=\"removed: ${removed[*]}\"\n fi\n if [[ ${#skipped[@]} -gt 0 ]]; then\n detail=\"${detail:+$detail; }manual review: ${skipped[*]}\"\n fi\n\n if [[ ${#skipped[@]} -gt 0 ]]; then\n add_check \"legacy_skills\" \"warning\" \"legacy skill cleanup requires manual review\" \"$detail\"\n else\n add_check \"legacy_skills\" \"advisory\" \"removed ${#removed[@]} legacy skill director$( [[ ${#removed[@]} -eq 1 ]] && echo \"y\" || echo \"ies\" )\" \"$detail\"\n fi\n}\n\n# ============================================================\n# Check 3: .agents directory\n# ============================================================\ncheck_agents_directory() {\n if [[ -d .agents ]]; then\n add_check \"agents_directory\" \"ok\" \".agents directory exists\"\n else\n add_check \"agents_directory\" \"warning\" \".agents directory missing\"\n fi\n}\n\n# ============================================================\n# Check 3: Tickets validation\n# ============================================================\ncheck_tickets() {\n if [[ ! -d .tickets ]]; then\n add_check \"tickets\" \"ok\" \"no .tickets directory (skipped)\"\n return\n fi\n\n local invalid=0\n for f in .tickets/*.md; do\n [[ -f \"$f\" ]] || continue\n if ! head -1 \"$f\" | grep -q '^---

<!-- swain-model-hint: sonnet, effort: low -- Doctor Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check. Running the script The script outputs structured JSON with . Each check has , , , and optional . Parse it and present the summary table to the operator. Then use the remediation sections below only for checks that reported or status — do not re-run detection. To auto-fix flat-file artifacts: If the script is unavailable (e.g., symlinks not yet bootstrapped)…

; then\n invalid=$((invalid + 1))\n fi\n done\n\n # Check stale locks\n local stale_locks=\"\"\n if [[ -d .tickets/.locks ]]; then\n stale_locks=$(find .tickets/.locks -type f -mmin +60 2>/dev/null | head -5 || true)\n fi\n\n if [[ $invalid -gt 0 && -n \"$stale_locks\" ]]; then\n add_check \"tickets\" \"warning\" \"$invalid invalid ticket(s), stale lock files found\"\n elif [[ $invalid -gt 0 ]]; then\n add_check \"tickets\" \"warning\" \"$invalid ticket(s) with invalid YAML frontmatter\"\n elif [[ -n \"$stale_locks\" ]]; then\n add_check \"tickets\" \"warning\" \"stale lock files in .tickets/.locks/\"\n else\n add_check \"tickets\" \"ok\" \".tickets valid\"\n fi\n}\n\n# ============================================================\n# Check 4: Stale .beads/ migration\n# ============================================================\ncheck_beads() {\n if [[ -d .beads ]]; then\n add_check \"beads_migration\" \"warning\" \"stale .beads/ directory needs migration to .tickets/\"\n else\n add_check \"beads_migration\" \"ok\" \"no stale .beads/ (skipped)\"\n fi\n}\n\n# ============================================================\n# Check 5: Tool availability\n# ============================================================\ncheck_tools() {\n local missing_required=\"\"\n local missing_optional=\"\"\n\n # Required\n for cmd in git jq; do\n if ! command -v \"$cmd\" >/dev/null 2>&1; then\n missing_required=\"${missing_required:+$missing_required, }$cmd\"\n fi\n done\n\n # Optional\n for cmd in tk uv gh tmux fswatch; do\n if [[ \"$cmd\" == \"tk\" ]]; then\n if [[ ! -x \"$SKILLS_ROOT/swain-do/bin/tk\" ]]; then\n missing_optional=\"${missing_optional:+$missing_optional, }tk\"\n fi\n else\n if ! command -v \"$cmd\" >/dev/null 2>&1; then\n missing_optional=\"${missing_optional:+$missing_optional, }$cmd\"\n fi\n fi\n done\n\n if [[ -n \"$missing_required\" ]]; then\n add_check \"tools\" \"warning\" \"required tools missing: $missing_required\" \"optional missing: ${missing_optional:-none}\"\n elif [[ -n \"$missing_optional\" ]]; then\n add_check \"tools\" \"ok\" \"all required tools present\" \"optional missing: $missing_optional\"\n else\n add_check \"tools\" \"ok\" \"all tools present\"\n fi\n}\n\n# ============================================================\n# Check 6: Settings validation\n# ============================================================\ncheck_settings() {\n local issues=\"\"\n\n if [[ ! -f swain.settings.json ]]; then\n issues=\"swain.settings.json missing\"\n elif command -v jq >/dev/null 2>&1 && ! jq empty swain.settings.json 2>/dev/null; then\n issues=\"swain.settings.json contains invalid JSON\"\n fi\n\n local user_settings=\"${XDG_CONFIG_HOME:-$HOME/.config}/swain/settings.json\"\n if [[ -f \"$user_settings\" ]] && command -v jq >/dev/null 2>&1 && ! jq empty \"$user_settings\" 2>/dev/null; then\n issues=\"${issues:+$issues; }user settings.json contains invalid JSON\"\n fi\n\n if [[ -n \"$issues\" ]]; then\n add_check \"settings\" \"warning\" \"$issues\"\n else\n add_check \"settings\" \"ok\" \"settings valid\"\n fi\n}\n\n# ============================================================\n# Check 7: Script permissions (SPEC-222: auto-repair)\n# ============================================================\ncheck_script_permissions() {\n local bad_scripts_list\n bad_scripts_list=$(find \"$SKILLS_ROOT\" -type f \\( -path '*/scripts/*.sh' -o -path '*/scripts/*.py' \\) ! -perm -u+x 2>/dev/null || true)\n local bad_count=0\n [[ -n \"$bad_scripts_list\" ]] && bad_count=$(echo \"$bad_scripts_list\" | grep -c .)\n\n if [[ \"$bad_count\" -gt 0 ]]; then\n local repaired=0\n while IFS= read -r script; do\n [[ -z \"$script\" ]] && continue\n chmod +x \"$script\" 2>/dev/null && repaired=$((repaired + 1))\n done \u003c\u003c\u003c \"$bad_scripts_list\"\n add_check \"script_permissions\" \"advisory\" \"fixed execute permission on $repaired script(s)\"\n else\n add_check \"script_permissions\" \"ok\" \"all scripts executable\"\n fi\n}\n\n# ============================================================\n# Check 8: Memory directory (SPEC-222: auto-repair)\n# ============================================================\ncheck_memory_directory() {\n local project_slug\n project_slug=$(echo \"$REPO_ROOT\" | tr '/' '-')\n local memory_dir=\"$HOME/.claude/projects/${project_slug}/memory\"\n\n if [[ -d \"$memory_dir\" ]]; then\n add_check \"memory_directory\" \"ok\" \"memory directory exists\"\n else\n mkdir -p \"$memory_dir\" 2>/dev/null\n if [[ -d \"$memory_dir\" ]]; then\n add_check \"memory_directory\" \"advisory\" \"memory directory created at $memory_dir\"\n else\n add_check \"memory_directory\" \"warning\" \"memory directory missing and could not be created at $memory_dir\"\n fi\n fi\n}\n\n# ============================================================\n# Check 9: Superpowers detection\n# ============================================================\ncheck_superpowers() {\n local found=0\n local missing=0\n local missing_names=\"\"\n for skill in brainstorming writing-plans test-driven-development verification-before-completion subagent-driven-development executing-plans; do\n if [[ -f \".agents/skills/$skill/SKILL.md\" ]] || [[ -f \".claude/skills/$skill/SKILL.md\" ]]; then\n found=$((found + 1))\n else\n missing=$((missing + 1))\n missing_names=\"${missing_names:+$missing_names, }$skill\"\n fi\n done\n\n if [[ $missing -eq 0 ]]; then\n add_check \"superpowers\" \"ok\" \"$found/6 skills detected\"\n elif [[ $found -eq 0 ]]; then\n add_check \"superpowers\" \"warning\" \"superpowers not installed (0/6)\" \"$missing_names\"\n else\n add_check \"superpowers\" \"warning\" \"partial install ($found/6)\" \"missing: $missing_names\"\n fi\n}\n\n# ============================================================\n# Check 10: Epics without parent-initiative\n# ============================================================\ncheck_epics_initiative() {\n local count=0\n while IFS= read -r -d '' f; do\n if grep -q '^parent-vision:' \"$f\" 2>/dev/null && ! grep -q '^parent-initiative:' \"$f\" 2>/dev/null; then\n count=$((count + 1))\n fi\n done \u003c \u003c(find docs/epic -name '*.md' -not -name 'README.md' -not -name 'list-*.md' -print0 2>/dev/null)\n\n if [[ $count -gt 0 ]]; then\n add_check \"epics_initiative\" \"advisory\" \"$count epic(s) without parent-initiative\"\n else\n add_check \"epics_initiative\" \"ok\" \"all epics have parent-initiative or no epics exist\"\n fi\n}\n\n# ============================================================\n# Check 11: Evidence pool / trove migration\n# ============================================================\ncheck_evidence_pools() {\n if [[ -d docs/evidence-pools ]]; then\n add_check \"evidence_pools\" \"warning\" \"docs/evidence-pools/ exists — trove migration needed\"\n elif [[ -d docs/troves ]]; then\n add_check \"evidence_pools\" \"ok\" \"troves found\"\n else\n add_check \"evidence_pools\" \"ok\" \"no evidence pools or troves (skipped)\"\n fi\n}\n\n# ============================================================\n# Check 12: Stale worktree detection\n# ============================================================\ncheck_worktrees() {\n local worktree_count\n worktree_count=$(git worktree list --porcelain 2>/dev/null | grep -c '^worktree ') || worktree_count=0\n\n if [[ \"$worktree_count\" -le 1 ]]; then\n add_check \"worktrees\" \"ok\" \"no linked worktrees\"\n return\n fi\n\n local stale=0\n local orphaned=0\n # Parse linked worktrees (skip main — first entry)\n local in_first=1\n local path=\"\" branch=\"\"\n while IFS= read -r line; do\n if [[ \"$line\" == worktree\\ * ]]; then\n path=\"${line#worktree }\"\n elif [[ \"$line\" == branch\\ * ]]; then\n branch=\"${line#branch }\"\n elif [[ -z \"$line\" ]]; then\n if [[ $in_first -eq 1 ]]; then\n in_first=0\n path=\"\"\n branch=\"\"\n continue\n fi\n if [[ -n \"$path\" ]]; then\n if [[ ! -d \"$path\" ]]; then\n orphaned=$((orphaned + 1))\n elif [[ -n \"$branch\" ]] && git merge-base --is-ancestor \"$branch\" HEAD 2>/dev/null; then\n stale=$((stale + 1))\n fi\n fi\n path=\"\"\n branch=\"\"\n fi\n done \u003c \u003c(git worktree list --porcelain 2>/dev/null; echo \"\")\n\n # SPEC-246: Cross-reference lockfiles with worktrees\n local lockfile_orphans=0\n local unclaimed=0\n local stale_locks=0\n local lockfile_dir=\"$REPO_ROOT/.agents/worktrees\"\n local lockfile_script=\"$REPO_ROOT/.agents/bin/swain-lockfile.sh\"\n\n if [[ -d \"$lockfile_dir\" ]] && [[ -f \"$lockfile_script\" ]]; then\n # Check lockfiles without corresponding worktrees\n for lockfile in \"$lockfile_dir\"/*.lock; do\n [[ -f \"$lockfile\" ]] || continue\n local lock_wt_path=\"\"\n lock_wt_path=$(grep '^worktree_path=' \"$lockfile\" | head -1 | cut -d= -f2-)\n if [[ -n \"$lock_wt_path\" ]] && [[ ! -d \"$lock_wt_path\" ]]; then\n lockfile_orphans=$((lockfile_orphans + 1))\n fi\n # Check for stale lockfiles\n local lock_branch\n lock_branch=\"$(basename \"$lockfile\" .lock)\"\n if bash \"$lockfile_script\" is-stale \"$lock_branch\" >/dev/null 2>&1; then\n stale_locks=$((stale_locks + 1))\n fi\n done\n\n # Check worktrees without lockfiles (non-trunk)\n local wt_in_first=1\n local wt_path=\"\"\n while IFS= read -r line; do\n if [[ \"$line\" == worktree\\ * ]]; then\n wt_path=\"${line#worktree }\"\n elif [[ -z \"$line\" ]]; then\n if [[ $wt_in_first -eq 1 ]]; then\n wt_in_first=0\n wt_path=\"\"\n continue\n fi\n if [[ -n \"$wt_path\" ]]; then\n # Check if any lockfile references this path\n local has_lock=false\n for lf in \"$lockfile_dir\"/*.lock; do\n [[ -f \"$lf\" ]] || continue\n if grep -q \"worktree_path=$wt_path\" \"$lf\" 2>/dev/null; then\n has_lock=true\n break\n fi\n done\n if [[ \"$has_lock\" = false ]]; then\n unclaimed=$((unclaimed + 1))\n fi\n fi\n wt_path=\"\"\n fi\n done \u003c \u003c(git worktree list --porcelain 2>/dev/null; echo \"\")\n fi\n\n # SPEC-290: Repair missing .swain/init.json symlinks in existing worktrees.\n # Worktrees created before the symlink code existed (or via using-git-worktrees)\n # lack .swain/init.json, causing swain-init-preflight to report \"onboard\" instead of \"delegate\".\n #\n # Source is always the MAIN repo root (first entry in git worktree list), not $REPO_ROOT,\n # which may itself be a linked worktree. Repair covers all linked worktrees including\n # the current one when running from inside a worktree.\n local swain_init_repaired=0\n local swain_init_missing=0\n local main_root=\"\"\n main_root=\"$(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')\"\n if [[ -n \"$main_root\" ]] && [[ -f \"$main_root/.swain/init.json\" ]]; then\n local si_in_first=1\n local si_path=\"\"\n while IFS= read -r line; do\n if [[ \"$line\" == worktree\\ * ]]; then\n si_path=\"${line#worktree }\"\n elif [[ -z \"$line\" ]]; then\n if [[ $si_in_first -eq 1 ]]; then\n si_in_first=0\n si_path=\"\"\n continue\n fi\n if [[ -n \"$si_path\" ]] && [[ -d \"$si_path\" ]]; then\n if [[ ! -e \"$si_path/.swain/init.json\" ]]; then\n mkdir -p \"$si_path/.swain\" 2>/dev/null || true\n ln -s \"$main_root/.swain/init.json\" \"$si_path/.swain/init.json\" 2>/dev/null \\\n && swain_init_repaired=$((swain_init_repaired + 1)) \\\n || swain_init_missing=$((swain_init_missing + 1))\n fi\n fi\n si_path=\"\"\n fi\n done \u003c \u003c(git worktree list --porcelain 2>/dev/null; echo \"\")\n # Also repair the current worktree if it's a linked worktree (REPO_ROOT != main_root).\n if [[ \"$REPO_ROOT\" != \"$main_root\" ]] && [[ ! -e \"$REPO_ROOT/.swain/init.json\" ]]; then\n mkdir -p \"$REPO_ROOT/.swain\" 2>/dev/null || true\n ln -s \"$main_root/.swain/init.json\" \"$REPO_ROOT/.swain/init.json\" 2>/dev/null \\\n && swain_init_repaired=$((swain_init_repaired + 1)) \\\n || swain_init_missing=$((swain_init_missing + 1))\n fi\n fi\n\n local total_issues=$((orphaned + stale + lockfile_orphans + unclaimed + stale_locks + swain_init_missing))\n if [[ $total_issues -gt 0 ]]; then\n local details=\"\"\n [[ $orphaned -gt 0 ]] && details=\"$orphaned orphaned\"\n [[ $stale -gt 0 ]] && details=\"${details:+$details, }$stale stale (merged)\"\n [[ $lockfile_orphans -gt 0 ]] && details=\"${details:+$details, }$lockfile_orphans lockfile(s) without worktree\"\n [[ $unclaimed -gt 0 ]] && details=\"${details:+$details, }$unclaimed unclaimed worktree(s)\"\n [[ $stale_locks -gt 0 ]] && details=\"${details:+$details, }$stale_locks stale lockfile(s)\"\n [[ $swain_init_missing -gt 0 ]] && details=\"${details:+$details, }$swain_init_missing worktree(s) missing .swain/init.json (symlink failed)\"\n add_check \"worktrees\" \"warning\" \"$details\"\n else\n local ok_msg=\"$((worktree_count - 1)) linked worktree(s), all active\"\n [[ $swain_init_repaired -gt 0 ]] && ok_msg=\"$ok_msg (repaired .swain/init.json symlink in $swain_init_repaired worktree(s))\"\n add_check \"worktrees\" \"ok\" \"$ok_msg\"\n fi\n}\n\n# ============================================================\n# Check 13a: Worktree context validation\n# ============================================================\n# Validates the CURRENT session's worktree (the one we're running\n# in), not all linked worktrees (that's check_worktrees).\n# Auto-fixes: ADR-034 location, lockfile creation, ADR-025 naming,\n# folder == branch consistency.\n# No symlink checks (ADR-042: track everything, not symlink).\n# ============================================================\ncheck_worktree_context() {\n local git_common git_dir\n git_common=\"$(git rev-parse --git-common-dir 2>/dev/null || true)\"\n git_dir=\"$(git rev-parse --git-dir 2>/dev/null || true)\"\n\n if [[ -n \"$git_common\" ]] && [[ \"$git_common\" != /* ]]; then\n git_common=\"$(cd \"$REPO_ROOT\" && cd \"$git_common\" 2>/dev/null && pwd || echo \"$git_common\")\"\n fi\n if [[ -n \"$git_dir\" ]] && [[ \"$git_dir\" != /* ]]; then\n git_dir=\"$(cd \"$REPO_ROOT\" && cd \"$git_dir\" 2>/dev/null && pwd || echo \"$git_dir\")\"\n fi\n\n if [[ -z \"$git_common\" ]] || [[ -z \"$git_dir\" ]] || [[ \"$git_common\" == \"$git_dir\" ]]; then\n add_check \"worktree_context\" \"ok\" \"not in a worktree\"\n return\n fi\n\n local fixed=0\n local failed=0\n local detail_parts=()\n local main_root\n main_root=\"$(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')\"\n local branch\n branch=\"$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'unknown')\"\n local current_wt_path=\"$REPO_ROOT\"\n\n # --- 1. Location sanity (ADR-034) — auto-move ---\n local expected_parent=\"$main_root/.worktrees\"\n if [[ -n \"$expected_parent\" ]] && [[ \"$current_wt_path\" != \"$expected_parent\"/* ]]; then\n local target_path=\"$expected_parent/$branch\"\n if [[ ! -e \"$target_path\" ]] && git worktree move \"$current_wt_path\" \"$target_path\" 2>/dev/null; then\n fixed=$((fixed + 1))\n detail_parts+=(\"moved to .worktrees/$branch (ADR-034)\")\n current_wt_path=\"$target_path\"\n else\n failed=$((failed + 1))\n detail_parts+=(\"outside .worktrees/ (ADR-034); fix: git worktree move $current_wt_path $target_path\")\n fi\n fi\n\n # --- 2. Lockfile creation — auto-create if missing ---\n local lockfile_dir=\"$main_root/.agents/worktrees\"\n local lockfile_path=\"$lockfile_dir/$branch.lock\"\n local lockfile_script=\"$main_root/.agents/bin/swain-lockfile.sh\"\n if [[ ! -f \"$lockfile_path\" ]]; then\n local lockfile_created=false\n if [[ -x \"$lockfile_script\" ]]; then\n local wt_purpose=\"\"\n if [[ -f \"$main_root/.agents/session.json\" ]]; then\n wt_purpose=$(grep -o '\"purpose\":\"[^\"]*' \"$main_root/.agents/session.json\" 2>/dev/null | head -1 | sed 's/\"purpose\":\"//')\n fi\n if bash \"$lockfile_script\" claim \"$branch\" \"$REPO_ROOT\" \"$wt_purpose\" >/dev/null 2>&1; then\n fixed=$((fixed + 1))\n lockfile_created=true\n detail_parts+=(\"created lockfile for $branch\")\n fi\n fi\n if [[ \"$lockfile_created\" == \"false\" ]]; then\n mkdir -p \"$lockfile_dir\"\n local actual_lockfile=\"$lockfile_path\"\n if [[ -f \"$lockfile_path\" ]]; then\n actual_lockfile=\"$lockfile_dir/$branch-$.lock\"\n fi\n local tmpfile\n tmpfile=\"$(mktemp \"$lockfile_dir/.claim-XXXXXX\")\"\n cat > \"$tmpfile\" \u003c\u003c LEOF\nversion=1\npid=$\nuser=$(whoami)\nexe=swain-doctor\npane_id=\nclaimed_at=$(date -Iseconds 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)\nworktree_path=$current_wt_path\npurpose=\nstatus=active\nLEOF\n mv \"$tmpfile\" \"$actual_lockfile\"\n fixed=$((fixed + 1))\n detail_parts+=(\"created lockfile for $branch\")\n fi\n fi\n\n # --- 3. Branch/folder naming (ADR-025) — auto-rename ---\n _wt_name_matches_adr025() {\n local name=\"$1\"\n echo \"$name\" | grep -qiE '^(spec|spike|adr|vision|journey|persona|runbook|design|train|epic|initiative)-[0-9]+' && return 0\n echo \"$name\" | grep -qiE '^[a-z].*-[0-9]{8}-(epic|initiative)-[0-9]+' && return 0\n echo \"$name\" | grep -qiE '^session-[0-9]{8}-[0-9]{6}' && return 0\n return 1\n }\n\n if ! _wt_name_matches_adr025 \"$branch\"; then\n local new_name=\"\"\n local name_script=\"$main_root/.agents/bin/swain-worktree-name.sh\"\n local wt_purpose=\"\"\n if [[ -f \"$lockfile_dir/$branch.lock\" ]]; then\n wt_purpose=$(grep '^purpose=' \"$lockfile_dir/$branch.lock\" | head -1 | sed 's/^purpose=//' | sed 's/^\"//;s/\"$//')\n fi\n if [[ -x \"$name_script\" ]] && [[ -n \"$wt_purpose\" ]]; then\n new_name=$(REPO_ROOT=\"$main_root\" PURPOSE=\"$wt_purpose\" bash \"$name_script\" \"$wt_purpose\" 2>/dev/null || true)\n fi\n if [[ -z \"$new_name\" ]]; then\n new_name=\"session-$(date +%Y%m%d-%H%M%S)\"\n fi\n if [[ -n \"$new_name\" ]] && [[ \"$new_name\" != \"$branch\" ]]; then\n if git branch -m \"$branch\" \"$new_name\" 2>/dev/null; then\n local new_lockfile=\"$lockfile_dir/$new_name.lock\"\n if [[ -f \"$lockfile_dir/$branch.lock\" ]] && [[ ! -f \"$new_lockfile\" ]]; then\n mv \"$lockfile_dir/$branch.lock\" \"$new_lockfile\" 2>/dev/null\n fi\n local old_wt_path new_wt_path\n old_wt_path=\"$main_root/.worktrees/$branch\"\n new_wt_path=\"$main_root/.worktrees/$new_name\"\n if [[ -d \"$old_wt_path\" ]]; then\n git worktree move \"$old_wt_path\" \"$new_wt_path\" 2>/dev/null || true\n fi\n fixed=$((fixed + 1))\n detail_parts+=(\"renamed $branch -> $new_name (ADR-025)\")\n branch=\"$new_name\"\n else\n failed=$((failed + 1))\n detail_parts+=(\"branch '$branch' violates ADR-025; auto-rename failed\")\n fi\n fi\n fi\n\n # --- 4. Folder name == branch name ---\n local folder_name\n folder_name=\"$(basename \"$current_wt_path\")\"\n if [[ \"$folder_name\" != \"$branch\" ]]; then\n local target_path=\"$main_root/.worktrees/$branch\"\n if [[ ! -e \"$target_path\" ]]; then\n if git worktree move \"$current_wt_path\" \"$target_path\" 2>/dev/null; then\n fixed=$((fixed + 1))\n detail_parts+=(\"renamed folder $folder_name -> $branch\")\n current_wt_path=\"$target_path\"\n else\n failed=$((failed + 1))\n detail_parts+=(\"folder '$folder_name' != branch '$branch'; fix: git worktree move $current_wt_path $target_path\")\n fi\n fi\n fi\n\n # --- Build result ---\n if [[ ${#detail_parts[@]} -eq 0 ]]; then\n add_check \"worktree_context\" \"ok\" \"in worktree for $branch\"\n elif [[ $failed -eq 0 ]]; then\n local detail_str\n detail_str=$(printf '%s; ' \"${detail_parts[@]}\" | sed 's/; $//')\n add_check \"worktree_context\" \"advisory\" \"auto-fixed: $detail_str\"\n else\n local detail_str\n detail_str=$(printf '%s; ' \"${detail_parts[@]}\" | sed 's/; $//')\n add_check \"worktree_context\" \"warning\" \"$detail_str\"\n fi\n}\n\n# ============================================================\n# Check 13: Lifecycle directory migration\n# ============================================================\ncheck_lifecycle_dirs() {\n local old_phases=\"Draft Planned Review Approved Testing Implemented Adopted Deprecated Archived Sunset Validated\"\n local found=0\n\n for dir in docs/*/; do\n [[ -d \"$dir\" ]] || continue\n for phase in $old_phases; do\n local phase_dir=\"${dir}${phase}\"\n if [[ -d \"$phase_dir\" ]]; then\n if find \"$phase_dir\" -maxdepth 1 -not -name '.*' -not -name \"$(basename \"$phase_dir\")\" -print -quit 2>/dev/null | grep -q .; then\n found=$((found + 1))\n fi\n fi\n done\n done\n\n if [[ $found -gt 0 ]]; then\n add_check \"lifecycle_dirs\" \"warning\" \"$found old lifecycle directory(ies) found — run migrate-lifecycle-dirs.py\"\n else\n add_check \"lifecycle_dirs\" \"ok\" \"no old lifecycle directories\"\n fi\n}\n\n# ============================================================\n# Check 14: tk health\n# ============================================================\ncheck_tk_health() {\n local tk_bin=\"$SKILLS_ROOT/swain-do/bin/tk\"\n if [[ ! -x \"$tk_bin\" ]]; then\n add_check \"tk_health\" \"warning\" \"vendored tk not found or not executable\"\n return\n fi\n\n if [[ ! -d .tickets ]]; then\n add_check \"tk_health\" \"ok\" \"tk available, no .tickets/ (skipped)\"\n return\n fi\n\n add_check \"tk_health\" \"ok\" \"tk available and .tickets/ present\"\n}\n\n# ============================================================\n# Check 15: Operator bin/ symlinks (SPEC-214, ADR-019)\n# Scans installed skill `usr/bin/` manifest directories for operator-facing\n# scripts and auto-repairs bin/ symlinks.\n# ============================================================\ncheck_operator_bin_symlinks() {\n local bin_dir=\"$REPO_ROOT/bin\"\n local repaired=0\n local conflicts=()\n local repairs=()\n local manifest_count=0\n\n # Scan all usr/bin/ manifest directories in the skill tree\n for manifest_dir in \"$SKILLS_ROOT\"/*/usr/bin; do\n [[ -d \"$manifest_dir\" ]] || continue\n for entry in \"$manifest_dir\"/*; do\n [[ -e \"$entry\" || -L \"$entry\" ]] || continue\n local cmd_name\n cmd_name=\"$(basename \"$entry\")\"\n manifest_count=$((manifest_count + 1))\n\n # Resolve the actual script through the manifest symlink\n local script_path\n script_path=\"$(cd \"$manifest_dir\" && readlink -f \"$cmd_name\" 2>/dev/null || true)\"\n if [[ -z \"$script_path\" || ! -f \"$script_path\" ]]; then\n # Manifest entry points to a missing script — skip\n continue\n fi\n\n # Compute relative path from bin/ to the script\n local rel_path\n rel_path=\"$(python3 -c \"import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))\" \"$script_path\" \"$bin_dir\" 2>/dev/null || echo \"\")\"\n [[ -z \"$rel_path\" ]] && continue\n\n if [[ -L \"$bin_dir/$cmd_name\" ]]; then\n # Symlink exists — check if target is correct\n local current_target\n current_target=\"$(readlink \"$bin_dir/$cmd_name\")\"\n if [[ \"$(cd \"$bin_dir\" && readlink -f \"$cmd_name\" 2>/dev/null)\" == \"$script_path\" ]]; then\n continue # resolves correctly\n fi\n # Stale — replace\n ln -sf \"$rel_path\" \"$bin_dir/$cmd_name\"\n repairs+=(\"$cmd_name (stale, repaired)\")\n repaired=$((repaired + 1))\n elif [[ -e \"$bin_dir/$cmd_name\" ]]; then\n # Real file — conflict, don't overwrite\n conflicts+=(\"$cmd_name\")\n else\n # Missing — auto-repair\n mkdir -p \"$bin_dir\"\n ln -sf \"$rel_path\" \"$bin_dir/$cmd_name\"\n repairs+=(\"$cmd_name (created)\")\n repaired=$((repaired + 1))\n fi\n done\n done\n\n if [[ \"$manifest_count\" -eq 0 ]]; then\n add_check \"operator_bin_symlinks\" \"ok\" \"no operator scripts in usr/bin/ manifests\"\n return\n fi\n\n local issues=()\n [[ ${#conflicts[@]} -gt 0 ]] && issues+=(\"${#conflicts[@]} conflict(s): ${conflicts[*]}\")\n [[ ${#repairs[@]} -gt 0 ]] && issues+=(\"${#repairs[@]} repaired: ${repairs[*]}\")\n\n if [[ ${#issues[@]} -eq 0 ]]; then\n add_check \"operator_bin_symlinks\" \"ok\" \"bin/ symlinks for $manifest_count operator script(s) OK\"\n elif [[ ${#conflicts[@]} -gt 0 ]]; then\n local detail\n detail=$(printf '%s; ' \"${issues[@]}\")\n add_check \"operator_bin_symlinks\" \"warning\" \"bin/ symlink issues\" \"${detail%;* }\"\n else\n local detail\n detail=$(printf '%s; ' \"${issues[@]}\")\n add_check \"operator_bin_symlinks\" \"ok\" \"bin/ symlinks repaired\" \"${detail%;* }\"\n fi\n}\n\n# ============================================================\n# Check 16: Commit signing (SPEC-222: auto-repair if signing key detectable)\n# ============================================================\ncheck_commit_signing() {\n if [[ \"$(git config --local commit.gpgsign 2>/dev/null)\" == \"true\" ]]; then\n add_check \"commit_signing\" \"ok\" \"commit signing configured\"\n return\n fi\n\n # Detect a usable signing key\n local key_found=false\n local conventional_key=\"$HOME/.ssh/swain_signing\"\n local allowed_signers\n allowed_signers=$(git config --global gpg.ssh.allowedSignersFile 2>/dev/null || echo \"\")\n\n [[ -f \"$conventional_key\" ]] && key_found=true\n [[ -n \"$allowed_signers\" && -f \"$allowed_signers\" ]] && key_found=true\n\n if [[ \"$key_found\" == \"true\" ]]; then\n git config --local commit.gpgsign true\n git config --local gpg.format ssh\n add_check \"commit_signing\" \"advisory\" \"commit signing enabled (gpgsign=true, gpg.format=ssh)\"\n else\n add_check \"commit_signing\" \"warning\" \"commit signing not configured (no signing key detected)\"\n fi\n}\n\n# ============================================================\n# Check 17: SSH alias readiness\n# ============================================================\ncheck_ssh_readiness() {\n local ssh_helper=\"$SCRIPT_DIR/ssh-readiness.sh\"\n if [[ ! -x \"$ssh_helper\" ]]; then\n add_check \"ssh_readiness\" \"ok\" \"ssh-readiness helper not found (skipped)\"\n return\n fi\n\n local ssh_output\n ssh_output=$(bash \"$ssh_helper\" --check 2>/dev/null || true)\n if [[ -n \"$ssh_output\" ]]; then\n local issue_count\n issue_count=$(echo \"$ssh_output\" | grep -c \"ISSUE:\") || issue_count=0\n add_check \"ssh_readiness\" \"warning\" \"$issue_count SSH readiness issue(s)\" \"$ssh_output\"\n else\n add_check \"ssh_readiness\" \"ok\" \"SSH alias readiness OK\"\n fi\n}\n\n# Check: README existence (SPEC-208)\n# ============================================================\ncheck_readme() {\n if [[ -f \"README.md\" ]]; then\n add_check \"readme\" \"ok\" \"README.md exists\"\n else\n add_check \"readme\" \"warning\" \"README.md missing — swain alignment loop has no public intent anchor\"\n fi\n}\n\n# ============================================================\n# Check 12: Artifact index staleness (SPEC-227)\n# Regenerates supported list-*.md files and reports deterministic repairs.\n# ============================================================\ncheck_artifact_indexes() {\n local rebuild_script=\"$REPO_ROOT/.agents/bin/rebuild-index.sh\"\n if [[ ! -x \"$rebuild_script\" ]]; then\n rebuild_script=\"$SKILLS_ROOT/swain-design/scripts/rebuild-index.sh\"\n fi\n\n if [[ ! -x \"$rebuild_script\" ]]; then\n add_check \"artifact_indexes\" \"warning\" \"rebuild-index.sh not found or not executable\"\n return\n fi\n\n local repaired=()\n local failures=()\n local type dir_name docs_dir index_file before_exists before_hash after_hash\n\n for type in spec epic initiative spike adr persona runbook design vision journey train; do\n dir_name=\"$type\"\n case \"$type\" in\n spike) dir_name=\"research\" ;;\n esac\n\n docs_dir=\"$REPO_ROOT/docs/$dir_name\"\n index_file=\"$docs_dir/list-${type}.md\"\n [[ -d \"$docs_dir\" ]] || continue\n\n before_exists=false\n before_hash=\"\"\n if [[ -f \"$index_file\" ]]; then\n before_exists=true\n before_hash=$(shasum -a 256 \"$index_file\" | awk '{print $1}')\n fi\n\n if ! bash \"$rebuild_script\" \"$type\" >/dev/null 2>&1; then\n failures+=(\"$type\")\n continue\n fi\n\n if [[ ! -f \"$index_file\" ]]; then\n failures+=(\"$type\")\n continue\n fi\n\n after_hash=$(shasum -a 256 \"$index_file\" | awk '{print $1}')\n if [[ \"$before_exists\" == \"false\" ]]; then\n repaired+=(\"${type} (created)\")\n elif [[ \"$before_hash\" != \"$after_hash\" ]]; then\n repaired+=(\"${type} (updated)\")\n fi\n done\n\n if [[ ${#failures[@]} -gt 0 ]]; then\n local detail\n detail=$(printf '%s, ' \"${failures[@]}\")\n if [[ ${#repaired[@]} -gt 0 ]]; then\n add_check \"artifact_indexes\" \"warning\" \"artifact indexes partially repaired\" \"repaired: ${repaired[*]}; failed: ${detail%, }\"\n else\n add_check \"artifact_indexes\" \"warning\" \"artifact index rebuild failed\" \"${detail%, }\"\n fi\n return\n fi\n\n if [[ ${#repaired[@]} -gt 0 ]]; then\n add_check \"artifact_indexes\" \"advisory\" \"repaired ${#repaired[@]} artifact index file(s)\" \"${repaired[*]}\"\n else\n add_check \"artifact_indexes\" \"ok\" \"artifact indexes current\"\n fi\n}\n\n# ============================================================\n# Check 18: Crash debris detection (SPEC-182, SPEC-222: auto-repair git lock only)\n# ============================================================\ncheck_crash_debris() {\n local lib=\"$SCRIPT_DIR/crash-debris-lib.sh\"\n if [[ ! -f \"$lib\" ]]; then\n add_check \"crash_debris\" \"ok\" \"crash-debris-lib.sh not found (skipped)\"\n return\n fi\n\n source \"$lib\"\n local output\n output=$(check_all_crash_debris \"$REPO_ROOT\" 2>/dev/null || true)\n\n local found_count\n found_count=$(echo \"$output\" | grep -c 'found' 2>/dev/null) || found_count=0\n\n if [[ \"$found_count\" -eq 0 ]]; then\n add_check \"crash_debris\" \"ok\" \"no crash debris detected\"\n return\n fi\n\n # Auto-repair: remove stale .git/index.lock if found (safe regardless of other debris)\n local lock_lines other_lines lock_removed=false\n lock_lines=$(echo \"$output\" | grep 'found' | grep '^git_index_lock' || true)\n other_lines=$(echo \"$output\" | grep 'found' | grep -v '^git_index_lock' || true)\n\n if [[ -n \"$lock_lines\" ]]; then\n local git_dir=\"$REPO_ROOT/.git\"\n if [[ -f \"$git_dir\" ]]; then\n git_dir=$(sed 's/^gitdir: //' \"$git_dir\")\n [[ \"$git_dir\" != /* ]] && git_dir=\"$REPO_ROOT/$git_dir\"\n fi\n local lock_file=\"$git_dir/index.lock\"\n if [[ -f \"$lock_file\" ]]; then\n rm -f \"$lock_file\"\n lock_removed=true\n fi\n fi\n\n if [[ \"$lock_removed\" == \"true\" && -z \"$other_lines\" ]]; then\n add_check \"crash_debris\" \"advisory\" \"removed stale .git/index.lock\"\n return\n fi\n\n if [[ \"$lock_removed\" == \"true\" ]]; then\n # Lock removed but other debris remains — warn about remaining items\n local remaining_count\n remaining_count=$(echo \"$other_lines\" | grep -c . 2>/dev/null) || remaining_count=0\n local details\n details=$(echo \"$other_lines\" | cut -f3 | tr '\\n' '; ' | sed 's/; $//')\n add_check \"crash_debris\" \"warning\" \"removed .git/index.lock; $remaining_count other debris item(s) remain\" \"$details\"\n return\n fi\n\n local details\n details=$(echo \"$output\" | grep 'found' | cut -f3 | tr '\\n' '; ' | sed 's/; $//')\n add_check \"crash_debris\" \"warning\" \"$found_count crash debris item(s) detected\" \"$details\"\n}\n\n# ============================================================\n# Check 19: bin/swain symlink (SPEC-180, ADR-019)\n# ============================================================\ncheck_swain_symlink() {\n local symlink=\"$REPO_ROOT/bin/swain\"\n if [[ ! -L \"$symlink\" ]]; then\n if [[ -f \"$SKILLS_ROOT/swain/scripts/swain\" ]]; then\n add_check \"swain_symlink\" \"warning\" \"bin/swain symlink missing (script exists at $SKILLS_ROOT/swain/scripts/swain)\"\n else\n add_check \"swain_symlink\" \"ok\" \"bin/swain not applicable (no pre-runtime script)\"\n fi\n return\n fi\n\n if [[ ! -e \"$symlink\" ]]; then\n add_check \"swain_symlink\" \"warning\" \"bin/swain symlink broken (target missing)\"\n return\n fi\n\n add_check \"swain_symlink\" \"ok\" \"bin/swain symlink resolves\"\n}\n\n# ============================================================\n# Check 20: .agents/bin/ symlink completeness (SPEC-206)\n# Aligns with preflight auto-repair (ADR-019, SPEC-186):\n# - Scans all executable files in the installed skill tree (not just .sh)\n# - Excludes test-* and operator-facing scripts (SPEC-214 manifest-driven)\n# - Uses os.path.relpath for portable symlink targets\n# - Auto-repairs missing/stale symlinks (detect + fix)\n# ============================================================\ncheck_agents_bin_symlinks() {\n local bin_dir=\"$REPO_ROOT/.agents/bin\"\n if [[ ! -d \"$bin_dir\" ]]; then\n mkdir -p \"$bin_dir\"\n add_check \"agents_bin_symlinks\" \"warning\" \".agents/bin/ directory was missing (created)\"\n return\n fi\n\n local broken=()\n local missing=()\n local stale=()\n local repaired=0\n\n # Check for broken symlinks in .agents/bin/\n while IFS= read -r link; do\n [[ -z \"$link\" ]] && continue\n if [[ ! -e \"$link\" ]]; then\n broken+=(\"$(basename \"$link\")\")\n rm -f \"$link\"\n fi\n done \u003c \u003c(find \"$bin_dir\" -type l 2>/dev/null)\n\n # Build operator-script exclusion set from usr/bin/ manifests (SPEC-214)\n local operator_scripts=\" \"\n for manifest_dir in \"$SKILLS_ROOT\"/*/usr/bin; do\n [[ -d \"$manifest_dir\" ]] || continue\n for entry in \"$manifest_dir\"/*; do\n [[ -e \"$entry\" || -L \"$entry\" ]] || continue\n operator_scripts+=\"$(basename \"$entry\") \"\n done\n done\n\n # Scan all executable scripts in the installed skill tree (ADR-019 convention)\n for skill_scripts_dir in \"$SKILLS_ROOT\"/*/scripts; do\n [[ -d \"$skill_scripts_dir\" ]] || continue\n for script in \"$skill_scripts_dir\"/*; do\n [[ -f \"$script\" && -x \"$script\" ]] || continue\n local script_name\n script_name=\"$(basename \"$script\")\"\n # Skip test scripts and operator-facing scripts\n [[ \"$script_name\" == test-* || \"$script_name\" == test_* ]] && continue\n # Skip operator-facing scripts — those belong in bin/, not .agents/bin/\n echo \"$operator_scripts\" | grep -q \" $script_name \" && continue\n # Compute portable relative path (works in worktrees and trunk)\n local target=\"$bin_dir/$script_name\"\n local rel_path\n rel_path=\"$(python3 -c \"import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))\" \"$script\" \"$bin_dir\" 2>/dev/null || echo \"\")\"\n [[ -z \"$rel_path\" ]] && continue\n if [[ -L \"$target\" ]] && [[ \"$(readlink \"$target\")\" == \"$rel_path\" ]]; then\n continue # ok\n elif [[ -e \"$target\" ]] && [[ ! -L \"$target\" ]]; then\n missing+=(\"$script_name (conflict: real file)\")\n elif [[ -L \"$target\" ]]; then\n # stale — wrong target\n stale+=(\"$script_name\")\n ln -sf \"$rel_path\" \"$target\"\n repaired=$((repaired + 1))\n else\n # missing — auto-repair\n missing+=(\"$script_name\")\n ln -sf \"$rel_path\" \"$target\"\n repaired=$((repaired + 1))\n fi\n done\n done\n\n local issues=()\n [[ ${#broken[@]} -gt 0 ]] && issues+=(\"${#broken[@]} broken (removed): ${broken[*]}\")\n [[ ${#stale[@]} -gt 0 ]] && issues+=(\"${#stale[@]} stale (repaired): ${stale[*]}\")\n [[ ${#missing[@]} -gt 0 ]] && issues+=(\"${#missing[@]} missing (repaired): ${missing[*]}\")\n\n if [[ ${#issues[@]} -eq 0 ]]; then\n add_check \"agents_bin_symlinks\" \"ok\" \".agents/bin/ symlinks complete\"\n elif [[ $repaired -gt 0 ]]; then\n local detail\n detail=$(printf '%s; ' \"${issues[@]}\")\n add_check \"agents_bin_symlinks\" \"advisory\" \"repaired $repaired .agents/bin/ symlink(s)\" \"${detail%;* }\"\n else\n local detail\n detail=$(printf '%s; ' \"${issues[@]}\")\n add_check \"agents_bin_symlinks\" \"warning\" \".agents/bin/ symlink issues\" \"${detail%;* }\"\n fi\n}\n\n# Check: Flat-file artifact detection (ADR-027, SPEC-225)\ncheck_flat_artifacts() {\n local fix_mode=false\n [[ \"${1:-}\" == \"--fix\" ]] && fix_mode=true\n\n # Artifact directories to scan (type -> docs subdir)\n # Retros excluded — SPEC-252 handles retro renumbering + foldering separately\n local -a dirs=()\n for d in docs/spec docs/epic docs/adr docs/initiative docs/research \\\n docs/vision docs/design docs/persona docs/runbook docs/journey \\\n docs/train; do\n [[ -d \"$d\" ]] && dirs+=(\"$d\")\n done\n\n if [[ ${#dirs[@]} -eq 0 ]]; then\n add_check \"flat_artifacts\" \"ok\" \"no artifact directories found (skipped)\"\n return\n fi\n\n # Find .md files in phase directories that are NOT inside a (TYPE-NNN) folder.\n # Flat files sit at: docs/\u003ctype>/\u003cPhase>/filename.md\n # Foldered files sit at: docs/\u003ctype>/\u003cPhase>/(TYPE-NNN)-Title/filename.md\n # Also catch flat retros: docs/swain-retro/filename.md (no phase subdir)\n local -a flat_files=()\n for d in \"${dirs[@]}\"; do\n while IFS= read -r f; do\n [[ -z \"$f\" ]] && continue\n local parent_name\n parent_name=\"$(basename \"$(dirname \"$f\")\")\"\n # Skip index files and READMEs\n local fname\n fname=\"$(basename \"$f\")\"\n [[ \"$fname\" == list-* ]] && continue\n [[ \"$fname\" == README.md ]] && continue\n # If parent dir name starts with ( it's inside a folder — skip\n [[ \"$parent_name\" == \\(* ]] && continue\n flat_files+=(\"$f\")\n done \u003c \u003c(find \"$d\" -maxdepth 3 -name \"*.md\" -not -path \"*/_unparented/*\" -not -path \"*/_Related/*\" -not -path \"*/_Depends-On/*\" 2>/dev/null)\n done\n\n if [[ ${#flat_files[@]} -eq 0 ]]; then\n add_check \"flat_artifacts\" \"ok\" \"all artifacts are foldered\"\n return\n fi\n\n if $fix_mode; then\n local migrated=0\n local failed=0\n for f in \"${flat_files[@]}\"; do\n # Parse artifact ID from frontmatter\n local artifact_id\n artifact_id=$(sed -n 's/^artifact: *//p' \"$f\" | head -1 | tr -d '[:space:]')\n if [[ -z \"$artifact_id\" ]]; then\n failed=$((failed + 1))\n continue\n fi\n\n # Derive folder name from filename convention\n local fname parent_dir folder_name target_dir target_file\n fname=\"$(basename \"$f\" .md)\"\n parent_dir=\"$(dirname \"$f\")\"\n\n # Build canonical folder name from frontmatter title\n local raw_title\n raw_title=$(sed -n 's/^title: *\"\\{0,1\\}\\(.*\\)\"\\{0,1\\}$/\\1/p' \"$f\" | head -1 | sed 's/^ *//;s/ *$//')\n if [[ -n \"$raw_title\" ]]; then\n # Title-Case with hyphens: lowercase → capitalize first letter of each word → spaces to hyphens\n local title_slug\n title_slug=$(echo \"$raw_title\" | sed 's/[^a-zA-Z0-9 -]//g; s/ */ /g; s/ /-/g')\n folder_name=\"(${artifact_id})-${title_slug}\"\n elif [[ \"$fname\" == \\(* ]]; then\n # Filename already has parens — use as-is\n folder_name=\"$fname\"\n elif [[ \"$fname\" == \"${artifact_id}\"* ]]; then\n # Filename starts with artifact ID — wrap in parens\n local title_part=\"${fname#\"${artifact_id}\"}\"\n title_part=\"${title_part#-}\"\n folder_name=\"(${artifact_id})-${title_part}\"\n else\n # Fallback: wrap artifact ID + filename\n folder_name=\"(${artifact_id})-${fname}\"\n fi\n\n target_dir=\"${parent_dir}/${folder_name}\"\n target_file=\"${target_dir}/${folder_name}.md\"\n\n if [[ -d \"$target_dir\" ]]; then\n failed=$((failed + 1))\n continue\n fi\n\n mkdir -p \"$target_dir\"\n git mv \"$f\" \"$target_file\" 2>/dev/null\n if [[ $? -eq 0 ]]; then\n migrated=$((migrated + 1))\n else\n # Fallback: plain move if not tracked\n mv \"$f\" \"$target_file\" 2>/dev/null && migrated=$((migrated + 1)) || failed=$((failed + 1))\n fi\n done\n\n if [[ $failed -gt 0 ]]; then\n add_check \"flat_artifacts\" \"warning\" \"migrated $migrated, failed $failed flat-file artifact(s)\"\n else\n add_check \"flat_artifacts\" \"advisory\" \"migrated $migrated flat-file artifact(s) to folders\"\n fi\n else\n # Report only\n local detail\n detail=$(printf '%s;' \"${flat_files[@]:0:10}\")\n [[ ${#flat_files[@]} -gt 10 ]] && detail=\"${detail}... and $((${#flat_files[@]} - 10)) more\"\n add_check \"flat_artifacts\" \"warning\" \"${#flat_files[@]} flat-file artifact(s) — run swain-doctor --fix-flat-artifacts to migrate\" \"$detail\"\n fi\n}\n\n# ============================================================\n# Check: Branch model (ADR-013)\n# ============================================================\ncheck_branch_model() {\n local has_trunk has_release\n has_trunk=$(git rev-parse --verify trunk >/dev/null 2>&1 && echo \"yes\" || echo \"no\")\n has_release=$(git rev-parse --verify release >/dev/null 2>&1 && echo \"yes\" || echo \"no\")\n\n if [[ \"$has_trunk\" == \"yes\" && \"$has_release\" == \"yes\" ]]; then\n add_check \"branch_model\" \"ok\" \"trunk+release branches present\"\n else\n local missing=\"\"\n [[ \"$has_trunk\" == \"no\" ]] && missing=\"trunk\"\n [[ \"$has_release\" == \"no\" ]] && missing=\"${missing:+$missing, }release\"\n add_check \"branch_model\" \"advisory\" \"missing branch(es): $missing — see ADR-013\"\n fi\n}\n\n# ============================================================\n# Check: Platform dotfolder cleanup\n# ============================================================\ncheck_platform_dotfolders() {\n if ! command -v jq >/dev/null 2>&1; then\n add_check \"platform_dotfolders\" \"skipped\" \"jq not available\"\n return\n fi\n\n local json_file=\"$SKILL_DIR/references/platform-dotfolders.json\"\n if [[ ! -f \"$json_file\" ]]; then\n add_check \"platform_dotfolders\" \"skipped\" \"platform-dotfolders.json not found\"\n return\n fi\n\n local stubs=()\n while IFS= read -r entry; do\n local dotfolder cmd det found=false\n dotfolder=$(echo \"$entry\" | jq -r '.project_dotfolder')\n cmd=$(echo \"$entry\" | jq -r '.command // empty')\n det=$(echo \"$entry\" | jq -r '.detection // empty')\n\n if [[ -n \"$cmd\" ]] && command -v \"$cmd\" &>/dev/null; then\n found=true\n fi\n if [[ \"$found\" == \"false\" && -n \"$det\" ]]; then\n local det_expanded\n det_expanded=$(echo \"$det\" | sed \"s|~|$HOME|g\")\n det_expanded=$(eval echo \"$det_expanded\" 2>/dev/null || echo \"\")\n [[ -n \"$det_expanded\" && -d \"$det_expanded\" ]] && found=true\n fi\n\n # If platform not installed but dotfolder exists in project, it's a stub\n if [[ \"$found\" == \"false\" && -d \"$REPO_ROOT/$dotfolder\" ]]; then\n # Verify it's an installer stub (only contains skills/ or is empty)\n local entries\n entries=$(ls -A \"$REPO_ROOT/$dotfolder\" 2>/dev/null | wc -l | tr -d ' ')\n if [[ \"$entries\" -le 1 ]] && { [[ -d \"$REPO_ROOT/$dotfolder/skills\" ]] || [[ \"$entries\" -eq 0 ]]; }; then\n stubs+=(\"$dotfolder\")\n fi\n fi\n done \u003c \u003c(jq -c '.platforms[]' \"$json_file\")\n\n if [[ ${#stubs[@]} -eq 0 ]]; then\n add_check \"platform_dotfolders\" \"ok\" \"no orphaned platform dotfolders\"\n else\n add_check \"platform_dotfolders\" \"warning\" \"${#stubs[@]} orphaned platform dotfolder(s): ${stubs[*]}\"\n fi\n}\n\n# ============================================================\n# Check: Skill folder gitignore hygiene\n# ============================================================\ncheck_skill_gitignore() {\n # Skip if this is the swain source repo\n local remote_url\n remote_url=\"$(git remote get-url origin 2>/dev/null || true)\"\n if [[ \"$remote_url\" == *\"cristoslc/swain\"* ]]; then\n add_check \"skill_gitignore\" \"ok\" \"swain source repo — skill folders are tracked\"\n return\n fi\n\n local missing=()\n for base in .claude/skills .agents/skills; do\n [[ -d \"$base\" ]] || continue\n for dir in \"$base\"/swain \"$base\"/swain-*/; do\n [[ -d \"$dir\" ]] || continue\n if ! git check-ignore -q \"$dir\" 2>/dev/null; then\n missing+=(\"$dir\")\n fi\n done\n done\n\n if [[ ${#missing[@]} -eq 0 ]]; then\n add_check \"skill_gitignore\" \"ok\" \"vendored swain skill folders gitignored (or none exist)\"\n else\n add_check \"skill_gitignore\" \"warning\" \"${#missing[@]} vendored swain skill folder(s) not gitignored: ${missing[*]}\"\n fi\n}\n\n# ============================================================\n# Migrate legacy .swain-init marker to .swain/init.json\n# ============================================================\nif [[ -f \".swain-init\" ]] && [[ ! -f \".swain/init.json\" ]]; then\n mkdir -p \".swain\"\n mv \".swain-init\" \".swain/init.json\"\nfi\n\n# ============================================================\n# Run all checks (set +e so failures don't cascade)\n# ============================================================\nset +e\n\ncheck_governance\ncheck_legacy_skills\ncheck_agents_directory\ncheck_tickets\ncheck_beads\ncheck_tools\ncheck_settings\ncheck_script_permissions\ncheck_memory_directory\ncheck_superpowers\ncheck_epics_initiative\ncheck_readme\ncheck_artifact_indexes\ncheck_evidence_pools\ncheck_worktrees\ncheck_worktree_context\ncheck_lifecycle_dirs\ncheck_tk_health\ncheck_operator_bin_symlinks\ncheck_commit_signing\ncheck_ssh_readiness\ncheck_crash_debris\ncheck_agents_bin_symlinks\ncheck_branch_model\ncheck_platform_dotfolders\ncheck_skill_gitignore\nif $FIX_FLAT; then\n check_flat_artifacts --fix\nelse\n check_flat_artifacts\nfi\n\nset -e\n\n# ============================================================\n# Build JSON output\n# ============================================================\ntotal=${#CHECKS[@]}\nok_count=0\nwarning_count=0\nadvisory_count=0\n\nfor check in \"${CHECKS[@]}\"; do\n status=$(echo \"$check\" | sed -n 's/.*\"status\":\"\\([^\"]*\\)\".*/\\1/p')\n case \"$status\" in\n ok) ok_count=$((ok_count + 1)) ;;\n warning) warning_count=$((warning_count + 1)) ;;\n advisory) advisory_count=$((advisory_count + 1)) ;;\n esac\ndone\n\n# Assemble JSON\nchecks_json=\"\"\nfor i in \"${!CHECKS[@]}\"; do\n if [[ $i -gt 0 ]]; then\n checks_json=\"$checks_json,\"\n fi\n checks_json=\"$checks_json${CHECKS[$i]}\"\ndone\n\ncat \u003c\u003cENDJSON\n{\"checks\":[$checks_json],\"summary\":{\"total\":$total,\"ok\":$ok_count,\"warning\":$warning_count,\"advisory\":$advisory_count}}\nENDJSON\n\nexit 0\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":52118,"content_sha256":"788b03a2511008248988215558382023396955b2c31d3e1fabea309f2ee84bf8"},{"filename":"scripts/swain-initiative-scan.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\n# Scan epics without parent-initiative, grouped by parent-vision\n# Usage: swain-initiative-scan.sh\n\nREPO_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null)\" || {\n echo \"Error: not inside a git repository\" >&2\n exit 1\n}\n\nEPIC_DIR=\"$REPO_ROOT/docs/epic\"\n\nif [[ ! -d \"$EPIC_DIR\" ]]; then\n echo \"No docs/epic directory found — nothing to scan.\"\n exit 0\nfi\n\necho \"=== Epics without parent-initiative ===\"\necho \"\"\necho \"VISION | EPIC | TITLE\"\necho \"-------|------|------\"\n\nfound=0\nwhile IFS= read -r -d '' f; do\n if grep -q '^parent-vision:' \"$f\" 2>/dev/null && ! grep -q '^parent-initiative:' \"$f\" 2>/dev/null; then\n vision=$(grep '^parent-vision:' \"$f\" | head -1 | sed 's/parent-vision: *//' | tr -d ' ')\n artifact=$(grep '^artifact:' \"$f\" | head -1 | sed 's/artifact: *//' | tr -d ' ')\n title=$(grep '^title:' \"$f\" | head -1 | sed 's/title: *//' | tr -d '\"')\n echo \"$vision | $artifact | $title\"\n found=$((found + 1))\n fi\ndone \u003c \u003c(find \"$EPIC_DIR\" -name '*.md' -not -name 'README.md' -not -name 'list-*.md' -print0 2>/dev/null | sort -z)\n\necho \"\"\nif [[ \"$found\" -eq 0 ]]; then\n echo \"All epics have parent-initiative. Migration complete.\"\nelse\n echo \"$found epic(s) need parent-initiative assignment.\"\n echo \"\"\n\n orphan_count=0\n echo \"Orphaned epics (no parent-vision or parent-initiative):\"\n while IFS= read -r -d '' f; do\n if ! grep -q '^parent-vision:' \"$f\" 2>/dev/null && ! grep -q '^parent-initiative:' \"$f\" 2>/dev/null; then\n artifact=$(grep '^artifact:' \"$f\" | head -1 | sed 's/artifact: *//' | tr -d ' ')\n title=$(grep '^title:' \"$f\" | head -1 | sed 's/title: *//' | tr -d '\"')\n echo \" $artifact | $title\"\n orphan_count=$((orphan_count + 1))\n fi\n done \u003c \u003c(find \"$EPIC_DIR\" -name '*.md' -not -name 'README.md' -not -name 'list-*.md' -print0 2>/dev/null | sort -z)\n\n if [[ \"$orphan_count\" -eq 0 ]]; then\n echo \" (none)\"\n fi\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1932,"content_sha256":"60487f759d6249fd38a2adf4480f1bef8a31fe72aa5d0875b8d75b67d3afe931"},{"filename":"scripts/swain-preflight.sh","content":"#!/usr/bin/env bash\n# swain-preflight.sh — lightweight session-start check\n#\n# Exit 0 = everything looks fine, skip swain-doctor\n# Exit 1 = something needs attention, invoke swain-doctor\n#\n# This replaces the unconditional auto-invoke of swain-doctor,\n# saving tokens on clean sessions. See ADR-001 / SPEC-008.\n\nset -euo pipefail\n\nREPO_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"\ncd \"$REPO_ROOT\"\n\n# Portable path resolution — works whether installed at skills/ or .agents/skills/\n_src=\"${BASH_SOURCE[0]}\"\nwhile [[ -L \"$_src\" ]]; do\n _dir=\"$(cd \"$(dirname \"$_src\")\" && pwd)\"\n _src=\"$(readlink \"$_src\")\"\n [[ \"$_src\" != /* ]] && _src=\"$_dir/$_src\"\ndone\nSCRIPT_DIR=\"$(cd \"$(dirname \"$_src\")\" && pwd)\"\nSKILL_DIR=\"$(dirname \"$SCRIPT_DIR\")\"\nSKILLS_ROOT=\"$(dirname \"$SKILL_DIR\")\"\nLEGACY_SKILLS_LIB=\"$SKILL_DIR/references/legacy-skills-lib.sh\"\n\nif [[ -f \"$LEGACY_SKILLS_LIB\" ]]; then\n # shellcheck disable=SC1090\n source \"$LEGACY_SKILLS_LIB\"\nfi\n\nissues=()\n\ncheck_legacy_skill_dirs() {\n local legacy_json=\"$SKILL_DIR/references/legacy-skills.json\"\n [[ -f \"$legacy_json\" ]] || return\n declare -F legacy_skill_entries >/dev/null 2>&1 || return\n\n local found=()\n local kind old_name replacement base_dir skill_dir\n while IFS=

<!-- swain-model-hint: sonnet, effort: low -- Doctor Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check. Running the script The script outputs structured JSON with . Each check has , , , and optional . Parse it and present the summary table to the operator. Then use the remediation sections below only for checks that reported or status — do not re-run detection. To auto-fix flat-file artifacts: If the script is unavailable (e.g., symlinks not yet bootstrapped)…

\\t' read -r kind old_name replacement; do\n [[ -n \"$old_name\" ]] || continue\n for base_dir in \"$REPO_ROOT/.agents/skills\" \"$REPO_ROOT/.claude/skills\"; do\n skill_dir=\"$base_dir/$old_name\"\n [[ -d \"$skill_dir\" ]] || continue\n if legacy_skill_matches_fingerprint \"$skill_dir\" \"$legacy_json\"; then\n found+=(\"${skill_dir#$REPO_ROOT/}\")\n fi\n done\n done \u003c \u003c(legacy_skill_entries \"$legacy_json\")\n\n if [[ ${#found[@]} -gt 0 ]]; then\n issues+=(\"legacy skill directories detected: ${found[*]} (run swain-doctor to remove them)\")\n fi\n}\n\n# 1. Governance files exist\nif [[ ! -f AGENTS.md ]] && [[ ! -f CLAUDE.md ]]; then\n issues+=(\"no governance file (AGENTS.md or CLAUDE.md)\")\nfi\n\n# 2. Governance markers present\nif ! grep -q \"swain governance\" AGENTS.md CLAUDE.md 2>/dev/null; then\n issues+=(\"governance markers missing\")\nfi\n\n# 2b. Governance freshness — compare installed block against canonical\nCANONICAL=\"$SKILL_DIR/references/AGENTS.content.md\"\nif [[ -f \"$CANONICAL\" ]] && grep -q \"swain governance\" AGENTS.md CLAUDE.md 2>/dev/null; then\n GOV_FILE=$(grep -l \"swain governance\" AGENTS.md CLAUDE.md 2>/dev/null | head -1 || true)\n if [[ -n \"$GOV_FILE\" ]]; then\n # Extract content between markers (exclusive) and hash\n extract_gov() { awk '/\u003c!-- swain governance/{f=1;next}/\u003c!-- end swain governance/{f=0}f' \"$1\"; }\n INSTALLED_HASH=$(extract_gov \"$GOV_FILE\" | shasum -a 256 | cut -d' ' -f1)\n CANONICAL_HASH=$(extract_gov \"$CANONICAL\" | shasum -a 256 | cut -d' ' -f1)\n if [[ \"$INSTALLED_HASH\" != \"$CANONICAL_HASH\" ]]; then\n issues+=(\"governance block is stale (differs from canonical AGENTS.content.md)\")\n fi\n fi\nfi\n\n# 3. .agents directory exists (ADR-020: self-heal)\nif [[ ! -d .agents ]]; then\n mkdir -p .agents\n echo \"advisory: created .agents/ directory\"\nfi\n\n# 4. .tickets/ directory is valid (if it exists)\nif [[ -d .tickets ]]; then\n for f in .tickets/*.md; do\n [[ -f \"$f\" ]] || continue\n if ! head -1 \"$f\" | grep -q '^---

<!-- swain-model-hint: sonnet, effort: low -- Doctor Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check. Running the script The script outputs structured JSON with . Each check has , , , and optional . Parse it and present the summary table to the operator. Then use the remediation sections below only for checks that reported or status — do not re-run detection. To auto-fix flat-file artifacts: If the script is unavailable (e.g., symlinks not yet bootstrapped)…

; then\n issues+=(\"invalid ticket frontmatter: $f\")\n break\n fi\n done\nfi\n\n# 5. No stale .beads/ directory (needs auto-migration)\nif [[ -d .beads ]]; then\n issues+=(\"stale .beads/ directory needs migration to .tickets/\")\nfi\n\n# Evidence pool migration check\nif [[ -d \"$REPO_ROOT/docs/evidence-pools\" ]]; then\n echo \"preflight: docs/evidence-pools/ detected — trove migration needed\"\n issues+=(\"docs/evidence-pools/ detected — trove migration needed\")\nfi\n\n# 5b. Worktree context (ADR-034) — location sanity gate\n_git_common=\"$(git rev-parse --git-common-dir 2>/dev/null || true)\"\n_git_dir=\"$(git rev-parse --git-dir 2>/dev/null || true)\"\nif [[ -n \"$_git_common\" ]] && [[ \"$_git_common\" != \"$_git_dir\" ]]; then\n _main_root=\"$(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')\"\n if [[ -n \"$_main_root\" ]] && [[ \"$REPO_ROOT\" != \"$_main_root/.worktrees\"/* ]]; then\n issues+=(\"worktree outside .worktrees/ (ADR-034: swain-doctor can auto-move)\")\n fi\nfi\n\n# Legacy swain skill directories\ncheck_legacy_skill_dirs\n\n# 6. Stale tk lock files (older than 1 hour) (ADR-020: self-heal)\nif [[ -d .tickets/.locks ]]; then\n _stale_lock_count=$(find .tickets/.locks -type d -mmin +60 2>/dev/null | wc -l | tr -d ' ')\n if [[ \"$_stale_lock_count\" -gt 0 ]]; then\n find .tickets/.locks -type d -mmin +60 -exec rm -rf {} + 2>/dev/null\n echo \"advisory: removed $_stale_lock_count stale tk lock(s)\"\n fi\nfi\n\n# 7. Old lifecycle phase directories (ADR-003 migration)\nOLD_PHASES=\"Draft Planned Review Approved Testing Implemented Adopted Deprecated Archived Sunset Validated\"\nfor dir in docs/*/; do\n [[ -d \"$dir\" ]] || continue\n for phase in $OLD_PHASES; do\n phase_dir=\"${dir}${phase}\"\n if [[ -d \"$phase_dir\" ]]; then\n # Only flag non-empty directories (ignore .DS_Store and hidden files)\n if find \"$phase_dir\" -maxdepth 1 -not -name '.*' -not -name \"$phase\" -print -quit 2>/dev/null | grep -q .; then\n issues+=(\"old lifecycle directory: $phase_dir (run migrate-lifecycle-dirs.py)\")\n break 2\n fi\n fi\n done\ndone\n\n# 8. Commit signing configured\nif [[ \"$(git config --local commit.gpgsign 2>/dev/null)\" != \"true\" ]]; then\n issues+=(\"commit signing not configured (run swain-keys --provision)\")\nfi\n\n# 9. Script permissions (spot check) (ADR-020: self-heal)\n_bad_perms=$(find \"$SKILLS_ROOT\" -type f \\( -path '*/scripts/*.sh' -o -path '*/scripts/*.py' \\) ! -perm -u+x 2>/dev/null || true)\nif [[ -n \"$_bad_perms\" ]]; then\n _fix_count=$(echo \"$_bad_perms\" | wc -l | tr -d ' ')\n echo \"$_bad_perms\" | xargs chmod +x\n echo \"advisory: fixed executable permissions on $_fix_count script(s)\"\nfi\n\n# 9b. SSH alias readiness for repos using swain-keys host aliases\nSSH_HELPER=\"$SCRIPT_DIR/ssh-readiness.sh\"\nif [[ -x \"$SSH_HELPER\" ]]; then\n ssh_output=\"$(bash \"$SSH_HELPER\" --check 2>/dev/null || true)\"\n if [[ -n \"$ssh_output\" ]]; then\n while IFS= read -r line; do\n [[ -z \"$line\" ]] && continue\n issues+=(\"${line#ISSUE: }\")\n done \u003c\u003c\u003c \"$ssh_output\"\n fi\nfi\n\n# 9c. Skill folder gitignore hygiene (advisory — non-blocking)\n# Only check vendored swain skill directories (swain/ and swain-*/), not all skills.\n# Skip if this is the swain source repo (skill folders are tracked there).\n_origin_url=\"$(git remote get-url origin 2>/dev/null || true)\"\nif [[ \"$_origin_url\" != *\"cristoslc/swain\"* ]]; then\n for _base in .claude/skills .agents/skills; do\n [ -d \"$_base\" ] || continue\n for _skill_path in \"$_base\"/swain \"$_base\"/swain-*/; do\n if [[ -d \"$_skill_path\" ]]; then\n git check-ignore -q \"$_skill_path\" 2>/dev/null\n _ignore_rc=$?\n # 0=ignored (ok), 1=not ignored (warn), 128=beyond symlink (skip)\n if [[ $_ignore_rc -eq 1 ]]; then\n echo \"swain-preflight: $REPO_ROOT/$_skill_path not gitignored (advisory)\"\n fi\n fi\n done\n done\nfi\n\n# 10. Superpowers detection (advisory — warn but don't fail)\nSUPERPOWERS_SKILLS=\"brainstorming writing-plans test-driven-development verification-before-completion subagent-driven-development executing-plans\"\nsp_missing=0\nfor skill in $SUPERPOWERS_SKILLS; do\n if ! ls .agents/skills/$skill/SKILL.md .claude/skills/$skill/SKILL.md 2>/dev/null | head -1 | grep -q .; then\n sp_missing=$((sp_missing + 1))\n fi\ndone\nif [[ $sp_missing -gt 0 ]]; then\n echo \"swain-preflight: superpowers: $sp_missing/6 skills missing (advisory)\"\nfi\n\n# 11. Security scanner availability (INFO — advisory, non-blocking) (SPEC-059)\nSCANNER_SCRIPT=\"$SKILLS_ROOT/swain-security-check/scripts/scanner_availability.py\"\nif [[ -x \"$SCANNER_SCRIPT\" ]]; then\n scanner_output=$(python3 \"$SCANNER_SCRIPT\" 2>/dev/null || true)\n # Extract the summary line (first line: \"Scanner availability: N/4 scanners found\")\n scanner_summary=$(echo \"$scanner_output\" | head -1)\n if [[ -n \"$scanner_summary\" ]] && ! echo \"$scanner_summary\" | grep -q \"4/4\"; then\n echo \"swain-preflight: $scanner_summary (advisory)\"\n # Print missing scanner details\n echo \"$scanner_output\" | grep '^\\s*\\[--\\]' | while read -r line; do\n echo \" $line\"\n done\n fi\nfi\n\n# Check mmdc availability (SPEC-110)\nif ! command -v mmdc >/dev/null 2>&1; then\n echo \"swain-preflight: mmdc (mermaid-cli) not found — quadrant chart will use inline Mermaid instead of PNG\"\nfi\n\n# 12. Lightweight security diagnostic (advisory, non-blocking) (SPEC-061)\nDOCTOR_SECURITY_SCRIPT=\"$SKILLS_ROOT/swain-security-check/scripts/doctor_security_check.py\"\nif [[ -x \"$DOCTOR_SECURITY_SCRIPT\" ]]; then\n security_output=$(python3 \"$DOCTOR_SECURITY_SCRIPT\" 2>/dev/null || true)\n if [[ -n \"$security_output\" ]]; then\n echo \"$security_output\"\n fi\nfi\n\n# 13. Skill change discipline (SPEC-148) — advisory, triggers doctor\nSKILL_CHECK_SCRIPT=\"$SCRIPT_DIR/check-skill-changes.sh\"\nif [[ -x \"$SKILL_CHECK_SCRIPT\" ]]; then\n skill_output=$(bash \"$SKILL_CHECK_SCRIPT\" 2>/dev/null || true)\n skill_status=$?\n if [[ $skill_status -ne 0 && -n \"$skill_output\" ]]; then\n echo \"$skill_output\"\n issues+=(\"non-trivial skill changes detected on trunk (use worktree branches)\")\n fi\nfi\n\n# Auto-repair .agents/bin/ symlinks (ADR-019, SPEC-186)\n# Agent-facing scripts live in the installed skill tree and are symlinked to .agents/bin/\nAGENTS_BIN=\"$REPO_ROOT/.agents/bin\"\nOPERATOR_SCRIPTS=\"swain swain-box\" # operator-facing — skip for .agents/bin/\n_agents_bin_repaired=0\nfor skill_scripts_dir in \"$SKILLS_ROOT\"/*/scripts; do\n [[ -d \"$skill_scripts_dir\" ]] || continue\n for script in \"$skill_scripts_dir\"/*; do\n [[ -f \"$script\" && -x \"$script\" ]] || continue\n script_name=\"$(basename \"$script\")\"\n # Skip test scripts and operator-facing scripts\n [[ \"$script_name\" == test-* ]] && continue\n echo \" $OPERATOR_SCRIPTS \" | grep -q \" $script_name \" && continue\n # Check .agents/bin/ symlink\n target=\"$AGENTS_BIN/$script_name\"\n rel_path=\"$(python3 -c \"import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))\" \"$script\" \"$AGENTS_BIN\" 2>/dev/null || echo \"\")\"\n if [[ -L \"$target\" ]] && [[ \"$(readlink \"$target\")\" == \"$rel_path\" ]]; then\n continue # ok\n elif [[ -e \"$target\" ]] && [[ ! -L \"$target\" ]]; then\n issues+=(\".agents/bin/$script_name is a real file, not a symlink — manual fix needed\")\n else\n # missing or stale — auto-repair\n mkdir -p \"$AGENTS_BIN\"\n ln -sf \"$rel_path\" \"$target\"\n _agents_bin_repaired=$((_agents_bin_repaired + 1))\n fi\n done\ndone\nif [[ $_agents_bin_repaired -gt 0 ]]; then\n echo \"advisory: repaired $_agents_bin_repaired .agents/bin/ symlink(s) (ADR-019)\"\nfi\n\n# Auto-repair bin/ symlinks for operator-facing scripts (ADR-019, SPEC-188)\nBIN_DIR=\"$REPO_ROOT/bin\"\n_bin_repaired=0\nfor op_script in $OPERATOR_SCRIPTS; do\n # Find canonical location from installed usr/bin manifests\n canonical=\"\"\n for manifest_dir in \"$SKILLS_ROOT\"/*/usr/bin; do\n [[ -d \"$manifest_dir\" ]] || continue\n if [[ -L \"$manifest_dir/$op_script\" || -e \"$manifest_dir/$op_script\" ]]; then\n canonical=\"$(cd \"$manifest_dir\" && readlink -f \"$op_script\" 2>/dev/null || true)\"\n break\n fi\n done\n [[ -n \"$canonical\" && -x \"$canonical\" ]] || continue\n target=\"$BIN_DIR/$op_script\"\n rel_path=\"$(python3 -c \"import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))\" \"$canonical\" \"$BIN_DIR\" 2>/dev/null || echo \"\")\"\n if [[ -L \"$target\" ]] && [[ \"$(readlink \"$target\")\" == \"$rel_path\" ]]; then\n continue # ok\n elif [[ -e \"$target\" ]] && [[ ! -L \"$target\" ]]; then\n issues+=(\"bin/$op_script is a real file, not a symlink — manual fix needed\")\n else\n # missing or stale — auto-repair\n mkdir -p \"$BIN_DIR\"\n ln -sf \"$rel_path\" \"$target\"\n _bin_repaired=$((_bin_repaired + 1))\n fi\n # Migrate old root symlink if present\n if [[ -L \"$REPO_ROOT/$op_script\" ]]; then\n rm -f \"$REPO_ROOT/$op_script\"\n echo \"advisory: migrated ./$op_script to bin/$op_script (ADR-019)\"\n fi\ndone\nif [[ $_bin_repaired -gt 0 ]]; then\n echo \"advisory: repaired $_bin_repaired bin/ symlink(s) (ADR-019)\"\nfi\n\n# Trunk/release branch model detection (EPIC-029, ADR-013, ADR-019)\n# Check that .agents/bin/swain-trunk.sh exists and the detected trunk branch has a remote\nTRUNK_SCRIPT=\"$REPO_ROOT/.agents/bin/swain-trunk.sh\"\nif [[ ! -x \"$TRUNK_SCRIPT\" ]]; then\n issues+=(\".agents/bin/swain-trunk.sh missing or not executable — no agent-facing scripts found in skill tree\")\nelse\n DETECTED_TRUNK=$(bash \"$TRUNK_SCRIPT\" 2>/dev/null || echo \"\")\n if [[ -z \"$DETECTED_TRUNK\" ]]; then\n issues+=(\"swain-trunk.sh returned empty — cannot detect trunk branch\")\n elif ! git ls-remote --heads origin \"$DETECTED_TRUNK\" 2>/dev/null | grep -q \"$DETECTED_TRUNK\"; then\n echo \"advisory: trunk branch '$DETECTED_TRUNK' has no remote counterpart on origin\"\n fi\n # Check if release branch exists (ADR-013 trunk+release model)\n if ! git rev-parse --verify release 2>/dev/null >/dev/null; then\n echo \"advisory: no 'release' branch found — trunk+release model (ADR-013) not configured\"\n echo \" run: bash .agents/bin/migrate-to-trunk-release.sh --dry-run\"\n fi\nfi\n\n# Check for epics without parent-initiative (initiative migration advisory)\nEPICS_WITHOUT_INITIATIVE=0\nwhile IFS= read -r -d '' f; do\n if grep -q '^parent-vision:' \"$f\" 2>/dev/null && ! grep -q '^parent-initiative:' \"$f\" 2>/dev/null; then\n EPICS_WITHOUT_INITIATIVE=$((EPICS_WITHOUT_INITIATIVE + 1))\n fi\ndone \u003c \u003c(find docs/epic -name '*.md' -not -name 'README.md' -not -name 'list-*.md' -print0 2>/dev/null)\nif [[ \"$EPICS_WITHOUT_INITIATIVE\" -gt 0 ]]; then\n echo \"advisory: $EPICS_WITHOUT_INITIATIVE epic(s) without parent-initiative — run initiative migration\"\nfi\n\n# Report\nif [[ ${#issues[@]} -eq 0 ]]; then\n exit 0\nelse\n echo \"swain-preflight: ${#issues[@]} issue(s) found:\"\n for issue in \"${issues[@]}\"; do\n echo \" - $issue\"\n done\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":14056,"content_sha256":"e3fd7e5505252e44970ae8d7b2ff7d5e5f895b5a2470344a884294535163c6a5"},{"filename":"tests/test-check-skill-changes.sh","content":"#!/usr/bin/env bash\n# test-check-skill-changes.sh — Tests for swain-doctor skill-change detection\n#\n# Usage: bash skills/swain-doctor/tests/test-check-skill-changes.sh\n\nset +e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nCHECK_SCRIPT=\"$(cd \"$SCRIPT_DIR/..\" && pwd)/scripts/check-skill-changes.sh\"\n\nPASS=0\nFAIL=0\n\npass() { echo \" PASS: $1\"; ((PASS++)); }\nfail() { echo \" FAIL: $1 — $2\"; ((FAIL++)); }\n\n# Create a temporary git repo with skill files for testing\nmake_test_repo() {\n local repo_dir=\"$1\"\n mkdir -p \"$repo_dir/skills/test-skill\"\n cd \"$repo_dir\"\n git init -q\n git config user.email \"[email protected]\"\n git config user.name \"Test\"\n\n # Initial commit with a skill file\n cat > skills/test-skill/SKILL.md \u003c\u003c'SKILL'\n---\nname: test-skill\ndescription: A test skill\n---\n\n# Test Skill\n\n## Overview\n\nThis is a test skill for unit testing.\n\n## When to Use\n\nUse when testing.\nSKILL\n git add -A\n git commit -q -m \"initial: add test skill\"\n}\n\necho \"=== swain-doctor Skill Change Detection Tests ===\"\necho \"Script: $CHECK_SCRIPT\"\necho \"\"\n\n# --- AC2: Non-trivial skill change emits warning ---\necho \"--- AC2: Non-trivial skill change on trunk emits warning ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/repo\"\nmake_test_repo \"$REPO_DIR\"\n\n# Make a non-trivial change (20+ lines)\ncat >> skills/test-skill/SKILL.md \u003c\u003c'ADDITION'\n\n## New Section\n\nThis is a brand new section with lots of content.\nLine 1 of new content.\nLine 2 of new content.\nLine 3 of new content.\nLine 4 of new content.\nLine 5 of new content.\nLine 6 of new content.\nLine 7 of new content.\nLine 8 of new content.\nLine 9 of new content.\nLine 10 of new content.\nLine 11 of new content.\nLine 12 of new content.\nLine 13 of new content.\nLine 14 of new content.\nLine 15 of new content.\nADDITION\ngit add -A\ngit commit -q -m \"refactor(test-skill): add new section with content\"\n\noutput=\"$(bash \"$CHECK_SCRIPT\" 2>&1)\"\nstatus=$?\nif [[ $status -ne 0 && \"$output\" == *\"skill files with non-trivial changes\"* ]]; then\n pass \"AC2: non-trivial skill change detected\"\nelse\n fail \"AC2\" \"status=$status output=$output\"\nfi\nrm -rf \"$TMPDIR\"\n\n# --- AC3: Trivial skill change passes clean ---\necho \"--- AC3: Trivial skill change passes clean ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/repo\"\nmake_test_repo \"$REPO_DIR\"\n\n# Make a trivial change (2-line typo fix, single file)\nsed -i.bak 's/This is a test skill for unit testing./This is a test skill for unit testing purposes./' skills/test-skill/SKILL.md\nrm -f skills/test-skill/SKILL.md.bak\ngit add -A\ngit commit -q -m \"fix(test-skill): fix typo\"\n\noutput=\"$(bash \"$CHECK_SCRIPT\" 2>&1)\"\nstatus=$?\nif [[ $status -eq 0 ]]; then\n pass \"AC3: trivial skill change passes clean\"\nelse\n fail \"AC3\" \"status=$status output=$output\"\nfi\nrm -rf \"$TMPDIR\"\n\n# --- Multi-file skill change is non-trivial ---\necho \"--- Multi-file skill change is non-trivial ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/repo\"\nmake_test_repo \"$REPO_DIR\"\n\n# Create a second skill file and change both (multi-file = non-trivial regardless of diff size)\nmkdir -p skills/other-skill\ncat > skills/other-skill/SKILL.md \u003c\u003c'SKILL2'\n---\nname: other-skill\ndescription: Another skill\n---\n# Other Skill\nSKILL2\n# Also touch the first skill with a small change\nsed -i.bak 's/A test skill/A modified test skill/' skills/test-skill/SKILL.md\nrm -f skills/test-skill/SKILL.md.bak\ngit add -A\ngit commit -q -m \"feat: add other skill and update test skill\"\n\noutput=\"$(bash \"$CHECK_SCRIPT\" 2>&1)\"\nstatus=$?\nif [[ $status -ne 0 && \"$output\" == *\"skill files with non-trivial changes\"* ]]; then\n pass \"Multi-file skill change detected as non-trivial\"\nelse\n fail \"Multi-file\" \"status=$status output=$output\"\nfi\nrm -rf \"$TMPDIR\"\n\n# --- Non-skill commits pass clean ---\necho \"--- Non-skill file changes pass clean ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/repo\"\nmake_test_repo \"$REPO_DIR\"\n\n# Make a big change to a non-skill file\ncat > README.md \u003c\u003c'README'\n# Test Project\nThis is a big change to a non-skill file.\nLine 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8\nLine 9\nLine 10\nREADME\ngit add -A\ngit commit -q -m \"docs: update README\"\n\noutput=\"$(bash \"$CHECK_SCRIPT\" 2>&1)\"\nstatus=$?\nif [[ $status -eq 0 ]]; then\n pass \"Non-skill file changes pass clean\"\nelse\n fail \"Non-skill\" \"status=$status output=$output\"\nfi\nrm -rf \"$TMPDIR\"\n\n# --- Version bump in frontmatter is non-trivial even if small diff ---\necho \"--- Version bump in frontmatter is non-trivial ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/repo\"\nmake_test_repo \"$REPO_DIR\"\n\n# Add a version field then bump it (small diff but structural)\nsed -i.bak 's/description: A test skill/description: A test skill\\nversion: 1.0.0/' skills/test-skill/SKILL.md\nrm -f skills/test-skill/SKILL.md.bak\ngit add -A\ngit commit -q -m \"chore: add version field\"\n\n# Now bump the version\nsed -i.bak 's/version: 1.0.0/version: 2.0.0/' skills/test-skill/SKILL.md\nrm -f skills/test-skill/SKILL.md.bak\ngit add -A\ngit commit -q -m \"chore(test-skill): bump version to 2.0.0\"\n\noutput=\"$(bash \"$CHECK_SCRIPT\" 2>&1)\"\nstatus=$?\nif [[ $status -ne 0 && \"$output\" == *\"skill files with non-trivial changes\"* ]]; then\n pass \"Version bump detected as non-trivial\"\nelse\n fail \"Version bump\" \"status=$status output=$output\"\nfi\nrm -rf \"$TMPDIR\"\n\necho \"\"\necho \"=== Summary ===\"\necho \"PASS: $PASS\"\necho \"FAIL: $FAIL\"\n\nif [[ $FAIL -gt 0 ]]; then\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5344,"content_sha256":"2c170690b6857a267585e9e3578fe198f952800908a0c2a387b72b1c85e6833d"},{"filename":"tests/test-gitignore-skill-folders.sh","content":"#!/usr/bin/env bash\n# test-gitignore-skill-folders.sh — Acceptance tests for gitignore skill-folder check\n#\n# Tests the detection logic used by swain-doctor and swain-preflight to verify\n# that .claude/skills/ and .agents/skills/ are gitignored in consumer projects.\n#\n# Usage: bash skills/swain-doctor/tests/test-gitignore-skill-folders.sh\n\nset +e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nPREFLIGHT=\"$(cd \"$SCRIPT_DIR/..\" && pwd)/scripts/swain-preflight.sh\"\nREPO_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"\n\nPASS=0\nFAIL=0\n\npass() { echo \" PASS: $1\"; ((PASS++)); }\nfail() { echo \" FAIL: $1 — $2\"; ((FAIL++)); }\n\n# Helper: create a minimal git repo with optional remote\nmake_consumer_repo() {\n local repo_dir=\"$1\"\n mkdir -p \"$repo_dir\"\n git -C \"$repo_dir\" init -q\n git -C \"$repo_dir\" remote add origin \"[email protected]:someuser/someproject.git\"\n # Create skill folders to simulate installed skills\n mkdir -p \"$repo_dir/.claude/skills/swain-doctor\"\n mkdir -p \"$repo_dir/.agents/skills/brainstorming\"\n}\n\nmake_swain_repo() {\n local repo_dir=\"$1\"\n mkdir -p \"$repo_dir\"\n git -C \"$repo_dir\" init -q\n git -C \"$repo_dir\" remote add origin \"[email protected]:cristoslc/swain.git\"\n mkdir -p \"$repo_dir/.claude/skills/swain-doctor\"\n mkdir -p \"$repo_dir/.agents/skills/brainstorming\"\n}\n\necho \"=== Gitignore Skill Folders Check Tests ===\"\necho \"\"\n\n# --- AC1: Warning on missing gitignore ---\necho \"--- AC1: consumer project without gitignore warns about skill folders ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/consumer1\"\nmake_consumer_repo \"$REPO_DIR\"\n\n# Run git check-ignore against the skill folders — should NOT be ignored\nclaude_ignored=$(cd \"$REPO_DIR\" && git check-ignore -q .claude/skills/ 2>/dev/null; echo $?)\nagents_ignored=$(cd \"$REPO_DIR\" && git check-ignore -q .agents/skills/ 2>/dev/null; echo $?)\n\nif [[ \"$claude_ignored\" -ne 0 && \"$agents_ignored\" -ne 0 ]]; then\n pass \"AC1: skill folders are not gitignored in bare consumer repo\"\nelse\n fail \"AC1\" \"expected both folders NOT ignored, got claude=$claude_ignored agents=$agents_ignored\"\nfi\nrm -rf \"$TMPDIR\"\n\n# --- AC2: OK when already gitignored ---\necho \"--- AC2: consumer project with gitignore reports OK ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/consumer2\"\nmake_consumer_repo \"$REPO_DIR\"\n\ncat > \"$REPO_DIR/.gitignore\" \u003c\u003c'EOF'\n# Vendored swain skills (managed by swain-update)\n.claude/skills/\n.agents/skills/\nEOF\n\nclaude_ignored=$(cd \"$REPO_DIR\" && git check-ignore -q .claude/skills/ 2>/dev/null; echo $?)\nagents_ignored=$(cd \"$REPO_DIR\" && git check-ignore -q .agents/skills/ 2>/dev/null; echo $?)\n\nif [[ \"$claude_ignored\" -eq 0 && \"$agents_ignored\" -eq 0 ]]; then\n pass \"AC2: skill folders are gitignored when .gitignore has entries\"\nelse\n fail \"AC2\" \"expected both folders ignored, got claude=$claude_ignored agents=$agents_ignored\"\nfi\nrm -rf \"$TMPDIR\"\n\n# --- AC3: Swain repo self-detection skips check ---\necho \"--- AC3: swain source repo is detected and check is skipped ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/swain\"\nmake_swain_repo \"$REPO_DIR\"\n\nremote_url=$(cd \"$REPO_DIR\" && git remote get-url origin 2>/dev/null)\nis_swain=no\nif [[ \"$remote_url\" == *\"cristoslc/swain\"* ]]; then\n is_swain=yes\nfi\n\nif [[ \"$is_swain\" == \"yes\" ]]; then\n pass \"AC3: swain repo detected via remote URL\"\nelse\n fail \"AC3\" \"expected swain detection, got remote=$remote_url\"\nfi\nrm -rf \"$TMPDIR\"\n\n# --- AC4: Creates .gitignore if absent ---\necho \"--- AC4: remediation creates .gitignore when file is absent ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/consumer3\"\nmake_consumer_repo \"$REPO_DIR\"\n\n# Simulate remediation: append entries to .gitignore\ngitignore_path=\"$REPO_DIR/.gitignore\"\nif [[ ! -f \"$gitignore_path\" ]]; then\n cat > \"$gitignore_path\" \u003c\u003c'GITIGNORE'\n# Vendored swain skills (managed by swain-update)\n.claude/skills/\n.agents/skills/\nGITIGNORE\nfi\n\nif [[ -f \"$gitignore_path\" ]]; then\n claude_ignored=$(cd \"$REPO_DIR\" && git check-ignore -q .claude/skills/ 2>/dev/null; echo $?)\n agents_ignored=$(cd \"$REPO_DIR\" && git check-ignore -q .agents/skills/ 2>/dev/null; echo $?)\n if [[ \"$claude_ignored\" -eq 0 && \"$agents_ignored\" -eq 0 ]]; then\n pass \"AC4: created .gitignore covers both skill folders\"\n else\n fail \"AC4\" \"gitignore created but folders not ignored: claude=$claude_ignored agents=$agents_ignored\"\n fi\nelse\n fail \"AC4\" \".gitignore was not created\"\nfi\nrm -rf \"$TMPDIR\"\n\n# --- AC5: Partial coverage — only one path gitignored ---\necho \"--- AC5: partial coverage detects the missing entry ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/consumer4\"\nmake_consumer_repo \"$REPO_DIR\"\n\n# Only gitignore .claude/skills/, not .agents/skills/\ncat > \"$REPO_DIR/.gitignore\" \u003c\u003c'EOF'\n.claude/skills/\nEOF\n\nclaude_ignored=$(cd \"$REPO_DIR\" && git check-ignore -q .claude/skills/ 2>/dev/null; echo $?)\nagents_ignored=$(cd \"$REPO_DIR\" && git check-ignore -q .agents/skills/ 2>/dev/null; echo $?)\n\nif [[ \"$claude_ignored\" -eq 0 && \"$agents_ignored\" -ne 0 ]]; then\n pass \"AC5: partial coverage detected — .claude/skills/ ignored, .agents/skills/ not\"\nelse\n fail \"AC5\" \"expected partial coverage, got claude=$claude_ignored agents=$agents_ignored\"\nfi\nrm -rf \"$TMPDIR\"\n\necho \"\"\necho \"=== Summary ===\"\necho \"PASS: $PASS\"\necho \"FAIL: $FAIL\"\n\nif [[ $FAIL -gt 0 ]]; then\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5298,"content_sha256":"0ae14eafc41a6a546e411a5cc6dd663231b44404c99d341d1d4b1abe686f2f59"},{"filename":"tests/test-legacy-skill-cleanup.sh","content":"#!/usr/bin/env bash\n# test-legacy-skill-cleanup.sh — verifies stale swain skill cleanup\n\nset +e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR/../../..\" && pwd)\"\nDOCTOR_SCRIPT=\"$REPO_ROOT/.agents/bin/swain-doctor.sh\"\n\nPASS=0\nFAIL=0\n\npass() { echo \" PASS: $1\"; ((PASS++)); }\nfail() { echo \" FAIL: $1 — $2\"; ((FAIL++)); }\n\nmake_test_repo() {\n local repo_dir=\"$1\"\n mkdir -p \"$repo_dir/.agents/bin\" \"$repo_dir/.agents/skills\" \"$repo_dir/.claude/skills\" \"$repo_dir/skills/swain/scripts\"\n cd \"$repo_dir\" || exit 1\n git init -q\n git checkout -q -b trunk\n git config user.email \"[email protected]\"\n git config user.name \"Test\"\n git config commit.gpgsign true\n\n cat > AGENTS.md \u003c\u003c'EOF_AGENTS'\n\u003c!-- swain governance -->\nSwain governance\n\u003c!-- end swain governance -->\nEOF_AGENTS\n\n cat > .agents/bin/swain-trunk.sh \u003c\u003c'EOF_TRUNK'\n#!/usr/bin/env bash\necho trunk\nEOF_TRUNK\n chmod +x .agents/bin/swain-trunk.sh\n\n cat > skills/swain/scripts/swain \u003c\u003c'EOF_SWAIN'\n#!/usr/bin/env bash\nexit 0\nEOF_SWAIN\n chmod +x skills/swain/scripts/swain\n\n git add -A\n git commit -q -m \"initial: create test repo\"\n}\n\necho \"=== Legacy Skill Cleanup Tests ===\"\necho \"\"\n\necho \"--- Removes fingerprinted swain-status from .agents/skills ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/repo\"\nmake_test_repo \"$REPO_DIR\"\nmkdir -p \"$REPO_DIR/.agents/skills/swain-status\"\ncat > \"$REPO_DIR/.agents/skills/swain-status/SKILL.md\" \u003c\u003c'EOF_SKILL'\n---\nname: swain-status\nauthor: cristos\nsource: swain\n---\n\n# swain-status\nEOF_SKILL\n\noutput=\"$(cd \"$REPO_DIR\" && bash \"$DOCTOR_SCRIPT\" 2>/dev/null)\"\nstatus=$?\nlegacy_status=\"$(echo \"$output\" | jq -r '.checks[] | select(.name == \"legacy_skills\") | .status' 2>/dev/null)\"\n\nif [[ $status -eq 0 && \"$legacy_status\" == \"advisory\" && ! -d \"$REPO_DIR/.agents/skills/swain-status\" ]]; then\n pass \"doctor removes fingerprinted stale skill\"\nelse\n fail \"doctor removes fingerprinted stale skill\" \"status=$status legacy_status=$legacy_status\"\nfi\nrm -rf \"$TMPDIR\"\n\necho \"--- Leaves non-swain skill with same directory name in place ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/repo\"\nmake_test_repo \"$REPO_DIR\"\nmkdir -p \"$REPO_DIR/.claude/skills/swain-status\"\ncat > \"$REPO_DIR/.claude/skills/swain-status/SKILL.md\" \u003c\u003c'EOF_SKILL'\n---\nname: swain-status\ndescription: third-party status helper\n---\n\n# Custom status helper\nEOF_SKILL\n\noutput=\"$(cd \"$REPO_DIR\" && bash \"$DOCTOR_SCRIPT\" 2>/dev/null)\"\nstatus=$?\nlegacy_status=\"$(echo \"$output\" | jq -r '.checks[] | select(.name == \"legacy_skills\") | .status' 2>/dev/null)\"\n\nif [[ $status -eq 0 && \"$legacy_status\" == \"warning\" && -d \"$REPO_DIR/.claude/skills/swain-status\" ]]; then\n pass \"doctor preserves non-swain directory name collision\"\nelse\n fail \"doctor preserves non-swain directory name collision\" \"status=$status legacy_status=$legacy_status\"\nfi\nrm -rf \"$TMPDIR\"\n\necho \"\"\necho \"=== Summary ===\"\necho \"PASS: $PASS\"\necho \"FAIL: $FAIL\"\n\nif [[ $FAIL -gt 0 ]]; then\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2965,"content_sha256":"850bda4bd377cc410a3f53f2b502f47766349565cbd681f40372b801473bfe9a"},{"filename":"tests/test-operator-bin-symlinks.sh","content":"#!/usr/bin/env bash\n# test-operator-bin-symlinks.sh — tests for SPEC-214 operator bin/ symlink auto-repair\n# Validates: usr/bin/ manifest scanning, auto-repair, conflict detection, dynamic exclusion\n\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR/../../..\" && pwd)\"\nDOCTOR_SCRIPT=\"$REPO_ROOT/.agents/bin/swain-doctor.sh\"\n\nPASS=0\nFAIL=0\nTOTAL=0\n\nassert() {\n local desc=\"$1\"\n local result=\"$2\"\n TOTAL=$((TOTAL + 1))\n if [[ \"$result\" == \"0\" ]]; then\n PASS=$((PASS + 1))\n echo \" PASS: $desc\"\n else\n FAIL=$((FAIL + 1))\n echo \" FAIL: $desc\"\n fi\n}\n\nget_check() {\n local name=\"$1\"\n local output=\"$2\"\n echo \"$output\" | jq -r \".checks[] | select(.name == \\\"$name\\\")\"\n}\n\n# --- Test 1: usr/bin/ manifest exists with expected entries ---\necho \"Test 1: usr/bin/ manifest directory\"\nassert \"skills/swain/usr/bin/ exists\" \"$([ -d \"$REPO_ROOT/skills/swain/usr/bin\" ] && echo 0 || echo 1)\"\nassert \"swain manifest entry exists\" \"$([ -L \"$REPO_ROOT/skills/swain/usr/bin/swain\" ] && echo 0 || echo 1)\"\nassert \"swain-box manifest entry exists\" \"$([ -L \"$REPO_ROOT/skills/swain/usr/bin/swain-box\" ] && echo 0 || echo 1)\"\nassert \"swain manifest resolves\" \"$([ -e \"$REPO_ROOT/skills/swain/usr/bin/swain\" ] && echo 0 || echo 1)\"\nassert \"swain-box manifest resolves\" \"$([ -e \"$REPO_ROOT/skills/swain/usr/bin/swain-box\" ] && echo 0 || echo 1)\"\n\n# --- Test 2: AC1 — missing bin/swain auto-repaired ---\necho \"Test 2: AC1 — missing bin/swain auto-repaired\"\nrm -f \"$REPO_ROOT/bin/swain\"\noutput=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null)\ncheck=$(get_check \"operator_bin_symlinks\" \"$output\")\nassert \"bin/swain recreated\" \"$([ -L \"$REPO_ROOT/bin/swain\" ] && echo 0 || echo 1)\"\nassert \"bin/swain resolves to script\" \"$([ -e \"$REPO_ROOT/bin/swain\" ] && echo 0 || echo 1)\"\n\n# --- Test 3: AC2 — missing bin/swain-box auto-repaired ---\necho \"Test 3: AC2 — missing bin/swain-box auto-repaired\"\nrm -f \"$REPO_ROOT/bin/swain-box\"\noutput=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null)\nassert \"bin/swain-box recreated\" \"$([ -L \"$REPO_ROOT/bin/swain-box\" ] && echo 0 || echo 1)\"\nassert \"bin/swain-box resolves to script\" \"$([ -e \"$REPO_ROOT/bin/swain-box\" ] && echo 0 || echo 1)\"\n\n# --- Test 4: AC3 — stale symlink repaired ---\necho \"Test 4: AC3 — stale bin/swain symlink repaired\"\nln -sf \"/nonexistent/old/path/swain\" \"$REPO_ROOT/bin/swain\"\noutput=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null)\ncheck=$(get_check \"operator_bin_symlinks\" \"$output\")\nassert \"stale symlink replaced\" \"$([ -e \"$REPO_ROOT/bin/swain\" ] && echo 0 || echo 1)\"\nactual_target=$(readlink -f \"$REPO_ROOT/bin/swain\" 2>/dev/null)\nexpected_target=$(readlink -f \"$REPO_ROOT/skills/swain/scripts/swain\" 2>/dev/null)\nassert \"points to correct script\" \"$([ \"$actual_target\" = \"$expected_target\" ] && echo 0 || echo 1)\"\n\n# --- Test 5: AC4 — real file conflict not overwritten ---\necho \"Test 5: AC4 — real file conflict not overwritten\"\nrm -f \"$REPO_ROOT/bin/swain\"\necho \"real file\" > \"$REPO_ROOT/bin/swain\"\noutput=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null)\ncheck=$(get_check \"operator_bin_symlinks\" \"$output\")\nstatus=$(echo \"$check\" | jq -r '.status')\nmessage=$(echo \"$check\" | jq -r '.detail // .message')\nassert \"check status is warning\" \"$([ \"$status\" = \"warning\" ] && echo 0 || echo 1)\"\nassert \"conflict mentioned\" \"$(echo \"$message\" | grep -q \"conflict\" && echo 0 || echo 1)\"\nassert \"real file preserved\" \"$([ ! -L \"$REPO_ROOT/bin/swain\" ] && echo 0 || echo 1)\"\n# Restore\nrm -f \"$REPO_ROOT/bin/swain\"\nbash \"$DOCTOR_SCRIPT\" >/dev/null 2>&1 # let doctor recreate\n\n# --- Test 6: AC5 — new script in usr/bin/ gets auto-linked ---\necho \"Test 6: AC5 — new operator script added to manifest\"\n# Create a test script\nmkdir -p \"$REPO_ROOT/skills/swain/scripts\"\necho '#!/bin/bash' > \"$REPO_ROOT/skills/swain/scripts/test-operator-dummy\"\nchmod +x \"$REPO_ROOT/skills/swain/scripts/test-operator-dummy\"\nln -sf ../../scripts/test-operator-dummy \"$REPO_ROOT/skills/swain/usr/bin/test-operator-dummy\"\nrm -f \"$REPO_ROOT/bin/test-operator-dummy\"\noutput=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null)\nassert \"new script gets bin/ symlink\" \"$([ -L \"$REPO_ROOT/bin/test-operator-dummy\" ] && echo 0 || echo 1)\"\n# Cleanup\nrm -f \"$REPO_ROOT/bin/test-operator-dummy\" \"$REPO_ROOT/skills/swain/usr/bin/test-operator-dummy\" \"$REPO_ROOT/skills/swain/scripts/test-operator-dummy\"\n\n# --- Test 7: AC6 — Check 20 excludes operator scripts dynamically ---\necho \"Test 7: AC6 — .agents/bin/ check excludes operator scripts\"\noutput=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null)\nagents_check=$(get_check \"agents_bin_symlinks\" \"$output\")\nagents_detail=$(echo \"$agents_check\" | jq -r '.detail // .message // \"\"')\n# Operator scripts (swain, swain-box) should NOT appear as missing in .agents/bin/\nassert \"swain not flagged in .agents/bin/\" \"$(echo \"$agents_detail\" | grep -qv \"missing.*swain[^-]\" && echo 0 || echo 1)\"\nassert \"swain-box not flagged in .agents/bin/\" \"$(echo \"$agents_detail\" | grep -qv \"missing.*swain-box\" && echo 0 || echo 1)\"\n\n# --- Test 8: old check names removed ---\necho \"Test 8: old check names removed\"\noutput=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null)\ncheck_names=$(echo \"$output\" | jq -r '.checks[].name')\nassert \"no swain_box check\" \"$(echo \"$check_names\" | grep -q \"^swain_box$\" && echo 1 || echo 0)\"\nassert \"no swain_symlink check\" \"$(echo \"$check_names\" | grep -q \"^swain_symlink$\" && echo 1 || echo 0)\"\nassert \"operator_bin_symlinks present\" \"$(echo \"$check_names\" | grep -q \"operator_bin_symlinks\" && echo 0 || echo 1)\"\n\n# --- Summary ---\necho \"\"\necho \"Results: $PASS/$TOTAL passed, $FAIL failed\"\nif [[ $FAIL -gt 0 ]]; then\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5586,"content_sha256":"e9a84c81a4acbd606c5ed5c6137537ae6724ad6a0ba5372e26df316a6d0f5444"},{"filename":"tests/test-preflight-legacy-skills.sh","content":"#!/usr/bin/env bash\n# test-preflight-legacy-skills.sh — verifies preflight surfaces stale skills\n\nset +e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR/../../..\" && pwd)\"\nPREFLIGHT=\"$REPO_ROOT/.agents/bin/swain-preflight.sh\"\nCANONICAL_AGENTS=\"$REPO_ROOT/skills/swain-doctor/references/AGENTS.content.md\"\n\nPASS=0\nFAIL=0\n\npass() { echo \" PASS: $1\"; ((PASS++)); }\nfail() { echo \" FAIL: $1 — $2\"; ((FAIL++)); }\n\nmake_test_repo() {\n local repo_dir=\"$1\"\n mkdir -p \"$repo_dir/.agents/bin\" \"$repo_dir/.agents/skills\"\n cd \"$repo_dir\" || exit 1\n git init -q\n git checkout -q -b trunk\n git config user.email \"[email protected]\"\n git config user.name \"Test\"\n git config commit.gpgsign true\n\n cp \"$CANONICAL_AGENTS\" \"$repo_dir/AGENTS.md\"\n\n cat > .agents/bin/swain-trunk.sh \u003c\u003c'EOF_TRUNK'\n#!/usr/bin/env bash\necho trunk\nEOF_TRUNK\n chmod +x .agents/bin/swain-trunk.sh\n\n git add -A\n git commit -q -m \"initial: create test repo\"\n}\n\necho \"=== Preflight Legacy Skill Tests ===\"\necho \"\"\n\necho \"--- Clean repo does not report legacy skill directories ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/repo\"\nmake_test_repo \"$REPO_DIR\"\n\noutput=\"$(cd \"$REPO_DIR\" && bash \"$PREFLIGHT\" 2>&1)\"\nstatus=$?\nif [[ \"$output\" != *\"legacy skill directories detected\"* ]]; then\n pass \"clean repo stays clear of legacy-skill warnings\"\nelse\n fail \"clean repo stays clear of legacy-skill warnings\" \"status=$status output=$output\"\nfi\nrm -rf \"$TMPDIR\"\n\necho \"--- Fingerprinted stale skill triggers doctor via preflight ---\"\nTMPDIR=\"$(mktemp -d)\"\nREPO_DIR=\"$TMPDIR/repo\"\nmake_test_repo \"$REPO_DIR\"\nmkdir -p \"$REPO_DIR/.agents/skills/swain-status\"\ncat > \"$REPO_DIR/.agents/skills/swain-status/SKILL.md\" \u003c\u003c'EOF_SKILL'\n---\nname: swain-status\nauthor: cristos\nsource: swain\n---\n\n# swain-status\nEOF_SKILL\n\noutput=\"$(cd \"$REPO_DIR\" && bash \"$PREFLIGHT\" 2>&1)\"\nstatus=$?\nif [[ $status -eq 1 && \"$output\" == *\"legacy skill directories detected\"* ]]; then\n pass \"stale skill triggers doctor invocation\"\nelse\n fail \"stale skill triggers doctor invocation\" \"status=$status output=$output\"\nfi\nrm -rf \"$TMPDIR\"\n\necho \"\"\necho \"=== Summary ===\"\necho \"PASS: $PASS\"\necho \"FAIL: $FAIL\"\n\nif [[ $FAIL -gt 0 ]]; then\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2221,"content_sha256":"6cdbaa90485f2927a1ec0f0f87575d47a84f168c7da26951e97cfe87d8c49136"},{"filename":"tests/test-preflight-skill-changes.sh","content":"#!/usr/bin/env bash\n# test-preflight-skill-changes.sh — Verify preflight integrates skill-change detection\n#\n# Tests that swain-preflight.sh calls check-skill-changes.sh and includes\n# its findings in the issues list (triggering exit 1 → doctor invocation).\n#\n# Usage: bash skills/swain-doctor/tests/test-preflight-skill-changes.sh\n\nset +e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nPREFLIGHT=\"$(cd \"$SCRIPT_DIR/..\" && pwd)/scripts/swain-preflight.sh\"\nCHECK_SCRIPT=\"$(cd \"$SCRIPT_DIR/..\" && pwd)/scripts/check-skill-changes.sh\"\n\nPASS=0\nFAIL=0\n\npass() { echo \" PASS: $1\"; ((PASS++)); }\nfail() { echo \" FAIL: $1 — $2\"; ((FAIL++)); }\n\necho \"=== Preflight Skill Change Integration Tests ===\"\necho \"\"\n\n# --- AC5: Preflight calls check-skill-changes and exits 1 on findings ---\necho \"--- AC5: Preflight includes skill-change check ---\"\n\n# The preflight script contains a reference to check-skill-changes.sh\nif grep -q \"check-skill-changes\" \"$PREFLIGHT\"; then\n pass \"AC5: preflight references check-skill-changes.sh\"\nelse\n fail \"AC5\" \"preflight does not reference check-skill-changes.sh\"\nfi\n\n# Verify the check script exists and is executable\nif [[ -x \"$CHECK_SCRIPT\" ]]; then\n pass \"AC5: check-skill-changes.sh exists and is executable\"\nelse\n fail \"AC5\" \"check-skill-changes.sh missing or not executable\"\nfi\n\necho \"\"\necho \"=== Summary ===\"\necho \"PASS: $PASS\"\necho \"FAIL: $FAIL\"\n\nif [[ $FAIL -gt 0 ]]; then\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":1441,"content_sha256":"cbf4a8661de3c620fd82469a6a9ee2bfaad5799652daf77ef2baac98318a93f3"},{"filename":"tests/test-ssh-readiness.sh","content":"#!/usr/bin/env bash\n# test-ssh-readiness.sh — Acceptance tests for swain-doctor SSH readiness helper\n#\n# Usage: bash skills/swain-doctor/tests/test-ssh-readiness.sh\n\nset +e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSSH_HELPER=\"$(cd \"$SCRIPT_DIR/..\" && pwd)/scripts/ssh-readiness.sh\"\n\nPASS=0\nFAIL=0\n\npass() { echo \" PASS: $1\"; ((PASS++)); }\nfail() { echo \" FAIL: $1 — $2\"; ((FAIL++)); }\n\nmake_repo() {\n local repo_dir=\"$1\"\n mkdir -p \"$repo_dir\"\n git -C \"$repo_dir\" init -q\n git -C \"$repo_dir\" remote add origin \"[email protected]:cristoslc/swain.git\"\n}\n\necho \"=== swain-doctor SSH Readiness Tests ===\"\necho \"Helper: $SSH_HELPER\"\necho \"\"\n\necho \"--- AC1: check reports missing alias config for alias remotes ---\"\nTMPDIR=\"$(mktemp -d)\"\nHOME_DIR=\"$TMPDIR/home\"\nREPO_DIR=\"$TMPDIR/repo\"\nmkdir -p \"$HOME_DIR\"\nmake_repo \"$REPO_DIR\"\n\noutput=\"$(cd \"$REPO_DIR\" && HOME=\"$HOME_DIR\" bash \"$SSH_HELPER\" --check 2>&1)\"\nstatus=$?\nif [[ $status -ne 0 && \"$output\" == *\"github.com-swain\"* && \"$output\" == *\"swain-keys --provision\"* ]]; then\n pass \"AC1: missing alias config reported\"\nelse\n fail \"AC1\" \"status=$status output=$output\"\nfi\n\necho \"--- AC2: repair creates include + alias config when key exists ---\"\nmkdir -p \"$HOME_DIR/.ssh\"\ntouch \"$HOME_DIR/.ssh/swain_signing\"\nchmod 600 \"$HOME_DIR/.ssh/swain_signing\"\n\noutput=\"$(cd \"$REPO_DIR\" && HOME=\"$HOME_DIR\" bash \"$SSH_HELPER\" --repair 2>&1)\"\nstatus=$?\nconfig_file=\"$HOME_DIR/.ssh/config\"\nalias_file=\"$HOME_DIR/.ssh/config.d/swain.conf\"\nif [[ $status -eq 0 && -f \"$config_file\" && -f \"$alias_file\" ]] \\\n && grep -q \"Include config.d/\\\\*\" \"$config_file\" \\\n && grep -q \"Host github.com-swain\" \"$alias_file\" \\\n && grep -q \"HostName ssh.github.com\" \"$alias_file\" \\\n && grep -q \"Port 443\" \"$alias_file\" \\\n && grep -q \"IdentityFile $HOME_DIR/.ssh/swain_signing\" \"$alias_file\"; then\n pass \"AC2: repair writes SSH include and alias config\"\nelse\n fail \"AC2\" \"status=$status output=$output\"\nfi\n\necho \"--- AC3: repair migrates legacy github.com:22 alias config ---\"\ncat > \"$alias_file\" \u003c\u003cEOF\nHost github.com-swain\n HostName github.com\n User git\n IdentityFile $HOME_DIR/.ssh/swain_signing\n IdentitiesOnly yes\nEOF\n\noutput=\"$(cd \"$REPO_DIR\" && HOME=\"$HOME_DIR\" bash \"$SSH_HELPER\" --repair 2>&1)\"\nstatus=$?\nif [[ $status -eq 0 ]] \\\n && grep -q \"HostName ssh.github.com\" \"$alias_file\" \\\n && grep -q \"Port 443\" \"$alias_file\"; then\n pass \"AC3: repair migrates legacy alias config\"\nelse\n fail \"AC3\" \"status=$status output=$output\"\nfi\n\nrm -rf \"$TMPDIR\"\n\necho \"\"\necho \"=== Summary ===\"\necho \"PASS: $PASS\"\necho \"FAIL: $FAIL\"\n\nif [[ $FAIL -gt 0 ]]; then\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2625,"content_sha256":"f75ba6b936f67e34ffd1c0a6a8123ecca31230bc523f4a35442a18c4a414c9be"},{"filename":"tests/test-swain-doctor-sh.sh","content":"#!/usr/bin/env bash\n# test-swain-doctor-sh.sh — tests for the consolidated swain-doctor.sh script\n# Verifies SPEC-192: parallel check cascade failure fix\n\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR/../../..\" && pwd)\"\nDOCTOR_SCRIPT=\"$REPO_ROOT/.agents/bin/swain-doctor.sh\"\n\nPASS=0\nFAIL=0\nTOTAL=0\n\nassert() {\n local desc=\"$1\"\n local result=\"$2\"\n TOTAL=$((TOTAL + 1))\n if [[ \"$result\" == \"0\" ]]; then\n PASS=$((PASS + 1))\n echo \" PASS: $desc\"\n else\n FAIL=$((FAIL + 1))\n echo \" FAIL: $desc\"\n fi\n}\n\n# --- Test 1: Script exists and is executable ---\necho \"Test 1: swain-doctor.sh exists and is executable\"\nassert \"script exists\" \"$([ -f \"$DOCTOR_SCRIPT\" ] && echo 0 || echo 1)\"\nassert \"script is executable\" \"$([ -x \"$DOCTOR_SCRIPT\" ] && echo 0 || echo 1)\"\n\n# --- Test 2: Script outputs valid JSON ---\necho \"Test 2: outputs valid JSON\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n echo \"$output\" | jq empty 2>/dev/null\n assert \"output is valid JSON\" \"$?\"\nelse\n assert \"output is valid JSON\" \"1\"\nfi\n\n# --- Test 3: JSON has expected structure ---\necho \"Test 3: JSON has expected top-level fields\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n assert \"has 'checks' array\" \"$(echo \"$output\" | jq -e '.checks | type == \"array\"' >/dev/null 2>&1 && echo 0 || echo 1)\"\n assert \"has 'summary' object\" \"$(echo \"$output\" | jq -e '.summary | type == \"object\"' >/dev/null 2>&1 && echo 0 || echo 1)\"\n assert \"summary has total count\" \"$(echo \"$output\" | jq -e '.summary.total | type == \"number\"' >/dev/null 2>&1 && echo 0 || echo 1)\"\nelse\n assert \"has 'checks' array\" \"1\"\n assert \"has 'summary' object\" \"1\"\n assert \"summary has total count\" \"1\"\nfi\n\n# --- Test 4: Each check has name, status, and message ---\necho \"Test 4: each check entry has required fields\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n # Every check must have name and status\n bad_checks=$(echo \"$output\" | jq '[.checks[] | select(.name == null or .status == null)] | length' 2>/dev/null || echo \"999\")\n assert \"all checks have name and status\" \"$([ \"$bad_checks\" = \"0\" ] && echo 0 || echo 1)\"\nelse\n assert \"all checks have name and status\" \"1\"\nfi\n\n# --- Test 5: Script does NOT exit non-zero when checks find issues ---\necho \"Test 5: script always exits 0 (findings reported in JSON, not exit code)\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n bash \"$DOCTOR_SCRIPT\" >/dev/null 2>&1\n assert \"exits 0 regardless of findings\" \"$?\"\nelse\n assert \"exits 0 regardless of findings\" \"1\"\nfi\n\n# --- Test 6: Known check names are present ---\necho \"Test 6: known check categories are present\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n check_names=$(echo \"$output\" | jq -r '.checks[].name' 2>/dev/null || true)\n assert \"governance check present\" \"$(echo \"$check_names\" | grep -q \"governance\" && echo 0 || echo 1)\"\n assert \"tools check present\" \"$(echo \"$check_names\" | grep -q \"tools\" && echo 0 || echo 1)\"\n assert \"agents_directory check present\" \"$(echo \"$check_names\" | grep -q \"agents_directory\" && echo 0 || echo 1)\"\nelse\n assert \"governance check present\" \"1\"\n assert \"tools check present\" \"1\"\n assert \"agents_directory check present\" \"1\"\nfi\n\n# --- Test 7: agents_bin_symlinks check is present and passes (SPEC-206) ---\necho \"Test 7: agents_bin_symlinks check\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n check_names=$(echo \"$output\" | jq -r '.checks[].name' 2>/dev/null || true)\n assert \"agents_bin_symlinks check present\" \"$(echo \"$check_names\" | grep -q \"agents_bin_symlinks\" && echo 0 || echo 1)\"\n symlink_status=$(echo \"$output\" | jq -r '.checks[] | select(.name == \"agents_bin_symlinks\") | .status' 2>/dev/null || echo \"missing\")\n assert \"agents_bin_symlinks not warning\" \"$([ \"$symlink_status\" != \"warning\" ] && echo 0 || echo 1)\"\nelse\n assert \"agents_bin_symlinks check present\" \"1\"\n assert \"agents_bin_symlinks not warning\" \"1\"\nfi\n\n# --- Test 8: agents_bin_symlinks detects broken symlinks (SPEC-206) ---\necho \"Test 8: agents_bin_symlinks detects broken symlinks\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n # Create a broken symlink, run doctor (which auto-repairs), verify it was caught\n FAKE_LINK=\"$REPO_ROOT/.agents/bin/_test_broken_link.sh\"\n ln -sf \"../../skills/nonexistent/scripts/fake.sh\" \"$FAKE_LINK\"\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n detail=$(echo \"$output\" | jq -r '.checks[] | select(.name == \"agents_bin_symlinks\") | .detail // \"\"' 2>/dev/null || echo \"\")\n assert \"detects broken symlink\" \"$(echo \"$detail\" | grep -q \"_test_broken_link\" && echo 0 || echo 1)\"\n # Verify auto-repair removed it\n assert \"auto-repairs broken symlink\" \"$([ ! -L \"$FAKE_LINK\" ] && echo 0 || echo 1)\"\n rm -f \"$FAKE_LINK\" 2>/dev/null || true\nelse\n assert \"detects broken symlink\" \"1\"\n assert \"auto-repairs broken symlink\" \"1\"\nfi\n\n# --- Test 9: agents_bin_symlinks detects missing symlinks (SPEC-206) ---\necho \"Test 9: agents_bin_symlinks detects and repairs missing symlinks\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n # Remove a known symlink, run doctor, verify it was repaired\n TARGET=\"$REPO_ROOT/.agents/bin/swain-session-greeting.sh\"\n if [[ -L \"$TARGET\" ]]; then\n SAVED_LINK=$(readlink \"$TARGET\")\n rm -f \"$TARGET\"\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n assert \"repairs missing symlink\" \"$([ -L \"$TARGET\" ] && [ -e \"$TARGET\" ] && echo 0 || echo 1)\"\n # Clean up — restore original if repair used a different path\n if [[ ! -e \"$TARGET\" ]]; then\n ln -sf \"$SAVED_LINK\" \"$TARGET\"\n fi\n else\n # Symlink wasn't there to begin with — skip\n TOTAL=$((TOTAL + 1))\n PASS=$((PASS + 1))\n echo \" PASS: (skipped — symlink not present to test removal)\"\n fi\nelse\n assert \"repairs missing symlink\" \"1\"\nfi\n# ============================================================\n# SPEC-222: Auto-repair promotion tests\n# ============================================================\n\n# --- Test 10: memory_directory auto-repair (SPEC-222 AC1) ---\necho \"Test 10: memory_directory auto-repair creates missing dir\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n project_slug=$(echo \"$REPO_ROOT\" | tr '/' '-')\n memory_dir=\"$HOME/.claude/projects/${project_slug}/memory\"\n memory_existed=false\n if [[ -d \"$memory_dir\" ]]; then\n memory_existed=true\n else\n # Remove it so we can test repair\n true\n fi\n\n if [[ \"$memory_existed\" == \"false\" ]]; then\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n mem_status=$(echo \"$output\" | jq -r '.checks[] | select(.name == \"memory_directory\") | .status' 2>/dev/null || echo \"missing\")\n assert \"memory_directory auto-creates dir and reports advisory\" \"$([ \"$mem_status\" = \"advisory\" ] && echo 0 || echo 1)\"\n assert \"memory_directory dir now exists after repair\" \"$([ -d \"$memory_dir\" ] && echo 0 || echo 1)\"\n # Idempotency: run again, should report ok\n output2=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n mem_status2=$(echo \"$output2\" | jq -r '.checks[] | select(.name == \"memory_directory\") | .status' 2>/dev/null || echo \"missing\")\n assert \"memory_directory idempotent (ok on second run)\" \"$([ \"$mem_status2\" = \"ok\" ] && echo 0 || echo 1)\"\n else\n # Dir already exists — just verify ok\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n mem_status=$(echo \"$output\" | jq -r '.checks[] | select(.name == \"memory_directory\") | .status' 2>/dev/null || echo \"missing\")\n assert \"memory_directory reports ok when dir exists\" \"$([ \"$mem_status\" = \"ok\" ] && echo 0 || echo 1)\"\n # Force test: remove dir, run, verify advisory + creation\n rmdir \"$memory_dir\" 2>/dev/null || true\n if [[ ! -d \"$memory_dir\" ]]; then\n output2=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n mem_status2=$(echo \"$output2\" | jq -r '.checks[] | select(.name == \"memory_directory\") | .status' 2>/dev/null || echo \"missing\")\n assert \"memory_directory auto-creates and reports advisory\" \"$([ \"$mem_status2\" = \"advisory\" ] && echo 0 || echo 1)\"\n assert \"memory_directory dir exists after repair\" \"$([ -d \"$memory_dir\" ] && echo 0 || echo 1)\"\n # Restore\n mkdir -p \"$memory_dir\"\n else\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — could not remove non-empty memory dir)\"\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — could not remove non-empty memory dir)\"\n fi\n fi\nelse\n assert \"memory_directory auto-creates dir and reports advisory\" \"1\"\n assert \"memory_directory dir now exists after repair\" \"1\"\n assert \"memory_directory idempotent (ok on second run)\" \"1\"\nfi\n\n# --- Test 11: script_permissions auto-repair (SPEC-222 AC2) ---\necho \"Test 11: script_permissions auto-repair chmod +x\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n # Find a swain-owned script to chmod -x temporarily\n test_script=$(find \"$REPO_ROOT/skills\" -path '*/scripts/*.sh' -perm -u+x -not -name 'test-*' | head -1 || true)\n if [[ -n \"$test_script\" ]]; then\n chmod -x \"$test_script\"\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n perm_status=$(echo \"$output\" | jq -r '.checks[] | select(.name == \"script_permissions\") | .status' 2>/dev/null || echo \"missing\")\n assert \"script_permissions reports advisory after repair\" \"$([ \"$perm_status\" = \"advisory\" ] && echo 0 || echo 1)\"\n assert \"script_permissions restores execute bit\" \"$([ -x \"$test_script\" ] && echo 0 || echo 1)\"\n # Idempotency\n output2=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n perm_status2=$(echo \"$output2\" | jq -r '.checks[] | select(.name == \"script_permissions\") | .status' 2>/dev/null || echo \"missing\")\n assert \"script_permissions idempotent (ok on second run)\" \"$([ \"$perm_status2\" = \"ok\" ] && echo 0 || echo 1)\"\n else\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — no suitable test script found)\"\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — no suitable test script found)\"\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — no suitable test script found)\"\n fi\nelse\n assert \"script_permissions reports advisory after repair\" \"1\"\n assert \"script_permissions restores execute bit\" \"1\"\n assert \"script_permissions idempotent (ok on second run)\" \"1\"\nfi\n\n# --- Test 12: crash_debris git lock auto-repair (SPEC-222 AC5) ---\necho \"Test 12: crash_debris removes stale .git/index.lock\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n # Resolve git dir (handles worktrees where .git is a file)\n if [[ -f \"$REPO_ROOT/.git\" ]]; then\n _git_dir=$(sed 's/^gitdir: //' \"$REPO_ROOT/.git\")\n [[ \"$_git_dir\" != /* ]] && _git_dir=\"$REPO_ROOT/$_git_dir\"\n else\n _git_dir=\"$REPO_ROOT/.git\"\n fi\n lock_file=\"$_git_dir/index.lock\"\n\n if [[ ! -f \"$lock_file\" ]]; then\n # Create a fake stale lock (PID 99999999 is not running)\n echo \"99999999\" > \"$lock_file\"\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n debris_status=$(echo \"$output\" | jq -r '.checks[] | select(.name == \"crash_debris\") | .status' 2>/dev/null || echo \"missing\")\n # Lock should be removed regardless of other debris in the environment\n assert \"crash_debris removes git index.lock\" \"$([ ! -f \"$lock_file\" ] && echo 0 || echo 1)\"\n # Status is advisory if lock was sole issue, warning if other debris also found\n assert \"crash_debris reports advisory or warning (not ok/missing) after lock repair\" \\\n \"$([ \"$debris_status\" = \"advisory\" ] || [ \"$debris_status\" = \"warning\" ] && echo 0 || echo 1)\"\n # Idempotency: run again, lock should still be gone (not re-created)\n output2=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n lock_in_output2=$(echo \"$output2\" | jq -r '.checks[] | select(.name == \"crash_debris\") | .message // \"\"' 2>/dev/null || echo \"\")\n assert \"crash_debris does not re-report removed lock on second run\" \\\n \"$(echo \"$lock_in_output2\" | grep -qv 'index.lock' && echo 0 || echo 1)\"\n rm -f \"$lock_file\" 2>/dev/null || true\n else\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — real index.lock exists, not safe to test)\"\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — real index.lock exists, not safe to test)\"\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — real index.lock exists, not safe to test)\"\n fi\nelse\n assert \"crash_debris removes git index.lock\" \"1\"\n assert \"crash_debris reports advisory or warning (not ok/missing) after lock repair\" \"1\"\n assert \"crash_debris does not re-report removed lock on second run\" \"1\"\nfi\n\n# --- Test 13: governance stale block auto-repair (SPEC-222 AC4) ---\necho \"Test 13: governance stale block auto-repair\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n # Find a context file with governance markers\n gov_file=$(grep -l \"\u003c!-- swain governance\" \"$REPO_ROOT/AGENTS.md\" \"$REPO_ROOT/CLAUDE.md\" 2>/dev/null | head -1 || true)\n canonical=$(find \"$REPO_ROOT/skills/swain-doctor/references\" -name \"AGENTS.content.md\" 2>/dev/null | head -1 || true)\n if [[ -n \"$gov_file\" && -f \"$canonical\" ]]; then\n # Corrupt the governance block by inserting a sentinel line\n backup=$(mktemp)\n cp -f \"$gov_file\" \"$backup\"\n awk '\n /\u003c!-- swain governance/{p=1; print; next}\n /\u003c!-- end swain governance/{p=0}\n p==1 && !done{print \"# SPEC-222-TEST-SENTINEL\"; done=1; next}\n {print}\n ' \"$gov_file\" > \"${gov_file}.tmp\" && mv -f \"${gov_file}.tmp\" \"$gov_file\"\n\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n gov_status=$(echo \"$output\" | jq -r '.checks[] | select(.name == \"governance\") | .status' 2>/dev/null || echo \"missing\")\n assert \"governance stale block auto-repair reports advisory\" \"$([ \"$gov_status\" = \"advisory\" ] && echo 0 || echo 1)\"\n # Sentinel should be gone after repair\n assert \"governance block sentinel removed after repair\" \"$(! grep -q 'SPEC-222-TEST-SENTINEL' \"$gov_file\" && echo 0 || echo 1)\"\n # Idempotency\n output2=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n gov_status2=$(echo \"$output2\" | jq -r '.checks[] | select(.name == \"governance\") | .status' 2>/dev/null || echo \"missing\")\n assert \"governance idempotent (ok on second run)\" \"$([ \"$gov_status2\" = \"ok\" ] && echo 0 || echo 1)\"\n # Restore\n cp -f \"$backup\" \"$gov_file\"\n rm -f \"$backup\"\n else\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — no governance file or canonical source found)\"\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — no governance file or canonical source found)\"\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — no governance file or canonical source found)\"\n fi\nelse\n assert \"governance stale block auto-repair reports advisory\" \"1\"\n assert \"governance block sentinel removed after repair\" \"1\"\n assert \"governance idempotent (ok on second run)\" \"1\"\nfi\n\n# --- Test 14: commit_signing auto-repair (SPEC-222 AC3) ---\necho \"Test 14: commit_signing auto-repair\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n current_gpgsign=$(git -C \"$REPO_ROOT\" config --local commit.gpgsign 2>/dev/null || echo \"\")\n signing_key_path=\"$HOME/.ssh/swain_signing\"\n allowed_signers=$(git -C \"$REPO_ROOT\" config --global gpg.ssh.allowedSignersFile 2>/dev/null || echo \"\")\n key_available=false\n [[ -f \"$signing_key_path\" ]] && key_available=true\n [[ -n \"$allowed_signers\" && -f \"$allowed_signers\" ]] && key_available=true\n\n if [[ \"$key_available\" == \"true\" ]]; then\n # Disable signing, run doctor, expect advisory\n git -C \"$REPO_ROOT\" config --local commit.gpgsign false\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n sign_status=$(echo \"$output\" | jq -r '.checks[] | select(.name == \"commit_signing\") | .status' 2>/dev/null || echo \"missing\")\n assert \"commit_signing auto-configures when key present (advisory)\" \"$([ \"$sign_status\" = \"advisory\" ] && echo 0 || echo 1)\"\n configured=$(git -C \"$REPO_ROOT\" config --local commit.gpgsign 2>/dev/null || echo \"\")\n assert \"commit_signing sets gpgsign=true\" \"$([ \"$configured\" = \"true\" ] && echo 0 || echo 1)\"\n # Restore\n if [[ -n \"$current_gpgsign\" ]]; then\n git -C \"$REPO_ROOT\" config --local commit.gpgsign \"$current_gpgsign\"\n else\n git -C \"$REPO_ROOT\" config --local --unset commit.gpgsign 2>/dev/null || true\n fi\n else\n # No key: doctor should warn, not configure\n git -C \"$REPO_ROOT\" config --local commit.gpgsign false 2>/dev/null || true\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n sign_status=$(echo \"$output\" | jq -r '.checks[] | select(.name == \"commit_signing\") | .status' 2>/dev/null || echo \"missing\")\n assert \"commit_signing warns when no key available\" \"$([ \"$sign_status\" = \"warning\" ] && echo 0 || echo 1)\"\n # Restore\n if [[ -n \"$current_gpgsign\" ]]; then\n git -C \"$REPO_ROOT\" config --local commit.gpgsign \"$current_gpgsign\"\n else\n git -C \"$REPO_ROOT\" config --local --unset commit.gpgsign 2>/dev/null || true\n fi\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — no signing key to test auto-configure path)\"\n fi\nelse\n assert \"commit_signing auto-configures when key present (advisory)\" \"1\"\n assert \"commit_signing sets gpgsign=true\" \"1\"\nfi\n\n# --- Test 15: artifact_indexes check is present ---\necho \"Test 15: artifact_indexes check\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n check_names=$(echo \"$output\" | jq -r '.checks[].name' 2>/dev/null || true)\n assert \"artifact_indexes check present\" \"$(echo \"$check_names\" | grep -q \"artifact_indexes\" && echo 0 || echo 1)\"\nelse\n assert \"artifact_indexes check present\" \"1\"\nfi\n\n# --- Test 16: artifact_indexes repairs stale index and is idempotent ---\necho \"Test 16: artifact_indexes repairs stale index\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n index_file=\"$REPO_ROOT/docs/spec/list-spec.md\"\n if [[ -f \"$index_file\" ]]; then\n backup=$(mktemp)\n cp -f \"$index_file\" \"$backup\"\n printf '\\n\u003c!-- SPEC-227-TEST-SENTINEL -->\\n' >> \"$index_file\"\n\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n idx_status=$(echo \"$output\" | jq -r '.checks[] | select(.name == \"artifact_indexes\") | .status' 2>/dev/null || echo \"missing\")\n assert \"artifact_indexes reports advisory after repairing stale index\" \"$([ \"$idx_status\" = \"advisory\" ] && echo 0 || echo 1)\"\n assert \"artifact_indexes removes stale sentinel from list-spec.md\" \"$(! grep -q 'SPEC-227-TEST-SENTINEL' \"$index_file\" && echo 0 || echo 1)\"\n\n output2=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n idx_status2=$(echo \"$output2\" | jq -r '.checks[] | select(.name == \"artifact_indexes\") | .status' 2>/dev/null || echo \"missing\")\n assert \"artifact_indexes is idempotent after stale repair\" \"$([ \"$idx_status2\" = \"ok\" ] && echo 0 || echo 1)\"\n\n cp -f \"$backup\" \"$index_file\"\n rm -f \"$backup\"\n else\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — docs/spec/list-spec.md not present)\"\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — docs/spec/list-spec.md not present)\"\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — docs/spec/list-spec.md not present)\"\n fi\nelse\n assert \"artifact_indexes reports advisory after repairing stale index\" \"1\"\n assert \"artifact_indexes removes stale sentinel from list-spec.md\" \"1\"\n assert \"artifact_indexes is idempotent after stale repair\" \"1\"\nfi\n\n# --- Test 17: artifact_indexes recreates missing index ---\necho \"Test 17: artifact_indexes recreates missing index\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]]; then\n index_file=\"$REPO_ROOT/docs/spec/list-spec.md\"\n if [[ -f \"$index_file\" ]]; then\n backup=$(mktemp)\n cp -f \"$index_file\" \"$backup\"\n rm -f \"$index_file\"\n\n output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n idx_status=$(echo \"$output\" | jq -r '.checks[] | select(.name == \"artifact_indexes\") | .status' 2>/dev/null || echo \"missing\")\n assert \"artifact_indexes reports advisory after recreating missing index\" \"$([ \"$idx_status\" = \"advisory\" ] && echo 0 || echo 1)\"\n assert \"artifact_indexes recreates docs/spec/list-spec.md\" \"$([ -f \"$index_file\" ] && echo 0 || echo 1)\"\n\n cp -f \"$backup\" \"$index_file\"\n rm -f \"$backup\"\n else\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — docs/spec/list-spec.md not present)\"\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — docs/spec/list-spec.md not present)\"\n fi\nelse\n assert \"artifact_indexes reports advisory after recreating missing index\" \"1\"\n assert \"artifact_indexes recreates docs/spec/list-spec.md\" \"1\"\nfi\n\n# --- Test 18: SPEC-290 — .swain/init.json symlink repair in linked worktrees ---\necho \"Test 18: SPEC-290 — .swain/init.json symlink repair in linked worktrees\"\nif [[ -x \"$DOCTOR_SCRIPT\" ]] && [[ -f \"$REPO_ROOT/.swain/init.json\" ]]; then\n # Create a real linked worktree without .swain/init.json to simulate the bug.\n tmp_wt=\"\"\n tmp_branch=\"test-spec-290-$\"\n tmp_wt=\"$(mktemp -d)\"\n rmdir \"$tmp_wt\" # git worktree add requires the path to not exist\n if git -C \"$REPO_ROOT\" worktree add \"$tmp_wt\" -b \"$tmp_branch\" >/dev/null 2>&1; then\n # Confirm .swain/init.json is absent before doctor runs.\n assert \"worktree starts without .swain/init.json\" \"$([ ! -e \"$tmp_wt/.swain/init.json\" ] && echo 0 || echo 1)\"\n\n # Run doctor from the worktree root (simulates swain-init invoking doctor inside worktree).\n wt_output=$(REPO_ROOT=\"$tmp_wt\" bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n wt_status=$(echo \"$wt_output\" | jq -r '.checks[] | select(.name == \"worktrees\") | .status' 2>/dev/null || echo \"missing\")\n\n assert \"worktrees check runs when doctor invoked from worktree\" \"$([ \"$wt_status\" != \"missing\" ] && echo 0 || echo 1)\"\n assert \".swain/init.json symlink created in worktree after doctor repair\" \"$([ -e \"$tmp_wt/.swain/init.json\" ] && echo 0 || echo 1)\"\n assert \".swain/init.json symlink points to main repo source\" \"$([ \"$(readlink \"$tmp_wt/.swain/init.json\" 2>/dev/null)\" = \"$REPO_ROOT/.swain/init.json\" ] && echo 0 || echo 1)\"\n\n # Verify preflight now reports delegate (not onboard) for the repaired worktree.\n preflight_script=\"$(find \"$REPO_ROOT\" -path '*/swain-init/scripts/swain-init-preflight.sh' -print -quit 2>/dev/null || true)\"\n if [[ -f \"$preflight_script\" ]]; then\n pf_action=$(bash \"$preflight_script\" --repo-root \"$tmp_wt\" 2>/dev/null | jq -r '.marker.action' 2>/dev/null || echo \"missing\")\n assert \"preflight reports delegate after repair\" \"$([ \"$pf_action\" = \"delegate\" ] && echo 0 || echo 1)\"\n else\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — preflight script not found)\"\n fi\n\n # Also verify repair from main root repairs all linked worktrees (not just self).\n rm -f \"$tmp_wt/.swain/init.json\"\n main_output=$(bash \"$DOCTOR_SCRIPT\" 2>/dev/null || true)\n assert \".swain/init.json symlink created from main root run\" \"$([ -e \"$tmp_wt/.swain/init.json\" ] && echo 0 || echo 1)\"\n\n # Cleanup.\n git -C \"$REPO_ROOT\" worktree remove --force \"$tmp_wt\" 2>/dev/null || true\n git -C \"$REPO_ROOT\" branch -D \"$tmp_branch\" 2>/dev/null || true\n else\n for _ in 1 2 3 4 5; do\n TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1))\n echo \" PASS: (skipped — could not create test worktree)\"\n done\n fi\nelse\n for _ in 1 2 3 4 5; do\n assert \".swain/init.json repair test (skipped)\" \"1\"\n done\nfi\n\n# --- Summary ---\necho \"\"\necho \"Results: $PASS/$TOTAL passed, $FAIL failed\"\nif [[ $FAIL -gt 0 ]]; then\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":23620,"content_sha256":"0dbf8e77b399d5c0f1058c8dcf007212254282e4b14a9d16980820a8ab06c031"}],"content_json":{"type":"doc","content":[{"type":"paragraph","content":[{"text":"\u003c!-- swain-model-hint: sonnet, effort: low -->","type":"text"}]},{"type":"heading","attrs":{"level":1},"content":[{"text":"Doctor","type":"text"}]},{"type":"paragraph","content":[{"text":"Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Running the script","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"REPO_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"\nbash \"$REPO_ROOT/.agents/bin/swain-doctor.sh\"","type":"text"}]},{"type":"paragraph","content":[{"text":"The script outputs structured JSON with ","type":"text"},{"text":"{ checks: [...], summary: {...} }","type":"text","marks":[{"type":"code_inline"}]},{"text":". Each check has ","type":"text"},{"text":"name","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"status","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"message","type":"text","marks":[{"type":"code_inline"}]},{"text":", and optional ","type":"text"},{"text":"detail","type":"text","marks":[{"type":"code_inline"}]},{"text":". Parse it and present the summary table to the operator. Then use the remediation sections below ","type":"text"},{"text":"only for checks that reported ","type":"text","marks":[{"type":"strong"}]},{"text":"warning","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" or ","type":"text","marks":[{"type":"strong"}]},{"text":"advisory","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" status","type":"text","marks":[{"type":"strong"}]},{"text":" — do not re-run detection.","type":"text"}]},{"type":"paragraph","content":[{"text":"To auto-fix flat-file artifacts: ","type":"text"},{"text":"bash \"$REPO_ROOT/.agents/bin/swain-doctor.sh\" --fix-flat-artifacts","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"If the script is unavailable (e.g., ","type":"text"},{"text":".agents/bin/","type":"text","marks":[{"type":"code_inline"}]},{"text":" symlinks not yet bootstrapped), fall back to running checks from the script source at ","type":"text"},{"text":"skills/swain-doctor/scripts/swain-doctor.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":". Run checks ","type":"text"},{"text":"sequentially","type":"text","marks":[{"type":"strong"}]},{"text":" (one Bash call at a time), never in parallel — parallel tool calls cascade-cancel on first error.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Preflight integration","type":"text"}]},{"type":"paragraph","content":[{"text":"A lightweight shell script (","type":"text"},{"text":"$REPO_ROOT/.agents/bin/swain-preflight.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":") performs quick checks before invoking the full doctor. If preflight exits 0, swain-doctor is skipped for the session. If it exits 1, swain-doctor runs normally.","type":"text"}]},{"type":"paragraph","content":[{"text":"When invoked directly by the user (not via auto-invoke), swain-doctor always runs regardless of preflight status.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Governance content reference","type":"text"}]},{"type":"paragraph","content":[{"text":"The canonical governance rules live in ","type":"text"},{"text":"references/AGENTS.content.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". Both swain-doctor and swain-init read from this single source of truth. If the upstream rules change in a future swain release, update that file and bump the skill version.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":1},"content":[{"text":"Remediation by check name","type":"text"}]},{"type":"paragraph","content":[{"text":"Each section below corresponds to a check name emitted by the script. Only consult the relevant section when the check status is ","type":"text"},{"text":"warning","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"advisory","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"governance","type":"text"}]},{"type":"paragraph","content":[{"text":"Injection (missing):","type":"text","marks":[{"type":"strong"}]},{"text":" Read ","type":"text"},{"text":"references/governance-injection.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/governance-injection.md","title":null}}]},{"text":" for Claude Code and Cursor injection procedures. Source: ","type":"text"},{"text":"references/AGENTS.content.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Replacement (stale):","type":"text","marks":[{"type":"strong"}]},{"text":" The script auto-repairs stale governance blocks when both markers (","type":"text"},{"text":"\u003c!-- swain governance -->","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"\u003c!-- end swain governance -->","type":"text","marks":[{"type":"code_inline"}]},{"text":") are present. If auto-repair fails (markers missing), read ","type":"text"},{"text":"references/governance-injection.md § Stale governance replacement","type":"text","marks":[{"type":"link","attrs":{"href":"references/governance-injection.md","title":null}}]},{"text":" for manual replacement.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"legacy_skills","type":"text"}]},{"type":"paragraph","content":[{"text":"Clean up renamed and retired skill directories using fingerprint checks. Read ","type":"text"},{"text":"references/legacy-cleanup.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/legacy-cleanup.md","title":null}}]},{"text":" for the full procedure. Data source: ","type":"text"},{"text":"references/legacy-skills.json","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"agents_directory","type":"text"}]},{"type":"paragraph","content":[{"text":"Create ","type":"text"},{"text":".agents/","type":"text","marks":[{"type":"code_inline"}]},{"text":" with ","type":"text"},{"text":"mkdir -p .agents","type":"text","marks":[{"type":"code_inline"}]},{"text":". This directory is used by swain-do for configuration and by swain-design scripts for logs.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"tickets","type":"text"}]},{"type":"paragraph","content":[{"text":"Validates ","type":"text"},{"text":".tickets/","type":"text","marks":[{"type":"code_inline"}]},{"text":" health — YAML frontmatter, stale locks. Read ","type":"text"},{"text":"references/tickets-validation.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/tickets-validation.md","title":null}}]},{"text":" for repair procedures.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"beads_migration","type":"text"}]},{"type":"paragraph","content":[{"text":"Auto-migrates ","type":"text"},{"text":".beads/","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":".tickets/","type":"text","marks":[{"type":"code_inline"}]},{"text":" if present. Read ","type":"text"},{"text":"references/beads-migration.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/beads-migration.md","title":null}}]},{"text":" for the migration procedure.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"tools","type":"text"}]},{"type":"paragraph","content":[{"text":"Required tools: ","type":"text"},{"text":"git","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"jq","type":"text","marks":[{"type":"code_inline"}]},{"text":". Optional: ","type":"text"},{"text":"tk","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"uv","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"gh","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"tmux","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"fswatch","type":"text","marks":[{"type":"code_inline"}]},{"text":". Never install automatically. Read ","type":"text"},{"text":"references/tool-availability.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/tool-availability.md","title":null}}]},{"text":" for degradation notes and install instructions.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"settings","type":"text"}]},{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"swain.settings.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" is missing, create it with default content. If it contains invalid JSON, fix the syntax. User settings live at ","type":"text"},{"text":"${XDG_CONFIG_HOME:-$HOME/.config}/swain/settings.json","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"script_permissions","type":"text"}]},{"type":"paragraph","content":[{"text":"The script auto-repairs missing execute permissions on ","type":"text"},{"text":".sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":".py","type":"text","marks":[{"type":"code_inline"}]},{"text":" files in the skill tree. No manual remediation needed — advisory status means it already fixed them.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"memory_directory","type":"text"}]},{"type":"paragraph","content":[{"text":"The script auto-creates the memory directory if missing. If creation fails, manually create:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mkdir -p \"$HOME/.claude/projects/$(echo \"$REPO_ROOT\" | tr '/' '-')/memory\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"superpowers","type":"text"}]},{"type":"paragraph","content":[{"text":"When status is ","type":"text"},{"text":"warning","type":"text","marks":[{"type":"code_inline"}]},{"text":" (missing or partial), ask the operator:","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Superpowers (","type":"text"},{"text":"obra/superpowers","type":"text","marks":[{"type":"code_inline"}]},{"text":") is not installed [or: partially installed]. It provides TDD, brainstorming, plan writing, and verification skills that swain chains into.","type":"text"}]},{"type":"paragraph","content":[{"text":"Install superpowers now? (yes/no)","type":"text"}]}]},{"type":"paragraph","content":[{"text":"If yes: ","type":"text"},{"text":"npx skills add obra/superpowers","type":"text","marks":[{"type":"code_inline"}]},{"text":". If no, note \"Superpowers: skipped\" and continue.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"epics_initiative","type":"text"}]},{"type":"paragraph","content":[{"text":"Non-blocking advisory. Report the count and suggest:","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"N Epic(s) have ","type":"text"},{"text":"parent-vision","type":"text","marks":[{"type":"code_inline"}]},{"text":" but no ","type":"text"},{"text":"parent-initiative","type":"text","marks":[{"type":"code_inline"}]},{"text":". Adding ","type":"text"},{"text":"parent-initiative","type":"text","marks":[{"type":"code_inline"}]},{"text":" links is optional but recommended. To run the guided migration, ask: \"run the initiative migration.\"","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":"references/initiative-migration.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/initiative-migration.md","title":null}}]},{"text":" for the full 6-step guided migration workflow.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"readme","type":"text"}]},{"type":"paragraph","content":[{"text":"Report: ","type":"text"},{"text":"README.md missing — swain alignment loop has no public intent anchor. Run swain-init to seed one.","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"artifact_indexes","type":"text"}]},{"type":"paragraph","content":[{"text":"The script auto-repairs stale indexes via ","type":"text"},{"text":"rebuild-index.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":". If the rebuild script is unavailable or a rebuild fails, check that ","type":"text"},{"text":".agents/bin/rebuild-index.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" exists and is executable. Re-run ","type":"text"},{"text":"swain-doctor","type":"text","marks":[{"type":"code_inline"}]},{"text":" after fixing the symlink.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"evidence_pools","type":"text"}]},{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"docs/evidence-pools/","type":"text","marks":[{"type":"code_inline"}]},{"text":" exists, run the trove migration:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"bash \"$REPO_ROOT/.agents/bin/migrate-to-troves.sh\" --dry-run # preview\nbash \"$REPO_ROOT/.agents/bin/migrate-to-troves.sh\" # migrate","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"worktrees","type":"text"}]},{"type":"paragraph","content":[{"text":"Stale worktrees (branch already merged into HEAD) can be pruned: ","type":"text"},{"text":"git worktree remove \u003cpath>","type":"text","marks":[{"type":"code_inline"}]},{"text":". Orphaned worktrees (directory missing) can be pruned: ","type":"text"},{"text":"git worktree prune","type":"text","marks":[{"type":"code_inline"}]},{"text":". Stale lockfiles and unclaimed worktrees are reported in the detail field. Read ","type":"text"},{"text":"references/worktree-detection.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/worktree-detection.md","title":null}}]},{"text":" for classification rules.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"worktree_context","type":"text"}]},{"type":"paragraph","content":[{"text":"Validates the current session's worktree, not all linked worktrees (that's ","type":"text"},{"text":"worktrees","type":"text","marks":[{"type":"code_inline"}]},{"text":"). All four sub-checks ","type":"text"},{"text":"auto-fix","type":"text","marks":[{"type":"strong"}]},{"text":" deterministically — warnings mean auto-fix failed, advisory means auto-fix succeeded.","type":"text"}]},{"type":"paragraph","content":[{"text":"Location outside .worktrees/","type":"text","marks":[{"type":"strong"}]},{"text":" (auto-move): ADR-034 mandates ","type":"text"},{"text":".worktrees/","type":"text","marks":[{"type":"code_inline"}]},{"text":" as the canonical location. The script auto-moves the worktree via ","type":"text"},{"text":"git worktree move \u003cpath> \u003cmain_root>/.worktrees/\u003cbranch>","type":"text","marks":[{"type":"code_inline"}]},{"text":". Failure (warning) means the target path already exists or ","type":"text"},{"text":"git worktree move","type":"text","marks":[{"type":"code_inline"}]},{"text":" failed — resolve manually.","type":"text"}]},{"type":"paragraph","content":[{"text":"Missing lockfile","type":"text","marks":[{"type":"strong"}]},{"text":" (auto-create): The script auto-creates a lockfile at ","type":"text"},{"text":".agents/worktrees/\u003cbranch>.lock","type":"text","marks":[{"type":"code_inline"}]},{"text":" using ","type":"text"},{"text":"swain-lockfile.sh claim","type":"text","marks":[{"type":"code_inline"}]},{"text":", or falls back to writing the lockfile directly. On collision (existing lockfile for same branch), a PID-suffixed lockfile is created. Advisory = auto-created; warning = creation failed.","type":"text"}]},{"type":"paragraph","content":[{"text":"Branch name violates ADR-025","type":"text","marks":[{"type":"strong"}]},{"text":" (auto-rename): The script auto-renames the branch and moves the worktree folder to match ADR-025 naming. It uses ","type":"text"},{"text":"swain-worktree-name.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" when a purpose is available, or falls back to ","type":"text"},{"text":"session-\u003ctimestamp>","type":"text","marks":[{"type":"code_inline"}]},{"text":". The lockfile is also renamed. Advisory = renamed; warning = rename failed.","type":"text"}]},{"type":"paragraph","content":[{"text":"Folder name != branch name","type":"text","marks":[{"type":"strong"}]},{"text":" (auto-move): The script auto-moves the worktree folder so ","type":"text"},{"text":"basename","type":"text","marks":[{"type":"code_inline"}]},{"text":" matches the branch name via ","type":"text"},{"text":"git worktree move","type":"text","marks":[{"type":"code_inline"}]},{"text":". Advisory = moved; warning = move failed (target already exists).","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"lifecycle_dirs","type":"text"}]},{"type":"paragraph","content":[{"text":"Old phase directories from before ADR-003's three-track normalization. Read ","type":"text"},{"text":"references/lifecycle-migration.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/lifecycle-migration.md","title":null}}]},{"text":" for detection commands, remediation steps, and the migration script.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"tk_health","type":"text"}]},{"type":"paragraph","content":[{"text":"If vendored tk is not found or not executable, try: ","type":"text"},{"text":"/swain update","type":"text","marks":[{"type":"code_inline"}]},{"text":" to reinstall skills. The expected path is ","type":"text"},{"text":"\u003cskills-root>/swain-do/bin/tk","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"operator_bin_symlinks","type":"text"}]},{"type":"paragraph","content":[{"text":"The script auto-repairs missing or stale ","type":"text"},{"text":"bin/","type":"text","marks":[{"type":"code_inline"}]},{"text":" symlinks. Conflicts (real file exists at ","type":"text"},{"text":"bin/\u003cname>","type":"text","marks":[{"type":"code_inline"}]},{"text":") require manual resolution — rename or remove the conflicting file, then re-run doctor.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"commit_signing","type":"text"}]},{"type":"paragraph","content":[{"text":"The script auto-enables commit signing when a signing key is detected at ","type":"text"},{"text":"~/.ssh/swain_signing","type":"text","marks":[{"type":"code_inline"}]},{"text":". If no key exists, run ","type":"text"},{"text":"/swain-keys","type":"text","marks":[{"type":"code_inline"}]},{"text":" to provision one.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"ssh_readiness","type":"text"}]},{"type":"paragraph","content":[{"text":"Runs ","type":"text"},{"text":"scripts/ssh-readiness.sh --check","type":"text","marks":[{"type":"code_inline"}]},{"text":". Issues are reported in the detail field. Common fixes: verify ","type":"text"},{"text":"~/.ssh/config.d/","type":"text","marks":[{"type":"code_inline"}]},{"text":" includes the project-specific SSH alias, check key permissions (","type":"text"},{"text":"chmod 600","type":"text","marks":[{"type":"code_inline"}]},{"text":"), ensure the key is added to the remote host.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"crash_debris","type":"text"}]},{"type":"paragraph","content":[{"text":"The script auto-removes stale ","type":"text"},{"text":".git/index.lock","type":"text","marks":[{"type":"code_inline"}]},{"text":" files. Other crash debris (orphaned temp files, partial merges) is reported for manual cleanup. Review the detail field for specific file paths and remove them if safe.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"agents_bin_symlinks","type":"text"}]},{"type":"paragraph","content":[{"text":"The script auto-repairs missing and stale ","type":"text"},{"text":".agents/bin/","type":"text","marks":[{"type":"code_inline"}]},{"text":" symlinks. Broken symlinks are removed. Conflicts (real files) are reported for manual resolution.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"flat_artifacts","type":"text"}]},{"type":"paragraph","content":[{"text":"Flat-file artifacts sit directly in phase directories instead of their own folders. Run with ","type":"text"},{"text":"--fix-flat-artifacts","type":"text","marks":[{"type":"code_inline"}]},{"text":" to auto-migrate: ","type":"text"},{"text":"bash \"$REPO_ROOT/.agents/bin/swain-doctor.sh\" --fix-flat-artifacts","type":"text","marks":[{"type":"code_inline"}]},{"text":". Each artifact gets a folder named ","type":"text"},{"text":"(\u003cID>)-\u003cTitle>/","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"swain_symlink","type":"text"}]},{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"bin/swain","type":"text","marks":[{"type":"code_inline"}]},{"text":" symlink is missing but the script exists at ","type":"text"},{"text":"\u003cskills-root>/swain/scripts/swain","type":"text","marks":[{"type":"code_inline"}]},{"text":", create it:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"ln -sf \"$(python3 -c \"import os,sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))\" \"$SKILLS_ROOT/swain/scripts/swain\" \"$REPO_ROOT/bin\")\" \"$REPO_ROOT/bin/swain\"","type":"text"}]},{"type":"paragraph","content":[{"text":"If the symlink is broken (target missing), the swain skill may need reinstalling: ","type":"text"},{"text":"/swain update","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"branch_model","type":"text"}]},{"type":"paragraph","content":[{"text":"Advisory — swain recommends a trunk+release branch model (ADR-013). ","type":"text"},{"text":"trunk","type":"text","marks":[{"type":"code_inline"}]},{"text":" is the development branch; ","type":"text"},{"text":"release","type":"text","marks":[{"type":"code_inline"}]},{"text":" is the distribution branch updated via squash-merge at release time. To adopt it, run ","type":"text"},{"text":".agents/bin/migrate-to-trunk-release.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or ","type":"text"},{"text":"--dry-run","type":"text","marks":[{"type":"code_inline"}]},{"text":" to preview). This is optional — swain works with any branch model, but sync and release features assume trunk+release when configured.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"platform_dotfolders","type":"text"}]},{"type":"paragraph","content":[{"text":"Remove dotfolder stubs for uninstalled agent platforms. Read ","type":"text"},{"text":"references/platform-cleanup.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/platform-cleanup.md","title":null}}]},{"text":" for the detection and cleanup procedure. Requires ","type":"text"},{"text":"jq","type":"text","marks":[{"type":"code_inline"}]},{"text":". The script reports which dotfolders are orphaned — verify they contain only installer symlinks before removing.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"skill_gitignore","type":"text"}]},{"type":"paragraph","content":[{"text":"Vendored swain skill folders should be gitignored in consumer projects. Read ","type":"text"},{"text":"references/gitignore-skill-folders.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/gitignore-skill-folders.md","title":null}}]},{"text":" for the remediation procedure. Append these entries to ","type":"text"},{"text":".gitignore","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":".claude/skills/swain/\n.claude/skills/swain-*/\n.agents/skills/swain/\n.agents/skills/swain-*/","type":"text"}]},{"type":"paragraph","content":[{"text":"Skipped automatically when running in the swain source repo.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Summary report","type":"text"}]},{"type":"paragraph","content":[{"text":"After all checks complete, output a concise summary table:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"swain-doctor summary:\n Governance ......... ok\n Legacy cleanup ..... ok (nothing to clean)\n Platform dotfolders ok (nothing to clean)\n .agents directory .. ok\n .tickets/ .......... ok\n Stale .beads/ ...... ok (not present)\n Tools .............. ok (1 optional missing: fswatch)\n Settings ........... ok\n Script perms ....... ok\n Memory directory ... ok\n Superpowers ........ ok (6/6 skills detected)\n Epics w/o initiative advisory (3 epics — see note below)\n README ............. ok\n Artifact indexes ... ok\n Evidence pools ..... ok\n Worktrees .......... ok\n Worktree context ... ok\n Lifecycle dirs ..... ok\n tk health .......... ok\n Operator bin/ ...... ok\n Commit signing ..... ok\n SSH readiness ...... ok\n Crash debris ....... ok\n .agents/bin/ ....... ok\n Flat artifacts ..... ok\n swain symlink ...... ok\n Branch model ....... ok\n Skill gitignore .... ok\n\n3 checks performed repairs. 0 issues remain.","type":"text"}]},{"type":"paragraph","content":[{"text":"Use these status values:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ok","type":"text","marks":[{"type":"strong"}]},{"text":" — nothing to do.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"advisory","type":"text","marks":[{"type":"strong"}]},{"text":" — auto-repaired or informational (give specifics).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"warning","type":"text","marks":[{"type":"strong"}]},{"text":" — issue found, user action recommended (give specifics).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"skipped","type":"text","marks":[{"type":"strong"}]},{"text":" — check could not run (e.g., jq missing for JSON validation).","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"If any checks have warnings, list them below the table with remediation steps from the sections above.","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"swain-doctor","author":"@skillopedia","source":{"stars":2,"repo_name":"swain","origin_url":"https://github.com/cristoslc/swain/blob/HEAD/skills/swain-doctor/SKILL.md","repo_owner":"cristoslc","body_sha256":"56250c8827f43c81f3e66eec1ca7b7e250b6f728e900e5d05bf243fde9bdec7b","cluster_key":"214cf0fdb506eaa3d91cbda0c3830a6ceeb070dc1ac280e871bd40b154666532","clean_bundle":{"format":"clean-skill-bundle-v1","source":"cristoslc/swain/skills/swain-doctor/SKILL.md","attachments":[{"id":"320432ce-dbe2-54cf-b058-8a5d243440e3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/320432ce-dbe2-54cf-b058-8a5d243440e3/attachment.md","path":"references/AGENTS.content.md","size":6525,"sha256":"dee0a6a0ed9dcd3ee9598d53bbef91bb707c7621644bed71117a63adb641ea6a","contentType":"text/markdown; charset=utf-8"},{"id":"143bc57f-e563-5efe-872e-e276f3d1cf60","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/143bc57f-e563-5efe-872e-e276f3d1cf60/attachment.md","path":"references/beads-migration.md","size":1416,"sha256":"58eead1c4d5059246a3a8fb9b1f56decab95114c3afc3161f618b22c4ad697b0","contentType":"text/markdown; charset=utf-8"},{"id":"a4bb0f5e-a7ba-5248-bdaf-a25bd40eaefe","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a4bb0f5e-a7ba-5248-bdaf-a25bd40eaefe/attachment.md","path":"references/gitignore-skill-folders.md","size":2265,"sha256":"de2b5ace167c4aed7a3c2ed41487b38c7bbba405ab514a3865a1c4077488d90c","contentType":"text/markdown; charset=utf-8"},{"id":"7cd9021d-517b-5dc2-807a-922ae3b0f207","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/7cd9021d-517b-5dc2-807a-922ae3b0f207/attachment.md","path":"references/governance-injection.md","size":2474,"sha256":"d40eab0562792411ea7a3b9efddde7ccb951ef9cb250bf682add3bb889cae5f2","contentType":"text/markdown; charset=utf-8"},{"id":"5e72c372-3aff-5fa1-8fff-0102a643b300","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5e72c372-3aff-5fa1-8fff-0102a643b300/attachment.md","path":"references/initiative-migration.md","size":3232,"sha256":"341437ffc27392e763f41c066d4d78cbb9ab0bc055aea2d47b93fe81fe939fc0","contentType":"text/markdown; charset=utf-8"},{"id":"b0de1b06-f53e-5a52-a8aa-088fce054e50","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b0de1b06-f53e-5a52-a8aa-088fce054e50/attachment.md","path":"references/legacy-cleanup.md","size":2723,"sha256":"7454e714d11d8c11ae496a020013ddab17b0d5452b57d5f66f92e8327dd0341b","contentType":"text/markdown; charset=utf-8"},{"id":"90c853e7-32b0-5062-945f-e125793f0566","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/90c853e7-32b0-5062-945f-e125793f0566/attachment.sh","path":"references/legacy-skills-lib.sh","size":1269,"sha256":"746a63ab210b5fb8f95bf05e58f5a7a3508be9a6e121058b197c037f9d2e7503","contentType":"application/x-sh; charset=utf-8"},{"id":"9459b73a-b972-5f04-8d18-c5d2ac799738","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9459b73a-b972-5f04-8d18-c5d2ac799738/attachment.json","path":"references/legacy-skills.json","size":920,"sha256":"ba051ad9fca4ef494c68b91beae12c1769a35fb578ee1b0d07dd10711d9be554","contentType":"application/json; charset=utf-8"},{"id":"d732329c-ac07-5575-8c97-28a8b6bacd9e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d732329c-ac07-5575-8c97-28a8b6bacd9e/attachment.md","path":"references/lifecycle-migration.md","size":1657,"sha256":"5a27a487ae59435c848f2c8e0d1b8072322dbd14f23ec22f4157b1f854b7421a","contentType":"text/markdown; charset=utf-8"},{"id":"87ce17eb-e8c9-59f0-9a30-0a4ddfde2320","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/87ce17eb-e8c9-59f0-9a30-0a4ddfde2320/attachment.md","path":"references/platform-cleanup.md","size":2457,"sha256":"243f3a002804adc2f9b858846448fe52a8cc3e8db1b6752580ef11c480890cbb","contentType":"text/markdown; charset=utf-8"},{"id":"f43f36c4-a6e2-5168-ad85-9427af752be8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f43f36c4-a6e2-5168-ad85-9427af752be8/attachment.json","path":"references/platform-dotfolders.json","size":3876,"sha256":"3eefa28d99452624042592375d63f0956b9c0755ad4feb795a998bf2458b5705","contentType":"application/json; charset=utf-8"},{"id":"e89e8f22-50bf-5cab-90bb-28a982f412e8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e89e8f22-50bf-5cab-90bb-28a982f412e8/attachment.md","path":"references/runtime-checks.md","size":5878,"sha256":"549834dc5f3e514ecde5918885be5b0e25952276435c987308291308df0c7614","contentType":"text/markdown; charset=utf-8"},{"id":"342a7083-184f-5ea3-bb72-e0ebb6cc09af","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/342a7083-184f-5ea3-bb72-e0ebb6cc09af/attachment.md","path":"references/tickets-validation.md","size":1255,"sha256":"27139db1e595bcc39529eeec764de9db873deb7670e9bf3b2d8a80a4cef30aaa","contentType":"text/markdown; charset=utf-8"},{"id":"b7fa494b-6793-59d9-8055-6b115b677436","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b7fa494b-6793-59d9-8055-6b115b677436/attachment.md","path":"references/tool-availability.md","size":3684,"sha256":"c5a48b8c6b4fce50c882b1c2c6238206c2af3bb2e462ad9b795e304b4dda41a1","contentType":"text/markdown; charset=utf-8"},{"id":"dc5d82fd-6df7-5ea7-b695-dc5bfa1258c7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/dc5d82fd-6df7-5ea7-b695-dc5bfa1258c7/attachment.md","path":"references/worktree-detection.md","size":1569,"sha256":"7e64200e6032c2aa8c68819c2d9e517268665e864076db42dbba3f24802035a9","contentType":"text/markdown; charset=utf-8"},{"id":"b565c101-a8cc-5231-8f20-84d1bdbc4900","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b565c101-a8cc-5231-8f20-84d1bdbc4900/attachment.sh","path":"scripts/check-skill-changes.sh","size":3670,"sha256":"9e51742d14bbea4531ed0bd432f623dbf1fab07ce32d9b7d892ce34e8fa6b074","contentType":"application/x-sh; charset=utf-8"},{"id":"b8305774-c4ce-5f47-a79d-71fe9aeca6e8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b8305774-c4ce-5f47-a79d-71fe9aeca6e8/attachment.sh","path":"scripts/crash-debris-lib.sh","size":7145,"sha256":"06d9268df0964e76614f802895759140cdf37735bd7e5c574ca5b00871cc4690","contentType":"application/x-sh; charset=utf-8"},{"id":"b6b40d19-f1a8-58bb-b333-606578cd8392","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b6b40d19-f1a8-58bb-b333-606578cd8392/attachment.sh","path":"scripts/migrate-to-trunk-release.sh","size":5926,"sha256":"3815f7fa789225c85fe936da74ca60653ce22ae9220f13a88a505351c7d2aa53","contentType":"application/x-sh; charset=utf-8"},{"id":"07b7b6d5-5fb1-595e-9a53-2f349976b72c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/07b7b6d5-5fb1-595e-9a53-2f349976b72c/attachment.sh","path":"scripts/ssh-readiness.sh","size":4423,"sha256":"05cbeac12fe8570f40d927248822ffe4af1cf40eff8bd0f6e756120c70497d64","contentType":"application/x-sh; charset=utf-8"},{"id":"98d7e90c-5ad5-532d-b063-3a0a671e763c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/98d7e90c-5ad5-532d-b063-3a0a671e763c/attachment.sh","path":"scripts/swain-doctor.sh","size":52118,"sha256":"788b03a2511008248988215558382023396955b2c31d3e1fabea309f2ee84bf8","contentType":"application/x-sh; charset=utf-8"},{"id":"6ad0d3e0-9fed-5ab4-8e87-acd2ef3113f1","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/6ad0d3e0-9fed-5ab4-8e87-acd2ef3113f1/attachment.sh","path":"scripts/swain-initiative-scan.sh","size":1932,"sha256":"60487f759d6249fd38a2adf4480f1bef8a31fe72aa5d0875b8d75b67d3afe931","contentType":"application/x-sh; charset=utf-8"},{"id":"ec874825-3a17-5c07-af1b-36a75713d801","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ec874825-3a17-5c07-af1b-36a75713d801/attachment.sh","path":"scripts/swain-preflight.sh","size":14056,"sha256":"e3fd7e5505252e44970ae8d7b2ff7d5e5f895b5a2470344a884294535163c6a5","contentType":"application/x-sh; charset=utf-8"},{"id":"75582ffa-e579-5144-933e-ddc5866bf3eb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/75582ffa-e579-5144-933e-ddc5866bf3eb/attachment.sh","path":"tests/test-check-skill-changes.sh","size":5344,"sha256":"2c170690b6857a267585e9e3578fe198f952800908a0c2a387b72b1c85e6833d","contentType":"application/x-sh; charset=utf-8"},{"id":"5836f6f7-69bf-5432-a822-e852669d0cb9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5836f6f7-69bf-5432-a822-e852669d0cb9/attachment.sh","path":"tests/test-gitignore-skill-folders.sh","size":5298,"sha256":"0ae14eafc41a6a546e411a5cc6dd663231b44404c99d341d1d4b1abe686f2f59","contentType":"application/x-sh; charset=utf-8"},{"id":"d0d584f8-43d3-5121-8306-093f442944aa","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d0d584f8-43d3-5121-8306-093f442944aa/attachment.sh","path":"tests/test-legacy-skill-cleanup.sh","size":2965,"sha256":"850bda4bd377cc410a3f53f2b502f47766349565cbd681f40372b801473bfe9a","contentType":"application/x-sh; charset=utf-8"},{"id":"d2019222-6fe7-5297-81bd-54fcdfc63d84","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d2019222-6fe7-5297-81bd-54fcdfc63d84/attachment.sh","path":"tests/test-operator-bin-symlinks.sh","size":5586,"sha256":"e9a84c81a4acbd606c5ed5c6137537ae6724ad6a0ba5372e26df316a6d0f5444","contentType":"application/x-sh; charset=utf-8"},{"id":"d00bf51c-a031-5fa7-b740-78f2eda98e62","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/d00bf51c-a031-5fa7-b740-78f2eda98e62/attachment.sh","path":"tests/test-preflight-legacy-skills.sh","size":2221,"sha256":"6cdbaa90485f2927a1ec0f0f87575d47a84f168c7da26951e97cfe87d8c49136","contentType":"application/x-sh; charset=utf-8"},{"id":"56dff3fd-76d5-5103-817e-bd0b718cf35e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/56dff3fd-76d5-5103-817e-bd0b718cf35e/attachment.sh","path":"tests/test-preflight-skill-changes.sh","size":1441,"sha256":"cbf4a8661de3c620fd82469a6a9ee2bfaad5799652daf77ef2baac98318a93f3","contentType":"application/x-sh; charset=utf-8"},{"id":"5767afd4-e862-5131-a6c4-3bce361531da","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5767afd4-e862-5131-a6c4-3bce361531da/attachment.sh","path":"tests/test-ssh-readiness.sh","size":2625,"sha256":"f75ba6b936f67e34ffd1c0a6a8123ecca31230bc523f4a35442a18c4a414c9be","contentType":"application/x-sh; charset=utf-8"},{"id":"ff1bc4a2-4fd5-509a-bb91-45f0b632e21e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ff1bc4a2-4fd5-509a-bb91-45f0b632e21e/attachment.sh","path":"tests/test-swain-doctor-sh.sh","size":23620,"sha256":"0dbf8e77b399d5c0f1058c8dcf007212254282e4b14a9d16980820a8ab06c031","contentType":"application/x-sh; charset=utf-8"}],"bundle_sha256":"f709012b5513d2a85af76fcef62124319cd667b40c0797d4edbdee37b0d39454","attachment_count":30,"text_attachments":30,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/swain-doctor/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"software-engineering","category_label":"Engineering"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"software-engineering","metadata":{"author":"cristos","source":"swain","version":"3.0.0","short-description":"Session-start health checks and repair"},"import_tag":"clean-skills-v1","description":"Auto-invoked at session start when swain-preflight detects issues. Also user-invocable for on-demand health checks. Validates project health: governance rules, tool availability, memory directory, settings files, script permissions, .agents directory, and .tickets/ validation. Auto-migrates stale .beads/ directories to .tickets/ and removes them. Remediates issues across all swain skills. Idempotent — safe to run any time.","allowed-tools":"Bash, Read, Write, Edit, Grep, Glob","user-invocable":true}},"renderedAt":1782981805800}

<!-- swain-model-hint: sonnet, effort: low -- Doctor Session-start health checks for swain projects. The consolidated script is authoritative for all detection. This skill file defines how to run the script and how to remediate each check. Running the script The script outputs structured JSON with . Each check has , , , and optional . Parse it and present the summary table to the operator. Then use the remediation sections below only for checks that reported or status — do not re-run detection. To auto-fix flat-file artifacts: If the script is unavailable (e.g., symlinks not yet bootstrapped)…