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
21 changes: 17 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
#
# 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.
# Then set GEMINI_API_KEY below and run `docker compose up` (or set
# TRAILHEAD_LLM=offline to try it with no key). 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
Expand All @@ -15,10 +16,17 @@

# --- 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
# the rich wiki bootstrap. The API refuses to start without it, unless
# TRAILHEAD_LLM=offline. Get one at https://aistudio.google.com/apikey
GEMINI_API_KEY=

# gemini (default) | offline. Offline runs with NO model: prompts are scored by
# the rule-based scorer (packages/scoring/src/heuristic-score.mjs), coaching
# text comes from static templates, the rich wiki bootstrap is disabled, and
# every /score and /coach response says so. For trying the stack without a key;
# it never switches on by itself.
# TRAILHEAD_LLM=offline

# =============================================================================
# OPTIONAL — every value below has a working default
# =============================================================================
Expand Down Expand Up @@ -60,6 +68,11 @@ TRAILHEAD_AUTO_CREATE_TEAMS=false
# X-Admin-Token (`init --admin-token`). Leave empty for open registration while
# the API is bound to 127.0.0.1; set it before exposing the API on a network.
TRAILHEAD_ADMIN_TOKEN=
# The seeded demo team (public secret trailhead_demo_acme_2026). on | off.
# Default: on, but off whenever TRAILHEAD_ADMIN_TOKEN is set, because anyone
# who can reach the API can write to the demo team and spend your Gemini quota
# through it. Set `on` to keep it on a networked deploy anyway.
# TRAILHEAD_DEMO_TEAM=

# How /coach prompts join the team library. Both modes require the exact
# average >= 7.0, no dimension < 5, and an independent re-score that agrees.
Expand Down
53 changes: 52 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ jobs:
run: npm audit --audit-level=high

docker:
name: docker image builds
name: docker image + compose smoke test
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
Expand All @@ -97,6 +97,31 @@ jobs:
run: GEMINI_API_KEY=ci-placeholder docker compose config --quiet
- name: build api image
run: docker build -f apps/api/Dockerfile -t trailhead-api:ci .
# Run the quick start the way a stranger would, with no key (offline
# mode), and check the stack actually works: schema applied, API up,
# registration, a wiki write, a rule-based score, and the demo seed.
- name: compose smoke test (offline mode, demo seed)
env:
TRAILHEAD_LLM: offline
run: |
set -euo pipefail
docker compose -p ci up -d --build --wait --wait-timeout 180
docker compose -p ci --profile demo run --rm seed
api=http://127.0.0.1:3000
curl -fsS $api/ | jq -e '.status == "ok" and .llm == "offline"'
secret=$(curl -fsS -X POST $api/teams -H 'content-type: application/json' \
-d '{"team_id":"team_ci_smoke","name":"ci"}' | jq -er .secret)
curl -fsS -X POST $api/wiki/propose -H "X-Team-Token: $secret" -H 'content-type: application/json' \
-d '{"node_path":"src/","insight":"Errors return JSON {error, detail}.","user_id":"ci"}' | jq -e '.action == "created"'
curl -fsS -X POST $api/score -H "X-Team-Token: $secret" -H 'content-type: application/json' \
-d '{"prompt":"fix it","user_id":"ci"}' | jq -e '.scorer == "heuristic" and .overall <= 2'
curl -fsS $api/team/metrics -H 'X-Team-Token: trailhead_demo_acme_2026' | jq -e '.total_obs >= 700 and .active_users == 3'
- name: compose logs
if: failure()
run: docker compose -p ci --profile demo logs
- name: compose down
if: always()
run: docker compose -p ci --profile demo down -v

integration:
name: api integration tests (Postgres)
Expand Down Expand Up @@ -131,3 +156,29 @@ jobs:
env:
TRAILHEAD_IT_DATABASE_URL: postgresql://trailhead:trailhead@localhost:5432/trailhead_it
run: npm --workspace=apps/api run test:integration

