# Project export: Narcore

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: An AI undercover agent to detect and intercept online drug advertising.
- Devpost: https://devpost.com/software/narcore
- GitHub: https://github.com/cibarbia05/narcore
- Video: https://www.youtube.com/embed/r2Dl7fKbcsU?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Result: winner (Best Use of Browserbase)
- Team: 3 GitHub contributor(s) — cibarbia05 (29 commits), Claude Opus 4.8 (1M context) (13 commits), durpdur (8 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

# narcore

A dark, focused web foundation for high-stakes government tooling.

## Stack

- **[Next.js](https://nextjs.org) (App Router)** + **React 19** + **TypeScript**
- **[Tailwind CSS v4](https://tailwindcss.com)** — CSS-first config via `@theme`
  (no `tailwind.config.js`)
- **[shadcn/ui](https://ui.shadcn.com)** (Base UI primitives, Lucide icons)
- **[next-themes](https://github.com/pacocoursey/next-themes)** — dark-first theming

## Getting started

```bash
pnpm install
pnpm dev          # http://localhost:3000
```

Other scripts:

```bash
pnpm build        # production build (type-check + lint + compile)
pnpm start        # serve the production build
pnpm lint         # ESLint
```

> Uses **pnpm**. Install it with `npm i -g pnpm` (or via Corepack) if needed.

## Project layout

```
src/
  app/
    layout.tsx        # fonts, dark-default ThemeProvider, metadata
    globals.css       # design tokens — single source of truth (dark + azure)
    page.tsx          # starter page
  components/
    ui/               # shadcn/ui primitives (e.g. button)
    theme-provider.tsx
    logo.tsx          # brand mark
  lib/
    utils.ts          # cn() class merge helper
components.json       # shadcn/ui config
brand.md              # brand & UI guidelines
```

# NARCORE — Technical Architecture

A field guide to how the system actually works. Focused on the core engine: the
parallel scraper fleets, the undercover outreach operative, and the
infrastructure underneath them (Browserbase, Redis, embeddings, and the LLMs).

---

## 1. The Big Picture

NARCORE is a closed-loop system that does three things and feeds each one back
into the next:

1. **Detect** — parallel browser agents scrape Instagram hashtags, and every
   post is risk-scored using semantic (vector) similarity against a corpus of
   known drug-dealing language.
2. **Engage** — a human operator launches an undercover "operative" against a
   flagged seller. An LLM-driven agent negotiates over Instagram DMs toward two
   objectives: confirm the deal and confirm a meeting location.
3. **Learn** — every confirmed engagement teaches the system. New coded slang is
   extracted from the seller's own words and added to the detection corpus (R1),
   and the successful tactics are stored as long-term memory to prime the next
   operative (R2).

```
   ┌─────────────┐     flagged      ┌──────────────┐    confirmed     ┌────────────┐
   │  DETECT     │  ───────────────▶│   ENGAGE     │ ───────────────▶ │   LEARN    │
   │ scraper     │      lead        │  operative   │   deal+location  │  R1 corpus │
   │ fleet (×N)  │                  │  (LLM + DM)  │                  │  R2 memory │
   └─────────────┘                  └──────────────┘                  └────────────┘
         ▲                                                                   │
         └───────────────── grown corpus re-flags more posts ◀──────────────┘
```

Everything is coordinated through a single **Redis** instance, which acts as the
vector database, the state store, the job registry, and the event bus all at
once.

---

## 2. Parallel Scraper Fleets

**What it is:** N independent browser agents (default 5, configurable 1–20) that
each drive a real Instagram session in parallel, scrape a hashtag feed, and push
every post into the scoring pipeline.

**Key files:** `src/lib/agents/orchestrator.ts`, `src/lib/agents/ig-agent.ts`,
`src/lib/agents/run-store.ts`

### How a fleet launches

1. `POST /api/agents/run` → `startRun()` resolves the agent count and assigns
   each agent a distinct Instagram hashtag target.
2. All N Browserbase sessions are provisioned **up front and in parallel**
   (`Promise.all` over `provisionAgent()`), so the live-view URLs exist before
   any scraping begins — the UI can render the video grid immediately.
3. Each agent's loop (`runAgentLoop`) is fired **un-awaited** (`void`). The HTTP
   request returns instantly; the agents keep running in the background.
4. The UI **polls** Redis for progress rather than holding an open connection.

### Why there are no race conditions

Every agent owns its own Redis hash: `run:{id}:agent:{idx}`. An agent only ever
patches its own key, so five siblings writing concurrently never collide — no
locks needed. The run is marked `done` only when *all* agents reach a terminal
state (`done`, `blocked`, `error`, or `stopped`).

### Budgets (per agent)

| Setting | Default | Meaning |
|---|---|---|
| `IG_AGENT_TIMEOUT_MS` | 120s | Wall-clock budget per agent |
| `IG_MAX_POSTS_PER_AGENT` | 8 | Posts captured before stopping |

### Cancellation

`stopRun()` immediately marks the run stopped, aborts every agent's
`AbortController`, and releases all Browserbase sessions in parallel. The UI sees
a clean terminal state right away.

---

## 3. The Undercover Operative (Outreach)

**What it is:** A single LLM-driven agent that opens an Instagram DM with a
flagged seller and negotiates, turn by turn, toward two confirmations. Unlike the
fleet, operations run **one at a time per post** (deduped) and have a much longer
budget because real conversations take time.

**Key files:** `src/lib/agents/operation-orchestrator.ts`,
`scraper/operative-agent.ts`, `src/lib/operative-brain.ts`,
`src/lib/agents/operation-store.ts`

### Launch flow (`startOperation`)

1. **Preconditions:** `ANTHROPIC_API_KEY` must be set, at least one logged-in
   Browserbase context must exist, and the target handle must pass the
   **allowlist gate** (`OPERATIVE_ALLOWLIST_ENFORCED=true` — this must never be
   off in a live demo; it restricts targets to consented demo accounts).
2. **Dedup:** `op:by-post:{postId}` ensures the same post can't launch two
   operations at once.
3. A Browserbase session is created, an `Operation` record is written to Redis
   with status `opening`, and the operative loop fires un-awaited (same
   fire-and-forget + poll pattern as the fleet).

### The negotiation loop

The operative tracks two objectives independently so the UI can show live state
like **"Deal ✓ · Location ✗"** even mid-conversation.

```
open DM thread  →  brain composes opener  →  ┌─────────────────────────────┐
                                             │  send message (verify it     │
                                             │  landed, up to 3 retries)    │
                                             │  wait for seller reply       │
                                             │  brain analyzes transcript   │
                                             │  patch deal/location state   │
                                             └──────────────┬──────────────┘
                                                            │
            both confirmed? ── yes ──▶ send closer, mark "confirmed", trigger R1+R2
                  │ no
            rejected / stalled / max turns? ── yes ──▶ terminal
                  │ no
                  └──▶ loop
```

Opening a DM thread uses one of two strategies (`OPERATIVE_DM_OPEN_STRATEGY`):
- **`ladder`** (default): a hand-rolled `observe → act` sequence (try the profile
  "Message" button, fall back to DM-inbox search).
- **`agent`**: a self-healing DOM-mode Stagehand agent that figures out the steps
  itself (more robust, more expensive).

### Budgets (per operation)

| Setting | Default | Meaning |
|---|---|---|
| `OPERATIVE_BUDGET_MS` | 25 min | Total wall-clock per negotiation |
| `OPERATIVE_REPLY_WAIT_MS` | 4 min | Max wait for each seller reply |
| `OPERATIVE_POLL_MS` | 15s | How often the thread is checked for new replies |
| `OPERATIVE_MAX_TURNS` | 12 | Max back-and-forth exchanges |

### Operation states

`opening → awaiting_reply → analyzing → negotiating` and then a terminal state:
`confirmed` (both objectives met), `rejected`, `stalled` (ran out of budget/turns),
`blocked` (login wall), `error`, or `stopped` (operator aborted).

### The Operative Brain (the LLM call)

`negotiate()` in `src/lib/operative-brain.ts` makes **one Claude call per turn**.
It is given the lead contex

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 458 recognized source files, 1875 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — 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
- Docker (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers
- AI coding agent: Codex — evidence: config files committed to the repository

## Codebase structure (from repository index)

### Files (120 of 504)

```
.agents/skills/browser/EXAMPLES.md
.agents/skills/browser/LICENSE.txt
.agents/skills/browser/REFERENCE.md
.agents/skills/browser/SKILL.md
.agents/skills/python-patterns/SKILL.md
.agents/skills/redis-connections/.cursor-plugin/plugin.json
.agents/skills/redis-connections/references/blocking.md
.agents/skills/redis-connections/references/client-cache.md
.agents/skills/redis-connections/references/pipelining.md
.agents/skills/redis-connections/references/pooling.md
.agents/skills/redis-connections/references/timeouts.md
.agents/skills/redis-connections/SKILL.md
.agents/skills/redis-core/.cursor-plugin/plugin.json
.agents/skills/redis-core/evals/core/baselines/aggregate-benchmark.json
.agents/skills/redis-core/evals/core/baselines/aggregate-benchmark.md
.agents/skills/redis-core/evals/core/baselines/baseline.json
.agents/skills/redis-core/evals/core/baselines/model-matrix.json
.agents/skills/redis-core/evals/core/baselines/README.md
.agents/skills/redis-core/evals/core/evals.json
.agents/skills/redis-core/evals/core/model-matrix.json
.agents/skills/redis-core/references/choose-data-structure.md
.agents/skills/redis-core/references/key-naming.md
.agents/skills/redis-core/SKILL.md
.agents/skills/redis-observability/.cursor-plugin/plugin.json
.agents/skills/redis-observability/references/commands.md
.agents/skills/redis-observability/references/metrics.md
.agents/skills/redis-observability/SKILL.md
.agents/skills/redis-semantic-cache/.cursor-plugin/plugin.json
.agents/skills/redis-semantic-cache/references/best-practices.md
.agents/skills/redis-semantic-cache/references/langcache-usage.md
.agents/skills/redis-semantic-cache/SKILL.md
.agents/skills/redis-vector-search/.cursor-plugin/plugin.json
.agents/skills/redis-vector-search/references/algorithm-choice.md
.agents/skills/redis-vector-search/references/hybrid-search.md
.agents/skills/redis-vector-search/references/index-creation.md
.agents/skills/redis-vector-search/references/rag-pattern.md
.agents/skills/redis-vector-search/SKILL.md
.agents/skills/security-review/SKILL.md
.agents/skills/vercel-composition-patterns/AGENTS.md
.agents/skills/vercel-composition-patterns/metadata.json
.agents/skills/vercel-composition-patterns/README.md
.agents/skills/vercel-composition-patterns/rules/_sections.md
.agents/skills/vercel-composition-patterns/rules/_template.md
.agents/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md
.agents/skills/vercel-composition-patterns/rules/architecture-compound-components.md
.agents/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md
.agents/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md
.agents/skills/vercel-composition-patterns/rules/react19-no-forwardref.md
.agents/skills/vercel-composition-patterns/rules/state-context-interface.md
.agents/skills/vercel-composition-patterns/rules/state-decouple-implementation.md
.agents/skills/vercel-composition-patterns/rules/state-lift-state.md
.agents/skills/vercel-composition-patterns/SKILL.md
.agents/skills/vercel-react-best-practices/AGENTS.md
.agents/skills/vercel-react-best-practices/metadata.json
.agents/skills/vercel-react-best-practices/README.md
.agents/skills/vercel-react-best-practices/rules/_sections.md
.agents/skills/vercel-react-best-practices/rules/_template.md
.agents/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md
.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md
.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md
.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md
.agents/skills/vercel-react-best-practices/rules/async-api-routes.md
.agents/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md
.agents/skills/vercel-react-best-practices/rules/async-defer-await.md
.agents/skills/vercel-react-best-practices/rules/async-dependencies.md
.agents/skills/vercel-react-best-practices/rules/async-parallel.md
.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md
.agents/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md
.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md
.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md
.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md
.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md
.agents/skills/vercel-react-best-practices/rules/bundle-preload.md
.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md
.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md
.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md
.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md
.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md
.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md
.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md
.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md
.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md
.agents/skills/vercel-react-best-practices/rules/js-early-exit.md
.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md
.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md
.agents/skills/vercel-react-best-practices/rules/js-index-maps.md
.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md
.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md
.agents/skills/vercel-react-best-practices/rules/js-request-idle-callback.md
.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md
.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md
.agents/skills/vercel-react-best-practices/rules/rendering-activity.md
.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md
.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md
.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md
.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md
.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md
.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md
.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md
.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md
.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md
.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md
.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md
.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md
.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md
.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md
.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md
.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md
.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md
.agents/skills/vercel-react-best-practices/rules/rerender-memo.md
.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md
.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md
.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md
.agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md
.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md
.agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md
.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md
.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md
.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md
.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md
[384 more files omitted for size]
```

### Dependencies

- package.json: @anthropic-ai/sdk@^0.105.0, @base-ui/react@^1.6.0, @browserbasehq/sdk@^2.10.0, @browserbasehq/stagehand@^3.6.0, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9, eslint-config-next@16.2.9, lucide-react@^1.21.0, next@16.2.9, next-themes@^0.4.6, react@19.2.4, react-dom@19.2.4, redis@^6.0.0, shadcn@^4.11.0, sonner@^2.0.7, swr@^2.4.1, tailwind-merge@^3.6.0, tailwindcss@^4, tsx@^4.22.4, tw-animate-css@^1.4.0, typescript@^5, umap-js@^1.4.0, zod@^4.4.3

### Recent commits (newest first)

- Revise README for NARCORE architecture overview
- Add architecture doc
- Add shared types, API hooks, and nav for the new features
- Add docs for the Redis and Browserbase deepenings
- Wire operative to field-intel, agent memory, and self-healing DM-open
- Add consistent Browserbase session identity with match badge
- Add Redis Iris agent memory for cross-operation operative recall
- Add field-intel loop so confirmed busts teach the detection corpus
- Update home page with 3d background and new text design
- Enhance vector space UI to use UMAP
- Fix fleet not showing leads vs posts difference properly
- Merge branch 'main' of https://github.com/cibarbia05/gov-dr-ai
- Add outreach via browserbase
- Merge pull request #4 from cibarbia05/risk-rating
- Risk rating formula updated
- Merge branch 'main' of https://github.com/cibarbia05/gov-dr-ai
- Add handling of up to 20 live views
- Merge pull request #3 from cibarbia05/formula-update
- formula change
- Add computer use multi-agent view

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

### AGENTS.md

```markdown
@../.codex/AGENTS.md

## Codex

Single source of truth: the shared instructions above live in the in-repo
`.codex/AGENTS.md` (imported relatively so the import resolves for every
teammate who clones this repo, regardless of their home directory). Codex
reads that same `.codex/AGENTS.md` directly, so both tools stay aligned.
Edit `.codex/AGENTS.md`, not this file.

When making **any** UI change, invoke these skills to ensure best practices:

- `vercel-react-best-practices`
- `vercel-composition-patterns`
- `web-design-guidelines`

```

### brand.md

```markdown
# Brand & UI Guidelines

The visual system for **narcore** — a dark, focused interface for high-stakes
government tooling. The bar is *world-class production*, not demo. Restraint is the
defining quality: a near-black ground, generous space, precise type, and a **single
authoritative-azure accent** that earns attention because nothing else competes for it.

> **Source of truth:** all color/spacing/radius/motion values live as tokens in
> [`src/app/globals.css`](./src/app/globals.css). This document explains *intent and
> usage* — it never restates raw values that could drift out of sync. When a token
> and this doc disagree, the token wins; fix the doc.

---

## 1. Principles

1. **Clarity over decoration.** Every element earns its place. If it doesn't aid
   comprehension or action, remove it.
2. **Restraint with the accent.** Azure marks the *one* thing that matters in a
   view — the primary action, focus, or a live signal. Never use it for large
   fills or as a background wash.
3. **Technical confidence.** Monospace for system/metadata, a clean grotesque for
   prose. The product should feel engineered, exact, and calm under pressure.
4. **Trust by default.** High contrast, predictable interaction, no dark patterns.
   This is software the public relies on.

---

## 2. Color

Dark-first. The light theme exists for completeness, but the brand surface is dark
(`<html class="dark">`, set in [`layout.tsx`](./src/app/layout.tsx)). Colors are
authored in **OKLCH** for perceptually-even steps and predictable contrast.

> **Severity ≠ brand.** Risk bands use dedicated semantic colors — red (`--destructive`)
> for *High*, amber (`--chart-2`) for *Elevated*, neutral for *Low* — and are
> intentionally independent of the azure accent. The brand color signals trust and
> action, never danger.

### Semantic roles (token → usage)

| Token | Role |
| --- | --- |
| `--background` / `--foreground` | Page ground (near-black, faintly warm) and primary text (soft off-white). |
| `--card` / `--popover` | Raised surfaces, one step above the ground. |
| `--primary` / `--primary-foreground` | **The azure accent** + the near-white text that sits on it. Primary buttons, focus, key emphasis. |
| `--secondary` / `--accent` | Quiet neutral surfaces for hover and low-emphasis controls. |
| `--muted` / `--muted-foreground` | Muted fills and secondary/supporting text. |
| `--border` / `--input` | Hairline separators and field outlines — low-contrast, translucent white. |
| `--ring` | Focus ring — azure, matches `--primary`. |
| `--destructive` | Errors, irreversible actions, and the **High** risk band. |
| `--chart-1…5` | Data viz — an azure-anchored multi-hue ramp; `--chart-2` is the amber used for the **Elevated** risk band. |
| `--sidebar-*` | App-shell surfaces (reserved for future navigation chrome). |

### The one-accent rule

- ✅ Primary CTA, focus ring, a live/status dot, a single emphasized number.
- ❌ Azure headings, azure body text, large azure panels, two compe
[truncated — 3294 more characters]
```

### package.json

```
{
  "name": "gov-dr-ai",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint",
    "typecheck": "tsc --noEmit",
    "seed": "tsx scripts/seed.ts",
    "seed:memories": "tsx scripts/seed-memories.ts",
    "post-mock": "tsx scripts/post-mock.ts",
    "scrape-demo": "tsx scripts/scrape-demo.ts",
    "ig:login": "tsx scripts/ig-login.ts",
    "ig:verify": "tsx scripts/ig-verify.ts"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.105.0",
    "@base-ui/react": "^1.6.0",
    "@browserbasehq/sdk": "^2.10.0",
    "@browserbasehq/stagehand": "^3.6.0",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lucide-react": "^1.21.0",
    "next": "16.2.9",
    "next-themes": "^0.4.6",
    "react": "19.2.4",
    "react-dom": "19.2.4",
    "redis": "^6.0.0",
    "shadcn": "^4.11.0",
    "sonner": "^2.0.7",
    "swr": "^2.4.1",
    "tailwind-merge": "^3.6.0",
    "tw-animate-css": "^1.4.0",
    "umap-js": "^1.4.0",
    "zod": "^4.4.3"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.2.9",
    "tailwindcss": "^4",
    "tsx": "^4.22.4",
    "typescript": "^5"
  }
}

```

### docker-compose.yml

```yaml
# Narcore local infrastructure.
# Phase 0 ships a working Redis Stack so WT-B can integration-test for real.
# WT-C is the SOLE editor of this file thereafter — it fleshes out the embedding
# sidecar (llama.cpp serving nomic-embed-text-v2-moe on an OpenAI-compatible
# /v1/embeddings endpoint). The Next.js app runs via `pnpm dev` (not containerized).

services:
  redis:
    image: redis/redis-stack:latest
    container_name: narcore-redis
    ports:
      - "6379:6379" # Redis
      - "8001:8001" # RedisInsight (http://localhost:8001)
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 10

  # --- WT-C: embedding sidecar ---
  # llama.cpp serving nomic-embed-text-v2-moe on an OpenAI-compatible POST /v1/embeddings.
  # We use the prebuilt ggml-org image (no local Dockerfile): its ENTRYPOINT is
  # /app/llama-server, so `command:` supplies args only. See infra/embedding/README.md.
  embedding:
    image: ghcr.io/ggml-org/llama.cpp:server # CPU build (multi-arch). GPU: see README (:server-cuda)
    container_name: narcore-embedding
    ports:
      - "8080:8080" # OpenAI-compatible POST /v1/embeddings — EMBEDDING_API_URL=http://localhost:8080/v1/embeddings
    # Pin the ggml-org GGUF (carries the v2-MoE pooling-assert conversion fix, #13534/#13689)
    # at its only file, the Q8_0 quant (near-lossless — embedding similarity is precision-
    # sensitive). --hf-file pins the exact file deterministically. The OpenAI /v1/embeddings
    # endpoint requires a non-`none` pooling type, hence --pooling mean. The app applies the
    # search_document:/search_query: prefixes itself — we must NOT.
    command: >
      -m /models/nomic-embed-text-v2-moe-q8_0.gguf
      --embeddings --pooling mean
      --host 0.0.0.0 --port 8080
      -c 2048 -b 2048 -ub 2048
    volumes:
      # GGUF is downloaded on the HOST (the container cannot reach Hugging Face) and
      # bind-mounted read-only. Fetch it once: see infra/embedding/README.md.
      - ./infra/embedding/models:/models:ro
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 5s
      retries: 12
      start_period: 300s # don't count health failures while the model downloads on first boot
    restart: unless-stopped

  # --- R2: Redis Iris agent memory (cross-operation operative memory) ---
  # Shared env for the agent-memory api + worker. Embeddings route through the
  # null-stripping proxy to the SAME local nomic embedder the app uses (one embedding
  # family, one Redis); generation uses Claude. Index is pinned to 768 dims to match
  # nomic. See OPERATIVE.md for why the proxy exists.
  embedding-proxy:
    image: node:20-alpine
    container_name: narcore-embed-proxy
    depends_on:
      - embedding
    command: ["node", "/app/server.js"]
    environment:
      UPSTREAM_URL: http://embedding:8080
      PORT: "8090"
    volumes:
      - ./infra/embedding-proxy/server.js:/app/server.js:ro
    restart: unless-stopped

  agent-memory:
    image: redislabs/agent-memory-server:latest
    container_name: narcore-memory
    depends_on:
      redis:
        condition: service_healthy
      embedding-proxy:
        condition: service_started
    ports:
      - "8000:8000" # REST API — AGENT_MEMORY_URL=http://localhost:8000
    environment: &agent-memory-env
      REDIS_URL: redis://redis:6379
      DISABLE_AUTH: "true"
      AUTH_MODE: disabled
      GENERATION_MODEL: anthropic/claude-sonnet-4-6
      FAST_MODEL: anthropic/claude-haiku-4-5
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
      # Embeddings → null-stripping proxy → nomic (text-embedding-3-small is just a
      # name LiteLLM accepts; OPENAI_API_BASE points it at our local embedder).
      OPENAI_API_BASE: http://embedding-proxy:8090/v1
      OPENAI_API_KEY: sk-noop
      EMBEDDING_MODEL: text-embedding-3-small
      REDISVL_VECTOR_DIMENSIONS: "768"
      # Explicit memories carry their own topics/entities — skip the heavy auto-extractors.
      ENABLE_NER: "false"
      ENABLE_TOPIC_EXTRACTION: "false"
      ENABLE_DISCRETE_MEMORY_EXTRACTION: "false"
    restart: unless-stopped

  # The Docket worker indexes long-term memories. It MUST run or memories are never
  # searchable (create returns ok but search stays empty).
  agent-memory-worker:
    image: redislabs/agent-memory-server:latest
    container_name: narcore-memory-worker
    depends_on:
      - agent-memory
    command: ["agent-memory", "task-worker"]
    environment: *agent-memory-env
    # The worker serves no HTTP port, so the image's API healthcheck would mark it
    # "unhealthy" even though it's indexing fine — disable it to avoid confusion.
    healthcheck:
      disable: true
    restart: unless-stopped

volumes:
  redis-data:

```

### src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";

const fontSans = Geist({
  variable: "--font-sans",
  subsets: ["latin"],
});

const fontMono = Geist_Mono({
  variable: "--font-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: {
    default: "Narcore",
    template: "%s · Narcore",
  },
  description: "Build something the public can trust.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html
      lang="en"
      className={`${fontSans.variable} ${fontMono.variable} h-full antialiased`}
      suppressHydrationWarning
    >
      <body className="min-h-full">
        <ThemeProvider
          attribute="class"
          defaultTheme="dark"
          enableSystem={false}
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

```

### infra/embedding-proxy/server.js

```javascript
// Tiny OpenAI-embeddings sanitizing proxy (zero deps).
//
// Why this exists: the Redis Agent Memory Server embeds via LiteLLM, which sends
// `"encoding_format": null` on the OpenAI /v1/embeddings call. The llama.cpp server
// that serves nomic-embed-text rejects a null where it expects a string
// ("[json.exception.type_error.302] type must be string, but is null"). This proxy
// sits between them and strips null-valued top-level fields from the JSON body, so
// the memory server can use the same local nomic embedder the app uses — one
// embedding family, one box, no external embedding key.
//
// Standalone CommonJS Node script run in a node:20-alpine container (`node
// server.js`) — not part of the Next/TS app, hence require() over import.
/* eslint-disable @typescript-eslint/no-require-imports */
const http = require("node:http");

const UPSTREAM = process.env.UPSTREAM_URL || "http://embedding:8080";
const PORT = Number(process.env.PORT || 8090);
const upstream = new URL(UPSTREAM);

function stripNulls(value) {
  if (Array.isArray(value)) return value.map(stripNulls);
  if (value && typeof value === "object") {
    for (const key of Object.keys(value)) {
      if (value[key] === null) delete value[key];
      else value[key] = stripNulls(value[key]);
    }
  }
  return value;
}

const server = http.createServer((req, res) => {
  const chunks = [];
  req.on("data", (c) => chunks.push(c));
  req.on("end", () => {
    let body = Buffer.concat(chunks);
    const contentType = req.headers["content-type"] || "";
    if (contentType.includes("application/json") && body.length) {
      try {
        body = Buffer.from(JSON.stringify(stripNulls(JSON.parse(body.toString("utf8")))));
      } catch {
        /* not JSON we can parse — forward unchanged */
      }
    }
    const options = {
      hostname: upstream.hostname,
      port: upstream.port || 80,
      path: req.url,
      method: req.method,
      headers: { ...req.headers, host: upstream.host, "content-length": Buffer.byteLength(body) },
    };
    const proxied = http.request(options, (pres) => {
      res.writeHead(pres.statusCode || 502, pres.headers);
      pres.pipe(res);
    });
    proxied.on("error", (err) => {
      res.writeHead(502, { "content-type": "application/json" });
      res.end(JSON.stringify({ error: { message: String(err), type: "proxy_error" } }));
    });
    proxied.end(body);
  });
});

server.listen(PORT, () => {
  console.log(`[embed-proxy] listening on :${PORT} -> ${UPSTREAM} (stripping null JSON fields)`);
});

```

### src/app/page.tsx

```typescript
import { ArrowRight } from "lucide-react";
import Link from "next/link";

import { Button } from "@/components/ui/button";
import { CodeSphereField } from "@/components/landing/code-sphere-field";
import { TypingHeadline } from "@/components/landing/typing-headline";
import { Logo } from "@/components/logo";
import { TopNav } from "@/components/top-nav";

export default function Home() {
  return (
    <main className="relative flex min-h-dvh flex-col items-center justify-center overflow-hidden px-6 py-16">
      <TopNav placement="absolute" />

      {/* Restrained background: rotating code-spheres, faint azure glow + hairline grid. */}
      <div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
        {/* Ambient "depth of the corpus": code-glyph spheres rotating in 3D. */}
        <CodeSphereField intensity="subtle" />
        <div
          className="absolute inset-0 opacity-[0.04]"
          style={{
            backgroundImage:
              "linear-gradient(to right, var(--foreground) 1px, transparent 1px), linear-gradient(to bottom, var(--foreground) 1px, transparent 1px)",
            backgroundSize: "64px 64px",
            maskImage:
              "radial-gradient(ellipse 80% 60% at 50% 0%, black, transparent 75%)",
          }}
        />
        <div
          className="absolute -top-40 left-1/2 size-[40rem] -translate-x-1/2 rounded-full blur-3xl"
          style={{
            background:
              "radial-gradient(circle, color-mix(in oklch, var(--primary) 22%, transparent), transparent 70%)",
          }}
        />
      </div>

      <div className="flex w-full max-w-2xl flex-col items-center text-center">
        <span className="inline-flex items-center gap-2 rounded-full border border-border bg-card/50 px-3 py-1 font-mono text-xs text-muted-foreground">
          <span className="size-1.5 rounded-full bg-primary" />
          Law-enforcement AI for illicit-drug trafficking
        </span>

        <div className="mt-8 flex items-center gap-3">
          <Logo className="size-9" />
          <span className="font-mono text-lg font-medium tracking-tight">
            narcore
          </span>
        </div>

        <TypingHeadline />

        <p className="mt-5 max-w-xl text-base text-pretty text-muted-foreground sm:text-lg">
          Narcore scans social platforms for drug ads, sends an autonomous undercover operative to
          confirm the deal and meeting, and exports a court-ready case report.
        </p>

        <div className="mt-10 flex flex-col items-center gap-3 sm:flex-row">
          <Button
            size="lg"
            className="h-11 px-5 text-sm"
            nativeButton={false}
            render={<Link href="/command" />}
          >
            Open Command Center
            <ArrowRight aria-hidden="true" />
          </Button>
          <Button
            variant="outline"
            size="lg"
            className="h-11 px-5 text-sm"
            nativeButton={false}
            render={<Link href="/feed" />}
          >
            See the live feed
          </Button>
        </div>
      </div>
    </main>
  );
}

```

### src/app/memory/page.tsx

```typescript
import type { Metadata } from "next";

import { MemoryClient } from "@/components/memory/memory-client";
import { TopNav } from "@/components/top-nav";

export const metadata: Metadata = {
  title: "Operative Memory",
  description: "Redis Iris agent memory — what the operative has learned across operations.",
};

export default function MemoryPage() {
  return (
    <>
      <TopNav />
      <MemoryClient />
    </>
  );
}

```

### src/app/semantic-drift/page.tsx

```typescript
import type { Metadata } from "next";

import { SemanticDriftClient } from "@/components/semantic-drift/semantic-drift-client";
import { TopNav } from "@/components/top-nav";

export const metadata: Metadata = {
  title: "Semantic Drift",
  description: "Redis vector-space visualization for Narcore.",
};

export default function SemanticDriftPage() {
  return (
    <>
      <TopNav />
      <SemanticDriftClient />
    </>
  );
}

```

### src/app/(dashboard)/layout.tsx

```typescript
// Route-group layout for the dashboard. Mounts the sonner <Toaster /> here so toast
// is scoped to the dashboard surface without touching the shared root layout.
import { TopNav } from "@/components/top-nav";
import { Toaster } from "@/components/ui/sonner";

export default function DashboardLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <>
      <TopNav />
      {children}
      <Toaster position="bottom-right" />
    </>
  );
}

```

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