Context
The inbound-lead farm exists end-to-end but is not connected on the beat, so live recruiter mail (incl. a scheduled interview) sat unfarmed while the daily Opportunity brief reported INBOUND LEADS: 0. Root cause of the dark feed (a variable-name wiring bug, not a dead credential) is fixed in #185 and its companion organvm/limen#1447 (an IMAP --verify probe so a genuinely stale password is caught loudly next time). This issue tracks the remaining integration so the beat actually farms.
Verified findings (2026-07-23)
- With the correct env (
GMAIL_USER/GMAIL_APP_PASSWORD), gmail_imap_sweep.py reads all 30 INBOX messages and its MATTER_MAP already buckets recruiter mail as Job Search — but that path is not the obligations source.
obligations_build.py sources Gmail from audit/inbox_sweep-<account>.json written by the AppleScript inbox_sweep.py, which sees an empty Gmail view (total:0). So no Gmail obligations are built.
build() (obligations_build.py:220-259) only processes action=="fire" rows and reads r.get("id") for message-ids — but gmail_imap_sweep.classify() rows carry uid (not id) and lack reply_to/snippet, so even if fed, opportunity_sync's message-id dedup (entry_id_for) would collide.
core/protocols.py inbound-lead-hire matcher (line ~254) keys on recruiter|role at|position at|hiring for|opportunity — it does not catch a calendar-invite interview (e.g. an Ashby/Google-Calendar .ics "Chat with ") nor a bare Needed: <role> subject.
Scope (bounded)
- Row schema:
gmail_imap_sweep.classify() — emit id (= uid), reply_to, and a snippet on each row so obligations/opportunity_sync dedup works.
- Classify-only receipt mode: a read-only pass that writes
audit/inbox_sweep-<account>.json (obligations schema) without archiving, so obligations_build sources Gmail from IMAP, not Apple Mail.
- Beat wire (
organvm/limen metabolize.sh via institutio/governance/sensors.yaml, derive-runner pattern — one registry entry): run the Gmail classify pass → obligations_build → opportunity_sync (application-pipeline). Add an audit/synced_to_pipeline.json receipt so farmed/unfarmed is queryable.
- Classification (
core/protocols.py): classify calendar-invite interviews and Needed:/Seeking: recruiter subjects as inbound-lead-hire (with tests to bound false positives across the corpus).
- Brief honesty (
organvm/limen scripts/opportunity-brief.py): distinguish 0 inbound leads from feed DARK (N accounts returned 0 while inbox non-empty) — a dead feed must read loud, never 0.
Predicate
A done.sh: Gmail feed non-empty when the inbox is non-empty; ≥1 live recruiter thread → inbound-lead-* obligation → applications/<date>/<org>--inbound/entry.yaml; brief reports real count or DARK; idempotent re-run is a no-op.
Related: organvm/application-pipeline#91 (educator/HigherEdJobs discovery inflow — same inflow shape). Companion PRs: #185, organvm/limen#1447.
Context
The inbound-lead farm exists end-to-end but is not connected on the beat, so live recruiter mail (incl. a scheduled interview) sat unfarmed while the daily Opportunity brief reported
INBOUND LEADS: 0. Root cause of the dark feed (a variable-name wiring bug, not a dead credential) is fixed in #185 and its companionorganvm/limen#1447(an IMAP--verifyprobe so a genuinely stale password is caught loudly next time). This issue tracks the remaining integration so the beat actually farms.Verified findings (2026-07-23)
GMAIL_USER/GMAIL_APP_PASSWORD),gmail_imap_sweep.pyreads all 30 INBOX messages and itsMATTER_MAPalready buckets recruiter mail as Job Search — but that path is not the obligations source.obligations_build.pysources Gmail fromaudit/inbox_sweep-<account>.jsonwritten by the AppleScriptinbox_sweep.py, which sees an empty Gmail view (total:0). So no Gmail obligations are built.build()(obligations_build.py:220-259) only processesaction=="fire"rows and readsr.get("id")for message-ids — butgmail_imap_sweep.classify()rows carryuid(notid) and lackreply_to/snippet, so even if fed,opportunity_sync's message-id dedup (entry_id_for) would collide.core/protocols.pyinbound-lead-hirematcher (line ~254) keys onrecruiter|role at|position at|hiring for|opportunity— it does not catch a calendar-invite interview (e.g. an Ashby/Google-Calendar.ics"Chat with ") nor a bareNeeded: <role>subject.Scope (bounded)
gmail_imap_sweep.classify()— emitid(= uid),reply_to, and asnippeton each row so obligations/opportunity_sync dedup works.audit/inbox_sweep-<account>.json(obligations schema) without archiving, soobligations_buildsources Gmail from IMAP, not Apple Mail.organvm/limenmetabolize.shviainstitutio/governance/sensors.yaml, derive-runner pattern — one registry entry): run the Gmail classify pass →obligations_build→opportunity_sync(application-pipeline). Add anaudit/synced_to_pipeline.jsonreceipt so farmed/unfarmed is queryable.core/protocols.py): classify calendar-invite interviews andNeeded:/Seeking:recruiter subjects asinbound-lead-hire(with tests to bound false positives across the corpus).organvm/limenscripts/opportunity-brief.py): distinguish0 inbound leadsfrom feed DARK (N accounts returned 0 while inbox non-empty) — a dead feed must read loud, never0.Predicate
A
done.sh: Gmail feed non-empty when the inbox is non-empty; ≥1 live recruiter thread →inbound-lead-*obligation →applications/<date>/<org>--inbound/entry.yaml; brief reports real count or DARK; idempotent re-run is a no-op.Related:
organvm/application-pipeline#91(educator/HigherEdJobs discovery inflow — same inflow shape). Companion PRs: #185,organvm/limen#1447.