name: code-review description: Perform a full code review of the currently checked out branch against main. Analyzes best practices, unit tests, DRY code, architecture, error handling, correctness & caller-impact, documentation, DB migration safety, and SvelteKit conventions across the diff for this Python (FastAPI/SQLModel) + SvelteKit project. Use when the user asks to review the branch, review a PR, or do a code review.
code-review
Perform a comprehensive code review of all changes on the currently checked out branch compared to main. Scope is limited to the branch diff — for a whole-codebase audit, use /codebase-audit instead.
This is a mixed-stack repo: a Python 3.12 backend (FastAPI, SQLModel over SQLite, a typer CLI exposed as job-applier) under src/job_applier/, and a SvelteKit frontend (Svelte 5 runes, TypeScript) under web/src/. Reviews must cover whichever side(s) the diff touches.
When to Use
- User asks to review the current branch or an open PR
- User asks for a code review before merging
- User invokes
/code-review
Instructions
Step 1: Identify Changes
Determine the diff between the current branch and main:
git diff main...HEAD --name-only
git diff main...HEAD --stat
If there are no changes vs main, inform the user and stop.
Collect the full diff for context:
git diff main...HEAD
Step 2: Spawn Parallel Review Agents
Launch nine agents in parallel using the Agent tool (subagent_type: general-purpose). Each agent receives the list of changed files and the full diff, and is responsible for one review category.
Critical preamble — include in every agent prompt:
You are performing an exhaustive code review of a Python (FastAPI / SQLModel / typer) + SvelteKit (TypeScript, Svelte 5 runes) project. You must check EVERY item in the checklist below — do not skip items or stop early. For each item, read the relevant code and report either a finding or "PASS". When an item asks you to grep or search, you MUST actually run the search and report the results — do not assume. Your review must be deterministic: given the same code, you should always find the same issues.
Each agent returns a structured report with:
- Severity:
critical,warning, orsuggestion - File path and line number (or range)
- Clear description of the issue
- Suggested fix or code snippet when appropriate
Agent 1: Test Quality & Coverage
-
Read CLAUDE.md — backend tests are
pytestintests/test_<name>.py, use fixtures fromtests/conftest.py(e.g. themake_rawRawJobfactory) and@pytest.mark.parametrizefor table-driven cases; frontend tests arevitestinweb/src/lib/*.test.tsorweb/src/lib/__tests__/*.test.tsusing@testing-library/svelte. Test command:make test(make test-api= pytest,make test-web= vitest). -
List all changed source files under
src/job_applier/(.py) andweb/src/(.ts,.svelte, excluding*.test.ts). For EACH: a. Search for a corresponding test (tests/test_<module>.pyfor backend; colocated*.test.tsfor frontend helpers). Report whether one exists. b. If a test exists, read it. Verify every new/changed public function, API endpoint, filter rule, source adapter, or exported helper has at least one test. c. If NO test file exists, flag per user preference ("always write tests"): new modules / new filter rules / new API routes / new source adapters =critical; new public functions on existing modules =warning; cosmetic-only =suggestion. -
Read every changed test file. For each test: a. Assertions must test outcomes (returned
FilterResult.status, persisted rows, response bodies, store state), not implementation details. b. Flag tests that only assert "is not None" / "does not raise" without checking behavior. c. Prefer fixture reuse: flag backend tests that hand-roll aRawJobinstead of using themake_rawfactory when the factory would do. d. For filter-rule changes, flag missing parametrized cases — the existing suite pins both pass and drop directions per rule; new rules should follow suit. e. Frontend: flag component/store tests that don't exercise the runes-driven state transition they claim to cover. -
For every new
if/match/ early-return branch added in the diff: check that at least one test exercises it. Untested filter outcomes (passed/manual/dropped) or error branches →warning. -
For every new API endpoint, CLI command, or source adapter: check that a test covers the success path AND at least one failure/validation/empty-result path.
Agent 2: DRY Code
-
Read every changed source file (not tests) under
src/job_applier/andweb/src/. For each: a. Any code block (3+ lines) repeated more than once in the same file →warning. b. Any block nearly identical to code in another changed file →warning. -
For each pattern found: grep the full repo (
src/job_applier/,web/src/) for ALL occurrences. Only flag if it appears 3+ times OR extracting it meaningfully reduces maintenance burden. -
Repeated string/integer literals used as source names, filter reasons, status strings, API paths, or DB column names across changed files — flag magic values appearing 3+ times (they belong in a constant, enum, or the shared types in
web/src/lib/api.ts). -
Source-adapter duplication: the per-company adapters (
greenhouse,lever,ashby,workday,workable,smartrecruiters) share shape via theSourceAdapterprotocol insrc/job_applier/sources/base.py. If the diff adds a new adapter or copies fetch/parse boilerplate that base helpers already provide, suggest consolidation. -
Do NOT flag: test verbosity, trivial property/getter repetition, or two-site patterns in clearly different contexts.
Agent 3: Architecture
User is a senior engineer who values cohesive boundaries and dislikes god objects and shared-state desync. Weight findings accordingly.
-
Read CLAUDE.md — layering: FastAPI app + Pydantic schemas in
src/job_applier/api/; the frontend talks to it only throughweb/src/lib/api.ts; source adapters implement theSourceAdapterprotocol and register insrc/job_applier/sources/__init__.py; the hard filter lives insrc/job_applier/filters/rules.py; schema + migrations insrc/job_applier/models/db.py. -
For every changed module: a. God object check: lines of code, number of public functions, number of distinct responsibilities. Flag any module growing past ~400 lines or mixing clearly distinct concerns (e.g. HTTP fetching + parsing + persistence in one function). b. Shared-state desync: does the change introduce a new source of truth that duplicates state the DB already owns (scores, dedupe hashes, application status)? Flag as
criticalif two places now own the same data. Score writes must go through the activeMatchScore+MatchScoreHistorysnapshot path — flag direct mutations that skip history. c. Layer coupling: does new frontend code reach the backend by rawfetchinstead of the typedapiclient inweb/src/lib/api.ts? Flag aswarning. (api.tsbrowser-safety is covered by Agent 9.) d. LLM boundary: the LLM is NEVER called server-side — scoring/drafting happen via slash commands in.claude/commands/. Grep the diff foranthropic,ANTHROPIC_API_KEY, or any server-side model call. Flag ascritical. -
For new source adapters: confirm they implement the
SourceAdapterprotocol and are registered insources/__init__.py; per-company sources must read slugs from theSourceSlugtable at runtime, not hardcode them (companies.pyis seed-only). Flag deviations. -
Right shape for data: backend —
SQLModeltable vs Pydantic schema vs plain dataclass (RawJob). Frontend — Svelte 5 rune store (*.svelte.ts) vs+page.server.tsloader data vs component-local state. Flag mismatches (e.g. cross-route state stuffed into a component instead of a rune store likedraftCart.svelte.ts). -
Feature cohesion: does the change place new files in the correct package (
api/,sources/,filters/,models/) or route folder? Flag files that cut across boundaries without justification.
Agent 4: Python / TypeScript Best Practices
-
Read CLAUDE.md for project conventions. Lint is
make lint(ruff check src/); frontend typecheck iscd web && npm run check. -
For every changed
.pyfile: a. Type hints: public functions have typed parameters and return types; modules start withfrom __future__ import annotations(established style acrosssrc/job_applier/andtests/). Flag missing aswarning. b. No bareexcept:and noexcept Exceptionthat silently swallows. (Error semantics in depth are Agent 5's job; here just flag the syntactic violation.) c. No strayprint()in library code. Grep forprint(insrc/job_applier/; in CLI code usetyper.echo, elsewhere use logging.warning. d. f-strings over%-formatting or.format()in new code.suggestion. e. Ruff cleanliness: flag unused imports, unsorted imports, unused variables thatmake lintwould catch. -
For every changed
.ts/.sveltefile: a. Explicit types on exported functions; noanyleaking into the typedapisurface. Flaganyaswarning. b.import typefor type-only imports. c. Noconsole.logleft in committed code. Grepweb/src/forconsole.log(.warning. d. Code should passnpm run check(svelte-check). Flag obvious type errors. -
Repo-specific hard rules (carry verbatim — these catch more than "follow conventions"): a. No
anthropicSDK / API-key handling server-side — grep new.pyfiles forimport anthropic,from anthropic,ANTHROPIC_API_KEY. Flag ascritical. (See also Agent 3.) b. No LinkedIn / Indeed scraping — grep new source code (especially undersrc/job_applier/sources/) forlinkedin.com/indeed.comendpoints or scraping logic. ToS violation and explicit project rule.critical. c. Generated-draft purity — if the diff touches.claude/commands/draft.mdor draft-rendering code, confirm no em dashes (—), en dashes (–), or smart quotes (""'') are introduced into generated resume/cover-letter output rules. Grep these literal characters.warning. d. No direct commits to main — check the current branch is notmain. If it is, flag ascriticaland stop review.
Agent 5: Error Handling
A focused pass on swallowed errors, unhandled failure paths, and incomplete returns. This is a baseline concern — never fold it into another agent.
-
Read CLAUDE.md for any error-handling / logging conventions.
-
Fallible sinks across every changed file — for each call below, confirm the result is checked or the exception is allowed to surface:
- Python:
httpxcalls (.get/.post/Response.raise_for_status),pypdf.PdfReader(...),pdf.render_to_pdf(...)(headless Chromium/Electron),json.loads(...),Path(...).read_text(...),sqlmodel.Session.exec(...).one()(raises on miss), and any subprocess call. Flag unguarded use where a failure would corrupt state or 500 a request.warning(orcriticalfor ingest/dedupe data paths). - TypeScript / SvelteKit:
await request.json()/request.formData()in actions without a try/catch or guard,fetch(...)without checkingresponse.ok, loaders/actions with implicitundefinedreturns. Flag aswarning.
- Python:
-
Swallowed errors:
- Python: grep changed
.pyforexcept:(bare),except Exception: pass,except Exception: ...that logs and falls through into invalid state, fire-and-forget cleanup. - TypeScript: grep
web/src/diff for barecatch {}andcatch (e) { console.log(e) }with no re-throw or recovery. A caught error must either recover, return a typed failure (e.g.fail(...)in form actions), or re-raise — never log-and-continue into an inconsistent state.
- Python: grep changed
-
Async/await failure paths:
- Python:
async defendpoints thatawaitwithout try/catch when downstream raises should map to a deliberate HTTP status, not a 500 + silent log. - TypeScript: form actions returning
fail(...)on bad input (see theVALID_STATUSguard inweb/src/routes/jobs/[id]/+page.server.ts); flag new actions that throw on user input instead of returningfail().
- Python:
-
Incomplete return paths: functions with a declared return type whose branches can fall through to implicit
None/undefined. Flag inconsistent return paths aswarning.
Agent 6: Correctness & Caller-Impact
Generic runtime-bug hunting plus the highest-value real-bug finder: checking every caller of every changed function. This is a baseline concern — never drop it, never merge it into a domain agent.
-
Generic correctness bugs across the diff — read each changed file and check: a. Nullable / sentinel lookups used unguarded:
- Python:
dict.get(key)→None,re.search(...)→None,next(iter, default)patterns, SQLModelSession.get(Model, id)→None. Flag dereference without aNoneguard →warning(orcriticalon a crash/data-corruption path). - TypeScript:
Array.prototype.find()→undefined,.indexOf()→-1, optional-chaining gaps where anull/undefinedwould slip through. b. Off-by-one: loop bounds,len(x) - 1, range/slice endpoints, SQLLIMIT/OFFSET, inclusive-vs-exclusive mistakes (date ranges, score thresholds). c. Inverted / incorrect boolean logic: wrong negation,and/orprecedence, comparison against the wrong constant or enum value (e.g.FilterStatus.droppedvsFilterStatus.manual,ApplicationStatusmismatches between Python and TS). d. Type confusion masked by a cast: a typed assignment that assumes a shape the data may not have — e.g. casting adict.get(...)to a non-Optional type, TSascasts hidingundefined.
- Python:
-
Caller-impact analysis (do this thoroughly — it is the strongest bug finder here). For EVERY function/method whose signature, return type, return-value semantics, error behavior, persisted-row shape, or response shape changed in the diff: a. Grep the ENTIRE repo (
src/job_applier/,web/src/,tests/) for call sites. Report the count. b. For each caller, verify it still passes correct args, handles the new return type/shape, and accounts for any newly introduced error path, early return, orNone/undefinedresult. c. Cross-language drift is in scope: if a backend API endpoint's request/response shape changed, the typedapiclient inweb/src/lib/api.tsand every+page.server.tsloader/action that calls it must be updated too. Flag mismatches ascritical. d. Flag callers that will now break or silently misbehave ascritical. -
Dangling references: for any function, signal, constant, DB column, or enum value renamed or removed in the diff, grep for now-dangling references to the old name →
critical. -
Partial-state transitions: new branches that leave a row with some fields updated and others stale (e.g. updating
MatchScorewithout snapshottingMatchScoreHistory, updatingApplication.statuswithoutlast_contact_at) →warning.
Agent 7: Documentation
Both halves of this stack have a doc-comment convention (Python docstrings; JSDoc/TSDoc). Goal: catch stale / wrong docs, not just missing ones. Presence-only checks are cheap and miss real bugs — the high-value finds are tag-vs-behavior drift.
-
Read CLAUDE.md for any documentation rules. There is no
pydocstyle/eslint-plugin-jsdocenforcement in this repo, so do NOT demand docstrings everywhere. Use the lint config (or its absence) as a false-positive guard: don't flag missing docs the project deliberately doesn't require. -
Tag-vs-behavior cross-checks (highest value — apply to every changed function with a doc comment): a. Python
Raises:/ "Raises" lines must match the actualraisestatements in the body. Flag drift:Raises: ValueErroron a function that actually raisesHTTPException.warning(orcriticalif a caller relies on the doc). b. PythonReturns:/:return:text must match the actual return type/shape. Flag a docstring saying "Returns the active MatchScore" when the function now returnsNoneon miss. c. PythonArgs:/:param:names and types must match the signature. Flag renamed/added/removed parameters that the docstring still describes the old way. d. TSDoc / JSDoc@param/@returns/@throwsinweb/src/— same drift checks.@throws {Error}when the code throws a specific subclass (or doesn't throw at all) is awarning. -
Top-of-module / top-of-file rationale in
src/job_applier/andweb/src/: a number of modules carry a short "what this is for" docstring (e.g.web/src/lib/api.ts's "must stay browser-safe" comment). If the diff changes the module's actual responsibility, the lead-in docstring/comment must move with it — flag drift. -
Public-surface presence (soft check, no lint backing): for new exported names in
src/job_applier/api/,web/src/lib/api.ts, and any module declared as the public seam, a one-line docstring/JSDoc is recommended but not required. Flag missing assuggestion, neverwarningor above — there is no project rule mandating docs. -
.claude/commands/*.mdslash commands are the primary user-facing documentation for the LLM workflow. If the diff changes scoring rubric, drafting rules, or status transitions, confirmmatch-pending.md/score-draft.md/draft.md/suggest-roles.mdare updated in step. Out-of-sync rubric files betweenmatch-pending.mdandscore-draft.mdare a CLAUDE.md-flagged rule — flag aswarning. -
README / CLAUDE.md drift: if the diff adds a new make target, source adapter, or status state, confirm
README.mdand/orCLAUDE.mdreflect it.suggestion.
Agent 8: DB Migration & Schema Safety
The project deliberately uses no alembic. Schema changes are hand-rolled idempotent _ensure_*_columns() helpers in src/job_applier/models/db.py, run on EVERY startup from init_db() (the block calling SQLModel.metadata.create_all(engine()) then each _ensure_* helper). make prune lightens old rows but must preserve dedupe hashes. Get this wrong and a fresh uv run job-applier init or a returning user's DB silently diverges.
-
Read the migration block in src/job_applier/models/db.py — the
init_db()function and every_ensure_*_columns()helper. Confirm the established pattern: read existing columns viaPRAGMA table_info(<table>), thenALTER TABLE ... ADD COLUMNonly for missing ones. -
For every new or changed
SQLModeltable field in the diff: there MUST be a matching_ensure_*helper that adds the column to an existing DB. A new field with no helper means returning users get a broken schema. Flag ascritical. -
Idempotency: every helper must guard with the
PRAGMA table_infomembership check beforeALTER TABLE. Flag any unconditionalALTER TABLE(it will throw on the second startup) ascritical. -
Wiring: every new
_ensure_*helper must actually be called frominit_db(). Grep to confirm. An orphaned helper is acriticalno-op. -
SQLite limits: SQLite
ALTER TABLEonly reliably supportsADD COLUMN. Flag any attempt to drop, rename, or retype a column via raw SQL, or any addedNOT NULLcolumn without a default (existing rows will violate it) ascritical. -
No alembic / ORM auto-migrate creep: flag any introduction of alembic,
create_allside effects that assume an empty DB, or migration logic outsidedb.py. -
Prune/dedupe safety: if the diff touches
pruneor JD-dedupe logic, confirm dedupe hash columns (cross_source_hash,jd_fingerprint) andduplicate_oflinks are preserved, not cleared. Flag data loss ascritical.
Agent 9: SvelteKit Frontend Conventions
Only run the substantive checks if the diff touches web/. If it doesn't, return "PASS — no frontend changes." The user is learning SvelteKit here and wants idiomatic patterns; convention drift is worth flagging even when it "works."
-
Read CLAUDE.md frontend section and skim web/src/lib/api.ts for the established client shape.
-
Mutations via form actions: status changes and other writes must go through SvelteKit form
actionsin+page.server.ts(seejobs/[id]/+page.server.ts), NOT client-sidefetch. Grep changed.sveltefiles forfetch(that performs a mutation. Flag aswarning. -
Loaders, not onMount fetching: page data should come from a
loadfunction in+page.server.ts, notonMount-driven client fetches. Flag client-side initial-data fetching aswarning. -
api.tsmust stay browser-safe (this is the single highest-impact rule and gets its own check here, not in Architecture): grepweb/src/lib/api.tsand anything it imports for$env/dynamic/private,$env/static/private,node:,fs,path, or any server-only import. Flag ascritical. -
Typed client: new backend calls from the frontend should go through a typed method on the
apiobject inapi.tswith a matching TypeScript type, not ad-hoc inlinefetch+any. Flag untyped responses aswarning. -
Svelte 5 runes: cross-route shared state belongs in a rune store (
*.svelte.ts, likedraftCart.svelte.ts); reactive state should use$state/$derived, not legacy Svelte 4 stores (writable/readable) unless there's a reason. Flag legacy patterns in new code assuggestion. -
Form-action contract: actions should validate input and return
fail(<status>, { error })on bad input rather than throwing (see theVALID_STATUSguard inweb/src/routes/jobs/[id]/+page.server.ts). Flag missing validation on new actions aswarning.
Step 3: Compile & Present Results
After all agents return:
Deduplication: multiple agents may flag the same issue. Keep the highest severity and most actionable description. Do NOT list the same issue twice.
Pre-existing vs new: clearly distinguish issues introduced by this branch from pre-existing issues in touched files. Downgrade pre-existing issues one severity level (critical → warning, warning → suggestion) unless they are data-corruption or schema-safety risks.
Contradiction check: scan all findings for contradictions (e.g., "remove X" vs "X is missing"). Resolve by picking the correct recommendation based on the full context.
Prioritization: within each severity, sort by category in this order: architecture > correctness > migrations > error-handling > tests > sveltekit > docs > DRY > best-practices.
## Code Review: [branch-name]
### Critical Issues
(Must fix before merging)
- [category] file:line — description
### Warnings
(Should fix — could cause problems)
- [category] file:line — description
### Suggestions
(Nice to have — improve code quality)
- [category] file:line — description
### Summary
- X critical, Y warnings, Z suggestions
- Overall: PASS / NEEDS WORK / BLOCK
Verdict logic:
- Any
criticalfinding → BLOCK - More than 3
warningfindings → NEEDS WORK - Otherwise → PASS
Step 4: Auto-Fix on BLOCK / NEEDS WORK
If the verdict is PASS, stop here — present the report and you're done.
If the verdict is BLOCK or NEEDS WORK, do NOT ask permission. Immediately address every high-level item that drove the verdict:
- BLOCK → fix every
criticalfinding. - NEEDS WORK → fix every
warningfinding (criticals too, if somehow present).
What counts as "high level":
- All
criticalandwarningitems in the categories the review reported on (tests, DRY, architecture, best practices, error handling, correctness & caller-impact, documentation, migrations, SvelteKit). - Do NOT auto-apply
suggestionitems — those wait for explicit user direction.
Fix-vs-defer rubric. Default to fixing. A finding is fixable when the correct change is determinable from the code and the review's own description — apply it. "I'm not sure" is not a valid reason to defer: if you're unsure how to fix a real finding, investigate (read the surrounding code, the callers, the tests) until you are sure, then fix it. Defer only for the specific reasons listed below — never out of uncertainty.
How to address them:
- Group findings by file to minimize churn.
- For each finding, make the smallest correct change that resolves the issue. Don't bundle unrelated cleanup.
- Add or update tests when the finding is test-related (missing test file, untested branch, untested endpoint).
- Re-run the project's full verification, not just unit tests. Run
make test(bothtest-apipytest andtest-webvitest). If frontend code changed, also runcd web && npm run checkfor svelte-check. If backend code changed, also runmake lint. If anything fails, fix the failures before reporting done — do not stop at red. - Do NOT commit or push. Leave changes in the working tree so the user can review the diff.
When to defer an item instead of fixing it:
- The fix requires a product/design decision the user hasn't made.
- The fix would expand scope well beyond the current branch (e.g., refactoring a module that pre-dates this branch).
- The finding is on pre-existing code the branch only touched incidentally.
- The fix conflicts with an explicit choice the user defended earlier in the conversation.
Defer means: leave the code as-is and call it out in the final report. Do NOT silently skip. The defer reason must be one of the four above — never "unsure".
Step 5: Final Report
After fixes land, report:
## Auto-Fix Report
### Fixed
- file:line — what changed and why (one line each)
### Deferred
- file:line — finding, and the specific reason it was deferred (must be one of the four defer reasons — never "unsure")
- Suggested next step: how the user should approach it
### Verification
- make test: PASS / FAIL (with failing test names if FAIL)
- make lint (if backend changed): PASS / FAIL
- npm run check (if frontend changed): PASS / FAIL
### Remaining Verdict
- Re-state the verdict after fixes: PASS / NEEDS WORK / BLOCK, and what (if anything) still drives a non-PASS.
Keep the report tight. The user will read the diff for the details — the report is the index.
Next.js App Router Expert
Development
A skill that turns Claude into a Next.js App Router expert.
README Generator
Development
Creates professional and comprehensive README.md files for your projects.
API Documentation Writer
Development
Generates comprehensive API documentation in OpenAPI/Swagger format.