Repository navigation
feat: a registry row refuses or replaces each identity hint by writing the list - #204
Merged
Merged
Conversation
…g the list A row's word on aliases, territory, addresses and rooms is now its presence: a list it writes is final, `[]` included, and only a row without the key adopts what the checkout's manifest hints. The row gains an `addresses` field, refused by name unless it is a list of non-empty strings. `fallback_sources` keeps `[]` as absence, because no checkout can hint a source, and forest repositories stay the type layout's (§FS-008-attribution.1.1). DF-006 records what was decided, rejected and deferred, and the deferred bullets of DF-003 and DF-005 point at it. E2E-057 runs one site-level mail source against a checkout that hints all four lists, under a row silent on them, one that writes `[]` for each, and one that writes its own. Today the second still places Alice's mail and the `acme` mail under the project, the third places Alice's mail and leaves Bob's waiting, and a row whose `addresses` is `[""]` loads.
…g the list A row's aliases, territory, addresses and rooms are now read by presence: a list the row writes is final, `[]` included, and only a row without the key adopts what the checkout's manifest hints (§FS-008-attribution.1.1). Before, an empty `aliases` or `territory` adopted the hint as though the row had said nothing, and addresses came only from the manifest, so a row could neither replace nor refuse a claimed address. The row gains an `addresses` field, which the registry schema declares as a list of non-empty strings and refuses by name otherwise, as it does `rooms`. The forest's repositories come from the project type's layout alone and a manifest's `identity.repos` is never adopted, which on a registry that loads was already so. The registry schema takes the `$id` `.../schemas/registry/v2`, so the release lists the new meaning of `[]` under its compatibility notices (§FS-006-project-interface.11).
§AR-003-attribution.2 compiles aliases, territory, addresses and rooms by one presence rule, and rooms differ only in matching by exact equality. The manual's §4.2.1 and §4.2.2 and the registry's "Identity and territory" say that `[]` refuses each hint, describe the row's `addresses`, and stop promising `identity.name`, `identity.ticket_patterns` and `identity.repos` as adopted hints, since identity reads none of them.
The registry schema had no `$id` and now takes `.../registry/v2`. That addition is what carries the compatibility notice for the new meaning of an empty identity list (§FS-006-project-interface.11), so pin that the release's schema diff notices it (§FS-002-release.1.4).
vjovanov
marked this pull request as ready for review
October 8, 2026 05:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #189
A project's checkout hints four identity lists in its
ephor.json, and anyone who can push to that repository can edit them:{ "identity": { "aliases": ["the widget"], "territory": ["acme"], "addresses": ["alice@example.org"], "rooms": ["whatsapp/acme#1@g.us"] } }Three registry rows point at that checkout. One says nothing, one writes
[]for every list, and one writes its own lists:{ "id": "silent", "root": "…" }, { "id": "says-none", "root": "…", "aliases": [], "territory": [], "addresses": [], "rooms": [] }, { "id": "says-its-own", "root": "…", "aliases": ["gizmo"], "territory": ["acme/plugin"], "addresses": ["bob@example.org"], "rooms": ["whatsapp/acme#2@g.us"] }On
main, onlyroomsfollows the row. An emptyaliasesorterritoryis read as silence, and nothing reads a row'saddresses, soalice@example.orgis the project's address whatever the row says. This is whatPlacement::identity()compiles, as the issue measured it:With this change, each row gets exactly what it wrote, and only the silent row adopts the hints. The unit test
a_row_has_the_last_word_on_every_hinted_list_by_writing_itloads these three rows over that manifest and asserts:Where the mail lands follows from that. The end-to-end case E2E-057 refreshes a site-level mail source over such a checkout and reads where each of eight mails is placed. Each mail carries exactly one signal: Alice or Bob as author, a link into
acme/toolsoracme/plugin, the wordwidgetorgizmo, or room #1 or #2. Its hinted alias iswidget, because resemblance matches whole words. Before the fix, on the spec commit, two of the rows placed mail the row had refused:Now
says-noneleaves all eight mails unattributed. Undersays-its-own, Bob's mail is the project's by reference and Alice's waits in the unattributed bucket. The silent row places what it placed before.A row whose
addressesis not a list of addresses used to load without a word, because the registry schema had noaddressesproperty and let row keys it did not know through. Now it is refused by name:What changed
A list the row writes is final,
[]included. For aliases, territory, addresses and rooms, the row's word is whether it writes the key, never whether the list is empty. A row that writes a list gets exactly that list, and nothing the checkout hints for it is added.[]refuses the hint. Only a row without the key adopts the manifest's hint. The rule holds one list at a time, so a row that refuses one hint keeps the manifest's other hints and its checks, gate and actions. A list present on the row wins at everymanifest_trustlevel, because trust decides only whether there is a hint at all. This is the ruleroomsalready followed, now applied to every list a checkout can hint. (§FS-008-attribution.1.1)In
src/branches.rs,Placement'saliasesandterritorybecomeOption<Vec<String>>, asroomsalready was, and the newaddressesjoins them. All four are loaded through one helper,listed().Placement::identity()compiles the four through oneadopt, which takes the row's list when the key is present and the manifest's hint only when it is absent. The emptiness rule (if own.is_empty() { hinted } else { own }) and the bareaddresseshint are gone.The row gains an
addressesfield. A row's own addresses are matched as written and place a mail at the strength of a reference, exactly as hinted ones do. The registry schema declares the field as a list of non-empty strings, with[]valid. So"alice@example.org",[""]and[7]are refused at/projects/<i>/addresses, as a malformedroomsis. Read as silence, a malformed list would let the checkout's hint stand in for what the row tried to say. (§FS-008-attribution.1.1, §FS-008-attribution.3)fallback_sourceskeeps[]equal to absent. No checkout can hint a source's name, so an empty list has nothing to refuse and claims nothing, as before. (§FS-008-attribution.1.1)reposstays the project type's layout. The compiled identity's forest repositories are the type'srepos[]alone, and a manifest'sidentity.reposis never adopted. On a registry that loads this changes nothing. Validation requires a known type on every row and a non-emptyrepos[]on every type, so the old emptiness rule always returned the layout. A row that wants a repository claimed beyond its forest puts it interritory, which places it as a forest repository would. (§FS-008-attribution.1.1)The docs stop promising hints identity never read. These now name the four adopted lists:
docs/manual.md§4.2.1 and §4.2.2;docs/registry.md, "Identity and territory";identitydescription.They say that a manifest's
identity.name,identity.ticket_patternsandidentity.reposare accepted and not read as identity. Ticket prefixes come from the branches the row declares, and the forest's repositories come from its type. In the registry schema, the descriptions ofaliases,territory,addressesandroomsstate the presence rule, soephor schema registrydocuments it.The specification.
reposlist read by presence, a second meaning for the type's layout, a rowaddressesthat keeps the emptiness rule, a none sentinel,manifest_trust: "ignore"as the remedy, and a doctor or validation note on a refusal..into identity, and those place nothing.[]bullet narrows torepos, which DF-006.3 defers.Who this breaks
"aliases": []or"territory": []adopted the checkout's hint before and refuses it now. The JSON shape is the same; only its meaning changes. To adopt the hint again, delete the key."addresses"was silently ignored before and now gets exactly what it wrote.The version marker
The
[]change keeps the JSON shape, and the release's schema diff ignores an edit to a description, so nothing in the schema would show the change. The registry schema therefore gains a version marker,"$id": "https://github.com/agent-grounds/ephor/schemas/registry/v2". The unversioned schema counts as the first version. (§FS-006-project-interface.11, §DF-006-every-hinted-list-is-read-by-presence.1)The marker is read only by a release that follows a tag. Such a release compares every published schema from the previous tag to
HEAD, and it lists a changed version marker, here an added/$id, under Compatibility notices. (§FS-002-release.1.4) ephor has no release tag yet (git ls-remote --tags originis empty). By the same point the first release writes no notices, because nothing it ships was released before. The marker is what a release after a tag reads. From then on, the registry schema's notices are labelledv2rather thanunversioned. The new release-notices test pins this: against a tagged unversioned schema, adding the marker is noticed.Smaller things a reviewer might trip on
identity()reads the four row fields, andPlacementis never serialized. So no JSON output turns a[]intonull.Placementfrom a registry goes through registry validation first. Solisted()never sees a present key that is malformed.main, theidentity()doc comment had drifted ontomanifest(). It is moved back ontoidentity(), with its text unchanged.Placementliterals in tests and helpers gainaddresses: None. They are insrc/branches.rs,src/manifest.rs,src/capabilities.rs,src/sweep.rs,src/feed/tui/actions.rsandsrc/work/mod_tests.rs.e336947525also carries the one-line edit to the manifest schema'sidentitydescription. Its message does not mention that edit. Its last sentence says the release lists the new meaning of[]under its compatibility notices. That holds only for a release that follows a tag, as explained above.docs/changelog.md§1.1 says no change edits the file, and the release writes its line from this pull request's title.How it was verified
The branch is spec first.
c7eaf2db2bholds the specification and the failing end-to-end case and nothing else. It is followed bye336947525(feat),5da608c815(docs) and71cb37daaf(test).E2E-057, "every hinted list is read by presence" (
cargo test --test e2e_057_every_hinted_list_is_read_by_presence). It has four tests:[];addressesrefusal."alice@example.org",[""]and[7]are refused at/projects/0/addresses, and[]and["bob@example.org"]load.On the spec commit, 1 passed and 3 failed, each for the reason the ticket names. Now all 4 pass. The silent row passed before and after, which guards against refusing too much.
Unit tests
branches::tests::a_row_that_writes_an_empty_list_is_told_apart_from_one_silent_on_it: aliases, territory and addresses, each absent,[], and the row's own list.branches::tests::a_row_has_the_last_word_on_every_hinted_list_by_writing_it: the issue's three rows.branches::tests::a_typed_row_ignores_the_manifests_repositories.manifest::row_tests::the_row_adopts_the_projects_hints_and_overrides_them, extended with[]refusals, a row address, and a row list winning underTrust::Ignore.registry::tests::addresses_on_a_row_is_a_list_of_addresses.Release notices.
tests/integration/test_release_notices.pygainstest_a_version_marker_given_to_an_unversioned_schema_is_noticed. It passes on the existingscripts/release_notices.py, which needed no change.The local gate on
71cb37daaf. Five commands ran, 4m04s in all, and all exited 0:pre-commit run --all-files, with CI's pinned grund 0.14.0 forgrund checkandgrund fmt, plus fissile, the private-words hook and the attribution hook;check_boundary.py;check_parity.py;cargo test, withTMPDIRon a symlinked directory.The implementation step also ran these, each passing:
RUSTFLAGS=-Dwarnings cargo build --all-targets --locked;cargo test --all-targets --locked, 72 binaries and 1855 tests, both plain and with a symlinkedTMPDIR.The private-words hook passed but checked nothing, because the machine it ran on has no word list.
Hosted CI on
71cb37daaf. Five checks succeeded:cargo testonubuntu-latestandmacos-latest,grund check,fissile checkandshipped steps (dogfood). The branch sits onmainatdf6d0cbad7. It is rebased before it merges.Review
The issue's
path:plannedlabel set the route. The author approved the proposal on the issue with a reaction before anything was written. One review round approved the change at71cb37daafwith 0 blocker, 0 major and 0 minor findings, so no fix round ran. The review confirmed each of these:identity()reads the four fields;listed();repospremise holds on every registry that loads;$idmarker yields exactly one compatibility notice;Found and deliberately not fixed
docs/registry.mdsays identity's forest repositories come from the type'srepos[]"and anyrepo_overrides". Butdeclarations()never readsrepo_overrides; they are merged only to render AGENTS. So an override that changes a repository'spathleavesidentity().reposunchanged. This needs its own ticket: either drop the clause or makedeclarations()apply the overrides.main, and this change narrowed it to "only". Commit5da608c815's message repeats it.Trust::Descriptions. §FS-008-attribution.1.1 says a present list wins at every trust level, and the tests coverFullandIgnore.Descriptionstakes the same code path asFull, because at that level the manifest is still read with only its commands stripped.AI workflow: `rhei`, 12 agent invocations across 1 model; 8 tasks completed, 8 in progress.
github-issues-agent-grounds-ephor-189-implement-581aa7b9.ticketsupervising (visit 1) — cld, anthropic/claude-opus-5-5 — 1m50s — 1.6M in / 10.8k outgithub-issues-agent-grounds-ephor-189-implement-581aa7b9.ticketsupervising (visit 2) — cld, anthropic/claude-opus-5-5 — 1m52s — 1.8M in / 10.9k outgithub-issues-agent-grounds-ephor-189-implement-581aa7b9.ticket.planplanning — cld, anthropic/claude-opus-5-5 — 8m44s — 7.1M in / 52.6k outgithub-issues-agent-grounds-ephor-189-implement-581aa7b9.ticketsupervising (visit 3) — cld, anthropic/claude-opus-5-5 — 49.0s — 831.1k in / 5.3k outgithub-issues-agent-grounds-ephor-189-implement-581aa7b9.ticketsupervising (visit 4) — cld, anthropic/claude-opus-5-5 — 1m31s — 1.0M in / 9.5k outgithub-issues-agent-grounds-ephor-189-implement-581aa7b9.ticket.specifyspecify — cld, anthropic/claude-opus-5-5 — 8m35s — 8.7M in / 49.7k outgithub-issues-agent-grounds-ephor-189-implement-581aa7b9.ticketsupervising (visit 5) — cld, anthropic/claude-opus-5-5 — 3m49s — 2.0M in / 11.2k outgithub-issues-agent-grounds-ephor-189-implement-581aa7b9.ticket.implementimplement — cld, anthropic/claude-opus-5-5 — 12m08s — 12.1M in / 53.8k outgithub-issues-agent-grounds-ephor-189-implement-581aa7b9.ticketsupervising (visit 6) — cld, anthropic/claude-opus-5-5 — 1m20s — 737.0k in / 8.1k outgithub-issues-agent-grounds-ephor-189-implement-581aa7b9.ticket.review-1review — cld, anthropic/claude-opus-5-5 — 7m48s — 9.3M in / 40.7k outgithub-issues-agent-grounds-ephor-189-implement-581aa7b9.ticketsupervising (visit 7) — cld, anthropic/claude-opus-5-5 — 1m13s — 1.0M in / 6.8k outgithub-issues-agent-grounds-ephor-189-implement-581aa7b9.ticketsupervising (visit 8) — cld, anthropic/claude-opus-5-5 — 1m26s — 1.2M in / 9.1k out