Browse Source

feat(skills): vendor orchestration artifacts as references/scripts/assets

Filling out portless-ops and process-compose-ops to match the depth of
postgres-ops (the structural exemplar in this repo). Material sourced
from the X:\00_Orchestration migration we just ran, with paths
anonymised to generic templates.

portless-ops (was 1 SKILL.md + 2 upstream refs, now 13 files):
  references/
    upstream-portless.md            (existing — verbatim canonical)
    upstream-oauth.md               (existing — verbatim canonical)
    tld-selection.md                (NEW — decision tree for TLD picking)
    windows-specifics.md            (NEW — openssl, certutil, curl, PS 5.1)
    integration-patterns.md         (NEW — combos with PC/Docker/PM2/Tailscale/worktrees)
  scripts/
    install-portless.ps1            (NEW — verified install with IOC scan)
    reset-state.ps1                 (NEW — clean reset for TLD changes)
    sync-aliases-from-yaml.ps1      (NEW — derive aliases from supervisor config)
  assets/
    portless.json.simple.json       (NEW — single-app template)
    portless.json.monorepo.json     (NEW — workspace template)
    portless.json.with-custom-tld.json (NEW — TLD documentation template)
    package.json-portless-key.json  (NEW — alternative inline config)

process-compose-ops (was 1 SKILL.md only, now 16 files):
  references/
    schema-reference.md             (NEW — full YAML schema + quoting gotchas)
    probe-patterns.md               (NEW — readiness probe recipes per stack)
    dependency-patterns.md          (NEW — depends_on patterns + conditions)
    tui-shortcuts.md                (NEW — TUI keybindings + status legend)
    boot-persistence-windows.md     (NEW — Task Scheduler with S4U + PATH wrapper)
    supply-chain-verification.md    (NEW — SHA-256 verification procedure)
  scripts/
    install-process-compose.ps1     (NEW — verified download/extract/record)
    verify-binary.ps1               (NEW — re-verify committed binary hash)
    boot-start.template.ps1         (NEW — PATH-aware boot wrapper)
    boot-task-install.template.ps1  (NEW — Task Scheduler registration)
  assets/
    python-uvicorn.yaml             (NEW — basic Python service template)
    django-with-companions.yaml     (NEW — three-process dependency chain)
    go-binary-service.yaml          (NEW — Go service with HTTP/TCP probe)
    tunnel-with-dependency.yaml     (NEW — Cloudflare tunnel pattern)
    cron-job.yaml                   (NEW — scheduled task pattern)

SKILL.md updated in both to expose all new resources in a "Resources in
this skill" section.

All templates are anonymised (generic paths like X:/path/to/myapp instead
of X:/Forge/Axiom; placeholder names like myapp instead of axiom).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0xDarkMatter 2 weeks ago
parent
commit
5a3b4be8be
27 changed files with 2325 additions and 0 deletions
  1. 20 0
      skills/portless-ops/SKILL.md
  2. 26 0
      skills/portless-ops/assets/package.json-portless-key.json
  3. 17 0
      skills/portless-ops/assets/portless.json.monorepo.json
  4. 12 0
      skills/portless-ops/assets/portless.json.simple.json
  5. 11 0
      skills/portless-ops/assets/portless.json.with-custom-tld.json
  6. 165 0
      skills/portless-ops/references/integration-patterns.md
  7. 112 0
      skills/portless-ops/references/tld-selection.md
  8. 147 0
      skills/portless-ops/references/windows-specifics.md
  9. 142 0
      skills/portless-ops/scripts/install-portless.ps1
  10. 85 0
      skills/portless-ops/scripts/reset-state.ps1
  11. 59 0
      skills/portless-ops/scripts/sync-aliases-from-yaml.ps1
  12. 23 0
      skills/process-compose-ops/SKILL.md
  13. 33 0
      skills/process-compose-ops/assets/cron-job.yaml
  14. 74 0
      skills/process-compose-ops/assets/django-with-companions.yaml
  15. 34 0
      skills/process-compose-ops/assets/go-binary-service.yaml
  16. 36 0
      skills/process-compose-ops/assets/python-uvicorn.yaml
  17. 48 0
      skills/process-compose-ops/assets/tunnel-with-dependency.yaml
  18. 190 0
      skills/process-compose-ops/references/boot-persistence-windows.md
  19. 167 0
      skills/process-compose-ops/references/dependency-patterns.md
  20. 165 0
      skills/process-compose-ops/references/probe-patterns.md
  21. 190 0
      skills/process-compose-ops/references/schema-reference.md
  22. 115 0
      skills/process-compose-ops/references/supply-chain-verification.md
  23. 122 0
      skills/process-compose-ops/references/tui-shortcuts.md
  24. 74 0
      skills/process-compose-ops/scripts/boot-start.template.ps1
  25. 92 0
      skills/process-compose-ops/scripts/boot-task-install.template.ps1
  26. 107 0
      skills/process-compose-ops/scripts/install-process-compose.ps1
  27. 59 0
      skills/process-compose-ops/scripts/verify-binary.ps1

+ 20 - 0
skills/portless-ops/SKILL.md

