Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Keep the build context small and reproducible. node_modules in particular is
# hundreds of MB and MUST NOT be copied — the image installs its own via
# `npm ci`, and a host copy would carry platform-specific binaries.
node_modules
**/node_modules

# Build output — the API runs from source via tsx.
dist
**/dist
build
**/build
out
**/out
.next
**/.next
*.tsbuildinfo
apps/vscode-ext/*.vsix
apps/browser-ext/.plasmo

# Secrets. GEMINI_API_KEY and friends are injected at runtime by compose,
# never baked into an image layer.
.env
.env.*
!.env.example

# VCS / tooling / docs — irrelevant to the API image.
.git
.github
.claude
.superpowers
coverage
.cache
.turbo
.parcel-cache
archive
docs
*.log
Dockerfile
docker-compose.yml
102 changes: 79 additions & 23 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,32 +1,88 @@
# Trailhead — root environment template. Copy to `.env` (which is gitignored).
# Every artifact (api, browser-ext, vscode-ext, mcp-server, dashboard)
# reads from this same set. Spec: docs/superpowers/specs/2026-04-25-trailhead-design.md

# --- Postgres (Neon) ----------------------------------------------------------
# 1. Sign in at https://console.neon.tech and create a project.
# 2. Copy the pooled connection string here (must include `sslmode=require`).
# 3. Apply schema: psql "$DATABASE_URL" -f packages/db/schema.sql
DATABASE_URL=postgresql://USER:PASSWORD@HOST.neon.tech/trailhead?sslmode=require

# --- Hardcoded team token -----------------------------------------------------
# Single shared secret. Every client sends this in the `X-Team-Token` header.
# Demo only — production swaps in Clerk per spec §3 / §16.
TEAM_TOKEN=trailhead_demo_acme_2026
# Trailhead — root environment template. Copy to `.env` (which is gitignored):
#
# cp .env.example .env
#
# Then set GEMINI_API_KEY below and run `docker compose up`. Everything else
# here has a working default. See SELFHOSTING.md for the full walkthrough.
#
# Trailhead is self-hosted: there is no hosted API. Every client (browser-ext,
# vscode-ext, mcp-server, dashboard) defaults to http://localhost:3000, which
# is what `docker compose up` publishes.

# =============================================================================
# REQUIRED
# =============================================================================

# --- Gemini -----------------------------------------------------------------
# Required for /score (gemini-3-flash-preview, JSON schema mode) and /diff (gemini-3-flash-preview).
# Get one at https://aistudio.google.com/apikey.
# --- Gemini ------------------------------------------------------------------
# The only value you must supply. Powers /score, /coach, /improve, /diff and
# the rich wiki bootstrap. The API refuses to start without it.
# Get one at https://aistudio.google.com/apikey
GEMINI_API_KEY=

# =============================================================================
# OPTIONAL — every value below has a working default
# =============================================================================

# --- Postgres ----------------------------------------------------------------
# Leave DATABASE_URL unset to use the `postgres` service in docker-compose.yml.
# Compose builds the connection string from POSTGRES_USER / POSTGRES_PASSWORD /
# POSTGRES_DB and points the API at it automatically, and applies
# packages/db/schema.sql on the first boot of a fresh volume.
#
# Set it only to use an EXTERNAL database instead (Neon, RDS, ...). A hosted
# Postgres generally needs `?sslmode=require`.
# DATABASE_URL=postgresql://USER:PASSWORD@HOST/trailhead?sslmode=require
POSTGRES_USER=trailhead
POSTGRES_PASSWORD=trailhead
POSTGRES_DB=trailhead
# Host port for Postgres — change if 5432 is already in use. Used by psql and
# the scripts in packages/db (seed.mjs, migrate.mjs, check.mjs).
POSTGRES_PORT=5432

# --- API ---------------------------------------------------------------------
# Host port the API is published on. The container always listens on 3000;
# this is the host side of the mapping. Changing it means updating every
# client's API URL too (see SELFHOSTING.md).
PORT=3000

# Any unrecognised X-Team-Token spawns its own team row. The right default for
# a single-tenant self-host — teammates cloning the repo land in the same team
# with no admin step. Set to false to require teams be registered explicitly.
TRAILHEAD_AUTO_CREATE_TEAMS=true

# Safety catch on DELETE /team/data for the seeded demo team. Set true only if
# you really want `trailhead-mcp reset` to be able to wipe it.
TRAILHEAD_ALLOW_DEMO_RESET=false

# --- Team token --------------------------------------------------------------
# The demo team's token, and the fallback the clients ship with. Real teams get
# a token derived from their git remote by `npx trailhead-mcp init` — this is
# only the demo/seed value.
TEAM_TOKEN=trailhead_demo_acme_2026

# --- Langfuse (optional) -----------------------------------------------------
# Hosted observability for every Gemini call made by the API. Keys live at
# https://cloud.langfuse.com Settings API Keys. When unset the API still
# works — tracing silently no-ops with a one-line warning at startup.
# Hosted observability for every Gemini call the API makes. Keys at
# https://cloud.langfuse.com -> Settings -> API Keys. When unset the API still
# works — tracing no-ops with a one-line warning at startup.
LANGFUSE_PUBLIC_KEY=
LANGFUSE_SECRET_KEY=
# EU region (default). Use https://us.cloud.langfuse.com for the US region.
LANGFUSE_BASEURL=https://cloud.langfuse.com

# --- API ---------------------------------------------------------------------
# Local dev only. Railway injects PORT automatically when deployed.
PORT=3000
# =============================================================================
# CLIENTS — only needed when running a client outside its default
# =============================================================================

# --- MCP server (apps/mcp-server) --------------------------------------------
# Base URL of your API. `npx trailhead-mcp init` writes this into the generated
# MCP config. Unset -> the CLIs warn and fall back to http://localhost:3000.
# TRAILHEAD_API_URL=http://localhost:3000
# Per-repo team token, normally auto-derived from the git remote.
# TRAILHEAD_TEAM_TOKEN=

# --- Dashboard (apps/dashboard) ----------------------------------------------
# Baked in at build time (NEXT_PUBLIC_*), so a deployed dashboard must set it
# before `next build`. Unset -> http://localhost:3000, and the Teams page says
# so explicitly rather than failing silently.
# NEXT_PUBLIC_API_URL=http://localhost:3000
# NEXT_PUBLIC_TEAM_TOKEN=trailhead_demo_acme_2026
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Bogdan Truta

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
90 changes: 63 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,26 +63,29 @@ Endpoints implemented in `apps/api/src/index.ts`:
| Method + Path | What it does |
|---|---|
| `GET /` | Health + endpoint catalog (unauth) |
| `GET /teams` | List all teams with tokens (unauth, drives the dashboard team picker) |
| `GET /teams` | Resolves the caller's own team (authenticated). Never returns tokens — `{ name, id }` where `id` is an opaque digest |
| `POST /score` | 5-dimension Gemini score; writes `skill_observation` rows with a 30 s per-dimension dedup window |
| `POST /coach` | Stateless 3-round teach→reveal coaching loop |
| `POST /capture` | Stores a `(prompt, response, outcome)` capture from any surface |
| `POST /wiki/propose` | Normalize + dedup an insight on `(node_id, body_normalized)`, increment `reinforcement_count`, promote `draft → durable` at ≥ 3 |
| `GET /context?path=` | Ancestor walk: returns every wiki node whose path is a prefix of the file path, plus its durable learnings |
| `GET /examples?path=` | Top graduated prompts for an ancestor of a file path |
| `GET /wiki/recent?since=ISO` | Polling endpoint for the VS Code wiki-toast surface |
| `POST /diff` | Picks the closest graduated team prompt by topic + ancestry, scores both prompts, asks Gemini Pro to narrate the difference |
| `POST /diff` | Picks the closest graduated team prompt by topic + ancestry, scores both prompts, asks Gemini to narrate the difference |
| `POST /improve` | Multi-turn Gemini-driven prompt rewrite, capped at 5 user replies |
| `GET /skill-arc` | Time-series of per-dimension scores (powers the dashboard hero chart) |
| `GET /team/metrics` | Snapshot: avg overall, reuse rate, durable count, draft count, active users |
| `GET /wiki/tree` | Full node + learnings tree |
| `GET /wiki/export` | The whole team wiki as one markdown document (`?drafts=true`, `?format=json`) |
| `POST /onboard/repo` | Bulk-upsert one node per path, idempotent, optional `initial_rules[path]` for seeding `body_md` |
| `POST /onboard/repo/full` | Async rich bootstrap: accepts a folder + file bundle (capped at 16 MB / 2 000 files / 32 KB per file), enqueues a `wiki_jobs` row, three-pass Gemini fan-out via `setImmediate` |
| `GET /onboard/jobs/:id` | Per-path progress for a rich-bootstrap job |
| `DELETE /team/data` | Wipes the requesting team's data; demo team is protected unless `TRAILHEAD_ALLOW_DEMO_RESET=true` |

LLM work runs through `apps/api/src/gemini.ts`: Gemini 2.5 Flash for scoring
(JSON-schema mode), Gemini 2.5 Pro for diff narration and rich bootstrap.
LLM work runs through `apps/api/src/gemini.ts`. Model assignments live in
`packages/scoring/src/models.mjs`: `gemini-3-flash-preview` for scoring
(JSON-schema mode), topic extraction and diff narration; `gemma-4-31b-it` for
async learning extraction, where latency is tolerable.

Every Gemini call is instrumented with **Langfuse** when
`LANGFUSE_PUBLIC_KEY` / `LANGFUSE_SECRET_KEY` are set — one trace per HTTP
Expand All @@ -92,21 +95,23 @@ Tracing silently no-ops when keys are missing.
### `apps/browser-ext` — Chrome MV3 extension for Claude.ai

Vanilla TypeScript + esbuild. Manifest declares `https://claude.ai/*` as the
content-script host and pre-allowlists the deployed Railway API.
content-script host and allowlists `http://localhost/*` for a self-hosted API.
The popup's **API server** row shows and edits that URL.

Implemented widgets (`src/widgets/`):

- **Score card** under the textarea — debounced 250 ms hits to `/score`,
- **Score card** under the textarea — scores on send (not on keystroke),
per-dimension bars, missing-dimension hints
- **Score badge** on each user bubble
- **Prompt diff panel** — "Compare to team" expands a `/diff` view inline
- **Outcome rating** chips on each assistant bubble (👍 / 🤷 / 👎 → `/capture`)
- **Wiki toast** — drops in when `/wiki/recent` polling sees a new learning
- **Improve chat** — multi-turn rewrite using `/improve`
- **Context pill + popup** — pick a wiki node to bias scoring
- **Send-intercept** — on send: `≥ 7` lets the native send fire; `< 7` shows a
5-second nudge with *Have Claude clarify* / *Send as-is*; auto-sends as-is on
timeout. Fail-open on every API error.
- **Send-intercept** — on send: `≥ 7` lets the native send fire; `< 7` keeps the
score card up with *Improve* / *Send as-is* / *Edit* and waits for the user.
There is no timer and nothing is ever sent automatically. Fail-open on every
API error.

Kill-switch: `chrome.storage.local.set({ 'trailhead.disabled': true })` halts
the extension on next page load.
Expand All @@ -120,19 +125,24 @@ toasts when `/wiki/recent` reports a new insight.

### `apps/mcp-server` — MCP server for Claude Code + Copilot Chat

STDIO MCP server distributed via `npx trailhead-mcp …`. Four hero tools
deliberately collapsed from a previous seven-tool surface so Copilot's tool
selector picks reliably:
STDIO MCP server. Five hero tools, deliberately collapsed from a previous
seven-tool surface so Copilot's tool selector picks reliably:

| Tool | Routes to |
|---|---|
| `coach` | `POST /coach` — server-side teach→reveal cycle returns `proceed: true/false` and a rendered `text` block; the directive is a thin "relay text, follow `proceed`" loop |
| `wiki_lookup` | `GET /context` + `GET /examples` (file-path based) and/or `GET /search` (query) |
| `wiki_save` | `POST /wiki/propose` with server-side dedup |
| `wiki_bootstrap` | `POST /onboard/repo` (skeleton) or `POST /onboard/repo/full` (rich, LLM-populated) |
| `wiki_proven_prompts` | `GET /prompts/proven` — the team's graduated prompts, filterable by score, path and topic |

Plus a `ping` for health checks.

> **Not published to npm.** The package is `private: true` and neither
> `trailhead-mcp` nor `@trailhead/mcp-server` exists on the registry, so
> `npx trailhead-mcp` does not work. Run it from a clone — see
> [SELFHOSTING.md](SELFHOSTING.md).

CLI subcommands (`bin/cli.mjs`):

- `trailhead-mcp init` — per-repo install. Writes `.mcp.json` + `CLAUDE.md`
Expand All @@ -151,7 +161,8 @@ CLI subcommands (`bin/cli.mjs`):
App router, server components for the team list, SWR for the live charts.
Pages (`src/app/`):

- `/` — team picker (lists every team returned by `/teams`)
- `/` — team view (shows the caller's own team; `GET /teams` is authenticated
and returns only the team the configured token resolves to)
- `/skill-arc?team=…` — per-dimension team chart driven by `/skill-arc`,
polls every 2 s during the demo
- `/team?team=…` — L1→L2 metric cards from `/team/metrics`
Expand Down Expand Up @@ -220,6 +231,26 @@ docs/

## Quick start

Trailhead is self-hosted. There is no hosted backend to sign up for — you run
the API, and every client points at it.

### The short way: Docker

Everything you need is Docker and a Gemini API key from
<https://aistudio.google.com/apikey>.

```bash
cp .env.example .env # then put your Gemini key in it
docker compose up
# → API on http://localhost:3000, Postgres schema applied automatically
```

That is the whole setup. See [SELFHOSTING.md](SELFHOSTING.md) for pointing the
browser extension, VS Code extension, MCP server and dashboard at it, and for
running against an external database instead.

### The long way: local Node + your own Postgres

Prerequisites:

- Node `>= 22.6`
Expand Down Expand Up @@ -262,10 +293,12 @@ npm --workspace=@trailhead/browser-ext run build
# VS Code extension — build, then F5 with apps/vscode-ext as the workspace
npm --workspace=apps/vscode-ext run build

# MCP server — install into a target repo
# MCP server — install into a target repo. The package is unpublished
# (private: true), so `npx trailhead-mcp` does NOT work — invoke the CLI by
# path from this clone. It operates on the cwd, so cd into the target first.
cd /path/to/your/repo
npx trailhead-mcp init
npx trailhead-mcp bootstrap
node /path/to/LearnLoop/apps/mcp-server/bin/cli.mjs init
node /path/to/LearnLoop/apps/mcp-server/bin/cli.mjs bootstrap
```

### Workspace scripts
Expand All @@ -285,7 +318,7 @@ Single root `.env.example` — every surface reads from the same set.
| Var | Used by | Notes |
|---|---|---|
| `DATABASE_URL` | api | Postgres connection string, `sslmode=require` |
| `GEMINI_API_KEY` | api | Gemini 2.5 Flash + 2.5 Pro |
| `GEMINI_API_KEY` | api | `gemini-3-flash-preview` + `gemma-4-31b-it` |
| `LANGFUSE_PUBLIC_KEY` | api | Optional. Hosted Langfuse public key (`pk-lf-…`) |
| `LANGFUSE_SECRET_KEY` | api | Optional. Hosted Langfuse secret key (`sk-lf-…`) |
| `LANGFUSE_BASEURL` | api | Defaults to `https://cloud.langfuse.com` (EU). Use `https://us.cloud.langfuse.com` for US |
Expand All @@ -310,18 +343,20 @@ Single root `.env.example` — every surface reads from the same set.
<https://learnloop-gules.vercel.app/>.
- **Browser extension** → loaded unpacked from `apps/browser-ext/dist/`.
- **VS Code extension** → `vsce package` from `apps/vscode-ext/`.
- **MCP server** → distributed via `npx trailhead-mcp init` (per-repo wiring,
- **MCP server** → not published to npm (`private: true`). Wired into a repo by
running `apps/mcp-server/bin/cli.mjs init` from a clone (per-repo wiring,
multi-tenant token derivation from the git remote).

---

## How the pieces fit

1. Engineer types a prompt. Browser extension debounces 250 ms and hits
`/score`. The card mounts under the textarea with five per-dimension bars
and missing-dimension hints.
2. Below 7 → 5-second *Have Claude clarify* nudge, or fall back to *Send
as-is*. Each `/score` writes 5 `skill_observation` rows; the dashboard's
1. Engineer types a prompt and hits send. The extension intercepts the send and
calls `/score`. The card mounts under the textarea with five per-dimension
bars and missing-dimension hints.
2. `≥ 7` sends straight through. Below 7 the card stays up with *Improve* /
*Send as-is* / *Edit* and waits for an explicit choice — no timer, no
auto-send. Each `/score` writes 5 `skill_observation` rows; the dashboard's
`/skill-arc` chart polls every 2 s, so the rightmost bucket climbs as the
user prompts.
3. In Claude Code or Copilot Chat, the MCP server's `coach` tool is called
Expand All @@ -346,7 +381,7 @@ The project was specced before it was built. Source of truth for *why*:

- `docs/superpowers/specs/2026-04-25-trailhead-design.md` — master spec
- `docs/superpowers/specs/2026-04-25-mcp-plugin-ux-design.md` — MCP install
story and four-tool surface
story and (then) four-tool surface
- `docs/superpowers/specs/2026-04-25-trailhead-browser-ext-design.md` —
Claude.ai content-script architecture
- `docs/superpowers/specs/2026-04-25-demo-completion-design.md` — dashboard
Expand All @@ -368,13 +403,14 @@ contracts, builds, and tests.

- **Backend:** Hono, TypeScript, Node 22, `@hono/node-server`, raw `pg`
- **DB:** Postgres on Neon, no ORM
- **LLMs:** Gemini 2.5 Flash (scoring, JSON-schema mode), Gemini 2.5 Pro
- **LLMs:** `gemini-3-flash-preview` (scoring, JSON-schema mode), `gemma-4-31b-it`
(diff narration, rich bootstrap)
- **Observability:** Langfuse (hosted) — one trace per request, one
generation per LLM call
- **Frontend:** Next.js 15 + Tailwind + Recharts + SWR (dashboard); vanilla
TS + esbuild (extensions); React via CDN (landing page)
- **MCP:** `@modelcontextprotocol/sdk`, STDIO transport
- **Build:** npm workspaces; per-package `tsc` / `esbuild`
- **Hosts:** Railway (API), Vercel (dashboard + landing page), per-repo MCP
install via `npx trailhead-mcp`
- **Hosts:** self-hosted API (see SELFHOSTING.md; `railway.json` remains for
anyone who wants a Railway deploy), Vercel (dashboard + landing page), per-repo
MCP wired from a clone via `apps/mcp-server/bin/cli.mjs init` (unpublished)
Loading
Loading