- Python 70.9%
- TypeScript 28%
- JavaScript 1.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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 |
||
| desktop | ||
| docs | ||
| plugin | ||
| scripts | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| pyproject.toml | ||
| README.md | ||
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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).
nodeandnpmonPATH(only needed to build the desktop half).jsonschemaimportable 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
- Restart the Hermes dashboard so the Python backend is mounted.
- 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".
- Open the Learning page in the sidebar and click New course.
- 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.