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.

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:

  1. Noise — Marketing-alert keywords match first; no tracker action suggested
  2. Rejected — Rejection keywords or signal: 'rejection' override all; suggests Rejected status
  3. Offer — Offer phrases ("offer letter", "congratulations on the offer"); suggests Offer status
  4. Auto-confirmation — Generic "application received" strings; no tracker action
  5. Need Action — Scheduling or task requests; suggests Interview if scheduling detected, else Responded
  6. Interview — Interview invite keywords including Chinese "邀您面试" or signal: 'interview_invite'; suggests Interview status
  7. Responded — Follow-up style phrases; suggests Responded status
  8. Unknown — Recruiting-related terms present but unclear; flags "Needs Review"
  9. 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 sync to 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.md mutation via updateTrackerStatuses
  • 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →