This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Description: Polyglot web application enabling recruiters to query a candidate's experience via AI chat with semantic search retrieval Tech Stack: TypeScript (React 19), Python 3.12 (FastAPI), Rust 1.96 (memvid gRPC) Repository: https://github.com/schwichtgit/ai-resume
This is an interactive AI-powered resume site built with Vite, React, TypeScript, shadcn/ui, and Tailwind CSS. The site showcases a candidate's experience with an AI chat interface that answers questions about their background, an honest fit assessment tool, and detailed experience cards.
This project uses the specforge framework for specification-driven development.
| Phase | Command | Artifact | Status |
|---|---|---|---|
| 0 | /specforge constitution |
.specify/memory/constitution.md |
Complete |
| 1 | /specforge spec |
.specify/specs/spec.md |
Complete |
| 2 | /specforge plan |
.specify/specs/plan.md |
Complete (Approved) |
| 3 | /specforge features |
feature_list.json |
Complete (124 features) |
| 4 | /specforge analyze |
Score report | Complete (87.4/100) |
| 5 | /specforge setup |
Setup checklist | Complete |
- Implementation gate: Do not implement features until
/specforge analyzescores >= 80 - Skip-if-current: If an artifact already exists and is current, skip its phase
- Session bootstrap: At the start of each coding session, read: constitution, plan,
feature_list.json - Feature tracking:
feature_list.jsonis the single source of truth for feature status - Feature verification gate: A feature may only be marked
"passes": trueand"verified": trueAFTER every entry in itstesting_stepsarray has been executed and confirmed passing in the current session. Never set these flags speculatively. - Brownfield awareness: 90 features have
verified: false(existing code, not yet validated). 34 features are greenfield (noverifiedfield)
- Code changes and feature implementations
- Security fixes and vulnerability remediation
- Multi-file documentation updates
- Testing and test coverage improvements
- Schema validation and data migration
- Codebase exploration spanning 3+ files
- Launch independent analyses as parallel subagents
- Spawn multiple subagents simultaneously when tasks have no data dependencies
- Use Explore agent for broad codebase research
- Orchestration and task sequencing
- Aggregating subagent results
- User clarification and decision-making
- Single-file reads and quick git commands
- Skill invocations (already isolated)
task setup # Bootstrap full dev environment
task deps # Check tool dependencies
task lint # Lint all services
task test # Test all services
task audit # Whole-SBOM vuln sweep (npm, pip-audit, cargo audit)
task build # Build all services
task check # Full quality sweep (lint + typecheck + test + build)
task ci # Reproduce CI pipeline locally
task dev # Show dev server instructions
task container:build # Build all container images
task clean # Remove build artifactsnpm run dev # Start dev server (http://localhost:8080)
npm run build # Production build
npm run build:dev # Development mode build
npm run preview # Preview production buildnpm run lint # Run ESLint
npm test # Run tests once
npm run test:watch # Run tests in watch mode- Test files:
src/**/*.{test,spec}.{ts,tsx} - Test setup:
src/test/setup.ts - Run all:
npm test - Run a single test file:
npm test -- src/path/to/file.test.ts
- Test files:
e2e/*.spec.ts - Config:
playwright.config.ts - Requires all three services running (frontend, api-service, memvid-service)
- Tests are data-driven (resume-agnostic) -- work with any
.mv2file - Components use
data-testidattributes for stable Playwright selectors
# Install browser (first time only)
npx playwright install chromium
# Run against local dev server (requires services running)
E2E_BASE_URL=http://localhost:8080 npx playwright test
# Run against production
E2E_BASE_URL=https://frank-ai-resume.schwichtenberg.us npx playwright test
# View HTML report after run
npx playwright show-reportCRITICAL: This project uses separate Python virtual environments for each service:
api-service/.venv- FastAPI backend for profile API and gRPC clientingest/.venv- Data ingestion pipeline (memvid-sdk for .mv2 file creation)deployment/.venv- Deployment utilities
When running Python commands, ALWAYS activate the correct venv first:
# For data ingestion or memvid operations
source ingest/.venv/bin/activate
python ingest/ingest.py
# For API service development
source api-service/.venv/bin/activate
python api-service/main.py
# For deployment tasks
source deployment/.venv/bin/activateEach venv is isolated with its own dependencies. Do not run Python scripts without activating the appropriate environment.
Full OpenTelemetry instrumentation across all three services (Python, Rust, TypeScript) with W3C trace context propagation. Grafana/Tempo/Prometheus/Loki stack on a separate observer host. See docs/OBSERVABILITY.md for architecture, dashboards, runbooks, and configuration.
Single-File Portability: ALL content comes from a single .mv2 file generated from markdown:
data/<your_resume>.md (YAML + Markdown)
↓ python ingest.py
data/.memvid/resume.mv2 (Vector DB + Profile JSON)
↓ gRPC + REST API
Frontend Components (Dynamic Rendering)
Data Flow:
- Source: A resume markdown file (e.g.,
data/example_resume.md) - YAML frontmatter + markdown sections - Ingest:
python ingest.py- Parses, chunks, embeds, stores in .mv2 - Storage:
.mv2file contains:- Vector embeddings for semantic search
- Profile metadata (name, title, email, experience, skills, fit examples)
- Chunked resume content
- API: Python FastAPI (
api-service/) loads profile from memvid - Frontend: React hooks (
useProfile) fetch from API
No Hardcoded Data: All components are fully data-driven from the API.
- Page-level:
src/pages/Index.tsx- Main page with section composition - Section components:
Hero,Experience,FitAssessment,AIChat,Header,Footer - Data hooks:
useProfile()- Loads profile from/api/v1/profile - Shared components:
src/components/ui/- shadcn/ui primitives - Layout: Single-page app with smooth scroll sections
AI Chat Interface (src/components/AIChat.tsx):
- Real LLM-powered chat via OpenRouter API
- Semantic search retrieval from memvid (<5ms)
- Streaming responses via Server-Sent Events (SSE)
- Session management with conversation history
- Suggested questions loaded from profile API
Fit Assessment (src/components/FitAssessment.tsx):
- Hybrid Approach:
- Tab 1: Pre-analyzed strong fit example (from profile)
- Tab 2: Pre-analyzed weak fit example (from profile)
- Tab 3: Real-time AI analysis (paste custom job description)
- POST
/api/v1/assess-fitendpoint for live assessments - Structured output: verdict, key matches, gaps, recommendation
Experience Cards (src/components/Experience.tsx):
- Dynamic loading from profile API
- Experience entries with ai_context (situation, approach, technical work, lessons learned)
- Skills categorization (strong/moderate/gaps)
- All data from resume markdown → API
| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check (root-level alias) |
| GET | /api/v1/health |
Health check with dependency status |
| POST | /api/v1/chat |
AI chat with semantic search (supports SSE streaming) |
| GET | /api/v1/profile |
Profile metadata from memvid |
| GET | /api/v1/suggested-questions |
Suggested chat questions from profile |
| POST | /api/v1/assess-fit |
Real-time job fit assessment via AI |
| POST | /api/v1/chat/{session_id}/feedback |
Submit thumbs up/down feedback on responses |
| POST | /api/v1/session/{session_id}/clear |
Clear conversation history for a session |
| DELETE | /api/v1/sessions/{session_id} |
Delete a chat session |
| GET | /api/v1/version |
Build version and commit SHA |
| GET | /api/v1/mcp/config/{client_id} |
MCP client configuration template |
| -- | /mcp |
MCP Streamable HTTP server (opt-out via env) |
| GET | /metrics |
Prometheus metrics (infrastructure) |
- Tailwind CSS v4 with CSS-first configuration (no
tailwind.config.ts) - Design tokens defined via
@theme {}directives insrc/index.css @tailwindcss/viteplugin (replaces PostCSS integration, nopostcss.config.js)tw-animate-cssfor component animations (replaces deprecatedtailwindcss-animate)- shadcn/ui component library for UI primitives
- Custom animations:
fade-in,slide-up,pulse-soft - Utility function:
cn()fromsrc/lib/utils.tsfor conditional classnames
@/maps tosrc/directory (configured in vite.config.ts and tsconfig.json)- Import example:
import { useProfile } from "@/hooks/useProfile"
- Relaxed type checking:
noImplicitAny: false,strictNullChecks: false - Split configs:
tsconfig.json(project),tsconfig.app.json(app),tsconfig.node.json(build tools)
- React Query (TanStack Query) configured in App.tsx but not actively used
- Profile data loaded via
useProfile()hook (fetches from API on mount) - Component-level state with useState (no global state management)
- Routes defined in
src/App.tsx - Current routes:
/(Index),*(NotFound) - Add custom routes ABOVE the catch-all
*route
To add new experience, skills, or fit assessment examples:
- Edit your resume markdown file (see
data/example_resume.mdfor the template) - Update the relevant section:
- Professional Experience: Add
### Company Namesections with role, period, highlights, AI Context - Skills Assessment: Update skills lists under
### Strong Skills,### Moderate Skills,### Gaps - Fit Assessment Examples: Add example job descriptions with assessments
- Professional Experience: Add
- Run
python ingest.py --input data/your_resume.md --output data/.memvid/resume.mv2 - Restart the API service (it will load the new .mv2 file)
- Components automatically render the new data from the API
Key Schema Elements:
- YAML Frontmatter: name, title, email, linkedin, suggested_questions, system_prompt, tags
- AI Context: situation, approach, technical_work, lessons_learned (for each experience)
- Fit Examples: title, fit_level, role, job_description, verdict, key_matches, gaps, recommendation
See data/example_resume.md for a complete template.
Quality is enforced by Claude Code hooks configured in .claude/settings.json:
| Hook | Trigger | Purpose |
|---|---|---|
protect-files.sh |
Pre-edit (Write/Edit) | Blocks modification of sensitive files (.env, keys, credentials, lock files) |
validate-bash.sh |
Pre-bash | Blocks destructive commands (rm -rf /, force push, hard reset) |
validate-pr.sh |
Pre-bash | Validates PR descriptions and targets |
format-changed.sh |
On stop | Auto-formats changed files by detected language (runs before quality checks) |
verify-quality.sh |
On stop | Runs lint, type check, and test suite before session end |
85% line coverage per service (defined in .specify/memory/constitution.md).
ci/principles/commit-gate.md-- enforced on every commitci/principles/pr-gate.md-- enforced on every pull requestci/principles/release-gate.md-- enforced on every release
Each service has its own Dockerfile (frontend/Dockerfile, api-service/Dockerfile, memvid-service/Dockerfile). The build script creates all container images:
Build all containers:
scripts/build-all.sh [tag]Deploy with compose:
cd deployment && podman compose up -dRun locally:
podman run -d -p 8080:8080 --name frank-resume localhost/frank-resume:latestWith custom domain (behind reverse proxy):
podman run -d \
-p 8080:8080 \
--name frank-resume \
localhost/frank-resume:latestThe application expects to be served at frank-resume.schwichtenberg.us when deployed.
- Frontend base: alpine:3.23 with OpenResty (runs as nginx user)
- Frontend build stage: node:26.2.0-trixie-slim for npm build
- API base: ubi10/ubi-micro runtime, rockylinux:10-minimal builder (3-stage build)
- Ingest base: ubi10/ubi-micro runtime, rockylinux:10-minimal builder (3-stage build)
- Memvid base: gcr.io/distroless/cc-debian13:nonroot (runs as nonroot user)
- Frontend port: 8080 (unprivileged)
- API port: 3000
- Memvid port: 50051 (gRPC)
- Health check: GET /health endpoint
The included nginx.conf provides:
- Security headers (X-Frame-Options, CSP, etc.)
- Gzip compression for assets
- SPA routing (all routes return index.html)
- Static asset caching (1 year for immutable files)
- Health check endpoint at /health
The container is designed to run behind a reverse proxy (Traefik, Caddy, nginx) that handles:
- TLS termination
- Domain routing to frank-resume.schwichtenberg.us
- Port mapping from 80/443 to container port 8080
This project uses GitHub Code Scanning with CodeQL to detect security vulnerabilities. Use the /gh-code-scanning skill to manage alerts.
Common Commands:
# List all open code scanning alerts
/gh-code-scanning list
# Get detailed information about an alert
/gh-code-scanning detail <alert-number>
# Fix a security vulnerability
/gh-code-scanning fix <alert-number>
# Dismiss a false positive or won't-fix alert
/gh-code-scanning dismiss <alert-number> <reason>
# Verify an alert has been fixed
/gh-code-scanning verify <alert-number>Dismissal Reasons:
false-positive- Alert is not a real issuewont-fix- Valid issue but won't be fixed (requires rationale)used-in-tests- Only used in test code
Best Practices:
- Prioritize fixing
errorseverity alerts beforewarningornote - Always use the
detailcommand to understand the vulnerability before fixing - Document all dismissals in
docs/SECURITY.mdwith clear rationale - Test fixes to ensure functionality isn't broken
- Use
verifycommand after fixes to confirm resolution
Manual Fallback:
If the skill is unavailable, use GitHub CLI directly:
# List alerts
gh api repos/schwichtgit/ai-resume/code-scanning/alerts \
--jq '.[] | select(.state == "open") | {number, rule: .rule.id, severity: .rule.severity}'
# Get alert detail
gh api repos/schwichtgit/ai-resume/code-scanning/alerts/1
# Dismiss alert
gh api --method PATCH repos/schwichtgit/ai-resume/code-scanning/alerts/1 \
-f state='dismissed' \
-f dismissed_reason='false-positive' \
-f dismissed_comment='Explanation here'Reference:
- Alert types and remediation:
.claude/skills/gh-code-scanning/reference/alert-types.md - Fix example:
.claude/skills/gh-code-scanning/examples/fix-example.md - Dismiss example:
.claude/skills/gh-code-scanning/examples/dismiss-example.md
.githooks/is tracked in git and contains all project git hooks (pre-commit,commit-msg)scripts/install-hooks.shsetsgit config core.hooksPath .githooks(no manual file copying needed)- To install hooks after cloning:
./scripts/install-hooks.sh - To skip hooks temporarily:
git commit --no-verify - To uninstall:
git config --unset core.hooksPath - Pre-commit ESLint: don't use
--max-warnings 0(shadcn/ui warnings are expected) - Pre-commit path fix: strip
frontend/prefix before passing to ESLint when cd'd into frontend/
- Conventional Commits:
type(scope): description - Types:
feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert - Subject line: <= 72 characters, imperative mood
- No emoji, no AI-isms, no Co-Authored-By trailers
- Create focused, atomic commits with clear messages as you work
- Each commit should be a meaningful, standalone change (not WIP or fixup)
- Push individual commits to the feature branch -- do NOT squash before push
- Squash happens at PR merge time (configured in GitHub), not by the developer
- Tone: Technical, direct, and terse
- No AI-isms: No self-references, filler phrases, or marketing adjectives
- No emoji in code, comments, commit messages, or PR descriptions unless explicitly requested
- Conventional Commits for all commit messages (see Git Commit Guidelines)