For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans for inline execution. This plan touches one live host and should be executed sequentially.
Goal: Convert kwiss-cosmictaco into the North OS interactive dev server with /home/kwiss/workspace/north-os and wildcard DNS routing for *.dev.heynorth.dev.
Architecture: Stop but preserve MAAF so host ports 80/443 are free. Install standard Caddy, generate explicit per-project hostnames from /home/kwiss/workspace/*/.devproxy.json, copy North OS into ~/workspace/north-os, and run the dev stack in tmux. Caddy issues normal per-host certificates via HTTP-01; no Cloudflare API token or wildcard certificate is required.
Tech Stack: Ubuntu 24.04, Tailscale SSH, Docker Compose, Bun 1.3.10-compatible runtime, Caddy 2.x, Python 3 for devproxy-sync, systemd timer, tmux.
kwiss-cosmictaco, SSH alias maaf, public IP 151.115.99.39./home/kwiss/workspace/north-os.*.dev.heynorth.dev A 151.115.99.39; optional landing host is dev.heynorth.dev A 151.115.99.39.80 and 443; Traefik must not run on those ports at the same time.deploy/preprod/install.sh, no north-os-preprod.target, no preprod database, no apex heynorth.dev preprod service.| Path | Action | Responsibility |
|---|---|---|
/home/kwiss/workspace | Create remote dir | Root for interactive dev projects. |
/home/kwiss/workspace/north-os | Create/copy remote repo | North OS working tree. |
/usr/local/bin/devproxy-sync | Create remote script | Scans workspace and writes generated Caddy routes. |
/etc/caddy/Caddyfile | Create/replace remote config | Caddy global config and import root. |
/etc/caddy/sites.d/devproxy.generated.caddyfile | Generated remote file | Workspace route table. |
/etc/systemd/system/devproxy-sync.service | Create remote unit | Runs the sync script once. |
/etc/systemd/system/devproxy-sync.timer | Create remote timer | Runs sync every minute. |
/var/www/dev-fallback/index.html | Create remote page | Fallback when no local dev process is running. |
/opt/maaf/docker/docker-compose.prod.yml | Read/operate only | Stop MAAF without deleting data. |
Status: already executed during this session. Backup directory: /home/kwiss/migration-backups/dev-server-2026-07-07.
Observed: host PostgreSQL is active but only default databases exist: postgres, template0, template1.
Files: operate on /opt/maaf/docker/docker-compose.prod.yml.
Produces: host ports 80/443 free for Caddy; MAAF rollback remains possible.
ssh maaf 'cd /opt/maaf/docker && docker compose -f docker-compose.prod.yml stop'Expected: MAAF containers stop; volumes remain.
ssh maaf 'docker update --restart=no maaf-traefik maaf-postgres maaf-web maaf-client maaf-audio-worker'Expected: each container name is printed by Docker.
ssh maaf 'ss -tulpn | grep -E ":80|:443" || true'Expected: no listener on :80 or :443.
ssh maaf 'cat > /home/kwiss/migration-backups/dev-server-2026-07-07/ROLLBACK-MAAF.txt <<"EOF"
cd /opt/maaf/docker
docker compose -f docker-compose.prod.yml up -d
EOF'Expected: rollback note exists.
Files: create/modify /etc/caddy/Caddyfile and Caddy package/service files.
Produces: Caddy can serve explicit generated hosts such as north-os-web.dev.heynorth.dev.
ssh maaf 'sudo apt-get update && sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl ca-certificates gnupg && \
curl -1sLf https://dl.cloudsmith.io/public/caddy/stable/gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg && \
curl -1sLf https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt | sudo tee /etc/apt/sources.list.d/caddy-stable.list >/dev/null && \
sudo apt-get update && sudo apt-get install -y caddy && caddy version'Expected: Caddy version prints.
ssh maaf 'sudo mkdir -p /etc/caddy/sites.d /var/www/dev-fallback; sudo chmod 755 /etc/caddy /etc/caddy/sites.d /var/www/dev-fallback'Expected: directories exist.
ssh maaf 'sudo tee /etc/caddy/Caddyfile >/dev/null <<"EOF"
{
email admin@heynorth.dev
}
import /etc/caddy/sites.d/*.caddyfile
EOF
sudo caddy validate --config /etc/caddy/Caddyfile'Expected: configuration validates.
Files: create /usr/local/bin/devproxy-sync, /var/www/dev-fallback/index.html, systemd service/timer.
ssh maaf 'sudo tee /var/www/dev-fallback/index.html >/dev/null <<"EOF"
North OS dev server
North OS dev server
The requested dev process is not running yet.
Start it in /home/kwiss/workspace/north-os, then reload.
EOF
sudo chmod 644 /var/www/dev-fallback/index.html'Expected: fallback page exists.
ssh maaf 'sudo tee /usr/local/bin/devproxy-sync >/dev/null <<"PY"
#!/usr/bin/env python3
import json
import re
import subprocess
from pathlib import Path
WORKSPACE = Path("/home/kwiss/workspace")
OUT = Path("/etc/caddy/sites.d/devproxy.generated.caddyfile")
DOMAIN = "dev.heynorth.dev"
FALLBACK = "/var/www/dev-fallback"
NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,62}$")
HEADER = "# Generated by /usr/local/bin/devproxy-sync. Do not edit.\n\n"
def slug(name: str) -> str:
s = re.sub(r"[^a-z0-9-]+", "-", name.lower()).strip("-")
s = re.sub(r"-+", "-", s)
if not s or not NAME_RE.match(s):
raise ValueError(f"unsafe project/app slug: {name!r} -> {s!r}")
return s
def route_block(hosts, port):
host_list = ", ".join(hosts)
return f"""{host_list} {{
encode gzip zstd
handle /health {{
respond "ok" 200
}}
reverse_proxy localhost:{port} {{
flush_interval -1
transport http {{
dial_timeout 2s
}}
}}
handle_errors {{
root * {FALLBACK}
file_server
}}
}}
"""
def fallback_block():
return f"""{DOMAIN} {{
encode gzip zstd
root * {FALLBACK}
file_server
}}
"""
def load_routes():
routes = []
if not WORKSPACE.exists():
return routes
for project_dir in sorted(p for p in WORKSPACE.iterdir() if p.is_dir()):
project = slug(project_dir.name)
config_path = project_dir / ".devproxy.json"
if not config_path.exists():
routes.append(([f"{project}.{DOMAIN}"], 3000))
continue
data = json.loads(config_path.read_text())
if not isinstance(data, dict):
raise ValueError(f"{config_path} must contain a JSON object")
for app, port in sorted(data.items()):
app_slug = slug(str(app))
if not isinstance(port, int) or port < 1 or port > 65535:
raise ValueError(f"{config_path}: {app} has invalid port {port!r}")
hosts = [f"{project}-{app_slug}.{DOMAIN}"]
if app_slug == "web":
hosts.insert(0, f"{project}.{DOMAIN}")
routes.append((hosts, port))
return routes
def main():
content = HEADER + fallback_block()
for hosts, port in load_routes():
content += route_block(hosts, port)
old = OUT.read_text() if OUT.exists() else ""
if old == content:
return
tmp = OUT.with_suffix(".tmp")
tmp.write_text(content)
tmp.chmod(0o644)
subprocess.run(["caddy", "validate", "--config", "/etc/caddy/Caddyfile"], check=True)
tmp.replace(OUT)
subprocess.run(["systemctl", "reload", "caddy"], check=True)
if __name__ == "__main__":
main()
PY
sudo chmod 0755 /usr/local/bin/devproxy-sync'Expected: script is executable.
ssh maaf 'sudo tee /etc/systemd/system/devproxy-sync.service >/dev/null <<"EOF"
[Unit]
Description=Generate Caddy devproxy routes from /home/kwiss/workspace
After=caddy.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/devproxy-sync
EOF
sudo tee /etc/systemd/system/devproxy-sync.timer >/dev/null <<"EOF"
[Unit]
Description=Run devproxy-sync every minute
[Timer]
OnBootSec=20s
OnUnitActiveSec=60s
AccuracySec=5s
Unit=devproxy-sync.service
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable devproxy-sync.timer'Expected: timer enabled.
ssh maaf 'mkdir -p /home/kwiss/workspace'rsync -az --delete --exclude .git --exclude node_modules --exclude .next --exclude .turbo --exclude dist --exclude .env --exclude .env.local transformative_studio/north_os/ maaf:/home/kwiss/workspace/north-os/ssh maaf 'if ! command -v bun >/dev/null 2>&1; then curl -fsSL https://bun.sh/install | bash; fi; /home/kwiss/.bun/bin/bun --version'ssh maaf 'cd /home/kwiss/workspace/north-os && /home/kwiss/.bun/bin/bun install'ssh maaf 'sudo systemctl enable --now caddy && systemctl is-active caddy'Expected: active.
ssh maaf 'sudo /usr/local/bin/devproxy-sync && sudo cat /etc/caddy/sites.d/devproxy.generated.caddyfile'Expected: generated file includes north-os.dev.heynorth.dev, north-os-web.dev.heynorth.dev, and north-os-clicky.dev.heynorth.dev.
ssh maaf 'sudo systemctl enable --now devproxy-sync.timer && systemctl list-timers devproxy-sync.timer --no-pager'ssh maaf 'cd /home/kwiss/workspace/north-os && docker compose up -d postgres redis'ssh maaf 'tmux has-session -t north-os 2>/dev/null || tmux new-session -d -s north-os -c /home/kwiss/workspace/north-os -n dev'ssh maaf 'tmux send-keys -t north-os:dev "cd /home/kwiss/workspace/north-os && /home/kwiss/.bun/bin/bun run dev" C-m'ssh maaf 'tmux capture-pane -pt north-os:dev -S -120'dig +short dev.heynorth.dev
dig +short north-os.dev.heynorth.devExpected after DNS update: 151.115.99.39. Before DNS update, use curl --resolve against 151.115.99.39.
curl -I --max-time 20 https://dev.heynorth.devcurl -I --max-time 20 https://north-os-web.dev.heynorth.devssh maaf 'systemctl list-unit-files | grep north-os-preprod || true; docker ps --format "{{.Names}}" | grep preprod || true'Expected: no output.
ssh maaf 'sudo ufw deny 5432/tcp || true; sudo ufw status verbose'ssh maaf 'cat > /home/kwiss/workspace/README-dev-server.txt <<"EOF"
North OS dev server
Repo: /home/kwiss/workspace/north-os
Run: tmux attach -t north-os
Main URL: https://north-os.dev.heynorth.dev
Routes: generated from /home/kwiss/workspace/*/.devproxy.json by /usr/local/bin/devproxy-sync
Caddy: systemctl status caddy
Route sync: systemctl status devproxy-sync.timer
MAAF rollback: cd /opt/maaf/docker && docker compose -f docker-compose.prod.yml up -d
EOF'*.dev.heynorth.dev; every North OS path uses /home/kwiss/workspace/north-os; every remote command targets SSH alias maaf.