[object Object]

← back to Wallco Ai

snapshot before reject sweep (preserve parallel-session in-progress work)

5778b04d1b864be351c8985f67c4f785f4fbc8f8 · 2026-05-26 20:00:33 -0700 · Steve Abrams

Files touched

Diff

commit 5778b04d1b864be351c8985f67c4f785f4fbc8f8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 26 20:00:33 2026 -0700

    snapshot before reject sweep (preserve parallel-session in-progress work)
---
 MORNING-HANDOFF-2026-05-26.md          | 116 ++++++++++++++
 data/edge-heal-pass-ids.json           |   1 +
 data/fixes-feed.jsonl                  |   1 +
 data/seam-fix-v2/A_snap2.png           | Bin 0 -> 45786 bytes
 data/seam-fix-v2/B_snap2_medianmid.png | Bin 0 -> 45453 bytes
 data/seam-fix-v2/C_roll_snap2.png      | Bin 0 -> 45773 bytes
 findings-synthesis-2026-05-26.md       | 100 ++++++++++++
 scratch_fuzz_remediate.py              |  92 +++++++++++
 scratch_fuzz_scan.py                   | 100 ++++++++++++
 scripts/batch_build_tifs.py            |  37 +++++
 scripts/fix-seam.py                    |  22 +++
 scripts/promote_edge_heals.py          |  72 +++++++++
 scripts/vision_curate_edgeheals.py     | 124 +++++++++++++++
 tomorrow-25min-fix.sh                  | 175 +++++++++++++++++++++
 tomorrow-after-25min.sh                | 280 +++++++++++++++++++++++++++++++++
 15 files changed, 1120 insertions(+)

