- Anchor CRED_DIR/KEY_PATH to __file__ (not CWD) in credential_store.py
- Fix email PUT: separate password pop from sentinel discard (was fragile or-chain)
- Fix email test: always use stored credential, remove password override path
- Move integrationResults into system store (was view-local — spec violation)
- saveFilePaths/saveDeployConfig write to dedicated error refs, not saveError
- add scripts/credential_store.py (keyring/file/env-ref backends, Fernet encryption)
- email password stored via credential store, never returned in GET
- email GET returns password_set flag; PUT accepts new password or ${ENV_VAR} ref
- move integration actions to store (connectIntegration, testIntegration, disconnectIntegration)
- add tier-gating UI with locked state and upgrade prompt
- move subprocess/socket/imaplib/ssl imports to top level
- add try/except to sync_identity endpoint
- strip id field from mission_preferences save body
- fix NDA v-for key to use company string (not index), add dedup guard
- move imports out of save_user_profile function body
- _parse_message now prefers text/html over text/plain so digest emails
retain href attribute values needed for link extraction
- Strip <head>, <style>, <script> blocks before storing to remove CSS/JS
garbage while keeping anchor tags intact
- Remove [:4000] truncation — digest emails need full body for URL regex
- Update test: large body should NOT be truncated (assert len == 10_000)
- Add tests/e2e/test_smoke.py: page-load error check for all pages
- Add tests/e2e/test_interactions.py: click every interactable, diff
errors, XFAIL expected demo failures, flag regressions as XPASS
- Fix conftest get_page_errors() to use text_content() instead of
inner_text() so errors inside collapsed expanders are captured with
their actual message text (inner_text respects CSS display:none)
- Fix tests/e2e/modes/demo.py base_url to include /peregrine path prefix
(STREAMLIT_SERVER_BASE_URL_PATH=peregrine set in demo container)
App fixes surfaced by the harness:
- task_runner.py: add DEMO_MODE guard for discovery task — previously
crashed with FileNotFoundError on search_profiles.yaml before any
demo guard could fire; now returns friendly error immediately
- 6_Interview_Prep.py: stop auto-triggering LLM session on page load
in demo mode; show "AI features disabled" info instead, preventing
a silent st.error() inside the collapsed Practice Q&A expander
Both smoke and interaction tests now pass clean against demo mode.
Implements idempotent calendar push for Apple Calendar (CalDAV) and
Google Calendar from the Interviews kanban.
- db: add calendar_event_id column (migration) + set_calendar_event_id helper
- integrations/apple_calendar: create_event / update_event via caldav + icalendar
- integrations/google_calendar: create_event / update_event via google-api-python-client;
test() now makes a real API call instead of checking file existence
- scripts/calendar_push: orchestrates push/update, builds event title from stage +
job title + company, attaches job URL and company brief to description,
defaults to noon UTC / 1hr duration
- app/pages/5_Interviews: "Add to Calendar" / "Update Calendar" button shown
when interview date is set and a calendar integration is configured
- environment.yml: pin caldav, icalendar, google-api-python-client, google-auth
- tests/test_calendar_push: 9 tests covering create, update, error handling,
event timing, idempotency, and missing job/date guards
When _profile is None the fallback pattern \w+ only matched the first
word of a two-word sign-off (e.g. 'Alex' from 'Alex Rivera'), silently
dropping the last name. Switch fallback to \w+(?:\s+\w+)? so a full
first+last sign-off is preserved in no-config environments (CI, first run).
When source == "jobgether", build_prompt() injects a recruiter context
note directing the LLM to address the Jobgether recruiter using
"Your client [at {company}] will appreciate..." framing rather than
addressing the employer directly. generate() and task_runner both
thread the is_jobgether flag through automatically.
Replaces the spawn-per-task model for LLM task types with scheduler
routing: cover_letter, company_research, and wizard_generate are now
enqueued via the TaskScheduler singleton for VRAM-aware batching.
Non-LLM tasks (discovery, email_sync, etc.) continue to spawn daemon
threads directly. Adds autouse clean_scheduler fixture to
test_task_runner.py to prevent singleton cross-test contamination.
LinkedIn's unauthenticated public profile only exposes name, summary (truncated),
current employer name, and certifications. Past roles, education, and skills are
blurred server-side behind a login wall — not a scraper limitation.
- Update selectors: data-section='summary' (was 'about'), .profile-section-card
for certs, .visible-list for current experience entry
- Strip login-wall noise injected into summary text after 'see more'
- Skip aria-hidden blurred placeholder experience items
- Add info callout in UI directing users to data export zip for full history
- app.py: wizard gate now reads get_config_dir()/user.yaml instead of
hardcoded repo-level config/ — fixes perpetual onboarding loop in
cloud mode where per-user wizard_complete was never seen
- app.py: page title corrected to "Peregrine"
- cloud_session.py: add get_config_dir() returning per-user config path
in cloud mode, repo config/ locally
- cloud_session.py: replace st.error() with JS redirect on missing/invalid
session token so users land on login page instead of error screen
- Home.py, 4_Apply.py, migrate.py: remove remaining AIHawk UI references
T13: Three fixes:
1. backup.py: _decrypt_db_to_bytes() decrypts SQLCipher DB before archiving
so the zip is portable to any local Docker install (plain SQLite).
2. backup.py: _encrypt_db_from_bytes() re-encrypts on restore in cloud mode
so the app can open the restored DB normally.
3. 2_Settings.py: _base_dir uses get_db_path().parent in cloud mode (user's
per-tenant data dir) instead of the hardcoded app root; db_key wired
through both create_backup() and restore_backup() calls.
6 new cloud backup tests + 2 unit tests for SQLCipher helpers (pysqlcipher3
mocked — not available in the local conda test env). 419/419 total passing.
cloud_session.py: no-op in local mode; in cloud mode resolves Directus JWT
from X-CF-Session header to per-user db_path in st.session_state.
get_connection() in scripts/db.py: transparent SQLCipher/sqlite3 switch —
uses encrypted driver when CLOUD_MODE=true and key provided, vanilla sqlite3
otherwise. libsqlcipher-dev added to Dockerfile for Docker builds.
6 new cloud_session tests + 1 new get_connection test — 34/34 db tests pass.
Document defensive behavior: openai_compat with no base_url returns True
(cloud) because unknown destination is assumed cloud. Add explanatory
comment to LOCAL_URL_MARKERS for the 0.0.0.0 bind-address case.
Replaces NotImplementedError stub with full LLM-backed implementation.
Builds a prompt from the last 3 resume positions plus already-selected
skills/domains/keywords, calls LLMRouter, and returns de-duped suggestions
in all three categories.
Replaces NotImplementedError stub with a real LLMRouter-backed implementation
that builds a structured prompt covering blocklist alias expansion, values
misalignment, and role-type filtering, then parses the JSON response into
suggested_titles and suggested_excludes lists.
Moves LLMRouter import to module level so tests can patch it at
scripts.suggest_helpers.LLMRouter.
Adds a fully neutered public demo for menagerie.circuitforge.tech/peregrine
that shows the Peregrine UI without exposing any personal data or real LLM inference.
scripts/llm_router.py:
- Block all inference when DEMO_MODE env var is set (1/true/yes)
- Raises RuntimeError with a user-friendly "public demo" message
app/app.py:
- IS_DEMO constant from DEMO_MODE env var
- Wizard gate bypassed in demo mode (demo/config/user.yaml pre-seeds a fake profile)
- Demo banner in sidebar: explains read-only status + links to circuitforge.tech
compose.menagerie.yml (new):
- Separate Docker Compose project (peregrine-demo) on host port 8504
- Mounts demo/config/ and demo/data/ — isolated from personal instance
- DEMO_MODE=true, no API keys, no /docs mount
- Project name: peregrine-demo (run alongside personal instance)
demo/config/user.yaml:
- Generic "Demo User" profile, wizard_complete=true, no real personal info
demo/config/llm.yaml:
- All backends disabled (belt-and-suspenders alongside DEMO_MODE block)
demo/data/.gitkeep:
- staging.db is auto-created on first run, gitignored via demo/data/*.db
.gitignore: add demo/data/*.db
Caddy routes menagerie.circuitforge.tech/peregrine* → 8504 (demo instance).
Personal Peregrine remains on 8502, unchanged.
- _MISSION_SIGNALS: add health category (pharma, clinical, patient care, etc.)
listed last so music/animals/education/social_impact take priority
- _MISSION_DEFAULTS: health note steers toward people-first framing, not
industry enthusiasm — focuses on patients navigating rare/invisible journeys
- _trim_to_letter_end(): cuts output at first sign-off + first name to prevent
fine-tuned models from looping into repetitive garbage after completing letter
- generate(): pass max_tokens=1200 to router (prevents runaway output)
- user.yaml.example: add health + social_impact to mission_preferences,
add candidate_voice field for per-user voice/personality context