# 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)

<div align="center">

<img src="src/app/icon.svg" alt="narcore logo" width="76" height="76" />

# narcore

### A self-improving undercover narcotics investigator for social media.

**A Browserbase agent infiltrates logged-in Instagram and negotiates a real deal in a live war room, and every bust it closes is written into Redis memory + a vector corpus that makes the *next* operation smarter. All on one Redis.**

<br/>

[![Next.js](https://img.shields.io/badge/Next.js-16.2-000000?logo=nextdotjs&logoColor=white)](https://nextjs.org)
[![React](https://img.shields.io/badge/React-19.2-149ECA?logo=react&logoColor=white)](https://react.dev)
[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
[![Redis](https://img.shields.io/badge/Redis_Stack-Vector_+_Iris_Memory-FF4438?logo=redis&logoColor=white)](https://redis.io)
[![Browserbase](https://img.shields.io/badge/Browserbase-Stagehand-1A1A1A?logo=googlechrome&logoColor=white)](https://browserbase.com)
[![Anthropic](https://img.shields.io/badge/Claude-Sonnet_4.6-D97757?logo=anthropic&logoColor=white)](https://anthropic.com)
[![Tailwind](https://img.shields.io/badge/Tailwind-v4-06B6D4?logo=tailwindcss&logoColor=white)](https://tailwindcss.com)

**Semantic detection · autonomous undercover negotiation · memory that compounds**

<br/>

[![Watch the narcore demo](narcore_home.png)](https://youtu.be/r2Dl7fKbcsU)

### ▶️ [Watch the 3-minute demo](https://youtu.be/r2Dl7fKbcsU)

</div>

---

## ⚡ The idea

Drug advertising left the dark web years ago. It's on Instagram, X, TikTok, and Telegram now, hidden in plain sight behind coded language, emojis, misspellings, and acronyms. The instant a keyword filter learns a term, the seller rotates to a new one. This is **semantic drift**, and static blocklists are always one step behind it.

**Narcore closes the loop.** It detects coded ads by *meaning* (not keywords), lets an analyst teach it new slang with one click, and then sends an autonomous **Browserbase operative** undercover into the seller's DMs to confirm a deal and a meeting location. Every confirmed bust feeds two Redis systems (a **vector corpus** and an **Iris agent memory**), so the *next* operation starts smarter.

> ### _It gets better at catching dealers every time it catches one._

---

## 🎯 The problem

Sellers don't write "selling oxycodone." They write:

```
"restockd 🔵 m30z, hmu for PUFF 🍃, cashapp ready, signal me 📲"
```

- **Coded vocabulary** that mutates weekly (`blues`, `m30`, `perc`, `zaza`, `plug`…).
- **Emoji & symbol substitution** (🍃 💊 🔵 👃) that slips past text filters.
- **Hand-offs to encrypted apps** (`telegram`, `signal`) to move the deal off-platform.

A blocklist can only catch what it already knows. By the time a human adds a new term, the seller has invented three more. **You cannot keyword-match your way out of an adversarial, drifting vocabulary.** You have to match *meaning*, and you have to *keep learning*.

---

## 🔁 How narcore works: one loop, one Redis

Narcore isn't a pile of features. It's a single self-reinforcing flywheel:

```
                ┌─────────────────────────── ONE REDIS ───────────────────────────┐
                │                                                                  │
  Instagram ───▶│   vector corpus (detection)      ◀── R1 writes the seller's     │
 (Browserbase)  │   agent memory (Redis Iris)      ◀── R2 pins each confirmed bust │
       ▲        │   operation state + live streams                                 │
       │        └──────────────────────────────────────────────────────────────-──┘
       │                          ▲                              │
       │   B2 consistent identity │                              │ R2 recall primes
       │                          │                              ▼ the next operation
       └─────────  the operative (Stagehand · B3 self-healing DM-open)  ───────────┘

  THE LOOP:  Browserbase produces field intel ──▶ Redis learns ──▶ the next op is smarter
```

### The end-to-end workflow

| # | Stage | What happens |
|---|-------|--------------|
| **1** | **Detect** | A Browserbase + Stagehand fleet scrapes posts → each is embedded (nomic, 768-d) → a **Redis vector KNN** finds the nearest known drug-slang vector → semantic score + corroborating heuristics → **flagged** if risk ≥ `0.7` (shown as 70 on the UI's 0–100 scale). |
| **2** | **Analyst learning loop** | An analyst clicks **Approve** on a real ad → the caption is re-embedded and added to the corpus (`corpus:approved:*`). Future paraphrases of that term now flag automatically. |
| **3** | **Operative negotiation** | Analyst clicks **Engage** → a Browserbase operative opens the seller's DM and negotiates as a buyer, confirming **Deal ✓** and **Location ✓** turn by turn in a live war room. |
| **4** | **Field-intel learning (R1)** | On a *confirmed* bust, Claude extracts the slang the **seller** actually used → embedded → `corpus:field:*` in the **same** vector index → pending posts re-scored → a live `stream:field-intel` ticker fires. |
| **5** | **Lead summary & export** | One click produces a court-ready report: handle, platform, detected code-words, risk breakdown, the matched term, and an LLM-drafted outreach email, exportable as `.md`/`.txt`. |
| **6** | **Cross-operation memory (R2)** | The bust is pinned to **Redis Iris agent memory**. Before the next operation, the operative **recalls** what worked against similar sellers and walks in already primed. |

> In one breath: **scrape → score by meaning → analyst approves → system learns → paraphrase now flags → operative DMs the seller → deal + location confirmed → field slang learned → next operative is smarter.**

---

## 🖼️ Screenshots

**Command Center:** the ranked lead queue beside a live undercover operative working a DM in real time.

![Command Center: ranked illicit-drug advertising leads and the live undercover operative](narcore_outreach.png)

| | |
|---|---|
| **Fleet / War Room** (`/agents`): parallel Browserbase browsers scanning Instagram hashtags, each in a live-view tile | **Lead detail:** the risk breakdown, nearest known term, and the heuristic hits behind every flag |
| ![Fleet of parallel Browserbase agents](narcore_fleet.png) | ![Lead risk breakdown and detected code words](narcore_details.png) |

---

## 🏗️ Architecture

```
                         ┌──────────────────────────────────────────────┐
                         │           Next.js 16 (App Router)            │
   Browser ◀────────────▶│  Frontend: dashboard · war room · feed ·     │
                         │            operations · memory · drift viz   │
                         │  Backend:  Route Handlers  (/api/*)          │
                         └───────┬───────────────┬───────────────┬──────┘
                                 │               │               │
                  embed(caption) │      KNN /    │   negotiate / │
                                 ▼      state    ▼   extract     ▼
                  ┌──────────────────┐  ┌──────────────────┐  ┌─────────────────────┐
                  │  Embedding side- │  │   Redis Stack    │  │  Browserbase +       │
                  │  car (llama.cpp) │  │  ▸ idx:corpus    │  │  Stagehand           │
                  │  nomic-v2-moe    │  │  ▸ idx:posts     │  │  ▸ scraper fleet     │
                  │  768-d /v1/embed │  │  ▸ op:* state    │  │  ▸ undercover op     │
                  └──────────────────┘  │  ▸ stream:field  │  └──────────┬──────────┘
                                        │  ▸ Iris memory   │             │
                                        └──────────────────┘   Anthropic │ Claude
                                                                  (brain) ▼
                                                          negotiate · extract slang · draft outreach
```

**Data flow (one line):**

```
scraper ─► POST /api/ingest ─► embed caption ─► Re

[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.]