email-comms
Skill4 starsChanged 10 days agoMITView on GitHub
Draft, reply to, follow up on, summarize, and track job-search email and recruiter-message threads — including thank-yous, scheduling, negotiation, and cold outreach — while persisting full thread context in the CareerRat tracker.
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
- evaluate-jobSkill
- ingest-mailSkill
- ingest-messagesSkill
- 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
45 rules. About 0 are common, 17 are distinctive, and 28 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈17
Distinctive · estimate
This author's own choices. Worth a look.
28
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 38
When the user pastes or types a prompt of the form:
- Very specific: line 44
Treat this as an outbound-follow-up (or thank-you if the notification kind is thank-you).
- Very specific: line 56
Mode detection: run careerrat data status.
- Distinctive: line 61
When the user says they already sent a message, already replied, or already completed the action manually (e.g.
- Very specific: line 72
If status is advancing to waiting (the common case): careerrat data comm mark-sent <comm-id> --at <iso> — sets status: waiting, clears …
- Very specific: line 73
Legacy workspace (no DB): write to tracker.json in one operation — no partial writes.
- Very specific: line 77
Legacy workspace (no DB): run careerrat tracker --verify, confirm clean exit, then snapshot (careerrat tracker).
- Distinctive: line 160
If the type is ambiguous, ask one clarifying question before proceeding.
- Very specific: line 169
Look the role up on the company's own careers page and pass what you found.
- Distinctive: line 173
Always include: "Do not run, install or open any code, repository, archive or file this contact sends until you have confirmed them through …
- Very specific: line 184
Read workspace/tracker.json.
- Distinctive: line 190
If no match and this is a real communication (not a test note):
- Distinctive: line 192
Create a new communications record with at minimum:
- Very specific: line 207
Legacy workspace (no DB): write the updated workspace/tracker.json immediately.
- Distinctive: line 209
Do not proceed with a draft until the record exists.
- Distinctive: line 215
Read each of the following before drafting:
- Very specific: line 222
If the thread is for a role but no applicationId is linked, ask the user to confirm the role before drafting.
- Very specific: line 233
If writing-style.md is current: proceed.
- Distinctive: line 239
Read candidate profile config through the shared DB-first accessor and extract:
- Very specific: line 253
If the user states a new comp boundary mid-flow (e.g., "I won't go below $210K on this one") — confirm-first, then run careerrat gate …
- Distinctive: line 259
Run this step immediately after STEP 4.
- Distinctive: line 263
If the inbound message cites the candidate's location or remote status to justify a lower offer:
- Very specific: line 265
Read the most recent workspace/research/comp-bench-<role>-<loc>-<yyyy-mm>.md artifact.
- Distinctive: line 266
If no benchmark artifact exists, do NOT cite a market number.
- Very specific: line 267
If the artifact is stale (> 6 months), surface a warning to the user before drafting: Benchmark artifact may be stale — run research-comp …
- Very specific: line 271
Never cite current_base. Never cite the discounted-locale figure the employer used — rebut it structurally, not numerically.
- Very specific: line 281
Read candidate/honesty.yml (claims.do_not_fabricate) before this sub-step.
- Distinctive: line 288
Use the competing offer as leverage to invite a stronger response, not as a threat.
- Very specific: line 292
Do not fabricate one. Say so plainly to the user before drafting: No competing offer or deadline found — coaching to honest alternatives …
- Very specific: line 326
When cash_over_equity is true, do not offer equity concessions before exhausting sign-on and start-date options.
- Distinctive: line 330
If the employer sets a deadline, acknowledge it and respond within it, or ask for a short extension (24–48 h) once.
- Distinctive: line 331
If no deadline is set, the candidate controls timing.
- Very specific: line 332
When to hold: round 1 (opening) and any round where the gap to target_base is > 10% — conceding early signals the floor is lower than it is.
- Very specific: line 333
When to close: the offer clears minimum_base / oe_min_base and any one of (a) the employer states this is final, (b) further rounds risk …
- Very specific: line 335
Persistence: every round's outbound draft is a negotiation-comp message appended to communications[].messages[] via STEP 6.
- Distinctive: line 343
Write a complete, finished artifact: subject line + body.
- Very specific: line 354
cold-outreach: No fabricated mutual connections, referrals, or recruiter commitments.
- Very specific: line 358
Run careerrat lint-placeholders <draft-path> if the draft is saved to a file before presenting it to the user.
- Very specific: line 393
Sent-clears-draft (hard — same write, no exceptions): When the message was SENT and status advances to waiting (or otherwise past drafted), …
- Very specific: line 410
Legacy workspace (no DB): run careerrat tracker --verify.
- Very specific: line 464
After every outbound-sent or outbound-draft capture, set nextActionDue:
- Very specific: line 472
When the user asks to work follow-ups (or careerrat tracker --followups surfaces them):
- Distinctive: line 474
Read the stale thread via STEP 2.
- Very specific: line 496
When surfacing currently-due follow-ups (the same set computeFollowUps returns), proactively generate a baked draft for each due item so …
- Very specific: line 507
Validate and snapshot: after all drafts are written — same mode branch as STEP 8 items 6–7: DB workspace runs careerrat data verify + …
≈ “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
email-comms
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.
Agent voice. Read
candidate/modes.yml#agent_voice(defaultstandard) before producing in-chat commentary around drafts. Apply the register from AGENTS.md#mode-switches. The drafted artifact itself (email subject + body) is always written in full regardless of register —writing-style.mdgoverns its tone. Register governs the agent commentary:exec-summary= draft only + one action line;standard= draft + short bullets on intent/next-step;technical= draft + thread-context summary + strategy note;verbose= draft + full thread reconstruction + alternatives.
Recipient classes. Employer- and recruiter-facing mail stays draft-only: CareerRat never sends it, and
communication.sendnever reaches a recipient who is not the person. Agent-to-person notifications (to the person whose workspace it is, at the address in their contact record, after they opted in) are a separate class that CareerRat may send, only on the operator's command:careerrat --for <name> roundup --send, or a recruiter-reply draft delivered to them with--deliver-to-person. No opt-in, an unregistered address, or quiet hours means the notification is refused. See AGENTS.md#recipient-classes-hard.
Inputs
workspace/tracker.json— communications[] and applications[] arraysworkspace/comms/<thread-id>.md— full raw thread body if previously savedcandidate/profile.yml— compensation fields, location, domaincandidate/targeting.yml— excluded_companies, cut/keep signalscandidate/honesty.yml— claims.do_not_fabricate, confirmed tools/claimscandidate/evidence.yml— factual sourcing for all claimscandidate/writing-style.md— voice and register calibrationworkspace/writing-samples/— raw samples for style calibration- the latest inbound message text OR the user's stated outbound intent
Outputs
- complete, finished email or message artifact (subject + body, no placeholders)
- updated
workspace/tracker.json— communications[].messages[] append + status fields - optional
workspace/comms/<thread-id>.mdfor long raw thread bodies ~/Documents/careerrat/<Company>/job-<application-id>/<Company> - Email.txt— convenience copy of the draft (workspace/stays source of truth)- tracker snapshot checkpoint
Entry point: Drafting from the Needs Attention surface
When the user pastes or types a prompt of the form:
Draft the follow-up email for {Company} — {Role}
this is the one-click Draft button on the dashboard's Needs Attention panel. It copies exactly that text to the clipboard and the user pastes it here.
Treat this as an outbound-follow-up (or thank-you if the notification kind is thank-you). Start at STEP 1 to match the communications or application record for {Company} + {Role} in workspace/tracker.json. Then run the full flow (STEP 2 → 5 → 6 → STEP 8) to:
- Draft the due follow-up (STEP 5 — complete, no placeholders).
- Capture the outbound draft message (STEP 6a/b).
- Advance the record status and reset
nextActionDue(STEP 6b / STEP 8 timer reset). - Persist the baked draft onto the record (STEP 8 —
comm.draftorapp.followUp.draft) so the notification bell can show it ready to send. - Validate and snapshot — the mode branch from STEP 6(d)/(e) and STEP 8 items 6–7 (DB workspace:
careerrat data verify+careerrat tracker --verify; legacy:careerrat tracker --verifythencareerrat tracker) — this clears the timer from the Needs Attention panel for that item.
Out-of-band completion (entry-point branch)
Mode detection: run careerrat data status. Exit 0 → DB workspace — every
write step below gives the careerrat data <verb> command (Data Write Contract,
AGENTS.md). Nonzero exit → legacy workspace (no DB yet) — every write step below
gives the existing direct JSON-edit instructions, unchanged.
When the user says they already sent a message, already replied, or already completed the action manually (e.g. "I already sent this", "I replied earlier today", "I did this on my phone"):
- Confirm which thread and action the user means (one clarifying question if ambiguous).
- Record the completion:
- Append an
outbound-sent(ornoteif no artifact exists) entry tocommunications[].messages[]withdirection: "outbound-sent", current timestamp, and a one-sentence summary of what was sent. - Set
status → waiting(orclosedif this was a terminal action such as a rejection response or withdrawal). - Set
nextActionDue = null— the CTA is satisfied. - Set
nextActionto the next expected event (e.g. "Await recruiter reply"), ornullif none. - Set
comm.draft = null(andapp.followUp.draft = nullif a draft was staged) — the draft is no longer pending. - DB workspace: compose two calls, back-to-back:
careerrat data comm append-message <comm-id> --data '{"direction":"outbound-sent","at":"<ISO>","summary":"<what was sent>"}'- If
statusis advancing towaiting(the common case):careerrat data comm mark-sent <comm-id> --at <iso>— setsstatus: waiting, clearscomm.draft(andapp.followUp.draftwhen linked), automatically. Otherwise (e.g.closed), or to setnextAction/nextActionDue: read the current row and persist the patched whole row withcareerrat data comm upsert --data '<patched full comm row JSON>'.
- Legacy workspace (no DB): write to
tracker.jsonin one operation — no partial writes. mark-sentalso records a verification tier on the write (verified | supervised | user_report); see Conversational workspace handoff below for what each means. The verb derives the tier itself when none is passed:supervisedwhen a CareerRat-prepared draft was in place at send time,user_reportwhen nothing was prepared. Never passverifiedyourself; that tier is reserved for a delivery executor with real confirmation evidence.
- Append an
- Validate:
- DB workspace:
careerrat data verify(re-exports + domain integrity), thencareerrat tracker --verify(schema-level parity). - Legacy workspace (no DB): run
careerrat tracker --verify, confirm clean exit, then snapshot (careerrat tracker).
- DB workspace:
The agent not having performed the action is not a reason to leave the CTA up. Record it immediately and clear state.
Conversational workspace handoff
In the Ask workspace, note capture, drafting, the send handoff, and the sent-report
confirm are native: the app runs typed intents (communication.note-request,
communication.draft-request, communication.handoff-request,
communication.record-external) directly in workspace-agent.mjs, not this
skill's step-by-step CLI verbs. A terminal or external-agent run of this skill
still follows STEP 1 → 8 and the out-of-band completion branch above exactly as
written, issuing the careerrat data comm ... commands directly.
- Note capture. A free-text request such as "add a note to the Acme thread:
called to follow up" resolves the referenced thread and appends a note the same
way STEP 6(a) does, then returns a durable
communication_notereceipt card in the thread. An empty note or an unresolved/ambiguous reference returns specific recovery copy instead of a generic error, never a silent no-op. - Send handoff. A request such as "send my reply to Acme" is a pure read; it
never sends anything. It resolves the thread, requires an existing draft (same
precondition as
communication.send), and returns acommunication_handoffartifact: a read-only card with the draft's subject/body prefilled into mailto, Gmail, and Outlook compose links built from the thread's first participant email. A thread with no resolvable recipient email returns an explicit no-recipient state instead of a dead link. The card's "I sent this" action runs the samecommunication.record-externalconfirm as the out-of-band branch above. Never auto-send stays absolute. No code path in the handoff ever submits, clicks send, or opens a real network connection on the candidate's behalf; it only builds links and waits for the candidate's explicit confirm. - Delivering the draft to the person. In a person workspace (
--for <name>),careerrat --for <name> comm handoff <id> --deliver-to-personemails the reviewed draft to that person instead of printing compose links. Subject: "Draft reply to<recruiter>for you to send". It goes only to the address in their contact record and only after they opted in; it is never sent to the recruiter, and quiet hours apply (--nowoverrides). The draft stays on the thread, so the person's later "sent" (careerrat --for <name> comm mark-sent <id>) records thesupervisedtier. - Verification tiers.
mark-sent(careerrat data comm mark-sent/ thecommMarkSentwriter) now records one of three tiers on every send, instead of a bare boolean:verified: an executor confirmed delivery. Onlycommunication.sendcan record this, and it still requires a connected delivery executor (COMMUNICATION_EXECUTOR_UNAVAILABLEif none exists) and now checks the thread's channel first (COMMUNICATION_CHANNEL_UNSUPPORTEDfor linkedin/phone/sms/portal threads, since those never had a delivery executor to begin with).supervised: CareerRat prepared the draft (via drafting or the handoff above) and the candidate confirmed, out of band, that they sent it. This is whatcommunication.record-externalrecords when acomm.draftwas present at confirm time.user_report: a bare self-report with no CareerRat-prepared draft behind it. This is the default when no draft exists, and the tier any unrecognized or omitted value normalizes to, since a caller can never silently upgrade to a stronger claim than it actually made.
- What's still open. An in-browser compose executor that could reach
verifiedsends directly (instead of requiring the handoff-plus-confirm path) was evaluated and deferred. Its entry criteria: a keystroke-safe design validated against real Gmail/Outlook compose DOMs (webmail send-hotkeys like Ctrl+Enter make "never clicks send" non-trivial), per-account compose/Sent verification for multi-account webmail, and a distinct Sent-folder read consent capability, since today'smail_accessis verification-code-only and must not silently widen. Recipient provenance, the remaining blocker at the time this was scoped, is solved by the resolver the handoff uses above.
STEP 0 — Classify message intent
Identify which type of communication this is. The type controls which sub-steps run:
| Type | Sub-steps triggered |
inbound-recruiter-reply | 2, 3, 4, 5, 6, 7 |
outbound-reply | 2, 3, 4, 5, 6, 7 |
outbound-follow-up | 2, 3, 4, 5, 6, 8 |
thank-you | 2, 3, 4, 5, 6 |
scheduling-confirm | 2, 3, 4, 5, 6, 7 — but a true scheduling thread (proposing/confirming/moving a call) routes to schedule-meeting, which owns timezone, double-booking, and calendar holds. Keep this type only for a one-line confirmation inside a broader comms reply. |
negotiation-comp | 2, 3, 4-COMP, 4b, 5, 6, 7 |
cold-outreach | 3, 4, 5, 6 |
note | 1, 6 |
If the type is ambiguous, ask one clarifying question before proceeding.
Fake-recruiter check (inbound recruiter and cold-outreach messages)
For an inbound-recruiter-reply or any unsolicited recruiter message, run
careerrat recruiter-check --sender <from address> --company-domain <the company's own
domain, if known> --posting found|missing [--body-file <message>] --json before drafting.
Look the role up on the company's own careers page and pass what you found; omit
--posting if you could not check. It is a flag, never a verdict. On suspect:
- Tell the person plainly, with the
RECRUITER SIGNAL: suspect - <signals>line. - Always include: "Do not run, install or open any code, repository, archive or file this contact sends until you have confirmed them through the company's own careers page or main switchboard."
- Draft only a short verify-first reply (ask them to confirm through the company's careers page or from a company address). Share no resume, phone number, ID or payment detail, and never offer to run their code.
STEP 1 — Match to tracker thread
Mode detection (same check as the entry-point branches above): run careerrat
data status. Exit 0 → DB workspace — every write step below gives the
careerrat data <verb> command (Data Write Contract, AGENTS.md). Nonzero exit →
legacy workspace (no DB yet) — every write step below gives the existing direct
JSON-edit instructions, unchanged.
Read workspace/tracker.json. Find the communications[] record whose:
applicationIdmatches a known application, ORcompany+rolematch, ORthreadIdorsubjectmatches the inbound message.
If no match and this is a real communication (not a test note):
Create a new communications record with at minimum:
{
"id": "comm-<applicationId or slug>",
"applicationId": "<id or null>",
"company": "...",
"role": "...",
"channel": "email|linkedin|portal|phone|sms|other",
"status": "needs-reply",
"summary": "<one line>"
}- DB workspace:
careerrat data comm upsert --data '<the record JSON above>'(full-row insert). - Legacy workspace (no DB): write the updated
workspace/tracker.jsonimmediately.
Do not proceed with a draft until the record exists.
STEP 2 — Load context
Read each of the following before drafting:
workspace/comms/<thread-id>.md— full thread body and prior exchanges, if the file exists.- The linked
applications[]row inworkspace/tracker.json— JD context, current status, artifacts. candidate/evidence.yml— factual claims available to use.candidate/honesty.yml— boundaries on what may and may not be claimed (claims.do_not_fabricate,tools.do_not_claim).
If the thread is for a role but no applicationId is linked, ask the user to confirm the role before drafting.
STEP 3 — Style gate
Read candidate/writing-style.md.
Then check workspace/writing-samples/ for any files modified AFTER writing-style.md was last written (compare file mtimes; treat absent mtimes as newer).
- If
writing-style.mdis absent OR newer samples exist: runnpm run calibrate:style, wait for it to finish, then read the updatedcandidate/writing-style.mdbefore drafting. If it reports "Not calibrated" (no writing samples yet), draft in a plain, neutral voice and ask the user for a few samples; don't treat the style as set. - If
writing-style.mdis current: proceed.
STEP 4 — Comp gate (negotiation-comp messages ONLY)
Read candidate profile config through the shared DB-first accessor and extract:
| Field | Use |
compensation.target_base | Primary outbound anchor (state this) |
compensation.minimum_base | Walk-away floor (do not reveal unless pressed) |
compensation.expected_base | Form-field value if a portal comp form is involved (foundations-spec §2 / §5; canonical read path: form-defaults.yml#expected_base first, then profile.compensation.expected_base as fallback) |
compensation.cash_over_equity | Flag equity vs. cash preference if asked |
compensation.current_comp_shareable | Gate: if false (the default), current_base is PRIVATE |
Privacy invariant: current_base must NEVER appear in any outbound draft. If current_comp_shareable is not explicitly true, treat current_base as if it does not exist. State target_base as the anchor; use minimum_base only as an internal walk-away reference, not as an outbound number unless the user explicitly instructs otherwise.
For OE-bucket roles: read profile.compensation.oe_min_base / profile.compensation.oe_max_base (foundations-spec §2; both confirmed in profile.schema.json) and apply them instead of the standard floor/anchor. The standard minimum_base / target_base do NOT apply for OE roles.
If the user states a new comp boundary mid-flow (e.g., "I won't go below $210K on this one") — confirm-first, then run careerrat gate comp-floor <N> --write --confirm. Echo the CLI confirmation.
STEP 4b — Written-negotiation depth (negotiation-comp messages ONLY)
Run this step immediately after STEP 4. It applies the three depth capabilities from the Negotiation Contract in AGENTS.md — read that section first for strategy; this step is written-channel mechanics only.
4b-1 Geographic-discount pushback
If the inbound message cites the candidate's location or remote status to justify a lower offer:
- Read the most recent
workspace/research/comp-bench-<role>-<loc>-<yyyy-mm>.mdartifact. Extractbenchmark.floor,benchmark.midpoint, andbenchmark.ceilingfrom frontmatter.- If no benchmark artifact exists, do NOT cite a market number. Reframe on value only (see 4b-3). State in the draft that the ask reflects role scope, not a local rate.
- If the artifact is stale (> 6 months), surface a warning to the user before drafting:
Benchmark artifact may be stale — run research-comp to refresh before sending.
- Anchor the rebuttal on the role market band, not a locale-discounted figure. Use
benchmark.midpointas the default reference. Usebenchmark.flooronly when the offer is already above the midpoint and the gap is narrow; usebenchmark.ceilingonly when the role scope or seniority matches the top of the benchmarked band and the artifact explicitly supports it. - Include a remote-comp-parity statement: distributed roles compete for talent nationally (or globally); the candidate's geography does not reduce role scope or output.
- Support with at least one evidence claim from
candidate/evidence.yml. Do not fabricate impact metrics. - Never cite
current_base. Never cite the discounted-locale figure the employer used — rebut it structurally, not numerically.
| Signal in inbound | Written-rebuttal framing |
| "Our remote employees are at a location-adjusted rate" | Cite role market band from benchmark; name the midpoint; assert output-not-location pricing |
| "Your area has a lower cost of living" | Redirect to role scope and national talent pool; use benchmark midpoint as anchor |
| No benchmark artifact exists | Value + role-scope reframe only; no market number |
4b-2 Competing-offer / BATNA framing
Read candidate/honesty.yml (claims.do_not_fabricate) before this sub-step. The honesty firewall applies in full.
If the candidate has a real competing offer or genuine alternative:
- Name the offer type generically ("another offer at the senior level", not the competing company unless the candidate explicitly instructs disclosure).
- State the walk-away clearly —
profile.compensation.minimum_base(oroe_min_basefor OE roles) is the internal floor; never quote it outbound unless the user explicitly instructs otherwise. Frame it as the candidate's genuine decision timeline. - Frame timeline honestly: state only a deadline that is real. Do not invent urgency. Ask the candidate to confirm the exact date before drafting any sentence that references a deadline.
- Use the competing offer as leverage to invite a stronger response, not as a threat. Tone: collegial, not adversarial.
If the candidate has NO competing offer or BATNA:
Do not fabricate one. Say so plainly to the user before drafting: No competing offer or deadline found — coaching to honest alternatives only.
Coach and draft using only:
- Market anchor from the benchmark artifact (4b-1), if one exists.
- Unique value from
candidate/evidence.yml— measurable impact, rare skills, or domain depth. - Time: a genuine response window the candidate controls. Ask the candidate for a specific day and write that real day into the sentence — e.g. "I'd like to give this proper consideration — could we reconnect on Tuesday?" Never emit a bracket placeholder for the date; use the day the candidate actually gives.
Absolute rule (restated from AGENTS.md Negotiation Contract): never fabricate a competing offer, a deadline, a number, or leverage that does not exist.
4b-3 Multi-round sequencing and concession ladder
For ongoing negotiation threads (round ≥ 2), read the full communications[].messages[] history for this thread before drafting. Identify the current round.
Round map:
| Round | Anchor | Tone | Concession posture |
| Opening | profile.compensation.target_base (or oe_max_base for OE roles) | Confident, evidence-backed | None — hold the full ask |
| Counter | Employer's counter vs. target_base delta | Collaborative, value-forward | Offer one tradeable item if needed (see ladder below) |
| Re-counter | Close the remaining gap | Decisive | One final concession, or hold and name a close condition |
| Close | At or above minimum_base / oe_min_base | Warm, forward-looking | Accept, decline, or ask for 24 h to decide |
Concession ladder — tradeables ranked by profile.compensation.cash_over_equity:
| Item | Trade when | Do NOT trade |
| Sign-on bonus | Base is hard-capped; bonus bridges the gap | When base is already below minimum_base / oe_min_base |
| Equity / vesting terms | cash_over_equity is false and the role includes equity compensation | When cash_over_equity is true |
| Start date | Candidate has flexibility; small goodwill signal | As a substitute for comp |
| Title / level | Materially affects future negotiating power or career path | To paper over a comp gap |
| Remote / relocation terms | Candidate is indifferent to location | When location is a hard constraint |
| Base salary | Only as a last move, and only to minimum_base / oe_min_base — never below | Never below the walk-away floor |
When cash_over_equity is true, do not offer equity concessions before exhausting sign-on and start-date options.
Deadline discipline:
- If the employer sets a deadline, acknowledge it and respond within it, or ask for a short extension (24–48 h) once.
- If no deadline is set, the candidate controls timing. Do not volunteer one.
- When to hold: round 1 (opening) and any round where the gap to
target_baseis > 10% — conceding early signals the floor is lower than it is. - When to close: the offer clears
minimum_base/oe_min_baseand any one of (a) the employer states this is final, (b) further rounds risk goodwill, or (c) the candidate's own deadline is real and near.
Persistence: every round's outbound draft is a negotiation-comp message appended to communications[].messages[] via STEP 6. The full round history accumulates in the thread so context is never lost between sessions.
Gate write-back: if a new comp boundary emerges mid-negotiation (e.g., the candidate says "I won't go below $210K on this one"), apply the STEP 4 write-back rule — confirm-first, then run careerrat gate comp-floor <N> --write --confirm for the per-offer walk-away. profile.compensation.minimum_base is the single source of truth for the comp floor — there is no separate targeting.comp_floor field.
STEP 5 — Draft
Write a complete, finished artifact: subject line + body. No placeholders. The artifact must be ready to send as written.
Real names, never placeholders — go generic when unknown. Always sign with the candidate's real name (candidate/profile.yml#candidate.preferred_name, falling back to full_name) and a real signature block — the candidate is always known, so the signature is never generic. Address the recipient by their real name and pull company/role/contact from the linked tracker.json record. If a detail is genuinely unknown, write it generically — NEVER emit a bracketed token like [Name], [Recruiter], or [Company], and never block or ask the user to fill one. An unknown recipient name → a natural greeting (Hi, / Hello,; use Hi {team} team only if the team is actually known). An unknown company/role → rephrase the sentence so it reads naturally without it. Brackets in the output are a build failure, not a TODO.
Apply writing-style.md register and patterns throughout. Match the tone and formality of the inbound thread.
Per-type requirements:
- thank-you: Reference one specific moment from the interview or conversation for genuine personalization. Read
workspace/tracker.json#applications[].conversations[]for notes. - scheduling-confirm: State candidate availability from
profile.yml#location/travel_tolerance. Confirm the channel (email vs. portal vs. video link). Do not confirm logistics that haven't been stated. - negotiation-comp: State
target_baseas the anchor. Do NOT mentioncurrent_base. Support the anchor with value framing fromevidence.yml. Apply the full STEP 4b written-negotiation depth: geographic-discount rebuttal (4b-1), competing-offer/BATNA framing (4b-2), and concession-ladder round sequencing (4b-3). See AGENTS.md Negotiation Contract for strategy; STEP 4b for written-channel mechanics. - cold-outreach: No fabricated mutual connections, referrals, or recruiter commitments. Claims sourced from
evidence.ymlonly. - follow-up: Mirror prior thread tone. Do not re-litigate prior exchanges; advance to next action.
- note: Factual summary only, no outbound artifact.
Run careerrat lint-placeholders <draft-path> if the draft is saved to a file before presenting it to the user.
For outreach and any draft that states candidate facts, also run careerrat fact-check <draft-path> --company "<Company>". Exit 1 means the draft names a number, tool, employer or person absent from the candidate's résumé and evidence: remove it or ask the user before presenting.
STEP 6 — Capture
Execute the following mutation sequence in order:
(a) Append the new message to communications[].messages[] in workspace/tracker.json:
{
"id": "msg-<commId>-<n>",
"direction": "inbound|outbound-sent|outbound-draft|note",
"at": "<ISO 8601 datetime>",
"from": "<sender>",
"to": ["<recipient>"],
"subject": "<subject line>",
"summary": "<concise gist — one sentence>",
"artifactPath": "<workspace/comms/... if saved>"
}- DB workspace:
careerrat data comm append-message <comm-id> --data '<the message JSON above>'. This appends tomessages[]and auto-rollslastInboundAt/lastOutboundAtforward fromdirection— the first half of (b) below. Include asummaryornextActionkey in the payload if you also want those set on the parent record in this same call (the verb applies them when present). - Legacy workspace (no DB): append to
communications[].messages[]directly inworkspace/tracker.json.
(b) Update the parent communications record with:
status:needs-reply | drafted | waiting | scheduled | closed | blockednextAction: what to do nextnextActionDue: ISO datelastInboundAtorlastOutboundAt: ISO datetime of this messageSent-clears-draft (hard — same write, no exceptions). When the message was SENT and status advances to
waiting(or otherwise pastdrafted), also setcomm.draft = nullon the communications record in this same write. If the draft came fromapp.followUp.draft, setapp.followUp.draft = null(or removeapp.followUp). This is the sent-clears-draft invariant from AGENTS.md "Actionability Write-Back Contract". Leaving a draft set after a send creates a ghost "Ready to send" panel that never resolves.- DB workspace: compose the call(s) matching what actually changed, back-to-back with (a) above (no combined messages+status transaction exists):
- Message was SENT and status is advancing to
waiting(the common send-clears-draft case):careerrat data comm mark-sent <comm-id> [--at <iso>] [--summary "<t>"]— setsstatus: waiting, clearscomm.draft(andapp.followUp.draftwhen that application exists), and stampslastOutboundAt, all in one transaction, automatically. Do not hand-write that clear. - Any other status value (
needs-reply,drafted,scheduled,closed,blocked) or to setnextAction/nextActionDueexplicitly: there is no single-field patch verb forcommunications[], so read the current row fromworkspace/tracker.json#communications[], apply the fields above (carrying every other field over unchanged), and persist the whole row:careerrat data comm upsert --data '<patched full comm row JSON>'.
- Message was SENT and status is advancing to
- Legacy workspace (no DB): edit the fields above directly on the communications record in
workspace/tracker.json, in the same write as (a).
- DB workspace: compose the call(s) matching what actually changed, back-to-back with (a) above (no combined messages+status transaction exists):
(c) Save long raw body to workspace/comms/<thread-id>.md if the body exceeds one paragraph. Reference the path in artifactPath. workspace/comms/ files are local-only and must not appear in any outbound artifact. (This is a local file write outside tracker.json — unaffected by DB vs legacy mode.)
(d) Validate:
- DB workspace: the calls in (a)/(b) already persisted and auto-exported
workspace/tracker.json+workspace/activity.jsonl(Data Write Contract, AGENTS.md). Runcareerrat data verify(re-exports + domain integrity) andcareerrat tracker --verify(schema-level parity). - Legacy workspace (no DB): run
careerrat tracker --verify.
Confirm it exits clean before proceeding. If it fails, fix the JSON (legacy) or the DB row via another verb call (DB — never hand-edit tracker.json, it is a regenerated file) and re-run.
(e) Snapshot:
- DB workspace: already exported by the calls above; if
careerrat tracker-devis running itsfs.watchalready picked it up. Runcareerrat trackeronly for a recovery checkpoint. - Legacy workspace (no DB): run
careerrat tracker.
Then log it to the Activity Pulse feed (the dashboard's live timeline — see Activity Pulse in AGENTS.md). If the message is a draft awaiting the user to send, log it as needing the user; if it was already sent, log it sent:
DB workspace: the (a)/(b) calls above already auto-logged their own generic events (
messagetype) in their own transactions. For the richer, outcome-specific type below, log an additional event (this verb only logs, it never bumps the stamp):# draft awaiting send: careerrat data activity append --data '{"type":"drafted","actor":"agent","needsUser":true,"title":"Drafted reply — <Company>","summary":"<one line: what the message does>","refs":{"applicationId":"<application id>","company":"<Company>"},"cta":{"label":"Review & send"}}' # already sent: careerrat data activity append --data '{"type":"message","actor":"agent","title":"Sent — <Company>","summary":"<one line: what the message does>","refs":{"applicationId":"<application id>","company":"<Company>"}}'Legacy workspace (no DB):
# draft awaiting send: careerrat activity append --type drafted --actor agent --needs-user \ --title "Drafted reply — <Company>" --summary "<one line: what the message does>" \ --company "<Company>" --app-id <application id> --cta-label "Review & send" --write # already sent: careerrat activity append --type message --actor agent \ --title "Sent — <Company>" --summary "<one line: what the message does>" \ --company "<Company>" --app-id <application id> --write
STEP 7 — Outcome routing
Branch on message type after capture:
| Condition | Action | DB-mode command |
| Scheduling confirmed / interview booked | In the SAME tracker.json write: update applications[].status to the appropriate stage (phone-screen, onsite, etc.) AND update the communications record — status → scheduled, nextActionDue = null, nextAction = 'Attend <stage> — <date>' (or clear it if no further prep action is needed), comm.draft = null if a draft was staged. Both records must land in one write so neither leaves a ghost CTA. Hand off to interview-prep for prep materials. | careerrat data app set-status <id> "<stage>" for the application row, then patch the comm record (read current row, apply the fields, persist): careerrat data comm upsert --data '<patched full comm row JSON>'. Run both back-to-back, never deferring the second. |
| Offer received | Before surfacing the comp comparison, write to tracker.json in one operation: comm status → waiting, nextActionDue = null, nextAction = 'Evaluate offer and respond' (set a real deadline if the employer gave one, else null), comm.draft = null if a draft was staged. Append a note-direction message to messages[] summarising the offer receipt. Then read the candidate config through the shared DB-first accessor (profile.compensation.minimum_base and target_base), surface whether the stated comp clears the floor, and recommend accept/negotiate/decline. Do not accept on the user's behalf. | Patch the comm record locally (status/nextActionDue/nextAction/draft), persist: careerrat data comm upsert --data '<patched full comm row JSON>'; then append the note: careerrat data comm append-message <comm-id> --data '{"direction":"note","at":"<ISO>","body":"<offer receipt summary>"}'. Comp comparison itself is a candidate-config read: SQLite in DB mode, YAML only in legacy mode. |
| Rejection or withdrawal | In the SAME tracker.json write as the STEP 6 capture: set status = closed, nextActionDue = null, nextAction = null, comm.draft = null (if any draft was staged). Do not rely on STEP 6's generic status field alone — state it explicitly here. Then hand off to track-outcomes for durable outcome recording and reevaluation-threshold check. | Same composition as STEP 6(b)'s DB branch — read-patch-persist careerrat data comm upsert --data '<patched full comm row JSON>' with status: "closed", nextActionDue: null, nextAction: null, draft: null. track-outcomes owns the durable app-row transition (its own DB verbs) — this call only closes the comm thread. |
| Ghosting / no response after follow-up | In the SAME tracker.json write: set status = closed (or blocked if appropriate), nextActionDue = null, nextAction = null, comm.draft = null if a draft was staged. The due-date CTA must clear together with the status in one write. Hand off to track-outcomes. | Same read-patch-persist pattern: careerrat data comm upsert --data '<patched full comm row JSON>' with the fields above. |
| User states new exclusion mid-thread (e.g., "never email this company again") | Run careerrat gate exclude-company "<Company>" --write; it applies directly and is logged with an Undo. Echo the CLI result. Run careerrat doctor to verify after write. | careerrat gate is DB-aware: it writes SQLite in DB mode and legacy YAML only in legacy mode. Never hand-edit candidate/targeting.yml. |
| User states new comp floor mid-thread | See STEP 4 comp write-back rule; use careerrat gate comp-floor <N> --write --confirm. | careerrat gate is DB-aware: it writes SQLite in DB mode and legacy YAML only in legacy mode. Never hand-edit candidate/profile.yml. |
STEP 8 — Follow-up timer management
After every outbound-sent or outbound-draft capture, set nextActionDue:
- Waiting for recruiter reply: 5–7 business days from send date.
- Awaiting scheduling follow-up: 2 business days before the event deadline (or 3 days if no deadline given).
- Thank-you: no follow-up needed unless the user requests one. After the thank-you is captured via STEP 6, set
nextActionDue = nullon the comm record in that same write — the thank-you satisfies the due event. Do NOT set a new follow-up timer unless the user explicitly asks for one.
Stale threshold: A waiting thread with no update for 7 days is stale and surfaces as a follow-up. Applied-with-no-response threads surface after 10 days. These defaults come from cadence.mjs — if the candidate's follow_up: config block in candidate/targeting.yml provides custom thresholds for a kind, those take precedence over the code defaults.
When the user asks to work follow-ups (or careerrat tracker --followups surfaces them):
- Read the stale thread via STEP 2.
- Apply STEP 3 style gate.
- Draft the follow-up (STEP 5).
- Capture (STEP 6): append outbound message, advance
statusfromwaitingto appropriate state, resetnextActionDue. - Persist a baked draft for the dashboard notification panel. After drafting, write
draft: { subject, body }onto the relevant record inworkspace/tracker.json:- For comm-based kinds (
needs-reply,comm-due,waiting-stale): setcomm.draft = { subject, body }on the communications record. - For application-based kinds (
app-nudge,post-interview-nudge,thank-you): setapp.followUp = { kind, dueAt, draft: { subject, body }, generatedAt }on the application record. The dashboard notification bell reads these fields directly; a baked draft appears as "ready to send." A baked draft is only valid for the DRAFT/awaiting-send state; once the message is sent, it must be nulled per the sent-clears-draft invariant (STEP 6b) so the "Ready to send" panel clears. - DB workspace: comm-based kinds — there is no single-field patch verb for
communications[], so read the current row, setdraft, and persist:careerrat data comm upsert --data '<patched full comm row JSON>'. Application-based kinds —app.followUpmerges one level underapp set-fields:careerrat data app set-fields <id> --data '{"followUp":{"kind":"<kind>","dueAt":"<iso>","draft":{"subject":"<s>","body":"<b>"},"generatedAt":"<iso>"}}'. Neither is outcome-changing. - Legacy workspace (no DB): edit
workspace/tracker.jsondirectly. Also write a convenience copy to the job's Documents folder —~/Documents/careerrat/<Company>/job-<application-id>/<Company> - <what> Email.txt(e.g.Aperture Science - Follow-up Email.txt), per the Artifact Contract (organized by company, job, then round). Use the real company name for the folder and file (never a bracket placeholder; if the company is somehow unknown, useunknown). File content: subject line on the first line, a blank line, then the body.workspace/stays the source of truth; Documents is for the user's convenience. Body must NEVER containcurrent_baseor any private comp field (per the Privacy Invariant). (This Documents copy is a local file write outsidetracker.json— unaffected by DB vs legacy mode.)
- For comm-based kinds (
- Validate:
- DB workspace: the calls above already persisted and auto-exported. Run
careerrat data verifyandcareerrat tracker --verify. - Legacy workspace (no DB):
careerrat tracker --verify(also:npm run verify:tracker). Confirm clean exit before proceeding.
- DB workspace: the calls above already persisted and auto-exported. Run
- Snapshot:
- DB workspace: already exported; run
careerrat trackeronly for a recovery checkpoint. - Legacy workspace (no DB):
careerrat tracker(so the timer clears from the dashboard).
- DB workspace: already exported; run
STEP 8a — Prepare follow-up drafts
When surfacing currently-due follow-ups (the same set computeFollowUps returns), proactively generate a baked draft for each due item so the bell panel shows them ready to send — not just as bare reminders.
For each item returned by computeFollowUps:
- Check the candidate
follow_up:config incandidate/targeting.yml. Do NOT generate a draft for any kind that isenabled: false— those reminders are suppressed entirely. Use the configuredafter_days/after_hoursthresholds (mapped viarulesFromConfiginsrc/core/tracker/cadence.mjs) rather than hardcoded defaults. - Draft per kind (apply STEP 5 requirements — complete, no placeholders, honest):
app-nudge: short, professional follow-up on the outstanding application; reference the role name and applied date. Contact-path precondition (hard): an app-nudge only makes sense when there's an actual person to nudge — a linked recruiter/email thread (communications[]for the row), a logged conversation with a namedwho, or an explicit contact on the application. A black-hole portal or cold application with none of these has nobody to follow up with, socomputeFollowUpsno longer emits anapp-nudgefor it (seeappHasContactPathincadence.mjs). Do not hand-draft a follow-up "to nobody" for such a row — the real move isrelationship-sourcing(find a contact first) or a plain wait/archive decision. The dashboard mirrors this: a contactless quiet application shows no action.post-interview-nudge: 4–6 lines referencing the interview stage, reiterating interest, and asking about next steps.thank-you: warm, specific thank-you referencing one detail fromapplications[].conversations[]; keep to 3–5 lines.needs-reply/comm-due/waiting-stale: advance the existing thread per STEP 5's follow-up guidance.
- Persist the draft to the record (see STEP 8 — "Persist a baked draft" above, including its DB-mode branch).
- Validate and snapshot after all drafts are written — same mode branch as STEP 8 items 6–7: DB workspace runs
careerrat data verify+careerrat tracker --verify; legacy workspace runscareerrat tracker --verifythencareerrat tracker.
HONESTY
See Negotiation Contract › Honesty Firewall (hard) in AGENTS.md. Read
candidate/honesty.ymlbefore every draft.
Known limitations
- Cadence is candidate-configurable via
follow_up:incandidate/targeting.yml. STEP 8's timing defaults come fromsrc/core/tracker/cadence.mjs(FOLLOWUP_RULES), but a candidate can override any kind'senabledflag andafter_days/after_hoursthreshold via afollow_up:block incandidate/targeting.yml— the schema for this block is defined inconfig/targeting.schema.jsonand the snake_case → camelCase mapping is handled byrulesFromConfig()incadence.mjs. Supported kinds:app_nudge,post_interview,waiting_stale,interview_thank_you,needs_reply,comm_due. Two kinds are new as of the current cadence engine:post-interview-nudge(interviewed but gone quiet, default 5 days) andthank-you(owed within 24 hours of a logged interview). Always check whether a kind is enabled before preparing or surfacing a draft for it.
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.