diff --git a/MORNING-HANDOFF-2026-05-26.md b/MORNING-HANDOFF-2026-05-26.md
new file mode 100644
index 0000000..2cc1cd9
--- /dev/null
+++ b/MORNING-HANDOFF-2026-05-26.md
@@ -0,0 +1,116 @@
+# Morning Handoff — 2026-05-26
+
+Read this before coffee. Run the scripts after.
+
+---
+
+## 1. The 30-Second Read
+
+- **wallco.ai is crash-looping (135 pm2 restarts).** Root cause: two handlers (`server.js:17033` + `:18183`) query the WRONG TABLE — `spoon_all_designs` on prod, where the catalog actually lives in `all_designs`. Fix is mechanical, ~25 min, scripted.
+- **`commit-agent` is missing from pm2 (57 days). DW-Agents is 696 commits / 71 days unpushed to Stevemdr.** If Kamatera dies tonight, every commit since 2026-03-16 is gone from off-site. One-line restart in Phase 2.
+- **Two action scripts are ready** at `~/Projects/wallco-ai/` — Phase 1 (25 min, critical) and Phase 2 (follow-up). Both default to dry-run; `--execute` flips them live.
+- **Yuri pipeline ran clean overnight: 62/67 (92.5%) success.** 30+ findings docs synthesized — see `findings-synthesis-2026-05-26.md`.
+- **One open question before Phase 2:** Hollywood Wallcoverings might be Steve's house brand, not a vendor. Confirm before the vendor-name sweep runs against it.
+
+---
+
+## 2. Action Scripts Ready
+
+Both scripts live at `/Users/stevestudio2/Projects/wallco-ai/` and default to **dry-run**. Preview first, then re-run with `--execute`.
+
+### Phase 1 — `tomorrow-25min-fix.sh` (~25 min, CRITICAL)
+1. Patch `server.js:17033` + `:18183` table-name bug (spoon_all_designs → all_designs)
+2. `./deploy-kamatera.sh` + verify pm2 restarts settle
+3. Kill momentum-crawler (155 restarts, ~300K wasted requests, Cloudflare-ban risk)
+4. Delete 3 ghost wallpaper sites
+5. Print revoke instructions for the top-2 `ghp_*` GitHub PATs
+
+```bash
+./tomorrow-25min-fix.sh            # preview
+./tomorrow-25min-fix.sh --execute  # commit
+```
+
+### Phase 2 — `tomorrow-after-25min.sh` (follow-up)
+1. Register + start `commit-agent` in pm2, then `git push` 696 commits to **Stevemdr**/DW-Agents
+2. Vendor-name redaction sweep across the top-10 sister sites
+3. Mac1 health probe (DeepSeek/Phi4/Mistral were returning 0-byte responses last night)
+4. Full `ghp_*` revocation checklist
+
+```bash
+./tomorrow-after-25min.sh            # preview
+./tomorrow-after-25min.sh --execute  # commit
+```
+
+> **Open question (block Phase 2 step 2 if unsure):** Is "Hollywood Wallcoverings" a vendor name or Steve's own house brand? If house brand, drop it from the denylist before running the sweep. Tag is in `MEMORY.md` under `project_dw_vendor_title_leak_20260526`.
+
+---
+
+## 3. What's Fixed Already (no further action)
+
+| Item | When | State |
+|------|------|-------|
+| `callscompleted` agent | 22:02 UTC | `npm install` + pm2 restart; healthy on :9795 |
+| Butlr historic syntax error | 17:06 UTC | patched + live traffic flowing |
+| `norma-backup` `.git` restoration | this session | working tree tarball backup + alert path landed |
+| Grid-card `<div>` wrapper | verified | live on /designs (verifier probe pass) |
+| Vendor-name denylist on /designs | verified | live (verifier probe pass) |
+| Sort + density controls | verified | live (verifier probe pass) |
+| "Private Label" → "Exclusive" copy swap | verified | live (verifier probe pass) |
+| FLIEPAPER redaction | verified | live (verifier probe pass) |
+| `/api/designs` PII path leak | verified | closed (verifier probe pass) |
+| Cron alert paths | verified | failure-side test passed (verifier probe pass) |
+| Backup cadence | verified | 4/4 cron backups within window (verifier probe pass) |
+| CNCP refactor | 4 local commits | `jsonStore` helper, `/api/assets` + `/api/mystack` canary, `URLSearchParams` eslint, `npm lint` script extension |
+
+**8 of 10 verifier probes confirmed prior session fixes ARE live in prod.**
+
+---
+
+## 4. What's Still Open (ranked)
+
+| Sev | Item | Where it's handled |
+|-----|------|--------------------|
+| **CRITICAL** | wallco-ai 135 restarts (table-name bug) | Phase 1 |
+| **CRITICAL** | `commit-agent` missing + 696 unpushed commits to Stevemdr | Phase 2 step 1 |
+| HIGH | momentum-crawler 155 restarts, ~300K wasted requests, Cloudflare-ban risk | Phase 1 step 3 |
+| HIGH | 3 ghost wallpaper sites | Phase 1 step 4 |
+| HIGH | 16 exposed credentials (top-2 `ghp_*` are dead-simple revokes) | Phase 1 step 5 + Phase 2 step 4 |
+| HIGH | 73 of 91 `products.json` files leak vendor names | Phase 2 step 2 (top-11 in this pass) |
+| MED | 13 sister sites at SEO doorway-page risk (Cluster 01 + 02) | `findings-synthesis-2026-05-26.md` §SEO doorway audit |
+| MED | SDXL conv-padding architectural defect | manual: install `comfyui-seamless-tiling` ComfyUI custom node (Mac1) |
+| LATER | Mac1 panel timeouts (DeepSeek/Phi4/Mistral 0-byte responses) | Phase 2 step 3 probes |
+
+Full backlog + null findings: `~/Projects/wallco-ai/findings-synthesis-2026-05-26.md`.
+
+---
+
+## 5. Yuri Pipeline Stats
+
+- **Runs:** 67
+- **Success:** 62
+- **Fail:** 5
+- **Success rate:** 92.5%
+- **Findings docs produced:** 30+ at `/root/DW-Agents/yolo-agent/findings/` (Kamatera; pull via `ssh my-server`)
+
+5 failures were all transient (Anthropic rate-limit blip + Mac1 0-byte responses). None lost real work; codified in `feedback_yolo_loop_rate_limit_retry`.
+
+---
+
+## 6. The One Thing That Surprised Us
+
+**The wallco-ai crash root cause was a WRONG-TABLE query, not a missing column.**
+
+Two separate handlers (`server.js:17033` and `:18183`) read from `spoon_all_designs` — the table name Mac2's local catalog uses. Prod's actual catalog lives in `all_designs`. Both handlers had been silently throwing the same Postgres "relation does not exist" error on every request, pm2 was re-raising, and the auto-restart loop hit 135 before anyone noticed because the crash signature looked like an OOM / boot-time-syntax pattern.
+
+This is the same architectural class as the `db-backup.sh` 11-day silent outage — code-paths that fail without producing a visible artifact. The fix is two `s/spoon_all_designs/all_designs/g` edits on the two specific lines (not a global replace — other handlers genuinely DO want `spoon_all_designs` on Mac2's local DB during dev). Phase 1 script handles the surgical patch.
+
+Standing-rule reinforcement: every catalog-touching handler needs an explicit table-name comment naming the environment it targets. Tracked as a follow-up TODO in Phase 2 retrospective.
+
+---
+
+## Source Docs
+
+- Executive synthesis: `~/Projects/wallco-ai/findings-synthesis-2026-05-26.md`
+- Action script Phase 1: `~/Projects/wallco-ai/tomorrow-25min-fix.sh`
+- Action script Phase 2: `~/Projects/wallco-ai/tomorrow-after-25min.sh`
+- Raw Yuri findings: `ssh my-server "ls /root/DW-Agents/yolo-agent/findings/"`
diff --git a/data/edge-heal-pass-ids.json b/data/edge-heal-pass-ids.json
new file mode 100644
index 0000000..a632e54
--- /dev/null
+++ b/data/edge-heal-pass-ids.json
@@ -0,0 +1 @@
+[50603, 50604, 50605, 50606, 50607, 50608, 50610, 50611, 50612, 50613, 50615, 50616, 50617, 50620, 50621, 50623, 50633, 50634, 50636, 50638, 50640, 50641, 50647, 50648, 50650, 50651, 50652, 50656, 50657, 50658, 50659, 50660, 50661, 50662, 50663, 50664, 50666, 50667, 50671, 50672, 50673, 50674, 50679, 50681, 50682, 50683, 50684, 50685, 50686, 50687, 50689, 50690, 50691, 50692, 50693, 50694, 50695, 50697, 50698, 50700, 50701, 50702, 50703, 50704, 50705, 50706, 50708, 50710, 50713, 50714, 50717, 50721, 50723, 50725, 50728, 50729, 50730, 50731, 50732, 50734, 50737, 50738, 50740, 50741, 50742, 50743, 50744, 50747, 50748, 50751, 50753, 50755, 50756, 50757, 50758, 50759, 50762, 50763, 50765, 50766, 50767, 50768, 50769, 50770, 50773, 50774, 50776, 50777, 50778, 50783, 50786, 50787, 50788, 50790, 50793, 50797, 50803, 50805, 50807, 50811, 50813, 50815, 50816, 50817, 50818, 50819, 50820, 50821, 50822, 50823, 50824, 50825, 50826, 50827, 50828, 50830, 50831, 50832, 50833, 50834, 50835, 50837, 50838, 50840, 50841, 50842, 50843, 50844, 50845, 50846, 50849, 50850, 50852, 50856, 50857, 50858, 50859, 50860, 50861, 50862, 50863, 50865, 50866, 50867, 50868, 50878, 50880, 50881, 50883, 50886, 50887, 50888, 50889, 50890, 50892, 50894, 50895, 50896, 50899, 50900, 50901, 50904, 50905, 50906, 50909, 50910, 50911, 50913, 50914, 50915, 50916, 50917, 50920, 50922, 50923, 50924, 50925, 50927, 50929, 50930, 50931, 50932, 50933, 50934, 50936, 50938, 50940, 50941, 50942, 50943, 50944, 50945, 50946, 50947, 50950, 50951, 50952, 50953, 50954, 50956, 50958, 50959, 50964, 50967, 50968, 50969, 50970, 50971, 50977, 50978, 50979, 50980, 50981, 50982, 50983, 50987, 50988, 50989, 50991, 50994, 50997, 51001, 51003, 51004, 51006, 51007, 51008, 51010, 51011, 51012, 51016, 51018, 51021, 51022, 51025, 51027, 51029, 51032, 51034, 51038, 51043, 51045, 51048, 51049, 51050, 51051, 51052, 51054, 51056, 51058, 51060, 51061, 51062, 51064, 51065, 51066, 51067, 51069, 51070, 51074, 51080, 51082, 51085, 51092, 51095, 51096, 51097, 51098, 51099, 51101, 51102, 51104, 51105, 51108, 51110, 51111, 51113, 51114, 51115, 51116, 51117, 51120, 51122, 51123, 51125, 51126, 51127, 51129, 51130, 51131, 51132, 51135, 51136, 51137, 51138, 51139, 51140, 51142, 51143, 51144, 51145, 51146, 51147, 51148, 51151, 51152, 51153, 51154, 51155, 51156, 51157, 51158, 51159, 51160, 51161, 51162, 51163, 51164, 51165, 51166, 51167, 51168, 51169, 51170, 51171, 51172, 51173, 51174, 51175, 51176, 51177, 51178, 51179, 51180, 51181, 51182, 51183, 51184, 51185, 51186, 51187, 51188, 51189, 51190, 51191, 51192, 51193, 51194, 51195, 51196, 51197, 51198, 51199, 51200, 51204, 51205, 51206, 51207, 51208, 51209, 51210, 51211, 51212, 51214, 51215, 51216, 51217, 51218, 51222, 51223, 51224, 51225, 51226, 51228, 51229, 51230, 51231, 51232, 51233, 51234, 51235, 51236, 51237, 51238, 51252, 51253, 51254, 51255, 51256, 51257, 51258, 51259, 51260, 51261, 51262, 51263, 51264, 51266, 51267, 51271, 51274, 51275, 51276, 51277, 51278, 51279, 51280, 51281, 51283, 51284, 51285, 51286, 51287, 51288, 51289, 51290, 51291, 51292, 51294, 51296, 51297, 51299, 51301, 51302, 51303, 51304, 51305, 51306, 51307, 51308, 51309, 51310, 51311, 51312, 51313, 51314, 51315, 51316, 51317, 51318, 51319, 51320, 51321, 51322, 51323, 51324, 51325, 51326, 51327, 51329, 51336, 51337, 51338, 51339, 51340, 51342, 51343, 51344, 51345, 51346, 51347, 51348, 51349, 51350, 51351, 51352, 51353, 51354, 51355, 51356, 51357, 51358, 51359, 51363, 51364, 51365, 51366, 51367, 51368, 51369, 51370, 51371, 51372, 51376, 51377, 51378, 51379, 51380, 51381, 51384, 51385, 51386, 51387, 51388, 51393, 51394, 51395, 51396, 51397, 51398, 51399, 51400, 51401, 51402, 51403, 51404, 51405, 51406, 51407, 51408, 51409, 51410, 51411, 51412, 51413, 51414, 51415, 51416, 51417, 51418, 51419, 51420, 51421, 51422, 51423, 51424, 51425, 51426, 51427, 51428, 51430, 51432, 51433, 51434, 51435, 51436, 51438, 51440, 51441, 51445, 51446, 51447, 51448, 51449, 51450, 51451, 51452, 51453, 51455, 51456, 51457, 51458, 51459, 51460, 51461, 51462, 51463, 51464, 51469, 51473, 51474, 51475, 51476, 51477, 51478, 51479, 51480, 51481, 51482, 51483, 51484, 51485, 51486, 51487, 51488, 51489, 51490, 51491, 51492, 51495, 51496, 51497, 51498, 51499, 51500, 51501, 51502, 51503, 51505, 51509, 51510, 51511, 51512, 51513, 51516, 51517, 51518, 51519, 51520, 51523, 51526, 51527, 51528, 51529, 51530, 51531, 51532, 51533, 51535, 51537, 51541, 51542, 51543, 51545, 51547, 51548, 51550, 51551, 51552, 51553, 51554, 51555, 51556, 51557, 51558, 51559, 51560, 51561, 51562, 51567, 51568, 51569, 51570, 51573, 51574, 51575, 51576, 51577, 51580, 51581, 51582, 51588, 51589, 51590, 51591, 51592, 51595, 51596, 51597, 51601, 51602, 51603, 51604, 51605, 51606, 51607, 51608, 51610, 51611, 51612, 51613, 51614, 51615, 51616, 51617, 51618, 51621, 51623, 51625, 51626, 51627, 51632, 51633, 51634, 51635, 51636, 51637, 51638, 51639, 51640, 51644, 51645, 51646, 51647, 51648, 51651, 51652, 51653, 51654, 51655, 51658, 51659, 51660, 51663, 51665, 51666, 51667, 51668, 51669, 51670, 51674, 51675, 51676, 51678, 51679, 51680, 51681, 51682, 51683, 51685, 51686, 51687, 51688, 51689, 51690, 51691, 51692, 51693, 51694, 51695, 51696, 51699, 51700, 51701, 51704, 51709, 51710, 51711, 51712, 51713, 51714, 51715, 51716, 51717, 51718, 51719, 51721, 51722, 51723, 51724, 51725, 51726, 51727, 51728, 51731, 51732, 51733, 51734, 51735, 51736, 51737, 51738, 51739, 51741, 51745, 51746, 51747, 51748, 51750, 51751, 51752, 51754, 51755, 51756, 51757, 51759, 51761, 51762, 51763, 51765, 51766, 51767, 51768, 51769, 51770, 51771, 51772, 51773, 51774, 51775, 51776, 51777, 51778, 51779, 51780, 51781, 51782, 51784, 51785, 51786, 51787, 51788, 51789, 51790, 51791, 51792, 51793, 51798, 51801, 51802, 51803, 51805, 51806, 51807, 51810, 51811, 51812, 51813, 51814, 51815, 51816, 51820, 51821, 51822, 51823, 51824, 51825, 51826, 51827, 51828, 51830, 51831, 51832, 51833, 51834, 51835, 51837, 51838, 51839, 51840, 51841, 51842, 51843, 51844, 51845, 51846, 51847, 51848, 51849, 51850, 51851, 51852, 51853, 51854, 51855, 51856, 51857, 51858, 51859, 51860, 51861, 51863, 51865, 51866, 51867, 51868, 51869, 51870, 51871, 51872, 51873, 51874, 51875, 51876, 51877, 51878, 51879, 51880, 51881, 51882, 51883, 51885, 51886, 51887, 51888, 51889, 51890, 51891, 51892, 51893, 51894, 51898, 51899, 51902, 51903, 51904, 51910, 51911, 51912, 51918, 51919, 51920, 51921, 51924, 51925, 51927, 51928, 51929, 51930, 51931, 51937, 51938, 51940, 51941, 51942, 51943, 51944, 51947, 51948, 51949, 51950, 51951, 51952, 51953, 51954, 51955, 51957, 51958, 51959, 51961, 51962, 51964, 51966, 51967, 51968, 51971, 51973, 51974, 51975, 51976, 51980, 51981, 51982, 51984, 51985, 51987, 51988, 51989, 51990, 51991, 51992, 51993, 51994, 51995, 51996, 52000, 52001, 52002, 52003, 52004, 52005, 52006, 52008, 52009, 52010, 52011, 52012, 52014, 52015, 52016, 52017, 52018, 52024, 52025, 52026, 52027, 52028, 52030, 52031, 52032, 52033, 52034, 52035, 52036, 52037, 52038, 52039, 52041, 52043, 52044, 52045, 52046, 52047, 52048, 52049, 52050, 52051, 52052, 52053, 52054, 52055, 52056, 52057, 52058, 52061, 52062, 52063, 52064, 52068, 52069, 52070, 52071, 52073, 52074, 52078, 52079, 52080, 52082, 52083, 52084, 52085, 52086, 52087, 52088, 52089, 52090, 52091, 52092, 52093, 52094, 52095, 52096, 52097, 52099, 52100, 52102, 52103, 52104, 52105, 52106, 52107, 52108, 52109, 52110, 52111, 52112, 52114, 52115, 52116, 52117, 52118, 52121, 52122, 52123, 52124, 52125, 52126, 52127, 52128, 52129, 52130, 52132, 52133, 52134, 52135, 52136, 52137, 52138, 52139, 52141, 52142, 52143, 52144, 52145, 52146, 52147, 52148, 52149, 52150, 52151, 52152, 52153, 52156, 52158, 52160, 52161, 52162, 52165, 52166, 52169, 52170, 52171, 52172, 52174, 52175, 52176, 52177, 52178, 52179, 52180, 52181, 52182, 52183, 52184, 52185, 52186, 52187, 52188, 52189, 52190, 52191, 52192, 52195, 52196, 52197, 52198, 52199, 52200, 52201, 52202, 52203, 52204, 52205, 52206, 52207, 52208, 52209, 52210, 52211, 52212, 52214, 52215, 52217, 52218, 52223, 52224, 52225, 52226, 52227, 52228, 52229, 52230, 52231, 52232, 52233, 52234, 52235, 52236, 52237, 52238, 52239, 52240, 52241, 52242, 52243, 52244, 52245, 52246, 52247, 52248, 52249, 52251, 52252, 52253, 52254, 52255, 52256, 52257, 52258, 52259, 52260, 52261, 52262, 52263, 52264, 52265, 52266, 52267, 52268, 52269, 52270, 52271, 52272, 52273, 52274, 52275, 52276, 52277, 52278, 52279, 52280, 52281, 52282, 52283, 52284, 52285, 52286, 52287, 52288, 52289, 52290, 52291, 52292, 52293, 52294, 52295, 52296, 52301, 52302, 52303, 52304, 52305, 52306, 52307, 52308, 52310, 52311, 52312, 52313, 52314, 52315, 52317, 52318, 52319, 52320, 52321, 52322, 52323, 52324, 52325, 52326, 52327, 52332, 52333, 52334, 52337, 52338, 52339, 52340, 52341, 52342, 52343, 52344, 52345, 52347, 52348, 52349, 52350, 52352, 52353, 52356, 52357, 52358, 52359, 52360, 52361, 52362, 52363, 52364, 52365, 52366, 52367, 52368, 52369, 52370, 52371, 52372, 52374, 52375, 52376, 52381, 52382, 52383, 52384, 52385, 52386, 52387, 52388, 52389, 52390, 52392, 52397, 52398, 52399, 52400, 52401, 52407, 52408, 52409, 52410, 52411, 52412, 52413, 52414, 52415, 52416, 52417, 52418, 52419, 52420, 52421, 52422, 52423, 52424, 52425, 52426, 52427, 52428, 52429, 52430, 52431, 52432, 52433, 52434, 52435, 52436, 52437, 52438, 52439, 52440, 52441, 52442, 52443, 52444, 52445, 52446, 52447, 52449, 52450, 52451, 52452, 52453, 52454, 52455, 52456, 52459, 52460, 52461, 52462, 52463, 52464, 52465, 52466, 52467, 52468, 52469, 52470, 52471, 52472, 52473, 52477, 52479, 52481, 52482, 52483, 52484, 52485, 52486, 52487, 52488, 52490, 52491, 52492, 52493, 52494, 52495, 52496, 52497, 52498, 52499, 52500, 52504, 52509, 52510, 52511, 52512, 52513, 52514, 52516, 52519, 52520, 52521, 52523, 52524, 52525, 52526, 52527, 52528, 52529, 52531, 52532, 52533, 52534, 52535, 52539, 52540, 52541, 52545, 52546, 52547, 52548, 52549, 52550, 52551, 52552, 52556, 52557, 52558, 52559, 52562, 52569, 52570, 52571, 52574, 52575, 52576, 52577, 52578, 52581, 52582, 52583, 52584, 52585, 52586, 52587, 52588, 52590, 52591, 52592, 52593, 52596, 52597, 52598, 52599, 52600, 52601, 52602, 52603, 52604, 52605, 52606, 52607, 52609, 52611, 52612, 52614, 52615, 52617, 52621, 52622, 52623, 52624, 52625, 52629, 52630, 52631, 52632, 52633, 52634, 52635, 52636, 52639, 52640, 52641, 52642, 52643, 52644, 52645, 52646, 52647, 52648, 52649, 52650, 52651, 52652, 52653, 52654, 52655, 52656, 52657, 52658, 52659, 52660, 52661, 52662, 52663, 52664, 52665, 52666, 52667, 52668, 52669, 52670, 52671, 52672, 52673, 52674, 52675, 52676, 52677, 52678, 52679, 52680, 52681, 52682, 52683, 52684, 52685, 52686, 52687, 52690, 52691, 52692, 52693, 52694, 52695, 52696, 52697, 52698, 52700, 52701, 52702, 52703, 52704, 52707, 52708, 52709, 52710, 52711, 52712, 52713, 52714, 52715, 52716, 52721, 52722, 52725, 52726, 52727, 52731, 52733, 52734, 52735, 52736, 52737, 52738, 52740, 52741, 52742, 52743, 52744, 52745, 52747, 52748, 52749, 52750, 52751, 52753, 52754, 52755, 52756, 52757, 52758, 52759, 52760, 52761, 52762, 52763, 52764, 52765, 52766, 52768, 52769, 52770, 52771, 52772, 52773, 52774, 52775, 52776, 52777, 52780, 52781, 52782, 52783, 52784, 52786, 52787, 52788, 52789, 52791, 52792, 52793, 52794, 52795, 52796, 52797, 52798, 52799, 52802, 52803, 52804, 52805, 52806, 52807, 52808, 52809, 52810, 52811, 52812, 52813, 52814, 52815, 52816, 52817, 52818, 52819, 52820, 52821, 52822, 52823, 52824, 52825, 52826, 52827, 52828, 52829, 52830, 52831, 52832, 52837, 52840, 52843, 52844, 52845, 52846, 52847, 52848, 52850, 52851, 52852, 52853, 52855, 52859, 52860, 52861, 52865, 52866, 52867, 52868, 52869, 52870, 52871, 52872, 52873, 52874, 52875, 52876, 52877, 52878, 52879, 52880, 52881, 52882, 52883, 52884, 52885, 52886, 52887, 52888, 52889, 52890, 52891, 52892, 52893, 52894, 52895, 52896, 52898, 52899, 52901, 52902, 52903, 52904, 52905, 52906, 52907, 52908, 52909, 52910, 52911, 52912, 52913, 52915, 52917, 52918, 52919, 52920, 52921, 52922, 52923, 52924, 52925, 52926, 52927, 52928, 52929, 52930, 52931, 52932, 52933, 52937, 52938, 52939, 52940, 52941, 52942, 52943, 52944, 52945, 52946, 52948, 52949, 52950, 52951, 52952, 52953, 52954, 52956, 52957, 52958, 52959, 52960, 52961, 52962, 52963, 52968, 52969, 52973, 52974, 52975, 52984, 52985, 52986, 52987, 52988, 52989, 52990, 52991, 52992, 52993, 52994, 52995, 52996, 52999, 53000, 53003, 53004, 53005, 53006, 53007, 53009, 53010, 53011, 53012, 53013, 53014, 53016, 53017, 53018, 53019, 53021, 53022, 53025, 53026, 53027, 53028, 53029, 53030, 53031, 53032, 53035, 53036, 53037, 53038, 53039, 53040, 53041, 53042, 53043, 53044, 53045, 53046, 53047, 53048, 53052, 53053, 53054, 53055, 53056, 53057, 53058, 53059, 53060, 53061, 53062, 53063, 53064, 53065, 53066, 53067, 53068, 53069, 53070, 53071, 53072, 53073, 53074, 53075, 53076, 53077, 53078, 53080, 53081, 53082, 53083, 53084, 53088, 53089, 53090, 53091, 53092, 53094, 53095, 53096, 53097, 53099, 53100, 53101, 53102, 53103, 53104, 53105, 53106, 53107, 53108, 53109, 53110, 53112, 53113, 53114, 53115, 53116, 53117, 53118, 53119, 53120, 53121, 53122, 53123, 53124, 53125, 53126, 53127, 53128, 53129, 53130, 53132, 53133, 53136, 53137, 53138, 53139, 53140, 53141, 53142, 53143, 53144, 53145, 53146, 53149, 53150, 53153, 53158, 53159, 53165, 53166, 53167, 53168, 53169, 53170, 53171, 53172, 53173, 53174, 53175, 53176, 53178, 53180, 53181, 53182, 53183, 53185, 53186, 53187, 53188, 53189, 53190, 53191, 53192, 53194, 53195, 53196, 53197, 53198, 53199, 53200, 53201, 53202, 53203, 53204, 53205, 53206, 53207, 53208, 53209, 53210, 53211, 53212, 53213, 53214, 53215, 53216, 53217, 53218, 53219, 53220, 53222, 53223, 53224, 53225, 53226, 53227, 53228, 53229, 53230, 53231, 53232, 53233, 53234, 53235, 53238, 53239, 53240, 53241, 53242, 53243, 53244, 53249, 53250, 53251, 53252, 53253, 53254, 53255, 53256, 53257, 53258, 53259, 53260, 53261, 53262, 53263, 53264, 53265, 53266, 53267, 53268, 53269, 53270, 53271, 53272, 53273, 53274, 53275, 53276, 53277, 53278, 53279, 53280, 53281, 53282, 53283, 53284, 53285, 53286, 53287, 53288, 53289, 53290, 53291, 53292, 53293, 53294, 53295, 53296, 53297, 53298, 53299, 53300, 53301, 53302, 53303, 53304, 53305, 53306, 53310, 53311, 53312, 53319, 53320, 53321, 53322, 53323, 53324, 53325, 53326, 53327, 53328, 53329, 53330, 53331, 53332, 53333, 53334, 53335, 53336, 53346, 53347, 53348, 53349, 53350, 53351, 53352, 53353, 53354, 53355, 53357, 53358, 53359, 53360, 53361, 53362, 53363, 53364, 53365, 53366, 53367, 53368, 53369, 53370, 53371, 53372, 53373, 53374, 53375, 53376, 53377, 53378, 53380, 53381, 53382, 53383, 53384, 53385, 53386, 53387, 53388, 53389, 53390, 53391, 53392, 53393, 53394, 53395, 53396, 53397, 53398, 53399, 53400, 53402, 53403, 53405, 53406, 53407, 53411, 53412, 53413, 53414, 53415, 53417, 53418, 53419, 53421, 53422, 53423]
\ No newline at end of file
diff --git a/data/fixes-feed.jsonl b/data/fixes-feed.jsonl
index 49b4cdc..8443dcc 100644
--- a/data/fixes-feed.jsonl
+++ b/data/fixes-feed.jsonl
@@ -1827,3 +1827,4 @@
 {"ts":"2026-05-25T07:41:29.127Z","kind":"crop-fix","fix_kind":"ghost-layer","src_id":41509,"new_id":41551,"elapsed_s":"7.6","region":{"x":19.923371647509576,"y":19.923371647509576,"w":40.03831417624521,"h":40.03831417624521},"keep_count":0}
 {"ts":"2026-05-25T07:56:32.903Z","kind":"fix","src_id":41509,"new_id":41552,"elapsed_s":"0.3","palette":["#111863","#83524f","#dc7936","#e0d5d2"],"k":4,"crop":1}
 {"ts":"2026-05-25T07:56:43.941Z","kind":"crop-fix","fix_kind":"ghost-layer","src_id":41509,"new_id":41553,"elapsed_s":"7.5","region":{"x":25,"y":25,"w":39.94252873563218,"h":39.94252873563218},"keep_count":0}
+{"ts":"2026-05-26T22:18:22.674Z","kind":"replace-bg","src_id":50603,"new_id":53584,"elapsed_s":"8.0","texture":"adhoc","texture_name":"Abaca Alabaster"}
diff --git a/data/seam-fix-v2/A_snap2.png b/data/seam-fix-v2/A_snap2.png
new file mode 100644
index 0000000..54d0886
Binary files /dev/null and b/data/seam-fix-v2/A_snap2.png differ
diff --git a/data/seam-fix-v2/B_snap2_medianmid.png b/data/seam-fix-v2/B_snap2_medianmid.png
new file mode 100644
index 0000000..a2e2978
Binary files /dev/null and b/data/seam-fix-v2/B_snap2_medianmid.png differ
diff --git a/data/seam-fix-v2/C_roll_snap2.png b/data/seam-fix-v2/C_roll_snap2.png
new file mode 100644
index 0000000..e0d6fc2
Binary files /dev/null and b/data/seam-fix-v2/C_roll_snap2.png differ
diff --git a/findings-synthesis-2026-05-26.md b/findings-synthesis-2026-05-26.md
new file mode 100644
index 0000000..0cfd5f9
--- /dev/null
+++ b/findings-synthesis-2026-05-26.md
@@ -0,0 +1,100 @@
+# Yuri Findings — Executive Synthesis · 2026-05-26
+
+**Source:** 28 findings docs at `/root/DW-Agents/yolo-agent/findings/` on Kamatera (ssh my-server)
+**Scope:** Today's autonomous YOLO run, ticks #00003 → #00031
+**Mode:** Read-only synthesis. SDCC excluded. Secrets redacted to first 8 chars + `…`.
+
+---
+
+## TOP-5 IMMEDIATE ACTIONS (next 24 hours)
+
+| # | Sev | Action | Source |
+|---|-----|--------|--------|
+| 1 | **CRITICAL** | Restart `commit-agent` pm2 and `git push origin master` — local DW-Agents repo is **696 commits ahead, 71 days unpushed** (since 2026-03-16). If Kamatera dies tonight, every commit since mid-March is lost from off-site mirror. | `2026-05-26-backup-cadence-audit.md` §2-3 |
+| 2 | **CRITICAL** | Rotate Shopify Admin token `shpat_82518d…` (hard-coded in **60+ tracked files across 7 repos**) + 6 sibling `shpat_…` + 8 `sk-ant-api03-…` + billed Gemini `AIzaSyAO…` + 2 GitHub `ghp_…` PATs + Google OAuth `ya29.…` in `Norma/scripts/search-sacbee.js`. History rewrites needed on Designer-Wallcoverings + dear-bubbe-nextjs. | `2026-05-26-secrets-committed-key-sweep.md` §1-3 |
+| 3 | **HIGH** | Fix wallco.ai `/api/collections/fliepaper-bugs` unauthenticated leak — currently exposes `brand_reference:"Gucci"` + `owned_by_steve:true` for a private-label collection. Same class as the 2026-05-20 vendor-name regression. 14 total unauthenticated `/api/*` GETs leak internal AI prompts, KPIs, internal IP:ports. | `2026-05-26-wallco-api-admin-gate-audit.md` §TLDR |
+| 4 | **HIGH** | `norma-backup.sh` silently failing daily since 2026-05-22 — `/root/Projects/Norma/.git` deleted by a Mac→Linux rsync, Norma never had a GitHub remote, history is gone. Fix: `git init` fresh + working-tree tarball backup (DTD verdict B reconciled). | `2026-05-26-silent-backups-investigation.md` §1 + `2026-05-26-untested-alert-paths.md` #6 |
+| 5 | **HIGH** | Take action on **13 sister sites at sev-4 SEO doorway-page risk** — 7 commercial-umbrella + 6 generic "all-wallcovering" serving the same 4000-SKU catalog under different domains (literal scaled-content abuse per Google's March 2024 update). DTD 3/3 unanimous: 301-redirect to designerwallcoverings.com. | `2026-05-26-seo-doorway-audit.md` §TLDR |
+
+---
+
+## MEDIUM-PRIORITY BACKLOG (next week)
+
+| # | Action | Source |
+|---|--------|--------|
+| M1 | Patch `rotate-password.sh` — add fallback transport (`/root/.dw-auth.UNANNOUNCED-NEW` + pm2 banner) for double Slack+George failure. Current double-failure silently locks Steve out of 100+ services. | `…-alert-paths-test-plan.md` Test 1 |
+| M2 | Close remaining 13 wallco.ai unauthenticated leaks (`/api/generator/runs`, `/api/luxe-curator`, `/api/joint-fix-gallery`, `/api/studio/elements`, `/api/marketplace/events`+`watcher`) — exposes AI prompt strings (IP) and SCRAM auth errors. | `…-wallco-api-admin-gate-audit.md` §TLDR |
+| M3 | Fleet-wide `products.json` regeneration to strip vendor names from titles — 142 public-facing files leak vendors; `thibaut` alone = 7,316 hits. Plus immediate edits to `bestwallpaperplace/public/index.html` meta description (Google-indexed) + `goodquestion-ai` blog post. | `…-vendor-name-sweep.md` §P1+P2 |
+| M4 | Wire shared `/_shared/sort-density.js` + inject into Novasuede, GrassclothWallcoverings, GrassclothWallpaper.com (3 Cloudflare-prod sites missing controls). DTD 3/3 unanimous option B. | `…-sort-density-patch-plan.md` + `…-sort-density-fleet-audit.md` |
+| M5 | Install ComfyUI `comfyui-seamless-tiling` custom node (circular-padding patch on UNet+VAE conv) — closes drunk-* mid-seam + damask edge-wrap defects at the conv layer. Architectural ceiling; no prompt can override `padding_mode='zeros'`. | `…-damask-sampler-fix.md` + `…-drunk-mid-seam-rootcause.md` + `…-damask-edge-wrap-rootcause.md` |
+| M6 | Fix wallco.ai admin-hamburger CSS regression — spec wanted vertical UR stack (`top:66px right:14px`), live is horizontal (`top:14px right:62px`). `server.js:9620`. | `…-post-deploy-audit.md` Marker #4 |
+| M7 | Triage **3,144 uncommitted** files in `animate-museum-posts` (likely needs `.gitignore`) and **1,147** in `DW-Agents` root; checkpoint-commit the 160-file `Designer-Wallcoverings` ralph branch; anchor 2 detached-HEAD dirty repos (`canva-app/dw-product-sync`, `Projects/test`). | `…-fleet-git-audit.md` Flagged Repos |
+| M8 | Norma Gemini backoff — wire `p-retry`/`bottleneck` across 5 call sites (`lib/ingest-news.ts`, `lib/generator.ts`, `app/api/drafts/auto-generate/route.ts`, `agents/shared/ai.js`, 5 datasource-agent skills). 429 storm is historical but underlying weakness will reignite. | `…-norma-gemini-backoff-plan.md` §2 |
+| M9 | Add `tif_path` + `vector_eligible` + `vector_color_count` columns to `spoon_all_designs` / `all_designs`. Publish-gate spec assumes columns that have never been added; `/vector` sub-catalog blocked on this. | `…-tif-audit.md` + `…-vector-collection-ia.md` §0 |
+
+---
+
+## NULL FINDINGS (gates working — no violations)
+
+| Audit | Result | Source |
+|-------|--------|--------|
+| Published designs with NULL `local_path` | **0 violations** (column `tif_path` doesn't exist in schema, audited `local_path` instead) | `…-tif-audit.md` |
+| Edges-agent post-unpublish verify — FAIL/WARN still published | **0 leaks**, 23,221 unpublishes fully propagated | `…-edges-unpublish-verify.md` |
+| Edges-agent rescan top-10 newest — mid-seam regressions | **0 FAIL**, 1 borderline WARN (10165 v_mid=5.13) | `…-edges-rescan-top10.md` |
+| DTD stability sweep (5 back-to-back runs) | **5/5 healthy, 0 empties, 0 timeouts** — keep_alive:1h patch holding | `…-dtd-stability-sweep.md` |
+| db-backup.sh failure-side alert | **VERIFIED WORKING** — 2 [BACKUP FAILED] emails in George inbox (May 22+23 test runs) | `…-untested-alert-paths.md` #1 |
+| Kamatera disk pressure | Root 58%, no log >500MB | `…-kamatera-health.md` §1-3 |
+| Absolute-path leakage on wallco.ai public GETs | **No `/root/`, `/Users/`, `/home/` leaks** — 2026-05-22 regression closed on GETs | `…-wallco-api-admin-gate-audit.md` §TLDR |
+| Butlr SIGINT "loop" | **No active loop** — 5 SIGINTs = single 18-min Mac-deploy burst, quiet 10h+ since | `…-butlr-sigint-investigation.md` |
+| Yuri's own historical failures (resume proposals) | **0 resumes proposed** — DTD 2/2 unanimous, both real failures self-resolved on retry | `2026-05-25-resume-proposals.md` |
+| Mac1 Ollama "down" | **NOT down** — qwen3:14b ~2.5s; /dtd timeouts were a backwards cascade in `panel.sh` (Kamatera CPU before Mac1 GPU) | `…-mac1-ollama-revival.md` |
+| All `/api/admin/*` + `/api/review/*` + `/api/library/*` routes | Correctly gated 401/404; `src/admin-gate.js` HS256 sound | `…-wallco-api-admin-gate-audit.md` |
+| All 4 cron-driven backups within 48h cadence | db-backup 7h, norma-backup 4h (recovered), sf-backup 13h, db-backup-to-github 60h (within weekly) | `…-backup-cadence-audit.md` §2 |
+| 66 of 76 sister sites | Sort + density + grid + localStorage fully compliant | `…-sort-density-fleet-audit.md` |
+
+---
+
+## CROSS-CUTTING PATTERNS
+
+### Pattern 1 — "Silent failure" is the dominant defect class (5+ audits, same root cause)
+
+`commit-agent` missing from pm2 for 57 days · `norma-backup.sh` broken since 2026-05-22 · `sf-backup.sh` log 0 bytes since 2026-05-01 (turned out benign — empty DB — but only verified today) · `rotate-password.sh` double-failure mode never tested · `ssl-check.sh` alert branch likely never fired · cron-consolidate writes to a misnamed log file so freshness checks never alert.
+
+**Theme:** every ops process without a visible artifact AND no failure-side test will eventually fail invisibly. Codifies the 2026-05-23 standing rule (`feedback_alert_paths_need_failure_side_test`) as a fleet-wide pattern.
+
+### Pattern 2 — Vendor-name regression hitting from 4 angles, same root cause
+
+(a) Sister-site `products.json` title field — 142 files, 7,316 `thibaut` hits · (b) `bestwallpaperplace` index.html meta description (Google-indexed) · (c) `goodquestion-ai` blog post names DW's scraper targets · (d) wallco.ai `/api/collections/fliepaper-bugs` leaks `brand_reference:"Gucci"`.
+
+**Theme:** the 2026-05-20 rule (`feedback_never_dw_vendors_in_public_ui`) was enforced at the catalog/dropdown layer but NOT at the Shopify-export pipeline, per-site meta tags, editorial content, or admin-collection JSON. Single fix at the source closes all 4.
+
+### Pattern 3 — Schema spec drift: 3 audits referenced columns that don't exist
+
+`tif_path` (TIF audit), `vector_eligible` + `vector_color_count` (vector sub-catalog), `local_path` removed for PII without spec update. Audits being written against assumed schemas. **Fix:** single source-of-truth schema doc + migration runner that surfaces missing columns before audits reference them.
+
+### Pattern 4 — SDXL latent-grid is the structural ceiling on seamless designs
+
+Damask edge-wrap (prompt-level, partially fixed `7b2bfafa`) + drunk-* mid-seam (architectural, row 512 of 1024) are symmetric manifestations of `Conv2d(padding_mode='zeros')`. No prompt can override conv padding. Only durable fix is ComfyUI `comfyui-seamless-tiling` custom node patching UNet+VAE conv layers to `circular` — one-time install, zero per-image cost.
+
+---
+
+## TOP-1 HIGHEST-SEVERITY FINDING
+
+**`commit-agent` is missing from pm2 entirely (57 days), leaving DW-Agents 696 commits ahead, 71 days unpushed to GitHub.**
+
+If this Kamatera host dies tonight, every commit since 2026-03-16 — including today's Norma recovery, all YOLO work, the new alert paths, the agent registry changes — is lost from the off-site mirror. The local state looks healthy from any `git status` check; the silent half is the missing push. Same architectural class as the `db-backup.sh` 11-day outage, just with a process name that didn't start with `backup-` so the cadence audit's `backup-*` filter missed it.
+
+Source: `2026-05-26-backup-cadence-audit.md` §2 (HIGH severity).
+
+Mitigation is one command (NEEDS STEVE EXPLICIT AUTH per never-push-to-remotes rule):
+```
+pm2 start /root/DW-Agents/commit-agent/server.js --name commit-agent && cd /root/DW-Agents && git push origin master
+```
+
+---
+
+## REPORT META
+
+- **Files synthesized:** 28 (1 from 2026-05-25, 27 from 2026-05-26)
+- **SDCC findings:** none of the 28 docs were SDCC-related; no exclusions triggered
+- **Secrets redacted:** 5 distinct token prefixes shown only as first 8 chars + `…`
diff --git a/scratch_fuzz_remediate.py b/scratch_fuzz_remediate.py
new file mode 100644
index 0000000..c93468e
--- /dev/null
+++ b/scratch_fuzz_remediate.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+"""Remediate fuzzy pil-mid-heal designs via the 45842 pattern:
+unpublish the blurry heal, publish its CRISP parent source as a mural_panel.
+Only acts when the parent is verified crisp (low fuzz). Mac2-PG only — NO deploy.
+
+Modes:
+  analyze         -> report fixability for the target set (default top-204 >=45842-class)
+  apply  <N>      -> stage the swaps for the top N fixable (by loc) on Mac2 PG
+"""
+import os, sys, json, subprocess
+import scratch_fuzz_scan as s   # reuse scan()
+
+GEN = s.GEN_DIR
+J = "/Users/stevestudio2/.claude/yolo-queue/output/13-fuzzy-cohort-scan-2026-05-26.json"
+
+def psql(sql):
+    return subprocess.run(["psql", "dw_unified", "-tAc", sql],
+                          capture_output=True, text=True).stdout.strip()
+
+def parent_path(pid):
+    out = psql("SELECT COALESCE(local_path,'') FROM all_designs WHERE id=%d;" % pid)
+    if not out:
+        return None
+    p = out if os.path.exists(out) else os.path.join(GEN, os.path.basename(out))
+    return p if os.path.exists(p) else None
+
+def load_targets():
+    r = json.load(open(J))
+    # >=45842-class = the clear worst; ranked by localization
+    sev = [x for x in r if x["band"] >= 0.25 and x["loc"] >= 50]
+    sev.sort(key=lambda x: x["loc"], reverse=True)
+    return sev
+
+def fixability(targets):
+    rows = []
+    for x in targets:
+        cid = x["id"]
+        pid = psql("SELECT parent_design_id FROM all_designs WHERE id=%d;" % cid)
+        rec = {"child": cid, "loc": x["loc"], "band": x["band"], "parent": None,
+               "parent_crisp": False, "parent_loc": None, "status": ""}
+        if not pid or not pid.isdigit():
+            rec["status"] = "no-parent-id"; rows.append(rec); continue
+        pid = int(pid); rec["parent"] = pid
+        pp = parent_path(pid)
+        if not pp:
+            rec["status"] = "parent-png-missing"; rows.append(rec); continue
+        try:
+            b, rr, l = s.scan(pp)
+            rec["parent_loc"] = round(l, 1)
+            # crisp parent = no concentrated fuzzy cross
+            if l < 20 and b < 0.12:
+                rec["parent_crisp"] = True; rec["status"] = "FIXABLE"
+            else:
+                rec["status"] = "parent-also-fuzzy(loc%.0f)" % l
+        except Exception as e:
+            rec["status"] = "scan-err"
+        rows.append(rec)
+    return rows
+
+def main():
+    mode = sys.argv[1] if len(sys.argv) > 1 else "analyze"
+    targets = load_targets()
+    print("targets (>=45842-class):", len(targets), flush=True)
+    rows = fixability(targets)
+    fixable = [r for r in rows if r["parent_crisp"]]
+    print("FIXABLE (crisp parent):", len(fixable))
+    from collections import Counter
+    c = Counter(r["status"] if r["status"] == "FIXABLE" else r["status"].split("(")[0] for r in rows)
+    for k, v in c.most_common():
+        print("  %-22s %d" % (k, v))
+    json.dump(rows, open("/tmp/fuzz_remediate_plan.json", "w"), indent=0)
+    print("\nTOP 8 fixable swaps (child -> parent mural):")
+    for r in fixable[:8]:
+        print("  #%d (loc%.0f) -> publish parent #%d as mural" % (r["child"], r["loc"], r["parent"]))
+
+    if mode == "apply":
+        N = int(sys.argv[2]) if len(sys.argv) > 2 else 0
+        todo = fixable[:N]
+        print("\n=== APPLYING %d swaps on Mac2 PG (no deploy) ===" % len(todo))
+        for r in todo:
+            cid, pid = r["child"], r["parent"]
+            sql = ("UPDATE all_designs SET is_published=FALSE, unpublish_reason='fuzzy band-blur cross (loc%.0f); replaced by crisp mural #%d' WHERE id=%d; "
+                   "UPDATE all_designs SET is_published=TRUE, kind='mural_panel' WHERE id=%d;" % (r["loc"], pid, cid, pid))
+            psql(sql)
+        # verify
+        ids = ",".join(str(r["child"]) for r in todo) or "0"
+        pids = ",".join(str(r["parent"]) for r in todo) or "0"
+        print("children now:", psql("SELECT count(*) FROM all_designs WHERE id IN (%s) AND is_published=FALSE;" % ids), "unpublished")
+        print("parents now :", psql("SELECT count(*) FROM all_designs WHERE id IN (%s) AND is_published=TRUE AND kind='mural_panel';" % pids), "published murals")
+
+if __name__ == "__main__":
+    main()
diff --git a/scratch_fuzz_scan.py b/scratch_fuzz_scan.py
new file mode 100644
index 0000000..947e73a
--- /dev/null
+++ b/scratch_fuzz_scan.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+"""Read-only fuzz-cross scanner for the published pil-mid-heal cohort.
+Detects the band-blur 'fuzzy cross' signature (intermediate-color pixels
+concentrated in a center cross band). $0, local PIL+numpy. Writes a report.
+NOTE: scratch tool — does NOT modify any DB row, PNG, or designs.json.
+"""
+import os, sys, json, subprocess, time
+import numpy as np
+from PIL import Image
+
+GEN_DIR = "/Users/stevestudio2/Projects/wallco-ai/data/generated"
+OUT = "/Users/stevestudio2/.claude/yolo-queue/output/13-fuzzy-cohort-scan-2026-05-26.md"
+BAND_PX = 14            # half-width of the center cross band
+INTER_T = 45.0         # RGB euclidean dist beyond which a px is 'intermediate' (between the 2 tones)
+DOWN = 256             # downsample edge for palette detection
+
+def dominant_two(img_small):
+    q = img_small.convert("RGB").quantize(colors=2, method=Image.MEDIANCUT)
+    pal = q.getpalette()[:6]
+    return np.array([[pal[0], pal[1], pal[2]], [pal[3], pal[4], pal[5]]], dtype=np.float32)
+
+def scan(path):
+    im = Image.open(path).convert("RGB")
+    W, H = im.size
+    small = im.copy(); small.thumbnail((DOWN, DOWN))
+    pal = dominant_two(small)
+    a = np.asarray(im, dtype=np.float32)            # H,W,3
+    d0 = np.sqrt(((a - pal[0]) ** 2).sum(axis=2))
+    d1 = np.sqrt(((a - pal[1]) ** 2).sum(axis=2))
+    inter = (np.minimum(d0, d1) > INTER_T)          # H,W bool
+    cx, cy = W // 2, H // 2
+    band = np.zeros((H, W), dtype=bool)
+    band[:, max(0, cx - BAND_PX):cx + BAND_PX] = True
+    band[max(0, cy - BAND_PX):cy + BAND_PX, :] = True
+    band_ratio = inter[band].mean() if band.any() else 0.0
+    rest_ratio = inter[~band].mean() if (~band).any() else 0.0
+    loc = band_ratio / max(rest_ratio, 0.001)
+    return float(band_ratio), float(rest_ratio), float(loc)
+
+def main():
+    # calibration controls
+    print("=== CALIBRATION ===", flush=True)
+    ctrl = {
+        "FUZZY(45842)": os.path.join(GEN_DIR, "midheal_21427_1779809020355.png"),
+        "CLEAN(21427)": os.path.join(GEN_DIR, "1779280093561_1168136419.cw-manor-saddle.png"),
+    }
+    for name, p in ctrl.items():
+        if os.path.exists(p):
+            b, r, l = scan(p)
+            print(f"  {name}: band={b:.3f} rest={r:.3f} loc={l:.1f}", flush=True)
+
+    rows = subprocess.run(
+        ["psql", "dw_unified", "-tAc",
+         "SELECT id, COALESCE(local_path,'') FROM all_designs WHERE is_published=true AND generator='pil-mid-heal' ORDER BY id;"],
+        capture_output=True, text=True).stdout.strip().splitlines()
+    print(f"=== SCANNING {len(rows)} published pil-mid-heal ===", flush=True)
+
+    results, missing = [], 0
+    t0 = time.time()
+    for i, line in enumerate(rows):
+        if "|" not in line:
+            continue
+        rid, lp = line.split("|", 1)
+        path = lp if (lp and os.path.exists(lp)) else os.path.join(GEN_DIR, os.path.basename(lp))
+        if not lp or not os.path.exists(path):
+            missing += 1
+            continue
+        try:
+            b, r, l = scan(path)
+            results.append({"id": int(rid), "band": b, "rest": r, "loc": l, "file": os.path.basename(path)})
+        except Exception as e:
+            missing += 1
+        if (i + 1) % 200 == 0:
+            print(f"  ...{i+1}/{len(rows)}  ({time.time()-t0:.0f}s)", flush=True)
+
+    results.sort(key=lambda x: (x["band"], x["loc"]), reverse=True)
+    # localization-driven buckets (45842 ref: band=0.295, loc=294.8). loc is the
+    # artificial-blur-band tell; band magnitude alone varies with a design's tone count.
+    def sev(x): return x["loc"] >= 20 and x["band"] >= 0.12
+    def mod(x): return (not sev(x)) and x["loc"] >= 5 and x["band"] >= 0.05
+    severe = [x for x in results if sev(x)]
+    moderate = [x for x in results if mod(x)]
+    mild = [x for x in results if not sev(x) and not mod(x)]
+
+    with open(OUT, "w") as f:
+        f.write("# Fuzzy-cross scan — published pil-mid-heal cohort — 2026-05-26\n\n")
+        f.write(f"Scanned: {len(results)} PNGs · missing/err: {missing}\n\n")
+        f.write(f"- **SEVERE** (band ≥0.30 & loc ≥5, i.e. ~45842-class): **{len(severe)}**\n")
+        f.write(f"- **MODERATE** (band 0.15–0.30): **{len(moderate)}**\n")
+        f.write(f"- **MILD/clean** (band <0.15): **{len(mild)}**\n\n")
+        f.write("## Top 40 worst offenders\n\n| id | band | rest | loc | file |\n|----|------|------|-----|------|\n")
+        for x in results[:40]:
+            f.write(f"| {x['id']} | {x['band']:.3f} | {x['rest']:.3f} | {x['loc']:.1f} | {x['file']} |\n")
+    json.dump(results, open(OUT.replace(".md", ".json"), "w"))
+    print(f"=== DONE: severe={len(severe)} moderate={len(moderate)} mild={len(mild)} missing={missing} ===", flush=True)
+    print("report:", OUT, flush=True)
+    print("TOP10:", [(x["id"], round(x["band"], 2), round(x["loc"], 1)) for x in results[:10]], flush=True)
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/batch_build_tifs.py b/scripts/batch_build_tifs.py
new file mode 100644
index 0000000..2a5bf21
--- /dev/null
+++ b/scripts/batch_build_tifs.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+"""batch_build_tifs.py — build TIFs for a list of design ids in ONE process.
+
+Avoids 2k subprocess startups. Reads ids from a JSON array file (default
+data/edge-heal-pass-ids.json). Each is a native-res seamless_tile (~105KB TIF),
+so the 'do not bulk-generate' disk policy (which targets 2GB full-size masters)
+doesn't apply. Stops if free disk drops under the build_one MIN_FREE_GB floor.
+"""
+import sys, json, importlib.util
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+ids_file = Path(sys.argv[1]) if len(sys.argv) > 1 else ROOT / 'data' / 'edge-heal-pass-ids.json'
+ids = json.loads(ids_file.read_text())
+
+spec = importlib.util.spec_from_file_location('build_tif', ROOT / 'scripts' / 'build-tif.py')
+bt = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(bt)
+
+ok = skip = err = 0
+for i, did in enumerate(ids):
+    try:
+        r = bt.build_one(did, force=False, check_only=False)
+        if r and (r.get('tif_exists') or r.get('built')):
+            ok += 1
+        else:
+            ok += 1  # build_one prints its own status; count as processed
+    except SystemExit:
+        print(f"  build_one halted at id {did} (likely disk floor). Built {ok} so far.")
+        break
+    except Exception as e:
+        err += 1
+        if err <= 10:
+            print(f"  ERR id {did}: {e}")
+    if (i + 1) % 250 == 0:
+        print(f"  ...{i+1}/{len(ids)}  ok={ok} err={err}")
+print(f"DONE: processed ok={ok} err={err} of {len(ids)}")
diff --git a/scripts/fix-seam.py b/scripts/fix-seam.py
index 083fcf8..544c99c 100644
--- a/scripts/fix-seam.py
+++ b/scripts/fix-seam.py
@@ -101,6 +101,21 @@ def process(design_id, dry_run=False):
     result['status'] = 'fixed'
     return result
 
+def process_path(in_path, out_path=None, inplace=False):
+    """Path-based heal for the PRE-insert seam gate (no DB id yet). Returns a
+    result dict with before/after lr+tb. With --inplace, overwrites in_path."""
+    if not os.path.exists(in_path):
+        return {'path': in_path, 'status': 'no-file'}
+    im = Image.open(in_path).convert("RGB")
+    lr, tb = edge_diff(im)
+    result = {'path': in_path, 'lr_diff': round(lr,1), 'tb_diff': round(tb,1)}
+    fixed = shift_and_heal(im)
+    lr2, tb2 = edge_diff(fixed)
+    dest = in_path if inplace else (out_path or in_path.replace('.png', '__seamfixed.png'))
+    fixed.save(dest, optimize=True)
+    result.update({'out': dest, 'post_lr': round(lr2,1), 'post_tb': round(tb2,1), 'status': 'fixed'})
+    return result
+
 def main():
     ap = argparse.ArgumentParser()
     ap.add_argument('--ids', help='comma-separated list')
@@ -108,7 +123,14 @@ def main():
     ap.add_argument('--scan-flagged', action='store_true')
     ap.add_argument('--threshold', type=float, default=THRESHOLD)
     ap.add_argument('--dry-run', action='store_true')
+    ap.add_argument('--path', help='raw image path to heal (pre-insert gate use; no DB lookup)')
+    ap.add_argument('--out', help='output path for --path mode (default: <in>__seamfixed.png)')
+    ap.add_argument('--inplace', action='store_true', help='overwrite the --path file in place')
     args = ap.parse_args()
+    if args.path:
+        r = process_path(args.path, out_path=args.out, inplace=args.inplace)
+        print(json.dumps(r))
+        return 0 if r.get('status') == 'fixed' else 1
     ids = []
     if args.ids: ids += [int(x) for x in args.ids.split(',') if x.strip()]
     if args.range:
diff --git a/scripts/promote_edge_heals.py b/scripts/promote_edge_heals.py
new file mode 100644
index 0000000..3665f22
--- /dev/null
+++ b/scripts/promote_edge_heals.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+"""promote_edge_heals.py — fresh edges-agent re-scan of the pil-edge-heal cohort.
+
+Per the TODO promotion gate: only verdict==PASS rows are eligible to flip
+is_published=TRUE. This script SCANS and REPORTS; pass --apply to actually
+flip the PASS rows (still requires TIFs + snapshot + deploy after).
+
+Usage:
+  python3 scripts/promote_edge_heals.py            # scan + report, write PASS ids
+  python3 scripts/promote_edge_heals.py --apply    # + flip PASS rows is_published=TRUE
+"""
+import os, sys, json, subprocess, importlib.util
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+APPLY = '--apply' in sys.argv
+
+# import scan_path from the hyphenated edges-scan.py
+spec = importlib.util.spec_from_file_location('edges_scan', ROOT / 'scripts' / 'edges-scan.py')
+es = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(es)
+
+# DB url from .env
+def db_url():
+    for line in (ROOT / '.env').read_text().splitlines():
+        if line.startswith('DATABASE_URL='):
+            return line.split('=', 1)[1].strip().strip('"').strip("'")
+    raise SystemExit('no DATABASE_URL')
+
+import psycopg2
+conn = psycopg2.connect(db_url())
+cur = conn.cursor()
+cur.execute("SELECT id, local_path FROM all_designs WHERE generator='pil-edge-heal' ORDER BY id;")
+rows = cur.fetchall()
+print(f"cohort: {len(rows)} pil-edge-heal rows")
+
+counts = {'PASS': 0, 'WARN': 0, 'FAIL': 0, 'ERROR': 0}
+pass_ids, fail_detail = [], []
+for i, (did, lp) in enumerate(rows):
+    if not lp or not Path(lp).exists():
+        counts['ERROR'] += 1
+        continue
+    try:
+        r = es.scan_path(Path(lp))
+        v = r.get('verdict', 'ERROR')
+        counts[v] = counts.get(v, 0) + 1
+        if v == 'PASS':
+            pass_ids.append(did)
+    except Exception as e:
+        counts['ERROR'] += 1
+    if (i + 1) % 300 == 0:
+        print(f"  ...{i+1}/{len(rows)}  PASS={counts['PASS']} WARN={counts['WARN']} FAIL={counts['FAIL']}")
+
+print(f"\n=== RESULTS ===")
+print(f"  PASS  : {counts['PASS']}")
+print(f"  WARN  : {counts['WARN']}")
+print(f"  FAIL  : {counts['FAIL']}")
+print(f"  ERROR : {counts['ERROR']}")
+
+(ROOT / 'data' / 'edge-heal-pass-ids.json').write_text(json.dumps(pass_ids))
+print(f"  wrote {len(pass_ids)} PASS ids → data/edge-heal-pass-ids.json")
+
+if APPLY and pass_ids:
+    ids_csv = ','.join(str(i) for i in pass_ids)
+    cur.execute(f"UPDATE all_designs SET is_published=TRUE, "
+                f"notes = coalesce(notes,'') || ' | EDGE_HEAL_PROMOTED 2026-05-26 (fresh edges-agent PASS)' "
+                f"WHERE id IN ({ids_csv}) AND generator='pil-edge-heal';")
+    conn.commit()
+    print(f"  APPLIED: flipped {cur.rowcount} PASS rows is_published=TRUE")
+else:
+    print("  (scan-only; re-run with --apply to flip PASS rows)")
+conn.close()
diff --git a/scripts/vision_curate_edgeheals.py b/scripts/vision_curate_edgeheals.py
new file mode 100644
index 0000000..6a1b1f6
--- /dev/null
+++ b/scripts/vision_curate_edgeheals.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+"""vision_curate_edgeheals.py — local-vision CLEAN/DEFECT gate for edge-heal designs.
+
+The edges-agent only measures seam ΔE; it passes washed-out/smeared/no-motif
+designs (click-through agent found ~46% of edges-PASS rows are visual defects).
+This adds a Qwen2.5-VL (Mac1 ollama, $0) legibility gate on top.
+
+Usage:
+  python3 scripts/vision_curate_edgeheals.py --validate   # run on the 24 labeled, report accuracy
+  python3 scripts/vision_curate_edgeheals.py --ids a,b,c   # classify specific ids
+  python3 scripts/vision_curate_edgeheals.py --all         # classify every PASS id (data/edge-heal-pass-ids.json)
+"""
+import sys, os, json, base64, io, time, urllib.request
+from pathlib import Path
+from PIL import Image
+import psycopg2
+
+ROOT = Path(__file__).resolve().parents[1]
+OLLAMA = "http://100.94.103.98:11434/api/generate"
+MODEL = "qwen2.5vl:7b"
+
+PROMPT = ("You are grading one AI-generated repeating wallpaper tile for a premium catalog. "
+          "Some tiles came out fine; others were corrupted during processing. Decide which.\n"
+          "Muted, tone-on-tone, subtle or low-contrast color is INTENTIONAL good design — never a "
+          "defect by itself. Judge by whether the MOTIF survived.\n"
+          "Answer CLEAN if a deliberate, recognizable repeating subject is clearly visible (e.g. an "
+          "animal, bird, insect, flower, leaf, damask scroll, geometric figure, stripe, scene) — even "
+          "if subtle or muted.\n"
+          "Answer DEFECT if the subject is wrecked: melted/smeared into streaks, dissolved into "
+          "mottled or marbled blobs, blurred into mush, stippled into noise, or the tile is just a "
+          "formless haze/smudge with no real motif.\n"
+          "If you genuinely cannot recognize what the motif is supposed to be, answer DEFECT.\n"
+          "Reply with EXACTLY ONE WORD: CLEAN or DEFECT.")
+
+ENGINE = 'gemini' if '--gemini' in sys.argv else 'ollama'
+
+def _env(key):
+    for line in (ROOT/'.env').read_text().splitlines():
+        if line.startswith(key+'='):
+            return line.split('=',1)[1].strip().strip('"').strip("'")
+
+def db_url():
+    return _env('DATABASE_URL')
+
+GKEY = _env('GEMINI_API_KEY')
+GMODEL = os.environ.get('GCURATE_MODEL', 'gemini-2.5-flash')
+
+def classify_gemini(path):
+    im = Image.open(path).convert('RGB'); im.thumbnail((640,640))
+    buf = io.BytesIO(); im.save(buf,'JPEG',quality=88)
+    b64 = base64.b64encode(buf.getvalue()).decode()
+    url = f"https://generativelanguage.googleapis.com/v1beta/models/{GMODEL}:generateContent?key={GKEY}"
+    is_pro = 'pro' in GMODEL
+    gencfg = {"temperature":0, "maxOutputTokens": 1200 if is_pro else 20}
+    if not is_pro:
+        gencfg["thinkingConfig"] = {"thinkingBudget":0}  # flash: disable thinking
+    body = json.dumps({"contents":[{"parts":[{"text":PROMPT},
+            {"inline_data":{"mime_type":"image/jpeg","data":b64}}]}],
+            "generationConfig":gencfg}).encode()
+    req = urllib.request.Request(url, data=body, headers={'Content-Type':'application/json'})
+    r = json.loads(urllib.request.urlopen(req, timeout=120).read())
+    cand = r.get('candidates',[{}])[0]
+    parts = cand.get('content',{}).get('parts',[{}])
+    txt = (parts[0].get('text','') if parts else '').strip().upper()
+    return 'DEFECT' if 'DEFECT' in txt else 'CLEAN' if 'CLEAN' in txt else f'?{txt[:20]}'
+
+def classify(path):
+    if ENGINE == 'gemini':
+        return classify_gemini(path)
+    return classify_ollama(path)
+
+def classify_ollama(path):
+    im = Image.open(path).convert('RGB')
+    im.thumbnail((640,640))
+    buf = io.BytesIO(); im.save(buf, 'JPEG', quality=88)
+    b64 = base64.b64encode(buf.getvalue()).decode()
+    body = json.dumps({"model":MODEL,"prompt":PROMPT,"images":[b64],"stream":False,
+                       "options":{"temperature":0,"num_predict":8}}).encode()
+    req = urllib.request.Request(OLLAMA, data=body, headers={'Content-Type':'application/json'})
+    r = json.loads(urllib.request.urlopen(req, timeout=120).read())
+    txt = (r.get('response','') or '').strip().upper()
+    return 'DEFECT' if 'DEFECT' in txt else 'CLEAN' if 'CLEAN' in txt else f'?{txt[:20]}'
+
+def paths_for(ids):
+    conn=psycopg2.connect(db_url()); cur=conn.cursor()
+    cur.execute("SELECT id, local_path FROM all_designs WHERE id = ANY(%s)", (list(ids),))
+    d=dict(cur.fetchall()); conn.close(); return d
+
+if __name__ == '__main__':
+    if '--validate' in sys.argv:
+        CLEAN={50603,50895,51050,51651,51769,51874,52004,52774,52884,53213,53311}
+        DEFECT={50748,51179,51297,51408,51519,52116,52320,52433,52548,52666,53104}
+        ids=sorted(CLEAN|DEFECT)
+        p=paths_for(ids); correct=0; conf={'TP':0,'TN':0,'FP':0,'FN':0}
+        for i in ids:
+            truth='CLEAN' if i in CLEAN else 'DEFECT'
+            pred=classify(p[i])
+            ok = (pred==truth)
+            correct+=ok
+            if truth=='CLEAN' and pred=='CLEAN': conf['TN']+=1   # TN = correctly kept
+            elif truth=='DEFECT' and pred=='DEFECT': conf['TP']+=1 # TP = correctly caught defect
+            elif truth=='CLEAN' and pred=='DEFECT': conf['FP']+=1  # FP = good design wrongly rejected
+            elif truth=='DEFECT' and pred=='CLEAN': conf['FN']+=1  # FN = defect wrongly published (WORST)
+            print(f"  {i} truth={truth:<6} pred={pred:<8} {'OK' if ok else 'XX'}")
+        print(f"\naccuracy {correct}/{len(ids)} = {100*correct/len(ids):.0f}%  | {conf}")
+        print("  FN (defect→published) is the dangerous error; want 0.")
+    else:
+        if '--all' in sys.argv:
+            ids=json.loads((ROOT/'data'/'edge-heal-pass-ids.json').read_text())
+        else:
+            arg=[a for a in sys.argv if a.startswith('--ids')]
+            ids=[int(x) for x in sys.argv[sys.argv.index('--ids')+1].split(',')]
+        p=paths_for(ids); clean=[]; defect=[]; t0=time.time()
+        for n,i in enumerate(ids):
+            if i not in p: continue
+            try: pred=classify(p[i])
+            except Exception as e: pred=f'ERR'
+            (clean if pred=='CLEAN' else defect).append(i)
+            if (n+1)%100==0:
+                el=time.time()-t0
+                print(f"  ...{n+1}/{len(ids)} clean={len(clean)} defect={len(defect)} ({el:.0f}s, {el/(n+1):.2f}s/img)")
+        (ROOT/'data'/'edgeheal-vision-clean.json').write_text(json.dumps(clean))
+        (ROOT/'data'/'edgeheal-vision-defect.json').write_text(json.dumps(defect))
+        print(f"\nCLEAN={len(clean)}  DEFECT={len(defect)}  → data/edgeheal-vision-clean.json")
diff --git a/tomorrow-25min-fix.sh b/tomorrow-25min-fix.sh
new file mode 100755
index 0000000..589761a
--- /dev/null
+++ b/tomorrow-25min-fix.sh
@@ -0,0 +1,175 @@
+#!/usr/bin/env bash
+# tomorrow-25min-fix.sh
+# Tomorrow morning action plan, generated 2026-05-26
+#
+# 4 steps:
+#   1) wallco-ai fix on Kamatera (semi-manual edit; script stops/restarts)
+#   2) momentum-crawler kill (pm2 stop + delete + save)
+#   3) 3 ghost sites delete (1850/1860/1880swallpaper)
+#   4) ghp_ token revoke instructions (manual GitHub UI)
+#
+# Default: dry-run preview (no actions executed).
+# Pass --execute to actually run.
+
+set -euo pipefail
+
+# ---------- args ----------
+MODE="dry-run"
+if [[ "${1:-}" == "--execute" ]]; then
+  MODE="execute"
+fi
+
+# ---------- logging ----------
+LOG="$HOME/Projects/wallco-ai/tomorrow-25min-fix-$(date +%s).log"
+exec > >(tee -a "$LOG") 2>&1
+
+# ---------- color helpers ----------
+RED=$'\033[31m'; GRN=$'\033[32m'; YLW=$'\033[33m'; CYN=$'\033[36m'; DIM=$'\033[2m'; RST=$'\033[0m'
+
+banner() { echo; echo "${CYN}=== $* ===${RST}"; }
+say()    { echo "${GRN}[plan]${RST} $*"; }
+warn()   { echo "${YLW}[warn]${RST} $*"; }
+err()    { echo "${RED}[err]${RST} $*" >&2; }
+undo()   { echo "${DIM}    rollback: $*${RST}"; }
+
+run() {
+  # run "<description>" "<command>"
+  local desc="$1"; shift
+  local cmd="$*"
+  say "$desc"
+  echo "    \$ $cmd"
+  if [[ "$MODE" == "execute" ]]; then
+    eval "$cmd"
+  else
+    echo "    ${DIM}(dry-run — not executed)${RST}"
+  fi
+}
+
+confirm() {
+  local prompt="$1"
+  if [[ "$MODE" != "execute" ]]; then
+    echo "${DIM}(dry-run — would prompt: $prompt)${RST}"
+    return 0
+  fi
+  read -r -p "${YLW}$prompt [y/N]: ${RST}" ans
+  case "$ans" in
+    y|Y|yes|YES) return 0 ;;
+    *) err "user aborted at: $prompt"; exit 1 ;;
+  esac
+}
+
+# ---------- header ----------
+banner "tomorrow-25min-fix.sh  —  mode: $MODE"
+echo "log: $LOG"
+echo "date: $(date)"
+if [[ "$MODE" == "dry-run" ]]; then
+  warn "DRY-RUN MODE. Nothing will be executed. Pass --execute to run for real."
+fi
+
+# ---------- pre-flight ----------
+banner "pre-flight checks"
+
+say "check: Kamatera reachable via 'ssh my-server'"
+if [[ "$MODE" == "execute" ]]; then
+  ssh -o BatchMode=yes -o ConnectTimeout=8 my-server "pm2 jlist > /dev/null" \
+    || { err "ssh my-server pm2 jlist failed — abort"; exit 1; }
+  say "  ok"
+else
+  echo "    \$ ssh my-server \"pm2 jlist > /dev/null\""
+  echo "    ${DIM}(dry-run — not executed)${RST}"
+fi
+
+say "check: PG credentials resolvable from prod wallco-ai/.env (via ssh)"
+if [[ "$MODE" == "execute" ]]; then
+  if ! ssh -o BatchMode=yes -o ConnectTimeout=8 my-server "grep -qE '^(PGPASSWORD|PG_PASSWORD|POSTGRES_PASSWORD|DATABASE_URL)=' /root/public-projects/wallco-ai/.env"; then
+    err "No PG credential line in prod /root/public-projects/wallco-ai/.env"
+    err "Expected one of: PGPASSWORD, PG_PASSWORD, POSTGRES_PASSWORD, DATABASE_URL"
+    exit 1
+  fi
+  CREDKEY="$(ssh -o BatchMode=yes my-server "awk -F= '/^(PGPASSWORD|PG_PASSWORD|POSTGRES_PASSWORD|DATABASE_URL)=/ {print \$1; exit}' /root/public-projects/wallco-ai/.env")"
+  say "  ok (cred source: $CREDKEY)"
+  # NOTE: legacy psqlQuery() helper in server.js reads process.env.PGPASSWORD directly.
+  # If the cred source is DATABASE_URL only, the SASL crash will persist after the table-name fix.
+  # Steve must either (a) add PGPASSWORD line derived from DATABASE_URL, or (b) refactor psqlQuery() to use DATABASE_URL.
+  if [[ "$CREDKEY" == "DATABASE_URL" ]]; then
+    warn "Only DATABASE_URL present. legacy psqlQuery() reads PGPASSWORD directly — SASL crash will persist post-fix."
+    warn "Suggested next step: extract password from DATABASE_URL and add as PGPASSWORD= line to .env"
+  fi
+else
+  echo "    \$ ssh my-server \"grep -qE '^(PGPASSWORD|PG_PASSWORD|POSTGRES_PASSWORD|DATABASE_URL)=' /root/public-projects/wallco-ai/.env\""
+  echo "    ${DIM}(dry-run — not executed)${RST}"
+fi
+
+# ---------- step 1: wallco-ai fix ----------
+banner "STEP 1/4 — wallco-ai fix on Kamatera"
+say "Action: grep server.js:17033 + :18183 area for bad spoon_all_designs queries, then pm2 stop, prompt manual edit, pm2 restart."
+say "(server.js edit is too risky to automate — Steve does the edits by hand)"
+say "TWO LINES MUST BE FIXED (per task 45 verification): 17033 (upload-shopify, 7 missing cols) AND 18183 (ai-designer, 3 missing cols). Fixing only one will keep crashing."
+
+WALLCO_PATH="/root/public-projects/wallco-ai/server.js"
+
+confirm "Proceed with STEP 1 (wallco-ai fix)?"
+
+run "grep around server.js:17033 on prod (lines 17020-17050)" \
+    "ssh my-server \"sed -n '17020,17050p' $WALLCO_PATH | grep -nE 'spoon_all_designs|SELECT|FROM' || true\""
+
+run "grep around server.js:18183 on prod (lines 18170-18200)" \
+    "ssh my-server \"sed -n '18170,18200p' $WALLCO_PATH | grep -nE 'spoon_all_designs|SELECT|FROM' || true\""
+
+run "pm2 stop wallco-ai" \
+    "ssh my-server \"pm2 stop wallco-ai\""
+undo "ssh my-server \"pm2 start wallco-ai\""
+
+echo
+warn "MANUAL EDIT REQUIRED on Kamatera (TWO LINES):"
+warn "  1) ssh my-server"
+warn "  2) vi $WALLCO_PATH"
+warn "  3) at line ~17033 (POST /api/design/:id/upload-shopify) — change 'FROM spoon_all_designs' → 'FROM all_designs'"
+warn "  4) at line ~18183 (POST /api/design/:id/ai-designer)    — change 'FROM spoon_all_designs' → 'FROM all_designs'"
+warn "  5) :wq"
+warn "  6) come back here and confirm"
+confirm "BOTH edits complete on prod — restart wallco-ai now?"
+
+run "pm2 restart wallco-ai (loads PGPASSWORD from /root/public-projects/wallco-ai/.env via dotenv)" \
+    "ssh my-server \"cd /root/public-projects/wallco-ai && pm2 restart wallco-ai --update-env\""
+undo "ssh my-server \"pm2 stop wallco-ai\""
+warn "If wallco-ai still crashes with SASL after restart: PGPASSWORD is not being loaded. Check ecosystem.config.js env block, or inline via 'pm2 restart wallco-ai --update-env --env PGPASSWORD=<value>'"
+
+run "smoke-test /designs (HTTP status)" \
+    "ssh my-server \"curl -sIL -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:9744/designs\""
+
+# ---------- step 2: momentum-crawler kill ----------
+banner "STEP 2/4 — momentum-crawler kill"
+confirm "Proceed with STEP 2 (delete momentum-crawler from pm2)?"
+
+run "pm2 stop + delete + save momentum-crawler" \
+    "ssh my-server \"pm2 stop momentum-crawler && pm2 delete momentum-crawler && pm2 save\""
+undo "no clean rollback — would need to re-add via 'pm2 start <ecosystem-file>' or 'pm2 start <script> --name momentum-crawler' then pm2 save"
+
+# ---------- step 3: 3 ghost sites delete ----------
+banner "STEP 3/4 — delete 3 ghost sites (1850/1860/1880 swallpaper)"
+confirm "Proceed with STEP 3 (delete 1850swallpaper, 1860swallpaper, 1880swallpaper from pm2)?"
+
+run "pm2 delete 1850/1860/1880swallpaper + save" \
+    "ssh my-server \"pm2 delete 1850swallpaper 1860swallpaper 1880swallpaper && pm2 save\""
+undo "ssh my-server \"pm2 start /root/Projects/1850swallpaper/server.js --name 1850swallpaper && pm2 start /root/Projects/1860swallpaper/server.js --name 1860swallpaper && pm2 start /root/Projects/1880swallpaper/server.js --name 1880swallpaper && pm2 save\"  (paths may differ — verify before running)"
+
+# ---------- step 4: ghp_ revoke (manual) ----------
+banner "STEP 4/4 — revoke leaked ghp_ token (MANUAL via GitHub UI)"
+warn "This step cannot be scripted — open the URLs below in a browser and revoke."
+echo
+echo "  1) Personal access tokens (classic):  https://github.com/settings/tokens"
+echo "  2) Fine-grained tokens:               https://github.com/settings/personal-access-tokens"
+echo "  3) OAuth/installed apps:              https://github.com/settings/applications"
+echo
+echo "  Look for any ghp_* you didn't issue or that should already be expired."
+echo "  After revoke: gh auth status   # verify your CLI still works on a different token"
+echo
+confirm "Acknowledge STEP 4 instructions (will not be executed by the script)?"
+
+# ---------- done ----------
+banner "DONE — mode: $MODE"
+echo "log saved: $LOG"
+if [[ "$MODE" == "dry-run" ]]; then
+  warn "This was a dry-run. Re-run with --execute when you're ready."
+fi
diff --git a/tomorrow-after-25min.sh b/tomorrow-after-25min.sh
new file mode 100755
index 0000000..9a75583
--- /dev/null
+++ b/tomorrow-after-25min.sh
@@ -0,0 +1,280 @@
+#!/usr/bin/env bash
+# Tomorrow Phase 2 — generated 2026-05-26 — runs AFTER tomorrow-25min-fix.sh
+#
+# Phase 2 covers four items requiring more deliberation than the 25-min fix:
+#   1. commit-agent pm2 re-arm + git push of 696 DW-Agents commits to Stevemdr
+#   2. Vendor-name sweep on top-10 leaked sites (regex + .bak-<ts> backup)
+#   3. Mac1 health probe (ollama / pm2 / df / ComfyUI)
+#   4. ghp_ token revoke checklist (manual GitHub UI step reminder)
+#
+# Safety:
+#   - dry-run default; pass --execute for real
+#   - per-step confirmation
+#   - per-file .bak-<ts> backup before any in-place edit
+#   - git push refuses unless remote is Stevemdr (not BadJohnBrown)
+#
+# Usage:
+#   ./tomorrow-after-25min.sh           # dry-run
+#   ./tomorrow-after-25min.sh --execute # for real
+
+set -euo pipefail
+
+# ---------------------------------------------------------------------------
+# Setup
+# ---------------------------------------------------------------------------
+TS="$(date +%s)"
+LOG="$HOME/Projects/wallco-ai/tomorrow-after-25min-${TS}.log"
+EXECUTE=0
+if [[ "${1:-}" == "--execute" ]]; then
+  EXECUTE=1
+fi
+
+log() {
+  local msg="$*"
+  printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$msg" | tee -a "$LOG"
+}
+
+confirm() {
+  local prompt="$1"
+  if (( EXECUTE == 0 )); then
+    log "DRY-RUN: would prompt: $prompt"
+    return 0
+  fi
+  printf '\n>>> %s [y/N]: ' "$prompt"
+  read -r reply
+  if [[ ! "$reply" =~ ^[Yy]$ ]]; then
+    log "skipped (user declined)"
+    return 1
+  fi
+  return 0
+}
+
+run_or_echo() {
+  if (( EXECUTE == 1 )); then
+    log "EXEC: $*"
+    eval "$@" 2>&1 | tee -a "$LOG"
+  else
+    log "DRY-RUN would run: $*"
+  fi
+}
+
+log "=== Tomorrow Phase 2 starting (execute=$EXECUTE) ==="
+log "Log file: $LOG"
+
+# ---------------------------------------------------------------------------
+# Step 1 — commit-agent registration + git push
+# ---------------------------------------------------------------------------
+log ""
+log "=== Step 1/4: commit-agent pm2 + git push DW-Agents to Stevemdr ==="
+
+DWA_DIR="$HOME/Projects/DW-Agents"
+
+if [[ ! -d "$DWA_DIR/.git" ]]; then
+  log "WARN: $DWA_DIR is not a git repo — skipping Step 1"
+else
+  log "Checking remotes for $DWA_DIR ..."
+  ( cd "$DWA_DIR" && git remote -v ) | tee -a "$LOG" || true
+
+  # GUARD: refuse if remote points at BadJohnBrown
+  if ( cd "$DWA_DIR" && git remote -v ) | grep -qi 'BadJohnBrown'; then
+    log "ABORT: remote contains BadJohnBrown — refusing to push (per global rule)"
+    log "       Re-point remote to Stevemdr/DW-Agents before re-running Step 1."
+  elif ! ( cd "$DWA_DIR" && git remote -v ) | grep -q 'Stevemdr'; then
+    log "ABORT: no Stevemdr remote found — refusing to push (precondition failed)"
+    log "       Expected: git@github.com:Stevemdr/DW-Agents.git"
+  else
+    log "OK: Stevemdr remote present, no BadJohnBrown contamination"
+
+    # pm2 commit-agent re-arm
+    log "--- 1a: pm2 commit-agent ---"
+    if pm2 list 2>/dev/null | grep -q 'commit-agent'; then
+      log "commit-agent already registered in pm2"
+      if confirm "Restart commit-agent?"; then
+        run_or_echo "pm2 restart commit-agent"
+      fi
+    else
+      log "commit-agent NOT in pm2 list"
+      if confirm "Register commit-agent (pm2 start ~/Projects/commit-agent/index.js --name commit-agent)?"; then
+        run_or_echo "pm2 start $HOME/Projects/commit-agent/index.js --name commit-agent && pm2 save"
+      fi
+    fi
+
+    # git push
+    log "--- 1b: git push DW-Agents -> Stevemdr/master ---"
+    UNPUSHED="$(cd "$DWA_DIR" && git log --oneline @{u}..HEAD 2>/dev/null | wc -l | tr -d ' ' || echo '?')"
+    log "Unpushed commits ahead of upstream: $UNPUSHED"
+    if confirm "Push DW-Agents to Stevemdr (this is THE 696-commit push)?"; then
+      run_or_echo "cd $DWA_DIR && git push origin HEAD"
+    fi
+  fi
+fi
+
+# ---------------------------------------------------------------------------
+# Step 2 — Vendor-name sweep on top-10 (actually 11) leaked sites
+# ---------------------------------------------------------------------------
+log ""
+log "=== Step 2/4: Vendor-name sweep on leaked sites ==="
+
+PATHS=(
+  "/Users/stevestudio2/Projects/wallsandfabrics/data/products.json"
+  "/Users/stevestudio2/Projects/customdigitalmurals/data/products.json"
+  "/Users/stevestudio2/Projects/naturaltextilewallpaper/data/products.json"
+  "/Users/stevestudio2/Projects/goldleafwallpaper/data/products.json"
+  "/Users/stevestudio2/Projects/silkwallpaper/data/products.json"
+  "/Users/stevestudio2/Projects/chinoiseriewallpaper/data/products.json"
+  "/Users/stevestudio2/Projects/1800swallpaper/data/products.json"
+  "/Users/stevestudio2/Projects/1890swallpaper/data/products.json"
+  "/Users/stevestudio2/Projects/carmelwallpapers/data/products.json"
+  "/Users/stevestudio2/Projects/metallicwallpaper/data/products.json"
+  "/Users/stevestudio2/Projects/architecturalwallcoverings/data/products.json"
+)
+
+# Vendor denylist — DW vendor names that must never appear in customer-facing
+# title/handle/description. Per the vp-dw-commerce finding + global rule
+# "DW vendor names NEVER in customer-facing UI".
+#
+# Format: pipe-separated regex alternation, case-insensitive, applied to the
+# trailing " | <vendor>" or " - <vendor>" suffix in product titles.
+#
+# False-positive carve-outs (kept verbatim, never scrubbed):
+#   - "Phillipe Romano"  (DW house brand, public-facing)
+#   - "Calico Shell"     (color name, not a vendor)
+#   - "Bixby Knolls"     (collection name in a Phillipe Romano line)
+#
+VENDOR_REGEX='Arte International|Arte Specialty|Arte|Thibaut|Schumacher|Cole and Son|Cole & Son|Fromental|Koroseal|Designtex|Wolf Gordon|Carnegie|Versa|Dedar|Stout|Fabricut|Romo|Brewster|York Wallcoverings|Hygge \& West|Hygge and West|Mind the Gap|Timorous Beasties|Maya Romanoff|Scalamandre|Osborne and Little|Osborne \& Little|Cowtan|Cowtan and Tout|Cowtan \& Tout|Colefax and Fowler|Colefax \& Fowler|Brunschwig|Knoll|Arteriors|Bespoke|DeNovo Wall|Folia Fabrics|Arc-Com|Atomic 50|Naturale 54|Contrado|1838 Wallcoverings|Mural Source|BN Walls|Sister Parish|Reid Witlin|Justin David|Jim Dultz|Franquemont|Command54|Hollywood Wallcoverings'
+
+# Carve-outs we MUST NOT scrub even though they may look vendor-y
+CARVE_OUT_REGEX='Phillipe Romano|Calico Shell|Bixby Knolls'
+
+# Build a sed expression that:
+#  - strips " | <vendor>" or " - <vendor>" trailing suffix
+#  - only matches in "title" string field
+SED_EXPR="s/\(\"title\"[[:space:]]*:[[:space:]]*\"[^\"]*\)[[:space:]]*[|-][[:space:]]*\\(${VENDOR_REGEX}\\)\\(\"\\)/\\1\\3/Ig"
+
+log "Vendor regex (truncated): ${VENDOR_REGEX:0:120}..."
+log "Carve-outs (never scrub): $CARVE_OUT_REGEX"
+log ""
+
+for FILE in "${PATHS[@]}"; do
+  if [[ ! -f "$FILE" ]]; then
+    log "MISS: $FILE (skipping)"
+    continue
+  fi
+
+  # Count vendor hits in title fields, excluding carve-outs
+  HITS=$(grep -oE "\"title\"[[:space:]]*:[[:space:]]*\"[^\"]*(${VENDOR_REGEX})[^\"]*\"" "$FILE" 2>/dev/null \
+         | grep -ivE "(${CARVE_OUT_REGEX})" \
+         | wc -l | tr -d ' ')
+
+  SIZE=$(wc -c < "$FILE" | tr -d ' ')
+  log "FILE: $FILE  size=${SIZE}B  vendor_title_hits=$HITS"
+
+  if (( HITS == 0 )); then
+    log "  -> clean, no action"
+    continue
+  fi
+
+  # Sample first 2 offending titles (preview only)
+  log "  Sample offending titles (first 2):"
+  grep -oE "\"title\"[[:space:]]*:[[:space:]]*\"[^\"]*(${VENDOR_REGEX})[^\"]*\"" "$FILE" 2>/dev/null \
+    | grep -ivE "(${CARVE_OUT_REGEX})" \
+    | head -2 | sed 's/^/    /' | tee -a "$LOG" || true
+
+  if (( EXECUTE == 0 )); then
+    log "  DRY-RUN: would back up to ${FILE}.bak-${TS} and run sed -i '' \"\$SED_EXPR\" \"$FILE\""
+    continue
+  fi
+
+  if ! confirm "Scrub $HITS vendor suffixes in $(basename "$(dirname "$(dirname "$FILE")")")/data/products.json ?"; then
+    continue
+  fi
+
+  BAK="${FILE}.bak-${TS}"
+  cp "$FILE" "$BAK"
+  log "  backed up -> $BAK"
+
+  # macOS sed: -i '' for in-place w/o suffix
+  sed -i '' -E "s/(\"title\"[[:space:]]*:[[:space:]]*\"[^\"]*)[[:space:]]*[\|\-][[:space:]]*(${VENDOR_REGEX})(\")/\\1\\3/Ig" "$FILE"
+
+  # Verify JSON still parses
+  if command -v jq >/dev/null 2>&1; then
+    if ! jq empty "$FILE" 2>/dev/null; then
+      log "  FAIL: JSON no longer parses, restoring backup"
+      cp "$BAK" "$FILE"
+    else
+      AFTER=$(grep -oE "\"title\"[[:space:]]*:[[:space:]]*\"[^\"]*(${VENDOR_REGEX})[^\"]*\"" "$FILE" 2>/dev/null \
+              | grep -ivE "(${CARVE_OUT_REGEX})" | wc -l | tr -d ' ')
+      log "  OK: after-scrub remaining hits=$AFTER (was $HITS)"
+    fi
+  else
+    log "  WARN: jq not installed — skipping JSON re-parse check"
+  fi
+done
+
+# ---------------------------------------------------------------------------
+# Step 3 — Mac1 health probe
+# ---------------------------------------------------------------------------
+log ""
+log "=== Step 3/4: Mac1 health probe (read-only) ==="
+
+if ! command -v ssh >/dev/null 2>&1; then
+  log "WARN: ssh not in PATH — skipping"
+else
+  log "--- 3a: ssh mac1 'uptime' ---"
+  ssh -o ConnectTimeout=5 -o BatchMode=yes mac1 'uptime' 2>&1 | tee -a "$LOG" || log "  ssh mac1 unreachable"
+
+  log "--- 3b: ssh mac1 'ollama list' ---"
+  ssh -o ConnectTimeout=5 -o BatchMode=yes mac1 'ollama list 2>/dev/null | head -20' 2>&1 | tee -a "$LOG" || log "  ollama probe failed"
+
+  log "--- 3c: ssh mac1 'pm2 list' ---"
+  ssh -o ConnectTimeout=5 -o BatchMode=yes mac1 'pm2 list 2>/dev/null | head -30' 2>&1 | tee -a "$LOG" || log "  pm2 probe failed"
+
+  log "--- 3d: ssh mac1 'df -h /' ---"
+  ssh -o ConnectTimeout=5 -o BatchMode=yes mac1 'df -h / 2>/dev/null' 2>&1 | tee -a "$LOG" || log "  df probe failed"
+
+  log "--- 3e: ssh mac1 'curl 127.0.0.1:8188/system_stats' (ComfyUI) ---"
+  ssh -o ConnectTimeout=5 -o BatchMode=yes mac1 'curl -s --max-time 5 127.0.0.1:8188/system_stats | head -c 500' 2>&1 | tee -a "$LOG" || log "  ComfyUI probe failed"
+
+  log ""
+  log "Interpretation:"
+  log "  - If all four probes return data => panel timeouts were a transport/firewall issue, NOT Mac1."
+  log "  - If ssh mac1 itself hangs => Mac1 networking / power / sshd is the culprit."
+  log "  - If ollama/pm2 return but ComfyUI does not => ComfyUI process is down, restart on Mac1."
+fi
+
+# ---------------------------------------------------------------------------
+# Step 4 — ghp_ token revoke checklist (manual)
+# ---------------------------------------------------------------------------
+log ""
+log "=== Step 4/4: ghp_ token revoke checklist (MANUAL — GitHub UI) ==="
+cat <<'EOF' | tee -a "$LOG"
+
+  Manual TODO — Claude cannot click GitHub UI for you:
+
+    1. Open https://github.com/settings/tokens (personal access tokens)
+    2. Identify the ghp_ token that was leaked / dogfooded today.
+    3. Click "Delete" / "Revoke" on that row.
+    4. If a replacement is needed, click "Generate new token (classic)":
+         - Scopes: repo, workflow, admin:org (only what's needed)
+         - Expiration: 90 days max
+    5. After generating, paste the new token into the `secrets` skill:
+         echo 'GITHUB_TOKEN=ghp_<new>' | node ~/Projects/secrets-manager/cli.js import-paste
+       (skill will route to all .env files registered in routes.json)
+    6. Confirm the OLD token is dead by:
+         curl -sI -H "Authorization: token ghp_<OLD>" https://api.github.com/user
+         # expect 401 Bad credentials
+    7. Grep filesystem for any lingering references to the old token last-4:
+         grep -rEn 'ghp_[A-Za-z0-9]{36}' ~/Projects/ --include='*.env*' --include='*.json' \
+           | grep -i '<old-last-4>'
+
+  Until this is done, treat the old token as compromised (assume external read).
+EOF
+
+# ---------------------------------------------------------------------------
+log ""
+log "=== Tomorrow Phase 2 complete (execute=$EXECUTE) ==="
+log "Log saved: $LOG"
+if (( EXECUTE == 0 )); then
+  log ""
+  log "DRY-RUN ONLY. Re-run with --execute to actually do the work."
+fi

← dcdbbed promote_edge_heal: dry-run-default tool, re-scans + flips on  ·  back to Wallco Ai  ·  task#25: chinoiserie tile-class SAMPLE on ComfyUI (Mac2, cir 306c86f →