How Reply-Watch Classifies Employer Email Replies and Updates the Tracker in Career-Ops
Reply-watch mode uses a deterministic keyword-based state machine in reply-matcher.mjs to classify employer replies into categories like Interview, Offer, Rejected, or Noise, then suggests tracker updates that require human approval before applying.
The reply-watch mode in santifer/career-ops transforms raw mailbox data into actionable tracker updates through a three-phase pipeline. This article breaks down exactly how the classification logic works, what signals it prioritizes, and how the human-in-the-loop approval mechanism prevents accidental data corruption.
Architecture Overview
The reply-watch system processes employer emails through three distinct phases orchestrated by reply-watch.mjs with core logic residing in reply-matcher.mjs.
Phase 1: Load and Match
The pipeline begins with candidate loading and fuzzy matching against existing tracker entries.
- Input:
data/reply-candidates.json(or user-supplied path) - Tracker sources:
data/applications.mdanddata/follow-ups.md - Matching function:
matchCandidatesbuilds text context fromfrom+subject+body_snippet
The matcher scores candidates against tracker rows using three weighted signals:
| Signal Combination | Confidence Level |
|---|---|
| Company name + Role title | High |
| Sender domain + Role title | High |
| Single signal only | Medium/Low |
Domain safety measures exclude SHARED_DOMAINS (LinkedIn, Greenhouse, Gmail, etc.) to prevent false matches. Short company names (≤3 characters) require word-boundary matching to avoid substring collisions like "HP" inside "PHP".
Phase 2: Classification via Deterministic State Machine
For matched candidates, classifyReply executes a strict precedence-ordered keyword check in reply-matcher.mjs:
- Noise — Marketing-alert keywords match first; no tracker action suggested
- Rejected — Rejection keywords or
signal: 'rejection'override all; suggestsRejectedstatus - Offer — Offer phrases ("offer letter", "congratulations on the offer"); suggests
Offerstatus - Auto-confirmation — Generic "application received" strings; no tracker action
- Need Action — Scheduling or task requests; suggests
Interviewif scheduling detected, elseResponded - Interview — Interview invite keywords including Chinese "邀您面试" or
signal: 'interview_invite'; suggestsInterviewstatus - Responded — Follow-up style phrases; suggests
Respondedstatus - Unknown — Recruiting-related terms present but unclear; flags "Needs Review"
- Fallback — Everything else defaults to "Needs Review"
Critical precedence rule: Rejection beats offer, noise beats everything. No LLM inference occurs—all decisions are rule-based.
Phase 3: Human-in-the-Loop Update
Classification outputs aggregate into a digest presented to the user. The updateTrackerStatuses function in reply-watch.mjs only executes after explicit "y"/"yes" confirmation:
- Acquires file lock on
data/applications.md - Rewrites only rows where current status ≠ recommended status
- Runs
node tracker.mjs syncto refresh SQLite index
No mutations occur without user approval regardless of classification confidence.
Internationalization Support
The classifier handles non-English content through normalizeChinese in reply-matcher.mjs:
- Strips corporate suffixes: "有限公司", "公司"
- Enables substring matching on Chinese characters (space-agnostic)
- Recognizes interview invitation patterns like "邀您面试"
Running Reply-Watch Mode
Basic invocation with default candidates file:
node reply-watch.mjs
Custom candidates file path:
node reply-watch.mjs /path/to/my-replies.json
Example Candidate Input and Output
Sample reply-candidates.json structure:
[
{
"message_id": "msg1",
"from": "hr@awesome-tech.com",
"subject": "Interview invitation – Senior Engineer",
"body_snippet": "We'd like to schedule a video interview next week.",
"signal": "interview_invite"
},
{
"message_id": "msg2",
"from": "noreply@job-alerts.com",
"subject": "New opportunities for you",
"body_snippet": "Check out these openings…",
"signal": null
}
]
Generated digest:
Today: 2 application updates need review
1. Awesome Tech — Senior Engineer
Type: Interview
Signal: interview_invite
Evidence: interview invitation; schedule an interview
Suggested tracker update: Interview
2. job-alerts.com
Type: Noise
Evidence: new opportunities for you
Suggested tracker update: none
Confirming "y" rewrites the Awesome Tech entry from Applied to Interview in the tracker.
Key Source Files
| File | Purpose |
|---|---|
reply-watch.mjs |
Orchestrates loading, matching, classification, and conditional tracker updates |
reply-matcher.mjs |
Implements matchCandidates, domain/company matching, and classifyReply state machine |
modes/reply-watch.md |
User documentation for inputs, invocation, and workflow |
Summary
- Deterministic classification via precedence-ordered keyword matching in
reply-matcher.mjs—no LLM dependency - Multi-signal matching using company name, sender domain, and role title with safety guards for shared domains and short names
- Strict precedence hierarchy: Noise → Rejected → Offer → Auto-confirmation → Need Action → Interview → Responded → Unknown
- Mandatory human approval before any
data/applications.mdmutation viaupdateTrackerStatuses - Chinese language support through normalized substring matching
Frequently Asked Questions
Does reply-watch use AI or machine learning to classify emails?
No. The classification relies entirely on deterministic keyword matching and explicit signal flags. The classifyReply function in reply-matcher.mjs uses static keyword lists with hardcoded precedence rules, making behavior predictable and debuggable without external API dependencies.
What prevents reply-watch from matching the wrong application entry?
Three safeguards operate in matchCandidates: excluded SHARED_DOMAINS prevent generic email providers from skewing results; company names of three characters or fewer require word-boundary matching; and scoring requires multiple signal alignment for high-confidence matches. Low-confidence matches surface for user review rather than auto-association.
Can reply-watch automatically update the tracker without asking me?
No. Even with perfect signal alignment (e.g., explicit interview_invite flag), updateTrackerStatuses in reply-watch.mjs requires explicit "y" or "yes" input. The file-lock mechanism only activates after confirmation, and the sync command runs post-rewrite to maintain index consistency.
How does the system handle non-English employer replies?
The normalizeChinese function in reply-matcher.mjs preprocesses Chinese text by removing common corporate suffixes and enabling character-level substring matching. Specific interview invitation patterns like "邀您面试" are directly encoded in the classification rules alongside English equivalents.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →