A walkthrough of the dev-server cleanup, the concepts behind it, and the workflow you'll use from here.
Same pipeline, two ways to drive it. Pick whichever feels natural in the moment.
The three workflow scripts live at /home/agentruntime/bin/ and run as the agentruntime user. If you're logged in as root, switch over first: sudo -u agentruntime -i.
# start a new feature (creates branch, folder, subdomain, SSL cert, pm2 process)
wf-new my-feature
# pull the latest from dev into the feature's branch and restart its subdomain
wf-sync my-feature
# tear it all down after the PR is merged
wf-retire my-feature
Each script will pause before any sudo command and ask before continuing — so it doesn't surprise you with system-level changes.
wf-retire — nginx cleanup (run as root)The aigent doesn't have permission to delete files in /etc/nginx/ (intentional, for safety). After wf-retire finishes, switch to your root shell and run:
sudo rm -f /etc/nginx/sites-enabled/my-feature.mikvehworks
sudo rm -f /etc/nginx/sites-available/my-feature.mikvehworks
sudo nginx -t && sudo systemctl reload nginx
Or just say it in plain English. Each phrase maps directly to one script:
wf-new my-feature
wf-sync my-feature
wf-retire my-feature
The aigent will pause before sudo and ask. Automation does the work, you stay in control of when it actually fires.
In one paragraph.
You wanted multiple branches running on multiple subdomains at the same time, all on this one dev server. It was already working — sort of — but fragile and tangled. We cleaned it up and built an automation pipeline on top. Each feature now lives in its own folder, on its own subdomain, on its own port, with auto-restart and reboot survival. The four-phrase workflow is installed, tested twice end-to-end, and production-ready. The next time you start a feature, you just say "set up a new feature called X" and 30 seconds later it's live.
Five subdomains, five folders, five ports. Each row is independent.
| URL | Folder | Branch | Port |
|---|---|---|---|
| mikvehworks.com | /opt/watchflow-dev | dev | 3009 |
| invoices.mikvehworks.com | /home/agentruntime/wf-invoice | invoice-module | 3011 |
| activity.mikvehworks.com | /home/agentruntime/wf-activity-log | activity-log | 3013 |
| reporting.mikvehworks.com | /home/agentruntime/wf-reporting | reporting | 3015 |
| marketing.mikvehworks.com | /home/agentruntime/wf-marketing | marketing | 3017 |
Edit code in a folder → changes show on its URL. Other rows keep running undisturbed.
Three problems we found and fixed today.
Your subdomain servers were started by hand using nohup — a Linux command that means "run this and don't kill it when I log off." Works fine until something goes wrong:
Earlier in the session, root-Claude (running as the root user) made a mistake — it created four duplicate copies of your code under /opt/watchflow-*. Each copy needs its own node_modules/ folder, weighing about 4 GB (mostly because of onnxruntime-node and Playwright's bundled browsers). That's roughly 17 GB of pure waste, on a 96 GB disk that was already crowded.
You have two Linux user accounts on this server: root (system admin) and agentruntime (your aigent app). Each had its own Claude session, its own pm2, its own folders. They were stepping on each other's toes — both trying to run the same subdomains.
Five concrete changes, in order.
node_modules in /opt/watchflow-dev (it was a self-referencing symlink — pointing at itself, infinite loop).pm2 instead.Plain definitions for the moving parts.
A branch is a version of your code that lives on GitHub. A folder is where that code is downloaded on a computer.
You can download the same repo into multiple folders — one per branch you want to work on at the same time. That's called a git worktree. All your wf-* folders share one underlying .git directory, but each has a different branch checked out.
nohup vs. pm2Two ways to run a Node.js server in the background:
nohup node server.js & → "Just run it and don't kill it when I log off." No safety net. Dies on crash. Dies on reboot.pm2 start server.js → "Run it, watch it, restart it if it dies." Auto-recovers. Combined with a systemd unit, also survives reboot.Your server runs many Node.js processes at once. Each one listens on a numbered "port" (3009, 3011, 3013...). When a visitor types invoices.mikvehworks.com in their browser, here's the chain of events:
Change the port number, or kill the process → the subdomain breaks. Keep them stable → everything works.
The site at mywatchflow.com (your live production site, with real customer data) runs on a different physical machine at 24.144.109.84. This dev server cannot reach it. They're completely separate.
Merging code to main on GitHub does not automatically deploy to mywatchflow.com — that machine has its own deploy process, run separately.
Talk to your aigent like this. Each phrase runs a script that handles the plumbing.
Set up a new feature called X
aigent creates the branch, the folder, the subdomain, the SSL cert, and starts the process. You get back a working URL.
I want to work on X in this chat
aigent switches its working directory to the right folder so further edits go in the right place.
Sync X with main
aigent pulls the latest main branch into your feature branch, pushes, and restarts the subdomain.
Retire X
After you've merged the feature, aigent tears it down: stops the process, deletes the cert, removes the folder, deletes the branch.
Reference table for paths you'll see again.
| Thing | Where it lives |
|---|---|
| Main dev folder | /opt/watchflow-dev |
| Feature folders | /home/agentruntime/wf-* |
| nginx configs | /etc/nginx/sites-available/ |
| SSL certs (Let's Encrypt) | /etc/letsencrypt/live/ |
| pm2 dumps (root) | /root/.pm2/dump.pm2 |
| pm2 dumps (aigent) | /home/agentruntime/.pm2/dump.pm2 |
| Workflow scripts (planned) | /home/agentruntime/bin/wf-* |
| Production server | 24.144.109.84 never touched from here |
The careful checks paid off.
We installed the workflow scripts, then tested them — twice. The first run found two real bugs. Here's what happened.
Your aigent installed three executables in /home/agentruntime/bin/: wf-new, wf-sync, wf-retire. Plus a section in /home/agentruntime/CLAUDE.md documenting the four phrases. All passed bash -n syntax check.
The script:
test-pipeline off dev/home/agentruntime/wf-test-pipelinenpm ci to install 1,107 packagesResult: https://test-pipeline.mikvehworks.com returned HTTP 200. The pipeline worked.
But the aigent flagged two issues it had to manually patch around:
.env wasn't being writtenwf-new was passing the port to pm2 as a one-time argument, but never writing a .env file. So if you ever ran pm2 restart --update-env from a normal shell, pm2 would re-read the environment, find no PORT defined, fall back to the default 3010 (already taken), and crash with EADDRINUSE.
Fix: Patched wf-new to copy /opt/watchflow-dev/.env into the new worktree and override PORT with the assigned port. Now every new feature is self-contained and survives any pm2 restart.
devYour app has a check that looks at the incoming hostname — for *.mikvehworks.com subdomains, it skips the storefront and renders the app. That bypass was added on the invoice-module branch but never merged back to dev. So every new feature branched off dev would inherit the missing bypass and 404 itself out.
Fix: Cherry-picked commit 302cc325 (renamed e1b96af9 on dev) onto dev, pushed, restarted watchflow-dev pm2. From now on, every wf-new inherits the bypass automatically.
Ran wf-retire test-pipeline. The aigent stopped the process, deleted the cert, removed the worktree, and deleted both the local and remote branches. The only manual step was three sudo rm commands you ran in your root shell — the aigent doesn't have permission to delete files in /etc/nginx/ (intentional, for safety).
This was the real test. Same command, same protocol, but with both bugs fixed:
| Check | Result |
|---|---|
| HTTP status | 200 OK at test-pipeline-2.mikvehworks.com |
| pm2 row | online, 0 restarts, port 3019 |
| SSL cert | Issued, expires 2026-07-30 |
.env written | PORT=3019 written automatically |
| Storefront bypass inherited | Yes — no manual patch needed |
| Manual interventions during run | Zero (besides the sudo "go ahead") |
Same retire flow, same clean exit. All four real feature subdomains (invoices, activity, reporting, marketing) returned 200 throughout. Nothing else on the server was disturbed.
node_modules detourMid-pipeline, when restarting watchflow-dev on the new code, we hit an unrelated mess: this repo tracks node_modules as a symlink (it shouldn't — node_modules is normally git-ignored). The previous broken-symlink cleanup left git's idea of the file out of sync with the real installed packages directory. Switching branches threw "Your local changes would be overwritten."
We sidestepped it by moving the real node_modules to /tmp, letting git restore its (broken) symlink, switching to dev, then moving the real folder back. No work lost. The underlying repo weirdness is still there — a real cleanup someday is to remove node_modules from git tracking and add it to .gitignore. Not urgent.
Two small things, neither blocking.
rm permission to aigent's sudoers, scoped to /etc/nginx/sites-{available,enabled}/*.mikvehworks. That would let wf-retire finish end-to-end without you running 3 manual sudo rm commands. Low risk because the path glob locks it to nginx vhost files only. Defer until you're tired of typing them.node_modules in git. It's tracked-as-symlink for historical reasons. git rm --cached -r node_modules + add to .gitignore would clean it up. Worth doing eventually but doesn't affect anything today.CLAUDE.md), fix .env handling (done), get the storefront bypass onto dev (done). Workflow is complete.