@@ -202,6 +202,26 @@ BAD:  change TLD by stopping/starting with different --tld and hoping aliases up
 GOOD: stop proxy, wipe routes.json, start with new TLD, re-register from supervisor config
 ```
 
+## Resources in this skill
+
+### `references/`
+- `upstream-portless.md` — canonical portless SKILL.md verbatim (CLI ref, monorepo, turborepo, worktrees, LAN, Tailscale, HTTPS, troubleshooting)
+- `upstream-oauth.md` — canonical OAuth setup for Google/Apple/Microsoft/Facebook/GitHub
+- `tld-selection.md` — decision tree for picking the right TLD; trade-offs of `.test`/`.dev`/`.localhost`/custom-owned
+- `windows-specifics.md` — openssl PATH, certutil quirks, curl-vs-browser cert handling, PS 5.1 gotchas
+- `integration-patterns.md` — combos with Process Compose / Docker / PM2 / Tailscale / git worktrees
+
+### `scripts/`
+- `install-portless.ps1` — verified install: inspect tarball, scan for IOCs from recent attacks, install only if clean
+- `reset-state.ps1` — clean state reset (used when changing TLD; `--remove` can't clear old-TLD aliases)
+- `sync-aliases-from-yaml.ps1` — derive portless aliases from a process-compose.yaml
+
+### `assets/`
+- `portless.json.simple.json` — single-app config template
+- `portless.json.monorepo.json` — workspace monorepo with name overrides
+- `portless.json.with-custom-tld.json` — documents TLD choice in repo
+- `package.json-portless-key.json` — alternative: portless config inside package.json
+
 ## Related Skills
 
 - `process-compose-ops` — the supervisor we pair with portless

+ 26 - 0
skills/portless-ops/assets/package.json-portless-key.json

@@ -0,0 +1,26 @@
+{
+  "_comment_purpose": "Example showing how to put portless config inside an existing package.json instead of a separate portless.json file",
+  "_comment_usage": "Add the 'portless' key to your existing package.json. String shorthand for just the name, object for full config.",
+
+  "name": "@myorg/myapp",
+  "version": "1.0.0",
+  "scripts": {
+    "dev": "next dev",
+    "dev:app": "next dev",
+    "build": "next build"
+  },
+
+  "_string_shorthand_example": "portless: 'myapp'",
+
+  "portless": {
+    "name": "myapp",
+    "script": "dev:app"
+  },
+
+  "_alternative_string_form": {
+    "_comment": "If you just want to set the name, use a string instead:",
+    "_example": "\"portless\": \"myapp\""
+  },
+
+  "_precedence": "CLI flags > package.json portless key > portless.json app entry > defaults"
+}

+ 17 - 0
skills/portless-ops/assets/portless.json.monorepo.json

@@ -0,0 +1,17 @@
+{
+  "_comment_purpose": "Monorepo portless.json — runs all workspace packages with name overrides",
+  "_comment_usage": "Drop in monorepo root. Each apps/<pkg> with its own dev script gets a URL.",
+
+  "_comment_default_naming": "Without an apps map, hostnames are <pkg>.<project>.<tld>",
+  "_comment_explicit_naming": "With apps map, exact name as written. Keys are relative paths.",
+
+  "apps": {
+    "apps/web": { "name": "myapp" },
+    "apps/api": { "name": "api.myapp" },
+    "apps/docs": { "name": "docs.myapp", "script": "dev:docs" }
+  },
+
+  "_optional_turbo": true,
+
+  "_strip_underscored_keys_before_using": "remove all keys starting with underscore before committing"
+}

+ 12 - 0
skills/portless-ops/assets/portless.json.simple.json

@@ -0,0 +1,12 @@
+{
+  "_comment_purpose": "Single-app portless.json — name + script overrides for one project",
+  "_comment_usage": "Drop in repo root. portless picks it up automatically.",
+
+  "name": "myapp",
+
+  "_optional_script": "dev",
+  "_optional_appPort": 4123,
+  "_optional_proxy": true,
+
+  "_strip_underscored_keys_before_using": "remove all keys starting with underscore before committing"
+}

+ 11 - 0
skills/portless-ops/assets/portless.json.with-custom-tld.json

@@ -0,0 +1,11 @@
+{
+  "_comment_purpose": "portless.json with documented TLD choice — actual TLD is a proxy-level setting (PORTLESS_TLD env or --tld flag); this file just records the choice for the repo",
+
+  "name": "myapp",
+
+  "_tld_choice": "test",
+  "_tld_reason": "IANA-reserved (RFC 6761), OAuth-safe, no DNS collision risk",
+  "_proxy_start_command": "portless proxy start --tld test --port 443",
+
+  "_strip_underscored_keys_before_using": "remove all keys starting with underscore before committing"
+}

+ 165 - 0
skills/portless-ops/references/integration-patterns.md

@@ -0,0 +1,165 @@
+# Integration Patterns
+
+Portless is a routing layer. It pairs with a process supervisor that owns lifecycle. Three common combos:
+
+## Pattern A — Portless + Process Compose (recommended for local dev)
+
+The whole stack speaks YAML and gives you health checks, restart policies, dependencies, and an MCP server.
+
+```yaml
+# process-compose.yaml — supervisor owns processes
+processes:
+  myapp:
+    command: "uv run python -m myapp"
+    working_dir: "X:/path/to/myapp"
+    readiness_probe:
+      http_get: { host: localhost, port: 8000, path: / }
+    availability: { restart: always }
+```
+
+```powershell
+# Portless owns routing — aliases derive from supervisor config
+portless proxy start --tld test
+portless alias myapp 8000   # https://myapp.test → :8000
+```
+
+**Single source of truth:** `process-compose.yaml`. Aliases derive from it. See the [`process-compose-ops`](../../process-compose-ops/SKILL.md) skill for the supervisor side.
+
+## Pattern B — Portless + Docker
+
+When some services run in containers (databases, n8n, custom containers) and others run locally.
+
+```bash
+# Container started independently, listening on host port 5678
+docker run -d -p 5678:5678 --name n8n n8nio/n8n
+
+# Make it reachable at a named URL
+portless alias n8n 5678
+# → https://n8n.test
+```
+
+This decouples container lifecycle from portless. `docker stop n8n` doesn't affect portless's alias (URL just stops resolving until container's back up).
+
+## Pattern C — Portless + PM2 (legacy, when migration isn't worth it)
+
+Same shape as Pattern A:
+
+```javascript
+// ecosystem.config.js
+module.exports = {
+  apps: [
+    { name: 'myapp', script: 'python', args: '-m myapp', cwd: 'X:/path/myapp' }
+  ]
+};
+```
+
+```powershell
+# PM2 owns processes
+pm2 start ecosystem.config.js
+
+# Portless owns routing
+portless alias myapp 8000
+```
+
+**Note:** PM2 5.x has 15+ known CVEs in its transitive npm dependencies (axios, lodash, tar, minimist...). Process Compose's Go-binary attack model is much narrower. New stacks should pick Pattern A.
+
+## Pattern D — Portless Spawning the Process (no separate supervisor)
+
+For zero-config / one-off / monorepo cases, portless can spawn the process itself:
+
+```bash
+# Run a Next.js dev server through the proxy
+portless myapp next dev
+# → https://myapp.test, with auto-assigned port
+
+# From a monorepo root, run all packages' dev scripts
+portless
+```
+
+Limitations:
+- **No crash recovery** — if the process dies, portless does NOT restart it
+- **No health checks** — only "process exists" matters
+- **No dependencies** between processes
+
+Good for: short-lived dev sessions, monorepos where everything is JS/TS and Vercel-like ergonomics matter.
+
+Bad for: long-running services, dependency chains, anything you want supervised through a reboot. Use Pattern A instead.
+
+## Pattern E — Portless + Tailscale (team sharing)
+
+Share local dev with teammates without a public deployment:
+
+```bash
+# Start the proxy
+portless proxy start --tld test
+
+# Run with --tailscale to register a tailnet URL too
+portless myapp --tailscale next dev
+# → https://myapp.test                    (you, local)
+# → https://yourdevbox.your-team.ts.net   (teammates, tailnet)
+```
+
+Requirements:
+- `tailscale` CLI installed and connected
+- HTTPS enabled on the tailnet (Tailscale admin console)
+- For public sharing: Funnel enabled (`--funnel` instead of `--tailscale`)
+
+See upstream docs (`references/upstream-portless.md`, section "Tailscale sharing") for the full setup.
+
+## Pattern F — Subdomain Routing in a Monorepo
+
+```bash
+portless myapp next dev          # → https://myapp.test
+portless api.myapp pnpm start    # → https://api.myapp.test
+portless docs.myapp next dev     # → https://docs.myapp.test
+```
+
+Add `--wildcard` so any unregistered subdomain falls back to the parent:
+
+```bash
+portless proxy start --wildcard --tld test
+# Now tenant1.myapp.test → routes to myapp (whatever's registered)
+```
+
+Useful for multi-tenant apps where you want to test tenant resolution locally.
+
+## Pattern G — Git Worktrees with Per-Branch URLs
+
+Portless auto-detects git worktrees and prepends the branch name as a subdomain:
+
+```bash
+# Main worktree
+cd X:/Forge/myapp
+portless run next dev
+# → https://myapp.test
+
+# Linked worktree on branch "fix-ui"
+cd X:/Forge/myapp/.worktrees/fix-ui
+portless run next dev
+# → https://fix-ui.myapp.test
+```
+
+No config — just works. Each worktree gets its own URL automatically, avoiding browser cookie/storage cross-contamination between branches.
+
+## Common Anti-Patterns
+
+```
+BAD:  use portless's spawn mode for production-equivalent local services
+GOOD: use Pattern A (Process Compose supervisor + portless routing)
+
+BAD:  let two different stacks fight over the same TLD
+GOOD: pick TLD per machine, document it; or use different ports
+
+BAD:  hardcode portless URLs in service config (e.g. CORS allowlists)
+GOOD: read PORTLESS_URL env var that portless injects into spawned processes
+      (Pattern D only); or use SERVICE_URL env injection in your supervisor
+
+BAD:  install portless globally without pinning a version
+GOOD: pin version: npm install -g portless@0.13.0; record in your repo
+```
+
+## See Also
+
+- `process-compose-ops` skill for the supervisor side of Pattern A
+- `references/upstream-portless.md` for full CLI reference (auto-port assignment, etc.)
+- `references/tld-selection.md` for picking the right TLD up front

+ 112 - 0
skills/portless-ops/references/tld-selection.md

@@ -0,0 +1,112 @@
+# TLD Selection for portless
+
+The TLD is a per-proxy setting — every alias resolves as `<name>.<tld>`. Choose with care: changing the TLD later means re-registering every alias.
+
+## Quick Reference
+
+| TLD | Resolution | OAuth-safe | Best for |
+|---|---|---|---|
+| `.localhost` (default) | Native in Chrome/Firefox/Edge; needs `/etc/hosts` on Safari | ❌ Rejected by Google/Apple | Solo dev, no OAuth |
+| `.test` | IANA-reserved (RFC 6761) | ✅ Yes | Recommended default — safe everywhere |
+| `.lab` | Not reserved (no DNS collision in practice) | Provider-dependent | Personal/distinctive naming |
+| `.dev` | Google-owned, HSTS-preloaded (forces HTTPS) | ✅ Yes | OAuth-heavy projects |
+| `.app` | Google-owned, HSTS-preloaded | ✅ Yes | Similar to `.dev` |
+| `.local` | mDNS — **conflicts with Bonjour/Avahi** | Provider-dependent | LAN mode only (`--lan`) |
+| Anything you own (e.g. `.local.mycorp.dev`) | Whatever you configure | ✅ Yes | Teams, enterprise |
+
+## Decision Flow
+
+```
+Do you need OAuth (Google/Apple/Facebook)?
+├── Yes
+│   ├── Do you control a real domain? → use a subdomain of it (best)
+│   └── No                            → use .dev or .test (good)
+└── No
+    ├── Solo dev, no special needs   → .test (recommended) or .localhost
+    └── Personal preference           → .lab or any short distinctive TLD
+```
+
+## Detailed Notes
+
+### `.localhost` (default)
+
+- Auto-resolves to `127.0.0.1` in all modern browsers (RFC 6761)
+- **Safari** needs `/etc/hosts` entries — run `portless hosts sync`
+- Rejected by **Google OAuth** (not in their bundled Public Suffix List)
+- Rejected by **Apple** (no localhost or IP addresses at all)
+- Accepted by **Microsoft / GitHub** with caveats
+
+### `.test` (recommended for general use)
+
+- IANA-reserved for testing per RFC 6761
+- No real DNS will ever resolve `.test`, so no collision risk
+- Accepted by every OAuth provider that respects the Public Suffix List
+- Requires `/etc/hosts` entries (portless auto-syncs)
+
+### `.dev` / `.app` (Google-owned)
+
+- Public Suffix List entries — provider-accepted
+- **HSTS-preloaded by Google** — browsers force HTTPS, so plain HTTP doesn't work
+- Portless defaults to HTTPS so this is fine
+- Slight cost: every browser hit issues an HSTS check (negligible in dev)
+
+### `.local` (avoid for non-LAN use)
+
+- mDNS uses `.local` for Bonjour / Avahi auto-discovery
+- Using `--tld local` without `--lan` mode confuses macOS in particular
+- **Only use** when you intend LAN sharing — portless's `--lan` mode actually advertises `<name>.local` over mDNS
+
+### Custom owned domain
+
+The most defensible option for OAuth and team setups:
+
+```bash
+# You own example.com. Set up DNS:
+*.local.example.com   A   127.0.0.1
+
+# Then portless:
+portless proxy start --tld local.example.com
+portless myapp next dev
+# → https://myapp.local.example.com
+```
+
+- OAuth providers see a real, resolvable domain
+- Other devs on your team can resolve too (real DNS, no /etc/hosts edits)
+- Apple's strict server-side resolution check passes
+- Zero risk of accidentally hitting a real domain you don't own
+
+### `.lab` (or any short distinctive TLD)
+
+- Not in the IANA root zone, not reserved
+- Won't ever resolve publicly, so no collision risk in practice
+- Short and memorable for personal use
+- **Won't work for OAuth** — Google/Apple require Public Suffix List domains
+- Good for personal dev when you don't need OAuth or external services
+
+## Change Procedure
+
+If you need to change TLD (e.g. you started on `.localhost`, now need OAuth):
+
+```bash
+# Stop proxy
+portless proxy stop
+
+# Wipe routes (because `portless alias --remove` appends the active TLD,
+# making it impossible to remove old-TLD aliases cleanly)
+rm ~/.portless/routes.json
+
+# Restart with new TLD
+portless proxy start --tld test --port 443
+
+# Re-register aliases against new TLD
+portless alias myapp 8000 --force
+portless alias api    8001 --force
+```
+
+Update any bookmarks, OAuth provider configs, and `NEXTAUTH_URL` / `AUTH_URL` / `BASE_URL` environment variables.
+
+## See Also
+
+- `references/upstream-oauth.md` — per-provider OAuth setup
+- `references/upstream-portless.md` — full CLI reference (search "tld" or "--tld")
+- `references/integration-patterns.md` — combining portless with process supervisors

+ 147 - 0
skills/portless-ops/references/windows-specifics.md

@@ -0,0 +1,147 @@
+# Windows Specifics for portless
+
+Things that bite on Windows but work transparently on macOS/Linux.
+
+## OpenSSL Required for Cert Generation
+
+Portless uses OpenSSL to generate the local CA on first run. Without it:
+
+```
+Error: openssl failed: spawnSync openssl ENOENT
+```
+
+### Fix — Add Git for Windows's bundled OpenSSL to PATH
+
+Git for Windows ships a usable OpenSSL at `C:\Program Files\Git\usr\bin\openssl.exe`. Add it to your user PATH permanently:
+
+```powershell
+$gitBin = "C:\Program Files\Git\usr\bin"
+$current = [Environment]::GetEnvironmentVariable("PATH", "User")
+if ($current -notlike "*$gitBin*") {
+    [Environment]::SetEnvironmentVariable("PATH", "$gitBin;$current", "User")
+}
+
+# Verify
+openssl version
+```
+
+For Task Scheduler / boot-time launches, the PATH must be set in the wrapper script (Task Scheduler runs with minimal PATH by default).
+
+### Alternative — install standalone OpenSSL
+
+```powershell
+winget install -e --id ShiningLight.OpenSSL.Light
+# or
+scoop install openssl
+```
+
+## CA Trust via certutil
+
+`portless trust` calls Windows's `certutil.exe` to add the portless CA to the system trust store. Side effects:
+
+- **Affects browsers using the system store** (Chrome, Edge, Firefox-with-Windows-certs) — they will trust `*.<tld>` certs after `portless trust`
+- **Does NOT affect curl on Windows** — curl ships its own CA bundle and ignores the system store
+- **Does NOT affect Firefox by default** — Firefox uses its own NSS cert store unless you set `security.enterprise_roots.enabled=true` in `about:config`
+
+`portless trust` may prompt the UAC dialog; without admin elevation it may silently fail to add system-wide trust. Run from an elevated PowerShell for reliable installation.
+
+## curl vs Browser Cert Handling
+
+Symptom: `curl https://myapp.test/` returns HTTP code 000, but the browser loads `https://myapp.test/` fine with a green padlock.
+
+Reason: curl uses its own CA bundle, browsers use the OS trust store.
+
+Three workarounds for curl on Windows:
+
+```bash
+# 1. Skip verification (quickest, fine for local dev)
+curl -k https://myapp.test/
+
+# 2. Point curl at portless's CA explicitly
+curl --cacert "$env:USERPROFILE/.portless/ca.pem" https://myapp.test/
+
+# 3. Add the portless CA to curl's bundle (one-time setup)
+# Locate curl's CA bundle (varies by install):
+curl-config --ca   # if curl-config is available
+# Or check $env:CURL_CA_BUNDLE or the bundle at the curl install dir
+
+# Then append portless's CA to it:
+type "$env:USERPROFILE\.portless\ca.pem" >> "C:\path\to\curl\bin\curl-ca-bundle.crt"
+```
+
+For most dev workflows just use `-k` — it's the fastest path.
+
+## Boot Persistence — Task Scheduler
+
+`portless service install` registers a Task Scheduler entry that runs the proxy at system startup. Notes:
+
+- Runs as **SYSTEM** (not your user account) — fine because portless only needs to bind ports and read its own state dir
+- The state dir defaults to `%USERPROFILE%\.portless\` — Task Scheduler running as SYSTEM might not see it. Override with `PORTLESS_STATE_DIR` env if needed.
+- Uninstall: `portless service uninstall` or `portless clean` (also removes the task)
+
+For Process Compose's boot task, see `process-compose-ops` skill's `boot-persistence-windows.md`.
+
+## /etc/hosts on Windows
+
+Windows uses `C:\Windows\System32\drivers\etc\hosts`. `portless hosts sync` writes to it — requires admin elevation.
+
+If portless isn't auto-syncing:
+
+```powershell
+# Check what's in hosts
+notepad C:\Windows\System32\drivers\etc\hosts
+
+# Force a re-sync (run as admin)
+portless hosts sync
+```
+
+## Port 443 Without sudo
+
+On macOS/Linux, binding port 443 requires `sudo` (portless auto-elevates). On Windows, no elevation is required to bind privileged ports for the current user — portless just binds them directly.
+
+Caveat: if **another service is already bound to 443** (IIS, Skype, Caddy from old setup), portless will fail to start with `EADDRINUSE`. Find the culprit:
+
+```powershell
+netstat -ano | findstr ":443 "
+# Look at the PID, then:
+Get-Process -Id <pid>
+```
+
+Stop the conflicting service or pick a different port (`--port 1355`).
+
+## PowerShell 5.1 vs 7+
+
+PowerShell 5.1 (the default Windows PowerShell that ships with Windows) lacks some newer flags that PowerShell 7 has. Examples that bite:
+
+```powershell
+# PS 7+: -SkipCertificateCheck
+Invoke-WebRequest -Uri https://x.lab -SkipCertificateCheck
+# PS 5.1: parameter not recognized → use curl.exe -k instead
+
+# PS 7+: ternary operator
+$x = $foo ? "yes" : "no"
+# PS 5.1: parse error → use if-else
+
+# PS 7+: pipeline parallel
+... | ForEach-Object -Parallel { ... }
+# PS 5.1: -Parallel not available
+```
+
+If a script needs PS 7+ features, the shebang doesn't help on Windows — invoke explicitly with `pwsh` instead of `powershell`:
+
+```powershell
+pwsh -File .\myscript.ps1
+```
+
+## Cleanup
+
+```powershell
+# Stop proxy + uninstall boot task + clear state
+portless clean
+
+# Verify clean state
+Get-NetTCPConnection -LocalPort 443 -State Listen -ErrorAction SilentlyContinue
+# Should return nothing if portless was the only thing on 443
+```
+
+`portless clean` removes the portless CA from the trust store too, so browsers will see warnings again until you re-trust.

+ 142 - 0
skills/portless-ops/scripts/install-portless.ps1

@@ -0,0 +1,142 @@
+<#
+.SYNOPSIS
+    Install a specific version of portless globally with verification.
+
+.DESCRIPTION
+    1. Inspects the published npm tarball BEFORE installing.
+    2. Verifies tarball SHA-512 against the npm registry.
+    3. Scans the package contents for known IOC strings from recent npm
+       supply-chain attacks (TanStack, mini-shai-hulud).
+    4. Confirms no install scripts (preinstall/postinstall) are present.
+    5. Installs globally via npm.
+    6. Verifies version matches what was requested.
+    7. Records the pinned version.
+
+.PARAMETER Version
+    Specific version to install (e.g. "0.13.0"). Required.
+
+.PARAMETER TargetDir
+    Where to record version metadata (a bin/PORTLESS_VERSION file). Optional.
+
+.EXAMPLE
+    .\install-portless.ps1 -Version 0.13.0 -TargetDir X:\my-stack
+#>
+
+[CmdletBinding()]
+param(
+    [Parameter(Mandatory)][string]$Version,
+    [string]$TargetDir = $null
+)
+
+$ErrorActionPreference = 'Stop'
+
+Write-Host "Installing portless@$Version with pre-install audit" -ForegroundColor Cyan
+Write-Host ("=" * 60)
+Write-Host ""
+
+# Step 1 — Inspect tarball without installing
+$tmp = Join-Path $env:TEMP "portless-inspect-$(Get-Random)"
+New-Item -ItemType Directory -Force -Path $tmp | Out-Null
+
+try {
+    Write-Host "[1/6] Downloading tarball..." -ForegroundColor Yellow
+    $tarballUrl = "https://registry.npmjs.org/portless/-/portless-$Version.tgz"
+    $tarball = Join-Path $tmp 'portless.tgz'
+    Invoke-WebRequest -Uri $tarballUrl -OutFile $tarball
+
+    Write-Host "[2/6] Verifying SHA-512 against npm registry..." -ForegroundColor Yellow
+    $meta = npm view portless@$Version --json 2>$null | ConvertFrom-Json
+    $expectedIntegrity = $meta.dist.integrity  # like "sha512-..."
+    if (-not $expectedIntegrity) {
+        throw "Could not fetch published integrity hash for portless@$Version"
+    }
+
+    # npm's integrity is "sha512-<base64>". Compare with our computed value.
+    $actualBytes = [System.Security.Cryptography.SHA512]::Create().ComputeHash([IO.File]::ReadAllBytes($tarball))
+    $actualB64 = [Convert]::ToBase64String($actualBytes)
+    $expectedB64 = $expectedIntegrity -replace '^sha512-', ''
+    if ($actualB64 -ne $expectedB64) {
+        throw "TARBALL SHA-512 MISMATCH - aborting. Expected: $expectedB64. Got: $actualB64."
+    }
+    Write-Host "  Match: sha512-$actualB64" -ForegroundColor Green
+
+    Write-Host "[3/6] Extracting + auditing contents..." -ForegroundColor Yellow
+    Push-Location $tmp
+    tar -xzf portless.tgz
+    Pop-Location
+
+    $pkgDir = Join-Path $tmp 'package'
+
+    # Scan for install scripts in package.json
+    $pkgJson = Get-Content (Join-Path $pkgDir 'package.json') | ConvertFrom-Json
+    $scripts = $pkgJson.scripts.PSObject.Properties.Name
+    $installScripts = @('preinstall', 'install', 'postinstall', 'prepare')
+    $foundInstallScripts = $scripts | Where-Object { $_ -in $installScripts }
+
+    if ($foundInstallScripts) {
+        Write-Warning "Package has install scripts: $($foundInstallScripts -join ', ')"
+        Write-Warning "Review these before installing!"
+    } else {
+        Write-Host "  ✓ No install scripts (preinstall/install/postinstall/prepare)"
+    }
+
+    # Check runtime dependencies (should be empty for portless)
+    if ($pkgJson.dependencies -and $pkgJson.dependencies.PSObject.Properties.Count -gt 0) {
+        Write-Warning "Package has runtime deps: $($pkgJson.dependencies.PSObject.Properties.Name -join ', ')"
+    } else {
+        Write-Host "  ✓ Zero runtime dependencies"
+    }
+
+    # Scan for known IOC strings from recent attacks
+    Write-Host "[4/6] Scanning for known supply-chain IOC strings..." -ForegroundColor Yellow
+    $iocPatterns = @(
+        'getsession.org', 'masscan.cloud', 'git-tanstack',
+        'router_init', 'router_runtime',
+        'EveryBoiWeBuildIsAWormyBoi',
+        'claude@users.noreply.github.com',
+        'filev2.getsession',
+        '@tanstack/setup',
+        'gh-token-monitor',
+        '/proc/self/environ', '.claude/settings.json'
+    )
+
+    $hits = @()
+    Get-ChildItem -Recurse -File $pkgDir | ForEach-Object {
+        $content = Get-Content $_.FullName -Raw -ErrorAction SilentlyContinue
+        foreach ($pattern in $iocPatterns) {
+            if ($content -and $content.Contains($pattern)) {
+                $hits += "$($_.Name): $pattern"
+            }
+        }
+    }
+    if ($hits) {
+        throw "IOC MATCH FOUND - aborting:`n$($hits -join "`n")"
+    }
+    Write-Host "  ✓ Zero IOC matches"
+
+    # Step 5 — Install via npm
+    Write-Host "[5/6] npm install -g portless@$Version..." -ForegroundColor Yellow
+    npm install -g portless@$Version 2>&1 | Tee-Object -Variable npmOutput | Out-Host
+
+    # Step 6 — Verify installed version
+    Write-Host "[6/6] Verifying installed version..." -ForegroundColor Yellow
+    $installed = (portless --version).Trim()
+    if ($installed -ne $Version) {
+        throw "Version mismatch: requested $Version, installed $installed"
+    }
+    Write-Host "  Installed: $installed" -ForegroundColor Green
+
+    # Optional: record in target dir
+    if ($TargetDir) {
+        $binDir = Join-Path $TargetDir 'bin'
+        New-Item -ItemType Directory -Force -Path $binDir | Out-Null
+        $Version | Out-File -FilePath (Join-Path $binDir 'PORTLESS_VERSION') -NoNewline -Encoding ascii
+        Write-Host "  Recorded version in: $binDir\PORTLESS_VERSION"
+    }
+
+    Write-Host ""
+    Write-Host "Done. portless@$Version installed and audited." -ForegroundColor Green
+
+} finally {
+    Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
+}

