[object Object]

← back to Discontinued Agent

Add disco/not-going-to-make-again triggers + info@ run-completion report

85a39147814ae61d1200e0a2660c77f233325208 · 2026-07-08 08:38:11 -0700 · Steve Abrams

Files touched

Diff

commit 85a39147814ae61d1200e0a2660c77f233325208
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 8 08:38:11 2026 -0700

    Add disco/not-going-to-make-again triggers + info@ run-completion report
---
 .env.example  |  4 ++++
 lib/george.js | 31 +++++++++++++++++++++++++++++++
 lib/parse.js  |  8 ++++++++
 poller.js     | 49 +++++++++++++++++++++++++++++++++++++++++++++++--
 4 files changed, 90 insertions(+), 2 deletions(-)

diff --git a/.env.example b/.env.example
index 1a41f68..eeaf77b 100644
--- a/.env.example
+++ b/.env.example
@@ -29,3 +29,7 @@ POLL_INTERVAL_SEC=10
 DISCO_AUTOCOMMIT=0
 # Where ambiguous/sensitive cases get parked for Steve to review.
 QUEUE_DIR=/Users/macstudio3/.claude/yolo-queue/pending-approval
+
+# Run-completion report (emailed to info@ after any run that took action)
+DISCO_REPORT_TO=info@designerwallcoverings.com
+DISCO_REPORT=1
diff --git a/lib/george.js b/lib/george.js
index 75008f4..4910bbd 100644
--- a/lib/george.js
+++ b/lib/george.js
@@ -76,4 +76,35 @@ export async function getMessage(id) {
   return george(`/api/messages/${encodeURIComponent(id)}`, { query: { account: ACCOUNT } });
 }
 
+// POST helper (Basic-auth reused). Used for the run-completion report email.
+async function georgePost(path, payload) {
+  const headers = { 'Content-Type': 'application/json' };
+  const auth = basicAuth();
+  if (auth) headers.Authorization = `Basic ${auth}`;
+  const res = await fetch(BASE_URL + path, {
+    method: 'POST',
+    headers,
+    body: JSON.stringify(payload),
+  });
+  const text = await res.text();
+  let data;
+  try {
+    data = JSON.parse(text);
+  } catch {
+    data = { raw: text };
+  }
+  if (!res.ok) {
+    const err = new Error(`George POST ${path} -> ${res.status}: ${text.slice(0, 200)}`);
+    err.status = res.status;
+    throw err;
+  }
+  return data;
+}
+
+// Send a plain email (used for the info@ run-completion report). Sending
+// info@ -> info@ is internal, so it clears George's external-send guard.
+export async function sendMail({ to, subject, body, account = ACCOUNT, source = 'discontinued-agent' }) {
+  return georgePost('/api/send', { to, subject, body, account, source });
+}
+
 export { ACCOUNT, BASE_URL };
