From 48c957da642ea3f53aefbaa1f95781b79cd87c1e Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 24 Feb 2026 19:41:09 -0800 Subject: [PATCH] =?UTF-8?q?feat:=20complete=20generalization=20=E2=80=94?= =?UTF-8?q?=20smoke=20tests,=20README,=20all=20personal=20refs=20extracted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserProfile class drives all personal data - First-run wizard gates app until user.yaml exists - Docker Compose stack: remote/cpu/single-gpu/dual-gpu profiles - Vision service containerized (single-gpu/dual-gpu) - All Alex/Library references removed from app and scripts - Circuit Forge LLC / Peregrine branding throughout Co-Authored-By: Claude Sonnet 4.6 --- README.md | 71 ++++++++++++++++++++++++++++++++++++++++ tests/test_app_gating.py | 23 +++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 README.md create mode 100644 tests/test_app_gating.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7ca537 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# Peregrine + +**AI-powered job search pipeline — by [Circuit Forge LLC](https://circuitforge.io)** + +Automates the full job search lifecycle: discovery → matching → cover letters → applications → interview prep. +Privacy-first, local-first. Your data never leaves your machine. + +--- + +## Quick Start + +```bash +git clone https://git.circuitforge.io/circuitforge/peregrine +cd peregrine +cp .env.example .env +docker compose --profile remote up -d +``` + +Open http://localhost:8501 — the setup wizard will guide you through the rest. + +--- + +## Inference Profiles + +| Profile | Services | Use case | +|---------|----------|----------| +| `remote` | app + searxng | No GPU; LLM calls go to Anthropic/OpenAI | +| `cpu` | app + ollama + searxng | No GPU; local models on CPU (slow) | +| `single-gpu` | app + ollama + vision + searxng | One GPU for cover letters + research + vision | +| `dual-gpu` | app + ollama + vllm + vision + searxng | GPU 0 = Ollama, GPU 1 = vLLM | + +Set the profile in `.env`: +```bash +# .env +DOCKER_COMPOSE_PROFILES=single-gpu +``` + +Or select it during the setup wizard. + +--- + +## First-Run Wizard + +On first launch, the app shows a 5-step setup wizard: + +1. **Hardware Detection** — auto-detects NVIDIA GPUs and suggests a profile +2. **Your Identity** — name, email, career summary (used in cover letters and prompts) +3. **Sensitive Employers** — companies masked as "previous employer (NDA)" in research briefs +4. **Inference & API Keys** — Anthropic/OpenAI keys (remote), or Ollama model (local) +5. **Notion Sync** — optional; syncs jobs to a Notion database + +Wizard writes `config/user.yaml`. Re-run by deleting that file. + +--- + +## Email Sync (Optional) + +Peregrine can monitor your inbox for job-related emails (interview requests, rejections, survey links) and automatically update job stages. + +Configure via **Settings → Email** after setup. Requires: +- IMAP access to your email account +- For Gmail: enable IMAP + create an App Password + +--- + +## License + +Core discovery pipeline: [MIT](LICENSE-MIT) +AI features (cover letter generation, company research, interview prep): [BSL 1.1](LICENSE-BSL) + +© 2026 Circuit Forge LLC diff --git a/tests/test_app_gating.py b/tests/test_app_gating.py new file mode 100644 index 0000000..7f53401 --- /dev/null +++ b/tests/test_app_gating.py @@ -0,0 +1,23 @@ +from pathlib import Path +import yaml +from scripts.user_profile import UserProfile + + +def test_wizard_gating_logic(tmp_path): + """Wizard gate should trigger when user.yaml is absent.""" + missing = tmp_path / "user.yaml" + assert not UserProfile.exists(missing) + + +def test_wizard_gating_passes_after_setup(tmp_path): + """Wizard gate should clear once user.yaml is written.""" + p = tmp_path / "user.yaml" + p.write_text(yaml.dump({"name": "Test User", "services": {}})) + assert UserProfile.exists(p) + + +def test_wizard_gating_empty_file_still_exists(tmp_path): + """An empty user.yaml still clears the gate (wizard already ran).""" + p = tmp_path / "user.yaml" + p.write_text("") + assert UserProfile.exists(p)