agentleFS
CodesWhatCodesWhat/careerrat/.agents/skills/ingest-mail/SKILL.md

ingest-mail

Skill4 starsChanged 11 days agoMITView on GitHub

Read job-search email updates from Apple Mail on macOS or, with the mail_access capability (on by default), Gmail/Outlook webmail through the session browser; fold recruiter replies and status changes into CareerRat tracker communication state. User-initiated, local-only, read-only.

The file

Show its rules and text

We split this skill into the rules it gives an agent

26 rules. About 0 are common, 9 are distinctive, and 17 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈9

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 17

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If using Apple Mail and the output is not Darwin, stop the Apple Mail path and offer either Gmail/Outlook webmail (mail_access) or pasted …

    line 29
  2. Distinctive:

    If using Gmail/Outlook webmail, continue to STEP 1W below.

    line 30
  3. Very specific:

    If the result is false, tell the user to open Apple Mail and re-run.

    line 40
  4. Very specific:

    If the first osascript call fails with a permission error ("Terminal wants to control System Events"), instruct the user to approve …

    line 43
  5. Very specific:

    Use this path only for gmail or outlook.

    line 49
  6. Very specific:

    If mail_access is not allowed, tell the operator it is turned off for that provider, give the command that turns it on, and skip the …

    line 64
  7. Distinctive:

    If allowed, use the session browser.

    line 72
  8. Distinctive:

    Never send, delete, reply, archive, or intentionally mark messages read.

    line 74
  9. Very specific:

    Read workspace/tracker.json.

    line 81
  10. Very specific:

    If no such entry exists or lastRunAt is absent, default the window to (current date) - 14 * days.

    line 86
  11. Distinctive:

    Run the metadata sweep using the resolved window from Step 2.

    line 96
  12. Distinctive:

    If the osascript call fails with a permission error, proceed to Step 4 (SQLite fallback) and explain which toggle the user must flip in …

    line 124
  13. Distinctive:

    Use the session browser to search Gmail/Outlook for messages newer than the resolved threshold and likely related to recruiting / …

    line 130
  14. Very specific:

    Read workspace/tracker.json (already loaded in Step 2).

    line 155
  15. Distinctive:

    If the body is long (over ~1,500 characters), ensure the directory exists then save the body:

    line 204
  16. Very specific:

    Write the body to workspace/comms/<company>-<threadId>.md and store the path in the message record as artifactPath.

    line 211
  17. Very specific:

    Never write message body content anywhere outside workspace/.

    line 215
  18. Very specific:

    Mode detection: run careerrat data status.

    line 220
  19. Very specific:

    If the message changes parent communication state (status, lastInboundAt, nextAction, nextActionDue, draft, outcomeSignalUnresolved), patch …

    line 242
  20. Very specific:

    Add a richer inbound-thread event only if needed with careerrat data activity append --data '<activity JSON>'.

    line 260
  21. Very specific:

    Update the parent communications[] record fields: status, lastInboundAt, nextAction, nextActionDue.

    line 288
  22. Very specific:

    Stale draft clearance: If the matched comm thread has a non-null comm.draft, set comm.draft = null — an inbound reply proves the outbound …

    line 292
  23. Distinctive:

    If the user states a new gate mid-flow (e.g., "never follow up with this company", "add them to excluded"), apply the write-back rule using …

    line 299
  24. Very specific:

    Write directly and report: when the change affects only the single application in scope (e.g., set nextAction: none on one record) or is a …

    line 303
  25. Very specific:

    Confirm first: when the change affects more than one application or has broad downstream effects (e.g., a comp floor change that re-screens …

    line 306
  26. Distinctive:

    Use one of these source objects:

    line 350

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

ingest-mail

Runs under AGENTS.md. These contracts bind without being restated here: Privacy Invariant (current_base never outbound), Honesty Firewall, Placeholder/Bracket Ban, Gate Write-back, Domain-Neutral Rule, Browser Automation Contract, Activity Pulse logging, Tracker verify+snapshot, and Sent-Clears-Draft. Inline reminders at point-of-use are intentional; standalone restatements point back to the relevant AGENTS.md section. Bare candidate/, workspace/, config/, and .internal/ paths below are symbolic; resolve them per AGENTS.md's Path Resolution rule.

STEP 0: SOURCE + CAPABILITY CHECK

Confirm the user explicitly requested this sync. Never run this skill unprompted or on a schedule.

Determine the requested mail source:

  • Apple Mail — default when the user asks for "mail" on macOS and does not name Gmail/Outlook.
  • Gmail / Outlook webmail — use only when the user asks for Gmail, Outlook, webmail, or the machine is not macOS and the user wants a browser-backed mail sync.

Run uname.

  • If using Apple Mail and the output is not Darwin, stop the Apple Mail path and offer either Gmail/Outlook webmail (mail_access) or pasted email capture via email-comms.
  • If using Gmail/Outlook webmail, continue to STEP 1W below.

STEP 1 — MAIL PROCESS PRE-FLIGHT

Run:

osascript -e 'tell application "System Events" to (name of processes) contains "Mail"'

If the result is false, tell the user to open Apple Mail and re-run. Do not attempt to launch Mail automatically.

If the first osascript call fails with a permission error ("Terminal wants to control System Events"), instruct the user to approve Automation access in System Settings → Privacy & Security → Automation.

STEP 1W: WEBMAIL CAPABILITY CHECK (GMAIL / OUTLOOK ONLY)

Use this path only for gmail or outlook. It is separate from Apple Mail and separate from in-platform messaging.

Run:

careerrat automation status --json

Inspect capabilities.mail_access for the requested platform. allowed: true means neither the mail_access capability switch nor that provider's switch is turned off. This is the verdict from mayRun() in src/core/automation/consent.mjs; never re-derive it in prose. Webmail access is on by default.

If mail_access is not allowed, tell the operator it is turned off for that provider, give the command that turns it on, and skip the webmail path (do not open a browser). If readError is set, say the automation settings file can't be read and name it.

  • careerrat automation enable mail_access --write
  • careerrat automation enable mail_access <gmail|outlook> --write

If allowed, use the session browser. Read only job-search/recruiting messages in the resolved window from STEP 2; do not browse personal mail outside that scope. Never send, delete, reply, archive, or intentionally mark messages read. Halt on a webmail login wall, mail 2FA prompt, captcha, or unexpected interstitial. Use classifyMailAccessBlocker() from src/core/automation/mail-access.mjs when classifying page text.

STEP 2 — LOAD WATERMARK + COMPUTE WINDOW

Read workspace/tracker.json. Find the entry in sources[] where id is "apple-mail" for Apple Mail, "gmail-webmail" for Gmail, or "outlook-webmail" for Outlook. If the entry exists and has a lastRunAt value, use that ISO-8601 date-time as the date received > threshold for the AppleScript query or webmail search window. If no such entry exists or lastRunAt is absent, default the window to (current date) - 14 * days.

Log the resolved window to the user before proceeding: Scanning <source> for messages received after <resolved-threshold>.

STEP 3 — READ MAIL METADATA (APPLESCRIPT PRIMARY)

Apple Mail path:

Run the metadata sweep using the resolved window from Step 2. Capture per message: date received, sender address, subject, message ID. Do NOT pull body content yet.

Compute the threshold as a shell variable first, then pass it into the AppleScript so the snippet is self-contained and nothing is left as a placeholder comment:

# Compute epoch seconds for the resolved threshold (from Step 2).
# Use the lastRunAt ISO date if present, or fall back to 14 days ago.
THRESHOLD_EPOCH=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${LAST_RUN_AT:-}" "+%s" 2>/dev/null \
  || date -v-14d "+%s")
THRESHOLD_HRS=$(( ( $(date "+%s") - THRESHOLD_EPOCH ) / 3600 ))

osascript -e "tell application \"Mail\"
  set out to \"\"
  set cutoff to (current date) - (${THRESHOLD_HRS} * hours)
  repeat with m in (messages of inbox whose date received > cutoff)
    set out to out & (date received of m as string) & \"\t\" & (sender of m) & \"\t\" & (subject of m) & \"\n\"
  end repeat
  return out
end tell"

LAST_RUN_AT is the raw lastRunAt string read from sources[id=apple-mail] in Step 2 (empty string if absent; the || date -v-14d branch then activates).

If the osascript call fails with a permission error, proceed to Step 4 (SQLite fallback) and explain which toggle the user must flip in System Settings → Privacy & Security → Full Disk Access.

Webmail path:

Use the session browser to search Gmail/Outlook for messages newer than the resolved threshold and likely related to recruiting / applications. Capture per message: date received, sender address, subject, and a stable provider message identifier or URL. Do NOT pull body content yet. Keep the visible search scoped to the window and job-search terms; do not scroll/browse the broader inbox.

STEP 4 — SQLITE FALLBACK (if Step 3 fails or permission denied)

Apple Mail only. The webmail path has no SQLite fallback; if the session browser hits a login wall, mail 2FA prompt, captcha, or provider error, halt and ask the user to paste the relevant email content or clear the blocker manually.

First probe the schema to confirm column names before querying:

sqlite3 ~/Library/Mail/V*/MailData/"Envelope Index" .schema 2>/dev/null | grep -i date

Once columns are confirmed, query the messages table for messages in the same window. Treat the result as best-effort — surface any schema mismatch to the user and do not guess column names. Bodies in this path live in per-account .emlx files under ~/Library/Mail/; reference them by message ID.

STEP 5 — MATCH TO TRACKER

Read workspace/tracker.json (already loaded in Step 2). Also read candidate targeting config through the shared DB-first accessor for excluded_companies, keep_signals, and any additional domain/company signals not yet in the tracker (e.g., a company in excluded_companies should be skipped at match time).

For each message from Step 3 or Step 4, attempt matching in priority order:

  1. Sender email domain vs. application domain field.
  2. Company name in subject or body vs. application company field.
  3. Subject vs. existing communications[].subject on any application.

Apply a subject-line pre-filter: skip messages whose subjects match obvious non-job patterns (e.g., contain "invoice", "receipt", "billing", "order confirmation") — list any filtered-out messages so the user can see what was skipped.

Collect all unmatched and ambiguous messages. Print them as a numbered table (row: index | sender | subject | date received) and ask the user to supply the application ID for each row or type skip. Do not proceed to Step 6 for any message until it is either matched or explicitly skipped.

STEP 6 — PULL BODIES FOR MATCHED MESSAGES ONLY

[DELEGATE: subagent] Body pulls for matched messages are independent. The Apple Mail / SQLite path is local and batches across subagents; the webmail (mail_access) path drives the session browser and stays sequential (one-browser rule). Each subagent returns the captured body for its message; the orchestrator folds them into communications[] and remains the only tracker writer. The STEP 0/1W capability check already cleared on the orchestrator. Degrade to inline sequential pulls with no subagent primitive. See the Delegation Contract in AGENTS.md.

For each confirmed match from Step 5, pull the full message body only for that matched message.

Apple Mail path:

osascript -e 'tell application "Mail" to get content of message id "<message-id>" of inbox'

Webmail path:

Open only the confirmed matched message in the session browser and read its visible body. Do not open unrelated messages. Do not send, delete, reply, archive, or intentionally mark messages read. If the provider blocks access with a login wall, mail 2FA prompt, captcha, or unexpected interstitial, halt and ask the user to paste the message body or clear the blocker manually.

If the body is long (over ~1,500 characters), ensure the directory exists then save the body:

mkdir -p workspace/comms

Write the body to workspace/comms/<company>-<threadId>.md and store the path in the message record as artifactPath. Short bodies may be stored inline in summary.

Never write message body content anywhere outside workspace/. Never transmit it outbound.

STEP 7 — CAPTURE TO TRACKER

Mode detection: run careerrat data status. Exit 0 → DB workspace — use the careerrat data <verb> commands below (Data Write Contract, AGENTS.md). Nonzero exit → legacy workspace (no DB yet) — use the direct workspace/tracker.json instructions further down in this step.

DB workspace: for each confirmed-matched message:

  1. Save any long raw body to workspace/comms/<thread-id>.md as before. This artifact file write is local-only and is unaffected by DB vs legacy mode.
  2. Find or create the parent communication row by patching the full communication object and persisting it:

    careerrat data comm upsert --data '<full communication row JSON>'
  3. Append the inbound message:

    careerrat data comm append-message <comm-id> --data '<message JSON>'
  4. If the message changes parent communication state (status, lastInboundAt, nextAction, nextActionDue, draft, outcomeSignalUnresolved), patch the full communication row from the current exported tracker state and persist it again:

    careerrat data comm upsert --data '<patched full communication row JSON>'
  5. For outcome-signal records, follow STEP 8: write only the message append and lastInboundAt first, hand the outcome to track-outcomes, and let track-outcomes settle application state. If track-outcomes fails to settle the communication state, persist the fallback communication row with status: "waiting", nextAction: null, nextActionDue: null, draft: null, and outcomeSignalUnresolved: true through careerrat data comm upsert.

The communication verbs bump/export/log Activity Pulse events automatically. Add a richer inbound-thread event only if needed with careerrat data activity append --data '<activity JSON>'; do not hand-edit activity.jsonl in DB mode. The message shape, CTA-clearance rules, privacy invariant, and gate write-back rules below apply in both modes.

Legacy workspace (no DB): for each confirmed-matched message, update workspace/tracker.json directly:

Find or create the communications[] record with fields: id, applicationId, company, role, channel: "email", status, summary.

Append to communications[].messages[] a message object with this exact shape (per the locked schema in foundations-spec §4):

{
  "id": "msg-<appId>-<n>",
  "direction": "inbound",
  "at": "<ISO-8601>",
  "from": "<sender-address>",
  "to": ["<recipient-address>"],
  "subject": "<subject>",
  "summary": "<concise gist — do NOT include current_base>",
  "artifactPath": "<workspace/comms/... or empty string>"
}

Update the parent communications[] record fields: status, lastInboundAt, nextAction, nextActionDue.

  • CTA clearance — expected event arrived: If the inbound message is the event that was being waited on (the recruiter replied, a confirmation email arrived, or a form-submission acknowledgment was received), set nextActionDue = null and rewrite nextAction to the next pending action or clear it entirely. Do not leave a satisfied due date in place.
  • Stale draft clearance: If the matched comm thread has a non-null comm.draft, set comm.draft = null — an inbound reply proves the outbound was sent; the draft is stale and must not surface as a CTA.
  • Acknowledgment / confirmation emails: When the inbound message subject or body matches acknowledgment patterns ("application received", "submission confirmed", "we have received", "thank you for submitting", "form received"), treat it as a CTA-clearing event: set nextActionDue = null, set nextAction to null or "awaiting-response", and set status = "waiting". Do not set a new future due date for these messages.

Privacy invariant: summary and artifactPath content must never echo profile.compensation.current_base. Outbound-safe comp fields are expected_base, target_base, and minimum_base only.

If the user states a new gate mid-flow (e.g., "never follow up with this company", "add them to excluded"), apply the write-back rule using this discriminator:

  • Write directly and report when the change affects only the single application in scope (e.g., set nextAction: none on one record) or is a low-blast-radius supported gate (careerrat gate cut-signal / keep-signal).
  • Confirm first when the change affects more than one application or has broad downstream effects (e.g., a comp floor change that re-screens the whole board, adding a wildcard domain exclusion, or resetting an aggregated field like minimum_base in profile.yml).

Route each gate type through its owning command:

  • Company exclusion → careerrat gate exclude-company "<Company>" --write
  • Comp floor change → careerrat gate comp-floor <N> --write --confirm
  • Per-application follow-up pause → workspace/tracker.json (that record's nextAction/nextActionDue)

Then log each inbound thread to the Activity Pulse feed (the dashboard's live timeline — see Activity Pulse in AGENTS.md). One event per inbound thread captured, actor world:

careerrat activity append --type message --actor world \
  --title "<Company> replied" --summary "<subject or one-line summary>" \
  --company "<Company>" --app-id <application id> --write

STEP 8 — CLASSIFY OUTCOME SIGNALS

For each captured message, scan the summary for outcome signals:

  • Rejection language → outcome label rejected
  • Offer language → outcome label offer
  • Interview advance / scheduling → outcome label interview-advance

For any message carrying one of these signals, hand off to track-outcomes with the applicationId and the outcome label before finalizing communication state for that record. For outcome-signal records, skip the Step 7 status/nextAction/nextActionDue write for that record — write only the messages[] append and lastInboundAt. Pass the full comm state write (status, nextAction, nextActionDue) to track-outcomes; a partial Step 7 write would surface a ghost CTA between the two writes.

If track-outcomes fails or returns without writing status, nextAction, and nextActionDue for a record, fall back to writing that record's status: "waiting", nextAction: null, nextActionDue: null directly (in a follow-up STEP 7 write), and set outcomeSignalUnresolved: true on the record so the next ingest-mail sweep surfaces it for re-resolution. Do not leave the record in a half-written state (messages[]/lastInboundAt set but comm state never resolved) — that is the ghost-CTA condition this fallback prevents.

STEP 9 — WRITE WATERMARK

Run this step unconditionally — even if STEP 8 (the track-outcomes hand-off) returned early or failed, the sweep happened, so the watermark must still be written. Write lastRunAt to the matching sources[] entry and also set meta.lastSweepAt to the same timestamp. Do not set meta.lastUpdatedAt here — that field drives the dashboard's "last updated" pill and must only be touched when this sweep actually mutated tracker data (a status, nextAction, or nextActionDue change). A sweep that found nothing or had no outcome-signal hand-offs must not reset that pill.

DB workspace:

careerrat data source watermark --at <ISO-8601 timestamp of now> --data '<source JSON>'

Use one of these source objects:

Apple Mail:

{ "id": "apple-mail", "kind": "apple-mail", "name": "Apple Mail", "lastRunAt": "<ISO-8601 timestamp of now>" }

Gmail webmail:

{ "id": "gmail-webmail", "kind": "webmail", "name": "Gmail webmail", "lastRunAt": "<ISO-8601 timestamp of now>" }

Outlook webmail:

{ "id": "outlook-webmail", "kind": "webmail", "name": "Outlook webmail", "lastRunAt": "<ISO-8601 timestamp of now>" }

source watermark updates sources[] and meta.lastSweepAt, exports tracker files, and intentionally does not bump meta.version, meta.lastUpdatedAt, or Activity Pulse.

Legacy workspace (no DB): upsert the selected source object into tracker.json#sources[]. If an entry with the selected source id already exists, update only lastRunAt. If no such entry exists, insert the full object. Write the change directly to workspace/tracker.json.

STEP 10 — VERIFY + SNAPSHOT

DB workspace:

careerrat data verify
careerrat tracker --verify
careerrat tracker --followups
careerrat tracker --summary

Both verify commands must exit 0. If either fails, do not proceed — show the validation errors and ask the user how to resolve them. Confirm new threads appear in the follow-ups surface and message counts incremented for the matched applications. Run careerrat tracker afterward only when a recovery snapshot is useful.

Legacy workspace (no DB):

careerrat tracker --verify
careerrat tracker --followups
careerrat tracker --summary
careerrat tracker

careerrat tracker --verify must exit 0. If it fails, do not proceed — show the validation errors and ask the user how to resolve them. Confirm new threads appear in the follow-ups surface and message counts incremented. The final command creates a deduplicated recovery checkpoint under workspace/.snapshots/.

Print a final ingest summary. The counts in this summary are agent-composed from the running tallies kept during Steps 5–8 — they are NOT parsed from CLI output (the CLI commands above confirm schema validity and rendering only, and do not emit these counts):

Ingest complete:
  Messages scanned:    N
  Matched:             M  (to K applications)
  Unmatched/skipped:   U  (listed above)
  Handed to track-outcomes: P  (application IDs: ...)
  Watermark updated:   <lastRunAt>

In-app and terminal paths

CareerRat's in-app path reads Apple Mail on macOS and, with the separate mail_access capability, Gmail and Outlook through its owned browser workflow. It captures relevant messages into communications[], stores long bodies under workspace/comms/, and advances the matching source watermark. A terminal or external-agent run still follows STEP 0 onward exactly as written.

  • Requesting a mail check (mail.sync-request). "Check my email" or "any new recruiter emails?" offers a "Check for new mail" chip. The handler reports each source's real state: Apple Mail (listed only when the app runs on macOS, since it reads the local Mail app and needs no webmail capability), plus Gmail and Outlook with their mayRun on/off state for the mail_access capability. Each row shows the last sweep time from that source's sources[] watermark ("Never checked" before a first run), and the card adds how many email threads are currently waiting on a reply. On a non-macOS host with both webmail platforms off, the request says it is turned off and offers a "Turn it on" action instead. Otherwise, choosing the action runs every allowed source and returns the captured count or a retry blocker in the same Ask thread.
  • Execution and recovery. The app performs the allowed read when the chip is chosen. Login, captcha, 2FA, and turned-off blockers return a visible retry state without advancing the watermark. The terminal path keeps the detailed STEP 5 match/skip review and the track-outcomes handoff. Recording a single recruiter email the candidate reports themselves stays with email-comms (communication.capture-inbound and its sibling intents).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.