A team-wide prompting coach. Every prompt sent through Claude.ai, VS Code, Claude Code, or Copilot Chat is scored on five dimensions in real time. Weak prompts trigger a short teaching loop. Strong prompts feed a team wiki, which gets injected back into the next person's context β so a team's "way of prompting" compounds without anyone writing docs.
Trailhead is the engineering codename inside the repo; LearnLoop is the product name on the marketing site.
- Landing page / waitlist: https://learnloop-gules.vercel.app/
- Demo video (3 min walkthrough): https://www.youtube.com/watch?v=kD6nnJAmRK8
Every prompt is scored 0β10 on:
goal_clarityβ what outcome is being asked forspecificityβ concrete files, functions, errors namedcontext_loadingβ relevant code/docs/examples attachedconstraint_articulationβ what must not change, perf/style limitsoutput_specificationβ desired shape of the response
overall = round(mean of the five dims). Below 7 triggers coaching; β₯ 7 lands
silently. Joining the team's prompt library is stricter: the unrounded mean
must be β₯ 7.0, no dimension below 5, and an independent re-score must agree
(optionally plus a teammate's review β see SELFHOSTING.md β Security model).
The rubric is concrete enough that a human reviewer could apply it β the LLM
is the implementation, not the product. Scores come from an LLM and vary run
to run; apps/api/eval/ measures how much.
The repo is a working npm-workspaces monorepo. Six surfaces, all wired to one backend, all sharing the same TypeScript contract.
Single source of truth. Multi-tenant: each team has a public team id and a
server-minted secret (only its SHA-256 is stored), sent as X-Team-Token.
Pre-2026-09-30 tokens derived from the git remote still work behind
TRAILHEAD_ACCEPT_LEGACY_TOKENS (deprecated). Routes are in
apps/api/src/app.ts (index.ts just serves them):
| Method + Path | What it does |
|---|---|
GET / |
Health + endpoint catalog (unauth) |
POST /teams |
Register a team (unauth; gated by TRAILHEAD_ADMIN_TOKEN when set). Returns { team_id, name, secret } once; 409 if the id is taken β the join flow |
POST /teams/rotate-secret |
New secret for the caller's team; the old credential stops working. Upgrades a legacy team |
GET /teams |
Resolves the caller's own team. Never returns the secret β { name, id, legacy, team_id? } (id is an opaque digest; team_id only for non-legacy teams) |
POST /score |
5-dimension Gemini score; writes skill_observation rows with a 30 s per-dimension dedup window |
POST /coach |
Stateless teachβreveal coaching loop, capped at 5 rounds |
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 /prompts/proven |
The team's graduated prompts, filterable by score, path, topic |
GET /prompts/pending |
Library candidates awaiting review (TRAILHEAD_PROMOTION_MODE=review) |
POST /prompts/:id/review |
{ approve: true } graduates a pending prompt, false discards it |
GET /search?q=&scope= |
Substring search over rules, durable learnings and graduated prompts |
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 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. 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
request, one nested generation per LLM call, with token usage and latency.
Tracing silently no-ops when keys are missing.
Vanilla TypeScript + esbuild. Manifest declares https://claude.ai/* as the
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 β 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
/diffview inline - Outcome rating chips on each assistant bubble (π / π€· / π β
/capture) - Wiki toast β drops in when
/wiki/recentpolling 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:
β₯ 7lets the native send fire;< 7keeps 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.
Sidebar webview registered under the trailhead activity bar. Settings expose
trailhead.apiUrl, trailhead.teamToken, trailhead.userId. Same 5-dimension
score-card render as the browser extension, plus a wiki-diff polling loop that
toasts when /wiki/recent reports a new insight.
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: trueand neithertrailhead-mcpnor@trailhead/mcp-serverexists on the registry, sonpx trailhead-mcpdoes not work. Run it from a clone β see SELFHOSTING.md.
CLI subcommands (bin/cli.mjs):
trailhead-mcp initβ per-repo install. Sets up the repo's team, then writes.mcp.json+CLAUDE.mdfor Claude Code and.vscode/mcp.json+.github/copilot-instructions.mdfor Copilot. Idempotent. Credential order:--team-tokenβTRAILHEAD_TEAM_TOKENβ.trailhead-teamβ otherwise registerteam_<hash of the normalised remote URL>viaPOST /teamsand save the returned secret in.trailhead-team(gitignored). If the team is already registered,initexplains how to join (get the secret from a teammate,--team-token).--upgrade-legacymoves a pre-2026-09-30 team to a secret. The MCP configs reference.trailhead-team(TRAILHEAD_TEAM_FILE) instead of embedding the secret.trailhead-mcp bootstrapβ walks the cwd, bundles source files, posts to/onboard/repo/full. Default rich mode shows a live progress bar. Flags:--minimal,--paths,--force,--dry-run,--yes.trailhead-mcp resetβ wipes the team's wiki/captures/observations.
App router, server components for the team view, SWR for the live charts.
Shows one team β the one whose secret is in the server-side
TRAILHEAD_TEAM_TOKEN; the browser never sees the secret (client charts go
through a read-only proxy route, /api/trailhead/*). Pages (src/app/):
/β team view/skill-arcβ per-dimension team chart driven by/skill-arc, polls every 2 s during the demo/teamβ L1βL2 metric cards from/team/metrics/wikiβ node tree + durable learnings from/wiki/tree/onboardingβ the wiki as an onboarding guide
Single static index.html + JSX components loaded at runtime via Babel
standalone. Tailwind via CDN. Sections: hero, problem, solution, features,
demo, footer. Deployed at https://learnloop-gules.vercel.app/.
packages/sharedβ TypeScript types for every API request/response. Every surface imports from here so wire shapes can't drift.packages/scoringβ Locked Gemini prompt templates (score, augment, teach, topic, extract) and pure helpers (buildAugmentation,normalize,normalizePath,ancestorPaths, the teach/skip/success reveal renderers).packages/score-cardβ Pure-DOM render function for the 5-dimension card. Used by the browser extension and the VS Code webview.packages/dbβschema.sql(idempotent, everyCREATEusesIF NOT EXISTS),migrate.mjs,check.mjs, andseed.mjsfor the Acme Fintech demo data.
Eight tables in packages/db/schema.sql:
teamsβ tenancynodesβ one row per folder or file path; carriesbody_mdlearningsβ accumulated insights with normalize-based dedup, counter, anddraft | durablestatuspromptsβ graduated prompt templates withtopicandreuse_countcapturesβ stored conversations with outcomeskill_observationsβ per-dimension score writes;prompt_hashbacks the 30 s dedup windowwiki_jobs+wiki_job_pathsβ async rich-bootstrap state
The demo team (Acme Fintech, public secret trailhead_demo_acme_2026) is hardcoded
into the schema with a fixed UUID so every surface can reference it without a
lookup.
apps/
api/ Hono + TypeScript backend (Railway)
browser-ext/ Chrome MV3 extension for Claude.ai
vscode-ext/ VS Code IDE extension
mcp-server/ MCP server (Claude Code + Copilot Chat) + CLI
dashboard/ Next.js 16 dashboard (Vercel)
landing-page/ Static marketing site (LearnLoop)
packages/
shared/ TypeScript types β single source of truth for API shapes
scoring/ Locked Gemini prompt templates + pure helpers
score-card/ Pure-DOM render of the 5-dimension card
db/ Postgres schema, migrations, seed data
docs/
superpowers/specs/ Design specs
roadmaps/ Per-surface 24h build roadmaps
Trailhead is self-hosted. There is no hosted backend to sign up for β you run the API, and every client points at it.
Everything you need is Docker and a Gemini API key from https://aistudio.google.com/apikey.
cp .env.example .env # then put your Gemini key in it
docker compose up
# β API on http://localhost:3000, Postgres schema applied automaticallyThat is the whole setup. See SELFHOSTING.md for pointing the browser extension, VS Code extension, MCP server and dashboard at it, and for running against an external database instead.
Prerequisites:
- Node
>= 22.6 - A Postgres database (Neon β
DATABASE_URLmust includesslmode=require) - A Gemini API key from https://aistudio.google.com/apikey
# 1. Install workspace dependencies
npm install
# 2. Configure environment
cp .env.example .env
# Edit .env β fill in DATABASE_URL and GEMINI_API_KEY
# 3. Apply the schema (idempotent, safe to re-run)
psql "$DATABASE_URL" -f packages/db/schema.sql
# 4. (optional) Seed the Acme Fintech demo data
node packages/db/seed.mjs
# 5. Run the API
npm run dev
# β http://localhost:3000The API refuses to boot without DATABASE_URL and GEMINI_API_KEY.
Before exposing the API beyond localhost, read
SELFHOSTING.md β Security model: set
TRAILHEAD_ADMIN_TOKEN, and turn legacy tokens off once your teams have
upgraded.
# Dashboard (Next.js, port 3001)
NEXT_PUBLIC_API_URL=http://localhost:3000 \
NEXT_PUBLIC_TEAM_TOKEN=trailhead_demo_acme_2026 \
npm --workspace=apps/dashboard run dev
# Browser extension β build, then load apps/browser-ext/dist as unpacked
npm --workspace=@trailhead/browser-ext run build
# chrome://extensions β Developer mode β Load unpacked β apps/browser-ext/dist/
# 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. 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
node /path/to/LearnLoop/apps/mcp-server/bin/cli.mjs init
node /path/to/LearnLoop/apps/mcp-server/bin/cli.mjs bootstrapnpm run typecheck # tsc --noEmit across all workspaces
npm run lint # ESLint (flat config: eslint.config.mjs) over the whole repo
npm run test # run all workspace tests
npm run build # build all workspaces that expose a build scriptSingle 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-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 |
TEAM_TOKEN |
β | Documentation only: the public demo team's token. The API does not read it; clients hardcode the same value as their fallback |
PORT |
api | Defaults to 3000; Railway injects automatically |
TRAILHEAD_ADMIN_TOKEN |
api | When set, POST /teams (registration) requires it as X-Admin-Token |
TRAILHEAD_ACCEPT_LEGACY_TOKENS |
api | Default true. Accept pre-2026-09-30 remote-derived tokens for teams without a secret (deprecated) |
TRAILHEAD_AUTO_CREATE_TEAMS |
api | Default false. Legacy only: unknown tokens create legacy teams |
TRAILHEAD_SCORE_TEMPERATURE / TRAILHEAD_SCORE_THINKING_BUDGET |
api | Scorer sampling (defaults 0.2 / -1 = dynamic). Measure before changing: apps/api/eval/ |
TRAILHEAD_RL_REGISTER_PER_IP / TRAILHEAD_RL_LLM_PER_TEAM / TRAILHEAD_RL_LLM_PER_IP / TRAILHEAD_RL_BOOTSTRAP_PER_TEAM |
api | Rate limits as N/W (defaults 10/1h, 120/1m, 120/1m, 6/1h), or off. In-process, so per replica β see SELFHOSTING.md |
TRAILHEAD_RATE_LIMIT |
api | off disables every rate limit |
TRAILHEAD_TRUST_PROXY |
api | true behind your own (single-hop) reverse proxy: per-IP limits key on the last X-Forwarded-For entry, the one the proxy appended |
TRAILHEAD_EXPOSE_ERRORS |
api | true to include the raw error message in 500 responses (local debugging). Default: only a request_id that matches the server log |
TRAILHEAD_PROMOTION_MODE |
api | auto (default): gated auto-promotion into the library. review: promoted prompts wait for a teammate's approval |
TRAILHEAD_ALLOW_DEMO_RESET |
api | true to allow DELETE /team/data on the demo team |
TRAILHEAD_API_URL |
dashboard | Server-side, runtime. Where the dashboard fetches (fallback: legacy NEXT_PUBLIC_API_URL) |
TRAILHEAD_TEAM_TOKEN |
dashboard | Server-side, runtime. The team secret; never sent to the browser (fallback: legacy NEXT_PUBLIC_TEAM_TOKEN) |
trailhead.apiUrl / .teamToken / .userId / .shareUserId |
vscode-ext | VS Code settings. userId empty = random per-install id; shareUserId: false sends anonymous |
TRAILHEAD_USER_ID / TRAILHEAD_SHARE_USER_ID |
mcp-server | Override the per-machine anonymous id, or false to send anonymous (see SELFHOSTING.md β Security model) |
TRAILHEAD_API_URL / TRAILHEAD_TEAM_FILE / TRAILHEAD_TEAM_TOKEN |
mcp-server | Per-repo MCP config. init writes TEAM_FILE (path to .trailhead-team); TEAM_TOKEN overrides it |
- API β Railway.
railway.jsondeclaresnpm --workspace=apps/api startwith healthcheck on/. - Dashboard β Vercel. Set
TRAILHEAD_API_URLandTRAILHEAD_TEAM_TOKEN(server-side env), thenvercel --prodfromapps/dashboard/. Anyone who can open it can read that team's data (read-only), so restrict access. - Landing page β Vercel β already live at https://learnloop-gules.vercel.app/.
- Browser extension β loaded unpacked from
apps/browser-ext/dist/. - VS Code extension β
vsce packagefromapps/vscode-ext/. - MCP server β not published to npm (
private: true). Wired into a repo by runningapps/mcp-server/bin/cli.mjs initfrom a clone (per-repo wiring, multi-tenant token derivation from the git remote).
- 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. β₯ 7sends 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/scorewrites 5skill_observationrows; the dashboard's/skill-arcchart polls every 2 s, so the rightmost bucket climbs as the user prompts.- In Claude Code or Copilot Chat, the MCP server's
coachtool is called first. Server returnsproceed: falseplus a teach-block when the score is low; the host LLM relays the block, gathers a reply, calls back. Five rounds max, then a reveal block shows the score arc and prompt diff. - When the user states a teamwide convention,
wiki_savecallsPOST /wiki/propose. Server-side normalize + dedup means repeated calls reinforce the same draft instead of duplicating;reinforcement_count >= 3promotesdraft β durable. The VS Code extension polls/wiki/recentand toasts the update β visible proof of the autonomous loop. - Next prompt the same engineer (or a teammate) types in the same path
triggers
/scoreagain, but nowcontext_pathpulls the team's HCL bundle into Gemini's system prompt, so the rubric is calibrated against the team's own conventions.
The project was specced before it was built. Source of truth for why:
docs/superpowers/specs/2026-04-25-trailhead-design.mdβ master specdocs/superpowers/specs/2026-04-25-mcp-plugin-ux-design.mdβ MCP install story and (then) four-tool surfacedocs/superpowers/specs/2026-04-25-trailhead-browser-ext-design.mdβ Claude.ai content-script architecturedocs/superpowers/specs/2026-04-25-demo-completion-design.mdβ dashboard and seeding plandocs/superpowers/specs/2026-04-26-trailhead-educational-loop-design.mdβ the teach β reveal coaching loopdocs/superpowers/specs/2026-04-26-wiki-bootstrap-rich-design.mdβ async rich bootstrapdocs/superpowers/specs/2026-04-26-improve-widget-design.mdβ multi-turn improve widgetdocs/roadmaps/β per-surface 24-hour build plans
Each app and package also has its own README.md covering surface-specific
contracts, builds, and tests.
- Backend: Hono, TypeScript, Node 22,
@hono/node-server, rawpg - DB: Postgres on Neon, no ORM
- 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 16 + 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: self-hosted API (see SELFHOSTING.md;
railway.jsonremains for anyone who wants a Railway deploy), Vercel (dashboard + landing page), per-repo MCP wired from a clone viaapps/mcp-server/bin/cli.mjs init(unpublished)

