get DELETES untracked files — only get! may clean
A plain (non-force) jab get must MERGE dirty tracked files and LEAVE UNTRACKED files alone; only jab get! may clean the worktree (GET wiki: "be get! resets the worktree to a clean state, all local changes discarded"). It does not: a non-force get that recurses into a submodule RESET the sub and SILENTLY DELETED every UNTRACKED file in it. Verified this session: in a beagle-ext worktree, test/ is the beagle-ext-tests store-backed submodule; a non-force jab get swept 7 dirs of untracked, newly-authored test files (left empty), while tracked files survived — silent data loss. Suspected locus: get's del-sweep / prune (verbs/get/get.js fanoutWholeTree + delSweep) unlinks worktree paths absent from newTree WITHOUT checking they were TRACKED (in oldTree), and/or the submodule recursion does a wholesale clean re-checkout of the sub (should be read-only, DIS-058). JS only (be/, via jab). Evidence: cost the GIT-016 T1–T4 tests. Method: work; siblings JGET-001/DIS-058/JGET-013.
beagle-ext worktree test/ is a SEPARATE store-backed submodule (beagle-ext-tests, own .be). A NON-force jab get (recursing) reset that sub and DELETED all UNTRACKED files under it — 7 test dirs emptied; the 40+ TRACKED test files survived.verbs/get/get.js fanoutWholeTree(ctx, r, wt, force): noPrune = !!force || (sameTip && !r.fresh); a del-sweep BARRIER (DELSWEEP/delSweep) folds delete leaves; if (!force) dirtyOverlapCheck(...) (get.js ~L382). The prune/del-sweep is what removes worktree files during a switch/update.be get! (force) "resets the worktree to a clean state, all local changes discarded" — so a non-force get must NOT discard untracked (or dirty tracked) content.oldTree NOR newTree. A tracked deletion = a path in oldTree but gone from newTree. Only the latter may be unlinked, and only when not dirty.jab get (switch / update / re-get / FF) NEVER deletes an untracked worktree path (in neither tree); it merges dirty tracked files and only removes TRACKED deletions.jab get! keeps its clean-reset (may remove untracked/dirty) — force is the SOLE cleaning path.be/, run via jab. Do NOT run native be; do NOT open beagle/.get.GET-040 comments, greppable, ticket-coded ≤64-char commit. No new deps.get! (still cleans), dirty-tracked merge, tracked-deletion sweep, or the get/dirty/restore buckets.oldTree"; a path in neither tree is untracked → LEAVE IT. Never unlink by "absent from newTree" alone.get!) is the only mode that may remove untracked/dirty; everything non-force is merge-or-leave.test/sub/untracked/run.sh (sub + nested untracked + dirty tracked preserved non-force; force arm cleans). Top-level untracked is safe by construction: the tree-reconcile enumerates only tree entries and a delete leaf fires only with an oldSha (get.js leaf/reconcileDir).shared/checkout.js apply gates unlinks on the OLD baseline tree (oldMap) — non-force unlinks only tracked deletions, preserves dirty edits (mrg). 0092271c.shared/submount.js threads the sub's PRIOR pin as oldTip + the global ambient force flag into checkout.apply; recursion no longer clean-resets the sub. 0092271c.get! clean-reset asserted by test/sub/untracked arm 2; suite green per d80bdb5c "js tests pass, no native use" (2026-07-06 audit: not re-run, read-only).--force orphan sweep); coordinate so force still cleans and non-force never does.0092271c "GET-040: fix recursion bug (submodule reset)": FORCE (get!) is the sole clean-reset; NON-force (re)writes only missing/clean tracked files, preserves dirty edits, and unlinks ONLY a tracked deletion — an untracked path (in neither tree) is never touched (shared/checkout.js apply oldTip/oldMap model; shared/submount.js passes the sub's prior pin + the global force flag).test/sub/untracked/run.sh (two arms: non-force preserve, force clean-reset).50e90012); suite not re-run (read-only audit) — green per d80bdb5c.