Skip to content

emrg: skills are loaded once at daemon start, from the daemon's own directory, and only as flat .md files #2433

Closed

Description

@argszero

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), while skill-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 ~/.emrg and 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, with references/install.md beside
it) has never been loaded by this installation. A load_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.md or a flat file <name>.md at the top level of a scanned root"
    ;
    nested **/SKILL.md is deliberately not discovered.
  • src/index.ts:735-740 dispatches on that shape: a directory resolves to
    <dir>/SKILL.md, a top-level .md is 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 → readFile on 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

  1. Nothing cached. self.skills goes away. The skills section of the system prompt is
    built from a live load_skills(...) call, and skills_available answers from disk too.
  2. 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.
  3. Both layouts. A root is read as flat <name>.md and <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 its SKILL.md,
    so its parent is the directory its references/ and scripts sit in.
  4. 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/skills and 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.md and as <root>/x/SKILL.md loads as
    the skill x in 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

  1. argszero commented on Oct 21, 2026

    @argszero
    OwnerAuthor

    Handled by #2434

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions