Creative ideas + design notes
Commits with substantial prose (≥120 chars) — the rationale behind each move.
5b98d5b · 2026-09-26 · Sent-stamp watcher: dead-letter heartbeat + vid-keyed pull-forward (TK-12351)
Heartbeat to ~/.claude/skills/sample-followup/data/latest.json after each live pass
(PASS/WARN/FAIL): WARN on unresolved sends within 5d of aging out of the 21d search or
aged out in the last 7d, or when the pass could not measure; FAIL when a pass throws.
--dry prints it and never writes. Pull-forward date ownership now keys on any shared
resolved vid (Lincrusta sample vs main email), address fallback when unmapped. Unreadable
ledger now fails closed instead of overwriting it. Skip entries record sent_on.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDCCv6ZcKPKfWxof41UzvC
d523fb1 · 2026-09-26 · sent-stamp watcher: fold review findings (never overwrite a chase date)
- covered-only is no longer terminal: a sibling item in the same letter
may not be in FileMaker yet, so the send keeps retrying.
- per-record read-before-write: skip any record that already carries a
chase date, independent of the already-stamped set; refuse to stamp a
send if that set hit its 800-row cap.
- an older unfinished send in backoff is pulled forward when a newer send
to the same recipient is due, so the older letter keeps its date.
Tests extended; negative fault injection still goes red. TK-12320
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDCCv6ZcKPKfWxof41UzvC
523a32c · 2026-09-26 · watch-sent-stamp: refuse unknown args instead of running a live pass (TK-12320)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbUKLFLXi7LEQcGyGeYqp9
a6b53a0 · 2026-09-26 · sent-stamp watcher: retry skipped/zero sends, stamp the real send date
A ledger entry now only suppresses re-processing once the send actually
stamped (or found its letter items already stamped) with no failed commit.
skip / stamped:0 sends are retried (15 min backoff) inside a 21-day Sent
scan, so sends that landed before their email->vid mapping existed are no
longer poisoned forever. The chase date is the message's send date
(internalDate), sends are processed oldest-first so the first letter owns
the date, FM/George lookup errors no longer masquerade as 'nothing to
stamp', and contact-only vendors (Phillip Jeffries) map via the
slug-derived vid. Pure logic moved to sent-stamp-core.mjs with a negative
test. TK-12320
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDCCv6ZcKPKfWxof41UzvC
616e3a8 · 2026-09-24 · sample-followup: fix TK-12091 window regression + shared window + canary tripwire
Part 1 (DTD panel verdict A): restore the FULL active [today-60..today-10] query
window every run. TK-12091 (a11a644) had narrowed scheduled-run.mjs to a
day-of-week band, silently reopening TK-12013/14's permanent-hole bug — a live
canary found 15 outstanding stragglers (some 56d old, incl. DWTT70793) never
drafted. Dedup (Sent-stamp + draft-ledger) makes the wide re-query a safe no-op.
Part 2 (Cody's co-required fix): single source of truth for the window —
followupWindow() in lib/sweep.js, called by BOTH scheduled-run.mjs and the
coverage canary, so production and audit can never drift again. Canary now runs
windowCoversActiveBand() as a TK-12091-regression tripwire: if production's
window is ever re-narrowed below the full active band, the canary goes FAIL
instead of silently auditing a wider window than production chases. Ships a
negative test (test 8) proving the tripwire goes red on an injected narrow window.
Verified: window now 07/26..09/14; DWTT70793/Thibaut back in candidate set;
canary leaked=0, band covers=true; 8/8 tests pass. sweep()'s orphaned DOW logic
is dead code (nothing calls it) — left untouched, noted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
83b47fa · 2026-09-24 · sample-followup: LA Walls/DWLC1059 tracked as known-unresolvable (TK-12119)
Add DWLC1059 (LA Walls pattern 23003) to known-unresolvable set — vendor has
no vid anywhere in the fleet, so sweep can't chase it. Bucket it as
noVendorMap (accounted/WARN) instead of leaked (FAIL). Canary verdict:
FAIL → WARN, leaked 1→0. Reversible: remove SKU from set or delete set entry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8c3b575 · 2026-09-24 · sample-followup: fix tk12118 SKU guard to verify on REPORT ON SAMPLES ORDERED (TK-12118)
'combo sku' is findable but does not serialize into fieldData on the write layout
(Report for old memo samples), so the SKU-match guard always saw "" and skipped.
Verify the SKU on REPORT ON SAMPLES ORDERED (same base table, same recordId space,
field reads correctly); write vid on the write layout; verify the write via the
canary leaked-count (vid does not read back on either layout). Backfill landed:
canary leaked 4->1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
32a76cf · 2026-09-24 · sample-followup: canary no longer counts deferred recipient-busy rows as covered (TK-12119)
Split the recipient-busy suppression by age: a fresh batching deferral (<=24d =
10-day trigger + one 14-day window) counts as covered, but a row deferred past
that window escalates to recipientBusyStale -> uncovered -> WARN. Closes the
false-green where a perpetually-recontacted vendor's never-chased sku hid as
"covered." First live run surfaced 33 real stale-deferred rows. Ships a negative
test proving the aged row flips from the old covered/PASS to WARN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1d2b680 · 2026-09-23 · sample-followup: coverage-reconciliation canary (silent-skip false-green safety net) — TK-12117
READ-ONLY scripts/coverage-reconcile.mjs queries live FileMaker for the true
outstanding population (two-read vid+letterSent join) and reconciles it against
the sweep's own bucketing (lib/sweep.js reconcileCoverage). A row outstanding in
FM but in NO sweep bucket = LEAKED = FAIL; never PASS on 'it ran'. Emits
PASS/WARN/FAIL + latest.json. Ships test/coverage-reconcile.test.cjs (5 asserts,
incl injected-leak FAIL) + a --self-test seam. First live run FOUND 4 real
empty-vid silent skips (FAIL). launchd install drafted to pending-approval (gated).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
473c034 · 2026-09-23 · sample-followup: resolve MAJ (Majilite) + BRAND (Brand McKenzie) vendor-map holes (TK-12114)
MAJ = Majilite -> jabshire@majilite.com (James Abshire), confirmed by DW's own
repeated 'New Sample Request' outbound (2026-09-10/21/22) + replies. BRAND =
Brand McKenzie -> sales@sancarwallcovering.com (Sancar acct 1674444, its US
distributor; replies to our sample requests/follow-ups), needs_confirm w/ direct
richard@brandmckenzie.com as documented alternate. no-vendor-map count 2 -> 0.
DRAFT/dry-run only, nothing sent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
05177f2 · 2026-09-23 · sample-followup: record 3 manually-sent 10-day letters + gated FM-stamp script (TK-12109)
Steve sent FABR/WAT/GRD 10-day letters manually from info@ (verified in Sent).
recently-contacted.json += the 3 recipients (14-day anti-dup now reflects it).
Added scripts/tk12109-stamp-letter-sent.mjs (+ --rollback) to stamp FM
Date Sample Request Letter Sent=09/23/2026 on the 3 outstanding records
(541857/541215/541860) with a restore-map — gated to Steve (classifier blocks
the FM write). BAR held (not sent), REID arrived, MDCKEN no-chase — not stamped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6c9b709 · 2026-09-23 · sample-followup: draft the 5 verified-owed 10-day SKUs + CONFIRM-RECIPIENT banner/ledger mark (TK-12107)
send-reconciled.mjs draft() now returns the George draftId; needs_confirm vendors
get an INTERNAL 'CONFIRM RECIPIENT' body banner + a '⚠ CONFIRM RECIPIENT' ledger
mark so Steve fixes To: before send. Created 4 drafts (5 SKUs): FABR customers@
fabricut.com, WAT sophie@watts1874.co.uk [confirm], BAR studio@/alice@barnebygates.com
x2 [confirm], GRD orders@graduatecollection.co.uk [confirm]. REID guard-suppressed
(arrived), MDCKEN no-chase. Drafts only — nothing sent, no FM write.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d04d106 · 2026-09-23 · sample-followup: two-part never-false-chase guard (arrival-dedup + already-sent) — TK-12105
Before chasing a sku, pull ALL FileMaker records ('Report for old memo samples')
for that combo sku and suppress if EITHER (a) a sibling with the SAME entered date
already has Date WP Sample Sent (arrived on a duplicate record — catches REID
DWRW210156, spares GRD8806 2024-vs-2026), or (b) any record has Date Sample
Request Letter Sent (Steve's authoritative 10-day-letter stamp). Pure suppressChase()
in lib/sweep.js (+4 unit tests), wired into scheduled-run.mjs + send-reconciled.mjs
with a new guard-suppressed bucket. Fail-open; 14-day Gmail anti-dup kept. Nothing sends.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7534717 · 2026-09-23 · sample-followup: exclude MDCKEN as non-vendor no-chase (TK-12101)
Steve 2026-09-23: MDCKEN is not a vendor. Added contacts.json disposition
no-chase entry (same mechanism as SPN/Spoonflower) so MDCKEN (DWPP rows)
resolves as no-chase BEFORE the 'no vendor map' path — never chased, never
drafts, never surfaces as blocked/unresolved. Verified in both scheduled-run.mjs
and send-reconciled.mjs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a11a644 · 2026-09-23 · sample-followup: day-of-week-aware window (Tue 4-day catch-up, Wed-Fri strict) — TK-12091
Replaces the pure single-day filter (e3a7a73). windowRange (scheduled-run.mjs),
WIN (send-reconciled.mjs), and lib/sweep.js now: hi=today-10 always; TUE widens
lo to today-13 (covers Sat/Sun/Mon/Tue 10th-day landings), Wed/Thu/Fri (+manual
off-days) strict single day. New test/sweep-dow.test.cjs asserts (a) Tue picks
Sat/Sun/Mon/Tue cohorts, (b) Wed/Thu/Fri pick only today-10, (c) Fri->Tue->Wed
selects every Entered date exactly once (no gap/dup). WIN/CATCHUP/WIDEN_DAYS kept
as manual recovery nets. pre-send-gate + human batch-send untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
e3a7a73 · 2026-09-23 · sample-followup: single-day rolling window (Entered==today-10), revert >10d backlog (TK-12087)
scheduled-run.mjs windowRange, send-reconciled.mjs WIN, and lib/sweep.js now
chase ONLY the cohort entered EXACTLY minAgeDays(10) calendar days ago instead
of the [10..60] cumulative backlog introduced by TK-12013/12014. WIDEN_DAYS/
CATCHUP/WIN remain as explicit manual recovery nets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7394a41 · 2026-09-23 · sample-followup: resolve 68 blocked overdue rows (TK-12085)
send-reconciled.mjs now falls back to contacts.json via slugForVid (single
source of truth) + strips \r from vids (KRA\r bug); adds fabr/wat/reid/grd,
corrects ftr->shafik@futuretextiles.com, 1838 email confirmed. 19 vendor
drafts created (DRAFT-only); MDCKEN(8) unresolved, SPN(2) no-chase.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
99d3128 · 2026-09-22 · fix(fleet): fall back to contacts.json vendor name before the (vid X) placeholder
Display-only. build-fleet.js name expressions (both the group row and the
alias-stub map) rendered '(vid GREEN)' because GREEN isn't in the NAME map,
even though contacts.json has green -> 'Greenland (bmwallpaper)'. Now falls
back to the contacts name keyed by slug. Helps any vendor with a contacts
name. Regenerated fleet.json: GREEN row label fixed, its 13 items / vid /
slug / aliasVids UNCHANGED. No send, no FileMaker write, no re-attribution.
TK-12013
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBN9bgoXhsUaDnGkXU1T6m
89f8650 · 2026-09-22 · refresh fleet.json from live FM sweep + declare 1838 WALLCOVERINGS slug alias
fleet.json (the :64845 console's vendor-row source) was 28 days stale
(Aug 25). Rebuilt from a fresh 10-60d outstanding sweep so all current due
vendors render as approvable rows. Added the '1838 WALLCOVERINGS' -> 1838-
wallcoverings alias the build-fleet dup-slug guard required (same vendor).
TK-12013
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBN9bgoXhsUaDnGkXU1T6m
5374f08 · 2026-09-22 · fix(window): query full active [today-60..today-10] cohort every run
The auto-widen anchor only reached back to the last SUCCESSFUL run
(lastCoveredHi=09/02 -> lo 09/03), so older still-outstanding, still-unchased
memos (07/24-09/02) fell outside the window and were never chased; a run of
missed days made the hole permanent. send-reconciled used a today-16..today-10
band with the same defect. Both now query the full active window; the FileMaker
Sent-stamp set-difference (scheduled-run) / draft-ledger (send-reconciled) is the
dedup, so re-covering already-chased memos is a no-op and a missed run day can
never drop the older cohort.
TK-12014 / TK-12013
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBN9bgoXhsUaDnGkXU1T6m
fcd34dc · 2026-09-22 · fix(sweep): include exactly-10-day-old requests (age < minAge, was <=)
The >10-day rule excluded requests aged exactly 10 days (a 9/12 request due
today was skipped until age 11). Steve: exactly-10 must be INCLUDED.
lib/sweep.js:36 and build-fleet.js:41 both had the age<=10 off-by-one.
TK-12014 / TK-12013
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBN9bgoXhsUaDnGkXU1T6m
be622a9 · 2026-09-12 · TK-11524: close sample-followup no-email gap (item d)
- lib/slug-aliases.cjs: add SCHUMACHER->sch to the single-source-of-truth
SLUG_ALIASES. The merge previously lived only in build-fleet.js's local
VID_ALIAS, so scheduled-run.mjs's runtime contact bridge mapped raw vid
SCHUMACHER->slug 'schumacher' (no contact) -> CONFIRM-SAMPLE-EMAIL skip-gap
(the two-copies-diverge class the file header warns about, cf SAND->Sandberg).
- data/contacts.json ulr: resolve Ulrich Welter -> info@welter-wandunikate.de
(Welter Wandunikate; verified via Gmail two-way order thread msg 17e4fa0f0ecd5b7d),
HIGH confidence, held needs_confirm:true pending Steve confirm (no auto-send).
- data/contacts.json tbd: disposition no-chase (FileMaker placeholder vid, not a
real vendor; real fix = correct the source memo vid in FileMaker, gated).
Verified: contacts.json valid, build-fleet.js parses, slugForVid(SCHUMACHER)=sch,
0 undispositioned no-email slugs remain (was 6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3SgrKtLCZRzmFK5fp9qy8
8e1807b · 2026-09-12 · lib/slug-aliases.cjs: one source of truth for slug routing — the two copies had already diverged (TK-11255)
The alias table lived in TWO hand-maintained places: scripts/build-fleet.js
(grouping) and scripts/scheduled-run.mjs (the vid->contact bridge). They had
ALREADY drifted apart — only the bridge knew that bare vid '1838' is the same
vendor as '1838 Wallcoverings'. Two copies of one routing table is precisely the
failure mode that produced SAND->'Sandberg' and sent Sanderson memo chases to
Gimmersta, an unrelated company, earlier in this ticket. Flagged it as a smell in
6771f15; the drift was already there when I looked.
Now exactly one definition exists in the repo (verified by grep), exporting
SLUG_ALIASES, normVid and slugForVid.
Two real bugs fixed by consolidating rather than by intent:
- build-fleet.js never stripped CR/whitespace from vid either, so a record with
vid "kra\r" grouped under its own bogus slug. It now shares normVid() with
scheduled-run, which got that fix in e343b5f.
- build-fleet.js lacked the '1838' alias, so the bare vid grouped separately
from '1838 Wallcoverings'. Both now converge on '1838-wallcoverings'.
Re-tested after the change, not assumed — including the negative test, because a
consolidation that quietly disarms the guard is the worst outcome here:
T1 distinct vids -> writes
T2 ANNA FRENCH with items -> merges into thib, no stray anna-french slug
T3 'FOO BAR' + 'FOO-BAR' -> exit 1, fleet.json NOT written (guard still reddens)
ESM -> CJS import proven at RUNTIME, not just by node --check, since
scheduled-run.mjs is ESM and this module is CJS.
Live re-verification through the REAL shared module (not a reimplementation of
its logic): 27 of 28 live vendors routable, 129 overdue records, only FUT
unresolved — unchanged, so consolidation regressed nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5c0d68e · 2026-09-12 · contacts.json: route VAH + BRU + bare-1838 — 27 of 28 live vendors now reachable (TK-11255)
Closes most of the silent-skip gap measured today: of 129 live overdue sample
records, 11 across 7 vendor codes resolved to NO contact and were dropped with no
error and no log line.
vah Vahallan -> info@vahallan.com, acct "On File"
Verified live before adopting: Annie Kaufman, Senior Sales Coordinator,
replied "Thank you for your sample request. We will get these sent out"
to our own request YESTERDAY (2026-09-11) and again 2026-09-03.
bru Brunschwig & Fils (@ Kravet) -> matt.schoffman@kravet.com, acct 10087117
Canonical says "Brunswig and fils @ Kravet" and Brunschwig is a Kravet
house, so it shares Kravet's desk and account. Held to the same bar
rather than assumed: Matt Schoffman replies to our sample requests with
UPS tracking, most recently 2026-09-10.
1838 bare vid routed to the existing 1838-wallcoverings contact via
SLUG_ALIASES — same vendor (Scott Myer), so this is a routing alias
rather than a duplicated contact record.
Measured effect, live: routable vendors 24 -> 27 of 28; unroutable records
8 -> 2.
NOT adopted: FUT (2 records). Canonical has no FUT row. It is probably the same
vendor as FTR ("future Textile", ftg1@futuretextiles.com) on a name-shaped hunch,
and that is exactly the kind of guess that put Sanderson memos at Gimmersta
earlier in this ticket. It stays unroutable until something confirms it.
Both new accounts use the house "On File"/inherited-account convention rather
than an invented number, and the scheduled job runs --draft, so the effect is a
reviewable draft per vendor, never an automatic send.
Undo: restore data/contacts.json.bak-1789224483
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6771f15 · 2026-09-12 · scheduled-run: decouple the vid->contact bridge from the fleet snapshot (TK-11255)
fleet.json does DOUBLE DUTY: it is the outstanding-items snapshot (which
legitimately shrinks as samples arrive) AND the vid->contact bridge (which must
stay complete). vidContactMap built the bridge only from fleet rows, so a vendor
with no CURRENT overdue items had no contact — and the next time one went
overdue it was silently skipped until someone regenerated the file. Nobody
regenerates it: no launchd job runs build-fleet.js and scheduled-run only reads.
This was measured, not theorised. I regenerated into a SANDBOX and diffed:
gains 4 vids (1838, BRU, FUT, VAH)
DROPS 37 (PJ, WOL, PF, SCH, MOR, THI, ELI, LIN, RAD, TWI, KOR, ...)
A naive regeneration would therefore have inflicted the exact bug I was chasing
on 37 vendors to fix it for 4. The regenerated file was NOT promoted.
Fix: slug is derivable from vid, so the bridge can go straight to contacts.json.
vidContactMap now takes the live vid set and UNIONs fleet-derived entries with
slug-derived ones. Union only — it cannot make a vendor that resolves today stop
resolving. Derivation runs from the live VID, never backwards from the slug,
because slug->vid is lossy ('1838-wallcoverings' could come from a space or a
hyphen).
HONEST EFFECT TODAY: zero vendors recovered. Verified live — resolvable is 24 of
28 both before and after, because e343b5f (vid \r trim) and a1c761a (POI, 1838)
already recovered everything recoverable. The value here is that regenerating
fleet.json is now SAFE, and a vendor dropping out of the outstanding list no
longer quietly loses its contact. Prevention, not a fix.
Still unroutable: 1838, FUT, VAH, BRU — 8 overdue records. Those need
contacts.json entries, not code.
Known smell, deliberately not addressed here: SLUG_ALIASES now exists in both
build-fleet.js and scheduled-run.mjs. Two copies of one routing table is exactly
the failure mode that produced SAND->Sandberg; it wants consolidating into one
shared module.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a1c761a · 2026-09-12 · contacts.json: unblock POI + 1838, both silently skipped by the mailer (TK-11255)
Measured against LIVE FileMaker: 11 of 129 overdue sample records in the current
10-60 day window resolve to no contact and are skipped with no error and no log.
These are two of them, and both are now recoverable.
poi -- already had samples@justindavid.com as of today, but
vidContactMap requires email AND account_number, so it
was still being skipped. Canonical carries a blank
account, so it takes the house "On File" convention
(already used by 32 of 58 contacts: pj, mor, green,
maya, twi, olp, con...).
1838-wallcoverings -- canonical VID '1838' = scott.myer@gmail.com. Verified
live before adopting: Scott replied "memo has been
ordered. thank you scott" to our own sample request on
2026-09-01, and an earlier thread quotes stock and euro
roll pricing. A personal gmail looks wrong for a vendor
desk, but it clears the hardened bar on its own evidence
(human-authored, sample-topical, current) and matches the
known small-brand pattern already accepted for twi.
Note account_number is customer-facing -- send-reconciled.mjs prints "Our account
number is X" -- so "On File" is a deliberate reuse of the existing convention,
not an invented number.
Safe by construction: the scheduled job runs scheduled-run.mjs --draft, and the
code is explicit that "Nothing sends -- the draft is still the human gate." These
two vendors now produce a reviewable draft instead of silently producing nothing.
Still unroutable and NOT touched here: VAH, BRU, 1838 (bare vid) and FUT are
absent from data/fleet.json entirely, so a contacts entry alone cannot reach them
-- vidContactMap iterates fleet rows. They need the fleet regenerated first.
Undo: restore data/contacts.json.bak-1789224232
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
e343b5f · 2026-09-12 · sample-followup: strip CR/whitespace from FileMaker vid — it was silently dropping chases (TK-11255)
Measured live against FileMaker, not hypothesised: of 129 overdue sample records
in the current 10-60 day window, 11 across 7 vendor codes resolve to NO contact
and are skipped with no error and no log line. One of those is a plain key bug.
A Kravet record's vid is literally "kra\r". scheduled-run.mjs keyed the contact
map with String(vid).toUpperCase() and no trim, so "KRA\r" never matched "KRA"
and that vendor's overdue sample was never chased. Same untrimmed pattern in
resolve-original-dates.mjs. build-fleet.js already strips \r from Mfr Pattern and
client name — vid was simply missed.
This is the silent-skip class: nothing errors, nothing logs, the sample just
never gets chased and a client waits.
Both sites now strip CR/LF and trim before upper-casing.
The other 6 skipped vids are NOT this bug and are deliberately not touched here:
1838, FUT, VAH, BRU -- vid absent from data/fleet.json, which is 29 days stale
and is regenerated by nobody (no launchd job runs
build-fleet.js; scheduled-run only READS the file).
These are vendors whose samples went overdue after the
last build. Fixed by regenerating, now that d8486af
makes regeneration safe for the alias stubs.
POI -- has an email as of today but no account_number, and
vidContactMap requires BOTH.
1838 WALLCOVERINGS -- no email, no account, and no canonical FileMaker row.
Note the item list itself is NOT stale — scheduled-run pulls it live from
FileMaker each run. Only the vid->contact bridge is stale, so the failure mode
is silently skipping a vendor, never chasing a dead lead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
eafdc77 · 2026-09-12 · chore: v1.1.0 -> v1.1.1 (TK-11255 vendor-desk routing fix, session close)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSLggdzyGrT8jd9Ru34ndL
e0317eb · 2026-09-12 · build-fleet: declare slug aliases + fail the build on an undeclared collision (TK-11255)
DTD verdict B, narrowly scoped. Peer commit 706e430 already fixed the SAND name
constant, so this is the remaining half and deliberately does NOT attempt the
rearchitecture the panel's D voters proposed.
WHAT THE PANEL GOT WRONG, and why this is small: 3 of 5 voted D ('identity does
not belong in a derived artifact') on a premise I supplied incorrectly. The
duplicate slugs are NOT a defect -- slug derives from vid, so THIB/ANNA FRENCH/
ANNA yield three DISTINCT slugs and the generator provably cannot collide them.
Every duplicate row has items:0 and commit ae6499b says 'fleet stubs so
vidContactMap resolves': they are deliberate distributor-alias stubs (Thibaut
distributes Anna French). Deduping them would have BROKEN contact routing.
Three changes:
1. SLUG_ALIASES -- the hand-edited stubs are now an explicit, commented,
version-controlled whitelist. They previously lived only as hand edits in
data/fleet.json, so any regeneration would have silently erased them.
2. The alias is applied when DERIVING the slug, not bolted on afterwards. A
negative test caught that ANNA FRENCH only routed to 'thib' while it had NO
items; give it real samples and it got its own 'anna-french' slug with no
contact behind it. Now routing is identical either way, and the aliased vid is
recorded in aliasVids so stamping/reply-matching don't lose it.
3. A build-time guard at GROUPING time, not on the output rows. The first
version of this guard PASSED its own injected-fault test for the wrong reason:
the generator's own `g[slug] = g[slug] || {...}` had already merged the
colliding vendors into one
row, so by the time the output was inspected the collision was invisible. That
is a worse bug than the one I set out to guard -- two distinct vids that
normalize to one slug silently attribute one vendor's outstanding samples to
the other. It now refuses to write fleet.json at all.
Tested (positive, alias, and injected fault -- a positive-only test would have
shipped the broken first guard):
T1 distinct vids -> writes, no false failure
T2 ANNA FRENCH WITH items -> merges into thib, aliasVids recorded, no stray
anna-french slug
T3 'FOO BAR' + 'FOO-BAR' -> exit 1, fleet.json NOT written
Run in an isolated tree so the real data/fleet.json was never touched.
Consumer hardening: server.js gains vendorBySlug() and scripts/fmpro.mjs an
inline equivalent, replacing 4 bare .find(x => x.slug === s) calls that depended
on stubs sorting last. Verified behaviour-identical on today's data -- this
removes fragility, it does not fix a live bug.
NOT done on purpose: no runtime join to canonical FileMaker. It is hand-typed,
17% of its sample-email values are not addresses, it became API-readable only
yesterday, and the mailer runs entirely off local files -- that trades a rare
silent-wrong-name for an intermittent cannot-send with nobody on call.
fleet.json is NOT regenerated here; regenerating is now safe but is a separate,
reviewable step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
706e430 · 2026-09-11 · build-fleet: VID SAND is Sanderson (@ Jasper Showroom), not Sandberg — fix name map so regenerated fleet.json stops mislabeling it (TK-11255)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSLggdzyGrT8jd9Ru34ndL
42be6f7 · 2026-09-11 · contacts.json: mor (Morris & Co) -> Jasper Showroom desk, matching sand; was departed Nicole.Monbarren@ (TK-11255)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HSLggdzyGrT8jd9Ru34ndL
641d84c · 2026-09-11 · contacts.json: fix sand — it is Sanderson via Jasper, NOT Sandberg (TK-11255)
This was a live misrouting, not a stale address. The mailer has been chasing
Zoffany and William Morris memo samples at Gimmersta, an unrelated Swedish
company, because contacts.json labelled slug 'sand' as 'Sandberg (via
Gimmersta US)'.
The products prove the identity, which is why this is not a judgement call:
slug sand -> ZOW0127-02 / DWZF187058 (Zoffany) and 217209 / DWWM14281
(William Morris) -- both Sanderson Design Group brands
slug sandb -> DWSS SKUs -- the REAL Sandberg, left untouched
Canonical FileMaker agreed all along: VID sAND Company = 'Sanderson Wallpaper
@ Jasper Showroom'. contacts.json and fleet.json were both wrong on the name.
Evidence chain for the new address, each link human-authored and sample-topical:
1. canonical Nicole.Monbarren@sandersondesigngroup.com AUTO-REPLIES 'no longer
with Sanderson Design Group' to our own 10-day chase (2026-07-07)
2. Melissa Bacoka (SDG National Sales Manager, West), 2026-07-09: 'you can
request samples through Jasper, copied here'
3. Kathalene Barton (Jasper outside sales), twice: 'Copying in our Sample Dept'
4. Sample@jaspershowroom.com -- Alisa Derghazian, Resource librarian --
confirms and ships DW sample requests with tracking, most recent 2026-07-09
5. corroborated by a 2022 SDG email: 'Jasper has Morris, Zoffany & Sanderson'
Note this is a role alias that PASSES the hardened bar on its own evidence
(human-authored, sample-topical), not on the 'desk beats named rep' prior that
this ticket already refuted 3-for-3.
Also corrected the display name, since the wrong label is what caused the
misrouting in the first place.
Undo: restore data/contacts.json.bak-1789163136
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f195f74 · 2026-09-11 · contacts.json: fix 3 vendor sample desks on Gmail evidence, not preference (TK-11255)
DTD verdict D-hardened. The contrarian's objection was that a naive 'recent
two-way correspondence' test measures whether a human replied to SOMETHING, not
whether the address is the sample-intake channel -- and that it would be fooled
by ticket-system auto-acks on exactly the desk-alias rows in dispute. So the
query required an INBOUND, human-authored, sample-topical reply, ran a positive
control in the same pass, and used sand as a negative control.
Both controls behaved: cathy.dy@thibautdesign.com (control) returned live
human sample replies; support@gimmersta.com (negative control) returned ONLY
machine order-confirmations and produced no qualifying reply. The instrument
discriminates rather than rubber-stamping domain activity.
mom customerservice@ -> ddedic@momtex.com
mined value returned ONLY machine 'SAMPLE SHIPPED NOTIFICATION' mail;
Diana Dedic answers our sample-followups by hand, most recently TODAY.
pf mocampo@ -> hbarragan@kneedlerfauchere.com
STRONGEST case: on 2026-08-17 mocampo@ replied to our own sample chase
with 'Please send your sample request directly to our Sample Department,
as I do not handle sample requests.' We kept mailing her for a month.
hbarragan@ signs as 'lead sample librarian'.
hyg support@ -> info@hyggeandwest.com
mined value is a MARKETING newsletter sender; info@ answers sample
requests with a human.
NOT changed -- sand falls to escalation exactly as the verdict required, and
BOTH candidates are now disproved: canonical Nicole.Monbarren@ auto-replies
'no longer with Sanderson Design Group' (forwarding to Melissa.Bacoka@), and
mined support@gimmersta.com is automated receipts only. New lead, not a fix.
Refutes the 'general desk beats named rep' prior the panel nearly adopted as a
tiebreaker: every generic alias here is a bot or a mailing list and every real
sample desk is a named human -- it would have chosen wrong 3 for 3.
Undo: restore data/contacts.json.bak-1789162556
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6d72c91 · 2026-09-11 · contacts.json: adopt canonical sample desks for dtex + mdc (TK-11255)
Steve: 'take your recommendations'. Applied ONLY the 2 drift rows where the
canonical FileMaker value is unambiguously the better one -- a dedicated sample
desk replacing a named rep / general CS address:
dtex ddawson@designtex.com -> samples@designtex.com
mdc cs@mdcwall.com -> sample@mdcwall.com
Each write asserted the CURRENT value equalled the expected prior value before
overwriting, so a state change since the reconciliation would abort rather than
silently clobber a value I never inspected.
Deliberately UNCHANGED, per the same recommendations:
wol keeps mined samples@wolfgordon.com -- a dedicated sample desk, and
dw_unified.vendor_contacts independently agrees against canonical's
customerservice@. Canonical is authoritative but not uniformly better.
thib keeps cathy.dy@thibautdesign.com -- this was never real drift. fleet.json
maps slug 'thib' to three vids (THIB / ANNA FRENCH / ANNA); canonical
Thib=cathy.dy@ MATCHES, and anna=cs@ is a different vendor. Fix the slug,
not the address.
Still open, needing a real answer rather than a preference:
mom, pf, hyg -- named rep vs general desk; whoever is current wins
sand -- Sanderson Design Group vs Gimmersta are DIFFERENT COMPANIES;
needs a decision about who actually fulfils Sandberg samples
Undo: restore data/contacts.json.bak-1789162028
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f7cb4cb · 2026-09-11 · contacts.json: fill 7 blank vendor sample desks from canonical FileMaker (TK-11255)
Steve enabled the fmrest extended privilege on the hosted `vendors` file,
clearing a 9-day blocker. Reconciled all 58 slugs against the canonical
five-column vendor table (layout 'Main Menu', 1,197 rows) on an exact-VID join.
Applied ONLY the 7 gap-fills -- every one was previously BLANK, so no working
address is overwritten, and each value was asserted to parse as a real email
before writing:
eli fol ftr lin poi rad sch
Deliberately NOT applied: the 8 DRIFT rows, which need a per-row human call.
Notably `wol` KEEPS the mined samples@wolfgordon.com over canonical's
customerservice@ -- it is a dedicated sample desk and dw_unified.vendor_contacts
independently agrees, so canonical is not uniformly the better source.
Also not applied: 47 of 280 (17%) canonical samples-values are NOT addresses --
the field is free text holding operator instructions ('***ORDER ONLINE***',
'DO NOT ORDER SAMPLES!!!!!!!!!!', a partner URL, 'CUT Samples FROM BOOKS IN
HOUSE', and one address with a note concatenated). Blanket-copying canonical
would inject non-addresses into the sample-followup mailer.
Resolves 3 of the 4 long-unresolved vendors: FTR=ftg1@futuretextiles.com,
POI=samples@justindavid.com, ULR=confirmed to have NO sample desk (its field
literally reads DO NOT ORDER SAMPLES). 1838 WALLCOVERINGS has no canonical row.
Undo: restore data/contacts.json.bak-1789161339
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d0bc2fd · 2026-09-10 · TK-11409: sent-poller stops pre-filling the 2nd-request slot on a first stamp
Last hole in this ticket. watch-sent-stamp.mjs wrote BOTH date fields whenever it
detected a real send, but this path only ever makes a FIRST stamp -- records already
carrying FIELD_SENT are excluded via the 'already' set. So it filled the 2nd-request
field on the first chase, and pass 2 then skipped those records forever because the
field was non-empty, silently defeating the tracking Steve asked for today.
Steve's 8/25 'stamp BOTH' was correct when written: the 2nd-request field had no
separate meaning then, so writing it alongside was the only way to populate it.
TK-11409 gave it a distinct meaning, which is what makes the dual-write wrong now.
This serves that intent rather than reversing it, and matches how fmpro.mjs pass 1
was aligned.
Also corrected two stale comments and a log line that reported '2ndReq=<date>' on a
write that was never a 2nd request.
Dry run clean (FM_READONLY=1, no stampable follow-ups in window, nothing written).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
74e43c2 · 2026-09-10 · TK-11409: the static contact list expires instead of blocking forever
data/recently-contacted.json was a PERMANENT blocklist: hand-written, last edited
2026-08-20, 26 addresses, nothing ever expiring -- and it held the biggest accounts
(Kravet, Thibaut, Osborne & Little, York, WallQuest, Sancar, Innovations, MDC,
Maharam, Koroseal, Phillip Jeffries, Wolf Gordon...). So even after the Gmail query
was corrected to match the chase subject, those vendors stayed suppressed from SEND
forever by a snapshot nobody had looked at in three weeks.
This was a THIRD suppression source. The earlier fix narrowed one of them and I never
checked whether another existed -- same failure family as the rest of the ticket: a
record taken once and trusted indefinitely.
- File converted from a bare array to { address: 'YYYY-MM-DD' }, every entry seeded
with the file mtime (the only honest date available).
- Entries now EXPIRE after RECENT_DAYS.
- A bare-array file is still accepted but treated as expired-unknown, so an old copy
or a hand-edit cannot resurrect permanent-blocklist behaviour.
- Each run prints how many are live vs expired, so the list can never go quiet again.
Verified: 0 within 14d, 26 expired. Sancar is now correctly sendable (no chase in 14
days -- its recent mail was new sample ORDERS). WallQuest stays correctly suppressed
(real chase sent 09/01).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
de5df55 · 2026-09-10 · TK-11409: stop shipping the internal 'From job:' banner to vendors
George's withSourceFooter() prepends a visible 'From job: node - <timestamp>' block
INSIDE the message body unless the caller passes no_source_tag. Nothing in this pipeline
passed it, so that internal provenance tag reached vendors on every chase -- confirmed
present in the current drafts AND in chases already SENT on 09/01 and 09/04.
Fixed centrally in the transport rather than at each of the four call sites
(scheduled-run draft + send, send-drafts, send-reconciled, server.js one-click send).
Every path through this transport is a vendor-facing letter -- /api/send and /api/drafts
are its only sendable paths -- and a per-caller flag is a step someone must remember,
which is the failure mode this entire ticket documents. An explicit no_source_tag:false
is still respected.
Verified: unit test confirms the flag on both paths and that explicit-false wins; a live
draft came back source='' with a snippet starting at the real first line instead of
'From job: unspecified'.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
42b4eba · 2026-09-10 · TK-11409: rescue the text of two Aug-15 drafts ~4 days from permanent deletion
com.steve.george-drain-old-drafts permanently deletes info@ drafts at 30 days with no
Trash recovery. Both were written 2026-08-15, so deletion is ~2026-09-14. One is real
unfinished correspondence to Kravet asking two specific unanswered questions (delivery
destination on four samples, status of two outstanding ALTHEA items), unsent for 26 days.
Could not extend/send/move them: the Gmail trash+modify tools are on the claude.ai Gmail
connector (session expired) and the classifier blocks the credential read needed to reach
George's draft endpoints. Capturing the text was the only non-destructive action available.
Also records a separate live defect found while reading them: every chase draft AND every
chase already sent carries a visible 'From job: node' provenance banner inside the message
body, so vendors have been receiving it for weeks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
b4d9ec1 · 2026-09-10 · TK-11409: anti-dup keys on the CHASE subject, not the new-order subject (DTD 6/7 verdict B)
The filter matched subject:'New Sample Request' -- a brand-new ORDER, a different
message type that happens to go to the same vendor desk. So a chase was suppressed
because we had placed an order. Our busiest accounts (Kravet, Thibaut, Osborne, York)
get orders almost daily and were therefore effectively un-chaseable; 33 overdue memos
went unchased for weeks.
Now matches the chase letter's own subject. The -subject:'Re:' exclusion is load-bearing,
not cosmetic: verified against 90 days of info@ Sent, a bare subject:'Sample Follow-Up'
also matches humans replying inside a chase thread AND the 'HAPPY LABOR DAY FROM DW! Re:'
auto-responder -- so without it this fix would have traded one over-suppression bug for
a new one. That correction came from the reviewer's dissent, after the panel had voted.
Verified live: 10 matches, 0 Re:/autoresponder false positives, suppressed recipients
40 -> 10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ed3bc8d · 2026-09-10 · TK-11409: verify the draft-ledger against reality; surface both silent draft-mode outcomes
Third instance of this ticket's failure family: a PROXY trusted without verification.
The draft-ledger recorded 'already drafted' for Osborne & Little, Thibaut and York
while ZERO drafts for them existed in info@ Drafts. 'if (!fresh.length) continue'
then skipped them SILENTLY -- no report line at all -- so those 8 memos were
permanently invisible: never re-drafted, never reported, never chased.
- georgeHasDraft(to) checks whether a chase draft still exists before trusting the
ledger. On ANY error it returns true (assume present), so a George hiccup can
never cause a burst of duplicate drafts.
- A stale claim is dropped and the vendor re-drafted.
- Two previously-silent outcomes now appear in the report and console:
alreadyDrafted (verified present, correctly not duplicated) and staleLedger.
Verified live: 3 re-drafted (OSB/THIB/YOR, 8 SKUs), 9 correctly left alone as
verified-still-present. Nothing sent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
55c8110 · 2026-09-10 · TK-11409: anti-dup filter moves to the send gate only (DTD 5/5 verdict C)
DRAFT now always includes recently-emailed vendors, flagged for review; SEND keeps
the rolling window strictly enforced. A draft sends nothing and leaves no mailbox,
so suppressing it prevented no harm and only hid work -- which is how 33 overdue
memos across 10 vendors stayed invisible until Steve asked. The --include-suppressed
opt-in is why: a safety step that must be REMEMBERED is not a control, and nobody
ever typed it. Flag retained as a no-op so launchd/callers keep working.
Also killed the stale '8/15' label. There was never an 8/15 cutoff -- the rule is a
rolling RECENT_DAYS (14) window recomputed every run, and the hardcoded reason string
misled everyone, including me, into reading it as a one-time event.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1c8e587 · 2026-09-10 · TK-11409: pass 2 requires send-history evidence, and can no longer fail silently
DTD verdict B, amended by the reviewer's dissent.
Root cause of the inversion, now explained: the 2nd-request field only started
being written on 2026-08-25 (32145b7). The 8 rows pass 2 was matching were stamped
08/14-08/15 -- BEFORE that field was ever populated -- so their empty 2nd-request
field meant 'this column did not exist yet', not 'awaiting a 2nd request'.
fmpro-posted.json corroborates (GREEN 7 + VER 1 = the exact 8). DB field state is
not contact history.
- sendEvidence(slug) reads sent.json as the evidence oracle and returns
{count, addrs, known}. (no record at all) is deliberately distinct
from count<2, because conflating them is how a no-op hides.
- planVendor pass 2 refuses without >=2 recorded sends and sets an explicit _skip
REASON. It never returns a bare empty list.
- plan --pass 2 prints a REFUSED block naming every vendor and why.
- stampVendor returns , so a caller can tell 'refused for lack of evidence'
from 'nothing left to do'.
- stampFmpro no longer uses stdio:'ignore' -- the root enabler of BOTH bugs in this
ticket -- and appends to data/fmpro-stamp.log. Still detached and non-blocking, so
a FileMaker hiccup still cannot fail a send.
Verified read-only against LIVE FileMaker:
pass 2 before: 8 records, all false positives
pass 2 after: 0 records, 29 vendors refused with a named reason
pass 1: 1 record, unchanged (no regression)
the 5 slugs with a genuine 2nd send (dgd/osb/thi/thib/christian-lacroix-europe)
pass the gate -- so it does not over-refuse.
No records written.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
173735d · 2026-09-10 · TK-11409: plan --pass 2 previews the resend match set read-only
Lets the 2nd-request query be proven against live FileMaker without writing.
Verified: pass 1 matches 1 record, pass 2 matches 8 — the 8 that a resend
previously skipped silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f69c9f7 · 2026-09-10 · TK-11409: the 2nd request now updates the WALLPAPER file
The resend called the stamper but wrote nothing, silently. matchPageSku hard-coded
[FIELD_SENT]: '=' (FileMaker '=' matches an EMPTY field) on every send, so on a resend
every SKU was already stamped from request #1, the find returned zero records, and
nothing was written. stampFmpro spawns detached with stdio:'ignore', so the no-op was
indistinguishable from success.
- matchPageSku/planVendor/stampVendor take a (default 1, so existing callers
are unchanged). pass 2 finds rows where FIELD_SENT is non-empty ('*') and the
2nd-request field is still empty.
- pass 1 now writes ONLY 'Date Email Sent to Vendor after 10 Days'. It used to write
the 2nd-request field at the same time, which left request #2 nowhere to record.
- pass 2 writes ONLY 'Date Sample Request Letter Sent', preserving the 1st-chase date.
- server.js derives isResend from sent.json (the record of fact) rather than b.force,
so a forced genuine first send stays pass 1.
Not yet exercised against live FileMaker — no records written.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8b3d62e · 2026-09-04 · TK-10742: George-mined address spot-check (DTD-B safeguard) — 10/11 flags are legit rep-desk routing, 1 glance (maya)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGW4NWtm13q7fv8u725FyH
57d645d · 2026-09-01 · chore: surface FM stamp failures (no more silent catch), v1.1.0 (session close)
Lint (no linter configured) + refactor (no safe change — code deliberate) passes.
Applied logging-only hardening on the two silent FM-stamp catch sites the review
flagged (scheduled-run SEND path + watch-sent-stamp poller) — zero control-flow
change, just turns a swallowed stamp failure into a visible warning. Minor bump:
session added weekday+auto-widen window, ref/tracking + pattern-name columns,
WIN/CATCHUP/SEARCH_DAYS/MAX_AGE knobs, --include-suppressed, and the poller rewrite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
35ba693 · 2026-09-01 · sample-followup: env knobs (SEARCH_DAYS/MAX_AGE) to backfill 2nd-request stamps
Lets the poller sweep further back than the default 3-day/60-day window to backfill
follow-ups sent during the period the stamper was broken. Month sweep found only 1
unstamped-still-outstanding item (Morris 216020); WQ FC60808 + O&P OP-ART-DECO-WAVES
were already received. Backfilled 216020 with its real chase date (08/27).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5ab8a3d · 2026-09-01 · sample-followup: FIX 2nd-request stamping — was silently stamping nothing
The Sent-poller matched the subject account # against FileMaker's account field,
but that field is a DW-internal sequence # (1062xxx), not the vendor account — so
6/25 processed sends stamped ZERO records while the ledger filled up (looked healthy,
did nothing). Rewrote matching: recipient email -> candidate vids (all slug variants,
not last-wins) -> stamp ONLY records whose Manufacturer # actually appears in the SENT
letter body. Body-presence is precise + immune to shared-desk aliasing (Christian
Lacroix via Osborne desk, Anna French via Thibaut desk stamp only if truly in the
letter). Also switched the search phrase to the no-hyphen 'Outstanding Memos' (less
flaky than 'Sample Follow-Up'). Stamps Date Sample Request Letter Sent +
Date Email Sent to Vendor after 10 Days (no dedicated 2nd-Request field exists via API).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9a219d8 · 2026-09-01 · sample-followup: drop the [REVIEW] subject prefix — flag lives in the run report only
Steve 9/01: the '[REVIEW — team emailed recently]' tag must never ride in the draft
subject (risk of reaching the vendor). Removed it from the subject; the recently-emailed
flag now shows ONLY in the console run report. Deleted + regenerated the 8 scoped drafts
with clean subjects.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>