- Rename user-facing env var CF_ORCH_URL → GPU_SERVER_URL with full
backward-compat alias (closes#116). Priority chain: GPU_SERVER_URL
→ CF_ORCH_URL → orch.circuitforge.tech when CF_LICENSE_KEY present.
Write-back to os.environ[CF_ORCH_URL] keeps all downstream callers
unchanged.
- Add four task-routed llm.yaml backends (cf_cover_letter, cf_ats_rewrite,
cf_job_research, cf_interview_prep) using cf_orch.product + cf_orch.task.
Coordinator resolves model/node from assignments.yaml (closes#115).
- Update compose.yml, compose.cloud.yml, compose.test-cfcore.yml,
.env.example to use GPU_SERVER_URL as primary documented var.
index.html set 'html, body { background: #eaeff8 }' hardcoded.
body paints on top of html — even with html correctly going dark
via CSS variable resolution, the hardcoded body background covered it.
Fix:
- Remove body background from inline style (body is now transparent)
- Add blocking script to read cf-theme/cf-hacker-mode from localStorage
and set data-theme on <html> before first paint (FOUT prevention)
- Add html[data-theme='dark'|'solarized-dark'|'hacker'] rules so the
correct background fires immediately on initial load for all themes
peregrine.css used :root:not([data-theme="hacker"]) in the
prefers-color-scheme:dark block, causing --app-primary-light and
--app-accent-light to resolve to dark navy/brown in every explicit
light theme (light, solarized-light, colorblind) on dark-OS machines.
Changed to :root:not([data-theme]) to match theme.css's pattern,
so the media query only fires in auto mode. Explicit [data-theme="dark"]
block handles the dark-theme-on-light-OS case unchanged.
Also fixed incorrect fallback values in HintChip.vue (#0d1829 → #eaeff8)
and App.vue global toast (#2a3650/#eaeff8 → light-mode values).
Closes: dark elements in light themes on dark-OS machines
Three root causes fixed:
- _section_text_for_prompt: strip existing bullet chars from bullet text before
adding the prompt's own marker (prevents • • text entering the LLM prompt)
- _reparse_experience_bullets: use + quantifier to strip all leading bullet chars,
not just the first (handles • • text from LLM output)
- _apply_section_rewrite (summary): run _clean_summary_markup to remove
markdown * bullets from career_summary before storing in struct
Also adds 'no markdown formatting' to the LLM rewrite prompt CRITICAL RULES.
Summary and experience bullet fields in the review modal are now
editable textareas. Edited values flow through decisions to
apply_review_decisions(), which uses edited_text/edited_bullets when
the section is accepted. Clearing unwanted LLM-added bullets (empty
lines filtered server-side) addresses the extra-bullets issue.
The preview textarea in the apply workspace is also now editable;
approveResume() passes preview_text_override so manual edits survive
the approve step without re-rendering from struct.
Insert Training at step 4 in WIZARD_STEPS (7→8), STEP_LABELS, and
STEP_ROUTES. Bump Identity→5, Inference→6, Search→7, Integrations→8 in
their respective saveStep calls. Cap resumeAt at 8. Await loadStatus()
before loadDbPairs() in FineTuneView onMounted so optedIn is set before
the early-exit guard runs.
Inserts a new optional Training Export step between Resume and Identity
in the setup wizard. Users can opt in to saving cover letters for
fine-tuning dataset export. Consent copy distinguishes local vs. cloud
storage. WIZARD_STEPS bumped to 7; router, and adjacent step
back/next navigation updated accordingly.
- PATCH /api/settings/fine-tune/opt-in — toggle training_export_opt_in in user.yaml
- GET /api/settings/fine-tune/db-pairs — list DB jobs with exclusion flags (403 without opt-in)
- PATCH /api/settings/fine-tune/db-pairs/{id}/exclude|include — per-job exclusion toggle
- GET /api/settings/fine-tune/export — NDJSON streaming download of all training pairs (DB + file)
- POST/GET /api/settings/fine-tune/cloud-request|cloud-status — Phase 2 stubs (501)
- finetune_status now includes opted_in field
- 6 new API tests; all 17 tests pass
Add excluded_from_training column to jobs table (migration 009 + _MIGRATIONS
entry for existing DBs). Add get_db_pairs(), get_training_pairs(), and
set_training_exclusion() helpers for the cover letter training export pipeline.
Add test_training_export.py with 8 tests covering all helpers (all passing).
Premium/ultra users with a custom_writing_model in their session are
routed to that model as the first cf-orch candidate; all other tiers
use the shared Qwen2.5-3B-Instruct base. complete_json() is unchanged
since fine-tuned writing models aren't trained for structured output.
Adds _request_tier and _request_writing_model ContextVars. Resolution
order: USER_WRITING_MODELS env var (Monday path) then Heimdall meta
(future path via peregrine#110).
Vue+FastAPI (api+web services) is the only frontend. The peregrine-cloud
Streamlit container was still running alongside the new stack and is now
removed. Port 8505 freed.
- app/cloud_session.py: CloudSessionFactory(product="peregrine") from
cf-core v0.16.0; get_session / require_tier FastAPI dependencies;
session_middleware_dep sets request-scoped user_id ContextVar
- app/llm.py: _request_user_id ContextVar + set/get helpers;
_allocate_orch_async includes user_id in payload when present so
premium users get their custom model path from cf-orch UserModelRegistry
- app/main.py: session_middleware_dep wired as global FastAPI dependency;
runs on every request, zero function-signature changes needed
Force-added to bypass resume_matcher/ gitignore (CF-specific patch files).
- Add _try_rerank() to job_ranker.py: after stack_score sort, rerank
top-50 candidates by (resume_text, description) cross-encoder relevance
- Add resume_text param to rank_jobs(); graceful no-op when empty
- Add reranker gap-reordering pass in rewrite_for_ats(): gaps sorted by
(jd_text, term) relevance before section grouping and LLM rewrite
- Both integrations fall back silently on ImportError or inference failure
- 13 new tests; CF_RERANKER_MOCK=1 for zero-weight test runs
- Closes#108
- Seed resumes table with a full UX designer base resume (Alex Rivera persona)
- Add ATS gap reports and optimized resumes for Spotify (job 1), Duolingo (job 2), NPR (job 3)
- Each gap report highlights role-specific keyword opportunities (audio UX, gamification, public media)
- Optimized resumes tailor the base resume framing to each company's emphasis
- Seed company_research for Asana (phone_screen), Notion (interviewing), Figma (hired)
- Includes company_brief, ceo_brief, talking_points, tech_brief, funding_brief,
competitors_brief, accessibility_brief for each
- Update demo/config/user.yaml career_summary to match UX designer persona
- Fixes mismatch between "software engineer" summary and UX/design job seeds
- Adds music and education mission preference notes
Six new screenshots (dashboard, card-stack review with approve/reject tint,
apply workspace, interviews kanban) plus an animated GIF of the swipe-review
flow. Adds demo link above the fold.
Replace the legacy Streamlit `app` service with a FastAPI `api` service
running dev_api:app on port 8601. The Vue SPA (nginx) proxies /api/ →
api:8601 internally so no host port is needed on the api container.
Move web service from port 8507 → 8504 to match the documented demo URL
(demo.circuitforge.tech/peregrine via Caddy → host:8504).
--color-primary in dark mode is a medium-light green (#6ab870); white on green
yields ~2.2:1 contrast (fails WCAG AA 4.5:1 minimum). Using --color-surface
(dark navy in dark mode, near-white in light mode) ensures the text always
contrasts strongly with the primary background regardless of theme.
Also tints banner background with 8% primary via color-mix() so it reads as
visually distinct from the page surface without being loud.
- JobCardStack: expose resetCard() to restore card after a blocked action
- JobReviewView: call resetCard() when approve/reject returns false; prevents
card going blank after demo guard blocks the action
- useApi: add 'demo-blocked' to ApiError union; return truthy error from the
403 interceptor so store callers bail early (no optimistic UI update)
- ApplyView: add HintChip to desktop split-pane layout (was mobile-only)
- HintChip: fix text color — --app-primary-light is near-white in light theme,
causing invisible text; switch to --color-text for cross-theme contrast
- vite.config.ts: support VITE_API_TARGET env var for dev proxy override
- migrations/006: add date_posted, hired_feedback columns and references_ table
(columns existed in live DB but were missing from migration history)
- DemoBanner: commit component and test (were untracked)
Adds location blocks for /peregrine/assets/ and /peregrine/ so the SPA
works correctly when accessed via a Caddy prefix that does not strip the
path (e.g. direct host access without reverse proxy stripping).
- CRITICAL: Remove X-CF-Tier header trust from _get_effective_tier; use
Heimdall in cloud mode and APP_TIER env var in single-tenant only
- HIGH: Add update_message_body helper + PUT /api/messages/{id} endpoint;
updateMessageBody store action; approveDraft now persists edits to DB
before calling approve so history always shows the final approved text
- Cleanup: Remove dead canDraftLlm ref, checkLlmAvailable function, and
v-else-if Enable LLM drafts link; show Draft reply button unconditionally
- MEDIUM: Cap GET /api/messages limit param with Query(ge=1, le=1000)
- Test: Update test_draft_without_llm_returns_402 to patch effective_tier
instead of sending X-CF-Tier header
- scripts/messaging.py: add logged_at param to create_message; use provided value or fall back to _now_utc()
- dev-api.py: add logged_at: Optional[str] = None to MessageCreateBody
- web/src/stores/messaging.ts: remove logged_at from Omit, add as optional intersection so callers can pass it through
- web/src/components/MessageLogModal.vue: pass logged_at in handleSubmit payload; move @keydown.esc from backdrop to modal-dialog (which holds focus); compute localNow fresh inside watch so it reflects actual open time