package:
name: package extensions
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
- name: install
run: npm ci
# Production builds of both extensions, packed the way a release ships
# them (release.yml attaches the same files to a GitHub release).
- name: browser extension (.zip)
run: npm --workspace=@trailhead/browser-ext run package
- name: VS Code extension (.vsix)
run: npm --workspace=apps/vscode-ext run package
- uses: actions/upload-artifact@v4
with:
name: extensions
path: |
apps/browser-ext/learnloop-browser-ext-*.zip
apps/vscode-ext/*.vsix
if-no-files-found: error
55 changes: 55 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: release

# Push a tag vX.Y.Z (from main) and this attaches both extension packages to
# the GitHub release for that tag, creating it as a draft if it doesn't exist
# yet. Publishing the release, and any store listing, stays a human step.
#
# The tag must match the version in apps/browser-ext/manifest.json and
# apps/vscode-ext/package.json, or the job fails before building anything.

on:
push:
tags: ['v*']

permissions:
contents: write

jobs:
extensions:
name: package and attach extensions
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: versions match the tag
run: |
want="${GITHUB_REF_NAME#v}"
for f in apps/browser-ext/manifest.json apps/browser-ext/package.json apps/vscode-ext/package.json; do
have=$(jq -r .version "$f")
if [ "$have" != "$want" ]; then
echo "::error file=$f::version is $have but the tag is $GITHUB_REF_NAME"
exit 1
fi
done
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
- name: install
run: npm ci
- name: test
run: npm test
- name: package
run: |
npm --workspace=@trailhead/browser-ext run package
npm --workspace=apps/vscode-ext run package
- name: attach to the release
env:
GH_TOKEN: ${{ github.token }}
run: |
if ! gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1; then
gh release create "$GITHUB_REF_NAME" --draft --verify-tag --generate-notes --title "LearnLoop $GITHUB_REF_NAME"
fi
gh release upload "$GITHUB_REF_NAME" --clobber \
apps/browser-ext/learnloop-browser-ext-*.zip \
apps/vscode-ext/*.vsix
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ apps/browser-ext/build/

# VS Code extension build artifacts
apps/vscode-ext/*.vsix
apps/browser-ext/*.zip
apps/api/eval/results/
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Changelog

## 0.1.0 — unreleased (to be tagged `v0.1.0`)

The first versioned release: the browser and VS Code extensions are packaged
and attached to the GitHub release. Changes since the PoliHack build, by pull
request:

- **#19 (2026-08-17) Audit fixes.** Revived the API (every LLM call was in an
infinite recursion), closed the `GET /teams` token leak, made self-hosting
the default with `docker compose`, added wiki export.
- **#20 (2026-09-30) Hardening.** Server-minted, hashed team secrets;
tenant-isolation integration tests; the extension calls the API from its
service worker (Chrome Local Network Access); rate limits; fencing of
team-written text; the scoring eval harness.
- **#21 Landing page.** The waitlist, which only wrote to the visitor's own
`localStorage`, is replaced by links to the code, the self-hosting guide and
the demo; feature claims match the code.
- **#22 API structure.** `app.ts` split into route modules; one Gemini client
for every call; dead learning-extraction code removed.
- **#23 Keyless demo.** `TRAILHEAD_LLM=offline` runs the stack with no model,
using a rule-based scorer that is measured on the golden set and a held-out
set in CI; seeded demo data (`docker compose --profile demo up`); a switch
for the public demo team; a `docker compose` smoke test in CI.
- **#24 Distribution.** Extension icons and packages, a `package` CI job and a
tag-triggered release workflow; MCP configs without machine-specific secret
paths; a coaching directive that adds to the agent's tools instead of
overriding them.
- **#25 Try it and docs.** The landing page scores prompts in the browser; the
README gains an architecture diagram and loses its stale claims.
Loading
Loading