Files
jobyssey/.claude/commands/gather-recent-context.md
T

5.9 KiB

/gather-recent-context - Sync Job-Email Context into Twenty CRM Notes

You are scanning the user's Fastmail inbox for conversations about active job loops (interview invites, assessments, scheduling, offers, recruiter follow-ups) and syncing each conversation's context onto its matching Opportunity in Twenty CRM as a Note. The mailbox is read-only to this command; Twenty receives classified summaries, never raw email bodies or attachments.

Two halves, cleanly split:

  • You (this command) own the judgment: finding relevant threads via the Fastmail MCP tools and classifying each one into a structured signal.
  • tools/gather_recent_context.py owns everything deterministic: matching threads to active opportunities, building the note markdown, deduplicating by thread id (job_scraper/context_sync.json), and applying via Twenty REST (TWENTY_API_KEY) - or emitting a plan you apply through the Twenty MCP session.

Follow these steps in order.


Step 0: Parse Input

$ARGUMENTS may contain:

  • Nothing → default lookback: 14 days
  • since <YYYY-MM-DD> → override the lookback start
  • A company name → scope the scan to that one company's threads

Step 1: Preflight (both MCP servers)

Silently optional, same bar as /sync-twenty: a missing prerequisite ends in one clear message and a clean exit.

  1. Fastmail MCP must be available (tools like search_email, read_thread). If not, say so and stop.
  2. Twenty must be reachable one way or the other: TWENTY_API_KEY in the environment (script applies via REST) or the Twenty MCP session (you apply the plan). Neither → one line and stop.
  3. Never initiate an OAuth flow from this command.

Step 2: Find Candidate Threads (Fastmail MCP)

  1. Build the search set from the CRM's active loops: fetch opportunities with stage in RANKED / DRAFTED / APPLIED / INTERVIEW / OFFER (via execute_tool → find_many_opportunities, selecting id, name, stage, companyId) and the company names via find_many_companies (id, name). Opportunity names are role-only; the employer comes from the companyId relation.
  2. search_email with the lookback from Step 0 - query for recruiting/job signals (subject or sender mentioning the tracked companies, plus generic signals like "interview", "assessment", "application", "offer", "recruiter"). Prefer several narrow searches over one broad one; skip newsletters and job-board digests (LinkedIn/Indeed notification blasts are noise, not conversations).
  3. read_thread on each candidate to see the whole conversation before classifying.

Step 3: Classify into the Contract

For each relevant thread, produce one entry - this is the exact input contract of the script:

{"threads": [{
  "thread_id": "<Fastmail thread id>",
  "company": "<employer name as the email identifies it>",
  "role": "<role title if the thread names one, else null>",
  "date": "YYYY-MM-DD",
  "from": "<counterparty address>",
  "subject": "<subject line>",
  "summary": "<1-3 sentences: what happened and what's next>",
  "signal": "interview | assessment | offer | rejection | scheduling | other"
}]}

Rules: the summary is your words, not a quote; no email bodies, links, or attachments are copied; a thread you cannot confidently tie to a job conversation is dropped, not guessed. Write the JSON to a temp file (e.g. /tmp/gather-emails.json).


Step 4: Run the Script

python3 tools/gather_recent_context.py --emails /tmp/gather-emails.json          # plan only
python3 tools/gather_recent_context.py --emails /tmp/gather-emails.json --apply  # with TWENTY_API_KEY

The script matches threads to active opportunities (normalized company name, role-token tiebreak), groups them into one note per opportunity titled Email Context — <Company> (YYYY-MM-DD), skips every thread id already in job_scraper/context_sync.json, and writes job_scraper/context_plan.json.

  • With --apply (REST key available): notes and note-targets are created and the dedup state is committed. Done - go to Step 6.
  • Without a key: the plan file is your work order - go to Step 5.

Step 5: Apply the Plan via Twenty MCP (keyless path)

For each entry in the plan's notes array:

  1. create_many_notes with {position: "last", title, bodyV2: {markdown}} (batches of ≤ 20).
  2. create_many_note_targets linking each returned note id to its opportunity_id via targetOpportunityId.
  3. After all notes land, commit the dedup state:
    python3 tools/gather_recent_context.py --commit-plan job_scraper/context_plan.json
    
    Never commit state for notes that failed to create - the uncommitted thread ids simply reappear next run.

Step 6: Report

## Recent Context Sync - YYYY-MM-DD

Scanned <N> threads (lookback: <D> days) · <M> classified as job conversations
Notes created: <K> (one per opportunity) · Unmatched: <U> · Already processed: <P>

| Opportunity | Signal(s) | Threads |
|-------------|-----------|---------|
| Backend Engineer — Acme | interview, scheduling | 2 |

List unmatched threads (company + subject) so the user can add the opportunity or correct the company name; they are not state-committed and will re-surface next run.


Important Rules

  1. Mailbox read-only. Never label, archive, delete, or send from this command.
  2. Summaries only. Raw email bodies, quoted text, links, and attachments never reach Twenty.
  3. Active loops only. Threads about companies with no active-stage opportunity go to unmatched, never to a closed loop's record.
  4. Idempotent by thread id. job_scraper/context_sync.json (personal state, gitignored) guarantees a re-run never duplicates a note.
  5. Notes are append-only context. This command creates new notes; it never edits or deletes existing ones - including the Fit Analysis notes owned by /sync-twenty.
  6. Never guess a match. An ambiguous thread is reported, not synced.