diff --git a/lib/parse.js b/lib/parse.js
index 514efbc..0e1ab40 100644
--- a/lib/parse.js
+++ b/lib/parse.js
@@ -137,12 +137,20 @@ export function extractMfrNumbers(outboundText) {
 // HIGH-CONFIDENCE explicit discontinuation phrases.
 const DISCO_PHRASES = [
   /\bdiscontinued\b/i,
+  /\bdiscontinu(?:e|ing)\b/i,
+  /\bdisco'?d?\b/i, // vendor shorthand "disco" / "disco'd"
   /\bno longer available\b/i,
   /\bno longer offer(?:ed|ing)?\b/i,
   /\bno longer carr(?:y|ied|ies)\b/i,
+  /\bno longer (?:be )?(?:mak\w+|produc\w+|manufactur\w+)\b/i,
   /\bhas been discontinued\b/i,
   /\bcan\s?not (?:be )?order(?:ed)?[^.]{0,40}discontinued/i,
   /\bdiscontinued[^.]{0,40}can\s?not (?:be )?order/i,
+  // "we are not going to make it again" family — vendor won't remake/reproduce.
+  /\bnot (?:going to |gonna )?(?:be )?(?:re)?(?:mak\w+|produc\w+|manufactur\w+)[^.]{0,30}\bagain\b/i,
+  /\b(?:won'?t|will not) be (?:re)?(?:mak\w+|produc\w+|manufactur\w+)/i,
+  /\b(?:won'?t|will not) (?:be )?(?:re)?(?:made|produced|manufactured)(?:\s+again)?\b/i,
+  /\bnot (?:be )?(?:re)?(?:made|produced|manufactured) again\b/i,
 ];
 
 // QUEUE-worthy (ambiguous / sensitive) phrases — do NOT auto-discontinue.
diff --git a/poller.js b/poller.js
index 0927032..5d90d05 100644
--- a/poller.js
+++ b/poller.js
@@ -42,6 +42,9 @@ const GMAIL_SEARCH =
   'subject:"Priority Request - Samples over 10 days old" newer_than:60d';
 const POLL_INTERVAL_SEC = Number(process.env.POLL_INTERVAL_SEC || 10);
 const MAX_RESULTS = Number(process.env.MAX_RESULTS || 100);
+// Completion report: email a summary to info@ after any run that took action.
+const REPORT_TO = process.env.DISCO_REPORT_TO || 'info@designerwallcoverings.com';
+const REPORT_ENABLED = process.env.DISCO_REPORT !== '0';
 
 function log(...a) {
   console.log(new Date().toISOString(), '[disco]', ...a);
@@ -165,7 +168,7 @@ async function applyCommit(plan) {
   return { today, perMfr };
 }
 
-async function processThread(threadId, headers, state) {
+async function processThread(threadId, headers, state, actions = []) {
   // Fetch full bodies for each message in the thread.
   const full = [];
   for (const h of headers) {
@@ -219,6 +222,7 @@ async function processThread(threadId, headers, state) {
       fieldsWritten: [],
     });
     state[key] = { at: new Date().toISOString(), verdict: 'QUEUE', memoPath };
+    actions.push({ verdict: 'QUEUE', threadId, vendor: plan.vendorFrom, mfrNumbers: plan.mfrNumbers, reasons: plan.reasons });
     log('QUEUE', threadId, plan.vendorFrom, '->', plan.reasons.join('; '));
     return;
   }
@@ -235,6 +239,7 @@ async function processThread(threadId, headers, state) {
     );
     appendLedger({ threadId, vendor: plan.vendorFrom, verdict: 'QUEUE_NO_MFR', reasons: plan.reasons, mfrNumbers: [], memoPath, fieldsWritten: [] });
     state[key] = { at: new Date().toISOString(), verdict: 'QUEUE_NO_MFR', memoPath };
+    actions.push({ verdict: 'QUEUE_NO_MFR', threadId, vendor: plan.vendorFrom, mfrNumbers: [], reasons: plan.reasons });
     log('QUEUE(no mfr#)', threadId, plan.vendorFrom);
     return;
   }
@@ -266,6 +271,13 @@ async function processThread(threadId, headers, state) {
   if (AUTOCOMMIT) {
     state[key] = { at: new Date().toISOString(), verdict: 'COMMIT', recordIds };
   }
+  actions.push({
+    verdict: AUTOCOMMIT ? 'COMMIT' : 'STAGED',
+    threadId,
+    vendor: plan.vendorFrom,
+    mfrNumbers: plan.mfrNumbers,
+    recordIds,
+  });
   log(
     AUTOCOMMIT ? 'COMMIT' : 'STAGED(dry-run)',
     threadId,
@@ -275,6 +287,37 @@ async function processThread(threadId, headers, state) {
   );
 }
 
+// Build + send the run-completion report to info@ (only when actions occurred).
+async function sendRunReport(actions) {
+  if (!REPORT_ENABLED || actions.length === 0) return;
+  const byVerdict = (v) => actions.filter((a) => a.verdict === v);
+  const committed = [...byVerdict('COMMIT'), ...byVerdict('STAGED')];
+  const queued = [...byVerdict('QUEUE'), ...byVerdict('QUEUE_NO_MFR')];
+  const line = (a) =>
+    `<li><b>${a.vendor || 'unknown vendor'}</b> — mfr# ${(a.mfrNumbers || []).join(', ') || '(none)'}` +
+    (a.recordIds ? ` — ${a.recordIds.length} row(s)` : '') +
+    (a.reasons ? ` — <i>${a.reasons.join('; ')}</i>` : '') +
+    ` <span style="color:#888">[thread ${a.threadId}]</span></li>`;
+  const mode = AUTOCOMMIT ? 'AUTO-COMMITTED to FileMaker' : 'STAGED (dry-run — DISCO_AUTOCOMMIT is OFF)';
+  const body = [
+    `<p><b>discontinued-agent run report</b> — ${new Date().toISOString()}</p>`,
+    `<p>Discontinuations ${mode}: <b>${committed.length}</b> · Queued for review: <b>${queued.length}</b></p>`,
+    committed.length ? `<p><b>Discontinued / closed:</b></p><ul>${committed.map(line).join('')}</ul>` : '',
+    queued.length ? `<p><b>Queued for Steve (not auto-written):</b></p><ul>${queued.map(line).join('')}</ul>` : '',
+    `<p style="color:#888">Ledger: data/ledger.jsonl · Queue memos: ~/.claude/yolo-queue/pending-approval/</p>`,
+  ].join('\n');
+  try {
+    await George.sendMail({
+      to: REPORT_TO,
+      subject: `discontinued-agent — ${committed.length} discontinued, ${queued.length} queued`,
+      body,
+    });
+    log(`report emailed to ${REPORT_TO} (${committed.length} disco / ${queued.length} queued)`);
+  } catch (e) {
+    log('WARN report email failed:', e.message);
+  }
+}
+
 async function scanOnce() {
   const state = loadProcessed();
   let headers;
@@ -287,14 +330,16 @@ async function scanOnce() {
   const threads = groupByThread(headers);
   log(`scanned ${headers.length} msgs across ${threads.size} threads`);
 
+  const actions = [];
   for (const [threadId, msgs] of threads) {
     try {
-      await processThread(threadId, msgs, state);
+      await processThread(threadId, msgs, state, actions);
     } catch (e) {
       log('thread error', threadId, e.message);
     }
   }
   saveProcessed(state);
+  await sendRunReport(actions);
 }
 
 async function main() {

← 637b1f3 discontinued-agent: George→FileMaker vendor-discontinued pip  ·  back to Discontinued Agent  ·  chore: v1.1.0 (session close — info@ report + disco trigger 65e4ec9 →