Skip to content

Farm inbound-lead mail into the pipeline on the beat (obligations via IMAP → opportunity_sync) #186

Description

@4444J99

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)

  1. Row schema: gmail_imap_sweep.classify() — emit id (= uid), reply_to, and a snippet on each row so obligations/opportunity_sync dedup works.
  2. 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.
  3. Beat wire (organvm/limen metabolize.sh via institutio/governance/sensors.yaml, derive-runner pattern — one registry entry): run the Gmail classify pass → obligations_buildopportunity_sync (application-pipeline). Add an audit/synced_to_pipeline.json receipt so farmed/unfarmed is queryable.
  4. 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).
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions