Files
stack/agents/darkwing/work/queue-118/r1/build.md
T
jason.woltjeandClaude Opus 5.5 5a68df7275 agents: darkwing row 118 round 1 packet (#1612)
SAFE in packages/seat/src/checkout.mjs adds maintenance.auto=false and
gc.auto=0; probe, tests, mutants m1-m3 (all killed), targeted gate
(seat 132/0, cli 184/0) and the leftover sweep. Candidate is build.patch
against 2727dd3ec; manifest digests check out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 14:41:09 -05:00

7.5 KiB

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.