diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml
index 9f70b2e..dcb0b48 100644
--- a/.forgejo/workflows/ci.yml
+++ b/.forgejo/workflows/ci.yml
@@ -29,6 +29,9 @@ jobs:
- name: Install dependencies
run: pip install -r requirements.txt
+ - name: Install lint tools
+ run: pip install ruff
+
- name: Lint
run: ruff check .
diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml
index 83d273d..5b1f887 100644
--- a/.forgejo/workflows/mirror.yml
+++ b/.forgejo/workflows/mirror.yml
@@ -1,6 +1,7 @@
# Mirror push to GitHub and Codeberg on every push to main or tag.
# Copied from Circuit-Forge/cf-agents workflows/mirror.yml
-# Required secrets: GITHUB_MIRROR_TOKEN, CODEBERG_MIRROR_TOKEN
+# Required secrets: GH_MIRROR_TOKEN, CODEBERG_MIRROR_TOKEN
+# Note: Forgejo reserves the GITHUB_* prefix for secret names — use GH_* instead.
name: Mirror
@@ -19,10 +20,10 @@ jobs:
- name: Mirror to GitHub
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_MIRROR_TOKEN }}
+ GH_MIRROR_PAT: ${{ secrets.GH_MIRROR_TOKEN }}
REPO: ${{ github.event.repository.name }}
run: |
- git remote add github "https://x-access-token:${GITHUB_TOKEN}@github.com/CircuitForgeLLC/${REPO}.git"
+ git remote add github "https://x-access-token:${GH_MIRROR_PAT}@github.com/CircuitForgeLLC/${REPO}.git"
git push github --mirror
- name: Mirror to Codeberg
diff --git a/dev-api.py b/dev-api.py
index eaf8094..82bf7b2 100644
--- a/dev-api.py
+++ b/dev-api.py
@@ -14,7 +14,6 @@ import sqlite3
import ssl as ssl_mod
import subprocess
import sys
-import threading
from contextvars import ContextVar
from datetime import datetime, timezone
from pathlib import Path
@@ -39,7 +38,7 @@ if str(PEREGRINE_ROOT) not in sys.path:
from circuitforge_core.api import make_feedback_router as _make_feedback_router # noqa: E402
from circuitforge_core.config.settings import load_env as _load_env # noqa: E402
-from scripts.credential_store import get_credential, set_credential, delete_credential # noqa: E402
+from scripts.credential_store import get_credential, set_credential # noqa: E402
DB_PATH = os.environ.get("STAGING_DB", "/devl/job-seeker/staging.db")
@@ -738,7 +737,6 @@ def preview_resume_review(job_id: int, body: ResumeReviewBody):
3. render_resume_text() — renders to plain text for the preview panel
Returns: {preview_text, preview_struct} — struct preserved for the approve step.
"""
- import json as _json
from scripts.db import get_resume_draft as _get_draft
from scripts.resume_optimizer import (
apply_review_decisions, frame_skill_gaps, render_resume_text,
@@ -759,7 +757,6 @@ def preview_resume_review(job_id: int, body: ResumeReviewBody):
# Step 2: inject gap framing for rejected skills (adjacent / learning)
framings = [f.model_dump() for f in body.gap_framings if f.mode in ("adjacent", "learning")]
if framings:
- db_path_obj = Path(_request_db.get() or DB_PATH)
job_row = _get_db().execute(
"SELECT title, company FROM jobs WHERE id=?", (job_id,)
).fetchone()
@@ -829,7 +826,6 @@ def approve_resume(job_id: int, body: dict):
saved_resume_id: int | None = None
if body.get("save_to_library"):
from scripts.db import create_resume as _create_r
- import json as _json2
resume_name = (body.get("resume_name") or "").strip() or f"Optimized for job {job_id}"
saved = _create_r(
db_path,
@@ -926,7 +922,7 @@ def create_resume_endpoint(body: dict):
@app.post("/api/resumes/import")
async def import_resume_endpoint(file: UploadFile, name: str = ""):
- import os, tempfile, json as _json
+ import json as _json
from scripts.db import create_resume as _create
db_path = Path(_request_db.get() or DB_PATH)
content = await file.read()
@@ -1128,6 +1124,35 @@ def set_job_resume_endpoint(job_id: int, body: dict):
# context. Avocet then routes these prompts through different local models to
# compare generation quality against the real Peregrine pipeline.
+_SYNTHETIC_JOB = {
+ "id": 0,
+ "title": "Senior Software Engineer",
+ "company": "Acme Corp",
+ "description": (
+ "We are looking for a Senior Software Engineer to join our platform team. "
+ "You will design and build scalable backend services in Python and Go, "
+ "contribute to our event-driven architecture using Kafka and Redis, and "
+ "mentor junior engineers. We value clear communication, strong code review "
+ "practices, and an ownership mindset.\n\n"
+ "Requirements:\n"
+ "- 5+ years of backend engineering experience\n"
+ "- Proficiency in Python or Go; experience with both is a plus\n"
+ "- Solid understanding of distributed systems and API design (REST/gRPC)\n"
+ "- Experience with containerization (Docker/Kubernetes)\n"
+ "- Comfort working in a remote-first, async team environment\n\n"
+ "Nice to have:\n"
+ "- Experience with Kafka or other message-queue systems\n"
+ "- Open-source contributions\n"
+ "- Familiarity with observability tooling (Prometheus, Grafana)\n"
+ ),
+ "status": "applied",
+ "cover_letter": "",
+ "raw_output": "",
+ "company_brief": "",
+ "ats_gap_report": "",
+ "talking_points": "",
+}
+
def _imitate_load_profile():
"""Load UserProfile from config/user.yaml, or None if missing."""
try:
@@ -1157,6 +1182,9 @@ def _imitate_cover_letter(db, profile, limit: int) -> dict:
except Exception:
corpus = []
+ if not rows:
+ rows = [_SYNTHETIC_JOB]
+
samples = []
for r in rows:
desc = r["description"] or ""
@@ -1213,6 +1241,9 @@ def _imitate_company_research(db, profile, limit: int) -> dict:
except Exception:
pass
+ if not rows:
+ rows = [_SYNTHETIC_JOB]
+
samples = []
for r in rows:
jd = (r["description"] or "")[:1500].strip()
@@ -1270,6 +1301,10 @@ def _imitate_interview_prep(db, profile, limit: int) -> dict:
).fetchall()
name = profile.name if profile else "the candidate"
+
+ if not rows:
+ rows = [_SYNTHETIC_JOB]
+
samples = []
for r in rows:
system_prompt = (
@@ -1324,6 +1359,9 @@ def _imitate_ats_resume(db, profile, limit: int) -> dict:
pass
resume_block = f"\n## Current Resume\n{resume_text}" if resume_text else ""
+ if not rows:
+ rows = [_SYNTHETIC_JOB]
+
samples = []
for r in rows:
desc = (r["description"] or "")[:1500].strip()
@@ -1462,14 +1500,8 @@ def calendar_push(job_id: int):
# ── Survey endpoints ─────────────────────────────────────────────────────────
# Module-level imports so tests can patch dev_api.LLMRouter etc.
-from scripts.llm_router import LLMRouter
-from scripts.db import insert_survey_response, get_survey_responses
+from scripts.db import insert_survey_response, get_survey_responses # noqa: E402
-from scripts.survey_assistant import (
- SURVEY_SYSTEM as _SURVEY_SYSTEM,
- build_text_prompt as _build_text_prompt,
- build_image_prompt as _build_image_prompt,
-)
@app.get("/api/vision/health")
@@ -2690,7 +2722,7 @@ def config_user():
# ── Settings: My Profile endpoints ───────────────────────────────────────────
-from scripts.user_profile import load_user_profile, save_user_profile
+from scripts.user_profile import load_user_profile, save_user_profile # noqa: E402
def _user_yaml_path() -> str:
@@ -4352,7 +4384,8 @@ def _fetch_cforch_nodes() -> list[dict]:
if not url:
return []
try:
- import urllib.request, json as _json
+ import urllib.request
+ import json as _json
req = urllib.request.Request(f"{url}/api/nodes", headers={"Accept": "application/json"})
with urllib.request.urlopen(req, timeout=3) as resp:
data = _json.loads(resp.read())
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..d3f3a21
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,32 @@
+[tool.ruff]
+# app/ is the deprecated Streamlit UI (replaced by Vue+FastAPI).
+# No new work goes there; exclude from linting rather than accumulate suppressions.
+exclude = ["app/"]
+
+[tool.ruff.lint.per-file-ignores]
+# dev-api.py / dev_api.py (symlink): E702 semicolons in compact Pydantic model
+# definitions — intentional style for dense data models with many simple fields.
+"dev-api.py" = ["E702"]
+"dev_api.py" = ["E702"]
+
+# finetune_local.py: E402 ML libs (torch, datasets, trl) are imported after
+# runtime CUDA / Unsloth availability checks — conditional import pattern.
+"scripts/finetune_local.py" = ["E402", "E741"]
+
+# scripts/: E402 mid-file imports used for lazy loading or post-env-setup imports.
+"scripts/task_runner.py" = ["E402"]
+"scripts/migrate.py" = ["E741"]
+
+# scrapers/: third-party script; minimal changes policy.
+"scrapers/companyScraper.py" = ["E722"]
+
+# tools/: deprecated label tool copy (canonical in avocet); suppress style warnings.
+"tools/label_tool.py" = ["E741"]
+
+# tests/: F841 unused variables are the standard mock-patch capture pattern
+# (e.g., `original_fn = obj.method` before monkeypatching).
+# E741 ambiguous `l` names and E402 conditional imports are common in test fixtures.
+# E702 compact `con.commit(); con.close()` is a common SQLite test helper idiom.
+"tests/**" = ["F841", "E741", "E402", "E702"]
+"tests/test_wizard_steps.py" = ["F841", "E741", "E402", "E702"]
+"scripts/test_email_classify.py" = ["E402", "F841"]
diff --git a/scrapers/companyScraper.py b/scrapers/companyScraper.py
index 1a01d83..61add58 100755
--- a/scrapers/companyScraper.py
+++ b/scrapers/companyScraper.py
@@ -14,7 +14,6 @@ Enhanced features:
import argparse
import csv
-import json
import os
import random
import re
diff --git a/scripts/benchmark_classifier.py b/scripts/benchmark_classifier.py
index 2eec77d..34ce405 100644
--- a/scripts/benchmark_classifier.py
+++ b/scripts/benchmark_classifier.py
@@ -31,7 +31,6 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
from scripts.classifier_adapters import (
LABELS,
- LABEL_DESCRIPTIONS,
ClassifierAdapter,
GLiClassAdapter,
RerankerAdapter,
diff --git a/scripts/calendar_push.py b/scripts/calendar_push.py
index 69b50b9..25ab067 100644
--- a/scripts/calendar_push.py
+++ b/scripts/calendar_push.py
@@ -5,7 +5,6 @@ push updates the existing event rather than creating a duplicate.
"""
from __future__ import annotations
-import uuid
import yaml
from datetime import datetime, timedelta, timezone
from pathlib import Path
diff --git a/scripts/db.py b/scripts/db.py
index e015a2b..6ccd2f7 100644
--- a/scripts/db.py
+++ b/scripts/db.py
@@ -121,6 +121,17 @@ CREATE TABLE IF NOT EXISTS survey_responses (
);
"""
+CREATE_RESUME_CORRECTIONS = """
+CREATE TABLE IF NOT EXISTS resume_optimizer_corrections (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ job_id INTEGER NOT NULL REFERENCES jobs(id),
+ section TEXT NOT NULL,
+ proposed_json TEXT NOT NULL,
+ accepted_json TEXT NOT NULL,
+ created_at TEXT DEFAULT (datetime('now'))
+);
+"""
+
CREATE_DIGEST_QUEUE = """
CREATE TABLE IF NOT EXISTS digest_queue (
id INTEGER PRIMARY KEY,
@@ -205,9 +216,10 @@ def _migrate_db(db_path: Path) -> None:
conn.execute("ALTER TABLE background_tasks ADD COLUMN params TEXT")
except sqlite3.OperationalError:
pass # column already exists
- # Ensure references tables exist (CREATE IF NOT EXISTS is idempotent)
+ # Ensure tables that can't be added via ALTER TABLE exist (all idempotent).
conn.execute(CREATE_REFERENCES)
conn.execute(CREATE_JOB_REFERENCES)
+ conn.execute(CREATE_RESUME_CORRECTIONS)
conn.commit()
conn.close()
@@ -223,6 +235,7 @@ def init_db(db_path: Path = DEFAULT_DB) -> None:
conn.execute(CREATE_DIGEST_QUEUE)
conn.execute(CREATE_REFERENCES)
conn.execute(CREATE_JOB_REFERENCES)
+ conn.execute(CREATE_RESUME_CORRECTIONS)
conn.commit()
conn.close()
_migrate_db(db_path)
@@ -1241,3 +1254,76 @@ def set_training_exclusion(db_path: Path, job_id: int, excluded: bool) -> None:
conn.commit()
finally:
conn.close()
+
+
+# ── Resume optimizer corrections ──────────────────────────────────────────────
+
+def save_resume_correction(
+ db_path: Path,
+ job_id: int,
+ section: str,
+ proposed: object,
+ accepted: object,
+) -> None:
+ """Persist a (proposed, accepted) correction pair from the resume review UI.
+
+ Called when a user edits an LLM-proposed value and accepts it. The pair
+ becomes a supervised fine-tuning (SFT) candidate routed through Avocet.
+
+ Args:
+ section: 'summary' or 'experience:
|'
+ proposed: Original LLM output (string for summary, list for bullets).
+ accepted: User-edited value (same type as proposed).
+ """
+ import json as _json
+ conn = sqlite3.connect(db_path)
+ try:
+ conn.execute(
+ """INSERT INTO resume_optimizer_corrections
+ (job_id, section, proposed_json, accepted_json)
+ VALUES (?, ?, ?, ?)""",
+ (job_id, section, _json.dumps(proposed), _json.dumps(accepted)),
+ )
+ conn.commit()
+ finally:
+ conn.close()
+
+
+def get_resume_corrections(
+ db_path: Path,
+ limit: int = 200,
+ job_id: int | None = None,
+) -> list[dict]:
+ """Return pending resume corrections for Avocet export.
+
+ Args:
+ limit: Maximum rows to return.
+ job_id: If set, filter to corrections for a specific job.
+ """
+ import json as _json
+ conn = sqlite3.connect(db_path)
+ conn.row_factory = sqlite3.Row
+ try:
+ if job_id is not None:
+ rows = conn.execute(
+ "SELECT * FROM resume_optimizer_corrections WHERE job_id=? ORDER BY created_at DESC LIMIT ?",
+ (job_id, limit),
+ ).fetchall()
+ else:
+ rows = conn.execute(
+ "SELECT * FROM resume_optimizer_corrections ORDER BY created_at DESC LIMIT ?",
+ (limit,),
+ ).fetchall()
+ finally:
+ conn.close()
+ return [
+ {
+ "id": r["id"],
+ "job_id": r["job_id"],
+ "section": r["section"],
+ "proposed": _json.loads(r["proposed_json"]),
+ "accepted": _json.loads(r["accepted_json"]),
+ "created_at": r["created_at"],
+ }
+ for r in rows
+ ]
diff --git a/scripts/finetune_local.py b/scripts/finetune_local.py
index c096e33..cec91a1 100644
--- a/scripts/finetune_local.py
+++ b/scripts/finetune_local.py
@@ -73,7 +73,7 @@ if not LETTERS_JSONL.exists():
sys.exit(f"ERROR: Dataset not found at {LETTERS_JSONL}\n"
"Run: make prepare-training (or: python scripts/prepare_training_data.py)")
-records = [json.loads(l) for l in LETTERS_JSONL.read_text().splitlines() if l.strip()]
+records = [json.loads(line) for line in LETTERS_JSONL.read_text().splitlines() if line.strip()]
print(f"Loaded {len(records)} training examples.")
# Convert to chat format expected by SFTTrainer
@@ -323,6 +323,6 @@ if gguf_path and gguf_path.exists():
else:
print(f"\n{'='*60}")
print(" Adapter saved (no GGUF produced).")
- print(f" Re-run without --no-gguf to generate a GGUF for Ollama registration.")
+ print(" Re-run without --no-gguf to generate a GGUF for Ollama registration.")
print(f" Adapter path: {adapter_path}")
print(f"{'='*60}\n")
diff --git a/scripts/generate_cover_letter.py b/scripts/generate_cover_letter.py
index 87f4dce..e2ff93b 100644
--- a/scripts/generate_cover_letter.py
+++ b/scripts/generate_cover_letter.py
@@ -186,7 +186,7 @@ def build_prompt(
)
parts.append(f"{recruiter_note}\n")
- parts.append(f"Now write a new cover letter for:")
+ parts.append("Now write a new cover letter for:")
parts.append(f" Role: {title}")
parts.append(f" Company: {company}")
if description:
diff --git a/scripts/integrations/apple_calendar.py b/scripts/integrations/apple_calendar.py
index 3da9b57..9554323 100644
--- a/scripts/integrations/apple_calendar.py
+++ b/scripts/integrations/apple_calendar.py
@@ -1,5 +1,5 @@
from __future__ import annotations
-from datetime import datetime, timedelta, timezone
+from datetime import datetime
from scripts.integrations.base import IntegrationBase
diff --git a/scripts/migrate.py b/scripts/migrate.py
index edf97cf..b1d888a 100644
--- a/scripts/migrate.py
+++ b/scripts/migrate.py
@@ -25,7 +25,6 @@ import argparse
import shutil
import sys
from pathlib import Path
-from textwrap import dedent
import yaml
diff --git a/scripts/preflight.py b/scripts/preflight.py
index 34d7907..46ddd83 100644
--- a/scripts/preflight.py
+++ b/scripts/preflight.py
@@ -348,14 +348,14 @@ def write_compose_override(ports: dict[str, dict]) -> None:
for name, info in to_disable.items():
lines += [
f" {name}: # adopted — host service on :{info['resolved']}",
- f" entrypoint: [\"/bin/sh\", \"-c\", \"sleep infinity\"]",
- f" ports: []",
- f" healthcheck:",
- f" test: [\"CMD\", \"true\"]",
- f" interval: 1s",
- f" timeout: 1s",
- f" start_period: 0s",
- f" retries: 1",
+ " entrypoint: [\"/bin/sh\", \"-c\", \"sleep infinity\"]",
+ " ports: []",
+ " healthcheck:",
+ " test: [\"CMD\", \"true\"]",
+ " interval: 1s",
+ " timeout: 1s",
+ " start_period: 0s",
+ " retries: 1",
]
OVERRIDE_YML.write_text("\n".join(lines) + "\n")
diff --git a/scripts/resume_optimizer.py b/scripts/resume_optimizer.py
index 7b13a20..ff853be 100644
--- a/scripts/resume_optimizer.py
+++ b/scripts/resume_optimizer.py
@@ -19,7 +19,6 @@ from __future__ import annotations
import json
import logging
import re
-from pathlib import Path
from typing import Any
log = logging.getLogger(__name__)
diff --git a/scripts/resume_parser.py b/scripts/resume_parser.py
index ed9f74b..aa7e67e 100644
--- a/scripts/resume_parser.py
+++ b/scripts/resume_parser.py
@@ -9,11 +9,9 @@ Falls back to empty dict on unrecoverable errors — caller shows the form build
from __future__ import annotations
import io
-import json
import logging
import re
import zipfile
-from pathlib import Path
from xml.etree import ElementTree as ET
import pdfplumber
diff --git a/scripts/survey_assistant.py b/scripts/survey_assistant.py
index 9fb4380..f7e9773 100644
--- a/scripts/survey_assistant.py
+++ b/scripts/survey_assistant.py
@@ -7,7 +7,6 @@ FastAPI application. Callable directly or via the survey_analyze background task
from __future__ import annotations
-import json
import logging
from pathlib import Path
from typing import Optional
diff --git a/scripts/task_runner.py b/scripts/task_runner.py
index c66298c..0795176 100644
--- a/scripts/task_runner.py
+++ b/scripts/task_runner.py
@@ -341,7 +341,6 @@ def _run_task(db_path: Path, task_id: int, task_type: str, job_id: int,
prioritize_gaps,
rewrite_for_ats,
hallucination_check,
- render_resume_text,
)
from scripts.user_profile import load_user_profile
diff --git a/scripts/task_scheduler.py b/scripts/task_scheduler.py
index c1be4db..09fb586 100644
--- a/scripts/task_scheduler.py
+++ b/scripts/task_scheduler.py
@@ -15,14 +15,13 @@ Public API (unchanged — callers do not need to change):
from __future__ import annotations
import logging
-import os
import threading
from pathlib import Path
from typing import Callable, Optional
from circuitforge_core.tasks.scheduler import (
- TaskSpec, # re-export unchanged
LocalScheduler as _CoreTaskScheduler,
+ TaskSpec, # noqa: F401 — re-exported as part of public API; tests import from here
)
logger = logging.getLogger(__name__)
diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py
index a567d1a..c0565e4 100644
--- a/tests/e2e/conftest.py
+++ b/tests/e2e/conftest.py
@@ -12,7 +12,7 @@ import pytest
from dotenv import load_dotenv
from playwright.sync_api import Page, BrowserContext
-from tests.e2e.models import ErrorRecord, ModeConfig, diff_errors
+from tests.e2e.models import ErrorRecord, ModeConfig
from tests.e2e.modes.demo import DEMO
from tests.e2e.modes.cloud import CLOUD
from tests.e2e.modes.local import LOCAL
diff --git a/tests/e2e/test_interactions.py b/tests/e2e/test_interactions.py
index 7bc9adb..af57a6b 100644
--- a/tests/e2e/test_interactions.py
+++ b/tests/e2e/test_interactions.py
@@ -9,9 +9,9 @@ from __future__ import annotations
import pytest
from tests.e2e.conftest import (
- wait_for_streamlit, get_page_errors, screenshot_on_fail,
+ wait_for_streamlit, screenshot_on_fail,
)
-from tests.e2e.models import ModeConfig, diff_errors
+from tests.e2e.models import diff_errors
from tests.e2e.pages.home_page import HomePage
from tests.e2e.pages.job_review_page import JobReviewPage
from tests.e2e.pages.apply_page import ApplyPage
diff --git a/tests/e2e/test_smoke.py b/tests/e2e/test_smoke.py
index fc02b8a..956596b 100644
--- a/tests/e2e/test_smoke.py
+++ b/tests/e2e/test_smoke.py
@@ -7,8 +7,7 @@ Run: pytest tests/e2e/test_smoke.py --mode=demo
from __future__ import annotations
import pytest
-from tests.e2e.conftest import wait_for_streamlit, get_page_errors, get_console_errors, screenshot_on_fail
-from tests.e2e.models import ModeConfig
+from tests.e2e.conftest import wait_for_streamlit, screenshot_on_fail
from tests.e2e.pages.home_page import HomePage
from tests.e2e.pages.job_review_page import JobReviewPage
from tests.e2e.pages.apply_page import ApplyPage
diff --git a/tests/test_app_gating.py b/tests/test_app_gating.py
index 796960f..eb07b62 100644
--- a/tests/test_app_gating.py
+++ b/tests/test_app_gating.py
@@ -1,4 +1,3 @@
-from pathlib import Path
import yaml
from scripts.user_profile import UserProfile
diff --git a/tests/test_backup.py b/tests/test_backup.py
index a02ccfe..d57d7d6 100644
--- a/tests/test_backup.py
+++ b/tests/test_backup.py
@@ -1,12 +1,10 @@
"""Tests for scripts/backup.py — create, list, restore, and multi-instance support."""
from __future__ import annotations
-import json
import zipfile
from pathlib import Path
from unittest.mock import MagicMock, patch
-import pytest
from scripts.backup import (
_decrypt_db_to_bytes,
diff --git a/tests/test_byok_guard.py b/tests/test_byok_guard.py
index a662dd6..3c88a5d 100644
--- a/tests/test_byok_guard.py
+++ b/tests/test_byok_guard.py
@@ -1,5 +1,4 @@
"""Tests for BYOK cloud backend detection."""
-import pytest
from scripts.byok_guard import is_cloud_backend, cloud_backends
diff --git a/tests/test_calendar_push.py b/tests/test_calendar_push.py
index 7880745..93850ad 100644
--- a/tests/test_calendar_push.py
+++ b/tests/test_calendar_push.py
@@ -8,7 +8,6 @@ from datetime import timezone
from pathlib import Path
from unittest.mock import MagicMock, patch
-import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
diff --git a/tests/test_cloud_session.py b/tests/test_cloud_session.py
index 00376f0..69e33df 100644
--- a/tests/test_cloud_session.py
+++ b/tests/test_cloud_session.py
@@ -1,7 +1,4 @@
-import pytest
-import os
-from unittest.mock import patch, MagicMock
-from pathlib import Path
+from unittest.mock import patch
def test_resolve_session_is_noop_in_local_mode(monkeypatch):
diff --git a/tests/test_cover_letter.py b/tests/test_cover_letter.py
index 4903ced..a469a1c 100644
--- a/tests/test_cover_letter.py
+++ b/tests/test_cover_letter.py
@@ -1,6 +1,4 @@
# tests/test_cover_letter.py
-import pytest
-from pathlib import Path
from unittest.mock import patch, MagicMock
@@ -90,7 +88,7 @@ def test_find_similar_letters_returns_top_k():
def test_load_corpus_returns_list():
"""load_corpus returns a list (empty if LETTERS_DIR absent) without crashing."""
- from scripts.generate_cover_letter import load_corpus, LETTERS_DIR
+ from scripts.generate_cover_letter import load_corpus
corpus = load_corpus()
assert isinstance(corpus, list)
diff --git a/tests/test_cover_letter_refinement.py b/tests/test_cover_letter_refinement.py
index c6ebc84..5103d10 100644
--- a/tests/test_cover_letter_refinement.py
+++ b/tests/test_cover_letter_refinement.py
@@ -95,7 +95,6 @@ class TestTaskRunnerCoverLetterParams:
patch("sqlite3.connect") as mock_conn, \
patch("scripts.task_runner.generate_cover_letter_fn", mock_generate, create=True):
- import sqlite3
mock_row = MagicMock()
mock_row.__iter__ = lambda s: iter(job.items())
mock_row.keys = lambda: job.keys()
diff --git a/tests/test_craigslist.py b/tests/test_craigslist.py
index 1fccaf4..8a7d03a 100644
--- a/tests/test_craigslist.py
+++ b/tests/test_craigslist.py
@@ -4,7 +4,6 @@ from email.utils import format_datetime
from unittest.mock import patch, MagicMock
import xml.etree.ElementTree as ET
-import pytest
import requests
diff --git a/tests/test_db.py b/tests/test_db.py
index b8b1331..47bab3e 100644
--- a/tests/test_db.py
+++ b/tests/test_db.py
@@ -1,7 +1,4 @@
-import pytest
import sqlite3
-from pathlib import Path
-from unittest.mock import patch
def test_init_db_creates_jobs_table(tmp_path):
diff --git a/tests/test_db_migrate.py b/tests/test_db_migrate.py
index 8da4a24..a1e0598 100644
--- a/tests/test_db_migrate.py
+++ b/tests/test_db_migrate.py
@@ -1,7 +1,6 @@
"""Tests for scripts/db_migrate.py — numbered SQL migration runner."""
import sqlite3
-import textwrap
from pathlib import Path
import pytest
diff --git a/tests/test_db_resumes.py b/tests/test_db_resumes.py
index f02a946..13bf6f4 100644
--- a/tests/test_db_resumes.py
+++ b/tests/test_db_resumes.py
@@ -1,7 +1,5 @@
"""Tests for resume library db helpers."""
import sqlite3
-import tempfile
-from pathlib import Path
import pytest
diff --git a/tests/test_demo_guard.py b/tests/test_demo_guard.py
index 63cfa78..9f5e176 100644
--- a/tests/test_demo_guard.py
+++ b/tests/test_demo_guard.py
@@ -1,6 +1,5 @@
"""IS_DEMO write-block guard tests."""
import importlib
-import os
import sqlite3
import pytest
diff --git a/tests/test_demo_toolbar.py b/tests/test_demo_toolbar.py
index c7cb155..1b19719 100644
--- a/tests/test_demo_toolbar.py
+++ b/tests/test_demo_toolbar.py
@@ -1,7 +1,6 @@
"""Tests for app/components/demo_toolbar.py."""
import sys
from pathlib import Path
-import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
diff --git a/tests/test_dev_api_digest.py b/tests/test_dev_api_digest.py
index 71a0a08..05c00ff 100644
--- a/tests/test_dev_api_digest.py
+++ b/tests/test_dev_api_digest.py
@@ -1,6 +1,5 @@
"""Tests for digest queue API endpoints."""
import sqlite3
-import os
import pytest
from fastapi.testclient import TestClient
diff --git a/tests/test_dev_api_interviews.py b/tests/test_dev_api_interviews.py
index eeb1eb5..68e0947 100644
--- a/tests/test_dev_api_interviews.py
+++ b/tests/test_dev_api_interviews.py
@@ -1,7 +1,5 @@
"""Tests for new dev-api.py endpoints: stage signals, email sync, signal dismiss."""
import sqlite3
-import tempfile
-import os
import pytest
from fastapi.testclient import TestClient
diff --git a/tests/test_dev_api_prep.py b/tests/test_dev_api_prep.py
index b0d20f8..4a5f964 100644
--- a/tests/test_dev_api_prep.py
+++ b/tests/test_dev_api_prep.py
@@ -1,5 +1,4 @@
"""Tests for interview prep endpoints: research GET/generate/task, contacts GET."""
-import json
import pytest
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
@@ -17,7 +16,6 @@ def client():
def test_get_research_found(client):
"""Returns research row (minus raw_output) when present."""
- import sqlite3
mock_row = {
"job_id": 1,
"company_brief": "Acme Corp makes anvils.",
diff --git a/tests/test_dev_api_settings.py b/tests/test_dev_api_settings.py
index 7985bf7..186a42a 100644
--- a/tests/test_dev_api_settings.py
+++ b/tests/test_dev_api_settings.py
@@ -1,10 +1,9 @@
"""Tests for all settings API endpoints added in Tasks 1–8."""
import os
-import sys
import yaml
import pytest
from pathlib import Path
-from unittest.mock import patch, MagicMock
+from unittest.mock import patch
from fastapi.testclient import TestClient
# credential_store.py was merged to main repo — no worktree path manipulation needed
diff --git a/tests/test_dev_tab.py b/tests/test_dev_tab.py
index 13a59af..275c6e2 100644
--- a/tests/test_dev_tab.py
+++ b/tests/test_dev_tab.py
@@ -1,6 +1,5 @@
import sys
from pathlib import Path
-import yaml
sys.path.insert(0, str(Path(__file__).parent.parent))
diff --git a/tests/test_discover.py b/tests/test_discover.py
index 4a62916..b54e838 100644
--- a/tests/test_discover.py
+++ b/tests/test_discover.py
@@ -1,8 +1,6 @@
# tests/test_discover.py
-import pytest
from unittest.mock import patch, MagicMock
import pandas as pd
-from pathlib import Path
SAMPLE_JOB = {
"title": "Customer Success Manager",
diff --git a/tests/test_e2e_helpers.py b/tests/test_e2e_helpers.py
index 2a38d4a..482882b 100644
--- a/tests/test_e2e_helpers.py
+++ b/tests/test_e2e_helpers.py
@@ -1,5 +1,4 @@
"""Unit tests for E2E harness models and helper utilities."""
-import fnmatch
import pytest
from unittest.mock import patch, MagicMock
import time
diff --git a/tests/test_feedback_api.py b/tests/test_feedback_api.py
index 8c7260a..790c112 100644
--- a/tests/test_feedback_api.py
+++ b/tests/test_feedback_api.py
@@ -1,7 +1,5 @@
"""Tests for the feedback API backend."""
-import pytest
from unittest.mock import patch, MagicMock
-from pathlib import Path
# ── mask_pii ──────────────────────────────────────────────────────────────────
diff --git a/tests/test_imap_sync.py b/tests/test_imap_sync.py
index 348da67..be0830f 100644
--- a/tests/test_imap_sync.py
+++ b/tests/test_imap_sync.py
@@ -1,5 +1,4 @@
"""Tests for imap_sync helpers (no live IMAP connection required)."""
-import pytest
from unittest.mock import patch, MagicMock
@@ -510,7 +509,7 @@ def test_search_folder_special_gmail_name():
def test_get_existing_message_ids_excludes_null(tmp_path):
"""NULL message_id rows are excluded from the returned set."""
import sqlite3
- from scripts.db import init_db, insert_job, add_contact
+ from scripts.db import init_db, insert_job
from scripts.imap_sync import _get_existing_message_ids
db_path = tmp_path / "test.db"
@@ -980,7 +979,6 @@ def test_scan_todo_label_stage_signal_set_for_non_neutral(tmp_path):
def test_scan_todo_label_body_fallback_matches(tmp_path):
"""Company name only in body[:300] still triggers a match (body fallback)."""
- from scripts.db import get_contacts
from scripts.imap_sync import _scan_todo_label
db_path = tmp_path / "test.db"
@@ -1110,7 +1108,6 @@ def test_parse_message_large_body_not_truncated():
def test_parse_message_binary_attachment_no_crash():
"""Email with binary attachment returns a valid dict without crashing."""
from scripts.imap_sync import _parse_message
- import email as _email
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
diff --git a/tests/test_integrations.py b/tests/test_integrations.py
index b2b0604..970ae79 100644
--- a/tests/test_integrations.py
+++ b/tests/test_integrations.py
@@ -72,7 +72,6 @@ def test_fields_returns_list_of_dicts():
def test_save_and_load_config(tmp_path):
"""save_config writes yaml; load_config reads it back."""
from scripts.integrations.base import IntegrationBase
- import yaml
class TestIntegration(IntegrationBase):
name = "savetest"
diff --git a/tests/test_license.py b/tests/test_license.py
index b72a868..0a9d519 100644
--- a/tests/test_license.py
+++ b/tests/test_license.py
@@ -1,7 +1,6 @@
import json
import pytest
from pathlib import Path
-from unittest.mock import patch, MagicMock
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import jwt as pyjwt
diff --git a/tests/test_license_tier_integration.py b/tests/test_license_tier_integration.py
index 0b78481..4e6b9d5 100644
--- a/tests/test_license_tier_integration.py
+++ b/tests/test_license_tier_integration.py
@@ -1,8 +1,6 @@
import json
import pytest
-from pathlib import Path
from datetime import datetime, timedelta, timezone
-from unittest.mock import patch
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
import jwt as pyjwt
diff --git a/tests/test_llm_config_generation.py b/tests/test_llm_config_generation.py
index 5e6bb69..6810cad 100644
--- a/tests/test_llm_config_generation.py
+++ b/tests/test_llm_config_generation.py
@@ -1,4 +1,3 @@
-from pathlib import Path
import yaml
from scripts.user_profile import UserProfile
from scripts.generate_llm_config import apply_service_urls
diff --git a/tests/test_llm_router.py b/tests/test_llm_router.py
index 09451f6..8004747 100644
--- a/tests/test_llm_router.py
+++ b/tests/test_llm_router.py
@@ -110,7 +110,7 @@ def test_complete_without_images_skips_vision_service(tmp_path):
"""When images=None, vision_service backend is skipped."""
import yaml
from scripts.llm_router import LLMRouter
- from unittest.mock import patch, MagicMock
+ from unittest.mock import patch
cfg = {
"fallback_order": ["vision_service"],
diff --git a/tests/test_llm_router_shim.py b/tests/test_llm_router_shim.py
index 23866a0..6bf8671 100644
--- a/tests/test_llm_router_shim.py
+++ b/tests/test_llm_router_shim.py
@@ -1,7 +1,7 @@
"""Tests for Peregrine's LLMRouter shim — priority fallback logic."""
import sys
from pathlib import Path
-from unittest.mock import patch, MagicMock, call
+from unittest.mock import patch, MagicMock
sys.path.insert(0, str(Path(__file__).parent.parent))
@@ -54,7 +54,6 @@ def test_uses_local_yaml_when_present():
def test_falls_through_to_env_when_no_yamls():
"""When no yaml files exist, super().__init__ is called with no args (env-var path)."""
- import scripts.llm_router as shim_mod
from circuitforge_core.llm import LLMRouter as _CoreLLMRouter
captured = {}
diff --git a/tests/test_match.py b/tests/test_match.py
index 25a823e..17cceca 100644
--- a/tests/test_match.py
+++ b/tests/test_match.py
@@ -1,4 +1,3 @@
-import pytest
from unittest.mock import patch, MagicMock
diff --git a/tests/test_messaging_integration.py b/tests/test_messaging_integration.py
index c25c4bb..57b8052 100644
--- a/tests/test_messaging_integration.py
+++ b/tests/test_messaging_integration.py
@@ -1,6 +1,4 @@
"""Integration tests for messaging endpoints."""
-import os
-from pathlib import Path
import pytest
from fastapi.testclient import TestClient
diff --git a/tests/test_mission_domains.py b/tests/test_mission_domains.py
index 6d6ace9..0e8ab9a 100644
--- a/tests/test_mission_domains.py
+++ b/tests/test_mission_domains.py
@@ -4,7 +4,6 @@ import sys
from pathlib import Path
import pytest
-import yaml
sys.path.insert(0, str(Path(__file__).parent.parent))
diff --git a/tests/test_preflight.py b/tests/test_preflight.py
index 82f0319..3de6a50 100644
--- a/tests/test_preflight.py
+++ b/tests/test_preflight.py
@@ -1,10 +1,8 @@
"""Tests for scripts/preflight.py additions: dual-GPU service table, size warning, VRAM check."""
-import pytest
from pathlib import Path
from unittest.mock import patch
import yaml
import tempfile
-import os
# ── Service table ──────────────────────────────────────────────────────────────
diff --git a/tests/test_preflight_env_adoption.py b/tests/test_preflight_env_adoption.py
index 21c4cf9..77b27d5 100644
--- a/tests/test_preflight_env_adoption.py
+++ b/tests/test_preflight_env_adoption.py
@@ -1,7 +1,7 @@
"""Tests: preflight writes OLLAMA_HOST to .env when Ollama is adopted from host."""
import sys
from pathlib import Path
-from unittest.mock import patch, call
+from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).parent.parent))
diff --git a/tests/test_reranker_integration.py b/tests/test_reranker_integration.py
index 50d87a0..e901c02 100644
--- a/tests/test_reranker_integration.py
+++ b/tests/test_reranker_integration.py
@@ -4,7 +4,6 @@
Set CF_RERANKER_MOCK=1 to avoid loading real model weights during tests.
"""
import os
-import pytest
from unittest.mock import patch
os.environ["CF_RERANKER_MOCK"] = "1"
diff --git a/tests/test_resume_optimizer.py b/tests/test_resume_optimizer.py
index 5425a5f..e185f4d 100644
--- a/tests/test_resume_optimizer.py
+++ b/tests/test_resume_optimizer.py
@@ -1,8 +1,7 @@
# tests/test_resume_optimizer.py
"""Tests for scripts/resume_optimizer.py"""
import json
-import pytest
-from unittest.mock import MagicMock, patch
+from unittest.mock import patch
# ── Fixtures ─────────────────────────────────────────────────────────────────
diff --git a/tests/test_resume_sync.py b/tests/test_resume_sync.py
index fa41a67..cce1542 100644
--- a/tests/test_resume_sync.py
+++ b/tests/test_resume_sync.py
@@ -1,6 +1,4 @@
"""Unit tests for scripts.resume_sync — format transform between library and profile."""
-import json
-import pytest
from scripts.resume_sync import (
library_to_profile_content,
profile_to_library,
diff --git a/tests/test_resume_sync_integration.py b/tests/test_resume_sync_integration.py
index 5c8a470..6eb4d56 100644
--- a/tests/test_resume_sync_integration.py
+++ b/tests/test_resume_sync_integration.py
@@ -1,7 +1,5 @@
"""Integration tests for resume library<->profile sync endpoints."""
import json
-import os
-from pathlib import Path
import pytest
import yaml
diff --git a/tests/test_resumes_api.py b/tests/test_resumes_api.py
index 30ea93c..07232fc 100644
--- a/tests/test_resumes_api.py
+++ b/tests/test_resumes_api.py
@@ -1,9 +1,6 @@
"""Tests for /api/resumes/* endpoints."""
-import json
import io
import sqlite3
-import tempfile
-from pathlib import Path
import pytest
from fastapi.testclient import TestClient
diff --git a/tests/test_sync.py b/tests/test_sync.py
index 21c3eea..8d7fa41 100644
--- a/tests/test_sync.py
+++ b/tests/test_sync.py
@@ -1,7 +1,5 @@
# tests/test_sync.py
-import pytest
from unittest.mock import patch, MagicMock
-from pathlib import Path
SAMPLE_FM = {
diff --git a/tests/test_task_runner.py b/tests/test_task_runner.py
index 6167a42..c112056 100644
--- a/tests/test_task_runner.py
+++ b/tests/test_task_runner.py
@@ -1,7 +1,5 @@
-import threading
import time
import pytest
-from pathlib import Path
from unittest.mock import patch
import sqlite3
@@ -178,7 +176,6 @@ def test_submit_task_actually_completes(tmp_path):
def test_run_task_enrich_craigslist_success(tmp_path):
"""enrich_craigslist task calls enrich_craigslist_fields and marks completed."""
from scripts.db import init_db, insert_job, insert_task, get_task_for_job
- from unittest.mock import MagicMock
db = tmp_path / "test.db"
init_db(db)
job_id = insert_job(db, {
@@ -200,7 +197,7 @@ def test_run_task_enrich_craigslist_success(tmp_path):
def test_scrape_url_submits_enrich_craigslist_for_craigslist_job(tmp_path):
"""After scrape_url completes for a craigslist job with empty company, enrich_craigslist is queued."""
- from scripts.db import init_db, insert_job, insert_task, get_task_for_job
+ from scripts.db import init_db, insert_job, insert_task
db = tmp_path / "test.db"
init_db(db)
job_id = insert_job(db, {
@@ -285,7 +282,7 @@ def test_wizard_generate_null_params_fails(tmp_path):
def test_wizard_generate_stores_result_as_json(tmp_path):
"""wizard_generate stores result JSON in error field on success."""
- from unittest.mock import patch, MagicMock
+ from unittest.mock import patch
db = tmp_path / "t.db"
from scripts.db import init_db, insert_task
init_db(db)
@@ -311,7 +308,7 @@ def test_wizard_generate_stores_result_as_json(tmp_path):
def test_wizard_generate_feedback_appended_to_prompt(tmp_path):
"""feedback and previous_result fields in input_data are appended to the prompt."""
- from unittest.mock import patch, MagicMock
+ from unittest.mock import patch
db = tmp_path / "t.db"
from scripts.db import init_db, insert_task
init_db(db)
diff --git a/tests/test_task_scheduler.py b/tests/test_task_scheduler.py
index 38b88ff..3819fee 100644
--- a/tests/test_task_scheduler.py
+++ b/tests/test_task_scheduler.py
@@ -3,7 +3,6 @@
import sqlite3
import threading
from collections import deque
-from pathlib import Path
import pytest
@@ -192,7 +191,6 @@ def test_max_queue_depth_logs_warning(tmp_db, caplog):
"""Queue depth overflow logs a WARNING."""
import logging
from scripts.db import insert_task
- from scripts.task_scheduler import TaskSpec
s = TaskScheduler(tmp_db, _noop_run_task)
s._max_queue_depth = 0 # immediately at limit
diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py
index ca4c338..3c79cf5 100644
--- a/tests/test_telemetry.py
+++ b/tests/test_telemetry.py
@@ -1,6 +1,4 @@
-import pytest
-import os
-from unittest.mock import patch, MagicMock, call
+from unittest.mock import patch, MagicMock
def test_no_op_in_local_mode(monkeypatch):
diff --git a/tests/test_user_profile.py b/tests/test_user_profile.py
index 84c1d72..5ed3c65 100644
--- a/tests/test_user_profile.py
+++ b/tests/test_user_profile.py
@@ -1,7 +1,7 @@
# tests/test_user_profile.py
import pytest
from pathlib import Path
-import tempfile, yaml
+import yaml
from scripts.user_profile import UserProfile
@pytest.fixture
diff --git a/tests/test_wizard_tiers.py b/tests/test_wizard_tiers.py
index a1252c6..1bb9ef0 100644
--- a/tests/test_wizard_tiers.py
+++ b/tests/test_wizard_tiers.py
@@ -4,7 +4,7 @@ from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).parent.parent))
-from app.wizard.tiers import can_use, tier_label, TIERS, FEATURES, BYOK_UNLOCKABLE
+from app.wizard.tiers import can_use, tier_label, TIERS, BYOK_UNLOCKABLE
def test_tiers_list():
diff --git a/tools/label_tool.py b/tools/label_tool.py
index be7ea99..0a7e36e 100644
--- a/tools/label_tool.py
+++ b/tools/label_tool.py
@@ -352,8 +352,8 @@ with tab_fetch:
if not accounts:
st.warning(
- f"No accounts configured. Copy `config/label_tool.yaml.example` → "
- f"`config/label_tool.yaml` and add your IMAP accounts.",
+ "No accounts configured. Copy `config/label_tool.yaml.example` → "
+ "`config/label_tool.yaml` and add your IMAP accounts.",
icon="⚠️",
)
else:
@@ -625,7 +625,7 @@ with tab_stats:
st.markdown(f"**{len(labeled)} labeled emails total**")
# Show known labels first, then any custom labels
- all_display_labels = list(LABELS) + [l for l in counts if l not in LABELS]
+ all_display_labels = list(LABELS) + [lbl for lbl in counts if lbl not in LABELS]
max_count = max(counts.values()) if counts else 1
for lbl in all_display_labels:
if lbl not in counts: