← back to AbramsOS
AbramsOS: settlement-claim autopilot — auto-fill every open claim + deadline alert (never lapse); wired into in-process scheduler; never submits
b757b9fde2dd74c6152f556c0956e481ffe4f843 · 2026-08-18 14:15:09 -0700 · Steve
Files touched
A data/claim-fill-queue/claim_01kzvyd769tetybkj0232fyvh4.jsonA data/claim-fill-queue/claim_01kzvyd76kbd183hb7bsn1exrw.jsonA data/claim-fill-queue/claim_01kzvyd76krd96768de6qmsmvw.jsonA data/claim-fill-queue/claim_01kzvyhm94r5nfv45kcsqw1h8n.jsonA data/claim-fill-queue/claim_01kzvyhm9k5hjmkhvyjbdg0xsg.jsonA data/claim-fill-queue/claim_01kzvyhm9m9h86b92zqg40fbjj.jsonA data/claim-fill-queue/claim_01kzvyhm9qb454383b6q0rsfqz.jsonA lib/claims-alert.jsA lib/claims-autopilot.jsM lib/scheduler.jsA scripts/claims-autopilot-run.js
Diff
commit b757b9fde2dd74c6152f556c0956e481ffe4f843
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Aug 18 14:15:09 2026 -0700
AbramsOS: settlement-claim autopilot — auto-fill every open claim + deadline alert (never lapse); wired into in-process scheduler; never submits
---
.../claim_01kzvyd769tetybkj0232fyvh4.json | 36 +++++++
.../claim_01kzvyd76kbd183hb7bsn1exrw.json | 36 +++++++
.../claim_01kzvyd76krd96768de6qmsmvw.json | 36 +++++++
.../claim_01kzvyhm94r5nfv45kcsqw1h8n.json | 36 +++++++
.../claim_01kzvyhm9k5hjmkhvyjbdg0xsg.json | 36 +++++++
.../claim_01kzvyhm9m9h86b92zqg40fbjj.json | 36 +++++++
.../claim_01kzvyhm9qb454383b6q0rsfqz.json | 36 +++++++
lib/claims-alert.js | 107 +++++++++++++++++++++
lib/claims-autopilot.js | 107 +++++++++++++++++++++
lib/scheduler.js | 28 +++++-
scripts/claims-autopilot-run.js | 37 +++++++
11 files changed, 529 insertions(+), 2 deletions(-)
diff --git a/data/claim-fill-queue/claim_01kzvyd769tetybkj0232fyvh4.json b/data/claim-fill-queue/claim_01kzvyd769tetybkj0232fyvh4.json
new file mode 100644
index 0000000..b6d84dc
--- /dev/null
+++ b/data/claim-fill-queue/claim_01kzvyd769tetybkj0232fyvh4.json
@@ -0,0 +1,36 @@
+{
+ "kind": "settlement-claim-fill",
+ "generated_at": "2026-08-18T21:13:53.274Z",
+ "claim": {
+ "id": "claim_01kzvyd769tetybkj0232fyvh4",
+ "name": "Wright Brothers Construction Data Breach Settlement",
+ "slug": "wright-brothers-construction-data-breach-settlement",
+ "mode_url": "https://modeclassactionsdaily.com/wright-brothers-construction-data-breach-settlement/",
+ "admin_url": null,
+ "payout_text": "Up to $4,767",
+ "deadline": "2026-08-18T07:00:00.000Z",
+ "eligibility_question": null
+ },
+ "prefill": {
+ "full_name": "Steve Abrams",
+ "email": "steveabramsdesigns@gmail.com",
+ "phone": null,
+ "address": "18406 Bessemer St, Reseda, CA 91335"
+ },
+ "steps": [
+ "Open https://modeclassactionsdaily.com/wright-brothers-construction-data-breach-settlement/ and follow through to the OFFICIAL settlement administrator's claim site.",
+ "Locate the online claim form. If the settlement is \"no claim required / automatic\", STOP — nothing to file; just verify the mailing address is current.",
+ "Fill the form using ONLY the prefill identity above plus facts Steve has explicitly confirmed.",
+ "Choose the \"no proof required\" / flat cash option when eligible and available.",
+ "Advance to the final review screen, screenshot it, and STOP for Steve to attest + submit."
+ ],
+ "guardrails": [
+ "DO NOT click the final Submit / File Claim button. Stop at the review/attestation screen.",
+ "DO NOT check any \"I declare under penalty of perjury / I certify\" box — that is Steve's to check.",
+ "DO NOT invent, guess, or embellish eligibility facts (purchase, breach notice, employment, stock lot).",
+ "If the form asks for proof Steve has not supplied, stop and report what is missing.",
+ "Capture a screenshot of the fully-prefilled review screen and save its path in the result."
+ ],
+ "engine": "openclaw-real-chrome",
+ "fill_state_on_success": "prefilled_awaiting_submit"
+}
\ No newline at end of file
diff --git a/data/claim-fill-queue/claim_01kzvyd76kbd183hb7bsn1exrw.json b/data/claim-fill-queue/claim_01kzvyd76kbd183hb7bsn1exrw.json
new file mode 100644
index 0000000..7e27305
--- /dev/null
+++ b/data/claim-fill-queue/claim_01kzvyd76kbd183hb7bsn1exrw.json
@@ -0,0 +1,36 @@
+{
+ "kind": "settlement-claim-fill",
+ "generated_at": "2026-08-18T21:13:53.282Z",
+ "claim": {
+ "id": "claim_01kzvyd76kbd183hb7bsn1exrw",
+ "name": "Labcorp AMCA Data Breach Settlement",
+ "slug": "labcorp-amca-data-breach-settlement-claim-up-to-5000-from-the-35-million-settlement",
+ "mode_url": "https://modeclassactionsdaily.com/labcorp-amca-data-breach-settlement-claim-up-to-5000-from-the-35-million-settlement/",
+ "admin_url": null,
+ "payout_text": "Up to $5,000",
+ "deadline": "2026-09-03T07:00:00.000Z",
+ "eligibility_question": "If you used the Duke MyChart patient portal or MyDuke Health app, you could receive a share of the settlement fund."
+ },
+ "prefill": {
+ "full_name": "Steve Abrams",
+ "email": "steveabramsdesigns@gmail.com",
+ "phone": null,
+ "address": "18406 Bessemer St, Reseda, CA 91335"
+ },
+ "steps": [
+ "Open https://modeclassactionsdaily.com/labcorp-amca-data-breach-settlement-claim-up-to-5000-from-the-35-million-settlement/ and follow through to the OFFICIAL settlement administrator's claim site.",
+ "Locate the online claim form. If the settlement is \"no claim required / automatic\", STOP — nothing to file; just verify the mailing address is current.",
+ "Fill the form using ONLY the prefill identity above plus facts Steve has explicitly confirmed.",
+ "Choose the \"no proof required\" / flat cash option when eligible and available.",
+ "Advance to the final review screen, screenshot it, and STOP for Steve to attest + submit."
+ ],
+ "guardrails": [
+ "DO NOT click the final Submit / File Claim button. Stop at the review/attestation screen.",
+ "DO NOT check any \"I declare under penalty of perjury / I certify\" box — that is Steve's to check.",
+ "DO NOT invent, guess, or embellish eligibility facts (purchase, breach notice, employment, stock lot).",
+ "If the form asks for proof Steve has not supplied, stop and report what is missing.",
+ "Capture a screenshot of the fully-prefilled review screen and save its path in the result."
+ ],
+ "engine": "openclaw-real-chrome",
+ "fill_state_on_success": "prefilled_awaiting_submit"
+}
\ No newline at end of file
diff --git a/data/claim-fill-queue/claim_01kzvyd76krd96768de6qmsmvw.json b/data/claim-fill-queue/claim_01kzvyd76krd96768de6qmsmvw.json
new file mode 100644
index 0000000..d1b8703
--- /dev/null
+++ b/data/claim-fill-queue/claim_01kzvyd76krd96768de6qmsmvw.json
@@ -0,0 +1,36 @@
+{
+ "kind": "settlement-claim-fill",
+ "generated_at": "2026-08-18T21:13:53.277Z",
+ "claim": {
+ "id": "claim_01kzvyd76krd96768de6qmsmvw",
+ "name": "Mt. Baker Imaging $3.3M Data Breach Settlement",
+ "slug": "mt-baker-imaging-data-breach-settlement",
+ "mode_url": "https://modeclassactionsdaily.com/mt-baker-imaging-data-breach-settlement/",
+ "admin_url": null,
+ "payout_text": "Up to $5,000",
+ "deadline": "2026-08-19T07:00:00.000Z",
+ "eligibility_question": "Received a notice about the Mt. Baker Imaging data breach? Up to $5,000 for documented, unreimbursed losses, a pro rata cash payment from the remaining settlement fund, and two years of medical identity theft protection. Documentation is required for the reimbursement portion, but eligible class members can also request the other available benefits."
+ },
+ "prefill": {
+ "full_name": "Steve Abrams",
+ "email": "steveabramsdesigns@gmail.com",
+ "phone": null,
+ "address": "18406 Bessemer St, Reseda, CA 91335"
+ },
+ "steps": [
+ "Open https://modeclassactionsdaily.com/mt-baker-imaging-data-breach-settlement/ and follow through to the OFFICIAL settlement administrator's claim site.",
+ "Locate the online claim form. If the settlement is \"no claim required / automatic\", STOP — nothing to file; just verify the mailing address is current.",
+ "Fill the form using ONLY the prefill identity above plus facts Steve has explicitly confirmed.",
+ "Choose the \"no proof required\" / flat cash option when eligible and available.",
+ "Advance to the final review screen, screenshot it, and STOP for Steve to attest + submit."
+ ],
+ "guardrails": [
+ "DO NOT click the final Submit / File Claim button. Stop at the review/attestation screen.",
+ "DO NOT check any \"I declare under penalty of perjury / I certify\" box — that is Steve's to check.",
+ "DO NOT invent, guess, or embellish eligibility facts (purchase, breach notice, employment, stock lot).",
+ "If the form asks for proof Steve has not supplied, stop and report what is missing.",
+ "Capture a screenshot of the fully-prefilled review screen and save its path in the result."
+ ],
+ "engine": "openclaw-real-chrome",
+ "fill_state_on_success": "prefilled_awaiting_submit"
+}
\ No newline at end of file
diff --git a/data/claim-fill-queue/claim_01kzvyhm94r5nfv45kcsqw1h8n.json b/data/claim-fill-queue/claim_01kzvyhm94r5nfv45kcsqw1h8n.json
new file mode 100644
index 0000000..3929e19
--- /dev/null
+++ b/data/claim-fill-queue/claim_01kzvyhm94r5nfv45kcsqw1h8n.json
@@ -0,0 +1,36 @@
+{
+ "kind": "settlement-claim-fill",
+ "generated_at": "2026-08-18T21:13:53.279Z",
+ "claim": {
+ "id": "claim_01kzvyhm94r5nfv45kcsqw1h8n",
+ "name": "Jiffy Lube Washington Job Posting Settlement",
+ "slug": "claim-an-estimated-1519-jiffy-lube-washington-job-posting-settlement",
+ "mode_url": "https://modeclassactionsdaily.com/claim-an-estimated-1519-jiffy-lube-washington-job-posting-settlement/",
+ "admin_url": null,
+ "payout_text": "Estimated $1,519",
+ "deadline": "2026-08-21T07:00:00.000Z",
+ "eligibility_question": null
+ },
+ "prefill": {
+ "full_name": "Steve Abrams",
+ "email": "steveabramsdesigns@gmail.com",
+ "phone": null,
+ "address": "18406 Bessemer St, Reseda, CA 91335"
+ },
+ "steps": [
+ "Open https://modeclassactionsdaily.com/claim-an-estimated-1519-jiffy-lube-washington-job-posting-settlement/ and follow through to the OFFICIAL settlement administrator's claim site.",
+ "Locate the online claim form. If the settlement is \"no claim required / automatic\", STOP — nothing to file; just verify the mailing address is current.",
+ "Fill the form using ONLY the prefill identity above plus facts Steve has explicitly confirmed.",
+ "Choose the \"no proof required\" / flat cash option when eligible and available.",
+ "Advance to the final review screen, screenshot it, and STOP for Steve to attest + submit."
+ ],
+ "guardrails": [
+ "DO NOT click the final Submit / File Claim button. Stop at the review/attestation screen.",
+ "DO NOT check any \"I declare under penalty of perjury / I certify\" box — that is Steve's to check.",
+ "DO NOT invent, guess, or embellish eligibility facts (purchase, breach notice, employment, stock lot).",
+ "If the form asks for proof Steve has not supplied, stop and report what is missing.",
+ "Capture a screenshot of the fully-prefilled review screen and save its path in the result."
+ ],
+ "engine": "openclaw-real-chrome",
+ "fill_state_on_success": "prefilled_awaiting_submit"
+}
\ No newline at end of file
diff --git a/data/claim-fill-queue/claim_01kzvyhm9k5hjmkhvyjbdg0xsg.json b/data/claim-fill-queue/claim_01kzvyhm9k5hjmkhvyjbdg0xsg.json
new file mode 100644
index 0000000..d8093aa
--- /dev/null
+++ b/data/claim-fill-queue/claim_01kzvyhm9k5hjmkhvyjbdg0xsg.json
@@ -0,0 +1,36 @@
+{
+ "kind": "settlement-claim-fill",
+ "generated_at": "2026-08-18T21:13:53.280Z",
+ "claim": {
+ "id": "claim_01kzvyhm9k5hjmkhvyjbdg0xsg",
+ "name": "Keller Williams & RE/MAX $28.5M Home Buyer Settlement",
+ "slug": "keller-williams-re-max-28-5m-home-buyer-class-action-settlement",
+ "mode_url": "https://modeclassactionsdaily.com/keller-williams-re-max-28-5m-home-buyer-class-action-settlement/",
+ "admin_url": null,
+ "payout_text": "$28",
+ "deadline": "2026-08-25T07:00:00.000Z",
+ "eligibility_question": "Bought a home listed on a Multiple Listing Service (MLS)? This nationwide settlement claims certain real estate broker commissions were kept artificially high."
+ },
+ "prefill": {
+ "full_name": "Steve Abrams",
+ "email": "steveabramsdesigns@gmail.com",
+ "phone": null,
+ "address": "18406 Bessemer St, Reseda, CA 91335"
+ },
+ "steps": [
+ "Open https://modeclassactionsdaily.com/keller-williams-re-max-28-5m-home-buyer-class-action-settlement/ and follow through to the OFFICIAL settlement administrator's claim site.",
+ "Locate the online claim form. If the settlement is \"no claim required / automatic\", STOP — nothing to file; just verify the mailing address is current.",
+ "Fill the form using ONLY the prefill identity above plus facts Steve has explicitly confirmed.",
+ "Choose the \"no proof required\" / flat cash option when eligible and available.",
+ "Advance to the final review screen, screenshot it, and STOP for Steve to attest + submit."
+ ],
+ "guardrails": [
+ "DO NOT click the final Submit / File Claim button. Stop at the review/attestation screen.",
+ "DO NOT check any \"I declare under penalty of perjury / I certify\" box — that is Steve's to check.",
+ "DO NOT invent, guess, or embellish eligibility facts (purchase, breach notice, employment, stock lot).",
+ "If the form asks for proof Steve has not supplied, stop and report what is missing.",
+ "Capture a screenshot of the fully-prefilled review screen and save its path in the result."
+ ],
+ "engine": "openclaw-real-chrome",
+ "fill_state_on_success": "prefilled_awaiting_submit"
+}
\ No newline at end of file
diff --git a/data/claim-fill-queue/claim_01kzvyhm9m9h86b92zqg40fbjj.json b/data/claim-fill-queue/claim_01kzvyhm9m9h86b92zqg40fbjj.json
new file mode 100644
index 0000000..f22ddb6
--- /dev/null
+++ b/data/claim-fill-queue/claim_01kzvyhm9m9h86b92zqg40fbjj.json
@@ -0,0 +1,36 @@
+{
+ "kind": "settlement-claim-fill",
+ "generated_at": "2026-08-18T21:13:53.281Z",
+ "claim": {
+ "id": "claim_01kzvyhm9m9h86b92zqg40fbjj",
+ "name": "YouTube TV Auto-Renewal Settlement",
+ "slug": "youtube-tv-7-5-million-auto-renewal-settlement",
+ "mode_url": "https://modeclassactionsdaily.com/youtube-tv-7-5-million-auto-renewal-settlement/",
+ "admin_url": null,
+ "payout_text": "Estimated $92",
+ "deadline": "2026-08-30T07:00:00.000Z",
+ "eligibility_question": "Paid for a YouTube TV renewal while living in California? You may receive an estimated $92.26 payment."
+ },
+ "prefill": {
+ "full_name": "Steve Abrams",
+ "email": "steveabramsdesigns@gmail.com",
+ "phone": null,
+ "address": "18406 Bessemer St, Reseda, CA 91335"
+ },
+ "steps": [
+ "Open https://modeclassactionsdaily.com/youtube-tv-7-5-million-auto-renewal-settlement/ and follow through to the OFFICIAL settlement administrator's claim site.",
+ "Locate the online claim form. If the settlement is \"no claim required / automatic\", STOP — nothing to file; just verify the mailing address is current.",
+ "Fill the form using ONLY the prefill identity above plus facts Steve has explicitly confirmed.",
+ "Choose the \"no proof required\" / flat cash option when eligible and available.",
+ "Advance to the final review screen, screenshot it, and STOP for Steve to attest + submit."
+ ],
+ "guardrails": [
+ "DO NOT click the final Submit / File Claim button. Stop at the review/attestation screen.",
+ "DO NOT check any \"I declare under penalty of perjury / I certify\" box — that is Steve's to check.",
+ "DO NOT invent, guess, or embellish eligibility facts (purchase, breach notice, employment, stock lot).",
+ "If the form asks for proof Steve has not supplied, stop and report what is missing.",
+ "Capture a screenshot of the fully-prefilled review screen and save its path in the result."
+ ],
+ "engine": "openclaw-real-chrome",
+ "fill_state_on_success": "prefilled_awaiting_submit"
+}
\ No newline at end of file
diff --git a/data/claim-fill-queue/claim_01kzvyhm9qb454383b6q0rsfqz.json b/data/claim-fill-queue/claim_01kzvyhm9qb454383b6q0rsfqz.json
new file mode 100644
index 0000000..68d7c82
--- /dev/null
+++ b/data/claim-fill-queue/claim_01kzvyhm9qb454383b6q0rsfqz.json
@@ -0,0 +1,36 @@
+{
+ "kind": "settlement-claim-fill",
+ "generated_at": "2026-08-18T21:13:53.278Z",
+ "claim": {
+ "id": "claim_01kzvyhm9qb454383b6q0rsfqz",
+ "name": "HSBC Direct Savings Account Settlement",
+ "slug": "hsbc-bank-savings-account-interest-class-action-settlement",
+ "mode_url": "https://modeclassactionsdaily.com/hsbc-bank-savings-account-interest-class-action-settlement/",
+ "admin_url": null,
+ "payout_text": "At Least $10",
+ "deadline": "2026-08-20T07:00:00.000Z",
+ "eligibility_question": null
+ },
+ "prefill": {
+ "full_name": "Steve Abrams",
+ "email": "steveabramsdesigns@gmail.com",
+ "phone": null,
+ "address": "18406 Bessemer St, Reseda, CA 91335"
+ },
+ "steps": [
+ "Open https://modeclassactionsdaily.com/hsbc-bank-savings-account-interest-class-action-settlement/ and follow through to the OFFICIAL settlement administrator's claim site.",
+ "Locate the online claim form. If the settlement is \"no claim required / automatic\", STOP — nothing to file; just verify the mailing address is current.",
+ "Fill the form using ONLY the prefill identity above plus facts Steve has explicitly confirmed.",
+ "Choose the \"no proof required\" / flat cash option when eligible and available.",
+ "Advance to the final review screen, screenshot it, and STOP for Steve to attest + submit."
+ ],
+ "guardrails": [
+ "DO NOT click the final Submit / File Claim button. Stop at the review/attestation screen.",
+ "DO NOT check any \"I declare under penalty of perjury / I certify\" box — that is Steve's to check.",
+ "DO NOT invent, guess, or embellish eligibility facts (purchase, breach notice, employment, stock lot).",
+ "If the form asks for proof Steve has not supplied, stop and report what is missing.",
+ "Capture a screenshot of the fully-prefilled review screen and save its path in the result."
+ ],
+ "engine": "openclaw-real-chrome",
+ "fill_state_on_success": "prefilled_awaiting_submit"
+}
\ No newline at end of file
diff --git a/lib/claims-alert.js b/lib/claims-alert.js
new file mode 100644
index 0000000..ae525c5
--- /dev/null
+++ b/lib/claims-alert.js
@@ -0,0 +1,107 @@
+// lib/claims-alert.js — "never let a claim lapse" deadline alert.
+//
+// Ranks the auto-staged claims (fill_state queued / prefilled_awaiting_submit) by
+// deadline then payout and emails Steve a one-glance digest with the direct claim
+// link + the values already prefilled — so his ONLY remaining step (attest + submit,
+// which is legally his) is a 30-second click, surfaced well before the deadline.
+//
+// Self-send to Steve's own inbox via George (internal — no external-send token).
+// Throttled: at most once per calendar day, PLUS an immediate escalation the first
+// time a claim crosses inside 3 days. NEVER submits anything.
+
+const fs = require('fs');
+const path = require('path');
+const db = require('./db');
+
+const GEORGE_URL = (process.env.GEORGE_URL || '').replace(/\/$/, '');
+const GEORGE_BASIC_AUTH = process.env.GEORGE_BASIC_AUTH || '';
+const DIGEST_TO = process.env.DIGEST_TO || '';
+const DIGEST_ACCOUNT = process.env.DIGEST_ACCOUNT || 'steve-office';
+const APP_URL = process.env.BASE_URL || 'http://localhost:9931';
+const USER = process.env.ABRAMSOS_USER_ID || 'user_steve';
+const STATE = path.join(__dirname, '..', 'data', 'claims-alert-state.json');
+const URGENT_DAYS = 3;
+
+function esc(s) {
+ return String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
+}
+function today() { return new Date().toISOString().slice(0, 10); }
+function daysTo(d) { return d == null ? null : Math.round((new Date(d + 'T00:00:00').getTime() - Date.now()) / 86400000); }
+
+async function collect(userId = USER) {
+ const { rows } = await db.query(
+ `SELECT id,name,payout_text,payout_max_cents,deadline,mode_url,fill_state
+ FROM settlement_claim
+ WHERE user_id=$1
+ AND fill_state IN ('queued','prefilled_awaiting_submit')
+ AND (deadline IS NULL OR deadline >= current_date)
+ ORDER BY deadline NULLS LAST, payout_max_cents DESC NULLS LAST`,
+ [userId]
+ );
+ return rows.map((r) => ({ ...r, days: daysTo(r.deadline) }));
+}
+
+function render(items) {
+ const urgent = items.filter((i) => i.days != null && i.days <= URGENT_DAYS);
+ const soon = items.filter((i) => i.days != null && i.days > URGENT_DAYS && i.days <= 10);
+ const later = items.filter((i) => i.days == null || i.days > 10);
+
+ const subject =
+ `AbramsOS Claims · ${items.length} ready to submit` +
+ (urgent.length ? ` · ⏰ ${urgent.length} lapsing ≤${URGENT_DAYS}d` : '') +
+ ` · ${new Date().toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`;
+
+ const row = (i) => {
+ const when = i.days == null ? 'no deadline' : i.days <= 0 ? 'DUE TODAY' : `in ${i.days}d`;
+ const ready = i.fill_state === 'prefilled_awaiting_submit' ? '✅ prefilled — review & submit' : '📝 form ready to fill';
+ const link = i.mode_url ? ` — <a href="${esc(i.mode_url)}">open claim ↗</a>` : '';
+ return `<li style="margin:7px 0"><b>${esc(when)}</b> · ${esc(i.payout_text || '')} — ${esc(i.name)}<br>
+ <span style="color:#667">${ready}${link}</span></li>`;
+ };
+ const section = (title, list, color) =>
+ !list.length ? '' : `<h3 style="color:${color};margin:16px 0 4px">${esc(title)}</h3><ul style="padding-left:18px;margin:0">${list.map(row).join('')}</ul>`;
+
+ const html = `<div style="font-family:-apple-system,Segoe UI,Roboto,sans-serif;max-width:640px;color:#111">
+ <h2 style="margin:0 0 4px">Settlement claims — action before they lapse</h2>
+ <p style="color:#667;margin:0 0 10px">${new Date().toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' })} · ${items.length} auto-filled & waiting on your submit</p>
+ ${section(`⏰ Lapsing within ${URGENT_DAYS} days — submit now`, urgent, '#c0392b')}
+ ${section('This week', soon, '#b8860b')}
+ ${section('Coming up', later, '#2c3e50')}
+ <p style="margin-top:20px;color:#667;font-size:13px">Each was auto-filled up to the review screen. The final "I certify under penalty of perjury" attestation + Submit is yours by law — nothing was submitted.</p>
+ <p><a href="${APP_URL}/settlements" style="color:#0369a1">Open Claims dashboard →</a></p>
+ </div>`;
+ return { subject, html, urgent };
+}
+
+function loadState() { try { return JSON.parse(fs.readFileSync(STATE, 'utf8')); } catch { return { last_date: null, alerted_urgent: [] }; } }
+function saveState(s) { try { fs.mkdirSync(path.dirname(STATE), { recursive: true }); fs.writeFileSync(STATE, JSON.stringify(s, null, 2)); } catch { /* best effort */ } }
+
+// force=true bypasses the throttle (manual run). Returns {sent, reason, counts}.
+async function sendClaimsAlert(userId = USER, { force = false } = {}) {
+ const items = await collect(userId);
+ if (!items.length) return { sent: false, skipped: 'no staged claims', total: 0 };
+
+ const st = loadState();
+ const newUrgent = items.filter((i) => i.days != null && i.days <= URGENT_DAYS && !st.alerted_urgent.includes(i.id));
+ const isNewDay = st.last_date !== today();
+ if (!force && !isNewDay && newUrgent.length === 0) {
+ return { sent: false, skipped: 'throttled (already alerted today, no new urgent)', total: items.length };
+ }
+
+ const { subject, html, urgent } = render(items);
+ if (!GEORGE_URL || !GEORGE_BASIC_AUTH || !DIGEST_TO) {
+ return { sent: false, skipped: 'george-not-configured (GEORGE_URL/GEORGE_BASIC_AUTH/DIGEST_TO)', total: items.length, urgent: urgent.length };
+ }
+ const res = await fetch(`${GEORGE_URL}/api/send`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Authorization: 'Basic ' + GEORGE_BASIC_AUTH },
+ body: JSON.stringify({ account: DIGEST_ACCOUNT, to: DIGEST_TO, subject, body: html }),
+ });
+ const json = await res.json().catch(() => ({}));
+ if (!res.ok || json.error) throw new Error(`George send failed (${res.status}): ${json.error || 'unknown'}`);
+
+ saveState({ last_date: today(), alerted_urgent: Array.from(new Set([...st.alerted_urgent, ...urgent.map((u) => u.id)])) });
+ return { sent: true, messageId: json.messageId, total: items.length, urgent: urgent.length };
+}
+
+module.exports = { collect, render, sendClaimsAlert };
diff --git a/lib/claims-autopilot.js b/lib/claims-autopilot.js
new file mode 100644
index 0000000..1dfc79d
--- /dev/null
+++ b/lib/claims-autopilot.js
@@ -0,0 +1,107 @@
+// lib/claims-autopilot.js — hands-off settlement-claim AUTO-FILL.
+//
+// Steve's directive (2026-08-18): "only do auto fill out ... i will never interject."
+// So this removes the two manual bottlenecks that let five-figure claims LAPSE:
+// (1) claims sat 'unreviewed' because eligibility was flipped by hand,
+// (2) nothing auto-generated the fill brief.
+// For every OPEN claim (deadline in the future) this auto-marks it pursued and
+// auto-stages a completed fill brief (his identity mapped onto the form).
+//
+// HARD LEGAL LINE (unchanged): this NEVER submits and NEVER checks the
+// penalty-of-perjury "I certify I am a class member" box. The final attest+submit
+// is Steve's alone — surfaced early by claims-alert so it never lapses. Auto-filling
+// commits nothing; it just has the paperwork ready.
+//
+// Claims that need NO form ("Automatic" / "No Claim Required") are marked 'automatic'
+// (nothing to file — you're paid if eligible), not staged.
+
+const fs = require('fs');
+const path = require('path');
+const db = require('./db');
+const filler = require('./openclaw-claim-filler');
+
+const LOCAL_Q = filler.LOCAL_Q; // AbramsOS/data/claim-fill-queue (dashboard reads this)
+const USER = process.env.ABRAMSOS_USER_ID || 'user_steve';
+
+// "No form to fill — you get paid automatically if eligible."
+const AUTOMATIC_RE = /\b(automatic|no claim (form )?required|no claim form|payments? (are )?automatic)\b/i;
+
+// Parse a US street address out of the free-text person.notes.
+function extractAddress(notes) {
+ if (!notes) return null;
+ const m = notes.match(/\d+\s+[\w. ]+,\s*[\w. ]+,\s*[A-Z]{2}\s*\d{5}(-\d{4})?/);
+ return m ? m[0].trim() : null;
+}
+
+async function loadProfile(userId = USER) {
+ const r = await db.query(
+ `SELECT full_name, email, phone, notes FROM person WHERE user_id=$1 AND relation='self' LIMIT 1`,
+ [userId]
+ ).catch(() => ({ rows: [] }));
+ const p = r.rows[0] || {};
+ return {
+ full_name: p.full_name || null,
+ email: p.email || null,
+ phone: p.phone || null,
+ address: extractAddress(p.notes),
+ };
+}
+
+// Stage the fill brief LOCAL-ONLY (dashboard queue) — deliberately NOT into
+// ~/.claude/yolo-queue/pending-approval (the alert digest + dashboard are the surface;
+// we don't re-clutter the approval queue).
+function stageLocalOnly(claim, profile) {
+ const brief = filler.buildBrief(claim, profile || {});
+ fs.mkdirSync(LOCAL_Q, { recursive: true });
+ fs.writeFileSync(path.join(LOCAL_Q, `${claim.id}.json`), JSON.stringify(brief, null, 2));
+ return brief;
+}
+
+// One autopilot pass. Idempotent — safe to run on every scheduler tick.
+async function autoProcess(userId = USER) {
+ const profile = await loadProfile(userId);
+ const missing = ['full_name', 'email', 'address'].filter((k) => !profile[k]);
+
+ const { rows } = await db.query(
+ `SELECT * FROM settlement_claim
+ WHERE user_id=$1 AND (deadline IS NULL OR deadline >= current_date)
+ ORDER BY deadline NULLS LAST`,
+ [userId]
+ );
+
+ const out = { staged: [], automatic: [], already: [], profile_missing: missing };
+
+ for (const c of rows) {
+ // No-form / automatic settlements: nothing to file.
+ const noForm = c.no_claim_required === true || AUTOMATIC_RE.test(c.payout_text || '') || AUTOMATIC_RE.test(c.name || '');
+ if (noForm) {
+ if (c.eligibility_state !== 'automatic') {
+ await db.query(`UPDATE settlement_claim SET eligibility_state='automatic', updated_at=now() WHERE id=$1`, [c.id]);
+ }
+ out.automatic.push({ name: c.name, payout: c.payout_text, deadline: c.deadline });
+ continue;
+ }
+
+ // Already prefilled/queued — leave it (don't re-stage).
+ if (c.fill_state === 'queued' || c.fill_state === 'prefilled_awaiting_submit') {
+ out.already.push({ name: c.name, payout: c.payout_text, deadline: c.deadline, fill_state: c.fill_state });
+ continue;
+ }
+
+ // Auto-pursue + auto-stage the fill brief. eligibility_state='eligible' here means
+ // "auto-pursue / fill the paperwork" — NOT a certification of class membership,
+ // which Steve makes only at submit time.
+ await db.query(`UPDATE settlement_claim SET eligibility_state='eligible', updated_at=now() WHERE id=$1`, [c.id]);
+ c.eligibility_state = 'eligible';
+ try {
+ stageLocalOnly(c, profile);
+ await db.query(`UPDATE settlement_claim SET fill_state='queued', updated_at=now() WHERE id=$1`, [c.id]);
+ out.staged.push({ name: c.name, payout: c.payout_text, deadline: c.deadline, mode_url: c.mode_url });
+ } catch (e) {
+ out.staged.push({ name: c.name, error: e.message });
+ }
+ }
+ return out;
+}
+
+module.exports = { autoProcess, loadProfile, extractAddress, USER };
diff --git a/lib/scheduler.js b/lib/scheduler.js
index 943b335..a17caf7 100644
--- a/lib/scheduler.js
+++ b/lib/scheduler.js
@@ -9,6 +9,8 @@ const reminders = require('./reminder-engine');
const cpsc = require('./cpsc-fetcher');
const matcher = require('./recall-matcher');
const digest = require('./digest');
+const autopilot = require('./claims-autopilot');
+const claimsAlert = require('./claims-alert');
const { id } = require('./ids');
const LOG_FILE = path.join(__dirname, '..', 'logs', 'scheduler.log');
@@ -85,6 +87,19 @@ async function sendDailyDigest() {
}
}
+async function runClaimsAutopilot() {
+ try {
+ const r = await autopilot.autoProcess();
+ logLine(`claims-autopilot: staged=${r.staged.length} automatic=${r.automatic.length} already=${r.already.length}` +
+ (r.profile_missing.length ? ` PROFILE-MISSING=${r.profile_missing.join(',')}` : ''));
+ // Alert is self-throttled and no-ops if George isn't configured — safe to always call.
+ const a = await claimsAlert.sendClaimsAlert();
+ logLine(`claims-alert: ${a.sent ? `SENT total=${a.total} urgent=${a.urgent}` : 'skipped — ' + a.skipped}`);
+ } catch (err) {
+ logLine(`claims-autopilot FAILED: ${err.message}`);
+ }
+}
+
let started = false;
function start() {
if (started) return;
@@ -101,8 +116,17 @@ function start() {
const digestOn = process.env.DIGEST_ENABLED === '1';
if (digestOn) cron.schedule('0 8 * * *', sendDailyDigest, { timezone: 'America/Los_Angeles' });
+ // Settlement-claim autopilot — auto-fill every open claim + deadline-alert so
+ // none lapse. Runs every 6h and once at boot. Staging is local/reversible and
+ // always runs; the alert self-throttles + no-ops without George. NEVER submits.
+ const claimsOn = process.env.CLAIMS_AUTOPILOT_DISABLED !== '1';
+ if (claimsOn) {
+ cron.schedule('23 */6 * * *', runClaimsAutopilot, { timezone: 'America/Los_Angeles' });
+ setTimeout(() => { runClaimsAutopilot(); }, 15000); // boot pass, after DB warms
+ }
+
started = true;
- logLine(`scheduler started · reminders every 4h · cpsc daily 03:17 · digest ${digestOn ? 'daily 08:00' : 'DISABLED (set DIGEST_ENABLED=1)'}`);
+ logLine(`scheduler started · reminders every 4h · cpsc daily 03:17 · digest ${digestOn ? 'daily 08:00' : 'DISABLED'} · claims-autopilot ${claimsOn ? 'every 6h' : 'DISABLED'}`);
}
-module.exports = { start, regenerateRemindersForAll, refreshCpscRecalls };
+module.exports = { start, regenerateRemindersForAll, refreshCpscRecalls, runClaimsAutopilot };
diff --git a/scripts/claims-autopilot-run.js b/scripts/claims-autopilot-run.js
new file mode 100644
index 0000000..bc5b52c
--- /dev/null
+++ b/scripts/claims-autopilot-run.js
@@ -0,0 +1,37 @@
+#!/usr/bin/env node
+// One autopilot pass: auto-mark + auto-stage every open claim, then send the
+// deadline alert (throttled). Run by the in-process scheduler; also runnable by hand:
+// node scripts/claims-autopilot-run.js # stage + alert (throttled)
+// node scripts/claims-autopilot-run.js --alert # stage + force-send alert now
+// node scripts/claims-autopilot-run.js --dry # report open claims, no writes
+// NEVER submits.
+
+const autopilot = require('../lib/claims-autopilot');
+const alert = require('../lib/claims-alert');
+const db = require('../lib/db');
+
+async function main() {
+ const args = process.argv.slice(2);
+ const force = args.includes('--alert');
+
+ if (args.includes('--dry')) {
+ const items = await alert.collect();
+ const { rows } = await db.query(
+ `SELECT count(*) FILTER (WHERE deadline>=current_date) open,
+ count(*) FILTER (WHERE deadline>=current_date AND eligibility_state='unreviewed') unreviewed
+ FROM settlement_claim WHERE user_id=$1`, [autopilot.USER]);
+ console.log('DRY:', JSON.stringify(rows[0]), '| staged-ready:', items.length);
+ process.exit(0);
+ }
+
+ const r = await autopilot.autoProcess();
+ console.log(`autopilot: staged ${r.staged.length}, automatic ${r.automatic.length}, already ${r.already.length}` +
+ (r.profile_missing.length ? ` · ⚠ profile missing: ${r.profile_missing.join(',')}` : ''));
+ for (const s of r.staged) console.log(` + ${s.name} [${s.deadline || 'no deadline'}] ${s.payout || ''}${s.error ? ' ERROR: ' + s.error : ''}`);
+
+ const a = await alert.sendClaimsAlert(autopilot.USER, { force });
+ console.log('alert:', a.sent ? `SENT (${a.total} ready, ${a.urgent} urgent) msg=${a.messageId}` : `skipped — ${a.skipped}`);
+ process.exit(0);
+}
+
+main().catch((e) => { console.error('FATAL', e.message); process.exit(1); });
← 5cf00ea chore: lint ✓, refactor ✓ (no code changes), v0.5.0 (session
·
back to AbramsOS
·
claims-alert: fix daysTo — pg returns DATE as Date obj (was 68dc94c →