Skip to content

Repository files navigation

HirePilot

A personal AI recruiting agent. It learns your professional profile, continuously discovers jobs, scores them, tailors your resume per opportunity, prepares applications, and assists or automates submission — under your control.

Not a job board. An agent that works your job search in the background.

New here? docs/MANUAL.md is the full operating manual — setup, the daily loop, how to start the automation, what the agent refuses to do, and the known gaps.


Quick start

Requires Node 20.11+ and Docker.

npm install
cp .env.example .env          # then generate the secrets below
npm run infra:up              # MongoDB (replica set) + Redis
npm run db:indexes            # apply indexes and constraints
npm run db:seed               # demo profile, preferences, career analysis

npm run dev                   # terminal 1 — the app
npm run worker                # terminal 2 — the agent worker

Open http://localhost:3000 and sign in with the seeded account:

email:    demo@hirepilot.local
password: demo-password-1234

Every seeded record is flagged isDemo: true so demo content can never be mistaken for your own.

Generating secrets

echo "AUTH_SECRET=$(openssl rand -base64 32)"
echo "ENCRYPTION_KEY=$(openssl rand -base64 32)"

ENCRYPTION_KEY must decode to exactly 32 bytes — the app refuses to start otherwise rather than silently using a weak key.

Running without an API key

Set AI_PROVIDER=mock and the whole product runs end to end with deterministic synthetic output and zero cost. It is also what the test suite uses. When a real provider is configured but has no credentials, the app falls back to the mock provider and says so loudly in the logs — it will not quietly present fake analysis as real.

To use Claude, set AI_PROVIDER=claude and CLAUDE_API_KEY.


Commands

Command What it does
npm run dev Next.js dev server
npm run worker Agent worker process (watch mode)
npm run build Production build
npm run start Production server
npm run worker:prod Agent worker (production)
npm run infra:up / infra:down MongoDB + Redis containers
npm run db:indexes Apply indexes; idempotent, safe on every deploy
npm run db:seed Reset and reload demo data
npm run db:seed:history Backfill ~60 applications so analytics has something to measure
npm run db:reset Re-seed from scratch
npm test Unit tests (no database, no network)
npm run test:integration Integration tests (real MongoDB, mocked AI)
npm run test:e2e Playwright end-to-end tests
npm run typecheck tsc --noEmit
npm run lint ESLint

The app and the worker are two processes. Without the worker running, uploads are accepted and queued but nothing analyzes them — the onboarding screen says so explicitly rather than spinning forever.

Ports

Non-default ports are used so this project never collides with an existing local MongoDB or Redis:

Service Port
App 3000
MongoDB 27019
Redis 6381
MinIO (optional, --profile storage) 9000 / 9001

Architecture

Full detail in docs/ARCHITECTURE.md; how to actually run it in docs/MANUAL.md. The essentials:

Two processes, one codebase. HTTP requests validate, authorize, and enqueue. Anything that calls an LLM, a job board, or a browser runs in the worker, so it is retryable, observable, and cancellable.

Layered, dependencies pointing one way.

app/ components/ features/     ← UI
services/                      ← use cases: orchestrate, authorize, enqueue
agents/                        ← AI workflows, one job each
repositories/                  ← the only code that talks to MongoDB
lib/                           ← db, ai, queues, storage, auth, browser

MongoDB, no ORM. The official driver plus a typed repository layer. Indexes are declared in src/lib/db/indexes.ts and are the real schema contract — they encode the uniqueness constraints that make job dedupe and one-application-per-job correct under concurrency.

AI behind a narrow interface. complete() for prose, extract(schema) for typed data. Three implementations: Claude (primary), OpenAI, and a deterministic mock. Call sites declare a task tier (fast / balanced / deep), never a model name, so model selection is configuration.

Scoring is deterministic where it should be. Profile completeness, employability, experience totals, and match sub-scores are pure functions of structured data — reproducible and explainable. The AI supplies rationale and may adjust any single dimension by at most ±10 points, with the reason recorded and shown in the UI. It never produces the score itself.

The browser decides nothing. Playwright extracts a serializable snapshot of the page; pure functions classify fields, detect blockers, and build a fill plan; Playwright then executes that plan. Every decision is unit-testable without a browser, and the automation layer is auditable rather than opaque.

One opportunity, one document. Jobs are global, not per-user. A posting is identified by a fingerprint over its normalized company, title, and location bucket, backed by a unique index — so the same role listed on Greenhouse and Lever merges into a single opportunity, and concurrent ingestion workers converge on one document without a transaction.


How the agent stays honest

