← 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
M .env.exampleM lib/george.jsM lib/parse.jsM poller.js
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 →