Claude Skill

process-compose-ops

Process Compose orchestration for non-containerized local services: process-compose.yaml schema, health checks, restart policies, dependencies, TUI/REST/MCP control, scheduling, and boot persistence. Use as a PM2/supervisord/Foreman replacement for local dev service management.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_process-compose-ops-3dfaf0b.zip · 29 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/process-compose-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Process Compose Operations

Process Compose is a Go-based supervisor for non-containerized services. Single binary, YAML config, built-in TUI, REST API, MCP server, and proper Windows support. Replacement for PM2/supervisord/Foreman in the local-dev role.

Why not PM2: PM2 5.x has 15+ known CVEs (axios/lodash/tar/minimist transitive npm exposure). PC compiles all deps in at build time with go.sum hashes — structurally resistant to TanStack-style npm worm attacks.

Why not Docker Compose: Container overhead is unnecessary for local Python/Node/Go dev servers running directly. PC gives you health checks, dependencies, and restart policies without the container layer.

Install (verified)

# Pin a specific version, verify SHA-256 against upstream checksums
VER="v1.110.0"
BASE="https://github.com/F1bonacc1/process-compose/releases/download/$VER"

curl -fsSL -o pc.zip "$BASE/process-compose_windows_amd64.zip"
curl -fsSL -o checksums.txt "$BASE/process-compose_checksums.txt"

EXPECTED=$(grep "process-compose_windows_amd64.zip" checksums.txt | awk '{print $1}')
ACTUAL=$(sha256sum pc.zip | awk '{print $1}')
[ "$EXPECTED" = "$ACTUAL" ] || { echo "HASH MISMATCH"; exit 1; }

unzip pc.zip
# Commit process-compose.exe to your repo's bin/ directory

Record the binary's hash in your repo's SUPPLY-CHAIN.md for re-verification on next upgrade.

process-compose.yaml Quick Reference

version: "0.5"

log_level: info
log_length: 1000

processes:

  my-service:
    command: "pythonw -m uvicorn main:app --host 127.0.0.1 --port 8000"
    working_dir: "X:/path/to/repo"
    environment:
      - "DJANGO_SETTINGS_MODULE=myapp.settings"
      - "PYTHONUNBUFFERED=1"
    readiness_probe:
      http_get:
        host: localhost
        port: 8000
        path: /
      initial_delay_seconds: 5
      period_seconds: 10
      timeout_seconds: 3
      failure_threshold: 3
    availability:
      restart: always           # always | exit_on_failure | on_failure | no
      backoff_seconds: 5
      max_restarts: 20
    depends_on:
      database:
        condition: process_healthy   # process_started | process_healthy | process_completed
    shutdown:
      signal: 15                # SIGTERM
      timeout_seconds: 30
    log_location: "logs/my-service.log"

  scheduled-job:
    command: "python backup.py"
    schedule: "0 2 * * *"       # 2am daily cron
    availability:
      restart: exit_on_failure

Restart Policies

Policy Restarts on...
always Any exit (success or failure) — best for long-running daemons
on_failure Non-zero exit codes only
exit_on_failure Stops PC entirely if this process fails — use for critical deps
no Never restart

Dependency Conditions

Condition Wait until...
process_started Dependency spawned (PID exists). Fastest, weakest guarantee.
process_healthy Dependency's readiness_probe passes. Strong guarantee.
process_completed Dependency exited successfully (for init/setup processes).

CLI Reference

# Lifecycle
process-compose up -f config.yaml          # Start (foreground TUI by default)
process-compose up -f config.yaml -t=false # Headless (no TUI)
process-compose up -f config.yaml --dry-run  # Validate config without starting
process-compose down                       # Stop all processes + project

# Inspection (against running PC)  (example values — substitute your own port/dir)
process-compose -p <your-pc-port> process list       # all processes + status
process-compose -p <your-pc-port> process logs <name> --follow
process-compose -p <your-pc-port> attach             # TUI for running project

# Process control
process-compose -p <your-pc-port> process restart <name>
process-compose -p <your-pc-port> process stop <name>
process-compose -p <your-pc-port> process start <name>

# Reload config without stopping (hot update)
process-compose -p <your-pc-port> project update -f config.yaml

# Standalone inspection (no running PC)
process-compose info                       # config home info
process-compose graph -f config.yaml       # dependency graph
process-compose analyze -f config.yaml     # startup timing analysis

Key flag gotcha: there's no --detached flag. To run in background:

  • Linux/Mac: process-compose up -t=false & (shell backgrounding)
  • Windows: launch via Task Scheduler or Start-Process with -WindowStyle Hidden

TUI Navigation

Launch: process-compose attach (or up without -t=false).

Key Action
↑ ↓ or j k Navigate process list
Tab Switch focus between process list and log pane
F4 Maximize current pane (toggle)
F5 Unfollow logs (lets you scroll history)
F6 Unwrap log lines
r Restart selected process
s Stop selected process
t Start selected process
/ Filter process list
? Help overlay
q Quit TUI (PC keeps running in background)

MCP Server Integration

PC ships a built-in MCP server exposing processes as tools for AI agents. Enable via the config or CLI flag. With the MCP server on, a Claude Code agent can directly:

  • List running processes
  • Get process status/health
  • Restart/stop/start processes
  • Read process logs

This replaces shell-based glue scripts (the old PM2-broker pattern).

API Port Selection

Default API port is 8080. Common collisions:

Port 8080 user Workaround
Dagu dashboard Use -p <your-pc-port> until Dagu decommissioned
Tomcat / Spring Boot dev Use -p <your-pc-port>
Other dev tool defaults Pick anything free in 8000–9999 range

If you change the API port, every subsequent CLI call needs -p <port>:

process-compose -p <your-pc-port> process list
process-compose -p <your-pc-port> process logs axiom --follow

Windows Boot Persistence Pattern

Task Scheduler runs with minimal PATH. Use a wrapper script that sets PATH explicitly before launching PC.

# scripts/boot-start.ps1
$root = "<your-process-compose-dir>"
$pcExe = "$root\bin\process-compose.exe"

# Explicit PATH for managed services (Python, uv, Git tools, cloudflared, etc.)
$env:PATH = (@(
    "$root\bin"
    "C:\Program Files\Git\usr\bin"          # openssl, bash
    "C:\Users\<user>\AppData\Local\Programs\Python\Python313\Scripts"
    "$env:PATH"
) -join ';')

