# Project export: FirstPass

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: FirstPass is a multi-agent AI firm coordinating live over Band that researches the city codes, reads your plans, and catches its own mistakes. Catch permit problems before the city does.
- Devpost: https://devpost.com/software/firstpass-mdta7f
- GitHub: https://github.com/kiromoussa/FirstPass
- Video: https://www.youtube.com/embed/xaxAYptLR-M?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Kiro Moussa (19 commits), Claude Opus 4.8 (1M context) (16 commits), Cursor (8 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

# FirstPass

![FirstPass landing page — permit-readiness pipeline](docs/images/landing-hero.png)

**AI-powered pre-submission permit-readiness assistant** for architecture and building projects.

Upload a plan set (PDF or DWG), and FirstPass runs a multi-agent pipeline: it resolves the jurisdiction, researches the governing building codes, reads your plans, runs compliance checks, audits the findings, and produces a **cited permit-readiness report** with a score, flagged issues, and a missing-documents checklist.

> FirstPass is a *pre-submission compliance assistant*, not official permit approval. Findings indicate *likely* issues for early correction and require confirmation by a licensed professional and the governing jurisdiction.

**Built for the [UC Berkeley AI Hackathon 2026](https://berkeley.ai)** by Kiro Moussa, Varun Sanjeev, David Pelazini, and Krishiv Bhatia.  
**[Devpost submission →](https://devpost.com/software/firstpass-mdta7f)**

**Demo video:** [youtu.be/xaxAYptLR-M](https://youtu.be/xaxAYptLR-M)

---

## The problem

Most permit submissions don't pass on the first try. Rules live across municipal portals, state amendments, and PDF code books — technically public, but hard to navigate without a compliance team. Architecture firms lose weeks (and clients) to avoidable rejections.

FirstPass is the **first pass** a plan set gets before a city plan reviewer sees it: catch likely violations early, with official citations and retrieval dates, in minutes instead of months.

---

## How it works

```
Upload plans → Jurisdiction → Code research → Plan reading → Compliance → Review → Report
```

1. **Jurisdiction** — Resolve the city and responsible agencies from the project address.
2. **Research** — Agents browse official municipal and state code sources (live via Browserbase, or cached corpus in Redis).
3. **Read** — Claude vision extracts structured facts from the plan set (setbacks, height, unit size, sheet inventory).
4. **Comply** — Deterministic checks compare extracted facts against jurisdiction rules.
5. **Review** — An auditor agent challenges findings; Arize evals catch misapplied rules and hallucinations.
6. **Report** — A scored, cited readiness report with PASS / FAIL / WARNING / NEEDS REVIEW per check.

The live **Run** screen shows the pipeline step-by-step: agent activity, tools in use, violations as they stream, and retrieved code sections. When checks finish, open the dashboard to inspect findings on the plan viewer and download the full report.

---

## Results

Sample output from a Los Angeles residential project. FirstPass reads the plan set in the Autodesk viewer, extracts setbacks and height from the sheets, runs deterministic compliance against the local code corpus, and surfaces a **permit-readiness score** with cited findings — violations flagged, items needing review, and passes called out per check.

![FirstPass dashboard — plan viewer, extracted facts, compliance findings, and agent activity](./docs/results-dashboard.png)

---

## Sponsor tools (hackathon stack)

FirstPass is built around the hackathon sponsor integrations. Each tool has a visible role in the product — not just a logo in the footer.

| Tool | Role in FirstPass |
|------|-------------------|
| **[Band](https://band.ai)** | Multi-agent orchestration. Python agents (CEO, researchers, visual analysis, compare codes, permit agent) collaborate in a real Band room. The dashboard shows live agent activity and @mention-driven handoffs. |
| **[Browserbase](https://browserbase.com)** | Headless browser for live navigation of official city permitting sites and code portals. Retrieves source URLs, excerpts, and retrieval timestamps for every citation. |
| **[Redis](https://redis.io)** | Shared brain: project state, multi-agent blackboard (`project:{id}:blackboard`), code corpus, and RedisVL hybrid search (vector + BM25) for token-efficient code retrieval. |
| **[Arize](https://arize.com)** | Tracing and evaluation. Runs citation, authority, applicability, and hallucination evals on findings; powers the "caught mistake" demo moment when a misapplied rule is corrected live. |
| **[Claude](https://anthropic.com)** (Anthropic) | Plan reading (vision), agent reasoning, rule interpretation, and report writing. Default model: `claude-haiku-4-5-20251001`. |
| **[Autodesk Platform Services](https://aps.autodesk.com)** | DWG upload, Model Derivative translation, and in-browser plan sheet viewer for `.dwg` uploads. |
| **[Vercel](https://vercel.com)** | Next.js hosting, SSE streaming for live run updates, serverless API routes. |

Every integration **degrades gracefully** — the app runs and demos with zero API keys using cached demo data, in-memory Redis fallback, and prebuilt viewer assets. Add keys in `.env.local` to make each integration go live.

---

## Agent team

Python Band agents (run locally alongside the web app):

| Agent | What it does |
|-------|--------------|
| CEO Boss | Delegates the run and kicks off the workflow |
| Project & Property Manager | Writes the project brief from address + plan metadata |
| Municipal Code Researcher | Scrapes city building codes via Browserbase |
| State Code Researcher | Scrapes California state amendments |
| Code Synthesizer | Merges municipal + state findings into one conclusion |
| Visual Analysis | Reads the plan set with Claude vision |
| Compare Codes | Flags plan-vs-code violations |
| Solutions Agent | Suggests design fixes for flagged issues |
| Permit Agent | Researches the city's permit portal and submittal checklist |

See [`PLAN.md`](./PLAN.md) for the full product spec and [`docs/REDIS_PLAN.md`](./docs/REDIS_PLAN.md) for the Redis blackboard + RAG architecture.

---

## Tech stack

| Layer | Technology |
|-------|------------|
| Frontend | Next.js 15 (App Router), React 19, Tailwind CSS |
| Backend | Next.js API routes, Server-Sent Events for live pipeline streaming |
| Agents | Python 3.11+, [Band SDK](https://pypi.org/project/band-sdk/), Playwright |
| Plan reading | Claude vision (`@anthropic-ai/sdk`), MuPDF for PDF rendering |
| Storage | Redis (ioredis) with in-memory fallback |
| Code retrieval | RedisVL hybrid search + lexical fallback |
| Observability | OpenTelemetry → Arize |
| Deploy | Vercel |

---

## Quick start

```bash
# Web app
npm install
cp .env.example .env.local          # optional — app demos without keys
npm run dev                         # http://localhost:3000

# Python Band agents (optional — for live multi-agent runs)
cp firstpass.config.yaml.example firstpass.config.yaml
uv sync
./scripts/run_agents.sh
```

**Try the demo:** open the app, enter a project address, upload a plan set (PDF or DWG — `Los_Angeles_(1).dwg` works well), and click **Run FirstPass**. The demo pipeline runs real plan reading and compliance against the cached Los Angeles code corpus.

---

## Running Band agents

Start the research agents in parallel:

```bash
./scripts/run_agents.sh
# or individually:
uv run firstpass-municipal
uv run firstpass-state
uv run firstpass-synthesizer
uv run firstpass-compare
```

Kick off a run in Band (address only):

```
@varbtw/ceo-boss @varbtw/project-property-intake

1216 E 92nd St, Los Angeles, CA 90002
```

Or from the CLI:

```bash
uv run firstpass-kickoff --address "1216 E 92nd St, Los Angeles, CA 90002"
```

**Scrape codes without Band** (no Claude API cost):

```bash
uv run firstpass-local --address "1109 Evelyn Ave, Albany, CA 94706"
```

Output lands in `output/municipal_codes.txt`, `output/state_codes.txt`, and `output/final_summary.txt`.

---

## Environment variables

Copy `.env.example` → `.env.local`. Key integrations:

```bash
ANTHROPIC_API_KEY=          # Claude — plan reading, agents, reports
BROWSERBASE_API_KEY=          # Live code portal navigation
BROWSERBASE_PROJECT_ID=
REDIS_URL=                    # Shared state + code corpus (Upstash works on Vercel)
ARIZE_API_KEY=                # Tracing + finding evals
ARIZE_SPACE_ID=
BAND_API_KEY=           

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 175 recognized source files, 39798 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 221)

```
.env.example
.gitignore
.vercelignore
data/cities/alameda-ca/CHANGELOG.md
data/cities/alameda-ca/chunks.json
data/cities/alameda-ca/meta.json
data/cities/alameda-ca/raw/amc-adu.txt
data/cities/alameda-ca/raw/cec-energy-code-full.txt
data/cities/alameda-ca/raw/dgs-bsc-codes.txt
data/cities/alameda-ca/raw/gov-code-adu-ch13.txt
data/cities/alameda-ca/raw/hcd-adu-handbook.txt
data/cities/alameda-ca/raw/municipal_codes.txt
data/cities/alameda-ca/raw/state_codes.txt
data/cities/los-angeles-ca/CHANGELOG.md
data/cities/los-angeles-ca/chunks.json
data/cities/los-angeles-ca/meta.json
data/cities/los-angeles-ca/raw/cec-energy-code-full.txt
data/cities/los-angeles-ca/raw/city_zoning_lamc.txt
data/cities/los-angeles-ca/raw/county_code.txt
data/cities/los-angeles-ca/raw/dgs-bsc-codes.txt
data/cities/los-angeles-ca/raw/gov-code-adu-ch13.txt
data/cities/los-angeles-ca/raw/hcd-adu-handbook.txt
data/cities/los-angeles-ca/raw/lamc-adu.txt
data/cities/los-angeles-ca/raw/state_adu_hcd.txt
data/cities/los-angeles-ca/raw/state_codes.txt
data/cities/los-angeles-ca/rules.json
data/cities/santa-ana-ca/CHANGELOG.md
data/cities/santa-ana-ca/chunks.json
data/cities/santa-ana-ca/meta.json
data/cities/santa-ana-ca/raw/cec-energy-code-full.txt
data/cities/santa-ana-ca/raw/dgs-bsc-codes.txt
data/cities/santa-ana-ca/raw/gov-code-adu-ch13.txt
data/cities/santa-ana-ca/raw/hcd-adu-handbook.txt
data/cities/santa-ana-ca/raw/sac-adu.txt
data/codes/ca-2025/gov.ca.bsc.building.2.2025_djvu_chunked.md
data/codes/ca-2025/gov.ca.bsc.electrical.2025_djvu_chunked.md
data/codes/ca-2025/gov.ca.bsc.fire.2025_djvu_chunked.md
data/codes/ca-2025/gov.ca.bsc.green.2025_djvu_chunked.md
data/codes/ca-2025/gov.ca.bsc.mechanical.2025_djvu_chunked.md
data/codes/ca-2025/gov.ca.bsc.plumbing.2025_djvu_chunked.md
data/codes/ca-2025/gov.ca.bsc.residential.2025_djvu_chunked.md
data/codes/ca-2025/Los Angeles, CA Municipal Code_chunked.md
data/codes/ca-2025/Santa Ana, CA Code of Ordinances_chunked.md
data/demo/los-angeles-1/viewer/meta.json
docs/CHUNKING.md
docs/REDIS_PLAN.md
firstpass.config.yaml.example
fixtures/plan_sets/la_adu_partial/plan_index.txt
fixtures/plan_sets/manifest_partial.json
next.config.mjs
package.json
PLAN.md
plans/.gitkeep
postcss.config.mjs
projects/.gitkeep
pyproject.toml
README.md
render.yaml
scripts/band_harvest.mjs
scripts/band_research.mjs
scripts/build-demo-viewer.mjs
scripts/chunk_codes.py
scripts/import_cadai_corpus.py
scripts/index_codes_redisvl.py
scripts/ingest_band_output.py
scripts/query_codes_redisvl.py
scripts/refresh_corpus.py
scripts/run_agents.sh
scripts/run_corpus_refresh_launchd.sh
scripts/run_workflow_agents.sh
src/app/api/agents/compare-codes/run/route.ts
src/app/api/aps/status/route.ts
src/app/api/aps/token/route.ts
src/app/api/aps/upload/route.ts
src/app/api/blob/dwg-token/route.ts
src/app/api/cities/[slug]/route.ts
src/app/api/cities/ingest/route.ts
src/app/api/cities/research/route.ts
src/app/api/cities/route.ts
src/app/api/code/retrieve/route.ts
src/app/api/dwg/stage/route.ts
src/app/api/plans/plot/route.ts
src/app/api/plans/render/route.ts
src/app/api/plans/upload/route.ts
src/app/api/projects/[id]/blackboard/route.ts
src/app/api/projects/[id]/route.ts
src/app/api/projects/route.ts
src/app/api/run/[id]/route.ts
src/app/dashboard/page.tsx
src/app/demo/page.tsx
src/app/globals.css
src/app/layout.tsx
src/app/page.tsx
src/app/project/[id]/page.tsx
src/app/project/[id]/report/page.tsx
src/app/sign-in/[[...sign-in]]/page.tsx
src/app/sign-up/[[...sign-up]]/page.tsx
src/components/AgentFeed.tsx
src/components/ApsSheetViewer.tsx
src/components/BandConversation.tsx
src/components/BlackboardPanel.tsx
src/components/BlueprintViewer.tsx
src/components/DwgViewer.tsx
src/components/FactsList.tsx
src/components/FeaturesGrid.tsx
src/components/FindingInspector.tsx
src/components/FindingsList.tsx
src/components/HowItWorks.tsx
src/components/LandingHero.tsx
src/components/LandingSections.tsx
src/components/NewProjectForm.tsx
src/components/PhaseRail.tsx
src/components/PlanSheetViewer.tsx
src/components/ProjectHistory.tsx
src/components/Reveal.tsx
src/components/RunProgress.tsx
src/components/ScoreGauge.tsx
src/components/SiteHeader.tsx
src/components/SponsorRail.tsx
src/firstpass/__init__.py
[101 more files omitted for size]
```

### Dependencies

- package.json: @clerk/nextjs@^7.5.12, @opentelemetry/api@^1.9.1, @opentelemetry/exporter-trace-otlp-proto@^0.57.2, @opentelemetry/resources@^1.30.1, @opentelemetry/sdk-trace-node@^1.30.1, @opentelemetry/semantic-conventions@^1.41.1, @types/node@^22.10.7, @types/react@^19.0.7, @types/react-dom@^19.0.3, @vercel/blob@^2.6.1, autoprefixer@^10.4.20, fflate@^0.8.3, ioredis@^5.4.1, mupdf@^1.27.0, next@^15.5.20, playwright-core@^1.61.0, postcss@^8.5.1, react@^19.2.7, react-dom@^19.2.7, tailwindcss@^3.4.17, typescript@^5.7.3, vitest@^4.1.9
- pyproject.toml: band-sdk@>=0.1.0, beautifulsoup4@>=4.12.0, browserbase@>=1.0.0, httpx@>=0.27.0, openai@>=1.50.0, playwright@>=1.49.0, pydantic@>=2.0.0, pypdf@>=4.0.0, pyyaml@>=6.0.0, redis@>=5.0.0, redisvl@>=0.3.0, sentence-transformers@>=2.2.0

### Recent commits (newest first)

- Autonomous jurisdiction researcher: find official code sites, ingest, derive rules
- Catch wrong-jurisdiction/type runs, recover missed dimensions, fix junk citations
- Route DWG uploads through Vercel Blob to bypass the 4.5MB Function body limit
- Add cited Title 24 (California Energy Code, Part 6) compliance finding
- Restore interactive APS pan/zoom viewer for DWG projects (CADAI-style)
- Capture every DWG sheet (model space + manifest) and read all sheets per-sheet
- Guard SSE state parse and never emit "data: undefined"
- Never route serverless runs through the Band pipeline (it stalls on Vercel)
- Add .vercelignore so local .env files never deploy
- Make the run reliably complete within the serverless time budget
- Ignore all .env* files to prevent committing local secrets
- Mirror APS-plotted DWG sheets to Vercel Blob for cross-invocation durability
- Bump Next.js to 15.5.20 and React to 19.2.7 to satisfy Clerk peer deps
- Add Clerk authentication and scope projects to the signed-in account
- Drop Clerk import from plans/upload route to unblock production build
- Route on-disk workspace writes through a writable DATA_ROOT (fix serverless upload crash)
- Add render.yaml Blueprint for Render deployment
- Degrade project-file disk mirror gracefully on read-only filesystems
- Upgrade Next.js to 15.1.9 to patch CVE-2025-66478 (RSC RCE)
- push

## Key source files (fetched from GitHub, selected and truncated for size)

### docs/CHUNKING.md

```markdown
# Chunking building codes for retrieval

How FirstPass turns a city's researched building code into small, retrievable
units, and *why* it chunks the way it does. The goal: at check time, retrieve
**only the one provision that governs a rule** — never the whole code book —
so prompts stay small, cheap, and citable.

This is implemented by [`scripts/chunk_codes.py`](../scripts/chunk_codes.py)
(raw → `chunks.json`) and consumed by [`src/lib/code-db.ts`](../src/lib/code-db.ts).

## Why building codes need their own strategy

Building/zoning codes aren't prose — they're a **hierarchy of numbered
provisions** (Title → Chapter → Article → Section → subsection), dense with
exact identifiers (`§30-5.21(b)`, `1,200 sq ft`, `18 feet`) and cross-references.
Two consequences:

1. **The section is the natural retrieval unit.** A single subsection usually
   states one complete rule. Splitting by fixed character windows would sever a
   provision from its qualifier ("…shall not exceed 1,200 sq ft" / "…except for
   conversions"). So we split on **structure first**, size second.
2. **Exact terms matter more than semantics.** Queries hit on section numbers
   and literal values. Pure embedding similarity under-retrieves these; lexical
   matching (BM25-style) is essential. FirstPass's retrieval is deterministic
   tag + term scoring, which is lexical by construction.

## The strategy (and the evidence)

| Decision | What we do | Why |
| --- | --- | --- |
| **Structure-aware split** | Split on `### Section [Sx]` headers; one section = one chunk. | Section-based / document-structure chunking is the recommended approach for structured legal docs. |
| **Size target** | Keep a whole section together; split only above ~1100 chars (~275 tokens). | "A few hundred tokens" / 256–512 is the retrieval sweet spot; codes sections fit. |
| **Overlap on splits** | When a section must split, carry ~120 chars of the previous piece forward. | Overlap preserves continuity across the boundary. |
| **Contextual header** | Prepend `City, State · Code · §Section` to each chunk **when indexing** (`context` field; display still uses `text`). | Anthropic *Contextual Retrieval*: prepending 50–100 tokens of situating context cut retrieval failures **35%** (embeddings) / **49%** with BM25. We build it deterministically from structure — no LLM call. |
| **Carry citation + source** | Every chunk keeps `section`, `citation`, and `sourceId` → URL in `meta.json`. | Findings must cite the exact code section + official source. |
| **Topic tags** | Infer rule keys (`maxSize`, `height`, `setbackSide`…) from the header, body as fallback. | Lets a check retrieve by the rule it's evaluating, header-first to avoid incidental-term mis-tagging. |
| **Hybrid + rerank (direction)** | Lexical term/tag scoring today; embeddings + BM25 rank-fusion + rerank is the upgrade path. | Contextual embeddings + BM25 + rerank cut failures **67%**. |

## Chunk shape

```jsonc
{
  "id": "los-angeles-ca-city-lamc-12-22-a-33
[truncated — 5569 more characters]
```

### docs/REDIS_PLAN.md

```markdown
# FirstPass — Redis Integration Plan

**Goal:** Use Redis as the firm's shared brain — not a cache — so judges see agent memory, vector search, and context retrieval in the demo.

Built for the **UC Berkeley AI Hackathon · Redis track**. Qualifying tools: Redis Cloud, Redis OSS, **RedisVL**, **Agent Memory Server**, Redis AI Incubator.

Related docs: [`PLAN.md`](../PLAN.md) §8 (data model), [`CHUNKING.md`](./CHUNKING.md) (retrieval strategy).

---

## 1. Current state

### What works today

| Component | File(s) | Redis usage |
|-----------|---------|-------------|
| Project state | `src/lib/store.ts` | JSON blobs at `state:{id}`, 6h TTL |
| Code corpus | `src/lib/code-db.ts` | Chunk index at `code:{slug}:*`, city registry |
| Plan uploads | `src/app/api/plans/upload/route.ts` | Base64 plan at `plan:{id}` |
| Pipeline | `src/lib/pipeline.ts` | Seeds chunks, emits "Indexed to Redis" sponsor message |
| City ingest | `src/app/api/cities/ingest/route.ts` | Persists runtime-ingested corpora to Redis |

Connection: `REDIS_URL` via **ioredis** (Upstash / Redis Cloud on Vercel). In-memory fallback when unset.

Retrieval today: **deterministic tag + lexical term scoring** in `retrieveCode()` — no embeddings, no vector index. See [`CHUNKING.md`](./CHUNKING.md) § "Hybrid + rerank (direction)".

### What's missing (gap vs. hackathon criteria)

| Criterion | Status |
|-----------|--------|
| Redis beyond caching | Partial — chunks are durable storage, but narrative still reads as "cache" |
| Agent memory | **Not built** — Python Band agents write to `output/*.txt`, not Redis |
| Vector search | **Not built** — lexical only |
| Context retrieval | Partial — `context` headers exist on chunks but aren't embedded |
| Cross-agent blackboard | **Not built** — Next.js and Python halves don't share state |
| Source dedup + freshness | **Not built** — sources live in project state only |

### The split-brain problem

```
Python Band agents          Next.js dashboard
─────────────────          ─────────────────
output/municipal_codes.txt     Redis (state, chunks)
output/state_codes.txt         SSE pipeline
output/final_summary.txt       Finding inspector
data/cities/<slug>/            Browserbase research

        ↑ no shared memory ↑
```

Until Redis bridges these, the multi-agent story is invisible to the dashboard and judges.

---

## 2. Target architecture

Redis sits at the center as **shared memory + retrieval infrastructure**:

```
                    ┌─────────────────────────────────┐
                    │         Redis Cloud             │
                    │  (Search + Vector + Streams)    │
                    └───────────────┬─────────────────┘
                                    │
        ┌───────────────────────────┼───────────────────────────┐
        │                           │                           │
   Blackboard                  Code RAG                   Agent feed
   project:{id}:*              RedisVL index              Streams
        │ 
[truncated — 13502 more characters]
```

### package.json

```
{
  "name": "firstpass",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "dev:clean": "lsof -ti:3000,3001,3002 2>/dev/null | xargs kill -9 2>/dev/null || true && next dev",
    "dev:reset": "lsof -ti:3000,3001,3002 2>/dev/null | xargs kill -9 2>/dev/null || true; rm -rf .next && next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "test": "vitest run"
  },
  "dependencies": {
    "@clerk/nextjs": "^7.5.12",
    "@opentelemetry/api": "^1.9.1",
    "@opentelemetry/exporter-trace-otlp-proto": "^0.57.2",
    "@opentelemetry/resources": "^1.30.1",
    "@opentelemetry/sdk-trace-node": "^1.30.1",
    "@opentelemetry/semantic-conventions": "^1.41.1",
    "@vercel/blob": "^2.6.1",
    "fflate": "^0.8.3",
    "ioredis": "^5.4.1",
    "mupdf": "^1.27.0",
    "next": "^15.5.20",
    "playwright-core": "^1.61.0",
    "react": "^19.2.7",
    "react-dom": "^19.2.7"
  },
  "devDependencies": {
    "@types/node": "^22.10.7",
    "@types/react": "^19.0.7",
    "@types/react-dom": "^19.0.3",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.5.1",
    "tailwindcss": "^3.4.17",
    "typescript": "^5.7.3",
    "vitest": "^4.1.9"
  }
}

```

### pyproject.toml

```
[project]
name = "firstpass"
version = "0.1.0"
description = "Multi-agent building code research via Internet Archive + Band"
requires-python = ">=3.11"
dependencies = [
    "band-sdk>=0.1.0",
    "beautifulsoup4>=4.12.0",
    "browserbase>=1.0.0",
    "httpx>=0.27.0",
    "openai>=1.50.0",
    "playwright>=1.49.0",
    "pydantic>=2.0.0",
    "pypdf>=4.0.0",
    "pyyaml>=6.0.0",
    "redis>=5.0.0",
]

[project.optional-dependencies]
# RedisVL hybrid (vector + BM25) code retrieval. Pulls in sentence-transformers
# (torch) for local, zero-cost embeddings — heavy, so it's opt-in:
#   uv sync --extra vector
# Required only to (re)build the firstpass:codes index; the app's query path
# degrades to lexical retrieval when the index is absent. See docs/REDIS_PLAN.md.
vector = [
    "redisvl>=0.3.0",
    "sentence-transformers>=2.2.0",
]

[project.scripts]
firstpass-municipal = "firstpass.agents.municipal:main"
firstpass-state = "firstpass.agents.state:main"
firstpass-building = "firstpass.agents.building:main"
firstpass-residential = "firstpass.agents.residential:main"
firstpass-plumbing = "firstpass.agents.plumbing:main"
firstpass-green = "firstpass.agents.green:main"
firstpass-synthesizer = "firstpass.agents.synthesizer:main"
firstpass-compare = "firstpass.agents.compare:main"
firstpass-ceo = "firstpass.agents.ceo:main"
firstpass-project-property-manager = "firstpass.agents.project_property_manager:main"
firstpass-ceo-planner = "firstpass.agents.project_property_manager:main"
firstpass-visual = "firstpass.agents.visual:main"
firstpass-solutions = "firstpass.agents.solutions:main"
firstpass-kickoff = "firstpass.orchestrator:main"
firstpass-local = "firstpass.local_research:main"
firstpass-permit = "firstpass.permit.cli:main"
firstpass-permit-agent = "firstpass.agents.permit:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/firstpass"]

```

### src/app/page.tsx

```typescript
import { SiteHeader } from "@/components/SiteHeader";
import { LandingHero } from "@/components/LandingHero";
import { FeaturesGrid } from "@/components/FeaturesGrid";
import {
  DemoVideo,
  PoweredBy,
  TheReality,
  TheCost,
  TheStack,
  ChecksBand,
  LandingCTA,
  LandingFooter,
} from "@/components/LandingSections";

export default function Home() {
  return (
    <div className="min-h-screen bg-[#fbfcfa] text-ink overflow-x-hidden">
      <SiteHeader />
      <LandingHero />
      <DemoVideo />
      <PoweredBy />
      <TheReality />
      <TheCost />
      {/* "Not nine prompts in a trenchcoat" sits before "What you get" */}
      <TheStack />
      <FeaturesGrid />
      <ChecksBand />
      <LandingCTA />
      <LandingFooter />
    </div>
  );
}

```

### src/app/layout.tsx

```typescript
import {ClerkProvider} from "@clerk/nextjs";
import type { Metadata } from "next";
import { Bricolage_Grotesque, Hanken_Grotesk, JetBrains_Mono } from "next/font/google";
import "./globals.css";

const bricolage = Bricolage_Grotesque({
  subsets: ["latin"],
  variable: "--font-display",
  weight: ["500", "600", "700", "800"],
  display: "swap",
});

const hanken = Hanken_Grotesk({
  subsets: ["latin"],
  variable: "--font-sans",
  weight: ["400", "500", "600", "700"],
  display: "swap",
});

const jetbrains = JetBrains_Mono({
  subsets: ["latin"],
  variable: "--font-mono",
  weight: ["400", "500", "600"],
  display: "swap",
});

export const metadata: Metadata = {
  title: {
    default: "FirstPass",
    template: "%s · FirstPass",
  },
  description:
    "Upload a plan set and get a cited, sheet-by-sheet readiness report with likely violations, official code citations, and a missing-documents checklist before you submit to the city.",
  icons: {
    icon: [
      { url: "/favicon.ico", sizes: "any" },
      { url: "/icon-32.png", type: "image/png", sizes: "32x32" },
      { url: "/icon-192.png", type: "image/png", sizes: "192x192" },
    ],
    apple: [{ url: "/icon-180.png", sizes: "180x180" }],
  },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html
      lang="en"
      className={`${bricolage.variable} ${hanken.variable} ${jetbrains.variable}`}
    >
      <body suppressHydrationWarning>
        <ClerkProvider>
          {children}
        </ClerkProvider>
      </body>
    </html>
  );
}
```

### src/app/demo/page.tsx

```typescript
import { redirect } from "next/navigation";

export default function DemoPage() {
  redirect("/dashboard");
}

```

### src/app/dashboard/page.tsx

```typescript
"use client";

import { useState } from "react";
import { DISCLAIMER } from "@/lib/types";
import { SiteHeader } from "@/components/SiteHeader";
import { NewProjectForm } from "@/components/NewProjectForm";
import { ProjectHistory } from "@/components/ProjectHistory";

export default function DashboardPage() {
  const [refreshKey, setRefreshKey] = useState(0);

  return (
    <div className="min-h-screen canvas-grid flex flex-col">
      <SiteHeader />

      <main className="flex-1 py-12 lg:py-16">
        <div className="max-w-[1180px] mx-auto px-6 lg:px-10">
          <div className="mb-10">
            <div className="font-mono text-[12px] tracking-[0.14em] uppercase text-teal mb-3">Dashboard</div>
            <h1 className="font-display text-[34px] font-bold text-ink tracking-[-0.03em]">
              Run a pre-submission review.
            </h1>
            <p className="mt-2 text-body text-[15px] max-w-xl">
              Create a new permit review or open a previous project.
            </p>
          </div>

          <div className="grid lg:grid-cols-2 gap-8 lg:gap-12 items-start">
            <div id="projects">
              <ProjectHistory refreshKey={refreshKey} />
            </div>
            <div id="new-project" className="lg:sticky lg:top-24">
              <NewProjectForm onCreated={() => setRefreshKey((k) => k + 1)} />
            </div>
          </div>
        </div>
      </main>

      <footer className="mt-auto border-t border-hairline bg-deep/60">
        <div className="max-w-[1180px] mx-auto px-6 lg:px-10 py-6">
          <p className="text-[11px] text-muted leading-relaxed max-w-3xl">{DISCLAIMER}</p>
        </div>
      </footer>
    </div>
  );
}

```

### src/firstpass/permit/cli.py

```python
"""Local CLI for permit package review (no Band / Claude)."""

from __future__ import annotations

import argparse
import json
from datetime import datetime, timezone
from pathlib import Path

from firstpass.permit.review import format_permit_package_text, review_permit_package

OUTPUT_DIR = Path(__file__).resolve().parents[3] / "output"


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Compare an uploaded plan set against the city's permit checklist.",
    )
    parser.add_argument("--address", required=True, help="Project address")
    parser.add_argument(
        "--plan-set",
        required=True,
        help="Path to plan set directory, sheet index .txt, or JSON manifest",
    )
    parser.add_argument("--project-type", default="Detached ADU")
    parser.add_argument(
        "--output-dir",
        default=str(OUTPUT_DIR),
        help="Directory for permit_package.json and permit_package.txt",
    )
    parser.add_argument("--json-only", action="store_true", help="Print JSON to stdout only")
    args = parser.parse_args()

    review = review_permit_package(
        address=args.address,
        plan_set_path=args.plan_set,
        project_type=args.project_type,
    )

    if args.json_only:
        print(json.dumps(review.model_dump(), indent=2))
        return

    out_dir = Path(args.output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    json_path = out_dir / "permit_package.json"
    txt_path = out_dir / "permit_package.txt"

    json_path.write_text(json.dumps(review.model_dump(), indent=2) + "\n", encoding="utf-8")
    header = (
        f"FirstPass Permit Package Review\n"
        f"Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}\n"
        f"{'=' * 60}\n\n"
    )
    txt_path.write_text(header + format_permit_package_text(review), encoding="utf-8")

    print(format_permit_package_text(review))
    print(f"JSON: {json_path}")
    print(f"Text: {txt_path}")


if __name__ == "__main__":
    main()

```

### src/app/sign-in/[[...sign-in]]/page.tsx

```typescript
import { SignIn } from "@clerk/nextjs";

export default function SignInPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignIn />
    </div>
  );
}

```

[155 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]