This is the part that matters most, so it is enforced structurally rather than by asking the model nicely:

  • Every generative prompt receives the profile as a closed set of facts plus the user's recorded corrections, and shares one factuality contract defined in src/prompts/registry.ts.
  • Outputs are schema-constrained and re-validated. Claims are tagged known / inferred / recommendation / unknown, and the UI renders an inference visibly as an inference.
  • User corrections are binding. "I don't have production AWS experience" is stored, injected into every downstream prompt, and outranks anything the agent infers from your resume or GitHub.
  • Fabrication is prevented structurally, then verified. The tailoring agent produces bullets only. Employers, titles, dates, degrees, institutions and certifications are copied from your profile by application code — the model has no field in which to write them, so it cannot invent one. What remains (a fabricated metric or an unevidenced technology inside a bullet) is caught by a verifier, and any violation blocks approval rather than being silently edited away.
  • Skills the model suggests that aren't in your profile are dropped and reported; skills it forgets are added back, so tailoring never silently deletes your own material.
  • Unanswerable application questions return needs user input, never a guess.
  • Estimates (like interview probability) are labelled as estimates, never stated as fact.
  • Every generated artifact records the prompt version and model that produced it.

What the agent will not do

  • Bypass CAPTCHA, MFA, bot protection, authentication, or rate limits. Job sources use official APIs and public job-board feeds only; a 429 causes the run to back off, never to retry around the limit.
  • Scrape LinkedIn. Profile import uses your own data export or your own pasted text — both normalize into the same model.
  • Submit an application containing information it does not actually have.
  • Answer questions about work eligibility, sponsorship, disability, demographics, criminal history, salary commitments, or relocation on your behalf. These are classified by rule and filtered out before the model ever sees them — not refused by prompting, which a model may or may not honour. They stop and wait for you in every automation mode, including Auto.

Automation stops, it does not push through. CAPTCHA, MFA, bot protection, login walls and rate limiting are detected and reported with a plain explanation of what to do — there is no code path in this repository that attempts to defeat any of them. Workday and LinkedIn are declared manual-only because their applications sit behind authentication.

Statistics are withheld rather than guessed. A response rate computed from three applications is noise, and showing it as a number invites real decisions on it. Rates below a minimum denominator render as an em dash with the reason; group comparisons are suppressed entirely below 10 applications per group; and every insight carries its sample size and a confidence derived from sample and effect size, not asserted. The feedback loop will say "too few to find patterns" — and will report that our own match scoring is not predicting responses, if that is what the data shows.

Insights are shown, never applied. Nothing in the feedback loop alters your preferences or rules. A system that quietly reweighted a job search off a twelve-application sample would be worse than one that stayed silent.

Rules are deterministic and fail closed. A rule fires only when every condition passes, and an unknown value never satisfies a condition — "salary is unknown" cannot pass "salary >= X", because that condition may be gating an auto-submit. A rule with no conditions never fires. Everything a rule does is written to the application's timeline naming the rule, so "why did this happen?" always has an answer on the page.

Submission passes a gate that cannot be skipped. SubmissionAuthorization is a branded type only authorizeSubmission can produce, and it checks an approved resume, every sensitive answer confirmed, no unanswered required questions, the daily cap, site automatability, and that it has not already been submitted. Submitting without those checks is a type error, not a code-review question. Adding require my approval to any rule vetoes auto-submit entirely.

The agent never submits. prepare fills the form and screenshots it; there is no branch from preparation to clicking submit. Submission is the person's action, recorded afterwards. A required field the agent cannot confidently classify becomes a blocker rather than a guess, and consent checkboxes are never ticked on your behalf.

Credentials never enter the system. Password fields are dropped at the snapshot boundary, before anything is logged or stored. Automation traces mask personal data (de**@example.com) and summarize long answers by length, so an execution log is useful for debugging without archiving what you wrote.


Security

  • Auth.js with bcrypt (cost 12) and optional GitHub OAuth; JWT sessions.
  • Authorization re-checked server-side on every render and every request. Ownership is part of the database query, not an assumption.
  • AES-256-GCM for stored OAuth tokens and site credentials. Credentials are never logged, never included in agent traces, and never sent to an LLM.
  • Uploads validated by magic bytes, not by extension or declared MIME type.
  • Zod validation at every trust boundary, including queue payloads — a worker never trusts that the producer was the version of the code it expects.
  • Redis-backed rate limiting on auth, uploads, and AI-triggering endpoints.
  • Structured logging with redaction defined centrally, so a credential cannot be logged by an unlucky call site.
  • Secrets only via environment variables; no API key ever reaches the browser.
  • CSP, X-Frame-Options: DENY, nosniff, a restrictive Permissions-Policy, and no X-Powered-By. Framing is denied outright — a dashboard carrying automation controls is a clickjacking target.
  • GET /api/health reports each dependency separately (503 only when the database is down; Redis unavailable is degraded, since pages still render). It exposes no hostnames, versions, or configuration.

Build status

Phase Scope Status
1 Setup, auth, database, UI shell, onboarding, profile intelligence Complete
2 Job model, discovery, normalization, dedupe, matching, ranking Complete
3 Resume tailoring, ATS analysis, versioning, cover letters, question generation Complete
4 Playwright, form detection, assisted application, approval workflow Complete
5 Automation rules, auto-apply, application tracking Complete
6 Analytics, feedback loop, command center, hardening Complete

Routes for unbuilt phases exist and run real queries. Until there is data they render an empty state that explains what the feature does, rather than showing invented sample rows.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages