. The inline basic-auth credential\n(\"admin:DWSecure2024!\") was already in this file at lines 60 + 66 before\nthis commit and is unchanged by it. Credential rotation across the fleet\nis tracked separately (cred is also in dozens of sibling repos per MEMORY).\n\nSurfaced by tick 169 session #102 Teal mechanical refactor pass; that\npass deferred the fix because the diff touches the cred line, but the\nLIVE-BROKEN production UI takes precedence — fixing now, rotation later.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) "},{"h":"941feb1","d":"2026-05-19","s":"Snapshot-files cleanup: untrack server.js.bak-*, broaden .gitignore, add 404 guard","b":"- Untrack the 7-month-old server.js.bak-20260421-094351 snapshot that was\n tracked in the repo (still present on disk, now ignored).\n- Broaden .gitignore to cover *.bak.*, *.bak-*, *.pre-*, *.orig, *.rej,\n *.swp, *~ so future snapshots don't slip back in.\n- Add a static-snapshot 404 guard middleware that runs before\n express.static('public'); any URL matching .bak, .bak-*, .pre-*, .orig,\n .rej, .swp, or trailing ~ returns 404 regardless of disk state.\n Defense-in-depth on top of the existing basic-auth gate.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) "},{"h":"c2bdfca","d":"2026-05-19","s":"add trash-fleet-mailtests.js — one-off cleanup of 89 fleet delivery-test emails (modeled on trash-redfin-zillow.js)","b":""},{"h":"ab70d2e","d":"2026-05-19","s":"George: make the 'From job:' provenance banner opt-out (no_source_tag flag / x-george-no-tag header) so customer-facing sends can be banner-free","b":"Co-Authored-By: Claude Opus 4.7 (1M context) "},{"h":"fd1481e","d":"2026-05-19","s":"Fleet auto-responder: stop logging non-fleet mail to UI run-log (run-log tracks fleet inquiries only)","b":""},{"h":"8f310af","d":"2026-05-19","s":"Fleet auto-responder: update usage docstring (daemon/force flags, HTML reply, UI)","b":""},{"h":"2f343b5","d":"2026-05-19","s":"Fleet auto-responder: fix scan query (bare deliveredto:steve+ never matches in Gmail) + probe-subject safety guard (RETEST/FLEETMAILTEST/DTEST/PMRCV)","b":""},{"h":"e9fdf0f","d":"2026-05-19","s":"Fleet auto-responder: --daemon self-scheduling mode (pm2-managed, 3-min interval); UI on port 9768","b":""},{"h":"9c7a470","d":"2026-05-19","s":"fleet-autoresponder UI: move port 9766→9768 (9766 reserved for agent-architect mindmap viewer)","b":""},{"h":"d6e30ce","d":"2026-05-19","s":"Fleet auto-responder: pm2 ecosystem config — 3-min cron schedule + UI process","b":""},{"h":"fb57ee9","d":"2026-05-19","s":"Fleet auto-responder admin UI dashboard (Express viewer, port 9766, pm2-managed)","b":""},{"h":"13d4e3b","d":"2026-05-19","s":"Fleet auto-responder: multipart/alternative HTML+plain reply, structured run-log + state","b":""},{"h":"7d77cad","d":"2026-05-19","s":"Fleet auto-responder: deliveredto query fix + --only-ids scoped-test flag","b":"Tested live on barwallpaper.com + grassclothwallcovering.com (2-domain test):\n- Gmail does not match plus-tags via to:; switched candidate query to\n deliveredto: and rely on Delivered-To header parsing for domain recovery.\n- Added --only-ids flag so a scoped test does not touch the inbox backlog\n of unrelated probe mail.\n- Verified: reply sent from info@designerwallcoverings.com landed in the\n recipient INBOX (not Spam), correct per-domain sign-off, no loop, label\n + 7-day ledger dedup both idempotent on re-run.\n\nLedger (data/fleet-autoresponder-ledger.json) gitignored — runtime state.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) "},{"h":"d135118","d":"2026-05-19","s":"Add fleet auto-responder for the 44 dw-domain-fleet domains","b":"Sends one warm acknowledgment from info@designerwallcoverings.com to genuine\nnew customer inquiries arriving at info@ (forwarded into the\nsteve-office Gmail via Purelymail catch-all + plus-tag). Reply-To is set to\ninfo@ so customer replies route back to the right site.\n\nSpam-safety: skips Spam, no-reply/bounce senders, bulk/auto/list mail, and any\nmail from designerwallcoverings.com or a fleet domain (loop guard). One reply\nper sender per rolling 7-day window (persisted ledger). Marks every processed\nmessage with the 'fleet-autoreplied' Gmail label — idempotent on re-run.\n\nReuses George's DW-MCP/.env OAuth credentials; no prod deploy needed.\nNot yet scheduled — run manually with: node fleet-autoresponder.js\n\nCo-Authored-By: Claude Opus 4.7 (1M context) "},{"h":"02ef28b","d":"2026-05-18","s":"Make /api/profile honor ?account= param like the other 31 routes","b":""},{"h":"2cbc311","d":"2026-05-18","s":"remove bogus stevesclaude account from George — theagentabrams@gmail.com (agentabrams) is the real AI account; also exempt /auth/agentabrams from basic auth","b":""},{"h":"23979b0","d":"2026-05-18","s":"Add agentabrams (theagentabrams@gmail.com) as 5th george account slot","b":""},{"h":"ec801aa","d":"2026-05-18","s":"george /auth honors ?account= — dispatches to per-account re-auth routes","b":""},{"h":"a4d0448","d":"2026-05-12","s":"fix(george): ENV_PATH portable across Mac2/Kamatera","b":"ENV_PATH was hardcoded to /root/Projects/Designer-Wallcoverings/DW-MCP/.env\nwhich only exists on Kamatera. On Mac2 the same .env lives at\n~/Projects/Designer-Wallcoverings/DW-MCP/.env. The readFileSync would\nquietly fail in the try/catch upstream, INFO_REFRESH_TOKEN stayed empty,\nand the boot log said 'Info@ not configured' even though the token IS\nin the Mac2 .env.\n\nFix: try $DW_MCP_ENV then $HOME/Projects/.../DW-MCP/.env then the\nlegacy /root/ path. First readable wins.\n\nSymptom that exposed this: GET /api/search?account=info returned\n'unknown account: info' because infoGmail was never built. After\nrestart with the fix, log says 'Info@ account initialized (full\nWorkspace)' and /api/search returns the real DW inbox (201 messages\nfor a Shopify-orders query, including 'New Order on the New DW')."},{"h":"facbb3b","d":"2026-05-06","s":"initial scaffold (gitify-all 2026-05-06)","b":""}];
const q = document.getElementById('q');
const list = document.getElementById('bp-commits');
const tally = document.getElementById('bp-tally');
function applyFilter() {
const term = q.value.toLowerCase().trim();
let shown = 0;
for (const li of list.children) {
const match = !term || li.dataset.blob.includes(term);
li.style.display = match ? '' : 'none';
if (match) shown++;
}
tally.textContent = term ? `${shown} / ${DATA.length} match "${q.value}"` : `${DATA.length} commits indexed`;
// Reflect search state in URL so a filtered view is shareable. Use
// replaceState so the back button still escapes the project page.
const url = new URL(window.location.href);
if (term) url.searchParams.set('q', q.value); else url.searchParams.delete('q');
history.replaceState(null, '', url);
}
q.addEventListener('input', applyFilter);
// Apply the initial ?q= from the server-rendered value field.
if (q.value) applyFilter();
build-pages
George Gmail
repo: ~/Projects/george-gmail · 201 commits · 1 in last 24h, 9 in last 7d · █▃ ▅▃▃▃
Search the build
201 commits indexed
2c8cc422026-09-26auto-data-snapshot: 2026-09-26T04:44:27 (2 data files) — data/archive-before-delete-2026-09-26.jsonl data/drain-old-drafts-latest.json
82835be2026-09-25auto-data-snapshot: 2026-09-25T04:38:13 (1 data files) — data/drain-old-drafts-latest.json
628273f2026-09-24auto-data-snapshot: 2026-09-24T04:27:32 (1 data files) — data/drain-old-drafts-latest.json
c06e5112026-09-23TK-11552: keep-list 6 content-bearing info@ drafts (Aug 26-28) + draft-id pins ahead of the 30-day drain
462a1842026-09-23auto-data-snapshot: 2026-09-23T04:15:24 (1 data files) — data/drain-old-drafts-latest.json
c1ded9d2026-09-21auto-data-snapshot: 2026-09-21T04:28:28 (1 data files) — data/drain-old-drafts-latest.json
fdc95152026-09-20TK-11952: add /api/live-check (real Google round-trip) so george arm-probe verifies deliverability, not object-existence
fbea1552026-09-20auto-data-snapshot: 2026-09-20T08:21:12 (1 data files) — data/bridge-alert-state.json
ab13f282026-09-20auto-data-snapshot: 2026-09-20T04:26:57 (1 data files) — data/drain-old-drafts-latest.json
a70f1df2026-09-19auto-data-snapshot: 2026-09-19T04:40:20 (1 data files) — data/drain-old-drafts-latest.json
67846822026-09-18auto-data-snapshot: 2026-09-18T04:37:40 (1 data files) — data/drain-old-drafts-latest.json
32ab62b2026-09-18auto-data-snapshot: 2026-09-18T00:54:05 (1 data files) — data/drain-keep-list.json
b43e27e2026-09-17auto-data-snapshot: 2026-09-17T04:30:36 (1 data files) — data/drain-old-drafts-latest.json
b7d18872026-09-16auto-data-snapshot: 2026-09-16T04:39:34 (1 data files) — data/drain-old-drafts-latest.json
a98b6412026-09-15auto-data-snapshot: 2026-09-15T04:27:49 (1 data files) — data/drain-old-drafts-latest.json
ec90fcf2026-09-14auto-data-snapshot: 2026-09-14T04:37:30 (1 data files) — data/drain-old-drafts-latest.json
65cc5792026-09-14auto-data-snapshot: 2026-09-14T00:22:42 (1 data files) — data/drain-keep-list.json
ca8ce702026-09-13auto-data-snapshot: 2026-09-13T23:50:25 (1 data files) — data/drain-keep-list.json
789f1c62026-09-13TK-11552: derive the protected set from the KEEP-LIST, not from isExempt() + gate contradictions
b8f78c82026-09-13TK-11552: drain asserts protected drafts SURVIVED, not just that it meant to skip them
b4c47622026-09-13TK-11552: keep-list exempts on EITHER id — Gmail rotates message.id on draft edit
f5f274f2026-09-13auto-data-snapshot: 2026-09-13T04:15:01 (1 data files) — data/drain-old-drafts-latest.json
cdf6c502026-09-13fix: add archive-before-delete path to drain script — saves subject/snippet locally before permanent-delete (TK-11609)
920a9462026-09-13fix: add 'calendar' to resolveAccount() map + add agentabrams+calendar to /health (TK-11605, TK-11606)
4411b8c2026-09-13drain: FAIL CLOSED when the keep-list cannot be loaded (it failed OPEN)
1fb64642026-09-13george drain keep-list: exempt the 9 content-bearing TK-11552 info@ reply drafts from the 30-day permanent purge
1cb1b7f2026-09-12george token-bridge: fix calendar re-consent link (TK-11553)
c4c9ba42026-07-15bridge: every-6-day 1-click renewal email to steve-office for the 2 consumer accounts (CADENCE mode) + dead-token safety net stays on 8/2/8
a2eaa2d2026-07-15bridge: nudge to renew at day 5 (2-day runway before the 7-day expiry)
23d49a62026-06-24send-with-attachment: resolve any account (steve-personal/agentabrams), not just office/info
2626a502026-06-24Add DELETE /api/drafts/:id route + make GET /api/drafts account-aware (was always steve-office)
a42bc742026-06-17George external-send guard: block outbound sends to non-Steve recipients unless human-approval token present (X-Send-Approval / send_approval_token); fail-closed. Born from autonomous-worker auto-send incident 2026-06-17. NOTE: must also deploy to the Kamatera george (the instance MCP actually uses).
918647c2026-06-17Add launchd KeepAlive supervisor for george-gmail :9850 (durable fix for the unsupervised-death outage)
8c22e562026-06-12secret-strip george-gmail: env-first the hardcoded George basic-auth cred (server) + remove browser-embedded copy (client)
cf272722026-06-12Add scoped one-shot trash for benign cron-drift alert flood in info@
8599fb22026-05-31Link favicon.svg in head (use branded SVG, not .ico fallback)
c2bdfca2026-05-19add trash-fleet-mailtests.js — one-off cleanup of 89 fleet delivery-test emails (modeled on trash-redfin-zillow.js)
ab70d2e2026-05-19George: make the 'From job:' provenance banner opt-out (no_source_tag flag / x-george-no-tag header) so customer-facing sends can be banner-free
fd1481e2026-05-19Fleet auto-responder: stop logging non-fleet mail to UI run-log (run-log tracks fleet inquiries only)
8f310af2026-05-19Fleet auto-responder: update usage docstring (daemon/force flags, HTML reply, UI)
2f343b52026-05-19Fleet auto-responder: fix scan query (bare deliveredto:steve+ never matches in Gmail) + probe-subject safety guard (RETEST/FLEETMAILTEST/DTEST/PMRCV)
e9fdf0f2026-05-19Fleet auto-responder: --daemon self-scheduling mode (pm2-managed, 3-min interval); UI on port 9768
9c7a4702026-05-19fleet-autoresponder UI: move port 9766→9768 (9766 reserved for agent-architect mindmap viewer)
fb57ee92026-05-19Fleet auto-responder admin UI dashboard (Express viewer, port 9766, pm2-managed)
13d4e3b2026-05-19Fleet auto-responder: multipart/alternative HTML+plain reply, structured run-log + state
7d77cad2026-05-19Fleet auto-responder: deliveredto query fix + --only-ids scoped-test flag
d1351182026-05-19Add fleet auto-responder for the 44 dw-domain-fleet domains
02ef28b2026-05-18Make /api/profile honor ?account= param like the other 31 routes
2cbc3112026-05-18remove bogus stevesclaude account from George — theagentabrams@gmail.com (agentabrams) is the real AI account; also exempt /auth/agentabrams from basic auth
23979b02026-05-18Add agentabrams (theagentabrams@gmail.com) as 5th george account slot
ec801aa2026-05-18george /auth honors ?account= — dispatches to per-account re-auth routes
a4d04482026-05-12fix(george): ENV_PATH portable across Mac2/Kamatera
Commits with substantial prose (≥120 chars) — the rationale behind each move.
c06e511 · 2026-09-23 · TK-11552: keep-list 6 content-bearing info@ drafts (Aug 26-28) + draft-id pins ahead of the 30-day drain
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E1xReXdF9rRznsRv8hyTve
fdc9515 · 2026-09-20 · TK-11952: add /api/live-check (real Google round-trip) so george arm-probe verifies deliverability, not object-existence
/health reports ready:!!gmail (OAuth object exists in memory), which stays truthy
through a DNS-to-Google outage (getaddrinfo ENOTFOUND oauth2.googleapis.com — the
09-13 failure). New /api/live-check does the same gmail.users.getProfile round-trip
george runs on OAuth exchange and returns 200 {live:true} ONLY when Google is
reachable (503 {live:false} otherwise). Exempted from both Basic-auth middlewares
like /health. Canaries repoint to this; fail-closed until george restart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActKpZmgeAcGMo4esfSb3K
789f1c6 · 2026-09-13 · TK-11552: derive the protected set from the KEEP-LIST, not from isExempt() + gate contradictions
Second-model review (Grok) caught that b8f78c8 reproduced the very bug it was written
to fix. The census built protectedBefore with isExempt(d) -- the same predicate that
builds `matches`. A PARTIAL false-negative in isExempt (8 of 11 keep-listed drafts
recognized) drops the other 3 out of BOTH the census and the exemption: deleted,
never missed, reported PASS. Two paths cross-check each other only if they can
DISAGREE, which means they must be different code even when they compute the same set.
- protectedBefore is now resolved by iterating KEEP_IDS against draft-id / message-id
maps built from the listing. isExempt() is never consulted there.
- CONTRADICTION GATE: if the keep-list says protect and isExempt selected the draft
for deletion, the run ABORTS before any delete. That turns a partial false-negative
from "permanent loss detected afterwards" into "loss prevented".
- An O(1) re-check immediately before each DELETE closes the in-batch window (a batch
of up to 500 deletes was previously only re-checked on the next batch).
- A full 500-draft listing while a keep-list is in force now ABORTS: the mailbox
cannot be fully seen, so neither the census nor the survival check is trustworthy,
and deleting permanently while blind to what is protected is the one thing this
must not do. Previously that was only a WARN.
- A failed post-run re-read is FAIL when the run actually deleted something -- "I
could not check" must not be quieter than "I checked and it was fine". WARN when
nothing was deleted, since there was nothing to lose.
- An empty/unparseable listing is reported as a READ FAILURE, not as "everything was
lost": same safe action, but the reason must not be fabricated.
Tests 20 -> 25 assertions, 5 -> 6 cases. CASE 6 is the headline: it breaks isExempt on
purpose and asserts the run aborts with deleted=0. b8f78c8 passed every other case in
this file while still deleting the protected draft.
Live read-only check of the new logic against info@: 27 keep-list ids -> 12 protected
drafts + 3 unresolved (matches the canary); >22d population 14 -> would delete 3 (the
3 known-abandoned empties), 0 contradictions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBRBeHTnkTTdi3DYLePNSY
b8f78c8 · 2026-09-13 · TK-11552: drain asserts protected drafts SURVIVED, not just that it meant to skip them
The exemption filter and the success verdict were the same code path: matches was
built with !isExempt(d) and the verdict was (totalFail>0 ? WARN : PASS). If isExempt
were ever wrong, the run would permanently delete (no Trash) every keep-listed draft
AND report PASS. A filter cannot audit itself.
Adds an outcome-side check observed through a different read than the one that made
the decision:
- census the keep-listed drafts that are LIVE before the drain loop
- re-verify their presence before each batch's deletes; a loss ABORTS the pass
(aborting can only prevent deletions, never cause one)
- re-read once more at the end; any loss is FAIL with the ids named
NOT-MEASURED is never PASS (TK-11431 rule 1): a truncated 500-draft listing, a failed
post-run re-read, or a keep-list whose ids resolve to no live draft each degrade to
WARN with protected_verified null, rather than a clean PASS that implies protection
was proven. New heartbeat fields: protected_before, protected_lost_count,
protected_lost_ids, protected_verified, census_truncated, protection_note.
Ships test/test-protected-survival.sh + test/mock-george.js (TK-11431 rule 3): 17
assertions over 5 cases against an isolated copy and a loopback mock, proving the
check goes RED on an injected fault on BOTH abort paths (mid-drain and post-loop) and
on all three not-measured paths. Harness guards rc=127 and a missing heartbeat so a
case that never ran cannot read as a pass.
Also fixes the archive filename: stamp.slice(0,8) on an ISO stamp yielded
"archive-before-delete-2026-09-.jsonl" (month, truncated). Now slice(0,10) = the date.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EBRBeHTnkTTdi3DYLePNSY
b4c4762 · 2026-09-13 · TK-11552: keep-list exempts on EITHER id — Gmail rotates message.id on draft edit
The drain matched exemptions on d.message.id but deletes by the draft id. Gmail mints a
NEW message id every time a draft is saved, while the draft id is stable — proven live:
draft r-5517420768746463086 kept its id while its message id went 1a09ba5cc706157b ->
1a09ba5ce13ee153 across one edit. So editing a protected draft silently dropped its
protection and the 04:15 permanent-delete (no Trash) could destroy it. That fired in
exactly the workflow the TK-11552 memo asks Steve to perform: open info@ and
send / rewrite-and-send / drop.
- isExempt(d): match message.id OR draft id. Strictly preservative — can only ADD
exemptions, never remove one.
- Pinned the 12 live keep-list entries by their stable draft id as well (15 -> 27).
- DRAIN_KEEP_LIST test seam (mirrors the canary's). Verified the launchd plist does NOT
set it; its env block carries only CONFIRM/MAX_AUTO_DELETE/PATH.
- test-draftid-exempt.sh 3/3 against the real script + real drafts, dry-run only. Case B
(pin removed -> deletable=1) is the negative control that proves the test can go red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdKLxdaTGYBRTVGAYwi8Vo
4411b8c · 2026-09-13 · drain: FAIL CLOSED when the keep-list cannot be loaded (it failed OPEN)
The keep-list loader was:
try { JSON.parse(readFileSync(KEEP_PATH)) } catch (_) { return {}; }
That fails OPEN. A corrupted, truncated, wrong-shaped, or deleted keep-list
silently became 'zero exemptions', and the very next 04:15 run would
PERMANENTLY delete (Gmail drafts.delete, no Trash) every draft the keep-list
existed to protect. Every protection this file provides — TK-11231's held vendor
asks and TK-11552's 9 live customer replies — rested on a file whose read
failure was swallowed without a word.
A no-Trash deleter must never infer 'nothing is protected' from 'I could not
read what is protected'. Now any failure to load a usable keep-list ABORTS the
deletion pass (exit 1) and writes a FAIL heartbeat so it surfaces on the fleet
panel instead of dying quietly. ENOENT aborts too: 'nothing is exempt' must be
STATED by writing '{}' — a deliberate one-second act — because an absent file is
indistinguishable from one that went missing. Dry runs only warn, so the script
stays usable for diagnosis when the keep-list is broken.
Strictly preservative: this change can only ever prevent deletions, never cause
one.
Ships test-keeplist-failclosed.sh — 5 cases (missing / corrupt / wrong-shape must
ABORT; explicit {} and dry-run must NOT). SAFETY: it runs an isolated copy of the
script in a throwaway dir pointed at a dead GEORGE_BASE port, so even a FAILED
abort has no reachable mailbox to delete from. The harness also guards itself
against rc=127 — the first draft of this test silently 'passed' a case because
the command never ran.
Found by an adversarial second-model review (Kimi k3) of the TK-11552 keep-list
work, which correctly pointed out I had fail-safed the canary but not the
delete site.
TK-11552
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ux1A8z45sLwUTNzz8kxn68
1fb6464 · 2026-09-13 · george drain keep-list: exempt the 9 content-bearing TK-11552 info@ reply drafts from the 30-day permanent purge
The 30-day info@ drain (com.steve.george-drain-old-drafts, daily 04:15) permanently
deletes drafts with NO Trash. 13 aged info@ drafts were on that fuse; 2 were already
keep-listed (TK-11231), 11 were not. Verified content: 9 carry live customer/vendor
data (a shipping PRO# the customer must call in, per-yd/per-roll net quotes, stock +
backorder terms, vinyl 60cm production specs, 8-panel mural set dims/pricing), 2 are
genuinely abandoned (one is pure signature, one is the word 'Looks').
Keep-listing the 9 removes the permanent-loss DEADLINE without sending anything, so
Steve decides on his own timing instead of racing a 04:15 cron. The first of these
would have been destroyed at the 2026-09-14 04:15 run.
Verified with a two-sided negative test against the drain's real predicate
(older.has(id) && !KEEP_IDS.has(id)): simulating all 13 as >30d, exactly the 2
abandoned drafts remain deletable and all 11 content-bearing are exempt.
Reversible: cp /tmp/drain-keep-list.PRE-TK11552.json data/drain-keep-list.json
(or git revert). No send, no externality, no Gmail write.
TK-11552
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ux1A8z45sLwUTNzz8kxn68
1cb1b7f · 2026-09-12 · george token-bridge: fix calendar re-consent link (TK-11553)
AUTH_ROUTE mapped GOOGLE_CALENDAR_REFRESH_TOKEN -> 'steve-office', but server.js
writes GOOGLE_CALENDAR_REFRESH_TOKEN only under the dedicated calendar flow
(state:'calendar', /auth/calendar). The steve-office /auth flow writes
GMAIL_REFRESH_TOKEN, so every automated re-consent nudge for a dead calendar
token pointed Steve at a flow that re-minted the healthy Gmail token and never
revived the calendar token. Advisory link text only; no runtime token behavior
changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VggjiaLGkK4kjR1zCnVJ65
Shellcheck-clean addition (matches existing SC2015 style used elsewhere
in the file); no refactor needed for an 8-line, already-verified change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASu1mqEndsJjj5bpueXKiE
ce59ce6 · 2026-09-12 · Chain bridge.mjs onto token-age-warn.sh so the fleet-health artifact stays fresh
cron-fire-canary was flagging com.steve.george-token-age as not-firing, but the
launchd job itself was firing fine every 6h — it's the separate bridge.mjs
republish script (~/.claude/skills/george-token-age/bridge.mjs, TK-11371) that
writes the monitored artifact, and it had no schedule of its own (its plist
was deliberately left uninstalled). Riding the detector's already-approved
cadence keeps data/latest.json current without installing a new scheduled job.
TK-11529
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ASu1mqEndsJjj5bpueXKiE
e7ccd8a · 2026-09-10 · TK-11365: never send the internal "From job:" banner to an outside party
The banner is an internal provenance aid and was OPT-OUT (inferSource), so any
caller that forgot no_source_tag prepended an internal job label to the top of
vendor and customer mail. Proven in prod on a real customer - four emails opening
"From job: kelly-reply-tk10830" - and found today on 14 of 15 queued vendor
drafts in info@ (WallQuest, York, Thibaut, Kravet, Brewster, MDC, Astek, OAL,
Sancar, Innovations, Egg-and-Dart, Quadrille).
The opt-out was also unreachable from the agent path: the george MCP tool
gmail_create_draft does not expose no_source_tag at all, so an agent drafting
vendor mail through the MCP could not suppress the banner even knowing about it.
Now: if ANY recipient is external, no banner. Internal mail keeps it, so the
provenance aid survives where it is actually useful. Fails open on error.
Logic unit-tested 5/5 offline: external, mixed internal+external, internal-only,
the internal gmail allowlist, and the explicit opt-out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0d2c8e2 · 2026-09-10 · george: give the 30-day draft drainer a KEEP-LIST — TK-11231
The drainer was a blanket age filter with no exemption mechanism, so a READY,
wanted draft sat on exactly the same permanent-delete fuse as the garbage. That
is why a live vendor cost-list ask (Newmor / LBI Boyd, 1,294 rows priced at 0)
was days from being destroyed with no Trash, purely because it happened to live
on info@ instead of steve-office.
Being able to protect a wanted draft WITHOUT sending it is the missing
capability: 'send it' and 'lose it' should never have been the only two options.
data/drain-keep-list.json maps messageId -> why it is kept; those ids are never
deleted at any age. Seeded with the Newmor ask; the entry documents that it is
held deliberately and says to remove it once sent or dropped.
Verified against the real patched predicate (extracted from source, not
retyped): a keep-listed >30d draft is excluded, an unprotected >30d draft is
still deleted (drainer not weakened), and fresh drafts stay untouched. Live dry
run confirms the list loads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
efdf4ea · 2026-09-10 · george: draft contract + duplicate detection at the SHARED layer — TK-11231
Red-team finding: the duplicate refusal shipped in george-mcp protects exactly
ONE client. Anything POSTing /api/drafts directly — cron scripts, other repos,
Kamatera, the ~20 caller scripts under DW-Agents — walked straight past it. A
single locked door on a building with no walls.
Now, for EVERY caller:
- POST /api/drafts returns status=awaiting_owner_send, delivered=false,
recipient_has_not_received_this=true + the do-not-close-the-ticket warning.
Free, non-breaking, and it reaches callers the MCP never touched.
- audit() records acknowledged_existing and duplicates_to_recipient, so it is
now possible to LEARN that the escape hatch went reflexive. Previously there
was no telemetry pointed at that trapdoor.
- Duplicate detection behind GEORGE_DRAFT_DEDUPE: off (default) | warn | enforce.
Default is off DELIBERATELY: the check costs one extra Gmail search per draft,
and the bulk callers here (sample-followup drafts many vendors in a burst) are
exactly the traffic that already exhausted the per-minute quota once. Turning
it on is a one-env-var decision for Steve, not a surprise I impose on a shared
bridge. 'warn' reports and never blocks; 'enforce' 409s and fails CLOSED on an
unverifiable check.
Verified on an ISOLATED instance (free port, live :9850 untouched): enforce
returns 409 naming the existing draft id; default-off leaves callers unchanged
while still returning the new contract. Probe draft deleted; counts back to
the 136/41 baseline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTxE4MgnygQ9EY2rPZvtcK
The create-only draft surface is what let agents compose DUPLICATE vendor asks
(Command x2, Maya Romanoff x2, Newmor x2): an agent that found its own stale
draft had no verb to revise it, so it composed a second one beside it.
drafts.update replaces in place, so revising is idempotent and leaves no
second artifact. Echoes the overwritten to/subject/date back as `previous`
so a bad edit is recoverable. Returns status=awaiting_owner_send /
delivered=false — a draft is never a delivered outcome.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTxE4MgnygQ9EY2rPZvtcK
60d5c89 · 2026-08-23 · Add labelIds support to POST /api/drafts and bulk-label endpoint
- POST /api/drafts now accepts optional `labelIds` array — after draft creation,
applies labels via users.messages.modify (the reliable path since drafts.create
raw-message doesn't surface custom labelIds directly). Non-fatal if label apply fails.
- New POST /api/messages/bulk-label endpoint — searches messages by query `q` and
applies/removes label IDs via batchModify in 1000-item chunks; paginated up to
5000 messages; supports dryRun mode.
- New bulk-label-sample-followup-drafts.js script — uses the new endpoint to label
all existing info@ drafts matching subject:"Sample Follow-Up" with the
"sample-followup" label (TK-10744 info@ drafts cleanup).
Deploy to Kamatera (pm2 restart george) is gated — Steve-go required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0fd5c29 · 2026-08-20 · Schedule daily info@ old-draft drain (launchd) with runaway cap + heartbeat
- Daily 04:15 launchd job com.steve.george-drain-old-drafts runs the drain
in CONFIRM=1 to keep info@ drafts >30d trimmed automatically.
- Runaway guard: MAX_AUTO_DELETE=800 — if a run ever finds a huge backlog it
refuses to auto-delete and emits WARN for human review (never silent mass-nuke).
- Heartbeat data/drain-old-drafts-latest.json in PASS/WARN/FAIL vocabulary for
the fleet-health rollup + meta-watchdog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7106f29 · 2026-08-20 · Add drain script: permanently delete info@ drafts older than 30 days
Batched drain (drafts.list caps at 500, no pagination): discovers the
authoritative in:drafts older_than:30d message-id set once, then loops
list->delete-matches until none remain. Quota backoff, dry-run default,
per-deletion jsonl log. Steve-authorized run 2026-08-20: 3665 deleted, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7b49e78 · 2026-07-15 · feat(bridge): painless George token renewal — WARN/CRIT accounts get a one-click re-consent email (DTD verdict B, 2026-07-15)
token-age-warn.mjs computed WARN/CRIT but never alerted. token-bridge.mjs reads its latest.json and emails Steve a "Renew now" button (George /auth/<account>) for each aging/dead token, throttled 1/20h per account, +CNCP card. 3x/day launchd plist runs monitor then bridge. Verified: emailed 4 accounts (steve-personal dead, calendar dead, steve@/info@ aging).
Adds a service-account/JWT impersonation path alongside the existing OAuth refresh-token
path so the Workspace pair (steve@ + info@) can drop off the ~7-day Testing-mode token
treadmill. Fully DORMANT by default: with no GOOGLE_SA_KEYFILE set, saAuthFor() returns
null and the SA-override block is a no-op — every account keeps its refresh-token client
byte-for-byte. Arm via GOOGLE_SA_KEYFILE + GEORGE_SA_ACCOUNTS=steve-office,info (Workspace
only; consumer gmail keys can never be SA-backed). resolveAccount() reads the module-level
service-client vars per request, so it transparently picks up the SA clients when armed.
Verified by boot tests: dormant -> 0 SA lines + all accounts on refresh tokens; armed w/
missing or bogus keyfile -> clear warn + safe fallback + still boots. Live JWT impersonation
awaits Steve's gated console work (SA creation + domain-wide delegation authorization).
Arming checklist + rollback in PLAN-OAUTH-DURABILITY.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Council #3 — end the weekly-token treadmill. Read-only token-age-warn.mjs probes each
refresh token against Google's token endpoint (idempotent refresh, never writes creds) and
combines validity with a hash-tracked age; WARNs ~1d before the 7d Testing-mode expiry,
CRIT on invalid_grant. Alerts on worsening transition only (CNCP + George email via run.sh).
First run surfaced a live finding: GOOGLE_CALENDAR_REFRESH_TOKEN is DEAD (appointments push
silently broken). Migration paths (service-account/DWD or Internal user-type) in
PLAN-OAUTH-DURABLE-TOKENS.md — those steps are Steve-gated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
918647c · 2026-06-17 · Add launchd KeepAlive supervisor for george-gmail :9850 (durable fix for the unsupervised-death outage)
Tracked plist + install note. Not bootstrapped here — Steve runs the bootstrap
(starts the email service + touches launchd). RunAtLoad+KeepAlive so the :9850
send service self-respawns; daily-overnight-skus + idea-loop-digest were only
failing because this send path died.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
xfetch was sitting OUTSIDE any <script> tag, wedged between the
react.production.min.js and react-dom.production.min.js script tags.
Browsers parsed it as text content → xfetch was undefined → every
api()/apiPost() call (lines 67-74, 88, 96, 105, 113) threw ReferenceError
on page load. George admin inbox UI was unusable in any browser.
Pure positional fix — wrapped the existing one-line function in
<script>...</script>. The inline basic-auth credential
("admin:DWSecure2024!") was already in this file at lines 60 + 66 before
this commit and is unchanged by it. Credential rotation across the fleet
is tracked separately (cred is also in dozens of sibling repos per MEMORY).
Surfaced by tick 169 session #102 Teal mechanical refactor pass; that
pass deferred the fix because the diff touches the cred line, but the
LIVE-BROKEN production UI takes precedence — fixing now, rotation later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Untrack the 7-month-old server.js.bak-20260421-094351 snapshot that was
tracked in the repo (still present on disk, now ignored).
- Broaden .gitignore to cover *.bak.*, *.bak-*, *.pre-*, *.orig, *.rej,
*.swp, *~ so future snapshots don't slip back in.
- Add a static-snapshot 404 guard middleware that runs before
express.static('public'); any URL matching .bak, .bak-*, .pre-*, .orig,
.rej, .swp, or trailing ~ returns 404 regardless of disk state.
Defense-in-depth on top of the existing basic-auth gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tested live on barwallpaper.com + grassclothwallcovering.com (2-domain test):
- Gmail does not match plus-tags via to:; switched candidate query to
deliveredto: and rely on Delivered-To header parsing for domain recovery.
- Added --only-ids flag so a scoped test does not touch the inbox backlog
of unrelated probe mail.
- Verified: reply sent from info@designerwallcoverings.com landed in the
recipient INBOX (not Spam), correct per-domain sign-off, no loop, label
+ 7-day ledger dedup both idempotent on re-run.
Ledger (data/fleet-autoresponder-ledger.json) gitignored — runtime state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
d135118 · 2026-05-19 · Add fleet auto-responder for the 44 dw-domain-fleet domains
Sends one warm acknowledgment from info@designerwallcoverings.com to genuine
new customer inquiries arriving at info@<fleet-domain> (forwarded into the
steve-office Gmail via Purelymail catch-all + plus-tag). Reply-To is set to
info@<fleet-domain> so customer replies route back to the right site.
Spam-safety: skips Spam, no-reply/bounce senders, bulk/auto/list mail, and any
mail from designerwallcoverings.com or a fleet domain (loop guard). One reply
per sender per rolling 7-day window (persisted ledger). Marks every processed
message with the 'fleet-autoreplied' Gmail label — idempotent on re-run.
Reuses George's DW-MCP/.env OAuth credentials; no prod deploy needed.
Not yet scheduled — run manually with: node fleet-autoresponder.js
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
a4d0448 · 2026-05-12 · fix(george): ENV_PATH portable across Mac2/Kamatera
ENV_PATH was hardcoded to /root/Projects/Designer-Wallcoverings/DW-MCP/.env
which only exists on Kamatera. On Mac2 the same .env lives at
~/Projects/Designer-Wallcoverings/DW-MCP/.env. The readFileSync would
quietly fail in the try/catch upstream, INFO_REFRESH_TOKEN stayed empty,
and the boot log said 'Info@ not configured' even though the token IS
in the Mac2 .env.
Fix: try $DW_MCP_ENV then $HOME/Projects/.../DW-MCP/.env then the
legacy /root/ path. First readable wins.
Symptom that exposed this: GET /api/search?account=info returned
'unknown account: info' because infoGmail was never built. After
restart with the fix, log says 'Info@ account initialized (full
Workspace)' and /api/search returns the real DW inbox (201 messages
for a Shopify-orders query, including 'New Order on the New DW').
File tree
43 files tracked. Click any to browse the source at HEAD.