ingest-messages
Skill4 starsChanged 11 days agoMITView on GitHub
Read in-platform DMs from LinkedIn and Wellfound via the session browser and fold them into tracker communications[] (the browser analog of ingest-mail). User-initiated, local-only. Capability = messaging; platforms = linkedin, wellfound.
In this repository31 other files it gives its agents.
- AGENTS.mdAGENTS.md
- templates/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- templates/CLAUDE.mdCLAUDE.md
- answer-questionSkill
- apply-jobSkill
- calendar-syncSkill
- coach-gapsSkill
- company-healthSkill
- configureSkill
- discover-companiesSkill
- email-commsSkill
- evaluate-jobSkill
- ingest-mailSkill
- ingest-profileSkill
- intake-extractSkill
- interview-prepSkill
- optimize-linkedinSkill
- reevaluate-strategySkill
- relationship-sourcingSkill
- report-issueSkill
- research-boardsSkill
- research-companySkill
- research-compSkill
- resume-extractSkill
- schedule-meetingSkill
- search-jobsSkill
- setup-searchesSkill
- sync-statusSkill
- tailor-applicationSkill
- track-outcomesSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
31 rules. About 0 are common, 9 are distinctive, and 22 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.
22
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 31
If no platform shows allowed: true, tell the operator messaging is turned off, give the command that turns it on, and stop (do not open a …
- Very specific: line 43
Read workspace/tracker.json.
- Very specific: line 66
Before reading anything, snapshot or read the current page state.
- Distinctive: line 69
If you encounter a login wall, captcha, 2FA prompt, or any unexpected interstitial at any point, halt immediately and ask the user to …
- Distinctive: line 90
Do NOT pull full message bodies yet.
- Very specific: line 95
Read workspace/tracker.json (already loaded in STEP 1).
- Distinctive: line 122
Only escalate threads that PASS the positive test (look recruiting-related) yet cannot be auto-matched to an existing application — e.g.
- Very specific: line 139
Ask the user to supply the application ID for each numbered row or type skip.
- Distinctive: line 140
Do not proceed to STEP 5 for any thread until it is either matched or explicitly skipped.
- Distinctive: line 157
If the full thread text is long (over ~1,500 characters), ensure the directory exists first then save the body:
- Very specific: line 164
Write the thread to workspace/comms/<company-slug>-<threadId>.md and record the path as artifactPath.
- Very specific: line 167
Never write message body content anywhere outside workspace/.
- Very specific: line 173
Mode detection: run careerrat data status.
- Very specific: line 195
If the thread changes parent communication state (status, lastInboundAt, nextAction, nextActionDue, draft), patch the full communication …
- Very specific: line 209
Add a richer inbound-thread event only if needed with careerrat data activity append --data '<activity JSON>'.
- Very specific: line 247
Update the parent communications[] record fields as follows — all in one write (partial writes leave ghost CTAs):
- Very specific: line 250
status transition (required): if the last captured message in this thread is direction: inbound, set status → needs-reply.
- Very specific: line 253
nextActionDue null-first rule: if the matched comm record previously had status: waiting (candidate was awaiting a recruiter reply) and an …
- Very specific: line 259
draft-clearing rule: if the existing comm record has comm.draft != null, set comm.draft = null in this same write and note "draft cleared — …
- Distinctive: line 263
Outcome-signal deferral: before writing the comm status above, peek at the outcome-signal classification (STEP 7).
- Distinctive: line 275
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 …
- Very specific: line 278
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 …
- Distinctive: line 281
Confirm first: when the change affects more than one application or has broad downstream effects (e.g., a comp floor change, adding a …
- Very specific: line 342
When both platforms were polled, pass a JSON array containing both objects.
- Very specific: line 350
Write the changes directly to workspace/tracker.json.
- Very specific: line 403
On by default, with an off-switch: Only poll platforms where careerrat automation status --json shows messaging allowed: true for that …
- Very specific: line 413
Honor the Browser Automation Contract: Follow AGENTS.md ## Browser Automation Contract in full.
- Distinctive: line 418
Never run on a schedule or unattended: Always user-initiated with the agent in the loop.
- Very specific: line 439
Outcome writes belong to
track-outcomeson this path: Never write status directly for records carrying rejection / offer / … - Very specific: line 444
Privacy invariant: Never write current_base into any communications field, message summary, or workspace/comms/ artifact.
- Very specific: line 448
Domain-neutral: No hardcoded companies, roles, or candidate-specific values in this skill.
≈ “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.
Files
ingest-messages
Runs under AGENTS.md. These contracts bind without being restated here: Privacy Invariant (
current_basenever 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. Barecandidate/,workspace/,config/, and.internal/paths below are symbolic; resolve them per AGENTS.md's Path Resolution rule.
STEP 0: Capability check
Run:
careerrat automation status --jsonInspect the capabilities.messaging entry. The applicable platforms are linkedin
and wellfound. For each platform, allowed: true means neither the messaging
capability switch nor that platform's switch is turned off. This is the verdict from
mayRun() in src/core/automation/consent.mjs; never re-derive it in prose.
Messaging is on by default.
If no platform shows allowed: true, tell the operator messaging is turned off,
give the command that turns it on, and stop (do not open a browser). If readError
is set, say the automation settings file can't be read and name it. Skip any single
platform that is off and note it.
- Capability:
careerrat automation enable messaging --write - One platform:
careerrat automation enable messaging <platform> --write
This skill is always user-initiated. Never run it unprompted or on a schedule.
STEP 1 — LOAD WATERMARK + COMPUTE WINDOW
Read workspace/tracker.json. For each platform that showed allowed: true in STEP 0,
find the entry in sources[] whose id is "linkedin-messages" or
"wellfound-messages" respectively. If the entry exists and has a lastRunAt value,
use that ISO-8601 date-time as the lookback threshold for that platform. If no such
entry exists or lastRunAt is absent, default the window to 14 days back.
Log the resolved window per platform before proceeding:
Scanning LinkedIn messages received after <resolved-threshold>.
Scanning Wellfound messages received after <resolved-threshold>.Proceed only with platforms that are both allowed and in scope. Skip any platform
that is not allowed: true and note the reason.
STEP 2 — OPEN THE MESSAGES INBOX IN THE SESSION BROWSER
For each allowed platform, navigate to that platform's messages or DM inbox URL in
the session browser. The session browser is Layer 3 per docs/BROWSER.md. Keep the
provider on auto unless the user explicitly changes it; CareerRat resolves the current
session browser and reuses its signed-in session.
Before reading anything, snapshot or read the current page state. Drive the live DOM
turn-by-turn. Never rely on hardcoded selectors — the same model as apply-job.
If you encounter a login wall, captcha, 2FA prompt, or any unexpected interstitial at any point, halt immediately and ask the user to complete it. Do not attempt to bypass or automate around any auth challenge.
Save any screenshot or scraped page fragment under workspace/ only.
STEP 3 — READ MESSAGE METADATA
Within the inbox, scroll to surface threads active within the window resolved in STEP 1. For each visible thread, capture — without opening the thread body:
- Correspondent name and/or handle
- Correspondent headline / title and any thread badge (recruiter / talent / hiring / InMail / Jobs / Sponsored / Promoted) when surfaced in the list — this is the recruiting-relevance signal consumed in STEP 4
- Thread subject or first-message snippet
- Most-recent message timestamp
- Thread ID or URL (to return for full body if matched)
- Direction of the most-recent message (inbound / outbound — infer from position or sender name vs. the candidate's own account name)
Do NOT pull full message bodies yet. Collect only enough metadata to classify recruiting-relevance and match threads to tracker applications in STEP 4.
STEP 4 — MATCH TO TRACKER
Read workspace/tracker.json (already loaded in STEP 1). Also read
candidate targeting config through the shared DB-first accessor for
excluded_companies and any company/domain signals relevant at match time.
For each thread from STEP 3, attempt matching in priority order:
- Correspondent name vs.
communications[].messages[].from/messages[].toon any existing application record. - Company name extracted from the thread subject or snippet vs. application
companyfield. - Thread subject vs. existing
communications[].subjecton any application.
Apply a two-sided relevance classification BEFORE escalating anything to the user — the human gate is for "real recruiter, which application?", never for "is this even a job thread?":
Negative pre-filter — auto-skip threads whose subject / snippet / sender match obvious non-recruiting patterns: marketing, event invite, newsletter, sponsored / promoted, or a clearly personal or networking exchange with no job signal.
Positive relevance test — for every remaining unmatched thread, decide whether it is plausibly recruiting-related from the STEP 3 signals: a recruiter / talent / hiring sender headline, an InMail or Jobs badge, or role / opportunity / interview / screening / offer / JD language in the snippet. A thread with NONE of these signals and no tracker match is auto-skipped as non-recruiting — do NOT put it in the user table. (A 1st-degree connection sending a casual reply with no job content is the canonical auto-skip; the agent classifies it, it does not ask the user about it.)
Only escalate threads that PASS the positive test (look recruiting-related) yet cannot be auto-matched to an existing application — e.g. a recruiter for a company not yet tracked, or an ambiguous match across two applications. When relevance is genuinely unclear from metadata alone, ONE lightweight body peek is permitted to classify (read-only, for classification only, never ingestion); if still unclear, escalate.
Log every auto-skip (both negative-filter and non-recruiting) so the user sees what was dropped and why. Then collect only the escalated unmatched / ambiguous threads into a numbered table:
# | Platform | Correspondent | Subject / snippet | Date
---|-----------|----------------------|----------------------------------------|----------
1 | linkedin | Jane Smith | "Following up on your application..." | 2026-06-10
2 | wellfound | Alex Rivera | "Intro from Acme Engineering team" | 2026-06-09Ask the user to supply the application ID for each numbered row or type skip.
Do not proceed to STEP 5 for any thread until it is either matched or explicitly
skipped.
STEP 5 — PULL BODIES FOR MATCHED THREADS ONLY
[DELEGATE: subagent — sequential] Thread body-pulls drive the session browser, so they
run one at a time per the one-browser rule — delegation here buys per-thread context
isolation, not parallelism. Each subagent reads its matched thread and returns the
chronological messages; the orchestrator captures them to communications[] (sole tracker
writer). The STEP 0 capability check already ran on the orchestrator. See the Delegation
Contract in AGENTS.md.
For each confirmed match from STEP 4, navigate to that thread in the session browser and read the full message bodies within the lookback window. Snapshot the page before reading. Capture all individual messages in the thread in chronological order: direction, timestamp, sender name, and body text.
If the full thread text is long (over ~1,500 characters), ensure the directory exists first then save the body:
mkdir -p workspace/commsWrite the thread to workspace/comms/<company-slug>-<threadId>.md and record the
path as artifactPath. Short bodies may be stored inline in summary.
Never write message body content anywhere outside workspace/. Never transmit it
outbound. Never send any message, reply, or reaction — this skill is read-only on
the platform.
STEP 6 — 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 thread:
- Save any long raw body to
workspace/comms/<thread-id>.mdas before. This artifact file write is local-only and is unaffected by DB vs legacy mode. 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>'Append each newly captured message in chronological order:
careerrat data comm append-message <comm-id> --data '<message JSON>'If the thread changes parent communication state (
status,lastInboundAt,nextAction,nextActionDue,draft), 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>'- For outcome-signal threads, follow STEP 7: defer intermediate
needs-replywrites, hand the outcome totrack-outcomes, then persist only the resolved communication state aftertrack-outcomesreturns. Do not hand-edittracker.jsonoractivity.jsonlin DB mode.
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>'. The message shape,
status-transition rules, privacy invariant, and gate write-back rules below
apply in both modes.
Legacy workspace (no DB): for each confirmed-matched thread, update
workspace/tracker.json directly.
Find or create the communications[] record with fields:
id, applicationId, company, role, status, summary.
Set channel based on platform:
- LinkedIn threads →
channel: "linkedin" - Wellfound threads →
channel: "portal"
Both enum values exist in config/tracker.schema.json; no schema change is needed.
For each individual message in the thread (within the lookback window), 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-name-or-handle>",
"to": ["<recipient-name-or-handle>"],
"subject": "<thread subject or first-line snippet>",
"summary": "<concise gist — do NOT include current_base>",
"artifactPath": "<workspace/comms/... or empty string>"
}For outbound messages in the thread (sent by the candidate), set "direction":
"outbound".
Update the parent communications[] record fields as follows — all in one write
(partial writes leave ghost CTAs):
- status transition (required): if the last captured message in this thread is
direction: inbound, setstatus → needs-reply; if the last captured message isdirection: outboundand no inbound message follows it, setstatus → waiting. - nextActionDue null-first rule: if the matched comm record previously had
status: waiting(candidate was awaiting a recruiter reply) and an inbound message now arrives, setnextActionDue → nullfirst (the expected event has occurred), then set a newnextActionDuefor the candidate's reply window (72 h from the inbound message timestamp, unless the message implies urgency — e.g. "let me know by EOD" → same-day). - draft-clearing rule: if the existing comm record has
comm.draft != null, setcomm.draft = nullin this same write and note"draft cleared — superseded by recruiter reply". This closes the ghost "Ready to send" panel that would otherwise persist after a new inbound message arrives on that thread. - Outcome-signal deferral: before writing the comm status above, peek at the
outcome-signal classification (STEP 7). If this thread will be handed to
track-outcomes(rejected / offer / interview-advance), do NOT write an intermediateneeds-replystatus — hold the comm write, complete thetrack-outcomeshand-off, then write comm state once as a single atomic update (status, nextActionDue, comm.draft — all in one pass) aftertrack-outcomesreturns the resolved state. This prevents a ghost CTA between STEP 6 and STEP 7 completing.
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: noneon 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, adding a wildcard domain exclusion).
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'snextAction/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> messaged" --summary "<one-line summary>" \
--company "<Company>" --app-id <application id> --writeSTEP 7 — CLASSIFY OUTCOME SIGNALS
For each captured thread, scan the summaries for outcome signals:
- Rejection language → outcome label
rejected - Offer language → outcome label
offer - Interview advance / scheduling request → outcome label
interview-advance
For any thread carrying one of these signals, before handing off: set
comm.draft = null on that record if a draft exists (draft is stale once an outcome
is known). Then hand off to track-outcomes with the applicationId and the outcome
label. Do not write status for those records before track-outcomes returns — wait
for the resolved state, then write comm and app in one atomic pass (status,
nextActionDue → null, comm.draft → null, messages[] append). The STEP 6 comm write
is deferred for outcome-carrying threads; track-outcomes owns the outcome write
and the final comm/app state is settled in that single pass.
Reply drafts are NOT this skill's job. If the user wants to reply to a DM, hand
off to email-comms — it is channel-aware for linkedin and portal and will
draft the response.
STEP 8 — WRITE WATERMARK
For each platform successfully polled, write the source watermark.
DB workspace:
careerrat data source watermark --at <ISO-8601 timestamp of now> --data '<source JSON or JSON array>'LinkedIn:
{ "id": "linkedin-messages", "kind": "linkedin-messages", "name": "LinkedIn Messages", "lastRunAt": "<ISO-8601 timestamp of now>" }Wellfound:
{ "id": "wellfound-messages", "kind": "wellfound-messages", "name": "Wellfound Messages", "lastRunAt": "<ISO-8601 timestamp of now>" }When both platforms were polled, pass a JSON array containing both objects.
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 same object(s) into
tracker.json#sources[]. If an entry with the matching id already exists,
update only lastRunAt. If no such entry exists, insert the full object.
Write the changes directly to workspace/tracker.json.
STEP 9 — VERIFY + SNAPSHOT
DB workspace:
careerrat data verify
careerrat tracker --verify
careerrat tracker --followups
careerrat tracker --summaryBoth 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 trackercareerrat 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 4–7 — they are NOT parsed from CLI output (the CLI commands above confirm schema validity and snapshotting only, and do not emit these counts):
Ingest complete:
Platforms polled: N (<platform list>)
Threads scanned: T (linkedin: A, wellfound: B)
Auto-skipped (marketing/non-recruiting): S (listed above, with reasons)
Matched: M (to K applications)
Escalated unmatched: U (listed above — awaiting your match/skip)
Handed to track-outcomes: P (application IDs: ...)
Watermarks updated: <linkedin lastRunAt>, <wellfound lastRunAt>RULES
- On by default, with an off-switch. Only poll platforms where
careerrat automation status --jsonshowsmessagingallowed: truefor that platform. Theallowedfield is themayRun()verdict fromsrc/core/automation/consent.mjs(neither the capability switch nor the platform switch is turned off); never re-derive it in prose. A turned-off platform is skipped and reported, not asked about. - Read-only. This skill only reads threads. It never sends or replies;
reply drafts go to
email-commsand stay confirm-first. - Honor the Browser Automation Contract. Follow AGENTS.md
## Browser Automation Contractin full. ThemayRun()verdict is the check; ifallowedis false, say the capability is turned off, give the enable command, and skip it, do not drive the browser. - Never run on a schedule or unattended. Always user-initiated with the agent in the loop. Never invoke this skill automatically in response to a paste, a timer, or an upstream skill trigger.
- Halt on auth challenges. On captcha, 2FA prompt, login wall, or any unexpected interstitial, halt immediately and ask the user to resolve it. Never attempt to bypass or automate around an auth challenge.
- Tool-agnostic browser prose. Say "the session browser," "snapshot or read the
page." Keep the provider on
auto; never ask the user to choose a CLI, extension, or browser driver, and never name an MCP namespace or vendor tool. - Local-only. Message bodies, thread artifacts, and screenshots stay under
workspace/. Nothing goes outbound. No credentials are stored — the session browser holds the logins. - Read-only on the platform. This skill reads messages and captures them to the
tracker. It does NOT send, reply to, react to, or otherwise mutate any in-platform
message. Reply drafts belong to
email-comms(channel-aware forlinkedinandportal). - Outcome writes belong to
track-outcomeson this path. Never writestatusdirectly for records carrying rejection / offer / interview-advance signals. Hand those totrack-outcomeswith the applicationId and label so it can apply the transition plus the required learning and follow-up work. - Privacy invariant. Never write
current_baseinto anycommunicationsfield, message summary, orworkspace/comms/artifact. Outbound-safe comp fields areexpected_base,target_base, andminimum_baseonly. - Domain-neutral. No hardcoded companies, roles, or candidate-specific values in
this skill. No bracketed placeholder tokens anywhere — if a detail is unknown, omit
it or go generic; never emit
[Company],[Role],[Name], or any bracket.
In-app and terminal paths
CareerRat's in-app path reads LinkedIn and Wellfound recruiting messages through
its owned browser workflow unless the operator turned messaging off for that
platform. It writes relevant threads into communications[], stores long bodies
under workspace/comms/, and advances each platform watermark. A terminal or
external-agent run still follows the steps above exactly as written.
- Requesting a message check (
messages.sync-request). "Check my LinkedIn messages" or "any new DMs?" offers a "Check for new messages" chip. The handler reports each platform'smayRunon/off state for themessagingcapability, the last sweep time from that platform'ssources[]watermark ("Never checked" before a first run), and how many LinkedIn threads are currently waiting on a reply. The count is LinkedIn-scoped on purpose: Wellfound threads record under the sharedportalchannel, so counting that channel would overclaim what a message sync covers. With every platform off, the request says it is turned off and offers a "Turn it on" action; there is no fallback source here, unlike mail's local Apple Mail path. Otherwise, choosing the action runs every allowed platform 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 relevance/escalation review and track-outcomes handoff. Reply drafting stays with email-comms.
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
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.

