Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
Tradeoff: These guidelines bias toward caution over speed. For trivial tasks, use judgment.
Don't assume. Don't hide confusion. Surface tradeoffs.
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
Minimum code that solves the problem. Nothing speculative.
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
Touch only what you must. Clean up only your own mess.
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
Define success criteria. Loop until verified.
Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
These guidelines are working if: fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
Project root: /home/$USER/www/job-scrapper
Last updated: 2026-05-10 Update this file when: model status changes, new API endpoints, new hard rules, pipeline prompt changes.
Job scraper + fit analyzer + local viewer. It crawls job boards via ATS provider APIs, scores listings against a candidate profile using LLMs, and surfaces results through a local web UI with filtering, analysis pipelines, and a Discord notification system.
job-scrapper/
├── crawler/ # Provider-specific scrapers
├── matcher/ # Python — LLM scoring pipelines
│ ├── job_post_parser.py
│ ├── job_fit_analyzer.py # Quick / Maverick pipeline
│ ├── ensemble_runner.py # Full / Ensemble pipeline
│ ├── benchmark.py
│ ├── benchmark_ensemble.py
│ └── career-ops/ # User-owned profile files — NEVER TOUCH
│ ├── _profile.md
│ ├── profile.yml
│ ├── cv.md
│ └── portals.yml
└── viewer/ # Node/Express + TypeScript
├── src/
│ ├── server.ts # Express API + SQLite
│ └── lib/
│ ├── types.ts
│ ├── queue.ts
│ └── config.ts
└── public/
├── index.html # Active UI (index.old.html = retired Bootstrap UI)
├── feed.js # All UI logic
├── feed.css # All styles
└── saved-searches.json # User-editable search presets
Never run npm / npx / python directly. Always use Docker.
| Change type | Command |
|---|---|
server.ts or any TypeScript |
docker compose build viewer && docker compose up -d viewer |
Static files (feed.js, feed.css, index.html) |
docker compose build viewer && docker compose up -d viewer |
| Python matcher changes | docker compose build matcher |
Rebuild automatically when files change — do not ask for permission first.
| Var | Purpose |
|---|---|
NVIDIA_API_KEY |
Required for all LLM calls |
NVIDIA_MODEL |
Default scorer — overrides Python default |
NVIDIA_ENSEMBLE_SCORERS |
Comma-separated scorer model list |
CATALOG_DB |
/app/state/catalog.sqlite |
CAREER_OPS_DIR |
Profile directory inside matcher, default career-ops |
SCORE_NOTIFY_MIN_SCORE |
Min score threshold for Discord notifications and To-Apply bucket |
| Method | Path | Notes |
|---|---|---|
| GET | /api/jobs |
Paginated job list. Filters: title, location, company, sources, days, page. days filters by first_seen_at |
| GET | /api/job |
Single job + analysis. Params: provider, source_key, job_id |
| GET | /api/job-parsed |
Live parse via job_post_parser. Query params only — Workday source_key contains / |
| POST | /api/match-runs |
Start analysis run. Body: {job_keys, mode} where mode = "maverick" or "ensemble" |
| GET | /api/match-runs/:id |
Poll run status |
| GET | /api/stats |
Job counts by provider |
| GET | /api/config |
Ensemble model config |
| GET | /api/sources |
Available sources |
| GET | /api/queue |
Queue items |
| POST | /api/queue/:id/retry |
Retry a failed queue item |
Greenhouse, Lever, BambooHR, Ashby, Workday, TeamTailor, Workable, SmartRecruiters, iCIMS. HiBob: SPA, not implemented.
- Mechanism: XML sitemap (
https://careers-{slug}.icims.com/sitemap.xml) - Fields available: title (from URL slug), job_url, job_id
- Fields NOT available: location, employment_type, compensation, posted_at (sitemap only)
- Recommended concurrency: 5 (safe), 8–10 (balanced/aggressive)
- Source file:
sources/icims.json— 9,937 slugs
File: job_fit_analyzer.py
Model: meta/llama-4-maverick-17b-128e-instruct (set via NVIDIA_MODEL env var)
Use: default on-demand analysis (~20s)
Pre-scoring checks injected in build_system_prompt():
- Check A — Employment model: Consulting / staffing / professional services firms → cap
target_alignment<= 2.0, add blocker. - Check B — Domain match: If required domain is a must-have with no direct CV evidence → cap
relevant_experience<= 2.0, add blocker. Adjacent experience only counts inmitigation. - Check C — Explicit must-haves with no CV evidence: Count hard requirements with zero direct evidence. 2+ unmet → reduce
requirements_coverageby 1.0 per gap (floor 1.0). 1 unmet → reduce by 0.75. Transferable skills do NOT satisfy hard requirements.
Note: cache_control: {"type": "ephemeral"} on system message. Strip this field if switching to Mistral — it rejects it.
File: ensemble_runner.py
Scorers (parallel): Maverick + Kimi-K2 + Nemotron-super-49b (thinking=on)
Synthesizer: Nemotron-super-49b (thinking=on)
Use: full analysis (~2min)
_PRE_SCORING_CHECKS string is shared by both SCORER_SYSTEM and SYNTHESIS_SYSTEM.
Synthesizer extra rule: "a model that ignored a pre-scoring check is wrong — override it."
max_tokens: scorers = 2000, synthesizer = 2000.
Kimi-K2 JSON failures: occasional parse errors — scorer is skipped, synthesis continues with remaining models.
Providers: Greenhouse, Lever, BambooHR, Ashby, Workday, TeamTailor, SmartRecruiters.
infer_compensation() only matches salary-formatted numbers ([$€£]\d{2,3}[,\s]?\d{3} or [$€£]\d{2,3}[kK]).
STATE_DIR/job-analysis-cache.json — flat JSON file.
Key: provider|source_key|job_id
Value: {analysis, analyzed_at, run_id}
analysis object must contain pipeline field ("maverick" or "ensemble").
SQLite migration discussed but not yet done.
| Model | Status | Notes |
|---|---|---|
meta/llama-4-maverick-17b-128e-instruct |
Active — default scorer | ~6–16s, clean JSON, thin reasoning |
moonshotai/kimi-k2-instruct |
Dead — HTTP 410 EOL | |
moonshotai/kimi-k2.6 |
Active — ensemble scorer | Replacement for kimi-k2-instruct |
nvidia/llama-3.3-nemotron-super-49b-v1.5 |
Active — ensemble scorer + synthesizer | Requires nvext: {"thinking": "on"}. 50–68s. Response in message.content, not reasoning_content |
deepseek-ai/deepseek-v3.2 |
Dead — HTTP 410 EOL | |
deepseek-ai/deepseek-v4-flash |
Removed — severe timeout on 16k payloads | |
mistralai/mistral-large-3-675b-instruct-2512 |
Rejects cache_control field — strip it to use |
API base: https://integrate.api.nvidia.com/v1/chat/completions (OpenAI-compatible)
viewer/public/index.html— main UIviewer/public/feed.js— all UI logicviewer/public/feed.css— all stylesviewer/public/saved-searches.json— search presets (user-editable, not hardcoded in HTML/JS)
- "Quick analyze fit" (
.btn-analyze--quick, blue) — Maverick pipeline. After run: label = "Quick — Re-run" - "Full analyze fit" (
.btn-analyze--full, purple) — Ensemble pipeline. After run: label = "Full — Re-run" - "JD data" (ghost, eye icon) — opens JD data modal (fetches
/api/job-parsed) - "Not interested" (ghost) — hides card
Score badge (.btn-score-badge): 20px pill, appears after analysis, colored blue/purple, clicking opens the side panel.
Both analyze buttons are disabled while either is running. Spinner on active button only.
footerHtml(job, pipeline)— full footer HTML, pipeline-awarebindFooterEvents(footer, job)— all footer event listenerssetMainBtnSpinner(job, label, mode)— disables both buttons, shows spinnerrestoreMainBtn(job)— re-renders footerreapplySpinners()— re-applies spinner state after DOM re-render
- Opens via
.js-open-panel(score badge) - Pipeline badge: "Preview · Maverick" (blue,
.pipeline-maverick) or "Ensemble pipeline" (purple,.pipeline-ensemble) - Panel re-run button uses Maverick mode
Row 1: job title (full width)
Row 2: location / company / providers / max age
Max age filter uses first_seen_at (not last_seen_at).
Queue drawer shows items with statuses: todo, running, retrying, done, error, permanent_error.
State managed via queueDrawerOpen flag and activeAnalysisJobs map.
Queue key format: provider|source_key|job_id
Loaded from /saved-searches.json at startup. Rendered dynamically into #saved-searches-strip.
Current entries: "TPM, SoFlo" and "PM, SoFlo".
Format: {id, label, title, location, company, sources, days}
matcher/career-ops/_profile.md, profile.yml, cv.md, portals.yml are user-owned.
All scoring logic goes exclusively in job_fit_analyzer.py and ensemble_runner.py.
companyWebsite() already returns https://domain. Never wrap it again in https://.
Guard the embed url field: only set it when the value starts with http:// or https://.
Any malformed URL causes Discord to reject the entire payload with 400.
Uses query params for /api/job-parsed, not path params. Required because Workday source_key contains /.
matcher/benchmark.py — single-model benchmark
matcher/benchmark_ensemble.py — standalone ensemble benchmark (not the production runner)
matcher/ensemble_runner.py — production ensemble runner (JSONL interface)
Candidate profile used: Michael Levy, Senior Technical PM, Cooper City FL.
BACKLOG.md — prioritized list of 20 planned features (user-visible ordering).
user-stories/ — one self-contained .md per US with full implementation spec.
user-stories/_context.md — shared context: file locations, DB patterns, LLM notes, dependency graph between US.
When implementing a US: read _context.md first, then the target US file. No need to return to this conversation for context.