+ 85 - 0
skills/portless-ops/scripts/reset-state.ps1

@@ -0,0 +1,85 @@
+<#
+.SYNOPSIS
+    Cleanly reset portless state (stop proxy, wipe routes, restart with new config).
+
+.DESCRIPTION
+    Use this when:
+    - Changing TLDs (portless alias --remove appends active TLD, so cleaning
+      old aliases is impossible without a full reset)
+    - Routes are corrupted or have stale entries
+    - You want to start fresh with a different proxy port / TLS mode
+
+.PARAMETER Tld
+    TLD to start the new proxy with. Defaults to current saved value.
+
+.PARAMETER Port
+    Proxy port. Defaults to 443.
+
+.PARAMETER PreserveCa
+    Keep the existing local CA. Default true (avoids re-trusting in browsers).
+
+.PARAMETER Aliases
+    Hashtable of name → port pairs to re-register after reset. Optional.
+
+.EXAMPLE
+    # Change TLD and re-register aliases
+    .\reset-state.ps1 -Tld test -Aliases @{
+        myapp = 8000
+        api   = 8001
+        db    = 5432
+    }
+#>
+
+[CmdletBinding()]
+param(
+    [string]$Tld,
+    [int]$Port = 443,
+    [bool]$PreserveCa = $true,
+    [hashtable]$Aliases = @{}
+)
+
+$ErrorActionPreference = 'Stop'
+
+Write-Host "Resetting portless state" -ForegroundColor Cyan
+
+# 1. Stop proxy
+Write-Host "[1/4] Stopping proxy..."
+portless proxy stop 2>&1 | Out-Null
+
+# 2. Wipe routes.json
+$routesFile = Join-Path $env:USERPROFILE '.portless\routes.json'
+if (Test-Path $routesFile) {
+    Write-Host "[2/4] Removing routes.json..."
+    Remove-Item $routesFile -Force
+} else {
+    Write-Host "[2/4] No routes.json to remove."
+}
+
+# Optional: nuke CA + everything (use `portless clean` for nuclear option)
+if (-not $PreserveCa) {
+    Write-Host "  Also clearing CA + /etc/hosts (will need re-trust)..."
+    portless clean 2>&1 | Out-Null
+}
+
+# 3. Start proxy with new TLD/port
+Write-Host "[3/4] Starting proxy: --tld $Tld --port $Port..."
+$args = @("proxy", "start", "--port", $Port)
+if ($Tld) { $args += @("--tld", $Tld) }
+& portless @args
+Start-Sleep -Seconds 2
+
+# 4. Re-register aliases if provided
+if ($Aliases.Count -gt 0) {
+    Write-Host "[4/4] Re-registering $($Aliases.Count) aliases..."
+    foreach ($name in $Aliases.Keys) {
+        $port = $Aliases[$name]
+        Write-Host "  alias: $name → $port"
+        & portless alias $name $port --force 2>&1 | Out-Null
+    }
+} else {
+    Write-Host "[4/4] No aliases to register."
+}
+
+Write-Host ""
+Write-Host "Done. Current state:" -ForegroundColor Green
+portless list

