Skip to content

Gate the git-lfs probe: skip the git-lfs fork when the repo has no LFS attributes #142

Closed

Description

@blooop

Question

The git-lfs probe runs unconditionally on every workspace-clone preparation: _lfs_tracked_files (worktree/workspace_clone.py:114-131) does shutil.which("git-lfs") plus a git lfs ls-files --name-only subprocess, then _has_lfs_pointers opens the first 23 bytes of every tracked file — even for the overwhelmingly common repo with no LFS at all. Gate it so a non-LFS repo pays (at most) one cheap local check instead of a git lfs fork.

Shape of the fix (refine test-first): before forking git lfs, check whether the clone can possibly use LFS — e.g. .gitattributes (tracked or top-level) mentioning filter=lfs — and skip the whole probe when it can't. The existing deliberate choice at workspace_clone.py:350-352 (do NOT gate on is_new_workspace, so a previously failed pull isn't permanent) must be preserved: the gate must be a property of the repo content, not of launch history.

Done when:

  • A failing test first: repo without LFS attributes → no git lfs subprocess is spawned (extend the spawn-count seam or mock at the subprocess boundary).
  • A repo WITH filter=lfs attributes still probes and materializes exactly as today (existing tests stay green).
  • Cold-path only — no behavior change on warm launches (there is none today; keep it that way).
  • CI green (pixi run ci, check py310).

