No description
  • Python 70.9%
  • TypeScript 28%
  • JavaScript 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
crowbot 36393371f1 Harden bundle against host's boundary-less import regex (#14) (#15)
Fixes #14.

## Root cause
Hermes Desktop's runtime loader (`apps/desktop/src/contrib/runtime-loader.ts`) scans plugin bundles with
```
/(from\s*|import\s*\(\s*|import\s+)(['"])([^'"]+)\2/g
```
— no word boundary before `from`. Our bundle inlines Temml (LaTeX, #7), whose symbol table contains `"\\mapsfrom"` and `"\\xtofrom"`. In the minified output `…mapsfrom",!0);o(i,f,"…` is read as an import of `,!0);o(i,f,` — the garbage specifiers in the error. Our own lint uses `\bfrom`, so it never caught it.

## Fix
- **`desktop/scripts/harden-bundle.mjs`** (new): post-build pass that rewrites `from`/`import` glued to an identifier char and followed by a quote (`mapsfrom"` → `mapsfro\x6d"`). That can only occur inside a string/regex/template literal, where the hex escape is semantically identical; real import statements are untouched.
- **`desktop/esbuild.config.mjs`**: runs the pass after every build (single and `--watch`) before lint.
- **`desktop/scripts/lint-bundle.mjs`** + **`scripts/install.py`**: additionally check with the host's *exact* regex, so a future vendored string cannot regress this. Verified both reject the unhardened bundle.
- **`desktop/test/harden-bundle.test.ts`**: host regex misreads the Temml snippet; hardened output is invisible to it and evaluates to identical strings; real imports untouched; idempotent.
- Docs: `AGENTS.md`, `desktop/README.md`, `design.md`.

## Verification
- `npm run build` → lint ok; host regex over `dist/plugin.js` now yields only `@hermes/plugin-sdk, react, react/jsx-runtime`
- `npm test` 25/25, `npm run typecheck` clean
- After merging, re-run `scripts/install.py` to refresh `~/.hermes/plugins/hermes-learning/desktop/plugin.js`.

The proper upstream fix is `\bfrom` / `\bimport` in hermes-agent's `importSpecifierRe`; this keeps the plugin working on current hosts regardless.

Reviewed-on: #15
2026-08-29 21:22:06 +00:00
desktop Harden bundle against host's boundary-less import regex (#14) (#15) 2026-08-29 21:22:06 +00:00
docs Harden bundle against host's boundary-less import regex (#14) (#15) 2026-08-29 21:22:06 +00:00
plugin Answering a check advances the lesson (#9) (#13) 2026-08-29 20:48:25 +00:00
scripts Harden bundle against host's boundary-less import regex (#14) (#15) 2026-08-29 21:22:06 +00:00
tests Answering a check advances the lesson (#9) (#13) 2026-08-29 20:48:25 +00:00
.gitignore chore: gitignore uv.lock 2026-08-28 22:02:03 -07:00
AGENTS.md Harden bundle against host's boundary-less import regex (#14) (#15) 2026-08-29 21:22:06 +00:00
pyproject.toml feat/pedagogy-plugin (#4) 2026-08-29 02:21:05 +00:00
README.md docs: improve readme and add AGENTS.md 2026-08-29 00:07:38 -07:00

Hermes Learning

A Hermes plugin for adaptive, mastery-based learning. Describe what you want to learn in plain language — a named course ("Calc II") or a goal ("I want to understand Maxwell's equations") — and the plugin places you with an adaptive diagnostic, generates a prerequisite- aware syllabus, and teaches you through a conversational tutor until every module is mastered.

What it does

  1. Intake — you submit a free-form learning request; the plugin interprets it (subject scope, target outcome) and shows you that interpretation for confirmation before anything is created.
  2. Adaptive diagnostic — a placement quiz, not a grade. One multiple-choice question at a time, with an explicit "I don't know" option, no correctness feedback, and early/forced stopping once there is enough evidence.
  3. Syllabus — the plugin derives a directed acyclic graph of modules from your demonstrated knowledge to your target outcome, and validates it (no cycles, no dangling prerequisites) before publishing it.
  4. Instruction — you enter any module on the knowledge frontier (its prerequisites are all passed) and learn through a conversational tutor grounded in researched sources, with low-stakes instructional checks along the way.
  5. Homework & mastery — each module ends in a feedback-delayed multiple-choice assessment. Passing it at your mastery threshold unlocks dependent modules. A course-level final exam completes the course.
  6. Review — learner-initiated review sessions keep passed material fresh; wrong answers flag degraded knowledge so you know what to revisit, but can never revoke mastery you have already earned.

Multiple courses progress independently; all state persists across sessions under Hermes' plugin-data directory and survives disable/re-enable cycles.

Design tenets

  • The model generates; it never grades. Every assessment is multiple-choice and graded by local comparison against a stored answer key. The LLM writes questions, keys, and explanations before an attempt starts; it is never in the mastery critical path.
  • Validate before persisting. Every structured model output (diagnostic question, syllabus, assignment, grading record) is validated against a declared JSON Schema before it touches course state. A validation failure leaves the previously valid state untouched and returns a retryable error.
  • Fallibility is inspectable, not hidden. Generated curricula are treated as fallible. Every accepted state change is an append-only event carrying its full validated payload, so any decision can be replayed and audited.
  • The backend owns truth; the UI renders it. The Python half owns course state, transactions, and all model calls. The desktop half posts intents and renders read-model DTOs — answer keys and diagnostic correctness are structurally unreachable from the UI while an attempt is active.
  • Never block on inference. Model-bound requests return an operation immediately; the client polls it. No HTTP request waits on the model.
  • Host-owned AI access. Model calls and web research go through Hermes' plugin LLM lane and tool layer with host-managed credentials; the plugin never reads provider keys.
  • Accessible by construction. The desktop half is keyboard-operable end to end, names every control accessibly, never relies on colour alone, and ships automated axe-core checks plus a manual keyboard walkthrough.

Installation

The plugin installs into your Hermes home (HERMES_HOME, typically ~/.hermes) as plugins/hermes-learning. The installer never modifies Hermes core, never installs anything into the Hermes runtime venv, and never touches learner data.

Prerequisites

  • A running Hermes installation (Python 3.11).
  • node and npm on PATH (only needed to build the desktop half).
  • jsonschema importable from Hermes' own venv — it ships with Hermes; the installer only reads the venv to confirm.

Install

# Verify host prerequisites (read-only)
python3 scripts/install.py --check

# Build the desktop bundle, lint it, and install atomically
python3 scripts/install.py

# ...and enable it in one step
python3 scripts/install.py --enable

Useful flags:

Flag Effect
--enable run hermes plugins enable hermes-learning after installing
--skip-build reuse the existing desktop/dist/plugin.js bundle
--uninstall remove the plugin directory (learner data is kept)

First run

  1. Restart the Hermes dashboard so the Python backend is mounted.
  2. In Hermes Desktop, open Settings → Plugins and toggle the Learning desktop half on (it ships opt-in), then use Cmd/Ctrl-K → "Reload desktop plugins".
  3. Open the Learning page in the sidebar and click New course.
  4. Describe what you want to learn, confirm the interpreted scope, and take the diagnostic. When it has enough evidence, generate the syllabus and start learning.

The mastery threshold is a learner-level setting on the Learning page, frozen onto each attempt when it starts.

Development

The repo is the source tree; scripts/install.py builds an installed tree into ~/.hermes/plugins/hermes-learning/. Nothing under ~/.hermes is edited in place.

hermes-learning/
├── plugin/                  # Python half (loaded by Hermes as a directory plugin)
│   ├── plugin.yaml          #   manifest: name, python_dependencies
│   ├── runtime.py           #   register(ctx) process-global bridge
│   ├── storage.py / events.py / projections.py / validators.py
│   ├── agents/              #   intake, diagnostics, syllabus, research, tutor, assignments, review
│   └── dashboard/           #   plugin_api.py + one APIRouter per feature (routes/)
├── desktop/                 # UI half source, bundled to a single plain-ESM file
│   ├── src/                 #   index.tsx, api.ts, nav.ts, pages/, components/, dag/
│   └── dist/plugin.js       #   build output Hermes Desktop loads
├── scripts/install.py       # build + lint + stage + atomic install
├── tests/                   # Python tests (not installed)
└── docs/
    ├── specs/pedagogy-plugin/   # requirements / design / tasks
    └── a11y-keyboard-walkthrough.md

Python backend

Tests run in a project-local virtualenv (Python 3.11, to match Hermes). Do not install anything into the Hermes venv (~/.hermes/hermes-agent/venv): hermes update can recreate it, and the plugin's only runtime dependency, jsonschema, already ships with Hermes.

uv venv --python 3.11 .venv
uv pip install --python .venv/bin/python -e '.[dev]'
.venv/bin/python -m pytest tests/ -q

On Windows use .venv\Scripts\python in place of .venv/bin/python.

Test-only: HERMES_AGENT_DIR overrides where tests/conftest.py looks for the Hermes source tree (default ~/.hermes/hermes-agent) before falling back to a hermes_constants stub; the plugin itself never reads it.

Desktop half

npm ci && npm run build bundles src/index.tsx into the single plain-ESM dist/plugin.js that Hermes Desktop loads. Runtime JS dependencies are zero; esbuild, TypeScript, and @types/react are dev-only.

cd desktop
npm ci
npm run build       # bundle + lint (fails on: >1 file, foreign imports, raw JSX, hard-coded colours)
npm run watch       # rebuild on save
npm run typecheck   # tsc --noEmit
npm test            # jsdom + axe-core accessibility spot checks against an SDK double

Every build runs scripts/lint-bundle.mjs, which fails the build unless the output is exactly one file, imports only @hermes/plugin-sdk / react, uses jsx() calls (no raw JSX), and contains no hard-coded colours (theme variables only).

Live-editing against a real Hermes install

python3 scripts/install.py --dev

This symlinks the Python half into the plugin directory and points desktop/plugin.js at desktop/dist/plugin.js while esbuild runs in watch mode. Development only: a symlinked tree does not match the install manifest's hashes and depends on this checkout staying put. Re-run a regular install before shipping.

Contributions

Read docs/specs/pedagogy-plugin/requirements.md (behaviour, REQ/NFR ids) and design.md (architecture decisions) before changing behaviour; acceptance criteria there are the contract the tests encode.