[object Object]

← back to Sample Followup Sweep

Sample follow-up: bound window to a rolling 3-day catch-up band at the 10-day mark (only memos that just crossed 10d, not the whole back-catalog)

3aa59b23665118d9ca5cdf90e0014c73b695e575 · 2026-08-20 10:22:42 -0700 · Steve Abrams

Files touched

Diff

commit 3aa59b23665118d9ca5cdf90e0014c73b695e575
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 20 10:22:42 2026 -0700

    Sample follow-up: bound window to a rolling 3-day catch-up band at the 10-day mark (only memos that just crossed 10d, not the whole back-catalog)
---
 data/draft-ledger.json              |  17 +
 data/runs/scheduled-08-20-2026.json | 640 +++---------------------------------
 scripts/contact-research.mjs        |  48 +++
 scripts/scheduled-run.mjs           |  77 +++--
 4 files changed, 167 insertions(+), 615 deletions(-)

diff --git a/data/draft-ledger.json b/data/draft-ledger.json
new file mode 100644
index 0000000..e06c0e6
--- /dev/null
+++ b/data/draft-ledger.json
@@ -0,0 +1,17 @@
+{
+  "538974": "08/20/2026",
+  "538977": "08/20/2026",
+  "538983": "08/20/2026",
+  "538986": "08/20/2026",
+  "538996": "08/20/2026",
+  "538997": "08/20/2026",
+  "539014": "08/20/2026",
+  "539103": "08/20/2026",
+  "539105": "08/20/2026",
+  "539106": "08/20/2026",
+  "539283": "08/20/2026",
+  "539284": "08/20/2026",
+  "539285": "08/20/2026",
+  "539286": "08/20/2026",
+  "539288": "08/20/2026"
+}
\ No newline at end of file
diff --git a/data/runs/scheduled-08-20-2026.json b/data/runs/scheduled-08-20-2026.json
index f33ecc0..28aaa8e 100644
--- a/data/runs/scheduled-08-20-2026.json
+++ b/data/runs/scheduled-08-20-2026.json
@@ -1,633 +1,81 @@
 {
-  "ranAt": "2026-08-20T17:04:45.117Z",
-  "mode": "DRY-RUN",
-  "window": "08/21/2021...08/10/2026",
+  "ranAt": "2026-08-20T17:22:23.387Z",
+  "mode": "DRAFT",
+  "window": "08/08/2026...08/10/2026",
   "stampDate": "08/20/2026",
-  "vendorsSent": 22,
-  "vendorsSkipped": 36,
-  "vendorsNeedsConfirm": 6,
-  "sent": [
-    {
-      "vid": "CON",
-      "name": "Concertex",
-      "to": "info@concertex.com",
-      "n": 1,
-      "skus": [
-        "noc105"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "YOR",
-      "name": "York",
-      "to": "RiveraH@yorkwall.com, orders@yorkwall.com",
-      "n": 52,
-      "skus": [
-        "DWJS15964",
-        "DWJS13287",
-        "DWJS14323",
-        "DWJS14322",
-        "DWJS13941",
-        "DWC263505",
-        "DWC279215",
-        "DWC279217",
-        "DWC279220",
-        "DWC179237",
-        "DWC182513",
-        "DWC279224",
-        "DWJS14643",
-        "DWJS14640",
-        "DWJS13140",
-        "DWJS12243",
-        "DWJS15198",
-        "DWJS12002",
-        "DWJS14580",
-        "DWJS14873",
-        "DWJS12956",
-        "DWJS13305",
-        "DWJS14652",
-        "DWJS16168",
-        "DWC285126",
-        "DWC285129",
-        "DWJS15440",
-        "DWJS14769",
-        "DWJS14454",
-        "DWJS13319",
-        "DWJS14600",
-        "DWJS12002",
-        "DWJS12535",
-        "DWJS13376",
-        "DWJS16892",
-        "DWJS16168",
-        "DWJS17237",
-        "DWJS14115",
-        "FLO28057",
-        "DWC304149",
-        "DWC304152",
-        "DWJS13678",
-        "DWJS14031",
-        "DWJS13995",
-        "DWJS14179",
-        "DWJS15783",
-        "PRC84025",
-        "DWC322184",
-        "DWC322186",
-        "DWJS91574",
-        "DWJS17387",
-        "DWJS16618"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "THIB",
-      "name": "Thibaut",
-      "to": "cathy.dy@thibautdesign.com",
-      "n": 24,
-      "skus": [
-        "DWC270781",
-        "DWTT72120",
-        "DWC273903",
-        "DWTT72044",
-        "DWTT72046",
-        "DWTT71557",
-        "DWTT71321",
-        "DWC275223",
-        "PRG71027",
-        "DWTT70638",
-        "DWTT71614",
-        "DWTT71872",
-        "CROC56551",
-        "DWTT71348",
-        "DWTT70435",
-        "DWTT71906",
-        "DWTT71908",
-        "DAW73004",
-        "DAW73002",
-        "DWC278298",
-        "DAW73053",
-        "DWTT81049",
-        "DWTT71766",
-        "DWTT70093"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "WQ",
-      "name": "WallQuest / Malibu",
-      "to": "orders@wallquest.com",
-      "n": 16,
-      "skus": [
-        "DWC271268",
-        "DWC275134",
-        "DWC275802",
-        "DWQW57639",
-        "DWQW56956",
-        "DWQW56952",
-        "DWQW57088",
-        "DWQW56865",
-        "DWQW56864",
-        "DWQW57484",
-        "DWQW58392",
-        "DWQW57564",
-        "DWQW58003",
-        "DWQW58001",
-        "DWQW57309",
-        "DWQW60039"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "BRE",
-      "name": "Brewster",
-      "to": "brewsterorders@brewp.com",
-      "n": 6,
-      "skus": [
-        "FFA82226",
-        "CGS83463",
-        "VVA84534",
-        "DWJS91471",
-        "DWJS90108",
-        "DWJS90108"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "DGD",
-      "name": "Designers Guild @ Osborne & Little",
-      "to": "Polancol@oalusa.com",
-      "n": 7,
-      "skus": [
-        "RALPH1011",
-        "EUR90000",
-        "EUR70844",
-        "EUR70847",
-        "EUR70846",
-        "RALPH1036",
-        "RALPH1096"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "PJ",
-      "name": "Phillip Jeffries",
-      "to": "jballard@phillipjeffries.com",
-      "n": 6,
-      "skus": [
-        "DWC275494",
-        "DWJP13372",
-        "DWJP14274",
-        "DWJP14266",
-        "DWJP13466",
-        "DWC279239"
-      ],
-      "dryRun": true
-    },
+  "vendorsSent": 0,
+  "vendorsDrafted": 8,
+  "vendorsSkipped": 1,
+  "vendorsNeedsConfirm": 0,
+  "sent": [],
+  "drafted": [
     {
       "vid": "OSB",
       "name": "Osborne & Little",
       "to": "Polancol@oalusa.com",
-      "n": 16,
-      "skus": [
-        "DWC275795",
-        "EUR70503",
-        "EUR71055",
-        "EUR71130",
-        "EUR71061",
-        "EUR71059",
-        "EUR71121",
-        "EUR71120",
-        "EUR70968",
-        "EUR80261",
-        "EUR80263",
-        "EUR71256",
-        "EUR71254",
-        "EUR71252",
-        "EUR90475",
-        "EUR80387"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "ARTE",
-      "name": "Arte (@ Egg & Dart)",
-      "to": "Therese@egg-and-dart.com",
-      "n": 8,
-      "skus": [
-        "RTE50062",
-        "RTE50065",
-        "RTE50016",
-        "RTE50084",
-        "RTE50110",
-        "RTE50110",
-        "RTE50111",
-        "DWC-Arte72143"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "RBL",
-      "name": "Rebel Walls (via Gimmersta US)",
-      "to": "support@gimmersta.com",
-      "n": 8,
-      "skus": [
-        "DWRW74200",
-        "DWRW74939",
-        "DWRW74940",
-        "DWRW75460",
-        "DWRW72275",
-        "DWRW74200",
-        "DWRW75898",
-        "DWRW76496"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "KOR",
-      "name": "Koroseal",
-      "to": "samples@koroseal.com, sshortino@koroseal.com",
-      "n": 7,
-      "skus": [
-        "DWC281919",
-        "DWC281921",
-        "DWC281923",
-        "DWK29236",
-        "DWK29238",
-        "DWC 67680\r\r",
-        "DWC67451\r\r"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "MAYA",
-      "name": "Maya Romanoff",
-      "to": "ari@quintushome.com",
-      "n": 1,
-      "skus": [
-        "DWYA60003"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "SAN",
-      "name": "Sancar (AS Création) — inferred",
-      "to": "sales@sancarwallcovering.com",
       "n": 2,
-      "skus": [
-        "DWRC140044",
-        "DWRC140045"
-      ],
-      "dryRun": true
+      "draftId": "r7522150672371779640",
+      "confirm": false
     },
     {
-      "vid": "ANNA",
-      "name": "Thibaut",
-      "to": "cathy.dy@thibautdesign.com",
-      "n": 1,
-      "skus": [
-        "DWTA65396"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "SANDB",
-      "name": "Sandberg (via Gimmersta US)",
-      "to": "support@gimmersta.com",
-      "n": 3,
-      "skus": [
-        "DWSS72759",
-        "DWSS72760",
-        "DWSS72675"
-      ],
-      "dryRun": true
+      "vid": "YOR",
+      "name": "York",
+      "to": "RiveraH@yorkwall.com, orders@yorkwall.com",
+      "n": 6,
+      "draftId": "r3080370794725730872",
+      "confirm": false
     },
     {
       "vid": "KRA",
       "name": "Kravet",
       "to": "matt.schoffman@kravet.com",
-      "n": 4,
-      "skus": [
-        "DWKK128364",
-        "DWKK135711",
-        "DWKK101625",
-        "DWKK130319"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "AS CREATION",
-      "name": "Sancar (AS Création) — inferred",
-      "to": "sales@sancarwallcovering.com",
-      "n": 1,
-      "skus": [
-        "DWAC1014925"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "ARTE INTERNATIONAL",
-      "name": "Arte (@ Egg & Dart)",
-      "to": "Therese@egg-and-dart.com",
-      "n": 1,
-      "skus": [
-        "DWAR320016"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "ANNA FRENCH",
-      "name": "Thibaut",
-      "to": "cathy.dy@thibautdesign.com",
-      "n": 3,
-      "skus": [
-        "DWAT65056",
-        "DWAF74869",
-        "DWAT65538"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "ASTSAND",
-      "name": "Astek",
-      "to": "amy.d@astek.com",
-      "n": 1,
-      "skus": [
-        "DWPX500016"
-      ],
-      "dryRun": true
-    },
-    {
-      "vid": "CHIN",
-      "name": "China Seas by Quadrille — inferred",
-      "to": "Rhaiza@quadrilleinc.com",
       "n": 2,
-      "skus": [
-        "DWCS914048",
-        "DWCS913548"
-      ],
-      "dryRun": true
+      "draftId": "r5458844388935871402",
+      "confirm": false
     },
     {
       "vid": "GREEN",
       "name": "Greenland (bmwallpaper)",
       "to": "sales@bmwallpaper.com",
-      "n": 2,
-      "skus": [
-        "PWV10040",
-        "GRS800010"
-      ],
-      "dryRun": true
-    }
-  ],
-  "needsConfirm": [
-    {
-      "vid": "MOM",
-      "name": "Momentum (→Hollywood, INTERNAL chase only)",
-      "to": "customerservice@momtex.com",
-      "n": 11,
-      "skus": [
-        "DUR72411",
-        "XPV48335",
-        "XWC53248",
-        "XWT53318",
-        "XWC53245",
-        "XA666454",
-        "XG566927",
-        "XVB73013",
-        "LLN6641",
-        "XWY53100",
-        "XWY53104"
-      ],
-      "reason": "main-email fallback — CONFIRM person vs recent emails before send"
-    },
-    {
-      "vid": "INN",
-      "name": "Innovations",
-      "to": "hmartinez@innovationsusa.com",
       "n": 1,
-      "skus": [
-        "XCG34342"
-      ],
-      "reason": "main-email fallback — CONFIRM person vs recent emails before send"
+      "draftId": "r6173246691603703575",
+      "confirm": false
     },
     {
-      "vid": "DTEX",
-      "name": "Designtex",
-      "to": "ddawson@designtex.com",
-      "n": 2,
-      "skus": [
-        "SUR21562",
-        "SUR22101"
-      ],
-      "reason": "main-email fallback — CONFIRM person vs recent emails before send"
+      "vid": "SANDB",
+      "name": "Sandberg (via Gimmersta US)",
+      "to": "support@gimmersta.com",
+      "n": 1,
+      "draftId": "r-84462059661524742",
+      "confirm": false
     },
     {
-      "vid": "VER",
-      "name": "Versace (via Sancar)",
-      "to": "sales@sancarwallcovering.com",
+      "vid": "BRE",
+      "name": "Brewster",
+      "to": "brewsterorders@brewp.com",
       "n": 1,
-      "skus": [
-        "VER46222"
-      ],
-      "reason": "main-email fallback — CONFIRM person vs recent emails before send"
+      "draftId": "r-4409334545407435014",
+      "confirm": false
     },
     {
-      "vid": "WOL",
-      "name": "Wolf Gordon",
-      "to": "samples@wolfgordon.com",
-      "n": 2,
-      "skus": [
-        "DWWG533039",
-        "DWWG533038"
-      ],
-      "reason": "main-email fallback — CONFIRM person vs recent emails before send"
+      "vid": "WQ",
+      "name": "WallQuest / Malibu",
+      "to": "orders@wallquest.com",
+      "n": 1,
+      "draftId": "r3077839332594228811",
+      "confirm": false
     },
     {
-      "vid": "ROMO",
-      "name": "Romo",
-      "to": "fawn.bowen@romo.com",
+      "vid": "THIB",
+      "name": "Thibaut",
+      "to": "cathy.dy@thibautdesign.com",
       "n": 1,
-      "skus": [
-        "ROP74578"
-      ],
-      "reason": "main-email fallback — CONFIRM person vs recent emails before send"
+      "draftId": "r3922288234510569275",
+      "confirm": false
     }
   ],
+  "needsConfirm": [],
   "skipped": [
-    {
-      "vid": "RAD",
-      "n": 1,
-      "reason": "no-chase vendor"
-    },
-    {
-      "vid": "GD",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "JF",
-      "n": 4,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "MISS",
-      "n": 2,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "SCH",
-      "n": 12,
-      "reason": "no-chase vendor"
-    },
-    {
-      "vid": "ARM",
-      "n": 3,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "PAUL",
-      "n": 3,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "TBD",
-      "n": 7,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "GRA",
-      "n": 3,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "RCV",
-      "n": 2,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "MINI",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "NLXL",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "REID",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "HOLLY",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "COW",
-      "n": 8,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "CARLISLE",
-      "n": 2,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "WAT",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "LAR",
-      "n": 2,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "SIR",
-      "n": 6,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "FTR",
-      "n": 4,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "ULR",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "EGG",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "AMER",
-      "n": 4,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "\r",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "COOR",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "FOL",
-      "n": 2,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "HYG",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "TWI",
-      "n": 3,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "CHRISTIAN LACROIX EUROPE",
-      "n": 2,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "1838 WALLCOVERINGS",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "PHILLIPE ROMANO",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "SCHUMACHER",
-      "n": 18,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "CARL",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
-    {
-      "vid": "SPN",
-      "n": 11,
-      "reason": "no-chase vendor"
-    },
-    {
-      "vid": "POI",
-      "n": 1,
-      "reason": "no sample email/account on file"
-    },
     {
       "vid": "BM",
       "n": 3,
diff --git a/scripts/contact-research.mjs b/scripts/contact-research.mjs
new file mode 100644
index 0000000..6e813e4
--- /dev/null
+++ b/scripts/contact-research.mjs
@@ -0,0 +1,48 @@
+'use strict';
+// READ-ONLY Gmail domain research for skipped sample-followup vendor vids.
+// Searches info@ mailbox by vendor DOMAIN (not name), returns recent human counterparts.
+import http from 'node:http';
+import { readFileSync } from 'node:fs';
+
+const GEORGE = { host: '127.0.0.1', port: 9850 };
+function georgeAuth() {
+  const files = ['/Users/macstudio3/Projects/Designer-Wallcoverings/DW-MCP/.env', '/Users/macstudio3/Projects/george-gmail/.env'];
+  const genv = (k) => { for (const f of files) { try { const m = readFileSync(f, 'utf8').match(new RegExp('^' + k + '=(.+)$', 'm')); if (m) return m[1].trim(); } catch {} } return ''; };
+  let a = genv('GEORGE_BASIC_AUTH'); if (!a.includes(':')) a = 'admin:' + genv('GEORGE_BASIC_AUTH_PASS');
+  return 'Basic ' + Buffer.from(a).toString('base64');
+}
+function gGet(path) {
+  return new Promise((resolve, reject) => {
+    const req = http.request({ ...GEORGE, path, method: 'GET', headers: { Authorization: georgeAuth() } }, (r) => {
+      let b = ''; r.on('data', (d) => b += d); r.on('end', () => { try { resolve(JSON.parse(b)); } catch { resolve(null); } });
+    });
+    req.on('error', reject); req.end();
+  });
+}
+async function search(q) {
+  const res = await gGet(`/api/info/search?q=${encodeURIComponent(q)}`).catch(() => null);
+  const msgs = res && (res.messages || res.results || (Array.isArray(res) ? res : null));
+  return Array.isArray(msgs) ? msgs : [];
+}
+
+// vid -> {vendor, domain(s)}  (domain from vendor_registry / known DW mappings)
+const MAP = JSON.parse(process.argv[2] || '{}');
+
+const run = async () => {
+  const out = {};
+  for (const [vid, info] of Object.entries(MAP)) {
+    const domains = info.domains || [];
+    const rec = { vendor: info.vendor, domains, from_hits: [], to_hits: [] };
+    for (const d of domains) {
+      // messages we RECEIVED from anyone @domain (who they route through)
+      const froms = await search(`from:@${d}`);
+      for (const m of froms.slice(0, 5)) rec.from_hits.push({ from: m.from || m.sender || '', subject: (m.subject || '').slice(0, 90), date: m.date || m.internalDate || '', snippet: (m.snippet || '').slice(0, 120) });
+      // messages we SENT to anyone @domain (who we chase)
+      const tos = await search(`to:@${d}`);
+      for (const m of tos.slice(0, 5)) rec.to_hits.push({ to: m.to || '', subject: (m.subject || '').slice(0, 90), date: m.date || m.internalDate || '' });
+    }
+    out[vid] = rec;
+  }
+  console.log(JSON.stringify(out, null, 2));
+};
+run().catch((e) => { console.error('research error:', e.message); process.exit(1); });
diff --git a/scripts/scheduled-run.mjs b/scripts/scheduled-run.mjs
index 1a81d9b..b432346 100644
--- a/scripts/scheduled-run.mjs
+++ b/scripts/scheduled-run.mjs
@@ -8,7 +8,7 @@
 // run only sends the follow-ups that are NEW since the last run.
 //
 // Rules enforced:
-//   • age ≥ 10 days, NO upper cap (Steve 8/20: chase everything 10+ days, not the 10–60 band)  • Date WP Sample Sent empty (still outstanding)
+//   • entered in a rolling 3-day band ending exactly at the 10-day mark (Steve 8/20 REVISED: "10 days old exactly, for the past 3 days" = only memos that JUST crossed 10d, NOT the whole back-catalog)  • Date WP Sample Sent empty (still outstanding)
 //   • Date Email Sent to Vendor after 10 Days EMPTY  → the "do not duplicate" guard
 //   • vendor must have a sample email + account       (else skip + report — can't send)
 //
@@ -29,13 +29,18 @@ const ROOT = join(__dir, '..');
 const { compose } = require(join(ROOT, 'lib', 'compose.js'));
 
 const SEND = process.argv.includes('--send');
+// Steve 8/20: DRAFT mode — create a Gmail draft per vendor in info@ Drafts for human review,
+// instead of sending directly. The draft IS the human gate; Steve sends from Drafts.
+const DRAFT = process.argv.includes('--draft');
 const DB = 'WALLPAPER';
 const LAYOUT = 'Report for old memo samples';
 const FIELD_SENT = 'Date Email Sent to Vendor after 10 Days';
-// Steve 8/20: 10-day threshold, drop the 60-day dead-lead cap — chase everything ≥10d not arrived.
-// True unlimited breaks the FileMaker date-range find (a lo-date >~5yr old returns 0 records), so
-// 1825d (5yr) is the widest window that reliably returns data = effectively "no cap" for sample memos.
-const MIN_AGE_DAYS = 10, MAX_AGE_DAYS = Number(process.env.MAXAGE || 1825);
+// Steve 8/20 (REVISED): chase ONLY memos that JUST crossed the 10-day mark — a rolling 3-day
+// catch-up band, NOT the whole back-catalog. "10 days old exactly, for the past 3 days" → on 8/20
+// that is entered dates 8/8, 8/9, 8/10. The open-ended `...hi` was pulling every old memo; this
+// bounds it to [today-10-(CATCHUP-1) .. today-10]. Each daily run picks up the one new day; the
+// 3-day width is a safety net so a skipped run still catches the memos it would have missed.
+const MIN_AGE_DAYS = 10, CATCHUP_DAYS = Number(process.env.CATCHUP || 3);
 const SHIP_TO = { name: 'Designer Wallcoverings', line1: '15442 Ventura Blvd. #102', city_state_zip: 'Sherman Oaks, CA 91403', phone: '1-888-373-4564' };
 
 // --- FileMaker client (reuse the connector; creds from ~/.claude.json) ---
@@ -51,11 +56,11 @@ const fm = await import('/Users/macstudio3/Projects/filemaker-mcp/src/fm-client.
 const pad = (n) => String(n).padStart(2, '0');
 const fmtDate = (d) => `${pad(d.getMonth() + 1)}/${pad(d.getDate())}/${d.getFullYear()}`;
 function windowRange(today = new Date()) {
+  // hi = the memo that hits 10 days old TODAY (entered today-10).
+  // lo = the memo that hit 10 days old CATCHUP_DAYS-1 days ago (entered today-10-(CATCHUP-1)).
+  // On 8/20 with CATCHUP=3 → hi=8/10, lo=8/8 → FileMaker range "08/08/2026...08/10/2026".
   const hi = new Date(today); hi.setDate(hi.getDate() - MIN_AGE_DAYS);
-  // Steve 8/20: no upper age cap. Use an OPEN-ENDED low bound (FileMaker `...hi`
-  // = "on or before hi") so we chase EVERYTHING at least MIN_AGE_DAYS old.
-  if (MAX_AGE_DAYS >= 3650) return `...${fmtDate(hi)}`;
-  const lo = new Date(today); lo.setDate(lo.getDate() - MAX_AGE_DAYS);
+  const lo = new Date(today); lo.setDate(lo.getDate() - MIN_AGE_DAYS - (CATCHUP_DAYS - 1));
   return `${fmtDate(lo)}...${fmtDate(hi)}`;
 }
 const norm = (s) => String(s || '').toLowerCase().trim();
@@ -103,6 +108,18 @@ function georgeSend(payload) {
     req.write(body); req.end();
   });
 }
+// Create a Gmail draft in info@ Drafts (POST /api/drafts — no send-approval token needed).
+function georgeDraft(payload) {
+  const { auth } = georgeCreds();
+  const body = JSON.stringify(payload);
+  return new Promise((resolve) => {
+    const req = http.request({ host: '127.0.0.1', port: 9850, path: '/api/drafts', method: 'POST',
+      headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), Authorization: auth } },
+      (r) => { let b = ''; r.on('data', (d) => b += d); r.on('end', () => { try { const j = JSON.parse(b); resolve({ ok: !!j.success, id: j.draftId || '', detail: b.slice(0, 200) }); } catch { resolve({ ok: false, detail: b.slice(0, 200) }); } }); });
+    req.on('error', (e) => resolve({ ok: false, detail: e.message }));
+    req.write(body); req.end();
+  });
+}
 
 const run = async () => {
   const win = windowRange();
@@ -130,14 +147,29 @@ const run = async () => {
   }
 
   const cmap = vidContactMap();
-  const sent = [], skipped = [], needsConfirm = [];
+  // DRAFT dedup ledger — drafting ≠ sending, so we do NOT false-stamp FileMaker; we track which
+  // recordIds already have a draft locally so the daily run never piles up duplicate drafts.
+  const LEDGER = 'data/draft-ledger.json';
+  const draftLedger = DRAFT ? readJSON(LEDGER, {}) : {};
+  const sent = [], skipped = [], needsConfirm = [], drafted = [];
   for (const [vid, rows] of Object.entries(byVid)) {
     const c = cmap[vid];
     if (!c || c.noChase) { skipped.push({ vid, n: rows.length, reason: c?.noChase ? 'no-chase vendor' : 'no sample email/account on file' }); continue; }
+    // In SEND mode, an unconfirmed (resolved/fallback) recipient is HELD — never fires. In DRAFT
+    // mode the draft itself IS the review, so everything is drafted for Steve to check + send.
+    if (c.needs_confirm && !DRAFT) { needsConfirm.push({ vid, name: c.name, to: (c.sample_email || c.main_email), n: rows.length, skus: rows.map((r) => r.sku), reason: 'main-email fallback — CONFIRM person vs recent emails before send' }); continue; }
+    if (DRAFT) {
+      const fresh = rows.filter((r) => !draftLedger[r.recordId]);   // only newly-outstanding SKUs
+      if (!fresh.length) continue;                                   // already drafted — no dup
+      const draft = compose({ name: c.name, account_number: c.account_number, sample_email: c.sample_email, main_email: c.main_email, ship_to: SHIP_TO }, fresh.map((r) => ({ mfr: r.mfr })));
+      const res = await georgeDraft({ account: 'info', to: draft.to, subject: draft.subject, body: draft.html });
+      if (res.ok) {
+        for (const r of fresh) draftLedger[r.recordId] = today;      // dedup marker (NOT a FileMaker stamp)
+        drafted.push({ vid, name: c.name, to: draft.to, n: fresh.length, draftId: res.id, confirm: !!c.needs_confirm });
+      } else { skipped.push({ vid, n: fresh.length, reason: 'George draft failed: ' + res.detail }); }
+      continue;
+    }
     const draft = compose({ name: c.name, account_number: c.account_number, sample_email: c.sample_email, main_email: c.main_email, ship_to: SHIP_TO }, rows.map((r) => ({ mfr: r.mfr })));
-    // Steve 8/20: a fallback/resolved recipient (main_email, not a hard sample desk) NEVER
-    // auto-sends — the person must be confirmed against recent emails first. Hold it out.
-    if (c.needs_confirm) { needsConfirm.push({ vid, name: c.name, to: draft.to, n: rows.length, skus: rows.map((r) => r.sku), reason: 'main-email fallback — CONFIRM person vs recent emails before send' }); continue; }
     if (!SEND) { sent.push({ vid, name: c.name, to: draft.to, n: rows.length, skus: rows.map((r) => r.sku), dryRun: true }); continue; }
     const res = await georgeSend({ account: 'info', to: draft.to, subject: draft.subject, body: draft.html });
     if (res.ok) {
@@ -147,16 +179,23 @@ const run = async () => {
       skipped.push({ vid, n: rows.length, reason: 'George send failed: ' + res.detail });
     }
   }
+  if (DRAFT) writeFileSync(join(ROOT, LEDGER), JSON.stringify(draftLedger, null, 2));
 
-  const report = { ranAt: new Date().toISOString(), mode: SEND ? 'SEND' : 'DRY-RUN', window: win, stampDate: today,
-    vendorsSent: sent.length, vendorsSkipped: skipped.length, vendorsNeedsConfirm: needsConfirm.length, sent, needsConfirm, skipped };
+  const mode = DRAFT ? 'DRAFT' : SEND ? 'SEND' : 'DRY-RUN';
+  const report = { ranAt: new Date().toISOString(), mode, window: win, stampDate: today,
+    vendorsSent: sent.length, vendorsDrafted: drafted.length, vendorsSkipped: skipped.length, vendorsNeedsConfirm: needsConfirm.length, sent, drafted, needsConfirm, skipped };
   mkdirSync(join(ROOT, 'data', 'runs'), { recursive: true });
   writeFileSync(join(ROOT, 'data', 'runs', `scheduled-${today.replace(/\//g, '-')}.json`), JSON.stringify(report, null, 2));
 
-  console.log(`Scheduled follow-up — ${report.mode} · window ${win} (${MIN_AGE_DAYS}-${MAX_AGE_DAYS}d) · dedup=FIELD empty\n`);
-  console.log(`Would ${SEND ? 'HAVE SENT' : 'send'} to ${sent.length} vendor(s):`);
-  sent.forEach((s) => console.log(`  ${SEND ? '✓' : '·'} ${s.name.padEnd(30)} [${s.vid}] → ${s.to}  (${s.n} SKU${s.n > 1 ? 's' : ''})`));
-  if (needsConfirm.length) { console.log(`\n⚠ ${needsConfirm.length} main-email fallback(s) — HELD for person-confirmation (never auto-sent):`); needsConfirm.forEach((s) => console.log(`  ? ${s.name.padEnd(30)} [${s.vid}] → ${s.to}  (${s.n} SKU${s.n > 1 ? 's' : ''}) — ${s.reason}`)); }
+  console.log(`Scheduled follow-up — ${mode} · window ${win} (>=${MIN_AGE_DAYS}d, ${CATCHUP_DAYS}-day catch-up band)\n`);
+  if (DRAFT) {
+    console.log(`Created ${drafted.length} draft(s) in info@ Drafts for review:`);
+    drafted.forEach((s) => console.log(`  ✎ ${s.name.padEnd(30)} [${s.vid}] → ${s.to}  (${s.n} SKU${s.n > 1 ? 's' : ''})${s.confirm ? '  [CONFIRM recipient]' : ''}`));
+  } else {
+    console.log(`Would ${SEND ? 'HAVE SENT' : 'send'} to ${sent.length} vendor(s):`);
+    sent.forEach((s) => console.log(`  ${SEND ? '✓' : '·'} ${s.name.padEnd(30)} [${s.vid}] → ${s.to}  (${s.n} SKU${s.n > 1 ? 's' : ''})`));
+    if (needsConfirm.length) { console.log(`\n⚠ ${needsConfirm.length} main-email fallback(s) — HELD for person-confirmation (never auto-sent):`); needsConfirm.forEach((s) => console.log(`  ? ${s.name.padEnd(30)} [${s.vid}] → ${s.to}  (${s.n} SKU${s.n > 1 ? 's' : ''}) — ${s.reason}`)); }
+  }
   if (skipped.length) { console.log(`\nSkipped ${skipped.length} (no contact / no-chase / error):`); skipped.forEach((s) => console.log(`  - [${s.vid}] ${s.n} SKU(s): ${s.reason}`)); }
   console.log(`\n→ data/runs/scheduled-${today.replace(/\//g, '-')}.json`);
 };

← 40a48ce auto-data-snapshot: 2026-08-20T10:13:33 (2 data files) — dat  ·  back to Sample Followup Sweep  ·  sample-sender: draft mode + 3-day exact-10d catch-up window b62f3d9 →