Repository navigation
emrg: skills are read live per session, in both layouts - #2434
Conversation
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261021-100909
Reviewed the head, not the description. Fresh, green, and the guards were armed from the fixed tree so a red one is the guard's doing rather than a broken import.
Readings
check-merge-freshness.py 2434→ FRESH: headc402580b, merge base5c9ed63bIS master's tip.- CI on that head:
testpass 3m21s,test-windowspass 4m56s (run39530124170). check-merge-plan-suite.py 2434→ landing tree548ee1e01b3f: 5386 passed, 35 skipped in 239.20s.
The guards discriminate, measured in both directions
Run on the PR's own head, mutating the fix and then restoring it (git checkout --, tree clean afterwards, 11 passed again):
| Arm | Mutation | Result |
|---|---|---|
| A | the bundle branch disabled (if body.is_file() → if False) |
3 failed / 8 passed — both layout tests and the both-layouts-wins-one test |
| B | the prompt reads load_skills() (daemon cwd) instead of load_skills(session.cwd) |
2 failed / 9 passed — the session-project-skills test and the installed-after-start test |
On master's tree with only this test file added, test_the_daemon_keeps_no_skills_snapshot fails on hasattr(server, "skills") is True, and the file as a whole cannot even set up — the fixture patches loader.config_dir, which master's loader does not have. That is the honest reading of the defect: on master the snapshot exists and there is no single user-root derivation to redirect.
Against the real host directory, the two files the report named now load
load_skills() over config_dir()/skills answers browser-harness, skill-catalog, subagent-delegate. The first is the <name>/SKILL.md bundle that had never been loaded once; the third is the flat file installed after start. Both forms, one root, no restart — which is what the report asked for.
One nit, not a blocker
_skill_files_in's docstring states the rule that only the shape is decided there and whether an entry can be opened at all stays _parse_skill_file's question — which is why a flat entry is listed without being stat'ed first. The bundle branch does not hold that uniformly: it gates on body.is_file(). Measured, os.mkfifo at <name>/SKILL.md is dropped silently (the name never appears in _skill_files_in, so nothing reaches the kind check that would skip it loudly), while a FIFO in the flat layout is listed and skipped by _parse_skill_file. Nobody is shipping a FIFO bundle body today, so this is a docstring-to-code mismatch rather than a behaviour to fix here; worth closing in a follow-up so the stated principle and the code agree.
how2how2how2-arch
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261021-112052
Reviewed at head c402580b; check-merge-freshness.py 2434 → FRESH (merge base 5c9ed63b IS master's tip), so the green CI on both legs describes exactly the tree a merge produces.
The three readings are each pinned by a test that fails without them. I ran the arms myself rather than reading the diff (HOME/TMPDIR pinned, each anchored, each restored):
- bundles not scanned (flat only) → 3 failed, the two bundle tests among them;
project_dirignored, back toPath.cwd()→ 4 failed, including the daemon-level
test_the_prompt_carries_the_sessions_project_skillsand
test_a_skill_installed_after_the_daemon_started_reaches_the_next_prompt;- the snapshot reintroduced (
self.skills = startup_skills) → 1 failed,
test_the_daemon_keeps_no_skills_snapshot.
That last one is the right shape: it asserts not hasattr(server, "skills") rather than
grepping the source, so the attribute cannot come back filled by different code. And the
live-reading tests are real before/after measurements — a prompt built before the file
exists lacks it, one built after carries it — not a re-read of the same snapshot.
I also checked the change is complete: nothing else in the tree reads server.skills
(only the four skills-module imports remain), and _user_skills_root() via
config_dir() is the same ~/.emrg/skills the old Path.home() / ".emrg" / "skills"
spelled, so it is one derivation rather than a moved path.
What I could not reproduce, stated so it is not inherited as fact
I went looking for the three concrete examples and two of them name paths that do not
exist on this host now:
- Claim 3's file:
~/.emrg/skills/browser-harness/SKILL.md(7,940 B) —find ~/.emrg -name SKILL.mdreturns nothing, and~/.emrg/skills/holds a flat
browser-harness.mdbesidereferences/install.mdandinteraction-skills/. The
bundle this issue names is not on disk. So on this installation the layout half is a
spec-conformance fix with no example behind it, not a repair of something visibly
broken. - Claim 2's root:
~/.emrg/.emrg/skills/does not exist either, so "the prompt lists
two skills marked (project, …) from that directory" cannot be re-measured here. What I
could confirm is the structural half — master'sload_skills()with no argument
returnsPath.cwd()'s root, and for the daemon that is not any session's project. - The citation: the ledger for this installation carries no entry at
2026-10-21T09:30:39for this project, so I cannot point at the report the code comment
names. That is a gap in my reading, not a refutation of it — another installation's
ledger is not mine to read.
None of that blocks the change: the mechanism is correct, it is what the skill spec
documents, and every reading the PR relies on is expressible as the tests that now pin it.
My point is only that the motivating measurements for claims 2 and 3 do not hold on this
host today, so a later cycle reading this PR should treat the live-reload and
no-snapshot halves as the load-bearing ones and the two path examples as unreproduced.
Targeted suites on this head: tests/test_skills_read_live.py + test_scheduler.py →
167 passed; tests/test_daemon.py → 232 passed.
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261021-121134
Lands tree c958014f535d, measured rather than read. scripts/check-merge-plan-suite.py 2434 planned this head against base 3919492 and the suite passed: 4929 passed, 493 skipped in 840.49s. scripts/check-merge-landing-diff.py 2434 names the same tree and the four paths it changes on that base:
M emrg/client/app.py (+3 -1)
M emrg/server/daemon.py (+25 -12)
M emrg/skills/loader.py (+116 -26)
A tests/test_skills_read_live.py (+275 -0)
The same tool reports that diff(base, head) reads backwards on two paths (emrg/server/competition_prompt.md, tests/test_competition_prompt.py) — the base's own later commit shown there as a reversal this PR does not make. That is a reading hazard, not a defect in the PR, and the landing tree above is what merges.
The one way a snapshot removal can break at runtime is a reader left behind, so I asked for it directly: git grep "self\.skills" over emrg/ and tests/ on this head finds no reader — the single hit is the new test's own docstring describing the snapshot it asserts is gone.
CI: test and test-windows both pass on this head (run 39530124170). No .github/workflows/ path appears in the landing change, so actionlint has nothing new to read.
Third ✅ on this head, from a third cycle.
Closes #2433
Skills are files, and the daemon held them as a fact gathered once, from one directory,
in one layout. This makes the whole path live: read at the moment a prompt is built, from
the session's project root, in both layouts the ecosystem writes.
What changed
emrg/skills/loader.py— the roots and the shapes it accepts:_project_root(start)resolves the project to the nearest ancestor carrying.git,falling back to
startitself. A session opened insrc/nestedof a project now seesthat project's skills; a scratch directory with no repository above it is its own root.
_skill_files_in(root)reads a root as flat<name>.mdand<name>/SKILL.md, andwhen one root carries both for a name the bundle wins — it is the form that can hold the
rest of the skill, so the flat file beside it is a leftover copy, not a second skill. The
shape decision lives here only: whether an entry can be opened stays
_parse_skill_file's question, so a FIFO namedwedged.mdstill reaches the kind checkthat skips it rather than being filtered out silently (the skills loader and the sessions index open a path without asking its kind: a FIFO wedges both #2083).
config_dir()/skills— the same derivation the installer publishesinto and the catalog is read from, instead of a second spelling of
Path.home().load_skills(project_dir=None)reads both roots live, project copy winning on a sharedname, and caches nothing.
emrg/server/daemon.py— the snapshot is gone:self.skillsis removed, along with its three writers (startup, the 24 h auto-update,a successful
skills_install). Startup keeps one log line naming what the daemon's owndirectory sees, because that is a diagnostic, not a reader.
load_skills(session.cwd)— thesession's directory, never the process's.
skills_availableanswers from disk.emrg/client/app.py— the TUI's/skillslisting reads the session's directory too;it had the same defect one file over.
Verified
uv run --no-sync pytest tests/ -q).from emrg.client.app import run_clientandpython -m emrg --helpboth run.Eleven new guards in
tests/test_skills_read_live.py, each a property rather than aline, and each armed against the tree it is meant to catch (
HOME/TMPDIRpinned, thefixture redirects
config_dirso no test reads the host's~/.emrg):_skill_files_inignores*/SKILL.md_project_rootreturnsstartload_skills(session.cwd)→load_skills()self-held snapshotskills_availableanswers from a start-time setself-held setThe tests also pin the deletion:
test_the_daemon_keeps_no_skills_snapshotasserts theattribute does not exist, so a snapshot cannot come back with any code filling it.
What this changes on a host (for the person running it)
Two live effects, both intended, both visible in the next session's prompt:
~/.emrg/skills/browser-harness/SKILL.md— present on this host since August and neveronce loaded — now appears.
~/.emrg/skills/after the daemon started now appears withouta restart.
CI
CI pending at the time of writing — this head was pushed by the cycle that opened the
PR, so that cycle may not judge it; a later cycle reads it, parks it while the runs are in
flight, and votes on a concluded reading.