+ 59 - 0
skills/portless-ops/scripts/sync-aliases-from-yaml.ps1

@@ -0,0 +1,59 @@
+<#
+.SYNOPSIS
+    Derive portless aliases from a process-compose.yaml and register them.
+
+.DESCRIPTION
+    Reads every process from process-compose.yaml, looks at its
+    readiness_probe.http_get.port, and registers a portless alias mapping
+    the process name to that port.
+
+    Idempotent: uses --force to overwrite existing aliases.
+
+    Requires yq (https://github.com/mikefarah/yq) on PATH.
+
+.PARAMETER YamlPath
+    Path to process-compose.yaml. Defaults to ./process-compose.yaml.
+
+.EXAMPLE
+    .\sync-aliases-from-yaml.ps1
+    .\sync-aliases-from-yaml.ps1 -YamlPath X:\my-stack\process-compose.yaml
+#>
+
+[CmdletBinding()]
+param(
+    [string]$YamlPath = (Join-Path (Get-Location) 'process-compose.yaml')
+)
+
+$ErrorActionPreference = 'Stop'
+
+if (-not (Test-Path $YamlPath)) {
+    throw "YAML file not found: $YamlPath"
+}
+
+if (-not (Get-Command yq -ErrorAction SilentlyContinue)) {
+    throw "yq not found on PATH. Install: scoop install yq"
+}
+
+Write-Host "Syncing aliases from: $YamlPath" -ForegroundColor Cyan
+Write-Host ""
+
+$services = & yq '.processes | keys | .[]' $YamlPath
+$count = 0
+
+foreach ($svc in $services) {
+    $port = & yq ".processes.$svc.readiness_probe.http_get.port" $YamlPath
+
+    if (-not $port -or $port -eq "null") {
+        Write-Host "  $svc -- no http_get probe, skipping (background process?)"
+        continue
+    }
+
+    Write-Host "  $svc → $port"
+    & portless alias $svc $port --force 2>&1 | Out-Null
+    $count++
+}
+
+Write-Host ""
+Write-Host "Registered $count aliases." -ForegroundColor Green
+Write-Host ""
+portless list

+ 23 - 0
skills/process-compose-ops/SKILL.md

@@ -287,6 +287,29 @@ BAD:  upgrade PC by running an installer (npm install -g, scoop install, brew in
 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)

+ 33 - 0
skills/process-compose-ops/assets/cron-job.yaml

@@ -0,0 +1,33 @@
+# 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"

+ 74 - 0
skills/process-compose-ops/assets/django-with-companions.yaml

@@ -0,0 +1,74 @@
+# 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"

+ 34 - 0
skills/process-compose-ops/assets/go-binary-service.yaml

@@ -0,0 +1,34 @@
+# 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"

+ 36 - 0
skills/process-compose-ops/assets/python-uvicorn.yaml

@@ -0,0 +1,36 @@
+# 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"

+ 48 - 0
skills/process-compose-ops/assets/tunnel-with-dependency.yaml

@@ -0,0 +1,48 @@
+# 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"

+ 190 - 0
skills/process-compose-ops/references/boot-persistence-windows.md

@@ -0,0 +1,190 @@
+# 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
+```

+ 167 - 0
skills/process-compose-ops/references/dependency-patterns.md

@@ -0,0 +1,167 @@
+# 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: "X:/Forge/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: "X:/Forge/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

+ 165 - 0
skills/process-compose-ops/references/probe-patterns.md

@@ -0,0 +1,165 @@
+# 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/

+ 190 - 0
skills/process-compose-ops/references/schema-reference.md

@@ -0,0 +1,190 @@
+# 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.

+ 115 - 0
skills/process-compose-ops/references/supply-chain-verification.md

@@ -0,0 +1,115 @@
+# 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

+ 122 - 0
skills/process-compose-ops/references/tui-shortcuts.md

@@ -0,0 +1,122 @@
+# 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/

+ 74 - 0
skills/process-compose-ops/scripts/boot-start.template.ps1

@@ -0,0 +1,74 @@
+<#
+.SYNOPSIS
+    TEMPLATE: Boot-time launcher for Process Compose with explicit PATH setup.
+
+.DESCRIPTION
+    Copy to <your-stack>/scripts/boot-start.ps1 and adapt the $pathParts array
+    for your machine. Invoked by Task Scheduler at boot (see
+    boot-task-install.template.ps1).
+
+    Why a wrapper: Task Scheduler runs with a minimal PATH, so services that
+    rely on python/uv/openssl/cloudflared would otherwise fail at boot.
+#>
+
+[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
+
+# ─── CUSTOMIZE PATH HERE ───────────────────────────────────────────────────
+# Add directories for binaries that your managed services need (python, uv,
+# git tools, cloudflared, language SDKs, etc.). Test by running this script
+# manually before relying on it at boot.
+$pathParts = @(
+    "$root\bin"                                                                  # PC + 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.
+    # Add more as needed:
+    # "C:\Program Files (x86)\cloudflared"
+    # "C:\Program Files\nodejs"
+    "C:\Windows\System32"
+    "C:\Windows"
+    $env:PATH
+)
+$env:PATH = ($pathParts -join ';')
+
+"[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')] PATH set, $($pathParts.Count) entries" | Out-File -FilePath $bootLog -Append
+
+# ─── OPTIONAL: load secrets from gitignored .env ──────────────────────────
+$envFile = Join-Path $root '.env'
+if (Test-Path $envFile) {
+    "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')] Loading .env" | Out-File -FilePath $bootLog -Append
+    Get-Content $envFile | ForEach-Object {
+        if ($_ -match '^\s*([A-Z_]+)\s*=\s*(.+?)\s*$') {
+            [Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process')
+        }
+    }
+}
+
+# ─── OPTIONAL: explicitly UNSET env vars that conflict with services ──────
+# Example: if a daemon enforces OAuth-only and refuses 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
+
+# ─── LAUNCH ───────────────────────────────────────────────────────────────
+# -p 8888    API port (avoid common collisions on 8080; pick what's free)
+# -t=false   no TUI (headless background 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

+ 92 - 0
skills/process-compose-ops/scripts/boot-task-install.template.ps1

@@ -0,0 +1,92 @@
+<#
+.SYNOPSIS
+    TEMPLATE: Register a Windows Task Scheduler entry to launch Process Compose at boot.
+
+.DESCRIPTION
+    Copy to <your-stack>/scripts/boot-task-install.ps1, customise $taskName, and
+    run as Administrator.
+
+    Pairs with boot-start.template.ps1 (the wrapper script that this task launches).
+    Uses LogonType S4U so the task runs at boot without storing a password or
+    requiring interactive logon.
+
+.EXAMPLE
+    # In elevated PowerShell:
+    .\boot-task-install.ps1
+#>
+
+[CmdletBinding()]
+param()
+
+$ErrorActionPreference = 'Stop'
+
+# Admin check
+$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
+$principal   = New-Object Security.Principal.WindowsPrincipal($currentUser)
+if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
+    throw "Run as Administrator. Task Scheduler creation requires it."
+}
+
+$scriptDir  = Split-Path -Parent $MyInvocation.MyCommand.Path
+$root       = (Resolve-Path (Join-Path $scriptDir '..')).Path
+$bootScript = Join-Path $scriptDir 'boot-start.ps1'
+
+if (-not (Test-Path $bootScript)) {
+    throw "boot-start.ps1 not found at $bootScript - copy boot-start.template.ps1 and customise"
+}
+
+# ─── CUSTOMIZE TASK NAME ──────────────────────────────────────────────────
+$taskName = "ProcessCompose-MyStack"   # rename per project
+
+Write-Host "Registering Task Scheduler entry: $taskName" -ForegroundColor Cyan
+
+# Idempotent: remove existing if present
+$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
+if ($existing) {
+    Write-Host "  Removing existing task..."
+    Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
+}
+
+# Launch via PowerShell, hidden window, running boot-start.ps1
+$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: runs at boot as current user, no password stored, no interactive logon needed.
+# Sufficient for local services with no GUI or network-share requirements.
+$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."
+
+Write-Host ""
+Write-Host "Done. Task registered." -ForegroundColor Green
+Write-Host ""
+Write-Host "Verify:"
+Write-Host "  Get-ScheduledTask -TaskName '$taskName'"
+Write-Host ""
+Write-Host "Test before reboot:"
+Write-Host "  Start-ScheduledTask -TaskName '$taskName'"
+Write-Host "  Start-Sleep 10"
+Write-Host "  process-compose -p 8888 process list"
+Write-Host ""
+Write-Host "Uninstall:"
+Write-Host "  Unregister-ScheduledTask -TaskName '$taskName' -Confirm:`$false"

+ 107 - 0
skills/process-compose-ops/scripts/install-process-compose.ps1

@@ -0,0 +1,107 @@
+<#
+.SYNOPSIS
+    Download, verify, and install a specific Process Compose version on Windows.
+
+.DESCRIPTION
+    1. Downloads the release zip and checksums file from GitHub.
+    2. Verifies SHA-256 against the published checksums.txt.
+    3. Extracts the .exe.
+    4. Records the .exe's own hash for future re-verification.
+    5. Writes VERIFICATION.md.
+
+    The result is a verified binary in <target>/bin/, ready to commit to your repo.
+
+.PARAMETER Version
+    Version tag (e.g. "v1.110.0"). Required — never auto-pick latest.
+
+.PARAMETER TargetDir
+    Where to put bin/. Defaults to current directory.
+
+.EXAMPLE
+    .\install-process-compose.ps1 -Version v1.110.0 -TargetDir X:\my-stack
+#>
+
+[CmdletBinding()]
+param(
+    [Parameter(Mandatory)][string]$Version,
+    [string]$TargetDir = (Get-Location).Path
+)
+
+$ErrorActionPreference = 'Stop'
+
+$binDir = Join-Path $TargetDir 'bin'
+New-Item -ItemType Directory -Force -Path $binDir | Out-Null
+
+$base = "https://github.com/F1bonacc1/process-compose/releases/download/$Version"
+$zipName = "process-compose_windows_amd64.zip"
+$checksumsName = "process-compose_checksums.txt"
+
+$zipPath = Join-Path $binDir $zipName
+$checksumsPath = Join-Path $binDir $checksumsName
+
+Write-Host "Downloading Process Compose $Version..." -ForegroundColor Cyan
+Invoke-WebRequest -Uri "$base/$zipName"       -OutFile $zipPath
+Invoke-WebRequest -Uri "$base/$checksumsName" -OutFile $checksumsPath
+
+Write-Host "Verifying SHA-256..." -ForegroundColor Cyan
+$expected = (Select-String -Pattern $zipName -Path $checksumsPath).Line.Split()[0].ToLower()
+$actual   = (Get-FileHash $zipPath -Algorithm SHA256).Hash.ToLower()
+
+if ($expected -ne $actual) {
+    Remove-Item $zipPath -Force
+    throw "CHECKSUM MISMATCH - aborting install. Expected $expected, got $actual."
+}
+Write-Host "  Match: $actual" -ForegroundColor Green
+
+Write-Host "Extracting..." -ForegroundColor Cyan
+Expand-Archive $zipPath -DestinationPath $binDir -Force
+
+# Clean up bundled docs (we don't ship upstream's LICENSE/README in our repo)
+Remove-Item (Join-Path $binDir 'LICENSE')   -ErrorAction SilentlyContinue
+Remove-Item (Join-Path $binDir 'README.md') -ErrorAction SilentlyContinue
+Remove-Item $zipPath -Force
+
+# Record version
+$Version | Out-File -FilePath (Join-Path $binDir 'VERSION') -NoNewline -Encoding ascii
+
+# Record the .exe's hash for re-verification
+$exePath = Join-Path $binDir 'process-compose.exe'
+$exeHash = (Get-FileHash $exePath -Algorithm SHA256).Hash.ToLower()
+$exeHash | Out-File -FilePath (Join-Path $binDir 'EXE_HASH') -NoNewline -Encoding ascii
+
+Write-Host "Runtime check..." -ForegroundColor Cyan
+$versionOutput = & $exePath version 2>&1 | Where-Object { $_ -notmatch 'level.*debug' }
+Write-Host $versionOutput
+
+# Write VERIFICATION.md
+$verifPath = Join-Path $binDir 'VERIFICATION.md'
+@"
+# Process Compose Binary Verification
+
+## $Version
+
+| Field | Value |
+|---|---|
+| Pinned | $(Get-Date -Format 'yyyy-MM-dd') |
+| Source | https://github.com/F1bonacc1/process-compose/releases/tag/$Version |
+| ZIP SHA-256 | ``$actual`` |
+| EXE SHA-256 | ``$exeHash`` |
+| Runtime check | ``$($versionOutput -join '; ')`` |
+
+Trust anchor: GitHub Releases (HTTPS, requires repo write access to tamper).
+Note: Process Compose releases are not GPG-signed.
+
+## Re-verify
+
+``````powershell
+`$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" }
+``````
+"@ | Out-File -FilePath $verifPath -Encoding utf8
+
+Write-Host ""
+Write-Host "Done. Binary committed to: $binDir" -ForegroundColor Green
+Write-Host "Files: process-compose.exe, $checksumsName, VERSION, EXE_HASH, VERIFICATION.md"
+Write-Host ""
+Write-Host "Next: git add bin/; git commit -m 'feat: pin process-compose $Version, verified'"

+ 59 - 0
skills/process-compose-ops/scripts/verify-binary.ps1

@@ -0,0 +1,59 @@
+<#
+.SYNOPSIS
+    Re-verify SHA-256 of the committed process-compose.exe against recorded EXE_HASH.
+
+.DESCRIPTION
+    Run periodically (e.g. monthly, or as a pre-commit hook). Fails loud on mismatch.
+
+.PARAMETER BinDir
+    Path to the bin/ directory containing process-compose.exe and EXE_HASH.
+    Defaults to ./bin or ../bin.
+
+.EXAMPLE
+    .\verify-binary.ps1
+#>
+
+[CmdletBinding()]
+param(
+    [string]$BinDir = $null
+)
+
+$ErrorActionPreference = 'Stop'
+
+# Auto-detect bin/
+if (-not $BinDir) {
+    $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+    foreach ($candidate in @(
+        (Join-Path $scriptDir '..\bin'),
+        (Join-Path (Get-Location) 'bin')
+    )) {
+        if (Test-Path (Join-Path $candidate 'process-compose.exe')) {
+            $BinDir = (Resolve-Path $candidate).Path
+            break
+        }
+    }
+}
+
+if (-not $BinDir -or -not (Test-Path $BinDir)) {
+    throw "bin/ directory not found - pass -BinDir explicitly"
+}
+
+$exePath  = Join-Path $BinDir 'process-compose.exe'
+$hashFile = Join-Path $BinDir 'EXE_HASH'
+
+if (-not (Test-Path $exePath))  { throw "process-compose.exe not found in $BinDir" }
+if (-not (Test-Path $hashFile)) { throw "EXE_HASH not found in $BinDir (run install-process-compose.ps1 to create it)" }
+
+$expected = (Get-Content $hashFile -Raw).Trim().ToLower()
+$actual   = (Get-FileHash $exePath -Algorithm SHA256).Hash.ToLower()
+
+Write-Host "Verifying: $exePath" -ForegroundColor Cyan
+Write-Host "  Expected: $expected"
+Write-Host "  Actual:   $actual"
+
+if ($expected -ne $actual) {
+    Write-Host "  Status:   MISMATCH" -ForegroundColor Red
+    throw "BINARY VERIFICATION FAILED - do not trust this binary"
+}
+
+Write-Host "  Status:   OK" -ForegroundColor Green