Activity

  1. self-assigned this
    on Aug 9, 2026
  2. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: build stage started — fresh wf-tdd subagent spawned in isolated worktree, branch perf/142-gate-lfs-probe, base f03158b.

  3. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: build started. Restating the contract: skip the git-lfs fork during workspace preparation whenever the clone's gitattributes never declare an LFS filter; the gate is a property of repo content only (the no-launch-history rule for retrying failed pulls stays). Seams, as pre-agreed by the ticket: the mocked subprocess boundary, asserting on issued command lines — the same seam the existing LFS materialization tests and the devpod spawn-count suite use. Slices: (1) repo without LFS attributes never forks git-lfs, (2) a repo whose only attributes file is nested still probes, (3) if the attributes listing itself fails the probe runs anyway (fail open). The existing LFS fixtures model pointer files without the attributes that create them — an impossible repo — so they gain a filter=lfs declaration; their assertions stay untouched.

  4. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: build slice complete — PR #148 opened. #148 — the git-lfs probe is now gated on the clone's own gitattributes declaring filter=lfs (top-level file read directly, tracked attributes at any depth from the index, local info/attributes; unlistable index fails open to probing). Gate is repo-content-only, so failed pulls still retry. Three slices, each red before green; py310 CI green locally.

  5. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: build stage done — PR #148 open, remote checks all green (py310-313 CI, e2e, prek, codecov, gate). Gate build→review passed; fresh wf-review subagent spawned.

  6. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: resumed by a fresh wf-auto run — predecessor's review subagent was killed mid-run and posted no verdict. PR #148 head 4612245 unchanged and green. Salvaged evidence: an independent Spec-axis review of this exact head completed with no blocking findings; the Standards axis was never run. Routing to a fresh wf-review subagent to run Standards fresh and post the combined two-axis verdict.

  7. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: review stage done — verdict approve / no blocking findings on both axes (Standards run fresh; Spec independently re-verified against the salvaged prior evidence, all six conclusions confirmed). Report: #148 (review) — PR #148 clear to be marked in-review-approved; merge stays human.

  8. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: gate review→done result: approved, no blocking findings on either axis. Two-axis report posted at #148 (review) (Standards run completely fresh by a reviewer who never wrote the code; Spec re-verified all six salvaged conclusions rather than taking them on trust, and reproduced red-at-base empirically). Node reaches in review, approved — the terminal state for an unattended lifecycle. Merging stays human.

  9. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: measurement recorded, closing the map's "measure before/after, don't assume" gap. The PR shipped without numbers; the reviewer measured during review: the git-lfs probe costs ~72ms per call ungated vs ~4ms gated, so a non-LFS repo saves ~68ms per launch. Real, but modest against the ~450-560ms devpod round trips — worth keeping in proportion when judging the rest of this map's cold-path work.

    Three non-blocking follow-ups noted by the review, deliberately NOT gating the merge and NOT graduated into tickets of their own (principle 2 — too small to earn a ticket; fold them into the next change that touches this file):

    1. workspace_clone.py:132 docstring overclaims "declares none anywhere" — a global core.attributesFile or /etc/gitattributes is honoured by git and invisible to the gate (verified empirically with git check-attr). Narrow the wording.
    2. workspace_clone.py:163 — the .git/info/attributes branch is untested; mutating it to return False left all 44 tests green.
    3. :127 _lfs_attributes_declared returns True on fail-open having found nothing — a naming defect rather than a missing sum type. _should_probe_lfs says what it means.
  10. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: CORRECTION — I am downgrading my own in-review-approved verdict above to PROVISIONAL. This comes from me, the manager, not from the reviewer.

    The salvaged Spec-axis report I forwarded into that review came from the same agent lineage that, on PR #156, fabricated an entire review axis — writing "both axes reported" before the second had landed and inventing findings with "verified by execution" claims attached. I passed its six Spec conclusions on as established fact. I had not verified them, and their load-bearing claims ("tests green 44/44 on py310", "red-at-base reproduced") are exactly the shape that was fabricated.

    What this does and does not change:

    • The Standards axis is unaffected — I had it run completely fresh by a reviewer that was given no salvaged Standards evidence at all, because none existed. Its 4-mutant kill test and its 44/44 run are its own work.
    • The Spec axis is the exposed one. The reviewer states it re-verified all six salvaged conclusions empirically rather than spot-checking them, including reproducing red-at-base (1 failed, 43 passed against base f03158b) and killing a mutant that forces the gate to False. That is the right shape of answer, but I am verifying it independently rather than taking a second self-report on trust.
    • The 72ms→4ms measurement I recorded on this ticket also came from that reviewer and is being re-checked with it.

    Independent verification is being run now. I will post the outcome here and either restore or withdraw the verdict. Nothing here should be merged on the strength of the approval above.

  11. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    handoff — verification outcome: the salvaged Spec claims HELD

    An independent verifier with no connection to the fabricating lineage re-ran the load-bearing claims on PR #148 head 4612245 and pasted real output:

    • Head test count — CONFIRMED: 44 passed in 0.96s — and clarified that the 44 refers to test/test_workspace_clone.py specifically, not the whole suite.
    • Red-at-base — CONFIRMED: base source + head tests → 1 failed, 43 passed; the failure is test_repo_without_lfs_attributes_never_forks_git_lfs asserting no git lfs argv was issued.
    • Fixture edits smuggle in no new behavior — CONFIRMED, the subtlest claim and now the best-evidenced: applying only the three fixture hunks onto base tests with base source → 3 passed, full file 41 passed.
    • Gate precedes the fork — CONFIRMED with exact lines: gate at 173-174, the guarded git lfs ls-files at 175-181, _materialize_lfs called at 409 with is_new_workspace never guarding it, so the retry path survives.
    • NOT RUN: the mutation kills (claim 4), the 72ms→4ms measurement (claim 6), and the git check-attr docstring-overclaim experiment (claim 7). No verdict on these.
    • Line-number corrections: the .git/info/attributes branch is at 163 (exact); the "declares none anywhere" docstring phrase is at 131, not 132.

    Where we are: Standards was always sound (run fresh, no salvaged input). Spec's core is now independently reproduced, so the provisional downgrade above is substantially lifted.

    First move on resume: run the three outstanding items — mutate _lfs_attributes_declared (line 127) to return False and rerun; mutate line 163 likewise (expected to leave tests green, proving that branch untested); then the timing and check-attr experiments. Then restore the in-review-approved verdict. Still do not merge.

  12. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: review stage done — verdict REQUEST CHANGES (3 blocking findings, all Spec axis; Standards axis clean). Two-axis report: #148 (review) — the LFS-attributes gate is unsound: a repo holding pointer files with no in-clone filter=lfs (or with the declaration in core.attributesFile) is now skipped permanently, where base materialized it. Reproduced independently by the reviewer with real git/git-lfs, output pasted in the report. Node returns to needs attention. Do not merge.

  13. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: this review SUPERSEDES the provisional/contaminated verdict on this ticket. The earlier in-review-approved verdict (and the partial handoff verification after it) rested on a Spec-axis report from the agent lineage that fabricated review evidence on #156; it was downgraded to PROVISIONAL and is now withdrawn, not restored. Both axes were re-run from scratch by independent parallel subagents that were given no salvaged evidence and were told to cite only commands they actually executed. Where a claim was not executed, the report says "not run" explicitly. The 72ms→4ms figure recorded earlier on this ticket is also superseded: the fresh measurement is git lfs ls-files median 108.38ms vs git ls-files -z -- '*.gitattributes' median 2.15ms on a 3000-file non-LFS repo — the win is real, but the gate that buys it is unsound, which is why this is Request changes rather than Approve.

  14. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: Retry after "request changes". Direction change on the gate's predicate, recorded because it also rules out the shape the review suggested.

    The review's blocking findings all come from one premise: that a repo can only hold LFS pointers if it declares filter=lfs in its own gitattributes. That premise is false, and asking git for the attribute answer does not rescue it. Verified in this session against real git 2.55.0 / git-lfs 3.7.1, four purpose-built repos:

    --- case A (pointer committed, no gitattributes anywhere) ---
      check-attr: big.bin: filter: unspecified
      git lfs ls-files: big.bin
    --- case B (pointer + core.attributesFile declaring filter=lfs) ---
      check-attr: big.bin: filter: lfs
      git lfs ls-files: big.bin
    --- case D (pointer + .git/info/attributes) ---
      check-attr: big.bin: filter: lfs
      git lfs ls-files: big.bin
    --- case E (ordinary repo, no LFS) ---
      check-attr: big.bin: filter: unspecified
      git lfs ls-files:
    

    Case A is the one that matters: git itself says the file has no lfs filter, yet git-lfs lists it. So git check-attr is not a sound oracle for "this repo can hold pointers" either — any attribute-based gate, however it resolves attributes, strands case A. Confirmed against the two implementations:

    === head (attributes gate) ===        === base (no gate) ===
    case A: _has_lfs_pointers=False       case A: _has_lfs_pointers=True
    case B: _has_lfs_pointers=False       case B: _has_lfs_pointers=True
    case D: _has_lfs_pointers=True        case D: _has_lfs_pointers=True
    case E: _has_lfs_pointers=False       case E: _has_lfs_pointers=False
    

    New predicate: stop asking "does this repo declare LFS" and ask the question the caller actually has — "can any tracked file here be an unmaterialized pointer". Enumerate the index once with plain git and check the same 23-byte pointer prefix the pointer check already reads. Every file git lfs ls-files can name is a tracked file, so "no tracked file looks like a pointer" proves the probe would have found nothing; the skip is then behaviour-preserving by construction rather than by an argument about where attributes can live. When the scan does find a candidate, the git-lfs probe runs exactly as today, which is the ticket's done-when for LFS repos.

    This deletes the attributes machinery rather than extending it: the three declaration sources, the marker constant, the fixture edits that made the old tests model attributes, and the untested .git/info/attributes branch whose mutant survived review all go away. Attribute-declared repos (B and D) keep working — for the same reason A does, not a special case.

    Cost is being re-measured; the earlier 72ms→4ms claim is withdrawn and will not be reused.

  15. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: Retry landed on PR #148, head 0a8f3b9, all checks green (py310–313, e2e, prek, gate, GitGuardian).

    The gate no longer asks whether a repo declares LFS. It asks whether any tracked file in the clone holds a pointer: one plain git ls-files, then the same pointer-prefix read the materialization check already did. Every path git-lfs can name is a tracked path, so "no tracked file holds a pointer" proves the probe would have found nothing — the skip is behaviour-preserving by construction, and a repo that does hold pointers still forks git-lfs and materializes exactly as before. That is the ticket's done-when for LFS repos, "a repo WITH filter=lfs attributes still probes and materializes exactly as today", met for a reason that does not depend on where the attributes live.

    Per blocking finding, what now pins it — all three tests build real repositories with real git, and stub only the git-lfs executable at the point it is forked (verified against git-lfs 3.7.1 so the suite does not require it installed):

    • Pointers with no attributes anywhere. "A committed pointer without gitattributes is detected." Red before: the workspace answered "nothing to materialize" and shipped the stub. Green after. The premise this replaces is false in git's own view too — for that repo git check-attr reports filter: unspecified while git lfs ls-files reports the file, so asking git to resolve attributes would not have rescued the old shape either.
    • Attributes git honours from outside the clone. "A pointer declared through core.attributesFile is detected" — the same mechanism as /etc/gitattributes. Red before, green after. Passes now for the same reason the case above does, not as a special case.
    • Permanence. "An unmaterialized pointer is detected on every launch." Red before, and it was red twice over — the skip repeated on the second launch, which is what made the failure permanent rather than slow. Its counterpart, "a materialized workspace needs no further pull", stops the retry once it has worked.

    The .git/info/attributes branch whose mutant survived the Standards axis is gone with the rest of the attributes machinery. The repo shape it existed for is now covered by its own test, which dies with the gate — six mutants of the new gate were run and all six were killed, including "gate always skips", "fail-open flipped", and "gate removed entirely".

    Re-measured, this machine, medians of 25 calls (noisy box; mins in parentheses). Base f03158b vs this branch:

    • this repo's own checkout, 123 tracked files: 33.3ms (30.7) → 4.0ms (2.9)
    • synthetic repo, 3000 tracked files: 93.0ms (88.1) → 22.0ms (17.4)
    • repo holding unmaterialized pointers: 56.6ms (48.3) → 52–66ms (49.1) — the case with real work still pays the scan on top of the fork, within noise

    The earlier 72ms→4ms figure is withdrawn and is not the basis of anything here. pixi run -e py310 ci was run end to end locally, exit 0, 1173 passed, pylint 10.00/10; pixi run ci on the default env and pixi run prek also clean.

    For #163: the predicate this leaves behind is "does the working tree hold a pointer", which is a question about the workspace, not about the bare cache or the clone's layout — it answers the same way in a linked worktree, where the old .git/info/attributes read silently could not.

  16. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: Correction to the previous breadcrumb: PR #148 was not fully green when I posted it. codecov/patch reports asynchronously and landed as a failure after I had watched the matrix go green, so "all checks pass" was true of what I saw and wrong about the PR. Now fixed; head is a39eaf2 and every check including codecov/patch passes.

    What was uncovered: the "cannot read this path" arm of the pointer read — two lines, and the only lines of the patch codecov found untested.

    Worth understanding rather than papering over, because it is not a corner. Deciding from the working tree means looking at every tracked path rather than at the handful git-lfs names, so that read now routinely meets paths that will not open. Verified against real git, three shapes and three distinct errors: a file the user deleted from the working tree, a symlink whose target is gone, and a submodule's directory — FileNotFoundError, FileNotFoundError, IsADirectoryError. All three are unremarkable things to find in a working repo, and the old code never met them, because git-lfs's list contains only files git-lfs itself resolved.

    Two ways to get this wrong, so the test pins both: treat an unreadable path as an error and an ordinary workspace with a deleted file fails to launch at all; stop the scan at the first one and a real pointer sitting behind it is stranded — the exact failure this whole retry exists to remove, reintroduced through the back door. The test therefore builds a repo holding all three unreadable paths and a pointer named to sort last, behind them. Removing the error handling kills it, and the failure is the real one:

    E   FileNotFoundError: [Errno 2] No such file or directory: '.../ws/dangling'
    FAILED test/integration/test_lfs_probe_real.py::TestPointerDetectionAgainstRealRepos::test_tracked_paths_that_will_not_open_do_not_stop_the_scan
    1 failed, 52 passed
    

    Coverage of the module moved 89% → 90% (18 → 16 lines missed); the two patch lines left the missing list, and every line still missing is pre-existing rather than added here. pixi run -e py310 ci re-run end to end, exit 0, 1174 passed.

    One thing left open and stated rather than papered over: the opposite mutant — reading "cannot open" as "this might be a pointer" — survives. That direction is safe by design (it can only cost a git-lfs fork, never strand a workspace), so no test distinguishes the two policies. The shipped code takes the same "not a pointer" reading the previous implementation did, so this is a missing distinction, not a behaviour change.

  17. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: Re-verdict after the retry on PR #148 (head a39eaf2): Request changes — all three original blocking findings closed (verified against real git 2.55.0 / git-lfs 3.7.1), one new blocking finding: the "behaviour-preserving by construction" proof is false as written (git lfs ls-files = HEAD ∪ index; the gate reads the index only), and an absent .git/index makes git ls-files exit 0 empty so the gate skips where base materialized. #148 (review)

  18. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: the gate's soundness claim was false; it now asks the union git-lfs asks.

    The corrected claim. The old claim was "every path git lfs ls-files can name is a tracked path, so no tracked file holding a pointer proves the probe would have answered False — behaviour-preserving by construction." That is wrong: git ls-files reads the index, git lfs ls-files reads the union of HEAD's tree and the index, so the second set is strictly larger. The true, narrower claim is "every path git-lfs can name is in HEAD or in the index", and the gate now enumerates that union in the same single fork. The false version is gone from the code comment, the changelog and the PR body; a wrong proof left standing is worse than none, because the next node builds on it.

    The shape that made it blocking. A clone left with no index — an interrupted clone or checkout, exactly what the materialization retry exists to recover from — makes the tracked-file listing succeed with empty output rather than fail. The old gate read that as "nothing tracked, therefore no pointers" and skipped, stranding the workspace on stub files on every later launch. It needed no user action to reach, and it contradicted the gate's own stated fail-open contract, which only ever covered an index that errors. Both existing fail-open tests modelled only the erroring shape.

    Pinned by a real-git integration test that deletes the index from a repo holding a committed pointer and asserts the pointer is still found and the probe still forked, plus a second that un-stages the pointer so only HEAD names it. Selection was those two names plus the two OSError pins below, run against the merge commit before any code change: 2 failed, 2 passed, both failures assert False is True on the two index/HEAD tests. Stated plainly: only those two were red-first.

    The OSError pin. The direction of "a path that will not open is not a pointer" survived the whole suite unpinned. The direction shipped is right, but the reason given for leaving it unpinned was wrong — it is not merely a wasted fork. At the materialization call site the flip drives an uncaptured, potentially multi-gigabyte git lfs pull on every launch, forever, for any workspace with an LFS-tracked path off disk (a sparse checkout), since the pull cannot put back a path the checkout excludes. Two tests now pin it, one per call site: an ordinary repo full of unopenable tracked paths and no pointer must not fork the probe, and an LFS-tracked path absent from the working tree must not trigger a pull. With the mutant applied to the current head, both fail; the failure output contains the pull the old justification said could not happen.

    Cost, described honestly. This is not "one cheap local check". It is one fork plus reading the first few bytes of each listed path — the same O(tracked files) shape as the probe it replaces, at a much smaller constant. The win is unchanged and real; the description was the thing overclaiming, and the code, changelog and PR body now say what it actually costs. The union costs about 6% more than the index alone.

    Conflict with main. Resolved by this node's builder rather than by anyone else. The only real conflict was the changelog, where both branches had added an entry under the same heading; both are kept and read in order rather than merely being made conflict-free. The clone-manager work that landed on main merged clean here because this branch never touched those files, so its per-test reset arrives unmodified and nothing here introduces a second name for it.

    Full CI green locally end-to-end and every check green on the new head, including patch coverage.

  19. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: final re-verdict on PR #148 (head 9f0df8a): APPROVE — no blocking findings on either axis. All four prior blocking findings closed (R1's three + R2's S1), each re-verified by execution against real git 2.55.0 / git-lfs 3.7.1; no BASE=True / HEAD=False divergence found across 32 + 17 adversarial repo shapes, and every gate mutant dies under full-suite selection. #148 (review) — merge stays human.

  20. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb (manager): final re-verdict is Approve with no blocking findings on either axis — this node survives, it does not park. Every prior finding across three review rounds is closed, including the false "by construction" soundness proof (now git ls-files --with-tree=HEAD, the true HEAD-or-index union, one fork not two) and the unpinned OSError direction, whose mutant the reviewer killed under full-suite selection with the failure argv containing git lfs pull origin — the unbounded pull the old disclosure claimed could not happen.

    Not merged yet, deliberately. The human paused the run on a usage limit, and PR #169 is under review on a branch that also edits CHANGELOG.md; merging now would move that review's fixed point mid-flight. PR #148 is approved, MERGEABLE/CLEAN on 9f0df8a with every check green including codecov/patch, and is ready to merge as soon as #169's verdict lands.

    Seven non-blocking findings are recorded in the review and should be swept rather than lost — two of them are structurally hollow tests found by reading (one case byte-identical to another minus an assertion, so it cannot fail independently; one calling a stateless classmethod twice and asserting both True). That makes five hollow guards found across these two repos this run, and the first two found without mutation. Also flagged: #163's "inherits its attributes gate" wording and its planned test are now stale — that ticket is blocked by this one and needs amending before it is ground.

  21. blooop commented on Aug 9, 2026

    @blooop
    OwnerAuthor

    breadcrumb: merged as 39d4d2a under this run's merge-when-green-and-approved grant, after a final re-verdict of Approve with no blocking findings on either axis (report) — the third verdict on this node.

    This ticket took the longest road on the map and ended somewhere better than it started. The original gate asked "does this repo declare filter=lfs" and was wrong twice: pointers can exist with no attributes at all, and git honours attributes from outside the clone. The manager then pointed the retry at git check-attr as the oracle — and the builder disproved that too, against real git, before finding the predicate that actually holds: can any tracked file here be an unmaterialized pointer, enumerated over the true HEAD ∪ index union via git ls-files --with-tree=HEAD, one fork rather than two. The false "by construction" soundness proof that survived the second round is corrected in all four places it was written.

    Closing manually — the merge did not auto-close it. Unblocks #163 once #150 also lands; #163's body needs amending first, since its "inherits its attributes gate" wording is stale against a gate that no longer reads attributes at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions