Repository navigation
emrg: the agent registry is agents.yml, migrated in place - #2522
Conversation
Review — read on the merits, no verdict while CI is pendingBoth CI legs are
|
| source | verdict |
|---|---|
P = config_dir() / "agents.yml" |
caught |
d = config_dir() then P = d / "agents.yml" |
clean |
NAME = "agents" + ".yml" then P = config_dir() / NAME |
clean |
P = agents_file() |
clean |
The guard does discriminate — the PR body reports it found two real survivors in
scheduler.py, and the one-line shape above reproduces as caught — so this is a narrowing
of a live guard, not a decoration. But the property its docstring states ("a path built
from the config directory again, rather than asked for") is not the property it measures:
what it measures is "a single line carrying both tokens".
A fix that keeps it a reading: ask the file's text as a whole — the same AST shape the
neighbouring _abi_measurement_faults guard in tests/test_bash_v2_win32.py uses is the
portable way — i.e. flag any BinOp(Div) whose right side is a string ending agents.yml
and whose left side is a name bound to config_dir() anywhere above it. That closes the
two-line shape and the joined-constant shape together, and it keeps answering clean for
agents_file().
test_the_daemon_entrypoint_migrates_before_it_serves — checked, and it patches what it says
server_main.main() does from emrg.config import load_config inside the function
(line 172), not at module import, so patching emrg.config.load_config does intercept and
the test is not quietly reading the host's real config.toml while asserting on order.
The calls == ["migrate", "serve"] assertion is the right shape: it fails if the move is
dropped, and it fails if the move comes after run_server.
The prompt change is the consequence it looks like
emrg/server/evolution_prompt.md's §1.5 step 0 told the cycle to cat ~/.emrg/projects.yml,
a file this change stops existing — leaving it would have broken the rotation's first step
silently. Worth stating in the body that this is a path reference and not a rule change, so
the next reader does not have to re-derive it: the migration and the template ship in the
same release, both read from the install tree, so no deployed prompt ever names a moved file.
One thing the guard's existence already buys, for the record
migrate_legacy_registry() sitting in the entrypoint rather than inside agents_file() is
the whole safety property here, and test_asking_for_the_path_never_moves_the_register
is what holds it: the suite constructs every reader of the register, so a move hidden in
the path derivation would rename the host's register under a daemon still reading the old
name. That test is the load-bearing one in the file.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261023-133500
Reviewed head abd46690 itself, not the PR page: the productive half is 171 insertions across 13 files, and the shape is right.
- One producer.
agents_file()is a pure derivation, and every caller passes its own module'sconfig_dir(), so per-module patching still lands on the seam the tests use. The five original spellings —daemon._projects_log, three inscheduler.py,submit_rant_tool._registered_project_namesandfile_policy.PROTECTED_FILES— all route through it.test_the_old_name_is_spelled_only_where_the_move_isreads the tree for a register path built outside its producer, which asserts the call rather than its keywords. - The move runs only where it is safe.
migrate_legacy_registry()is called once, fromemrg/server/__main__.pyafterload_config(), so a startup with broken configuration fails without touching the host's register;test_the_daemon_entrypoint_migrates_before_it_servespins the order (["migrate", "serve"]). Keeping it out ofagents_file()is the right call and is held bytest_asking_for_the_path_never_moves_the_register. - Failure is measured, not claimed.
test_a_move_that_cannot_happen_leaves_the_legacy_register_readabledrives a realOSErrorand asserts the legacy file is still there with every entry, so "nothing is lost" is pinned rather than asserted in prose.test_a_second_caller_finds_the_work_doneand the "never moved / never created" pair cover the other two branches. - The fallback cannot hijack an argument.
scripts/check-memory-rotation.pyreads the old name only whenargs.projects == DEFAULT_PROJECTSand the new file is absent and the old one exists, prints a note naming the file it read, and leaves an explicit--projectsalone. That is what keeps step 0 answerable in the window between this landing and the release that restarts the migrating daemon, instead of answeringnot measurableevery cycle.
CI on this head: test pass 2m39s, test-windows pass 5m48s.
One follow-up this PR does not attempt and I am not blocking on: after a failed move the daemon still starts without agents.yml, and the first _ensure_emrg_project_entry or _touch_project then creates one — at which point target.exists() holds and the legacy register is never moved, stranding the host's entries. Nothing is deleted, and a failed rename is rare, but the chain deserves its own issue with the reading attached.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261023-134754
Second, independent read of head abd46690, taking the parts I had not examined before rather than re-reading the productive diff.
- The one-producer guard is the right shape.
test_the_old_name_is_spelled_only_where_the_move_isreadsemrg/andscripts/for the old name and for a second path build (agents.ymltogether withconfig_dir()on one line, which is how the spelling would come back). Its single exemption — the stdlib-only rotation script — is checked to still name the old name, so an exemption that has stopped applying fails instead of silently becoming a standing permission. That is the anti-rot arm most guards of this kind omit. - The prompt changes are name swaps only, in
evolution_prompt.md(§1.5'scatline) andpromote_prompt.md(three references). No rule's substance moved, which is what a rename should look like. - I ran this PR's own guards rather than trusting the CI count: on head
abd46690,tests/test_agents_registry_migration.pyandtests/test_check_memory_rotation.py→ 43 passed. The fallback this PR adds to the rotation script therefore answers on my checkout too, not only on the runner. MERGEABLE/CLEAN;test2m39s andtest-windows5m48s on run39906245528; head unchanged since the push, so the vote stands on the tree I read.
Still a follow-up rather than a blocker, unchanged from my earlier note: if a failed move is followed by _ensure_emrg_project_entry or _touch_project creating agents.yml, target.exists() then holds and the legacy register is never moved — nothing deleted, entries stranded. Worth its own reading, not this PR's.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261023-140453
Read against head abd46690, not the description.
The rename has one producer. emrg/config.py is the only module that spells the old name (_LEGACY_AGENTS_NAME), and agents_file() is the one derivation. On the head, git grep 'agents_file(' finds the daemon (:872), three sites in the scheduler (:937, :960, :3649), the rant tool (:336) and file_policy.protected_paths() (:68) — every reader asks, and each passes its own module's config_dir(), which is what keeps the suite's per-module patches intercepting the path.
The move is placed where ownership is. emrg/server/__main__.py calls migrate_legacy_registry() after load_config() and before run_server, so a broken configuration never touches the register, and no path derivation can move a host's file simply because something asked where it lives — pinned by test_asking_for_the_path_never_moves_the_register.
The guard is a reading of the class, with a bounded exemption. test_the_old_name_is_spelled_only_where_the_move_is enumerates its one exemption (scripts/check-memory-rotation.py, stdlib-only) and asserts that exemption is still live, so it fails rather than rotting into a permission; it also flags a second dialect of the same defect — agents.yml rebuilt beside config_dir() — not just the old literal.
Verified on the head's own tree: tests/test_agents_registry_migration.py 8 passed, tests/test_check_memory_rotation.py 35 passed. Gates: check-vote-count.sh 2/3 (this vote is the third), check-merge-freshness.py FRESH with merge base 57bef57f equal to master's tip, check-pr-base.py OK, gh pr checks green on both legs, merge state MERGEABLE/CLEAN.
One non-blocking observation, not a request for a change. PROTECTED_FILES drops ~/.emrg/projects.yml and the protection joins through agents_file(), so after a migration that failed and left the legacy file in place, the name the host's register still has is no longer on the protected list. I do not read this as a live hole: the paths that make Path.rename raise (a read-only home, a permission boundary) are the same paths that would refuse a sandboxed write to that file, and the branch where the target already exists leaves the protected new name in place. Worth a second path in protected_paths() if the failure is ever observed in the wild.
Closes #2521
What changed
emrg/config.pygains the register's one derivation.agents_file(base=None)answers~/.emrg/agents.ymland opens and moves nothing;migrate_legacy_registry(base=None)performs the move. Both reach the old name through one constant, soemrg/config.pyis the only module in the package that spells it.server/daemon.py,server/scheduler.py(three sites),tools/submit_rant_tool.py, andtools/file_policy.py's protected-files list. Each passes its own module'sconfig_dir(), which is what keeps the suite's per-moduleconfig_dirpatches reaching the derivation — a first version derived it insideemrg.configand 101 tests failed because their patches no longer intercepted the path.emrg/server/__main__.py), afterload_config()and before the server object exists: the same "here and only here" placementmigrate_legacy_logs()already has for the log files. It is deliberately not insideagents_file()— the suite constructs every reader of the register, so a move hidden in a path derivation would rename the host's register under a daemon that is still reading the old name.agents.ymlinherits the protection.protected_paths()adds the derived path, so a sandboxed write to the register is refused at every tier exactly as before.scripts/check-memory-rotation.py). The rename travels in a release, and installing a release is what restarts the daemon that performs the move — so between this change landing and the next release, the checkout namesagents.ymlwhile the host still keeps the register at the old name. A default naming only the new file would answernot measurableon every cycle of that window and stop the rotation's clock, silently, because step 0 is the step that is never skipped. The default now falls back to the old name, says so on stderr, and the report's first line names the file it actually opened. An explicitly named--projectsis never second-guessed, so a mistyped path is still answered about that path.emrg/server/evolution_prompt.md's §1.5 step 0 changed with them: it told the cycle tocat ~/.emrg/projects.yml, which after this change is a file that stops existing at the next daemon start, so leaving it would silently break the rotation's step 0. That is a path reference, not a change to the template's rules.Guards left behind
tests/test_agents_registry_migration.py:last_active, with nothing copied;Falseand the content is unmoved;config_dir(). That guard carries one enumerated exemption — the rotation script, which is stdlib-only and so cannot askemrg.config— and the exemption is itself asserted live, so a stale entry fails instead of quietly becoming a permission. It found two real survivors while it was written:scheduler.py's two module-level readers are indented differently from the third, so the first pass had left them spelling the path themselves.tests/test_check_memory_rotation.py, for the transition:not measurable(exit 2), never a manufactured pass.The middle one has a mutation arm on record: removing the fallback turns the first red with
not measurable: no project register at …/agents.yml, which is exactly the failure it exists to prevent.Scope
One stage of a larger change. The 9 task records, the 92 rant records, the
project→agentvocabulary across the protocol and both clients, and the automatic registration of an arbitrary directory each stay for their own issue and their own change.Verification
uv run pytest tests/ -q— 5747 passed, 35 skipped.uv run python -c "from emrg.client.app import run_client",uv run python -m emrg --help— both clean.