# Optional: source secrets from gitignored .env
$envFile = "$root\.env"
if (Test-Path $envFile) {
    Get-Content $envFile | ForEach-Object {
        if ($_ -match '^\s*([A-Z_]+)\s*=\s*(.+?)\s*$') {
            [Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process')
        }
    }
}

# Launch headless
& $pcExe -p <your-pc-port> -t=false -L "$root\logs\process-compose.log" up -f "$root\process-compose.yaml"

Register as a Task Scheduler entry with LogonType S4U (runs at boot, no password, no interactive logon needed):

$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType S4U -RunLevel Highest
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$root\scripts\boot-start.ps1`""
$trigger = New-ScheduledTaskTrigger -AtStartup
Register-ScheduledTask -TaskName "ProcessCompose-Boot" `
    -Action $action -Trigger $trigger -Principal $principal -Force

YAML Gotchas

Gotcha Symptom Fix
Windows PATH with backslashes in double-quoted YAML yaml: found unknown escape character Use single quotes: - 'PATH=C:\Program Files\Git\usr\bin;...'
command with quoted paths containing spaces First arg eaten Wrap whole command in single quotes, inner paths in double: '"C:/Program Files/foo.exe" arg1 arg2'
Forgot working_dir Process starts in PC's cwd, can't find files Always specify absolute working_dir
Health probe wrong port Process restart-loops with Not Ready Match readiness_probe.http_get.port to where the process actually binds
Secrets in YAML Committed to git Use environment to pass-through; set in shell env or gitignored .env

Common Operations

# Validate config before applying
process-compose up --dry-run -f process-compose.yaml

# Hot-reload after editing config
process-compose -p <your-pc-port> project update -f process-compose.yaml

# Restart one service after code change
process-compose -p <your-pc-port> process restart axiom

# Watch logs of a misbehaving service
process-compose -p <your-pc-port> process logs axiom --follow

# Stop one service temporarily for debugging
process-compose -p <your-pc-port> process stop axiom
# Now run it manually with your debugger, then:
process-compose -p <your-pc-port> process start axiom

When to Use Process Compose vs Alternatives

Need Tool
Local non-containerized services with health/dependencies/MCP Process Compose
Production node.js process supervision PM2 (despite age)
Container-based stack Docker Compose
Job queue with cron + DAGs Dagu, Temporal, Airflow
System service supervision systemd (Linux), Windows Services
One-shot Procfile run Foreman / Overmind / Hivemind (Unix-only)

Worked Example

See <your-process-compose-dir>\ for an 11-process production stack:

  • process-compose.yaml — health-checked services with depends_on chains
  • scripts/boot-start.ps1 — PATH-aware boot wrapper
  • docs/MIGRATION-LOG.md — full migration from PM2 + Caddy, every gotcha documented
  • docs/SUPPLY-CHAIN.md — binary verification procedure

Anti-Patterns

BAD:  process-compose up --detached       # flag does not exist
GOOD: process-compose up -t=false &       # background via shell

BAD:  put secrets in process-compose.yaml (commits to git)
GOOD: source from gitignored .env in boot wrapper

BAD:  use API port 8080 (clashes with Dagu, Tomcat, others)
GOOD: -p <your-pc-port> (or any free port), document the choice

BAD:  ignore readiness_probe and just hope services come up
GOOD: configure http_get probe on a real endpoint; depends_on uses process_healthy

BAD:  upgrade PC by running an installer (npm install -g, scoop install, brew install)
GOOD: download specific version, verify SHA-256 against upstream checksums.txt, commit binary

Resources in this skill

references/

  • schema-reference.md — full process-compose.yaml schema with field semantics, defaults, and command-quoting gotchas
  • probe-patterns.md — readiness probe recipes by stack (Python, Go, Node, TCP-only, daemons)
  • dependency-patterns.md — depends_on patterns: companion daemons, DB-before-app, tunnel-after-service, one-shot init
  • tui-shortcuts.md — TUI cheatsheet (keys, status legend, search/sort/filter)
  • boot-persistence-windows.md — Task Scheduler setup with S4U logon, PATH-aware wrapper
  • supply-chain-verification.md — full SHA-256 verification procedure for the binary

scripts/

  • install-process-compose.ps1 — download + verify + extract a pinned version, writes VERIFICATION.md
  • verify-binary.ps1 — re-verify committed binary hash (monthly / pre-commit)
  • boot-start.template.ps1 — PATH-aware boot wrapper (copy + adapt per machine)
  • boot-task-install.template.ps1 — Task Scheduler entry registration (S4U logon)

assets/

  • python-uvicorn.yaml — uvicorn/FastAPI/Django basic service template
  • django-with-companions.yaml — Django + queue daemon + audit watcher chain
  • go-binary-service.yaml — Go binary with HTTP or TCP probe
  • tunnel-with-dependency.yaml — Cloudflare tunnel waiting on its target service
  • cron-job.yaml — scheduled task patterns

Related Skills

  • portless-ops — the routing layer we pair with PC (replaces Caddy)
  • docker-ops — container alternative for the same role
  • mcp-ops — PC's MCP server fits this ecosystem
  • cli-ops — general CLI tool patterns
Files (claude-mods)
  • assets
    • cron-job.yaml 968 B
      # process-compose.yaml — Scheduled (cron) job
      #
      # Pattern: Periodic batch task that PC runs on schedule, not as a daemon.
      
      version: "0.5"
      
      processes:
      
        daily-backup:
          command: "python scripts/backup.py --target s3://my-bucket"
          working_dir: "X:/path/to/scripts"
      
          environment:
            - "PYTHONUNBUFFERED=1"
            # AWS creds, etc., from boot-start .env loading
      
          availability:
            restart: exit_on_failure       # Stop the whole project if this fails
            schedule: "0 2 * * *"          # cron syntax: 02:00 every day
            # Or interval-based:
            # schedule: "@every 6h"
      
          log_location: "logs/daily-backup.log"
      
        hourly-cleanup:
          command: "python scripts/cleanup.py"
          working_dir: "X:/path/to/scripts"
          environment:
            - "PYTHONUNBUFFERED=1"
          availability:
            restart: no                    # One-shot per schedule run
            schedule: "0 * * * *"          # Every hour on the hour
          log_location: "logs/hourly-cleanup.log"
      
    • django-with-companions.yaml 2.6 KB
      # process-compose.yaml — Django web app + queue daemon + audit watcher
      #
      # Pattern: A main HTTP service with two long-running companion daemons
      # that depend on the main service being up.
      #
      # Notes:
      #   - depends_on ensures startup ordering, not runtime coupling.
      #     If the web app restarts, the daemons keep running.
      #   - The audit watcher uses Git Bash for a wrapper script that needs
      #     coreutils on PATH (single-quoted YAML to escape backslashes).
      #   - The daemon enforces an OAuth-only policy: ANTHROPIC_API_KEY must
      #     be unset (handled by the boot-start wrapper).
      
      version: "0.5"
      
      processes:
      
        webapp:
          command: "uv run python manage.py serve --host 127.0.0.1 --port 8000 --no-reload"
          working_dir: "X:/path/to/myapp"
          environment:
            - "DJANGO_SETTINGS_MODULE=myapp.settings.local"
            - "PYTHONUNBUFFERED=1"
          readiness_probe:
            http_get:
              host: localhost
              port: 8000
              path: /
            initial_delay_seconds: 30   # Django w/ migrations is slow to come up
            period_seconds: 15
            timeout_seconds: 5
            failure_threshold: 3
          availability:
            restart: always
            backoff_seconds: 5
            max_restarts: 20
          log_location: "logs/webapp.log"
      
        webapp-daemon:
          command: "uv run python -m myapp.worker.daemon-start"
          working_dir: "X:/path/to/myapp"
          environment:
            - "PYTHONUNBUFFERED=1"
            # NOTE: ANTHROPIC_API_KEY must be UNSET — daemon enforces OAuth-only.
            # The boot-start wrapper handles this; for manual runs, unset it first.
          depends_on:
            webapp:
              condition: process_started   # Just needs webapp's pid to exist;
                                            # daemon polls webapp itself for readiness
          availability:
            restart: always
            backoff_seconds: 5
            max_restarts: 20
          shutdown:
            signal: 15           # SIGTERM
            timeout_seconds: 35  # Allow daemon's 30s graceful shutdown_grace_s
          log_location: "logs/webapp-daemon.log"
      
        webapp-feedback:
          # Bash wrapper script — paths in single quotes to avoid YAML escape interpretation
          command: '"C:/Program Files/Git/usr/bin/bash.exe" --login X:/path/to/myapp/scripts/feedback-wrapper.sh'
          working_dir: "X:/path/to/myapp"
          environment:
            - "PYTHONUNBUFFERED=1"
            # Belt-and-braces PATH for the bash wrapper:
            - 'PATH=C:\Program Files\Git\usr\bin;C:\Program Files\Git\bin;C:\Users\me\AppData\Local\Programs\Python\Python313\Scripts;C:\Windows\System32'
          depends_on:
            webapp:
              condition: process_started
          availability:
            restart: always
            backoff_seconds: 15
            max_restarts: 20
          log_location: "logs/webapp-feedback.log"
      
    • go-binary-service.yaml 848 B
      # process-compose.yaml — Go binary service
      #
      # Pattern: Fast-startup Go service with TCP probe.
      # Use HTTP probe if it serves HTTP; tcp_socket is the fallback for
      # protocols that aren't HTTP.
      
      version: "0.5"
      
      processes:
      
        mysvc:
          command: "X:/path/to/mysvc/bin/mysvc.exe serve"
          working_dir: "X:/path/to/mysvc"
      
          readiness_probe:
            http_get:                  # Use http_get if the service is HTTP
              host: localhost
              port: 8080
              path: /
            # OR for non-HTTP protocols:
            # tcp_socket:
            #   host: localhost
            #   port: 5432
            initial_delay_seconds: 1   # Go services usually come up in < 1s
            period_seconds: 5
            timeout_seconds: 3
            failure_threshold: 3
      
          availability:
            restart: always
            backoff_seconds: 10
            max_restarts: 10
      
          log_location: "logs/mysvc.log"
      
    • python-uvicorn.yaml 1.1 KB
      # process-compose.yaml — Python uvicorn/FastAPI/Django service
      #
      # Pattern: Python web service with health endpoint, always-restart, dedicated log file.
      # Copy + adapt to your stack.
      
      version: "0.5"
      
      processes:
      
        myapp:
          # Wrap pythonw exe path in single quotes if it contains spaces; use forward
          # slashes inside or single-quote backslash-paths to avoid YAML escapes.
          command: "pythonw -m uvicorn myapp.main:app --host 127.0.0.1 --port 8000"
          working_dir: "X:/path/to/myapp"
      
          environment:
            - "PYTHONUNBUFFERED=1"
            - "DJANGO_SETTINGS_MODULE=myapp.settings.local"
            # Don't put secrets here — source from gitignored .env via boot-start wrapper
      
          readiness_probe:
            http_get:
              host: localhost
              port: 8000
              path: /          # bare root if no /health endpoint; redirects count as healthy
            initial_delay_seconds: 5
            period_seconds: 10
            timeout_seconds: 3
            failure_threshold: 3
      
          availability:
            restart: always
            backoff_seconds: 5
            max_restarts: 20
      
          log_location: "logs/myapp.log"
      
    • tunnel-with-dependency.yaml 1.5 KB
      # process-compose.yaml — Cloudflare tunnel exposing a local service
      #
      # Pattern: A web service + a Cloudflare tunnel that forwards external
      # traffic to it. The tunnel must depend on the service being HEALTHY,
      # not just started, to avoid Cloudflare seeing repeated connection
      # refused errors during service warmup.
      
      version: "0.5"
      
      processes:
      
        internal-svc:
          command: "myservice serve --port 8000"
          working_dir: "X:/path/to/myservice"
      
          readiness_probe:
            http_get:
              host: localhost
              port: 8000
              path: /
            initial_delay_seconds: 5
            period_seconds: 15
            timeout_seconds: 3
            failure_threshold: 3
      
          availability:
            restart: always
            backoff_seconds: 3
            max_restarts: 20
      
          log_location: "logs/internal-svc.log"
      
        tunnel:
          # Single-quoted YAML string with embedded paths-with-spaces.
          # Tunnel UUID + cert paths should come from a gitignored .env.
          command: '"C:/Program Files (x86)/cloudflared/cloudflared.exe" tunnel --origincert C:/Users/me/.cloudflared/cert.pem --credentials-file C:/Users/me/.cloudflared/<TUNNEL_UUID>.json run --url http://localhost:8000 my-tunnel'
          working_dir: "C:/Users/me/.cloudflared"
      
          depends_on:
            internal-svc:
              condition: process_healthy   # Critical: don't open tunnel until service is ready
      
          availability:
            restart: always
            backoff_seconds: 5
            max_restarts: 50               # Tunnels can disconnect; allow many retries
      
          log_location: "logs/tunnel.log"
      
  • references
    • boot-persistence-windows.md 7 KB
      # Boot Persistence on Windows
      
      Process Compose has no built-in `service install` (unlike portless). On Windows, register a Task Scheduler entry.
      
      ## Key Constraints
      
      1. Task Scheduler runs with a **minimal PATH** — Python, uv, Git tools, custom binaries won't be found unless we set PATH explicitly
      2. Tasks running at boot-before-logon need **LogonType S4U** (no stored password, no interactive logon)
      3. Hidden window style avoids console flash on login
      
      ## Two-File Pattern
      
      Use a wrapper script that sets the environment, then have Task Scheduler launch the wrapper. Keeps task definition simple and lets you tweak env without re-registering.
      
      ### File 1 — `boot-start.ps1` (wrapper)
      
      ```powershell
      <#
      .SYNOPSIS
          Boot-time launcher for Process Compose. Sets PATH and launches headless.
      #>
      
      [CmdletBinding()]
      param()
      
      $ErrorActionPreference = 'Continue'
      
      $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
      $root      = (Resolve-Path (Join-Path $scriptDir '..')).Path
      $pcExe     = Join-Path $root 'bin\process-compose.exe'
      $pcYaml    = Join-Path $root 'process-compose.yaml'
      $logFile   = Join-Path $root 'logs\process-compose.log'
      $bootLog   = Join-Path $root 'logs\boot-start.log'
      
      New-Item -ItemType Directory -Force -Path (Join-Path $root 'logs') | Out-Null
      
      "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')] boot-start invoked. User: $env:USERNAME" | Out-File -FilePath $bootLog -Append
      
      # Build PATH explicitly. Tune for your machine.
      $pathParts = @(
          "$root\bin"                                                       # PC + any committed binaries
          "C:\Program Files\Git\usr\bin"                                    # openssl, bash, coreutils
          "C:\Program Files\Git\bin"                                        # git
          "C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python313"  # python, pythonw
          "C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python313\Scripts"  # uv, pip, etc.
          "C:\Program Files (x86)\cloudflared"                              # optional: cloudflared
          "C:\Windows\System32"
          "C:\Windows"
          $env:PATH
      )
      $env:PATH = ($pathParts -join ';')
      
      # Optional: load secrets from gitignored .env (e.g. API keys)
      $envFile = Join-Path $root '.env'
      if (Test-Path $envFile) {
          Get-Content $envFile | ForEach-Object {
              if ($_ -match '^\s*([A-Z_]+)\s*=\s*(.+?)\s*$') {
                  [Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process')
              }
          }
      }
      
      # Ensure incompatible env vars are unset (example: OAuth-only services that
      # refuse to start with stale API keys)
      # [Environment]::SetEnvironmentVariable('SOME_API_KEY', $null, 'Process')
      
      "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')] Starting process-compose..." | Out-File -FilePath $bootLog -Append
      
      # -p 8888    API port (pick something free, avoid 8080 if you have other tools there)
      # -t=false   no TUI (headless daemon mode)
      # -L         PC's own log file
      & $pcExe -p 8888 -t=false -L $logFile up -f $pcYaml
      
      "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')] process-compose exited code $LASTEXITCODE" | Out-File -FilePath $bootLog -Append
      ```
      
      ### File 2 — `boot-task-install.ps1` (registers the task)
      
      ```powershell
      [CmdletBinding()]
      param()
      
      $ErrorActionPreference = 'Stop'
      
      # Must be admin to create scheduled tasks
      $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
      $principal   = New-Object Security.Principal.WindowsPrincipal($currentUser)
      if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
          throw "Run as Administrator."
      }
      
      $scriptDir  = Split-Path -Parent $MyInvocation.MyCommand.Path
      $root       = (Resolve-Path (Join-Path $scriptDir '..')).Path
      $bootScript = Join-Path $scriptDir 'boot-start.ps1'
      
      $taskName = "ProcessCompose-MyStack"   # rename per project
      
      # Idempotent: remove existing if present
      Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue |
          Unregister-ScheduledTask -Confirm:$false
      
      $action = New-ScheduledTaskAction `
          -Execute "powershell.exe" `
          -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$bootScript`"" `
          -WorkingDirectory $root
      
      $trigger = New-ScheduledTaskTrigger -AtStartup
      
      $settings = New-ScheduledTaskSettingsSet `
          -ExecutionTimeLimit (New-TimeSpan -Seconds 0) `
          -AllowStartIfOnBatteries `
          -DontStopIfGoingOnBatteries `
          -RestartCount 3 `
          -RestartInterval (New-TimeSpan -Minutes 1)
      
      # S4U: run at boot as user without interactive logon or stored password
      $taskPrincipal = New-ScheduledTaskPrincipal `
          -UserId $env:USERNAME `
          -LogonType S4U `
          -RunLevel Highest
      
      Register-ScheduledTask -TaskName $taskName `
          -Action $action -Trigger $trigger -Settings $settings -Principal $taskPrincipal `
          -Description "Starts Process Compose at boot."
      ```
      
      ## LogonType Trade-offs
      
      | LogonType | Runs at boot before logon? | Needs password? | Capability |
      |---|---|---|---|
      | `Interactive` | No — waits for user logon | No | Full user context (UI, network shares) |
      | `S4U` | Yes | No | User context but no UI, no network shares |
      | `Password` | Yes | Yes (stored encrypted) | Full user context |
      | `ServiceAccount` | Yes (as Local System / Network Service) | No | Limited to service account perms — typically can't read user files |
      
      For Process Compose managing user-scoped dev services, **S4U** is usually the right choice: services run as the user (can read `C:\Users\<user>\...`) without requiring an interactive logon.
      
      ## Verify After Registration
      
      ```powershell
      # Check task exists
      Get-ScheduledTask -TaskName "ProcessCompose-MyStack" |
          Format-List TaskName, State, Triggers, Principal
      
      # Manually run the task to test before reboot
      Start-ScheduledTask -TaskName "ProcessCompose-MyStack"
      
      # Wait, then check PC is up
      Start-Sleep -Seconds 10
      process-compose -p 8888 process list
      ```
      
      ## Troubleshooting Boot Failures
      
      After a reboot, if services don't come up:
      
      1. **Check the boot log:** `<root>/logs/boot-start.log` — confirm the wrapper actually ran
      2. **Check PC's log:** `<root>/logs/process-compose.log` — confirm PC started and look for process-spawn errors
      3. **Check Task Scheduler history:** Right-click the task → History tab. Look for failure reasons.
      4. **Reproduce manually:** open elevated PS, run `.\scripts\boot-start.ps1` and watch what happens.
      
      Common failures:
      - PATH missing a tool → add to `pathParts` array
      - Working dir not absolute → ensure all paths in `process-compose.yaml` are absolute
      - Secrets not loaded → `.env` file not in expected location
      - Port collision (PC API port 8888 occupied) → check `netstat -ano | findstr :8888`
      
      ## Pair with portless service install
      
      portless has its own boot task. The two are independent — register both:
      
      ```powershell
      portless service install              # registers portless's task
      .\scripts\boot-task-install.ps1       # registers PC's task
      
      # Verify both
      Get-ScheduledTask | Where-Object {
          $_.TaskName -like "*ortless*" -or $_.TaskName -like "*ompose*"
      }
      ```
      
      ## Uninstall
      
      ```powershell
      # In the same script:
      Get-ScheduledTask -TaskName "ProcessCompose-MyStack" -ErrorAction SilentlyContinue |
          Unregister-ScheduledTask -Confirm:$false
      
      portless service uninstall
      ```
      
    • dependency-patterns.md 4.9 KB
      # Dependency Patterns (depends_on)
      
      How to express startup ordering and runtime dependencies between processes.
      
      ## The Four Conditions
      
      | Condition | Meaning | Best for |
      |---|---|---|
      | `process_started` | Dependency has spawned (PID exists, may not be ready) | Coarse ordering when readiness doesn't matter |
      | `process_healthy` | Dependency's `readiness_probe` passes | Runtime services that must be queryable |
      | `process_completed` | Dependency exited (any code) | One-shot tasks that may fail |
      | `process_completed_successfully` | Dependency exited with code 0 | One-shot init that must succeed |
      
      ## Pattern 1 — Web app + companion daemon
      
      A common pattern: a web service + a worker daemon that talks to the same DB or queue. Daemon should start AFTER the web app has its DB connection pool warm.
      
      ```yaml
      processes:
        webapp:
          command: "uv run python manage.py serve --port 8000"
          working_dir: "D:/code/MyApp"
          readiness_probe:
            http_get: { host: localhost, port: 8000, path: / }
            initial_delay_seconds: 10
          availability: { restart: always }
      
        worker:
          command: "uv run python -m myapp.worker"
          working_dir: "D:/code/MyApp"
          depends_on:
            webapp:
              condition: process_healthy
          availability: { restart: always }
      ```
      
      Result: `worker` doesn't start until `webapp`'s readiness probe passes. If `webapp` restarts, `worker` keeps running (depends_on is a startup ordering rule, not a runtime tether).
      
      ## Pattern 2 — Three-tier chain
      
      Web app + background daemon + audit watcher (Axiom pattern):
      
      ```yaml
      processes:
        app:
          command: "..."
          readiness_probe: { ... }
      
        app-daemon:
          command: "..."
          depends_on:
            app:
              condition: process_healthy
      
        app-feedback:
          command: "..."
          depends_on:
            app:
              condition: process_started   # weaker — just needs app's pid to exist
      ```
      
      ## Pattern 3 — Database before app
      
      Postgres in the same PC stack, app depends on it:
      
      ```yaml
      processes:
        postgres:
          command: "postgres -D /var/lib/pg"
          readiness_probe:
            tcp_socket: { host: localhost, port: 5432 }
            initial_delay_seconds: 3
      
        migrate:
          command: "alembic upgrade head"
          working_dir: "X:/MyApp"
          depends_on:
            postgres:
              condition: process_healthy
          availability:
            restart: exit_on_failure   # one-shot; if it fails, the whole stack fails
      
        app:
          command: "uvicorn main:app"
          working_dir: "X:/MyApp"
          depends_on:
            migrate:
              condition: process_completed_successfully
            postgres:
              condition: process_healthy
          availability: { restart: always }
      ```
      
      `migrate` runs once, must succeed. `app` waits for both `migrate` (success) and `postgres` (healthy).
      
      ## Pattern 4 — Tunnel that depends on the service it tunnels
      
      E.g. Cloudflare tunnel exposing a local service:
      
      ```yaml
      processes:
        mcp-server:
          command: "fastmcp serve --port 8000"
          readiness_probe:
            http_get: { host: localhost, port: 8000, path: / }
            initial_delay_seconds: 5
      
        mcp-tunnel:
          command: '"C:/Program Files/cloudflared/cloudflared.exe" tunnel run my-tunnel'
          depends_on:
            mcp-server:
              condition: process_healthy   # don't open tunnel until server is ready
          availability:
            restart: always
            backoff_seconds: 5
            max_restarts: 50               # tunnels can disconnect, allow many retries
      ```
      
      ## Pattern 5 — Static (one-time) setup task
      
      ```yaml
      processes:
        fetch-secrets:
          command: "python scripts/fetch_secrets.py"
          availability:
            restart: exit_on_failure   # must complete; stop the project if it fails
          # No readiness_probe — task either completes or doesn't
      
        app:
          command: "..."
          depends_on:
            fetch-secrets:
              condition: process_completed_successfully
      ```
      
      ## Cycle Detection
      
      PC detects cycles at startup. This fails immediately:
      
      ```yaml
      processes:
        a: { depends_on: { b: { condition: process_started } } }
        b: { depends_on: { a: { condition: process_started } } }
      # Error: dependency cycle detected: a -> b -> a
      ```
      
      ## What `depends_on` Does NOT Do
      
      - **Does not** restart dependents when a dependency restarts. If `webapp` crashes and recovers, `worker` doesn't automatically restart.
      - **Does not** stop a dependent when the dependency stops. You'll need to model this with `restart: exit_on_failure` and probes.
      - **Does not** enforce shutdown order (PC shuts down in any order unless `--ordered-shutdown` flag is used).
      
      For runtime coupling, the dependent process needs application-level reconnect/retry logic.
      
      ## Shutdown Ordering
      
      By default PC shuts processes down in any order. For services with stateful deps, use:
      
      ```bash
      process-compose down --ordered-shutdown
      # Stops in reverse dependency order: dependents first, then dependencies
      ```
      
      ## See Also
      
      - `probe-patterns.md` for crafting good `readiness_probe`s (without these, `process_healthy` is useless)
      - `schema-reference.md` for full availability/shutdown field semantics
      
    • probe-patterns.md 4 KB
      # Readiness Probe Patterns
      
      Concrete probe recipes by stack. Used in `process-compose.yaml`'s `readiness_probe` field.
      
      ## Python web servers (Django, Flask, FastAPI)
      
      ### Has a health endpoint
      
      ```yaml
      readiness_probe:
        http_get:
          host: localhost
          port: 8000
          path: /health/
        initial_delay_seconds: 5
        period_seconds: 10
        timeout_seconds: 3
        failure_threshold: 3
      ```
      
      ### No health endpoint (use any 200-returning path)
      
      ```yaml
      readiness_probe:
        http_get:
          host: localhost
          port: 8000
          path: /            # bare root; whatever returns 200
        initial_delay_seconds: 10  # Django often takes 5-15s to come up
        period_seconds: 10
        failure_threshold: 3
      ```
      
      ### Auth-required app
      
      If `/` returns 302 redirecting to login, that's still healthy (server is up). Probe a path that handles redirects:
      
      ```yaml
      readiness_probe:
        http_get:
          host: localhost
          port: 8000
          path: /
          # PC follows redirects; 200/302/301 all count as healthy
        initial_delay_seconds: 10
      ```
      
      ### Long-running startup (DB migrations, model loading)
      
      ```yaml
      readiness_probe:
        http_get:
          host: localhost
          port: 8000
          path: /ready
        initial_delay_seconds: 30      # give Django apps with migrations ~30s
        period_seconds: 15
        timeout_seconds: 5
        failure_threshold: 5            # tolerant during warmup
      availability:
        restart: always
        backoff_seconds: 10             # longer backoff matching startup cost
      ```
      
      ## Go binaries
      
      Most Go web servers come up in < 1 second:
      
      ```yaml
      readiness_probe:
        http_get:
          host: localhost
          port: 8080
          path: /
        initial_delay_seconds: 1
        period_seconds: 5
        failure_threshold: 3
      ```
      
      ## Node.js / Express / Next.js
      
      ```yaml
      readiness_probe:
        http_get:
          host: localhost
          port: 3000
          path: /
        initial_delay_seconds: 5         # Next.js cold start
        period_seconds: 10
        timeout_seconds: 3
      ```
      
      For dev servers (`next dev`), the initial route may not be ready immediately. Use a known static asset path or `_next/static/` if the home route is dynamic.
      
      ## Static file servers (`python -m http.server`)
      
      ```yaml
      readiness_probe:
        http_get:
          host: localhost
          port: 8000
          path: /
        initial_delay_seconds: 1
        period_seconds: 5
      ```
      
      ## TCP-only services (databases, message queues, custom protocols)
      
      ```yaml
      readiness_probe:
        tcp_socket:
          host: localhost
          port: 5432
        initial_delay_seconds: 5
        period_seconds: 10
        failure_threshold: 3
      ```
      
      ## Stuff that doesn't expose ports (daemons, watchers, cron-like)
      
      Use `exec` probe with a custom check, or skip probes entirely:
      
      ```yaml
      # Option A — exec probe checks daemon's pid file or self-reported status
      readiness_probe:
        exec:
          command: "test -f /var/run/myd.pid"
        initial_delay_seconds: 5
        period_seconds: 30
      
      # Option B — no probe at all; depends_on only uses process_started
      # (no readiness_probe block, just availability config)
      availability:
        restart: always
      ```
      
      ## When the probe is failing — debugging
      
      1. **Check the actual port:** does the service really bind that port? `netstat -ano | grep :8000` (Linux/Mac: `lsof -i :8000`)
      2. **Check the actual path:** does `curl -i http://localhost:8000/health` return 2xx/3xx? PC follows redirects but doesn't accept 4xx/5xx as healthy.
      3. **Check initial_delay_seconds:** does the service take longer than this to come up?
      4. **Check failure_threshold:** is the service flaky, returning 5xx intermittently?
      
      ## Anti-patterns
      
      ```yaml
      # BAD: probing a path that requires auth and returns 401
      readiness_probe:
        http_get: { port: 8000, path: /api/users/me }
      
      # BAD: probing a path that 404s during startup but eventually 200s
      # (the probe sees 404 → marks Not Ready → never recovers)
      readiness_probe:
        http_get: { port: 8000, path: /admin/some-resource }
      
      # BAD: zero initial_delay_seconds — guarantees first probe sees connection refused
      readiness_probe:
        http_get: { port: 8000, path: / }
        initial_delay_seconds: 0    # don't do this
      ```
      
      ## See Also
      
      - `dependency-patterns.md` for using readiness probes with `depends_on`
      - Upstream docs: https://f1bonacc1.github.io/process-compose/health/
      
    • schema-reference.md 6 KB
      # process-compose.yaml Schema Reference
      
      Comprehensive reference for the YAML schema (`version: "0.5"`). Annotated with field semantics, defaults, and gotchas.
      
      ## Top-level
      
      ```yaml
      version: "0.5"               # required; current schema version
      
      log_level: info              # debug | info | warn | error  (default: info)
      log_length: 1000             # lines retained in TUI's in-memory log buffer
      log_no_color: false          # disable ANSI colour in log file
      log_timestamps: true         # prepend ISO-8601 timestamp to each line
      log_truncate: false          # truncate logs on startup instead of appending
      
      processes:                   # required; map of process-name → spec
        <name>:
          ...                      # see Process Spec below
      
      environment:                 # OPTIONAL global env vars applied to every process
        - "GLOBAL_VAR=value"
      ```
      
      ## Process Spec
      
      ### Required
      
      ```yaml
      command: "string"            # shell command to execute (no shell interpolation
                                   # unless explicitly wrapped — see "command quoting"
                                   # below)
      ```
      
      ### Recommended
      
      ```yaml
      working_dir: "/abs/path"     # cwd; otherwise inherits PC's cwd
      environment:                 # array of KEY=value strings
        - "ENV_VAR=value"
        - "PYTHONUNBUFFERED=1"
      ```
      
      ### Lifecycle
      
      ```yaml
      availability:
        restart: always            # always | exit_on_failure | on_failure | no
        backoff_seconds: 5         # delay before next restart attempt
        max_restarts: 20           # absolute cap; 0 = unlimited
        exit_on_skipped: false     # treat skipped-by-dep failure as exit
        schedule: "0 2 * * *"      # cron schedule for periodic execution
                                   # (mutually exclusive with restart: always)
      
      shutdown:
        signal: 15                 # SIGTERM (default) or 9 for SIGKILL
        timeout_seconds: 30        # SIGKILL escalation deadline
        command: "graceful-cli stop"  # optional pre-stop command
      ```
      
      ### Health checks
      
      ```yaml
      readiness_probe:             # marks process "Ready" for depends_on
        http_get:
          host: localhost
          port: 8000
          path: /health            # bare "/" is fine if no health endpoint
          scheme: HTTP             # HTTP (default) or HTTPS
        # OR alternative probe types:
        # exec:
        #   command: "curl -f http://localhost:8000/health"
        # tcp_socket:
        #   host: localhost
        #   port: 8000
        initial_delay_seconds: 5   # wait before first probe
        period_seconds: 10         # interval between probes
        timeout_seconds: 3         # per-probe timeout
        success_threshold: 1       # consecutive successes to mark Ready
        failure_threshold: 3       # consecutive failures to mark Not Ready
      
      liveness_probe:              # restarts process if probe fails
        ...                        # same shape as readiness_probe
      ```
      
      ### Dependencies
      
      ```yaml
      depends_on:
        database:
          condition: process_healthy   # wait until database's readiness_probe passes
        migrations:
          condition: process_completed_successfully   # wait for one-shot init
      ```
      
      Conditions:
      
      | Condition | Wait until... | Use when |
      |---|---|---|
      | `process_started` | Dependency spawned (PID exists) | Weakest; use only when ordering matters but readiness doesn't |
      | `process_healthy` | Dependency's readiness_probe passes | Strongest; preferred for runtime services |
      | `process_completed` | Dependency exited (any code) | One-shot init that may fail |
      | `process_completed_successfully` | Dependency exited 0 | One-shot init that must succeed |
      
      ### Logging
      
      ```yaml
      log_location: "logs/myapp.log"   # relative to PC's cwd or absolute path
      log_max_size_kb: 0               # rotation threshold; 0 = no rotation
      log_max_backups: 0               # rotated files to retain
      log_max_age_days: 0              # age-based rotation
      log_compress: false              # gzip rotated logs
      ```
      
      ### Identity / grouping
      
      ```yaml
      namespace: "backend"         # group processes; --namespace flag filters by it
      replicas: 3                  # spawn N independent copies (named myapp@0, @1, @2)
      disabled: false              # exclude from `up` without deleting the spec
      is_daemon: false             # set true for processes that fork and exit
                                   # (e.g. systemd-style daemons)
      ```
      
      ### Visibility
      
      ```yaml
      is_foreground: false         # show full output in TUI immediately
      is_tty: false                # allocate a PTY (interactive processes)
      disable_ansi_colors: false   # strip ANSI from this process's logs
      ```
      
      ## Command Quoting
      
      YAML loves to surprise here. Three reliable patterns:
      
      ```yaml
      # Pattern 1 — simple command, no quotes anywhere
      command: pythonw manage.py runserver 0.0.0.0:8000
      
      # Pattern 2 — single-quoted (literal, no escapes processed)
      command: 'pythonw "C:\Program Files\foo\app.py" --port 8000'
      
      # Pattern 3 — double-quoted (escapes processed, watch the backslashes)
      command: "pythonw -m my_module"
      
      # Pattern 4 — wrap the exe path in double quotes inside a single-quoted string
      command: '"C:/Program Files/Git/usr/bin/bash.exe" --login script.sh'
      ```
      
      **Windows PATH env vars must be single-quoted** to escape backslashes:
      
      ```yaml
      environment:
        # WRONG: double quotes try to interpret \P, \U, etc. as escape codes
        # - "PATH=C:\Program Files\Git\usr\bin;..."
      
        # RIGHT:
        - 'PATH=C:\Program Files\Git\usr\bin;C:\Users\me\AppData\Local\Programs\Python\Python313'
      ```
      
      ## File Composition
      
      You can split config across files and merge:
      
      ```bash
      process-compose up -f base.yaml -f overrides.yaml -f local.yaml
      ```
      
      Later files override earlier ones. Useful pattern: base config in repo + per-machine overrides in gitignored `local.yaml`.
      
      ## Validation
      
      ```bash
      process-compose up -f process-compose.yaml --dry-run
      # → "Validated N configured processes from M files."
      ```
      
      Run before committing. Catches:
      - YAML parse errors (escape issues, indentation)
      - Missing required fields
      - Invalid restart policy values
      - Circular depends_on chains
      
      ## Hot Reload
      
      ```bash
      process-compose -p 8888 project update -f process-compose.yaml
      ```
      
      Reloads the config in a running PC instance. Added processes start; removed processes stop; changed processes restart.
      
    • supply-chain-verification.md 4.1 KB
      # Supply-Chain Verification for Process Compose
      
      Process Compose ships as a single Go binary via GitHub Releases with SHA-256 checksums. This is structurally safer than npm/PyPI packages but still requires verification before use.
      
      ## Why bother
      
      Even with Go's `go.sum` model:
      - The compiled binary is what runs, not the source — must verify the binary matches what was built from verified source
      - GitHub Releases artifacts can theoretically be tampered if repo permissions are compromised
      - Checksums file is on GitHub Releases too, so without a separate signature, you're trusting GitHub auth integrity
      - No GPG signing on Process Compose releases (as of v1.110.0) — relies entirely on GitHub Release access controls
      
      ## The Procedure
      
      ### 1. Download from official release
      
      ```bash
      VER="v1.110.0"   # pin a specific tag
      BASE="https://github.com/F1bonacc1/process-compose/releases/download/$VER"
      
      curl -fsSL -o pc.zip                       "$BASE/process-compose_windows_amd64.zip"
      curl -fsSL -o process-compose_checksums.txt "$BASE/process-compose_checksums.txt"
      ```
      
      ### 2. Verify hash BEFORE extraction
      
      ```bash
      EXPECTED=$(grep "process-compose_windows_amd64.zip" process-compose_checksums.txt | awk '{print $1}')
      ACTUAL=$(sha256sum pc.zip | awk '{print $1}')
      
      [ "$EXPECTED" = "$ACTUAL" ] || { echo "HASH MISMATCH - ABORT"; exit 1; }
      ```
      
      **Never** extract or run the binary before this check passes.
      
      ### 3. Extract, record the binary's own hash
      
      ```bash
      unzip pc.zip
      EXE_HASH=$(sha256sum process-compose.exe | awk '{print $1}')
      echo "$EXE_HASH" > bin/EXE_HASH
      ```
      
      This is the hash you re-verify on future installs to confirm the binary in your repo hasn't been tampered.
      
      ### 4. Commit binary + checksums to repo
      
      ```bash
      git add bin/process-compose.exe \
              bin/process-compose_checksums.txt \
              bin/VERSION                              # contains "v1.110.0"
      git commit -m "feat: pin process-compose $VER, verified SHA-256"
      ```
      
      ### 5. Document the verification
      
      Write a `bin/VERIFICATION.md`:
      
      ```markdown
      # Binary Verification
      
      ## process-compose.exe — v1.110.0
      
      - Pinned: 2026-MM-DD
      - Source: https://github.com/F1bonacc1/process-compose/releases/tag/v1.110.0
      - ZIP SHA-256:    018c660f...        (matched checksums.txt)
      - EXE SHA-256:    2e2a09a9...858637  (recorded for re-verification)
      - Runtime check:  "Process Compose v1.110.0, Commit cd7f6af"
      
      Trust anchor: GitHub Releases (HTTPS, requires repo write access to tamper).
      Limitation: No GPG signing on Process Compose releases.
      ```
      
      ## Re-verification
      
      Periodically — or as part of CI — re-hash the committed binary and confirm it matches the recorded value:
      
      ```powershell
      # Windows
      $expected = (Get-Content bin/EXE_HASH).Trim()
      $actual   = (Get-FileHash bin/process-compose.exe -Algorithm SHA256).Hash.ToLower()
      if ($expected -ne $actual) { throw "binary tampered" }
      ```
      
      ```bash
      # Unix
      expected=$(cat bin/EXE_HASH | tr -d '[:space:]')
      actual=$(sha256sum bin/process-compose.exe | awk '{print $1}')
      [ "$expected" = "$actual" ] || { echo "binary tampered"; exit 1; }
      ```
      
      ## Upgrade Procedure
      
      To bump versions:
      
      1. Run the download + verify procedure above with the new version tag
      2. Replace `bin/process-compose.exe`, `bin/process-compose_checksums.txt`, `bin/VERSION`, `bin/EXE_HASH`
      3. Update `VERIFICATION.md` with new hashes + date
      4. Run a parallel test (non-prod port) before cutting over
      5. Single PR with all of the above; review before merge
      
      ## What's NOT Covered
      
      The verification confirms **you got the binary the project intended to publish**. It does NOT cover:
      
      - Compromise of the project's source code (would need full source audit)
      - Compromise of the build environment (GoReleaser + GitHub Actions infrastructure)
      - The Go modules the binary was compiled with (transitive dependency risk)
      
      For deeper supply-chain analysis: tools like `osv-scanner`, `govulncheck`, or commercial tools (Socket.dev, Snyk) inspect the **source** dependency tree. Use those upstream of the verification step.
      
      ## See Also
      
      - `boot-persistence-windows.md` for how to launch the verified binary at boot
      - The repo's own AGENTS.md should document the pinned version policy
      
    • tui-shortcuts.md 4.5 KB
      # TUI Shortcuts Cheatsheet
      
      Launch:
      
      ```bash
      process-compose attach              # connect to the default port (8080)
      process-compose -p 8888 attach      # connect to a non-default API port
      ```
      
      If you launched PC with `-t=false` (headless), `attach` is how you bring up the TUI later. Quitting the TUI with `q` does **not** stop PC.
      
      ## Layout
      
      ```
      ┌──────────────────────────────────────────────────────────────┐
      │  Version + Project info                                       │
      │  Resources (RAM, CPU)                                         │
      ├──────────────────────────────────────────────────────────────┤
      │  PROCESS LIST (focused by default)                           │
      │  PID  NAME           NS  STATUS    AGE  HEALTH  RESTARTS  EX │
      │  ...                                                         │
      ├──────────────────────────────────────────────────────────────┤
      │  LOG PANE (logs for selected process)                        │
      │  [timestamp] log line                                        │
      │  [timestamp] log line                                        │
      ├──────────────────────────────────────────────────────────────┤
      │  F1 Shortcuts  F2 Scale  F3 Find  F4 Maximize  ...           │
      └──────────────────────────────────────────────────────────────┘
      ```
      
      ## Navigation
      
      | Key | Action |
      |---|---|
      | `↑` / `↓` | Move process selection up/down |
      | `k` / `j` | Same (vim-style) |
      | `Tab` | Move focus between process list and log pane |
      | `Home` / `End` | Jump to first / last process |
      | `Page Up` / `Page Down` | Page through process list |
      
      ## Pane controls
      
      | Key | Action |
      |---|---|
      | `F4` | Maximize current pane (toggle — second press un-maximizes) |
      | `F5` | Toggle log follow (unfollow lets you scroll history) |
      | `F6` | Toggle log wrap |
      | `Ctrl-S` | Toggle "select on" — clicks select process |
      
      ## Process control (selected process)
      
      | Key | Action |
      |---|---|
      | `r` | Restart |
      | `s` | Stop (graceful — SIGTERM) |
      | `t` | Start (if stopped) |
      | `Ctrl-D` | Disable the process (won't auto-restart) |
      | `Ctrl-E` | Re-enable a disabled process |
      
      ## Search / filter
      
      | Key | Action |
      |---|---|
      | `/` | Open filter input (filter process list by name) |
      | `F3` | Find in logs (search current log pane) |
      | `n` / `N` | Next / previous match in logs |
      | `Esc` | Clear filter / cancel input |
      
      ## Sorting
      
      | Key | Action |
      |---|---|
      | `Ctrl-N` | Sort by Name |
      | `Ctrl-T` | Sort by Status |
      | `Ctrl-A` | Sort by Age |
      | `Ctrl-H` | Sort by Health |
      | `R` (uppercase) | Reverse current sort |
      
      ## Status column legend
      
      | Status | Meaning |
      |---|---|
      | `Running` | Process is up |
      | `Ready` | `readiness_probe` passing (only shown if probe defined) |
      | `Not Ready` | Probe failing; PC will keep checking |
      | `Restarting` | Between restart attempts (`backoff_seconds`) |
      | `Completed` | Exited successfully (only for non-restart processes) |
      | `Failed` | Exited with error and out of restart budget |
      | `Pending` | Waiting on `depends_on` to be satisfied |
      | `Disabled` | Manually disabled or `disabled: true` in YAML |
      | `Skipped` | A dependency failed so this process was skipped |
      
      ## Exit
      
      | Key | Action |
      |---|---|
      | `q` | Quit TUI (PC keeps running headless) |
      | `Ctrl-C` | Same |
      | `?` | Show help overlay |
      
      ## Headless workflow (no TUI)
      
      If you don't want the TUI but need to peek at state:
      
      ```bash
      # Process list
      process-compose -p 8888 process list
      
      # Logs for one process
      process-compose -p 8888 process logs my-service --follow
      process-compose -p 8888 process logs my-service        # one-shot, no follow
      
      # Control without TUI
      process-compose -p 8888 process restart my-service
      process-compose -p 8888 process stop my-service
      process-compose -p 8888 process start my-service
      ```
      
      ## See Also
      
      - Schema reference for `is_foreground`, `is_tty`, `disable_ansi_colors` which affect log rendering in the TUI
      - Upstream docs: https://f1bonacc1.github.io/process-compose/tui/
      
  • scripts
    • boot-start.template.ps1 3.7 KB · in bundle
    • boot-task-install.template.ps1 3.3 KB · in bundle
    • install-process-compose.ps1 3.8 KB · in bundle
    • verify-binary.ps1 1.7 KB · in bundle
  • tests
    • run.sh 4.4 KB
      #!/usr/bin/env bash
      # Self-test for process-compose-ops — fully offline: no network, no download.
      #
      # Wraps the skill's binary-integrity verifier (scripts/verify-binary.ps1), which
      # re-checks the committed process-compose.exe SHA-256 against the recorded
      # EXE_HASH. Unlike the three §7 staleness scripts this verifier is PowerShell and
      # platform-locked to a Windows binary, so the suite has two layers:
      #   * contract / structural (run anywhere, no PowerShell host required): the .ps1
      #     exists and encodes the Get-FileHash/EXE_HASH compare plus the mismatch
      #     throw — proving it is a real check, not a vacuous rubber stamp;
      #   * behavioural happy + negative against a temp bin/ — exercised only when a
      #     PowerShell host (pwsh | powershell) is present. A dummy .exe and a
      #     hand-written EXE_HASH stand in for the real binary, so no download and no
      #     network ever occur. SKIP'd (suite stays green) on a PowerShell-less Linux
      #     runner — the structural layer still runs.
      #
      # Usage:   bash tests/run.sh
      # Exit:    0 all pass, 1 one or more failures
      
      set -uo pipefail
      
      HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      SKILL="$(dirname "$HERE")"
      V="$SKILL/scripts/verify-binary.ps1"
      
      # A PowerShell host, if any (pwsh = PowerShell 7 cross-platform; powershell =
      # Windows PowerShell 5.1). Absent on most Linux CI runners.
      PSHOST=""
      for c in pwsh powershell; do
        if command -v "$c" >/dev/null 2>&1 && "$c" -NoProfile -Command 'exit 0' >/dev/null 2>&1; then PSHOST="$c"; break; fi
      done
      
      SB="$(mktemp -d)"; trap 'rm -rf "$SB"' EXIT
      PASS=0; FAIL=0
      ok() { PASS=$((PASS+1)); printf '  PASS  %s\n' "$1"; }
      no() { FAIL=$((FAIL+1)); printf '  FAIL  %s\n' "$1"; }
      expect_exit() { [[ "$2" == "$3" ]] && ok "$1 (exit $3)" || no "$1 (want $2 got $3)"; }
      expect_has()  { case "$3" in *"$2"*) ok "$1";; *) no "$1 (missing '$2')";; esac; }
      
      echo "=== process-compose-ops self-test ==="
      
      # ── contract / structural (runs anywhere, no PowerShell host needed) ──────────
      echo "-- contract --"
      [[ -f "$V" ]] && ok "verify-binary.ps1 present" || no "verify-binary.ps1 present"
      [[ -s "$V" ]] && ok "verify-binary.ps1 non-empty" || no "verify-binary.ps1 non-empty"
      # The verifier must encode the integrity check it claims to run: a SHA-256
      # compare of the binary against EXE_HASH that throws on mismatch. If these
      # markers were gutted the verifier would be a vacuous rubber stamp.
      vtxt="$(cat "$V")"
      expect_has "computes SHA-256 of the binary" "Get-FileHash" "$vtxt"
      expect_has "reads recorded EXE_HASH" "EXE_HASH" "$vtxt"
      expect_has "fails loud on mismatch" "MISMATCH" "$vtxt"
      
      # ── behavioural (happy + negative) under a PowerShell host, if present ─────────
      echo "-- behavioural (${PSHOST:-no powershell host}) --"
      if [[ -z "$PSHOST" ]]; then
        echo "  SKIP  happy/negative (no pwsh/powershell on this runner — structural layer above still ran)"
      else
        # happy: a dummy .exe whose recorded EXE_HASH matches its actual SHA-256.
        mkdir -p "$SB/ok/bin"
        printf 'process-compose-fake-bytes' > "$SB/ok/bin/process-compose.exe"
        # sha256sum and Get-FileHash compute the identical digest; the verifier
        # compares lowercased + trimmed, so this matches what it recomputes.
        sha256sum "$SB/ok/bin/process-compose.exe" | awk '{print $1}' > "$SB/ok/bin/EXE_HASH"
        "$PSHOST" -NoProfile -File "$V" -BinDir "$SB/ok/bin" >/dev/null 2>&1
        expect_exit "matching EXE_HASH -> 0" 0 $?
      
        # negative: a recorded EXE_HASH that disagrees with the actual SHA-256.
        mkdir -p "$SB/bad/bin"
        printf 'process-compose-fake-bytes' > "$SB/bad/bin/process-compose.exe"
        printf '0000000000000000000000000000000000000000000000000000000000000000' > "$SB/bad/bin/EXE_HASH"
        "$PSHOST" -NoProfile -File "$V" -BinDir "$SB/bad/bin" >"$SB/neg.out" 2>&1
        rc=$?
        [[ "$rc" -ne 0 ]] && ok "mismatched EXE_HASH -> nonzero (exit $rc)" || no "mismatched EXE_HASH -> nonzero (got 0)"
        expect_has "verifier reports MISMATCH" "MISMATCH" "$(cat "$SB/neg.out")"
      fi
      
      # ── SKILL.md sanity ───────────────────────────────────────────────────────────
      echo "-- SKILL.md --"
      grep -q '^name: process-compose-ops$' "$SKILL/SKILL.md" && ok "frontmatter name" || no "frontmatter name"
      grep -q 'verify-binary.ps1' "$SKILL/SKILL.md" && ok "verifier cited from SKILL.md" || no "verifier cited from SKILL.md"
      
      echo ""
      echo "=== $PASS passed, $FAIL failed ==="
      [[ "$FAIL" -eq 0 ]] || exit 1
      exit 0
      
  • SKILL.md 12.2 KB
    ---
    name: process-compose-ops
    description: "Process Compose orchestration for non-containerized local services: process-compose.yaml schema, health checks, restart policies, dependencies, TUI/REST/MCP control, scheduling, and boot persistence. Use as a PM2/supervisord/Foreman replacement for local dev service management."
    license: MIT
    allowed-tools: "Read Write Bash Edit"
    metadata:
      author: claude-mods
      related-skills: portless-ops, docker-ops, cli-ops
      upstream: https://github.com/F1bonacc1/process-compose
    ---
    
    # Process Compose Operations
    
    Process Compose is a Go-based supervisor for non-containerized services. Single binary, YAML config, built-in TUI, REST API, **MCP server**, and proper Windows support. Replacement for PM2/supervisord/Foreman in the local-dev role.
    
    **Why not PM2:** PM2 5.x has 15+ known CVEs (axios/lodash/tar/minimist transitive npm exposure). PC compiles all deps in at build time with `go.sum` hashes — structurally resistant to TanStack-style npm worm attacks.
    
    **Why not Docker Compose:** Container overhead is unnecessary for local Python/Node/Go dev servers running directly. PC gives you health checks, dependencies, and restart policies without the container layer.
    
    ## Install (verified)
    
    ```bash
    # Pin a specific version, verify SHA-256 against upstream checksums
    VER="v1.110.0"
    BASE="https://github.com/F1bonacc1/process-compose/releases/download/$VER"
    
    curl -fsSL -o pc.zip "$BASE/process-compose_windows_amd64.zip"
    curl -fsSL -o checksums.txt "$BASE/process-compose_checksums.txt"
    
    EXPECTED=$(grep "process-compose_windows_amd64.zip" checksums.txt | awk '{print $1}')
    ACTUAL=$(sha256sum pc.zip | awk '{print $1}')
    [ "$EXPECTED" = "$ACTUAL" ] || { echo "HASH MISMATCH"; exit 1; }
    
    unzip pc.zip
    # Commit process-compose.exe to your repo's bin/ directory
    ```
    
    Record the binary's hash in your repo's `SUPPLY-CHAIN.md` for re-verification on next upgrade.
    
    ## process-compose.yaml Quick Reference
    
    ```yaml
    version: "0.5"
    
    log_level: info
    log_length: 1000
    
    processes:
    
      my-service:
        command: "pythonw -m uvicorn main:app --host 127.0.0.1 --port 8000"
        working_dir: "X:/path/to/repo"
        environment:
          - "DJANGO_SETTINGS_MODULE=myapp.settings"
          - "PYTHONUNBUFFERED=1"
        readiness_probe:
          http_get:
            host: localhost
            port: 8000
            path: /
          initial_delay_seconds: 5
          period_seconds: 10
          timeout_seconds: 3
          failure_threshold: 3
        availability:
          restart: always           # always | exit_on_failure | on_failure | no
          backoff_seconds: 5
          max_restarts: 20
        depends_on:
          database:
            condition: process_healthy   # process_started | process_healthy | process_completed
        shutdown:
          signal: 15                # SIGTERM
          timeout_seconds: 30
        log_location: "logs/my-service.log"
    
      scheduled-job:
        command: "python backup.py"
        schedule: "0 2 * * *"       # 2am daily cron
        availability:
          restart: exit_on_failure
    ```
    
    ## Restart Policies
    
    | Policy | Restarts on... |
    |---|---|
    | `always` | Any exit (success or failure) — best for long-running daemons |
    | `on_failure` | Non-zero exit codes only |
    | `exit_on_failure` | Stops PC entirely if this process fails — use for critical deps |
    | `no` | Never restart |
    
    ## Dependency Conditions
    
    | Condition | Wait until... |
    |---|---|
    | `process_started` | Dependency spawned (PID exists). Fastest, weakest guarantee. |
    | `process_healthy` | Dependency's readiness_probe passes. Strong guarantee. |
    | `process_completed` | Dependency exited successfully (for init/setup processes). |
    
    ## CLI Reference
    
    ```bash
    # Lifecycle
    process-compose up -f config.yaml          # Start (foreground TUI by default)
    process-compose up -f config.yaml -t=false # Headless (no TUI)
    process-compose up -f config.yaml --dry-run  # Validate config without starting
    process-compose down                       # Stop all processes + project
    
    # Inspection (against running PC)  (example values — substitute your own port/dir)
    process-compose -p <your-pc-port> process list       # all processes + status
    process-compose -p <your-pc-port> process logs <name> --follow
    process-compose -p <your-pc-port> attach             # TUI for running project
    
    # Process control
    process-compose -p <your-pc-port> process restart <name>
    process-compose -p <your-pc-port> process stop <name>
    process-compose -p <your-pc-port> process start <name>
    
    # Reload config without stopping (hot update)
    process-compose -p <your-pc-port> project update -f config.yaml
    
    # Standalone inspection (no running PC)
    process-compose info                       # config home info
    process-compose graph -f config.yaml       # dependency graph
    process-compose analyze -f config.yaml     # startup timing analysis
    ```
    
    **Key flag gotcha:** there's no `--detached` flag. To run in background:
    - Linux/Mac: `process-compose up -t=false &` (shell backgrounding)
    - Windows: launch via Task Scheduler or `Start-Process` with `-WindowStyle Hidden`
    
    ## TUI Navigation
    
    Launch: `process-compose attach` (or `up` without `-t=false`).
    
    | Key | Action |
    |---|---|
    | `↑` `↓` or `j` `k` | Navigate process list |
    | `Tab` | Switch focus between process list and log pane |
    | `F4` | Maximize current pane (toggle) |
    | `F5` | Unfollow logs (lets you scroll history) |
    | `F6` | Unwrap log lines |
    | `r` | Restart selected process |
    | `s` | Stop selected process |
    | `t` | Start selected process |
    | `/` | Filter process list |
    | `?` | Help overlay |
    | `q` | Quit TUI (PC keeps running in background) |
    
    ## MCP Server Integration
    
    PC ships a built-in MCP server exposing processes as tools for AI agents. Enable via the config or CLI flag. With the MCP server on, a Claude Code agent can directly:
    
    - List running processes
    - Get process status/health
    - Restart/stop/start processes
    - Read process logs
    
    This replaces shell-based glue scripts (the old PM2-broker pattern).
    
    ## API Port Selection
    
    Default API port is 8080. Common collisions:
    
    | Port 8080 user | Workaround |
    |---|---|
    | Dagu dashboard | Use `-p <your-pc-port>` until Dagu decommissioned |
    | Tomcat / Spring Boot dev | Use `-p <your-pc-port>` |
    | Other dev tool defaults | Pick anything free in 8000–9999 range |
    
    If you change the API port, every subsequent CLI call needs `-p <port>`:
    
    ```bash
    process-compose -p <your-pc-port> process list
    process-compose -p <your-pc-port> process logs axiom --follow
    ```
    
    ## Windows Boot Persistence Pattern
    
    Task Scheduler runs with minimal PATH. Use a wrapper script that sets PATH explicitly before launching PC.
    
    ```powershell
    # scripts/boot-start.ps1
    $root = "<your-process-compose-dir>"
    $pcExe = "$root\bin\process-compose.exe"
    
    # Explicit PATH for managed services (Python, uv, Git tools, cloudflared, etc.)
    $env:PATH = (@(
        "$root\bin"
        "C:\Program Files\Git\usr\bin"          # openssl, bash
        "C:\Users\<user>\AppData\Local\Programs\Python\Python313\Scripts"
        "$env:PATH"
    ) -join ';')
    
    # Optional: source secrets from gitignored .env
    $envFile = "$root\.env"
    if (Test-Path $envFile) {
        Get-Content $envFile | ForEach-Object {
            if ($_ -match '^\s*([A-Z_]+)\s*=\s*(.+?)\s*$') {
                [Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process')
            }
        }
    }
    
    # Launch headless
    & $pcExe -p <your-pc-port> -t=false -L "$root\logs\process-compose.log" up -f "$root\process-compose.yaml"
    ```
    
    Register as a Task Scheduler entry with `LogonType S4U` (runs at boot, no password, no interactive logon needed):
    
    ```powershell
    $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType S4U -RunLevel Highest
    $action = New-ScheduledTaskAction -Execute "powershell.exe" `
        -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$root\scripts\boot-start.ps1`""
    $trigger = New-ScheduledTaskTrigger -AtStartup
    Register-ScheduledTask -TaskName "ProcessCompose-Boot" `
        -Action $action -Trigger $trigger -Principal $principal -Force
    ```
    
    ## YAML Gotchas
    
    | Gotcha | Symptom | Fix |
    |---|---|---|
    | Windows PATH with backslashes in double-quoted YAML | `yaml: found unknown escape character` | Use single quotes: `- 'PATH=C:\Program Files\Git\usr\bin;...'` |
    | `command` with quoted paths containing spaces | First arg eaten | Wrap whole command in single quotes, inner paths in double: `'"C:/Program Files/foo.exe" arg1 arg2'` |
    | Forgot `working_dir` | Process starts in PC's cwd, can't find files | Always specify absolute `working_dir` |
    | Health probe wrong port | Process restart-loops with `Not Ready` | Match `readiness_probe.http_get.port` to where the process actually binds |
    | Secrets in YAML | Committed to git | Use `environment` to pass-through; set in shell env or gitignored `.env` |
    
    ## Common Operations
    
    ```bash
    # Validate config before applying
    process-compose up --dry-run -f process-compose.yaml
    
    # Hot-reload after editing config
    process-compose -p <your-pc-port> project update -f process-compose.yaml
    
    # Restart one service after code change
    process-compose -p <your-pc-port> process restart axiom
    
    # Watch logs of a misbehaving service
    process-compose -p <your-pc-port> process logs axiom --follow
    
    # Stop one service temporarily for debugging
    process-compose -p <your-pc-port> process stop axiom
    # Now run it manually with your debugger, then:
    process-compose -p <your-pc-port> process start axiom
    ```
    
    ## When to Use Process Compose vs Alternatives
    
    | Need | Tool |
    |---|---|
    | Local non-containerized services with health/dependencies/MCP | **Process Compose** |
    | Production node.js process supervision | PM2 (despite age) |
    | Container-based stack | Docker Compose |
    | Job queue with cron + DAGs | Dagu, Temporal, Airflow |
    | System service supervision | systemd (Linux), Windows Services |
    | One-shot Procfile run | Foreman / Overmind / Hivemind (Unix-only) |
    
    ## Worked Example
    
    See `<your-process-compose-dir>\` for an 11-process production stack:
    - `process-compose.yaml` — health-checked services with depends_on chains
    - `scripts/boot-start.ps1` — PATH-aware boot wrapper
    - `docs/MIGRATION-LOG.md` — full migration from PM2 + Caddy, every gotcha documented
    - `docs/SUPPLY-CHAIN.md` — binary verification procedure
    
    ## Anti-Patterns
    
    ```
    BAD:  process-compose up --detached       # flag does not exist
    GOOD: process-compose up -t=false &       # background via shell
    
    BAD:  put secrets in process-compose.yaml (commits to git)
    GOOD: source from gitignored .env in boot wrapper
    
    BAD:  use API port 8080 (clashes with Dagu, Tomcat, others)
    GOOD: -p <your-pc-port> (or any free port), document the choice
    
    BAD:  ignore readiness_probe and just hope services come up
    GOOD: configure http_get probe on a real endpoint; depends_on uses process_healthy
    
    BAD:  upgrade PC by running an installer (npm install -g, scoop install, brew install)
    GOOD: download specific version, verify SHA-256 against upstream checksums.txt, commit binary
    ```
    
    ## Resources in this skill
    
    ### `references/`
    - `schema-reference.md` — full process-compose.yaml schema with field semantics, defaults, and command-quoting gotchas
    - `probe-patterns.md` — readiness probe recipes by stack (Python, Go, Node, TCP-only, daemons)
    - `dependency-patterns.md` — `depends_on` patterns: companion daemons, DB-before-app, tunnel-after-service, one-shot init
    - `tui-shortcuts.md` — TUI cheatsheet (keys, status legend, search/sort/filter)
    - `boot-persistence-windows.md` — Task Scheduler setup with S4U logon, PATH-aware wrapper
    - `supply-chain-verification.md` — full SHA-256 verification procedure for the binary
    
    ### `scripts/`
    - `install-process-compose.ps1` — download + verify + extract a pinned version, writes VERIFICATION.md
    - `verify-binary.ps1` — re-verify committed binary hash (monthly / pre-commit)
    - `boot-start.template.ps1` — PATH-aware boot wrapper (copy + adapt per machine)
    - `boot-task-install.template.ps1` — Task Scheduler entry registration (S4U logon)
    
    ### `assets/`
    - `python-uvicorn.yaml` — uvicorn/FastAPI/Django basic service template
    - `django-with-companions.yaml` — Django + queue daemon + audit watcher chain
    - `go-binary-service.yaml` — Go binary with HTTP or TCP probe
    - `tunnel-with-dependency.yaml` — Cloudflare tunnel waiting on its target service
    - `cron-job.yaml` — scheduled task patterns
    
    ## Related Skills
    
    - `portless-ops` — the routing layer we pair with PC (replaces Caddy)
    - `docker-ops` — container alternative for the same role
    - `mcp-ops` — PC's MCP server fits this ecosystem
    - `cli-ops` — general CLI tool patterns
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related