# Row 118 (#1612) round 1: no auto-maintenance from the host's git in a role clone Brief: docs/plans/2026-10-14_clone-fetch-no-maintenance.md (blob a4f38943f). Sage's sage-0214 briefed the row, and sage-0215 granted the slot and accepted the plan below. BASE: 2727dd3ec. The candidate is build.patch against BASE, and candidate-manifest.sha256 lists its 3 files. `repro.sh` exports BASE with `git archive`, applies build.patch and checks every manifest digest: DIGESTS_OK. | File | Change | |---|---| | packages/seat/src/checkout.mjs | `SAFE` adds `-c maintenance.auto=false` and `-c gc.auto=0`, with a comment saying why. Nothing else in the file changes | | packages/seat/tests/checkout.test.mjs | two tests and two helpers (`loggedAll`, `loose`), and a `node:zlib` import. No existing test changes | | packages/seat/README.md | the forced-config paragraph names both keys and the reason | ## What git 2.55.0 does I probed this by hand before writing the tests (probe/probe.sh, probe/loose.mjs, output in probe/probe-run.log; not a suite). The probe makes a root and a clone with the host's `SAFE`, adds loose objects to the clone, commits to the root, then runs the host's fetch and `merge --ff-only` in the clone. Every git goes through a logger that also sees the gits git starts itself (below). The modes: | Mode | Extra keys | Loose objects in objects/17 | What ran after fetch and merge | objects/17 after | |---|---|---|---|---| | base 40 | none | 40 | `maintenance run --auto --quiet --detach` after the fetch and again after the merge; the detached run repacked and wrote a multi-pack-index | 0 loose, 2 packs | | base 0 | none | 0 | `maintenance run --auto` after each, which found nothing to do | 0, 0 packs | | maonly 40 | `maintenance.auto=false` | 40 | no maintenance | 40 loose | | gconly 40 | `gc.auto=0` | 40 | no maintenance | 40 loose | | keys 40 | both | 40 | no maintenance | 40 loose | So the merge starts maintenance as well as the fetch, and the detached run outlives the call. runGit's timeout signals the call's process group, and `--detach` leaves that group, so the host's time limit never reaches it. The clone itself starts no maintenance. Either key alone stops the start. The reason gc.auto=0 does is in git's run-command.c, `prepare_auto_maintenance` (tag v2.55.0): when `maintenance.auto` is unset, it reads `gc.auto` and enables auto-maintenance only if that is above 0. A clone can't set `maintenance.auto=true` in its own config, because allowed() keeps a clone with any key a clone doesn't write. Both keys still go on the command line, so the outcome doesn't rest on that fallback, and gc.auto=0 also stops a `gc --auto` that runs anyway. No third key is needed on git 2.55.0. ## Tests checkout.test.mjs, 77 of 77 passing on its own (checkout-test.log). 1. "every git call the host makes, for a new clone and an existing one, carries maintenance.auto=false and gc.auto=0". It uses the existing `logged()` wrapper, which logs every git the host starts. It runs a clone, then an update that fetches and fast-forwards, checks that a clone, a fetch and a merge each ran, and checks that every logged call has `-c maintenance.auto=false` and `-c gc.auto=0`. That covers the root reads as well as the calls in the clone. 2. "a clone with loose objects enough for gc --auto gets no maintenance or gc from the fetch and the merge". The clone's config can't turn maintenance on, so the trigger is the default config plus 40 valid loose blobs in objects/17 (`loose()`), over gc's default limit of 6700. `loggedAll()` puts a logger on PATH whose own `GIT_EXEC_PATH` is a copy of git's exec path with the logger as its `git`. git puts its exec path first on PATH for its children, so the log also shows the gits git starts. The test asserts that the log has such a child call, so it could show maintenance, that no call is `maintenance`, `gc` or `repack`, and that all 40 objects are still loose. The rigged-clone test ("a clone the session rigged is kept...") and the row 117 tests are unchanged and pass. The brief allows pinning the arguments where behaviour can't be observed. Here behaviour can be observed, but only when both keys are missing, because either one alone stops the start. So test 2 kills only the mutant that drops both keys, and test 1 kills each single-key mutant. Sage accepted this in sage-0215. ## Mutants mutants.py drops keys from `SAFE`, runs checkout.test.mjs, restores the file from `.orig` with a sha256 check, and then kills any process whose cwd is under this row's TMPDIR. mutants-run.log has the run and the failing tests. All 3 are killed, and the run ended `restored: True`. | Mutant | Change | Result | |---|---|---| | m1-drop-maintenance-auto | drop `-c maintenance.auto=false` | killed, 1 of 77 fail: test 1 | | m2-drop-gc-auto | drop `-c gc.auto=0` | killed, 1 of 77 fail: test 1 | | m3-drop-both | drop both | killed, 2 of 77 fail: tests 1 and 2 | Under m3, test 2's log shows `maintenance run --auto --quiet --detach` twice (fetch and merge) and the detached `repack` (mutants/mutant-m3-drop-both.log). I ran the mutants twice. The first run's kills were the same, but its failing-test extraction looked for TAP lines and the reporter prints spec lines, so it listed no test names (mutants-run1.log). I fixed the extraction and reran all three; mutants-run.log is the second run. ### Leftovers (sweep.log) No process was left: neither run's sweep killed anything, a /proc cwd scan under the TMPDIR found none, and no `maintenance run` process of mine was running afterwards. Two `mosaic-checkout-*` directories were left under the TMPDIR, one from each m3 run. Each held only `ws/stack/.git/objects/pack/tmp_rev_*`, so the detached repack wrote into the clone after the test had removed it. That is the defect this row fixes, and it appears only under the mutant. Three `mosaic-launch-*` directories, timed inside the gate's cli step, came from packages/cli's own tests and are unrelated to this change. sweep.log lists all five with their contents and mtimes, then records that I removed them and the TMPDIR was empty. A follow-up could have the cli tests clean up their launch directories; it is outside this row. ## Gate A targeted builder gate under #271 and decision 136 (gate.sh, logs in gate/). Sage's landing gate stays full. | Step | Result | |---|---| | packages/seat `node --test tests/` | 132 pass, 0 fail | | packages/cli `node --test tests/*.test.mjs` | 184 pass, 0 fail | GATE_PASSED (gate-run.log). cli/src/launcher.mjs is the one caller of ensureCheckouts. No scripts/test-*.sh names a changed path. webui/src/reads.mjs imports `readNotifyConfig` from cli.mjs, and cli.mjs imports launcher.mjs, so webui loads checkout.mjs but never calls ensureCheckouts. harness/src/bundle.mjs and git-tools.mjs name checkout.mjs only in comments. working.mjs imports `runGit`, `gitEnv` and `GIT_LIMITS` from checkout.mjs, not `SAFE`, and the seat step runs its tests. Sage's full gate covers webui and harness. No step failed, so the flake rule had nothing to count. ## working.mjs No change, and none needed in my reading. Its SAFE at line 76 is its own. All three of its fetches (lines 147, 192 and 200) already pass `--no-auto-maintenance`, and nothing else it runs (`init --bare`, `bundle`, `merge-base`, `update-ref`, `rev-parse`) starts auto-maintenance. My slot request said two fetches; Sage corrected it to three in sage-0215. ## Going live Nothing goes live in this row. The launcher runs from the pinned copy, so the change reaches launches only through a re-pin on Mos's go (decision 109), after Dewey's approval and Sage's landing.