← back to Pm2 Migration
initial scaffold (gitify-all 2026-05-06)
06e64fa153ca12e72688f8fb3dcedf9e8b3d7a05 · 2026-05-06 10:25:45 -0700 · Steve Abrams
Files touched
A .gitignoreA README.mdA append-ecosystem.jsA cleanup-stopped.shA ecosystem.config.cjsA finalize-on-kamatera.shA migrate.shA patch-connections.shA processes.jsonA root-shim.jsA start-tunnels.shA state/am-recrawl.jsonA state/archive-agent.jsonA state/arte-coverage-watch.jsonA state/arte-rescrape-loop.jsonA state/artie-agent.jsonA state/blog-agent.jsonA state/claudette-agent.jsonA state/color-search.jsonA state/cost-hunter.jsonA state/dex-dedup-agent.jsonA state/dex-hash-backfill.jsonA state/dw-central.jsonA state/full-monty-agent.jsonA state/gap-agent.jsonA state/garrett.jsonA state/gemini-catalog-tagger.jsonA state/gemini-classifier.jsonA state/gemini-tag-engine.jsonA state/george-gmail.jsonA state/graphic-agent.jsonA state/hawke.jsonA state/hormuz-orchestrator.jsonA state/launch-supervisor.jsonA state/momentum-pricer.jsonA state/norma-bluesky.jsonA state/norma-email.jsonA state/norma-facebook.jsonA state/norma-instagram.jsonA state/norma-price.jsonA state/norma-twitter.jsonA state/plumbing-agent.jsonA state/product-agent.jsonA state/resize-it.jsonA state/room-setting-agent.jsonA state/shopify-expert-agent.jsonA state/shopify-queue-worker.jsonA state/silas-sku-agent.jsonA state/sku-check-skill.jsonA state/slack-dm-viewer.jsonA state/timeout-agent.jsonA state/update-sku-agent.jsonA state/vendor-command-center.jsonA state/vendor-discount-agent.jsonA state/vendor-onboarding.jsonA state/yolo-agent.json
Diff
commit 06e64fa153ca12e72688f8fb3dcedf9e8b3d7a05
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 6 10:25:45 2026 -0700
initial scaffold (gitify-all 2026-05-06)
---
.gitignore | 12 +
README.md | 130 ++++++++
append-ecosystem.js | 59 ++++
cleanup-stopped.sh | 17 ++
ecosystem.config.cjs | 635 +++++++++++++++++++++++++++++++++++++++
finalize-on-kamatera.sh | 142 +++++++++
migrate.sh | 208 +++++++++++++
patch-connections.sh | 42 +++
processes.json | 104 +++++++
root-shim.js | 75 +++++
start-tunnels.sh | 27 ++
state/am-recrawl.json | 1 +
state/archive-agent.json | 1 +
state/arte-coverage-watch.json | 1 +
state/arte-rescrape-loop.json | 1 +
state/artie-agent.json | 1 +
state/blog-agent.json | 1 +
state/claudette-agent.json | 1 +
state/color-search.json | 1 +
state/cost-hunter.json | 1 +
state/dex-dedup-agent.json | 1 +
state/dex-hash-backfill.json | 1 +
state/dw-central.json | 1 +
state/full-monty-agent.json | 1 +
state/gap-agent.json | 1 +
state/garrett.json | 1 +
state/gemini-catalog-tagger.json | 1 +
state/gemini-classifier.json | 1 +
state/gemini-tag-engine.json | 1 +
state/george-gmail.json | 1 +
state/graphic-agent.json | 1 +
state/hawke.json | 1 +
state/hormuz-orchestrator.json | 1 +
state/launch-supervisor.json | 1 +
state/momentum-pricer.json | 1 +
state/norma-bluesky.json | 1 +
state/norma-email.json | 1 +
state/norma-facebook.json | 1 +
state/norma-instagram.json | 1 +
state/norma-price.json | 1 +
state/norma-twitter.json | 1 +
state/plumbing-agent.json | 1 +
state/product-agent.json | 1 +
state/resize-it.json | 1 +
state/room-setting-agent.json | 1 +
state/shopify-expert-agent.json | 1 +
state/shopify-queue-worker.json | 1 +
state/silas-sku-agent.json | 1 +
state/sku-check-skill.json | 1 +
state/slack-dm-viewer.json | 1 +
state/timeout-agent.json | 1 +
state/update-sku-agent.json | 1 +
state/vendor-command-center.json | 1 +
state/vendor-discount-agent.json | 1 +
state/vendor-onboarding.json | 1 +
state/yolo-agent.json | 1 +
56 files changed, 1496 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7e6a9c3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+node_modules/
+.env
+.env.local
+.env.*.local
+.env.*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+*.bak
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e0b9682
--- /dev/null
+++ b/README.md
@@ -0,0 +1,130 @@
+# PM2 Migration Kit — Kamatera → Local
+
+**Rule:** Kamatera is for outward-facing apps. Internal automation runs on Steve's Mac.
+
+## Files
+
+- `processes.json` — full classification (`keep_kamatera`, `move_local`, `investigate`, `delete_dead`, `high_priority`)
+- `migrate.sh` — driver script (per-process or batch, dry-run by default)
+- `append-ecosystem.js` — adds each migrated app to `ecosystem.local.cjs`
+- `ecosystem.local.cjs` — generated PM2 config that runs locally
+- `cleanup-stopped.sh` — drops already-dead processes (`hollywood-style-tagger`)
+- `state/` — captured PM2 metadata for each migrated process (used for rollback)
+
+## Prereqs
+
+1. **SSH to Kamatera** working as `root@45.61.58.125` with key auth (no password prompt). Test:
+ ```bash
+ ssh root@45.61.58.125 'pm2 -v'
+ ```
+2. **PM2 installed locally**: `npm i -g pm2 && pm2 startup`.
+3. **`jq`** locally and on Kamatera: `brew install jq`.
+4. **rsync** (preinstalled on macOS).
+
+If SSH lives under a different alias, set:
+```bash
+export KAMATERA_USER=root KAMATERA_HOST=kamatera # or whatever your ~/.ssh/config calls it
+```
+
+## Run order
+
+```bash
+cd /Users/stevestudio2/Projects/pm2-migration
+
+# 1. Reality check — what would happen for the whole bucket, no changes
+./migrate.sh --dry-run --batch move_local
+
+# 2. Pilot one (low-risk pick — george-gmail is already partially local)
+./migrate.sh george-gmail
+
+# 3. After 30s smoke test passes, leave it overnight, then finalize
+./migrate.sh --finalize george-gmail
+
+# 4. Batch the rest
+./migrate.sh --batch move_local
+
+# 5. After 24h, finalize each
+for n in $(jq -r '.buckets.move_local.processes[]' processes.json); do
+ ./migrate.sh --finalize "$n"
+done
+
+# 6. Drop the dead
+./cleanup-stopped.sh
+```
+
+## How hardcoded `/root/*` paths are handled (the shim)
+
+Most DW-Agents have hardcoded Kamatera paths like `/root/Projects/Designer-Wallcoverings/DW-MCP/.env` and `/root/DW-Agents/logs/...`. Rather than patching every agent, every migrated agent runs with `node --require root-shim.js`, which monkey-patches `fs.readFileSync` (and ~30 sibling fs methods) to rewrite any `/root/...` path to `~/kamatera-mirror/...`.
+
+**The mirror layout** (`~/kamatera-mirror/`):
+```
+~/kamatera-mirror/
+ Projects/
+ Designer-Wallcoverings/
+ DW-MCP/
+ .env ← rsynced from Kamatera (creds; gitignored)
+ .env.example
+ DW-Agents/
+ logs/ ← agent log writes land here
+ shared/
+```
+
+Synced once via:
+```bash
+rsync -avz --include='.env*' --exclude='*' \
+ root@45.61.58.125:/root/Projects/Designer-Wallcoverings/DW-MCP/ \
+ ~/kamatera-mirror/Projects/Designer-Wallcoverings/DW-MCP/
+```
+
+**Verified working** for `george-gmail` — boots all 4 Workspace accounts, no code changes:
+```
+[root-shim] /root/* → /Users/stevestudio2/kamatera-mirror/*
+[George] Loaded credentials from /root/Projects/Designer-Wallcoverings/DW-MCP/.env
+[George] Gmail + Tasks API client initialized (steve@)
+[George] Info@ account initialized (full Workspace)
+[George] Steve Office account upgraded to full Workspace
+[George] Steve Personal account initialized (full Workspace)
+```
+
+**Limits of the shim**
+- Only intercepts Node `fs` calls. Python agents and child-process invocations (`spawn('cat', ['/root/...'])`) are not covered. For Python agents, set `KAMATERA_MIRROR` env and patch the agent.
+- Doesn't rewrite hardcoded URLs printed to logs (e.g., `Dashboard: http://45.61.58.125:9850/`) — cosmetic only.
+- Doesn't refresh OAuth tokens. Tokens live inside `DW-MCP/.env`, so the rsync covers them. If an agent stores tokens elsewhere, sync those dirs too.
+
+**Adding new mirror paths**
+If a migrated agent fails reading `/root/<some/other/path>`, just rsync that path under `~/kamatera-mirror/<some/other/path>` and restart the agent. No code changes.
+
+## Per-process flow (what migrate.sh does)
+
+1. `pm2 jlist` over SSH → grab the process's `cwd`, `script`, `env`, `args` → save to `state/<name>.json`.
+2. `rsync` the project directory from Kamatera to `~/Projects/<name>/` (skips `node_modules`, `.git`, `logs`).
+3. `npm install` locally if a `package.json` is present.
+4. Append/update an entry for `<name>` in `ecosystem.local.cjs`.
+5. **Stop** (don't delete) on Kamatera and `pm2 save`.
+6. **Start** locally via `pm2 start ecosystem.local.cjs --only <name>` and `pm2 save`.
+7. 30-second smoke test — if `restart_time` > 3, automatically roll back (start on Kamatera, stop locally).
+8. Print a finalize hint.
+
+## Rollback
+
+If something misbehaves on local after the smoke test passes:
+```bash
+./migrate.sh --rollback <name>
+```
+Restarts the original on Kamatera and stops the local copy. The Kamatera process was only stopped (not deleted) until you ran `--finalize`, so this is safe up to that point.
+
+## Investigate bucket
+
+These four need a 30-second decision before moving — see `processes.json → buckets.investigate`. After deciding, just call `./migrate.sh <name>` (or leave them on Kamatera).
+
+## High-priority crash-loopers
+
+`momentum-crawler` (879 restarts), `color-search` (18), `george-gmail` (14). Moving them local is also the debug session — once local you can `pm2 logs <name>` without SSH lag.
+
+## Why not auto-batch all 40 right now?
+
+Each migration is reversible-with-effort but not free: rsync size, `npm install` time, and a 30-second smoke test per process means ~5 min × 40 = 3 hours wall-clock if everything works. Pilot one, sleep on it, batch the rest in the morning.
+
+## What's NOT touched
+
+Customer-facing sites in `buckets.keep_kamatera` are explicitly skipped. The script will refuse if you try (TODO: add the guard if needed).
diff --git a/append-ecosystem.js b/append-ecosystem.js
new file mode 100755
index 0000000..c71999e
--- /dev/null
+++ b/append-ecosystem.js
@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+// Append a single PM2 app entry to ecosystem.config.cjs.
+// Usage: append-ecosystem.js <name> <cwd> <script>
+
+const fs = require("fs");
+const path = require("path");
+
+const [name, cwd, script] = process.argv.slice(2);
+if (!name || !cwd || !script) {
+ console.error("usage: append-ecosystem.js <name> <cwd> <script>");
+ process.exit(1);
+}
+
+const file = path.join(__dirname, "ecosystem.config.cjs");
+
+let apps = [];
+if (fs.existsSync(file)) {
+ try {
+ const cfg = require(file);
+ apps = cfg.apps || [];
+ } catch (e) {
+ console.error("Could not require existing ecosystem; starting fresh:", e.message);
+ }
+}
+
+// After rsync, the script lives at cwd root with the same basename.
+const localScript = path.basename(script);
+const isPython = /\.py$/i.test(localScript);
+
+const SHIM = path.join(__dirname, "root-shim.js");
+
+const existing = apps.findIndex((a) => a.name === name);
+const entry = {
+ name,
+ cwd,
+ script: localScript,
+ ...(isPython
+ ? { interpreter: "python3" }
+ : { node_args: `--require ${SHIM}` }), // monkey-patches /root/* → ~/kamatera-mirror/*
+ autorestart: true,
+ watch: false,
+ max_memory_restart: "500M",
+ env: {
+ NODE_ENV: "production",
+ LOCAL_MIGRATION: "1",
+ KAMATERA_MIRROR: path.join(require("os").homedir(), "kamatera-mirror"),
+ },
+};
+
+if (existing >= 0) {
+ apps[existing] = entry;
+ console.log(`updated ${name} in ecosystem.config.cjs`);
+} else {
+ apps.push(entry);
+ console.log(`appended ${name} to ecosystem.config.cjs`);
+}
+
+const out = `module.exports = {\n apps: ${JSON.stringify(apps, null, 2).replace(/\n/g, "\n ")},\n};\n`;
+fs.writeFileSync(file, out);
diff --git a/cleanup-stopped.sh b/cleanup-stopped.sh
new file mode 100755
index 0000000..b2b93f4
--- /dev/null
+++ b/cleanup-stopped.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+# Drop processes that are already stopped/dead on Kamatera.
+# Currently: hollywood-style-tagger.
+
+set -euo pipefail
+KAMATERA_USER="${KAMATERA_USER:-root}"
+KAMATERA_HOST="${KAMATERA_HOST:-45.61.58.125}"
+
+DEAD=(hollywood-style-tagger)
+
+for name in "${DEAD[@]}"; do
+ echo "→ deleting $name on Kamatera"
+ ssh "$KAMATERA_USER@$KAMATERA_HOST" "pm2 delete '$name' || true"
+done
+
+ssh "$KAMATERA_USER@$KAMATERA_HOST" "pm2 save"
+echo "done"
diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs
new file mode 100644
index 0000000..44dc3c3
--- /dev/null
+++ b/ecosystem.config.cjs
@@ -0,0 +1,635 @@
+module.exports = {
+ apps: [
+ {
+ "name": "george-gmail",
+ "cwd": "/Users/stevestudio2/Projects/george-gmail",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "slack-dm-viewer",
+ "cwd": "/Users/stevestudio2/Projects/slack-dm-viewer",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "archive-agent",
+ "cwd": "/Users/stevestudio2/Projects/archive-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "resize-it",
+ "cwd": "/Users/stevestudio2/Projects/resize-it",
+ "script": "server.py",
+ "interpreter": "python3",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "timeout-agent",
+ "cwd": "/Users/stevestudio2/Projects/timeout-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "sku-check-skill",
+ "cwd": "/Users/stevestudio2/Projects/sku-check-skill",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "plumbing-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/plumbing-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "update-sku-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/update-sku-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "vendor-discount-agent",
+ "cwd": "/Users/stevestudio2/Projects/vendor-discount-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "claudette-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/claudette-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "dex-dedup-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/dex-dedup-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "gemini-classifier",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/scripts",
+ "script": "gemini-texture-classify-loop.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "am-recrawl",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/vendor-scrapers",
+ "script": "scrape-andrewmartin.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "blog-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/blog-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror",
+ "PORT": "9971"
+ }
+ },
+ {
+ "name": "color-search",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/color-search",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "cost-hunter",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/cost-hunter",
+ "script": "cost-hunter.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "dex-hash-backfill",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/dex-dedup-agent",
+ "script": "hash-worker.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "dw-central",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/dw-central",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "full-monty-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/full-monty-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "gap-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/gap-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "gemini-tag-engine",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/Projects/Designer-Wallcoverings/shopify/scripts/gemini-tag-engine",
+ "script": "index.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "gemini-catalog-tagger",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/Projects/Designer-Wallcoverings/shopify/scripts",
+ "script": "gemini-catalog-tagger.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "graphic-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/graphic-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "hormuz-orchestrator",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/hormuz-orchestrator",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "launch-supervisor",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/launch-supervisor",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "product-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/product-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "hawke",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/hawke-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "yolo-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/yolo-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "silas-sku-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/silas-sku-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "vendor-command-center",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/vendor-command-center",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "vendor-onboarding",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/vendor-onboarding-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "room-setting-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/room-setting-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "shopify-expert-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/shopify-expert-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "shopify-queue-worker",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/shopify-queue-worker",
+ "script": "worker.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "momentum-pricer",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/momentum-pricer",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "norma-bluesky",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/Projects/Norma/agents/bluesky-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "norma-email",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/Projects/Norma/node_modules/.bin",
+ "script": "next",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "norma-facebook",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/Projects/Norma/agents/facebook-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "norma-instagram",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/Projects/Norma/agents/instagram-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "norma-price",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/Projects/Norma/agents/price-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "norma-twitter",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/Projects/Norma/agents/twitter-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "arte-coverage-watch",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/Projects/Designer-Wallcoverings/DW-Programming",
+ "script": "arte-coverage-watch.sh",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "arte-rescrape-loop",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/Projects/Designer-Wallcoverings/DW-Programming",
+ "script": "arte-rescrape-loop.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "artie-agent",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/artie-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ },
+ {
+ "name": "garrett",
+ "cwd": "/Users/stevestudio2/kamatera-mirror/DW-Agents/godaddy-agent",
+ "script": "server.js",
+ "node_args": "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js",
+ "autorestart": true,
+ "watch": false,
+ "max_memory_restart": "500M",
+ "env": {
+ "NODE_ENV": "production",
+ "LOCAL_MIGRATION": "1",
+ "KAMATERA_MIRROR": "/Users/stevestudio2/kamatera-mirror"
+ }
+ }
+ ],
+};
diff --git a/finalize-on-kamatera.sh b/finalize-on-kamatera.sh
new file mode 100755
index 0000000..8da4b6c
--- /dev/null
+++ b/finalize-on-kamatera.sh
@@ -0,0 +1,142 @@
+#!/usr/bin/env bash
+# finalize-on-kamatera.sh
+#
+# Single atomic operation that:
+# 1. Adds the 24 migrated agents to the SKIP_RESTART allowlist in
+# /root/.skills/fix-ngx/scripts/monitor-all-services.sh
+# (the script already supports SKIP_RESTART — we just append names).
+# 2. Saves a backup of the original script before modifying.
+# 3. pm2 delete each migrated agent on Kamatera.
+# 4. pm2 save the new (smaller) state.
+# 5. Verifies: lists what's still running on Kamatera.
+#
+# Run: bash finalize-on-kamatera.sh
+#
+# Reversible: restore the .bak file, then for each agent:
+# ssh root@kamatera "cd <agent_cwd> && pm2 start ecosystem.config.js"
+
+set -euo pipefail
+
+KAMATERA_USER="${KAMATERA_USER:-root}"
+KAMATERA_HOST="${KAMATERA_HOST:-45.61.58.125}"
+
+# The 40 confirmed-healthy local migrations.
+MIGRATED=(
+ archive-agent
+ blog-agent
+ claudette-agent
+ color-search
+ cost-hunter
+ dex-dedup-agent
+ dex-hash-backfill
+ dw-central
+ full-monty-agent
+ gap-agent
+ gemini-catalog-tagger
+ gemini-classifier
+ gemini-tag-engine
+ george-gmail
+ graphic-agent
+ hawke
+ hormuz-orchestrator
+ launch-supervisor
+ momentum-pricer
+ norma-bluesky
+ norma-email
+ norma-facebook
+ norma-instagram
+ norma-price
+ norma-twitter
+ plumbing-agent
+ product-agent
+ resize-it
+ room-setting-agent
+ shopify-expert-agent
+ shopify-queue-worker
+ silas-sku-agent
+ sku-check-skill
+ slack-dm-viewer
+ timeout-agent
+ update-sku-agent
+ vendor-command-center
+ vendor-discount-agent
+ vendor-onboarding
+ yolo-agent
+)
+
+# Build space-separated list for SKIP_RESTART export
+SKIP_LIST="${MIGRATED[*]}"
+
+echo "═══════════════════════════════════════════════════════════"
+echo " Finalize PM2 migration on Kamatera"
+echo " Will allowlist + delete ${#MIGRATED[@]} agents"
+echo "═══════════════════════════════════════════════════════════"
+echo
+echo "Agents to delete from Kamatera:"
+printf ' - %s\n' "${MIGRATED[@]}"
+echo
+read -p "Proceed? Type 'yes' to confirm: " ans
+[[ "$ans" == "yes" ]] || { echo "Aborted."; exit 0; }
+
+ssh "$KAMATERA_USER@$KAMATERA_HOST" "SKIP_LIST='$SKIP_LIST' bash -s" <<'REMOTE'
+set -euo pipefail
+SCRIPT="/root/.skills/fix-ngx/scripts/monitor-all-services.sh"
+
+echo "→ backing up monitor script"
+cp -n "$SCRIPT" "${SCRIPT}.bak-$(date +%Y%m%d-%H%M%S)"
+
+echo "→ adding $(echo "$SKIP_LIST" | wc -w) names to SKIP_RESTART allowlist"
+# Match the indented `local SKIP_RESTART="..."` line (or unindented), single sed pass.
+# Use python for safe substring replacement to avoid sed-quoting hell.
+python3 - "$SCRIPT" "$SKIP_LIST" <<'PY'
+import re, sys
+path, names = sys.argv[1], sys.argv[2]
+with open(path) as f: src = f.read()
+def repl(m):
+ indent, prefix, existing, suffix = m.group(1), m.group(2), m.group(3), m.group(4)
+ have = set(existing.split())
+ add = [n for n in names.split() if n not in have]
+ new = (existing + " " + " ".join(add)).strip()
+ return f'{indent}{prefix}SKIP_RESTART="{new}"{suffix}'
+new_src, n = re.subn(
+ r'^(\s*)(local\s+)?SKIP_RESTART="([^"]*)"(.*)$',
+ repl, src, count=1, flags=re.MULTILINE
+)
+if n == 0:
+ sys.exit("ERROR: SKIP_RESTART line not found")
+with open(path, 'w') as f: f.write(new_src)
+print(f"OK: appended {len([n for n in names.split()])} names (skipping dups)")
+PY
+
+echo "→ verifying"
+grep -E "SKIP_RESTART=" "$SCRIPT" | head -1
+
+echo
+echo "→ deleting migrated agents from PM2"
+for n in $SKIP_LIST; do
+ pm2 delete "$n" 2>&1 | grep -E "✓|not found" | head -1 || true
+done
+
+echo
+echo "→ saving PM2 state"
+pm2 save
+
+echo
+echo "→ remaining PM2 processes:"
+pm2 jlist 2>/dev/null | jq -r '.[] | "\(.pm2_env.status)\t\(.name)"' | sort | column -t
+
+echo
+echo "✓ Finalize complete."
+REMOTE
+
+echo
+echo "═══════════════════════════════════════════════════════════"
+echo " Done. Watch for 5 min — if nothing reappears, you're clear."
+echo "═══════════════════════════════════════════════════════════"
+echo
+echo "Verify in 5 minutes (after service-monitor.timer fires once):"
+echo " ssh root@$KAMATERA_HOST 'pm2 list'"
+echo
+echo "If any of the 24 reappear, the SKIP_RESTART pattern doesn't match —"
+echo " inspect the script with:"
+echo " ssh root@$KAMATERA_HOST 'grep -n SKIP_RESTART /root/.skills/fix-ngx/scripts/monitor-all-services.sh'"
diff --git a/migrate.sh b/migrate.sh
new file mode 100755
index 0000000..fe8e531
--- /dev/null
+++ b/migrate.sh
@@ -0,0 +1,208 @@
+#!/usr/bin/env bash
+# pm2-migration/migrate.sh
+# Move PM2 processes from Kamatera (outward-facing-only) to local Mac.
+# Per-process loop:
+# 1. SSH in, pull `pm2 describe` for the process (cwd, script, env, args)
+# 2. rsync the project dir to ~/Projects/<name>/
+# 3. Generate a local PM2 ecosystem entry
+# 4. Stop on Kamatera (do NOT delete yet — kept as rollback for 24h)
+# 5. Start locally, tail for 30s, abort & roll back if it crashes
+# 6. After 24h soak, run --finalize to delete from Kamatera
+#
+# Usage:
+# ./migrate.sh --dry-run # show what would happen, no changes
+# ./migrate.sh <process-name> # migrate one
+# ./migrate.sh --batch move_local # migrate the whole bucket
+# ./migrate.sh --finalize <process-name> # after 24h soak, delete from Kamatera
+# ./migrate.sh --rollback <process-name> # restart on Kamatera, stop local
+
+set -euo pipefail
+
+KAMATERA_USER="${KAMATERA_USER:-root}"
+KAMATERA_HOST="${KAMATERA_HOST:-45.61.58.125}"
+# Mirror Kamatera's /root/* layout under ~/kamatera-mirror/* so:
+# - shared node_modules and sibling dirs (../shared/X) resolve via Node's normal lookup
+# - hardcoded /root/* paths in agent code resolve via root-shim.js
+MIRROR_ROOT="${MIRROR_ROOT:-/Users/stevestudio2/kamatera-mirror}"
+KIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+STATE_DIR="$KIT_DIR/state"
+mkdir -p "$STATE_DIR"
+
+DRY_RUN=0
+BATCH=""
+FINALIZE=""
+ROLLBACK=""
+TARGETS=()
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --dry-run) DRY_RUN=1; shift ;;
+ --batch) BATCH="$2"; shift 2 ;;
+ --finalize) FINALIZE="$2"; shift 2 ;;
+ --rollback) ROLLBACK="$2"; shift 2 ;;
+ --help|-h) sed -n '2,30p' "$0"; exit 0 ;;
+ *) TARGETS+=("$1"); shift ;;
+ esac
+done
+
+red() { printf '\033[31m%s\033[0m\n' "$*"; }
+green() { printf '\033[32m%s\033[0m\n' "$*"; }
+yellow() { printf '\033[33m%s\033[0m\n' "$*"; }
+blue() { printf '\033[34m%s\033[0m\n' "$*"; }
+
+run() {
+ if [[ $DRY_RUN -eq 1 ]]; then
+ yellow "[dry-run] $*"
+ else
+ eval "$@"
+ fi
+}
+
+ssh_run() {
+ if [[ $DRY_RUN -eq 1 ]]; then
+ yellow "[dry-run] ssh $KAMATERA_USER@$KAMATERA_HOST '$*'"
+ else
+ ssh -o ConnectTimeout=10 "$KAMATERA_USER@$KAMATERA_HOST" "$*"
+ fi
+}
+
+probe_kamatera() {
+ blue "→ probing $KAMATERA_USER@$KAMATERA_HOST"
+ ssh -o ConnectTimeout=5 -o BatchMode=yes "$KAMATERA_USER@$KAMATERA_HOST" 'pm2 -v' > /dev/null \
+ || { red "SSH/PM2 probe failed. Check ~/.ssh/config and that Kamatera is reachable."; exit 2; }
+ green "✓ Kamatera reachable"
+}
+
+describe() {
+ local name="$1"
+ # Keep it simple: just the four fields we need. SSH-escape via single quotes.
+ ssh -o ConnectTimeout=10 "$KAMATERA_USER@$KAMATERA_HOST" \
+ "pm2 jlist 2>/dev/null | jq -c --arg n '$name' '.[] | select(.name == \$n) | {name:.name, cwd:.pm2_env.pm_cwd, script:.pm2_env.pm_exec_path, interpreter:.pm2_env.exec_interpreter, args:(.pm2_env.args // [])}'"
+}
+
+migrate_one() {
+ local name="$1"
+ local state_file="$STATE_DIR/$name.json"
+
+ blue "════════ $name ════════"
+
+ # Step 1: capture metadata
+ blue "→ pulling metadata"
+ local meta
+ meta="$(describe "$name")"
+ if [[ -z "$meta" || "$meta" == "null" ]]; then
+ red "✗ $name not found on Kamatera (maybe already moved?)"
+ return 1
+ fi
+ echo "$meta" > "$state_file"
+ green "✓ metadata captured → $state_file"
+
+ local cwd script script_dir source_dir
+ cwd="$(echo "$meta" | jq -r '.cwd')"
+ script="$(echo "$meta" | jq -r '.script')"
+ script_dir="$(dirname "$script")"
+ # PM2 cwd is often wrong (process started from another dir). Trust the script's parent.
+ if [[ "$cwd" != "$script_dir" ]]; then
+ yellow " cwd ($cwd) != script dir ($script_dir) — using script dir"
+ source_dir="$script_dir"
+ else
+ source_dir="$cwd"
+ fi
+ echo " pm2 cwd: $cwd"
+ echo " script: $script"
+ echo " syncing: $source_dir"
+
+ # Step 2: rsync — target mirrors Kamatera path under ~/kamatera-mirror
+ # /root/DW-Agents/foo → ~/kamatera-mirror/DW-Agents/foo
+ local local_dir="${MIRROR_ROOT}${source_dir#/root}"
+ blue "→ rsyncing $source_dir → $local_dir"
+ if [[ -d "$local_dir" ]]; then
+ yellow " $local_dir exists — syncing in place (use --force-overwrite to wipe)"
+ fi
+ run "mkdir -p '$local_dir'"
+ run "rsync -avz --exclude=node_modules --exclude=.git --exclude=logs --exclude='*.log' \
+ '$KAMATERA_USER@$KAMATERA_HOST:$source_dir/' '$local_dir/'"
+
+ # Step 3a: rewrite hardcoded :5432/:6379 → tunnel ports (15432/16379)
+ blue "→ patching DB connection strings"
+ run "bash '$KIT_DIR/patch-connections.sh' '$local_dir'"
+
+ # Step 3b: install deps locally (if package.json present)
+ if [[ $DRY_RUN -eq 0 && -f "$local_dir/package.json" ]]; then
+ blue "→ npm install in $local_dir"
+ (cd "$local_dir" && npm install --silent)
+ fi
+
+ # Step 4: append to local ecosystem
+ blue "→ appending to ecosystem.config.cjs"
+ run "node '$KIT_DIR/append-ecosystem.js' '$name' '$local_dir' '$script'"
+
+ # Step 5: stop on Kamatera (KEEP, don't delete — rollback hatch)
+ blue "→ stopping on Kamatera (kept for 24h rollback)"
+ ssh_run "pm2 stop '$name' && pm2 save"
+
+ # Step 6: start locally
+ blue "→ starting locally via PM2"
+ run "pm2 start '$KIT_DIR/ecosystem.config.cjs' --only '$name'"
+ run "pm2 save"
+
+ # Step 7: 30s smoke test
+ blue "→ 30s smoke test (watching for crash loop)"
+ if [[ $DRY_RUN -eq 0 ]]; then
+ sleep 30
+ local restarts
+ restarts="$(pm2 jlist 2>/dev/null | jq -r --arg n "$name" '.[] | select(.name==$n) | .pm2_env.restart_time // 0')"
+ if [[ "$restarts" -gt 3 ]]; then
+ red "✗ $name crash-looping locally ($restarts restarts in 30s) — rolling back"
+ pm2 stop "$name" || true
+ ssh_run "pm2 start '$name' && pm2 save"
+ return 1
+ fi
+ fi
+ green "✓ $name running locally — 24h soak, then ./migrate.sh --finalize $name"
+}
+
+finalize_one() {
+ local name="$1"
+ blue "→ finalizing $name (delete from Kamatera)"
+ ssh_run "pm2 delete '$name' && pm2 save"
+ green "✓ $name removed from Kamatera"
+}
+
+rollback_one() {
+ local name="$1"
+ blue "→ rolling back $name"
+ run "pm2 stop '$name' && pm2 save"
+ ssh_run "pm2 start '$name' && pm2 save"
+ green "✓ $name restored on Kamatera, stopped locally"
+}
+
+# ─── main ────────────────────────────────────────────────────────────────
+
+probe_kamatera
+
+if [[ -n "$ROLLBACK" ]]; then rollback_one "$ROLLBACK"; exit 0; fi
+if [[ -n "$FINALIZE" ]]; then finalize_one "$FINALIZE"; exit 0; fi
+
+if [[ -n "$BATCH" ]]; then
+ mapfile -t TARGETS < <(jq -r --arg b "$BATCH" '.buckets[$b].processes[] | if type=="string" then . else .name end' "$KIT_DIR/processes.json")
+ yellow "Batch '$BATCH' → ${#TARGETS[@]} processes"
+ printf ' - %s\n' "${TARGETS[@]}"
+ echo
+ read -p "Proceed? (y/N) " -n 1 -r; echo
+ [[ "$REPLY" =~ ^[Yy]$ ]] || exit 0
+fi
+
+if [[ ${#TARGETS[@]} -eq 0 ]]; then
+ red "No targets. Pass a process name, --batch <bucket>, or --help."
+ exit 1
+fi
+
+ok=0; fail=0
+for name in "${TARGETS[@]}"; do
+ if migrate_one "$name"; then ok=$((ok+1)); else fail=$((fail+1)); fi
+done
+
+echo
+green "Done — $ok migrated, $fail failed"
+echo "After 24h: ./migrate.sh --finalize <name> for each successful one"
diff --git a/patch-connections.sh b/patch-connections.sh
new file mode 100755
index 0000000..7c56837
--- /dev/null
+++ b/patch-connections.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+# patch-connections.sh <dir>
+# Rewrite hardcoded Kamatera DB ports to the local autossh tunnel ports.
+# postgres :5432 → :15432
+# redis :6379 → :16379
+# Only touches .js, .ts, .json, .py, .sh files (not node_modules).
+
+set -euo pipefail
+TARGET="${1:?usage: patch-connections.sh <dir>}"
+
+if [[ ! -d "$TARGET" ]]; then
+ echo "no such dir: $TARGET" >&2
+ exit 1
+fi
+
+# Find candidate files. Skip node_modules and .git.
+files=$(find "$TARGET" -type f \
+ \( -name '*.js' -o -name '*.ts' -o -name '*.cjs' -o -name '*.mjs' \
+ -o -name '*.json' -o -name '*.py' -o -name '*.sh' -o -name '.env*' \) \
+ -not -path '*/node_modules/*' -not -path '*/.git/*')
+
+count=0
+for f in $files; do
+ # postgres — rewrite with separate sed calls to avoid macOS sed delimiter conflict
+ if grep -qE '(127\.0\.0\.1|localhost):5432' "$f" 2>/dev/null; then
+ sed -i '' \
+ -e 's|127\.0\.0\.1:5432|127.0.0.1:15432|g' \
+ -e 's|localhost:5432|localhost:15432|g' \
+ "$f"
+ count=$((count+1))
+ fi
+ # redis
+ if grep -qE '(127\.0\.0\.1|localhost):6379' "$f" 2>/dev/null; then
+ sed -i '' \
+ -e 's|127\.0\.0\.1:6379|127.0.0.1:16379|g' \
+ -e 's|localhost:6379|localhost:16379|g' \
+ "$f"
+ count=$((count+1))
+ fi
+done
+
+echo "patched $count file occurrence(s) in $TARGET"
diff --git a/processes.json b/processes.json
new file mode 100644
index 0000000..9aaa428
--- /dev/null
+++ b/processes.json
@@ -0,0 +1,104 @@
+{
+ "$schema": "pm2-migration/v1",
+ "kamatera_host": "45.61.58.125",
+ "kamatera_user": "root",
+ "rule": "Kamatera = outward-facing only. Everything else moves to local.",
+ "buckets": {
+ "keep_kamatera": {
+ "reason": "Customer-facing public sites and visualizers — must stay on a public host",
+ "processes": [
+ "novasuede-website",
+ "philipperomano",
+ "bubbe",
+ "goodquestion-blog",
+ "iwascute",
+ "pastdoor",
+ "victorystays",
+ "marburg-live",
+ "flockedwallpaper",
+ "glassbeadedwallpaper",
+ "grassclothwallcoverings",
+ "grassclothwallpaper",
+ "room-setting-app",
+ "paint-visualizer-"
+ ]
+ },
+ "move_local": {
+ "reason": "Internal automation, scrapers, agents, mailers, internal tools — no public traffic",
+ "processes": [
+ "am-recrawl",
+ "archive-agent",
+ "art-print-agent",
+ "blog-agent",
+ "claudette-agent",
+ "color-search",
+ "cost-hunter",
+ "dex-dedup-agent",
+ "dex-hash-backfill",
+ "dw-central",
+ "full-monty-agent",
+ "gap-agent",
+ "gemini-catalog-tagger",
+ "gemini-classifier",
+ "gemini-tag-engine",
+ "george-gmail",
+ "graphic-agent",
+ "hawke",
+ "launch-supervisor",
+ "momentum-crawler",
+ "momentum-pricer",
+ "nora-agent",
+ "norma-bluesky",
+ "norma-email",
+ "norma-facebook",
+ "norma-instagram",
+ "norma-price",
+ "norma-twitter",
+ "plumbing-agent",
+ "product-agent",
+ "resize-it",
+ "room-setting-agent",
+ "shopify-expert-agent",
+ "shopify-queue-worker",
+ "silas-sku-agent",
+ "sku-check-skill",
+ "slack-dm-viewer",
+ "timeout-agent",
+ "update-sku-agent",
+ "vendor-command-center",
+ "vendor-discount-agent",
+ "vendor-onboarding",
+ "yolo-agent",
+ "hormuz-orchestrator"
+ ]
+ },
+ "investigate": {
+ "reason": "Verify outward-facing role before moving",
+ "processes": [
+ { "name": "art-print-agent", "question": "Is this the print-on-demand customer page, or a fulfillment worker?" },
+ { "name": "room-setting-agent", "question": "Confirm this is the *generator* (move) vs room-setting-app (keep)" },
+ { "name": "hormuz-orchestrator", "question": "Do you ever open the studio viewer (port 9801) from outside your network?" },
+ { "name": "dw-central", "question": "Does any customer-facing site call its API?" },
+ { "name": "paint-visualizer-", "question": "Is this customer-facing or just an internal tool?" }
+ ]
+ },
+ "delete_dead": {
+ "reason": "Already stopped, no recent restarts — drop them",
+ "processes": ["hollywood-style-tagger"]
+ },
+ "high_priority": {
+ "reason": "Crash-looping or chronically restarting — moving local is also a chance to debug",
+ "processes": [
+ { "name": "momentum-crawler", "restarts": 879 },
+ { "name": "color-search", "restarts": 18 },
+ { "name": "george-gmail", "restarts": 14 },
+ { "name": "novasuede-website", "restarts": 13, "note": "stays Kamatera but investigate" }
+ ]
+ }
+ },
+ "local_layout": {
+ "projects_root": "/Users/stevestudio2/Projects",
+ "ecosystem_config": "/Users/stevestudio2/Projects/pm2-migration/ecosystem.local.cjs",
+ "logs_dir": "/Users/stevestudio2/.pm2/logs"
+ }
+}
diff --git a/root-shim.js b/root-shim.js
new file mode 100644
index 0000000..dffa213
--- /dev/null
+++ b/root-shim.js
@@ -0,0 +1,75 @@
+// pm2-migration/root-shim.js
+// Universal path-rewrite shim. Loaded via node --require so all fs calls
+// that reference Kamatera-style /root/... paths get transparently redirected
+// to /Users/stevestudio2/kamatera-mirror/...
+//
+// Used by every migrated agent in ecosystem.config.cjs as:
+// node_args: "--require /Users/stevestudio2/Projects/pm2-migration/root-shim.js"
+//
+// No agent code changes required. Add new dirs to MIRROR as needed.
+
+const fs = require("fs");
+const os = require("os");
+const path = require("path");
+
+const MIRROR = process.env.KAMATERA_MIRROR || path.join(os.homedir(), "kamatera-mirror");
+const PREFIX = "/root/";
+
+function rewrite(p) {
+ if (typeof p !== "string") return p;
+ if (!p.startsWith(PREFIX)) return p;
+ return path.join(MIRROR, p.slice(PREFIX.length)); // strip "/root/" → mirror root
+}
+
+// Wrap any fs method whose first arg is a path string.
+const PATH_FIRST_METHODS = [
+ "readFileSync", "writeFileSync", "appendFileSync", "existsSync", "statSync",
+ "lstatSync", "readdirSync", "mkdirSync", "rmdirSync", "rmSync", "unlinkSync",
+ "renameSync", "copyFileSync", "accessSync", "chmodSync", "chownSync",
+ "createReadStream", "createWriteStream", "openSync", "watchFile",
+ "readFile", "writeFile", "appendFile", "stat", "lstat", "readdir",
+ "mkdir", "rmdir", "rm", "unlink", "rename", "copyFile", "access",
+ "chmod", "chown", "open", "exists",
+];
+
+for (const m of PATH_FIRST_METHODS) {
+ const orig = fs[m];
+ if (typeof orig !== "function") continue;
+ fs[m] = function (p, ...rest) {
+ return orig.call(fs, rewrite(p), ...rest);
+ };
+}
+
+// fs.promises mirror
+if (fs.promises) {
+ for (const m of [
+ "readFile", "writeFile", "appendFile", "stat", "lstat", "readdir",
+ "mkdir", "rmdir", "rm", "unlink", "rename", "copyFile", "access",
+ "chmod", "chown", "open",
+ ]) {
+ const orig = fs.promises[m];
+ if (typeof orig !== "function") continue;
+ fs.promises[m] = function (p, ...rest) {
+ return orig.call(fs.promises, rewrite(p), ...rest);
+ };
+ }
+}
+
+// path.resolve / path.join called with /root/... will produce /root/... — handle as-is.
+// dotenv config({ path: '/root/...' }) calls fs.readFileSync internally, so it's covered.
+
+// Patch Node's module resolver — require('/root/...') needs to resolve to mirror.
+// This catches both `require('/root/foo')` and `require('/root/foo/bar')` patterns
+// that fs-shimming alone can't reach (Module._resolveFilename uses internal fs).
+const Module = require("module");
+const origResolve = Module._resolveFilename;
+Module._resolveFilename = function (request, parent, ...rest) {
+ if (typeof request === "string" && request.startsWith(PREFIX)) {
+ request = path.join(MIRROR, request.slice(PREFIX.length));
+ }
+ return origResolve.call(this, request, parent, ...rest);
+};
+
+if (process.env.LOCAL_MIGRATION === "1" && !process.env.SHIM_QUIET) {
+ console.log(`[root-shim] /root/* → ${MIRROR}/* (fs + require)`);
+}
diff --git a/start-tunnels.sh b/start-tunnels.sh
new file mode 100755
index 0000000..88daa08
--- /dev/null
+++ b/start-tunnels.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+# autossh tunnel for Kamatera postgres + redis.
+# Run via PM2 so it survives reboots/restarts.
+#
+# pm2 start ./start-tunnels.sh --name kamatera-tunnels --no-autorestart
+# pm2 save
+#
+# Local ports avoid collision with the user's existing local postgres on 5432.
+# Migrated agents have their connection strings rewritten to point here.
+
+KAMATERA_USER="${KAMATERA_USER:-root}"
+KAMATERA_HOST="${KAMATERA_HOST:-45.61.58.125}"
+
+# Check responsiveness every 30s; restart tunnel if dead.
+export AUTOSSH_POLL=30
+export AUTOSSH_GATETIME=0 # don't wait for first successful connection
+export AUTOSSH_LOGLEVEL=4
+export AUTOSSH_LOGFILE=/Users/stevestudio2/.pm2/logs/kamatera-tunnels.log
+
+exec autossh -M 0 -N \
+ -o ServerAliveInterval=30 \
+ -o ServerAliveCountMax=3 \
+ -o ExitOnForwardFailure=yes \
+ -o StrictHostKeyChecking=no \
+ -L 15432:127.0.0.1:5432 \
+ -L 16379:127.0.0.1:6379 \
+ "$KAMATERA_USER@$KAMATERA_HOST"
diff --git a/state/am-recrawl.json b/state/am-recrawl.json
new file mode 100644
index 0000000..f3d7975
--- /dev/null
+++ b/state/am-recrawl.json
@@ -0,0 +1 @@
+{"name":"am-recrawl","cwd":"/root/DW-Agents/shared","script":"/root/DW-Agents/vendor-scrapers/scrape-andrewmartin.js","interpreter":"node","args":[]}
diff --git a/state/archive-agent.json b/state/archive-agent.json
new file mode 100644
index 0000000..66a5054
--- /dev/null
+++ b/state/archive-agent.json
@@ -0,0 +1 @@
+{"name":"archive-agent","cwd":"/root/DW-Agents/archive-agent","script":"/root/DW-Agents/archive-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/arte-coverage-watch.json b/state/arte-coverage-watch.json
new file mode 100644
index 0000000..2f523a9
--- /dev/null
+++ b/state/arte-coverage-watch.json
@@ -0,0 +1 @@
+{"name":"arte-coverage-watch","cwd":"/root","script":"/root/Projects/Designer-Wallcoverings/DW-Programming/arte-coverage-watch.sh","interpreter":"bash","args":[]}
diff --git a/state/arte-rescrape-loop.json b/state/arte-rescrape-loop.json
new file mode 100644
index 0000000..53cc5f8
--- /dev/null
+++ b/state/arte-rescrape-loop.json
@@ -0,0 +1 @@
+{"name":"arte-rescrape-loop","cwd":"/root/Projects/Designer-Wallcoverings","script":"/root/Projects/Designer-Wallcoverings/DW-Programming/arte-rescrape-loop.js","interpreter":"node","args":[]}
diff --git a/state/artie-agent.json b/state/artie-agent.json
new file mode 100644
index 0000000..b24c02d
--- /dev/null
+++ b/state/artie-agent.json
@@ -0,0 +1 @@
+{"name":"artie-agent","cwd":"/root/DW-Agents/artie-agent","script":"/root/DW-Agents/artie-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/blog-agent.json b/state/blog-agent.json
new file mode 100644
index 0000000..f627427
--- /dev/null
+++ b/state/blog-agent.json
@@ -0,0 +1 @@
+{"name":"blog-agent","cwd":"/root/DW-Agents/blog-agent","script":"/root/DW-Agents/blog-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/claudette-agent.json b/state/claudette-agent.json
new file mode 100644
index 0000000..bd9a662
--- /dev/null
+++ b/state/claudette-agent.json
@@ -0,0 +1 @@
+{"name":"claudette-agent","cwd":"/root/DW-Agents/claudette-agent","script":"/root/DW-Agents/claudette-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/color-search.json b/state/color-search.json
new file mode 100644
index 0000000..c4d9569
--- /dev/null
+++ b/state/color-search.json
@@ -0,0 +1 @@
+{"name":"color-search","cwd":"/root/DW-Agents/color-search","script":"/root/DW-Agents/color-search/server.js","interpreter":"node","args":[]}
diff --git a/state/cost-hunter.json b/state/cost-hunter.json
new file mode 100644
index 0000000..6f3785b
--- /dev/null
+++ b/state/cost-hunter.json
@@ -0,0 +1 @@
+{"name":"cost-hunter","cwd":"/root/DW-Agents/cost-hunter","script":"/root/DW-Agents/cost-hunter/cost-hunter.js","interpreter":"node","args":["--vendor","all","--loop"]}
diff --git a/state/dex-dedup-agent.json b/state/dex-dedup-agent.json
new file mode 100644
index 0000000..613de6e
--- /dev/null
+++ b/state/dex-dedup-agent.json
@@ -0,0 +1 @@
+{"name":"dex-dedup-agent","cwd":"/root/Projects/goodquestion-ai","script":"/root/DW-Agents/dex-dedup-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/dex-hash-backfill.json b/state/dex-hash-backfill.json
new file mode 100644
index 0000000..836f9c4
--- /dev/null
+++ b/state/dex-hash-backfill.json
@@ -0,0 +1 @@
+{"name":"dex-hash-backfill","cwd":"/root/DW-Agents/dex-dedup-agent","script":"/root/DW-Agents/dex-dedup-agent/hash-worker.js","interpreter":"node","args":[]}
diff --git a/state/dw-central.json b/state/dw-central.json
new file mode 100644
index 0000000..ac98f44
--- /dev/null
+++ b/state/dw-central.json
@@ -0,0 +1 @@
+{"name":"dw-central","cwd":"/root/DW-Agents/dw-central","script":"/root/DW-Agents/dw-central/server.js","interpreter":"node","args":[]}
diff --git a/state/full-monty-agent.json b/state/full-monty-agent.json
new file mode 100644
index 0000000..9f75d15
--- /dev/null
+++ b/state/full-monty-agent.json
@@ -0,0 +1 @@
+{"name":"full-monty-agent","cwd":"/root/DW-Agents/full-monty-agent","script":"/root/DW-Agents/full-monty-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/gap-agent.json b/state/gap-agent.json
new file mode 100644
index 0000000..540f350
--- /dev/null
+++ b/state/gap-agent.json
@@ -0,0 +1 @@
+{"name":"gap-agent","cwd":"/root/Projects/goodquestion-ai","script":"/root/DW-Agents/gap-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/garrett.json b/state/garrett.json
new file mode 100644
index 0000000..d43a8e9
--- /dev/null
+++ b/state/garrett.json
@@ -0,0 +1 @@
+{"name":"garrett","cwd":"/root/DW-Agents/godaddy-agent","script":"/root/DW-Agents/godaddy-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/gemini-catalog-tagger.json b/state/gemini-catalog-tagger.json
new file mode 100644
index 0000000..f8bb845
--- /dev/null
+++ b/state/gemini-catalog-tagger.json
@@ -0,0 +1 @@
+{"name":"gemini-catalog-tagger","cwd":"/root/Projects/Designer-Wallcoverings","script":"/root/Projects/Designer-Wallcoverings/shopify/scripts/gemini-catalog-tagger.js","interpreter":"node","args":["--skip-shopify"]}
diff --git a/state/gemini-classifier.json b/state/gemini-classifier.json
new file mode 100644
index 0000000..65b37b8
--- /dev/null
+++ b/state/gemini-classifier.json
@@ -0,0 +1 @@
+{"name":"gemini-classifier","cwd":"/root/DW-Agents/scripts","script":"/root/DW-Agents/scripts/gemini-texture-classify-loop.js","interpreter":"node","args":[]}
diff --git a/state/gemini-tag-engine.json b/state/gemini-tag-engine.json
new file mode 100644
index 0000000..532d573
--- /dev/null
+++ b/state/gemini-tag-engine.json
@@ -0,0 +1 @@
+{"name":"gemini-tag-engine","cwd":"/root/Projects/Designer-Wallcoverings/shopify/scripts/gemini-tag-engine","script":"/root/Projects/Designer-Wallcoverings/shopify/scripts/gemini-tag-engine/index.js","interpreter":"node","args":[]}
diff --git a/state/george-gmail.json b/state/george-gmail.json
new file mode 100644
index 0000000..167ea97
--- /dev/null
+++ b/state/george-gmail.json
@@ -0,0 +1 @@
+{"name":"george-gmail","cwd":"/root/DW-Agents/gmail-agent","script":"/root/DW-Agents/gmail-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/graphic-agent.json b/state/graphic-agent.json
new file mode 100644
index 0000000..974213b
--- /dev/null
+++ b/state/graphic-agent.json
@@ -0,0 +1 @@
+{"name":"graphic-agent","cwd":"/root/Projects/goodquestion-ai","script":"/root/DW-Agents/graphic-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/hawke.json b/state/hawke.json
new file mode 100644
index 0000000..81caba2
--- /dev/null
+++ b/state/hawke.json
@@ -0,0 +1 @@
+{"name":"hawke","cwd":"/root/DW-Agents/hawke-agent","script":"/root/DW-Agents/hawke-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/hormuz-orchestrator.json b/state/hormuz-orchestrator.json
new file mode 100644
index 0000000..d99a458
--- /dev/null
+++ b/state/hormuz-orchestrator.json
@@ -0,0 +1 @@
+{"name":"hormuz-orchestrator","cwd":"/root/DW-Agents/hormuz-orchestrator","script":"/root/DW-Agents/hormuz-orchestrator/server.js","interpreter":"node","args":[]}
diff --git a/state/launch-supervisor.json b/state/launch-supervisor.json
new file mode 100644
index 0000000..91539a5
--- /dev/null
+++ b/state/launch-supervisor.json
@@ -0,0 +1 @@
+{"name":"launch-supervisor","cwd":"/root","script":"/root/DW-Agents/launch-supervisor/server.js","interpreter":"node","args":[]}
diff --git a/state/momentum-pricer.json b/state/momentum-pricer.json
new file mode 100644
index 0000000..1807c02
--- /dev/null
+++ b/state/momentum-pricer.json
@@ -0,0 +1 @@
+{"name":"momentum-pricer","cwd":"/root/DW-Agents/momentum-pricer","script":"/root/DW-Agents/momentum-pricer/server.js","interpreter":"node","args":[]}
diff --git a/state/norma-bluesky.json b/state/norma-bluesky.json
new file mode 100644
index 0000000..72eb344
--- /dev/null
+++ b/state/norma-bluesky.json
@@ -0,0 +1 @@
+{"name":"norma-bluesky","cwd":"/root/Projects/Norma/agents/bluesky-agent","script":"/root/Projects/Norma/agents/bluesky-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/norma-email.json b/state/norma-email.json
new file mode 100644
index 0000000..f6ec9e6
--- /dev/null
+++ b/state/norma-email.json
@@ -0,0 +1 @@
+{"name":"norma-email","cwd":"/root/Projects/Norma","script":"/root/Projects/Norma/node_modules/.bin/next","interpreter":"/usr/local/bin/node","args":["start","-p","7400"]}
diff --git a/state/norma-facebook.json b/state/norma-facebook.json
new file mode 100644
index 0000000..dfe0760
--- /dev/null
+++ b/state/norma-facebook.json
@@ -0,0 +1 @@
+{"name":"norma-facebook","cwd":"/root/Projects/Norma/agents/facebook-agent","script":"/root/Projects/Norma/agents/facebook-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/norma-instagram.json b/state/norma-instagram.json
new file mode 100644
index 0000000..99deaf8
--- /dev/null
+++ b/state/norma-instagram.json
@@ -0,0 +1 @@
+{"name":"norma-instagram","cwd":"/root/Projects/Norma/agents/instagram-agent","script":"/root/Projects/Norma/agents/instagram-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/norma-price.json b/state/norma-price.json
new file mode 100644
index 0000000..de955bf
--- /dev/null
+++ b/state/norma-price.json
@@ -0,0 +1 @@
+{"name":"norma-price","cwd":"/root/Projects/Norma/agents/price-agent","script":"/root/Projects/Norma/agents/price-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/norma-twitter.json b/state/norma-twitter.json
new file mode 100644
index 0000000..802e59b
--- /dev/null
+++ b/state/norma-twitter.json
@@ -0,0 +1 @@
+{"name":"norma-twitter","cwd":"/root/Projects/Norma/agents/twitter-agent","script":"/root/Projects/Norma/agents/twitter-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/plumbing-agent.json b/state/plumbing-agent.json
new file mode 100644
index 0000000..06c6ecf
--- /dev/null
+++ b/state/plumbing-agent.json
@@ -0,0 +1 @@
+{"name":"plumbing-agent","cwd":"/root/DW-Agents/plumbing-agent","script":"/root/DW-Agents/plumbing-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/product-agent.json b/state/product-agent.json
new file mode 100644
index 0000000..8f6ba08
--- /dev/null
+++ b/state/product-agent.json
@@ -0,0 +1 @@
+{"name":"product-agent","cwd":"/root/Projects/goodquestion-ai","script":"/root/DW-Agents/product-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/resize-it.json b/state/resize-it.json
new file mode 100644
index 0000000..44baf6f
--- /dev/null
+++ b/state/resize-it.json
@@ -0,0 +1 @@
+{"name":"resize-it","cwd":"/root/.claude/skills/resize-it","script":"/root/.claude/skills/resize-it/server.py","interpreter":"python3","args":[]}
diff --git a/state/room-setting-agent.json b/state/room-setting-agent.json
new file mode 100644
index 0000000..a624849
--- /dev/null
+++ b/state/room-setting-agent.json
@@ -0,0 +1 @@
+{"name":"room-setting-agent","cwd":"/root/DW-Agents/room-setting-agent","script":"/root/DW-Agents/room-setting-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/shopify-expert-agent.json b/state/shopify-expert-agent.json
new file mode 100644
index 0000000..7e47ead
--- /dev/null
+++ b/state/shopify-expert-agent.json
@@ -0,0 +1 @@
+{"name":"shopify-expert-agent","cwd":"/root/DW-Agents/gap-agent","script":"/root/DW-Agents/shopify-expert-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/shopify-queue-worker.json b/state/shopify-queue-worker.json
new file mode 100644
index 0000000..a5d69fd
--- /dev/null
+++ b/state/shopify-queue-worker.json
@@ -0,0 +1 @@
+{"name":"shopify-queue-worker","cwd":"/root/DW-Agents/gap-agent","script":"/root/DW-Agents/shopify-queue-worker/worker.js","interpreter":"node","args":[]}
diff --git a/state/silas-sku-agent.json b/state/silas-sku-agent.json
new file mode 100644
index 0000000..044f463
--- /dev/null
+++ b/state/silas-sku-agent.json
@@ -0,0 +1 @@
+{"name":"silas-sku-agent","cwd":"/root/DW-Agents/silas-sku-agent","script":"/root/DW-Agents/silas-sku-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/sku-check-skill.json b/state/sku-check-skill.json
new file mode 100644
index 0000000..40fefc0
--- /dev/null
+++ b/state/sku-check-skill.json
@@ -0,0 +1 @@
+{"name":"sku-check-skill","cwd":"/root/DW-Agents/sku-check-skill","script":"/root/DW-Agents/sku-check-skill/server.js","interpreter":"node","args":[]}
diff --git a/state/slack-dm-viewer.json b/state/slack-dm-viewer.json
new file mode 100644
index 0000000..00c7aea
--- /dev/null
+++ b/state/slack-dm-viewer.json
@@ -0,0 +1 @@
+{"name":"slack-dm-viewer","cwd":"/root/DW-Agents/slack-dm-viewer","script":"/root/DW-Agents/slack-dm-viewer/server.js","interpreter":"node","args":[]}
diff --git a/state/timeout-agent.json b/state/timeout-agent.json
new file mode 100644
index 0000000..c7dca4c
--- /dev/null
+++ b/state/timeout-agent.json
@@ -0,0 +1 @@
+{"name":"timeout-agent","cwd":"/root/Projects/goodquestion-ai","script":"/root/DW-Agents/timeout-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/update-sku-agent.json b/state/update-sku-agent.json
new file mode 100644
index 0000000..5a1e2f1
--- /dev/null
+++ b/state/update-sku-agent.json
@@ -0,0 +1 @@
+{"name":"update-sku-agent","cwd":"/root/DW-Agents/update-sku-agent","script":"/root/DW-Agents/update-sku-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/vendor-command-center.json b/state/vendor-command-center.json
new file mode 100644
index 0000000..a37c179
--- /dev/null
+++ b/state/vendor-command-center.json
@@ -0,0 +1 @@
+{"name":"vendor-command-center","cwd":"/root/DW-Agents/vendor-command-center","script":"/root/DW-Agents/vendor-command-center/server.js","interpreter":"node","args":[]}
diff --git a/state/vendor-discount-agent.json b/state/vendor-discount-agent.json
new file mode 100644
index 0000000..03c87c4
--- /dev/null
+++ b/state/vendor-discount-agent.json
@@ -0,0 +1 @@
+{"name":"vendor-discount-agent","cwd":"/root/Projects/goodquestion-ai","script":"/root/DW-Agents/vendor-discount-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/vendor-onboarding.json b/state/vendor-onboarding.json
new file mode 100644
index 0000000..978117a
--- /dev/null
+++ b/state/vendor-onboarding.json
@@ -0,0 +1 @@
+{"name":"vendor-onboarding","cwd":"/root/DW-Agents/vendor-onboarding-agent","script":"/root/DW-Agents/vendor-onboarding-agent/server.js","interpreter":"node","args":[]}
diff --git a/state/yolo-agent.json b/state/yolo-agent.json
new file mode 100644
index 0000000..de585ab
--- /dev/null
+++ b/state/yolo-agent.json
@@ -0,0 +1 @@
+{"name":"yolo-agent","cwd":"/root/DW-Agents/yolo-agent","script":"/root/DW-Agents/yolo-agent/server.js","interpreter":"node","args":[]}
(oldest)
·
back to Pm2 Migration
·
snapshot — gitify backup 2026-05-19 a844dde →