Pan-ticket: be/ has no ONE resolver turning a URI into an object. The record URI §URI->hash resolution advertises (store, mpath, shard, wtree, spath, rpath, chash, otype, ohash) is produced nowhere; each verb re-derives its own half. URI-013 unified the syntax layer (parse/compose via the URI class); this unifies the resolution layer. Umbrella over the per-site twins, gated on the URI-010 merge.
AUDIT 2026-07-14 (orchestrator) — the FRAME half is largely BUILT, contrary to this ticket's first draft. core/discover.js already carries the spine, and most verbs are on it. Do NOT rebuild it; consolidate it and fill the two gaps.
discover.js:300 resolve(context, rel) — BE-030's design law, implemented: context (a URI) carries the tree NAME + the TRUSTED in-repo dir; rel is UNTRUSTED and AUTHORITY-BLIND (no //other, no scheme: — a tree swap cannot ride the arg slot); resolveInTree throws NAVESCAPE on climb-out, so the result can never leave the cell.discover.js:399 argRel(repo, raw) = resolveInTree(_ctxSub(repo), raw), PRESERVING the dir-form (sub/, ./). discover.js:389 _ctxSub reads be.ctxDir, which core/loop.js:405 sets as opts2.reentry ? repo.wt : io.cwd() — CLI cwd and pager reentry ALREADY unify here (BE-032).argRel: status.js:123/129, diff.js:445, cat.js:155, ls.js:142, why.js:83, log.js:494. wtpath (discover.js:427) routes every wt file access through resolve().//URI-016-cwd (orchestrator-verified): verbs/post/post.js had argRel 0 / ctxSub 0 and passed slots.narrow raw to decide->makeNarrow. Fix: isPathSlot accepts bare ./.., slots.narrow = discover.argRel(info, …) once after parseSlots. put.js/delete.js were ALREADY correct (argRel first, then a normRel dir-form decode) — audited, not churned../-shed (makeNarrow, put.js:78/delete.js:66 normRel): argRel PRESERVES dir-form and emits ./ for the wt root, and the shed decodes that to "no narrow". Retiring it → POSTNONE: no changes since base on cd <root> && jab post . '#msg'. Guarded by test/post/rootdot-narrow. Resolve upstream, decode downstream — two different steps.shared/project.js:48 resolve(arg) is NOT a rival CLI resolver: its only callers are verbs/mark/mark.js:52 and verbs/rss/rss.js:115, the publication verbs over the journal tree. Leave it be.chash/otype/ohash and the one record — plus the submodule descent, which today lives inside GET-side mount code rather than in a resolver:be/shared/project.js:48 resolve(arg) -> {root,tree,rel,abs}: authority + resolveInTree confinement ONLY; never crosses a gitlink, never reads the store.be/shared/submount.js owns the gitlink crossing (shard <- child Title, sibling-shard clone, sub wtlog anchor) as GET-side mount logic — unreachable for why/diff/cat.be/shared/relate.js:34 resolveRef maps ?/project -> refs/heads/project (leading-/ strip, GIT-015 defect A) and POSTREFs a trailing slash — contradicting URI L49 ?/project/branch/.be/core/discover.js wtpath + be.wtdir (URI-011) are a third path-fixing route.One resolver, two halves, split at the seam the spec currently hides between steps 4 and 5.
put staging an untracked file needs exactly this and no hash).ok64, never a partial.test/branch.js:72, test/submount.js:125; work 4 otherwise still stands for every other test./home/gritzko/src/URI-010/, unmerged, and BE-011 has since superseded its HELPER half (resolveInTree/wtJoin replace cwdRel/resolveWtPath). Only its ROUTING half survives, and it is being PORTED onto main's API here, not merged. URI-015 landed 082b172c.RULED 2026-07-14 by gritzko, in conversation; transcribed here.
?/project. A worktree gives the shard; otherwise the store path does: file:/path/.be/shard. .be is the pivot — prefix incl. .be = store, next segment = shard, rest = rpath. be:/https:/ssh: keep the git convention (path = repo). Cross-shard stays relative: //ABC-123/abc? = the sub's trunk.chash; the working copy is never consulted for the object half. A dirty file DOES resolve — to the base blob, which is what diff names as its left side. Untracked / absent-at-chash is a plain failure.otype/ohash are always defined on success.resolve_hash ignores ?branch when # is set; the VERB still consumes the branch — track vs base are the worktree's two parameters ([the dirty-words post]).///path/to/sub?proj_branch#pin (main tree) or //worktree/path/to/sub#pin. A pin-branch is the gitlink value as a function of the parent's branch — it MOVES as that branch moves, so it is ref-like yet DERIVED, never stored. ahead/behind = relate.verdict(sub's own chash, the resolved pin).//URI-016/test = the sub's ROOT TREE — shard: "beagle-ext-tests", spath: "test/", rpath: "", otype: "tree". The parent's gitlink renders as an empty dir, and an empty dir has no value; nobody wants ls //URI-016/test to answer with a commit hash. A path AT a mount descends and re-anchors the frame exactly as a path THROUGH it does — one rule, no boundary case.///path/to/sub#pin = the sub's frame at the parent's gitlink value. A pin IS a commit in the sub's shard, so decision 4 ("the fragment sets the frame's chash") applies unchanged — no special case. Decision 8's refresh needs no URI at all: the parent walks its own tree on get/post and holds the gitlink already.7a. Decision 1's gloss STANDS: //ABC-123/abc? is the SUB's trunk. (An earlier draft "corrected" it to the parent's trunk under a mount-boundary rule that decision 6 has now removed.)
///path/to/sub?proj_branch#pin. It replaces the synthetic dotted chain in the tip row.relate.verdict(base, tracked pin) — the decision-5 use case, answerable from the sub's wtlog alone.get SETS the current branch — GET-OWNED, per SUBS-040's surviving ruling and Worktree: a bare get FFs the tip but KEEPS the branch, and post/put/delete advance the tip yet NEVER switch branch. Local commits land as post #hash rows (the base advances). The parent's get/post writes the ///path#pin row (the tracked pin) — but ONLY when the sub is freshly cloned or SITS ON the pin.adv from gitlink != base. This is what makes the decision-8 refresh safe: the parent never clobbers a sub you are working in. It also supersedes SUBS-040's I3 (ahead/behind measured against the dot-branch) — the answer is now gitlink != base, no ref involved.Spec fixes first (they are cheap and they pin the code's target); URI is -- gritzko hand-written, so those need his hand or his say-so.
//WT/sub/file?rev, a dirty file (-> base blob), an untracked one (-> fail), a #pin under a sub.//WT/wtrel stands for $SRC_ROOT/work/wtrel DROPS WT — must be $SRC_ROOT/work/WT/wtrel..be below $HOME" vs project.js:17-41 NEAREST .be-or-.git PLUS meta/ — a spec-code discrepancy, its own ticket per todo..be-pivot parse and the per-scheme split.rpath: "SHA.h" must yield otype: "blob" and ohash != chash; spath: "abc/" under shard: "libdog" reads wrong.?7d37f90f (L53) falls through 5.2 (#-only) into 5.3's reflog and resolves to nothing — accept both sides or strike L53.project.js + discover.wtpath + submount.js's crossing), recording the crossing chain, not just the innermost spath.store/shard index + reflog; ok64s for no-root, NAVESCAPE, unknown wt/shard, ambiguous hashlet, not-a-commit, no-such-path-at-chash.?/project consumers — relate.js:34, submount.js:12 (?/<proj> -> child Title swap), serve.js.ingest.js saveRemoteRef + clone/add strip the query to ? (remoteUri.replace(/\?.*/,"?")), collapsing every branch of one host onto the trunk key — store.eachRemote (~L451) keys by authority+query and already tells them apart, so the WRITER is the defect. Row becomes <scheme>://host/project?branch#tip; the fix is to stop stripping. Repro: record two branches of one host, assert two distinct tips (the second clobbers the first today). Callers inheriting the loss: verbs/post/post.js pushRemote, verbs/put/put.js pushWire, head.$be get be://localhost?/project and $be get file:/$SRC_ROOT/.be?/project — both become paths under decision 1.uriarg/resolveHashlet).//URI-016: branch.sub() gone (verified sole non-test caller), syntheticBranch → trackedRow, currentSubPin → subBase/subTracked. Repro test/subpin.js builds a sub wtlog [redirect, get ///dog/abc#PIN, post #NEW] and asserts subTracked()==PIN / subBase()==NEW, replicating the old pattern-blind reader inline to pin the conflation. ORCHESTRATOR-VERIFIED 2026-07-14: subpin.js PASS, and submount/branch/uri/checkout_guard/submount_guard units PASS in that wt.format()'s segs[0][0] === "." re-heading (branch.js:49) is still in — it is inert once nothing mints dotted chains, but test/branch.js:46-52,97-101 assert the dotted labels. Test grant extended 2026-07-14 to cover those. NOTE parse()'s dot tolerance is READ-COMPAT for legacy rows and its comment claims "read-compat is forever" — gritzko's edit-in-place ruling OVERRIDES that; the live ~/src/journal/be test sub still carries a dotted row until migrated.//URI-016-adv, verified: views/status/status.js gains subDivergence(k, repo) = relate.verdict(subBase, subTracked) off the sub's OWN anchor (fresh mount → no sub-own row → base falls back to tracked → eq → clean), preferred over the old computeDivergence; shared/subs.js classifyMount R4.base now reads submount.checkoutBase (pattern-aware) instead of the pattern-BLIND curTip, with a curTip fallback for a SUBS-049 primary nested wt. Repro test/sub/pinstatus (4 arms; arm 4 = a fresh mount at a STALE pin, the real shape). ORCHESTRATOR-VERIFIED: fresh real mount reads ? 302 ok CLEAN with the fix vs ? 302 ok (behind 5, ahead 69) on trunk.adv is deliberately NOT a raw gitlink != base (worker, confirmed by orchestrator): subs.enumerate's adv bucket DRIVES post recursion (post.js:463 if (s.bucket === "adv") return true; via subScopedDeep/postSubs), so a raw inequality would make a BEHIND sub recurse and bump the parent's gitlink BACKWARDS. Only the base SOURCE changed (a no-op until decision 8's refresh lands); the recursion half stays carved out.subTracked/subBase are called only inside submount.js (checkoutBase), while status.js still derives ahead/behind from attachedBranch() + the ref (:296, :475). A FRESH mount under the combined wt writes the new row correctly (//x/test#1bd2d3b7…) but then reads ? 302 ok (behind 5, ahead 69) — TRUNK-relative noise — where the dot-branch gave (behind 1, ahead 2). That is SUBS-040's I3 symptom returning ("status diffs against the WRONG ref; a cascade-committed sub should read CLEAN"). The dot-branch's FUNCTION must be replaced in the same landing: status's sub ahead/behind = relate.verdict(subBase, subTracked) + decision 11's adv on gitlink != base. No test caught this — the sub cases assert tip+gitlink only (SUBS-040's own note).?/beagle-ext-tests/.beagle-ext#<pin> (branch in the QUERY, no authority → the new reader classes them SUB-OWN). Rewriting them to //WT/test#pin BEFORE status reads the pin would drop the branch and make the display worse, not better. Migrate WITH the consumer, not before.post.js:404-408 postTree bumps the PARENT's gitlink (a synthesised put <sub>#<newsha>, appended to the parent's ulog at put.js:571); only submount.js:286/331 ever write a SUB's anchor, at mount. So a sub's recorded pin goes stale after a parent post. Likely its own SUBS ticket — the resolver only READS the row.submount.js:50 currentSubPin keeps the LAST #<40hex> fragment across ALL rows, pattern-blind, and its comment calls that "the checkout baseline" — so a parent-written pin row can be mistaken for the sub's base. Harmless today only because base and tracked COINCIDE at mount; they diverge the moment the sub posts its own commit, i.e. exactly the ahead/behind case. Split the reader in two: subBase() and subTracked().///path/to/sub#pin / //WT/path/to/sub#pin (tracked). The sub's OWN rows carry no mount path — get ?branch#hash sets the branch, post #hash advances the base. Implemented in wt //URI-016 as lastPin(anchor, tracked) + subBase/subTracked/checkoutBase; row composed by trackedRow(navCwd(wt), subpath, parentBranch, pin). Rests on URI-009 (absent vs present-empty authority) having landed.u.authority !== undefined alone is NOT total — a sub-own remote get records get be://host/proj?br#tip, which HAS an authority and a fragment, so it misreads as a parent-written pin and corrupts subTracked(). A parent row is a NAV URI: require u.scheme === undefined && u.authority !== undefined. The mount's own redirect row (scheme file:, no fragment) is safe either way.chash stays the ground truth the refresh reads FROM. State which wins when the row is stale (decision 8's refresh is get/post-driven, so it CAN be).//WT/rel/dir/, $PWD-LIKE — so a bare arg joins it, as in a shell. Fix: a BARE path JOINS the context (like ./); a /-led path stays wt-ROOT-anchored (matching argRel's s[0] === "/" root-anchor). No double-resolve: spell.js:61 roots the merged arg0, and argRel skips ctx re-resolve on rooted args (status.js:120). In wt //URI-016-tui. Probed live below:
cd verbs/post && jab cat post.js → verbs/post/post.js (argRel over _ctxSub). Verified working.spell.js:56-60 (URI-011b) REPLACES the context path for a bare//-led arg; only .//../ joins it. So put shared/wire.js in a //journal/be view → //journal/shared/wire.js, dropping /be (URI-010 L13's pager twin).spell.js:61 prepends /), and argRel then skips ctx re-resolve on rooted args (status.js:120 documents this). Coherent, but it violates BE-030's law ("verbs resolve args, the pager is arg-blind, no pre-merge") — the law says retire the path merge from mergeUri, keep only its NAV slots (//WT, ?ref, #frag), and let argRel resolve against ctxDir (loop.js:389 ALREADY sets ctxDir = nav.dir for a threaded pager context).//WT/verbs/post: bare post.js -> //WT/post.js (DROPS verbs/post); ./post.js -> //WT/verbs/post/post.js. So :cat post.js in a subdir view opens the WRONG file while cd verbs/post && jab cat post.js opens the right one — the same spell, the same dir, two answers. That is the defect, not a UX choice.//URI-016-tui, but NOT landable — see the test ruling below. spell.js mergeUri PATH slot inverted (4 lines): a BARE path joins the context, a /-led one stays wt-root. Repro test/bro/ctxrel (pager-level). Orchestrator-verified by swapping the baseline spell.js in: RED (merged-arg0-rooted got "/DOG.h") -> GREEN. No double-resolve (spell.js:61 roots arg0; argRel root-anchors); empty context degrades to wt-root, covering loop.js:405.//WHY-001/shared/shared/util). Three sites shared the dead convention (_compTok, _fsCompletions, _glob); all now anchor on one extracted seam _ctxUri()/_ctxPath(). Repro test/bro/tabnav asserts the COMPOSED NAV after Tab+Enter — the assertion tab-nested-wtrel lacks, which is why it passed through the breakage. Subsumes BRO-022 goal 2 (a ./ insert matching what the spell resolves against; root cause = the completer scavenging _verbUri(), which _composeCall's own comment forbids). NOT goal 1: _fsCompletions/_glob still readdir be.wt_root, ignoring the view's //authority.driver.js:420 asserts "util/" (its intent line kept, since the new code still navs to //WHY-001/shared/util — now asserted at the COMPOSED-NAV level by test/bro/tabnav, which the old case never checked); glob.js:36-39 drives the real seam p.ctx = viewPath, its L73-74 assertion UNCHANGED. The two tests below were the last gate:
1. test/bro/pager/driver.js:420 check("tab-nested-wtrel", t6.cmd === "shared/util/") ENCODES the old wt-root rule (comment L411: "a bare stem completes to the FULL wt-relative path"); it now yields util/. Irreconcilable — ANY correct completer emits a context-relative or /-led token here. NOTE its stated INTENT ("both nav to //WHY-001/shared/util") IS satisfied by the new code; only the assertion on the intermediate TOKEN encodes the dead mechanism.
PRE-SPECIFIED so the ruling is one word — the EXACT amendments, measured not guessed (orchestrator 2026-07-14; each test fails on exactly ONE assertion, nothing else):
driver.js:420: check("tab-nested-wtrel", t6.cmd === "shared/util/") -> === "util/", and its L411-413 comment loses "a bare stem completes to the FULL wt-relative path". The comment's INTENT line ("both nav to //WHY-001/shared/util") STAYS — the new code delivers exactly that, which test/bro/tabnav now asserts at the composed-nav level the old case never checked.glob.js:36-39: mkPager sets the view by stubbing p._verbUri = …; replace with the real seam (p.ctx = viewPath). The L73-74 assertion ["./x.c","./y.c"] is UNCHANGED and correct — driving the real seam reproduces it. Today the stub is ignored (by design: _ctxUri() refuses the scavenge _composeCall forbids) so the glob falls back to the wt root and returns ["./a.c","./b.c"].test/bro/glob/glob.js:73-74 — the ASSERTION is still correct; only the FIXTURE is stale. mkPager (L36-39) stubs p._verbUri, the scavenging seam _composeCall forbids and _ctxUri() therefore ignores. Driving the real seam (p.ctx = "src") reproduces the expected ["./x.c","./y.c"] exactly. A one-line fixture change, not a re-golden.loop.js:405 notes a context-less reentry has "args pre-merged root-relative" and pins ctxDir to the wt root; BRO-017 can leave the initial view with an EMPTY context — a bare arg with no context must stay wt-root-relative (nothing to join). Touches BRO-016.?/project/branch premise dies with decision 1; its live defect (the saveRemoteRef query strip) carries into the TODOs above.resolveInTree(base, rel) is only ever called with base "", so a relative arg resolves at the wt ROOT, not the cwd. Ported in wt //URI-016-cwd.//X rulings await gritzko and overlap the frame half's context model.main in relate.js:36 vs the Title) still needs settling, but only in the wire codec, not the URI.WIP 2026-07-14 — FIVE slices COMBINED in wt //URI-016, uncommitted, pending gritzko's inspect/merge. Suite *168 pass / 10 fail*, and all 10 fail on main too (bro/color, bro/view, cat/links, dag/remoteindex, get/http, get/oddname, put/wire-refguard, relate/verdict, sub/wtsrc, wire/saveremote); 8/8 JS units; 6 new repros green. NO red left that this work introduced — the two that encoded the superseded TUI rule were amended under gritzko's 2026-07-14 "amend both". Slice 5 = BRO-022 goal 1 (views/bro/pager.js only): _fsCompletions/_glob readdir the VIEW's authority tree via a new _ctxRoot() seam over be.wtdir, not the launch tree — repro test/bro/tabtree (two hive trees with DISJOINT filenames, so a candidate can only come from the tree actually read). Pre-fix the launch tree's names leaked into BOTH a //OTHER view and a REMOTE view. Null wtdir (scheme/unresolvable) → no FS candidates (hunk candidates still serve); a SCOPE-LESS ctx keeps the launch-tree fallback. BRO-022 goal 2 was already subsumed by slice 4's _compTok. TUI and CLI now resolve a bare arg by the SAME rule (probed live, context //WT/verbs/post: bare post.js -> //WT/verbs/post/post.js in the pager, verbs/post/post.js in the CLI; /post.js -> wt-root in both). TWO EXISTING TESTS FAIL BY DESIGN pending a gritzko ruling (see the bare-path entry above) — bro/pager (tab-nested-wtrel asserts the superseded wt-root token) and bro/glob (stale _verbUri fixture). NEITHER was edited (work 4/5). All other failures are pre-existing on main. Suite: main 163 pass / 12 fail vs combined 165 / 10 — identical failures minus this ticket's two repros; 8/8 JS units. The 10 are pre-existing on BOTH sides (bro/color, bro/view, cat/links, dag/remoteindex, get/http, get/oddname, put/wire-refguard, relate/verdict, sub/wtsrc, wire/saveremote).
trackedRow + subBase/subTracked/checkoutBase, split on scheme === undefined && authority !== undefined. Repro test/subpin.js.post's Path slot resolves through discover.argRel; isPathSlot takes bare ./... Repro test/post/subdir-narrow (+ rootdot-narrow guarding the ./-shed).test/sub/pinstatus.//URI-016: context-scoped status from a subdir, bare-arg cat/diff resolving against the context, cross-tree status //OTHER / ls //OTHER with no context leak, NAVESCAPE refusal on climb-out, and a fresh sub mount reading CLEAN.jab get ?/beagle-ext clone checks out beagle-ext's OWN jsrc -> . self-locator, and jab's scan stops at the NEAREST jsrc — so a plant ABOVE the clone is shadowed and the clone runs its own TRUNK code. The clone step itself still uses the plant, so a probe can write new-format rows then read them back with trunk code, which looks exactly like "the fix does not work". test/lib/*case.sh scratch repos are hand-seeded and carry no jsrc, so the real harness is immune.Still open: the CLI-vs-TUI bare-path ruling (above), decision 8's post-side pin refresh, decision 11's recursion half, the legacy-row migration (must land WITH its consumer), and the hash half (chash/otype/ohash + the one record) — this ticket's original core, untouched by these three slices.