What was measured
Scratch bare repo, a clone, and a second clone standing in for another filing host. Sequence, all of it production shape:
- the other host files
backlog/<slug>.mdand pushes; - this clone fetches and checks out that sha detached — the stub is in
the worktree, so
stub_backlog_path()finds it and the body reads fine; - the close writes
done/<slug>.mdand unlinks the backlog copy; Clone.publish()runsgit checkout --quiet master— and this clone'smasterpredates the filing commit, so the path is neither present nor tracked;git add -- devdocs/progress/backlog/<slug>.md devdocs/progress/done/<slug>.md→fatal: pathspec ... did not match any files, exit 128.
sh() raises, and nothing between close_stub_tickets and the cycle catches
it, so every other close queued in the same call is lost too.
Why it is prio 30 and not higher
It cannot produce a wrong verdict, and it is loud. It also needs a filer that
is not this host: publish() runs on essentially every cycle and leaves the
clone's branch at origin, so the window is small with one watcher. plexus and
seven are both RETIRED, so today there is exactly one. Re-rank this up if a
second watcher host is enrolled — the xeon enrolment plan would do it.
Fix shape, and why it is not patched here
git add failing on a pathspec that matches nothing is the correct behaviour
of the instrument; what is wrong is that publish() is handed a path its
branch cannot know about. Candidates, none obviously right:
- have
publish()drop paths that are neither present nor tracked, with a printed reason — cheap, but it makes a real staging bug silent, which is the thingpublish()'s current shape is deliberately loud about; - have
close_stub_tickets()refuse a stub whose path is absent from the branch tip and say so — narrower, one extra git call per close; - have the close pull before it decides, so the branch carries what it read.
That is a contract question about publish(), which every verdict goes
through. It wants deciding rather than guessing.
Repro
tools/twatch_close_stubs_devtest.py case 8 covers the sibling (duplicate)
bug. This one is NOT guarded — a guard would have to assert a raise, and the
fix will change what happens instead, so the guard belongs with the fix.