Repository navigation
emrg: skills are loaded once at daemon start, from the daemon's own directory, and only as flat .md files #2433
Description
What is wrong
A skill is a file on disk, but emrg/skills/loader.py and the two places
emrg/server/daemon.py consumes it treat the skill set as something gathered once, from
one directory, in one file layout. Three consequences, each measured on 5c9ed63b.
1. self.skills is a snapshot taken when the daemon starts
EmrgServer keeps the loaded skills in self.skills. It is written in three places —
daemon.py:790 (startup), :1228 (the 24 h auto-update, when it actually updated
something) and :3192 (a successful skills_install) — and read in two — :2447 (the
system prompt) and :3163 (skills_available). No client message reloads it, so a skill
file that appears on disk for any other reason never reaches a prompt:
a skill written to
~/.emrg/skills/at 09:13 does not appear in a prompt built at
09:31 (grep -c <skill-name> system.md→ 0), whileskill-catalog— same
directory, same loader, present since before startup — does.
2. The project directory is the daemon's working directory, not the session's
load_skills() takes an optional project_dir and falls back to Path.cwd()
(loader.py:163), then reads <project_dir>/.emrg/skills (:177). Path.cwd() is the
process's directory, which for the daemon is whatever it was started in.
with the daemon started in
~/.emrgand a session whose working directory is
/Users/argszero/.emrg/evolution/emrg, the prompt lists two skills marked
(project, …)from~/.emrg/.emrg/skills/— neither belongs to the session's project,
and no other project's.emrg/skills/is ever read by any session.
3. Only flat <name>.md is read; <name>/SKILL.md never is
Both roots are walked with a one-level sorted(dir.glob("*.md")) (loader.py:170,
:179): no rglob, no */SKILL.md. The repository contains no occurrence of SKILL.md
outside .emrg/.
~/.emrg/skills/browser-harness/SKILL.md(7,940 B, withreferences/install.mdbeside
it) has never been loaded by this installation. Aload_skills()over a directory
holding it returns only the flat files.
Evidence that this is not how it is meant to work
The reference implementation for a filesystem skill pack, deepseek-harness
(687f282e86), documents and implements the arrangement these three defects break:
packages/skill/skill-filesystem/README.md— "A skill is either a directory bundle
<name>/SKILL.mdor a flat file<name>.mdat the top level of a scanned root";
nested**/SKILL.mdis deliberately not discovered.src/index.ts:735-740dispatches on that shape: a directory resolves to
<dir>/SKILL.md, a top-level.mdis the skill itself.tests/skill-filesystem.spec.ts:249
pins the flat form, so both are intended.src/index.ts:244-257— multiple roots with ranks, and the project root is the nearest
ancestor carrying.git, not the process's cwd.- "every load re-reads the current file, so editing a skill body needs no versioning or
cache invalidation" —get()→parseSkillFile→readFileon every call
(src/index.ts:211,:861).
This project's own installer also publishes flat files
(emrg/skills/installer.py:175, target = dest / f"{name}.md"), and the shipped catalog
is a flat skill — so the flat form cannot be dropped either. Both forms have live writers.
What a fix must satisfy
- Nothing cached.
self.skillsgoes away. The skills section of the system prompt is
built from a liveload_skills(...)call, andskills_availableanswers from disk too. - Two roots, and the project root is the session's. The user root and the project
root are both read; the project root is resolved from the session's working
directory — nearest ancestor carrying.git, falling back to that directory — never
from the process's cwd. A shared name keeps today's semantics: the project copy wins. - Both layouts. A root is read as flat
<name>.mdand<name>/SKILL.md; one
name present in both forms loads once, and the bundle is the copy kept (it is the form
that can hold a skill's other files). A bundled skill's recorded path is itsSKILL.md,
so its parent is the directory itsreferences/and scripts sit in. - No cache anywhere, so no invalidation step. A skill written, edited or deleted on
disk reaches the next prompt without a restart.
Acceptance
- (a) A skill file installed or edited after the daemon started reaches the next
prompt, with no daemon restart, and an edited body is what the next round reads. - (b) A session working in project A lists the skills under
A/.emrg/skillsand not the
ones under the directory the daemon was started in, including when the session's cwd is
a subdirectory of A. - (c) The same skill content placed as
<root>/x.mdand as<root>/x/SKILL.mdloads as
the skillxin both cases, and loads once when both are present.
Cost
Reading live on every prompt build is not a caching regression: load_skills() over this
installation's roots measured a 0.074 ms median (20 runs, min 0.071 / max 0.116), and
two consecutive build_skills_context calls over an unchanged tree are byte-identical
(957 B) — the output is a deterministic function of the files, so the prompt prefix does
not move and the provider's prompt cache still matches.
Activity
argszero commentedon Oct 21, 2026
Handled by #2434