Expand description
Pushes: the automatic push after a commit, manual push handling, bounded Git queries, retries and progress watching. What: Pushes: the automatic push after a commit, manual push handling, bounded Git queries, retries and progress watching. Why: Push has a long lifecycle with many waits.
In TS you’d write (pseudocode):
// push/index.ts: one `export * from './file'` per module below.Modules§
- auto_
push - Post-commit auto-push of a landed commit: the push argv, the surfaced output and the failure reporting.
What:
The commit service’s push of a landed commit’s branch: which push runs, what its output shows,
and how a failure is reported without failing the commit.
Why:
Every commit in this repository is backed up (
AGENTS.mdrule APG), the service pushes the branch its record names and never readsHEAD, prints only GitHubremote:lines on success and the full output on failure, and leaves a notice for the next command when the push fails. The service is the only pusher of the repositories it serves and coalesces pending pushes of one branch into one call, so the push needs no lock. - auto_
push_ branch_ key - The per-branch file name of the push failure notice: a reversible, flat, case-safe key.
What:
The reversible, flat, case-insensitive-safe file name of one branch’s push files.
Why:
The failure notice of a push is kept per branch (
push_failure_notice), and the incumbent’s per-branch push files (src/auto-push-branch-key.ts) used the same names, so a name is the same in both wrappers. Uppercase letters are escaped too, so refs that differ only in case never share a file on a case-insensitive filesystem. - auto_
push_ destination - Where a plain
git pushof one named branch goes, from the configuration and the local refs. What: Where a plaingit pushof one named branch would send it, decided from the configuration and the local refs, or why Git would send it nowhere. Why: The service pushes the branch a record names, which may not be the branch checked out, so it must name both sides of the push itself; naming them from a partial reading of Git’s rules pushed to places a plaingit pushrefuses or never goes (amatchingpush recreating a branch the remote deleted, one of several upstream branches chosen at random, a destination that another configured refspec had already taken). Everything here followssetup_default_push_refspecsin Git’sbuiltin/push.candmatch_push_refsinremote.c; a setting Git refuses or a form this module does not interpret becomes a skip with a note that names the setting, never a push. - auto_
push_ dirty_ paths - The paths of a dirty worktree and of a fast-forward, and their overlap.
What:
The paths a dirty worktree has uncommitted work in, the paths a fast-forward would change, and the overlap
between them.
Why:
After an auto-push publishes a merge, local
maincan follow it even in a worktree that holds uncommitted work, provided the move changes none of the files that work touches. Another session’s staged renames then never keep this branch behind the remote. - auto_
push_ reconcile - Publishing a landed commit that a moved remote branch rejected: the merge is made from objects alone and pushed.
What:
Publishing a landed commit when the remote branch advanced after the commit landed:
the merge of the remote tip and the local tip is computed in the object store,
committed without a worktree or an index, and pushed with a plain push.
Why:
A rejected
pushof a branch that the remote moved cannot succeed again, and an ordinary merge in the active worktree can be blocked, or made unsafe, by another consumer’s uncommitted files.git merge-tree --write-treeandgit commit-treeread and write only objects, so the active worktree, its index and its untracked files are never touched while merging, and both histories stay reachable from the published tip. Only after the push succeeds is the branch fast-forwarded, and only where nothing can be lost: a dirty worktree follows too when the move changes none of the files its uncommitted work touches. An amended or rebased branch is merged like any other: nothing already published is lost, and the replaced commits stay in the remote history beside the new ones. - auto_
push_ single_ flight - The real Git of a push, its query runner and the failure types of a push. What: The service’s Git for a push: the real Git, its working directory and environment, the command builder, the deadline-bound read-only query and the failure types of a push. Why: The push, its reconcile and the queries before them all start Git the same way, so none re-enters the wrapper and every Git child receives the same environment.
- auto_
push_ trigger - The push a merge, pull, cherry-pick, revert or am hands to the service when it leaves the branch ahead of its upstream.
What:
Hands a push to the commit service after a command other than
git commitadded commits to the branch: a merge, a pull’s merge, a cherry-pick, a revert or an applied patch. Why: A cleangit mergeused to stay local until the next commit pushed it. The owner chose (2026-11-02) that any command adding commits queues the push as a commit does, when the branch tip moved and the branch is now ahead of its upstream and not diverged from it, and that a rebase does not, because a rebased branch would need a forced push. The check reads the branch, its upstream tracking ref and the commit parents from the files, with no Git process: the tip is read before and after the forwarded command, and the push is handed over only when the old tip and the upstream tip are both ancestors of the new tip. The old tip being an ancestor is what leaves a rebase (a rewritten branch) out,pull --rebaseandpull.rebaseincluded, without reading the configuration for it. - bounded_
push - The auto-push’s real
git pushwith a deadline and its low-speed abort. What: Run the auto-push’s realgit push(and its fetch and Git LFS upload) until it ends, or stops showing any sign of work with no connection to a remote open. Why: A push that shows nothing and holds no connection would otherwise hold the service’s push of that branch forever, and every later push of the repository would wait behind it. Such a push is stopped when its stall window passes, the commit stays saved locally, and the failure is reported like any other failed push. A push whose connection to the remote is open is never stopped for time (owner, 2026-11-03: “Wait while connected (Recommended)”; pick 5 ofdoc/planning/cli-git-shipped-limits-audit.md, the coordinator’s under the owner’s delegation and open to the owner’s veto): it has no ceiling, and the wrapper sets no low-speed abort for it. - bounded_
query - Read-only Git queries of the auto-push, each with a deadline and separate output streams.
What:
Run one read-only Git query that is ended only when it shows no sign of work, with separate output streams.
Why:
The auto-push asks Git a handful of questions before it pushes,
and a question that never returns would hold the service’s push for good.
A query that shows no sign of work for the quiet window is stopped and reported, so the commit stays saved. A query that
works is never stopped for being slow: no total time bounds it (owner, 2026-11-03: “Stop only on no progress”; the
coordinator’s scope addition to picks 4 and 9 of
doc/planning/cli-git-shipped-limits-audit.md, open to the owner’s veto). Before this, a query shared the push’s wall-clock stop and the journal showed agit statusstopped at 2026-11-03T08:28 withoutcome=retrying push_ms=97398. The longest quiet gap of every query is kept in the ledger. - child_
class - The classes of Git step a commit starts for itself, with the bound and the notice grace of each.
What:
The two kinds of Git step a commit starts for itself, and the quiet window and the notice grace of each.
A step is ended only when it has shown no sign of work for its window (
child_watch.rs); elapsed time alone never ends one (owner, 2026-11-03: “Stop only on no progress”; pick 4 ofdoc/planning/cli-git-shipped-limits-audit.md, the coordinator’s under the owner’s delegation and open to the owner’s veto). Why: One window for every step had to be long enough for the slowest, so a stalledrev-parseheld a commit for as long as a stalledpack-objects, and the first notice came after one second for steps that only needed a moment. The classes follow what a step touches, measured on the repository this crate lives in (doc/planning/cli-git-commit-timeouts.md): a query reads refs, commits and trees and ends in milliseconds whatever the worktree holds, while a step that writes objects, or that compares the index with the worktree, costs time in proportion to the bytes it reads (reset,update-index --refreshandls-files --modifiedtook 2.9 s on a stat-dirty 20 000-file index against 0.03 s on a clean one, and a cold disk multiplies that). The user’s owngit commitwith its hooks is not a class: its runtime belongs to the user and it is never bounded. - child_
watch - A watcher for waits that block on a child process: notices at the cadence and the child stopped at the bound.
What:
A watcher for a wait that blocks inside a read or a wait on a child process:
it prints a notice after the grace and at every interval while the wait lasts,
and ends the child when it has shown no sign of work for the quiet window.
Why:
The wrapper reads Git objects from a long-lived
git cat-file --batchand runs Git steps of a commit with their output captured. Both block in the operating system with nothing printed and no end, so a stalled child looks the same as a slow one and holds the commit for good (issue 1811). A watcher thread sees the wait from outside: the waiting thread marks where a wait begins and ends, the watcher prints, and when the child has shown no sign of work for the window it ends the child so the blocked call returns. Elapsed time never ends a child (owner, 2026-11-03: “Stop only on no progress”; pick 4 ofdoc/planning/cli-git-shipped-limits-audit.md, the coordinator’s under the owner’s delegation and open to the owner’s veto). The signs of work are those the push reads (push_progress.rs): bytes moved, processor time, a task able to run or waiting for the disk. A child that is slow and works is waited for however long it takes. The window is not measured: no log held a quiet gap before this change, so the class numbers stay as labelled unmeasured windows (child_class.rs), and the longest quiet gap of every watched step is kept for the timing records (quiet_ledger.rs) so a measured interval can replace them. A person’s hook is never ended: every Git the wrapper starts carriescore.hooksPathbelow/dev/nulland the hook switches (hooks_off.rs), so no hook of the person is ever below a watched child, and the stop signals the child and what it started, never a process beside it. - manual_
push_ cause - Why a manual push’s scope could not be established, as the cause its hint is chosen by.
What:
Why a manual push’s scope could not be established, as a cause the hint is chosen by.
Why:
Every such failure stops the push with a
content-unavailableengine failure, and that one code covers causes the person answers differently: a remote that moved is retried, a remote that could not be read is checked for network and credentials, and output the wrapper cannot read is a defect to report. A hint chosen by the code alone named missing content for all of them, which was true for one. The cause travels with the failure so the hint is chosen by it, never by matching the message text. - manual_
push_ lifecycle - The manual-push candidate scope of one push: the commits and trees the destination lacks.
What:
The manual-push candidate scope of one
git push: the commits and trees the destination lacks. Why: The manual-push policies (final-newline,forbidden-strings) scan the content a push publishes. The incumbent (src/policy-engine/manual-push-lifecycle.ts) discovers the updates, bounds each by what its destination already has, and hands the scan to the policy engine; a failure stops the push with exit2. - manual_
push_ porcelain - Authoritative manual-push updates discovered by a private dry-run push and confirmed with ls-remote.
What:
The parser of
git push --dry-run --porcelainoutput, which tells the manual-push probe what a push would send. Why: The probe used to learn the update lines from a recording hook written as shell text. Git’s own dry-run report carries the same facts without any program of ours: for each destination aTo <url>line, then one tab-separated line per ref (<flag>\t<from>:<to>\t<summary>), thenDone. Withcore.abbrev=nothe summary of an update holds the full old and new object names (old..new, orold...newwhen forced); a new ref, a deletion and a refusal hold a bracketed word instead. - manual_
push_ probe - The updates a manual
git pushwould send, learned from Git’s own dry-run report and confirmed against the remote. What: The authoritative ref updates agit pushwould send, discovered by a dry-run push whose report Git writes itself. Why: The manual-push policies must scan exactly what Git would publish, so the probe asks Git:git push --dry-run --porcelainreports each destination and ref update (manual_push_porcelain), the local objects come from that report or from onerev-parse, andgit ls-remote --refsconfirms each destination value. A remote ref that changed between negotiation and confirmation stops the probe. No program of ours is started for this, and no hook or shell text is written: an earlier version recorded Git’s update lines with a#!/bin/shhook (doc/planning/cli-git-no-self-invocation.md). Every Git child here is bounded by the query deadline, so a remote that never answers cannot hold the push. - manual_
push_ range - Published-set and range computation for manual-push scans: which pushed commits a remote does not have.
What:
The commits a push newly publishes, relative to what each destination remote already has.
Why:
The manual-push policies scan only what becomes public. A pushed commit is already published when a
remote-tracking ref of the destination, or an authoritative prior value of an update to that remote,
reaches it (
src/policy-engine/manual-push-published.ts). The rest of the history is the scan’s range. - push_
deadline_ advice - The words that say how to change the push deadline, for the terminal and for the resident commit service.
What:
The words that tell a person how to change the push deadline, for the program that is running the push.
Why:
A push in a terminal reads
CLI_GIT_PUSH_DEADLINE_SECONDSfrom that terminal’s environment, but the resident commit service pushes after the commit command returned and reads it from the environment the user manager gave the service when it started, so advice to set the variable is not something a person can act on there. In the service the advice names the user manager’s own command and the restart that makes the service read the new value. - push_
failure_ notice - The record of a failed backup push and the rule for when a wrapper command shows it.
What:
The record of a backup push that failed after its commit was saved, and the rule for when the wrapper shows it:
the resident service writes one record per failed push under the Git directory of the worktree whose commit it was, a
later wrapper command in that worktree shows each record once on standard error, and a later push of the same branch
that succeeds removes the records of that branch.
Why:
The push runs in the service after the commit command returned, so nothing is attached to the command that could
print its failure; without a record the person would never learn that their backup push was refused
(
doc/planning/cli-git-resident-service.md, “After-commit work”). Records are plain files in the worktree’s own Git directory so the check on every command is one directory read that finds nothing when nothing failed. A record holds the branch, the whole commit name and Git’s words with credentials already masked; the wrapper words the notice when it shows it, so the commit name is abbreviated the way Git abbreviates it in the repository as it is then. Records are shown on standard error only, and not at all to a command whose output a program reads (--porcelain,-z, plumbing), so that program neither sees the text in its output nor consumes the record before a person can read it. - push_
progress - The bytes a push and its processes move, sampled, and the clock that stops it on a stall.
What:
The progress of a push: whether the push and everything it started are at work, sampled at a small interval, and the
clock that stops it on a stall (no sign of work for the stall window, and no open connection to a remote).
Why:
A fixed deadline stops a slow push that is still working and lets a stuck push take its whole deadline. A process shows
that it works in three ways, each read from
/proc, and only a tree that shows none of them is stalled: bytes moved:rcharpluswcharof/proc/<pid>/io, summed over the tree; processor time:utimeplusstimeof/proc/<pid>/statgrowing; a task able to run or waiting for the disk: stateRorDin/proc/<pid>/task/<tid>/stat. Measured on 2026-11-03, which decided the three: a 30 MB push to a local bare remote moved 94 MB ofrcharand 156 MB ofwcharover its four processes whileread_byteswas 64 KB andwrite_bytescame late and in bursts, so the block-layer counters see neither pipes nor sockets and lag behind the transfer;rcharandwcharcountreadandwritecalls, notsendandrecv(102 bytes for 100000 bytes throughsend/recv, 100102 throughread), so a stretch that only curl performs shows through the pipe that feeds it; addof 64 MB withconv=fsyncwas in stateDin six of seven looks and used 0 processor ticks (a tick is 10 ms), so a wait for the disk shows in neither bytes nor processor time, only in the state; a parent invforkis in stateDwith 0 ticks, the way the controls hold a tree in disk wait exactly; a process reading a silent socket and asleepare in stateSwith a constant tick count, so a wait on a server that says nothing shows nothing in the process; only the connection’s state shows it. What each costs: bytes miss work that moves none; processor time makes a process that spins or polls look alive, and its resolution is a tick; stateDmakes a process wedged for ever in the kernel (a dead network file system) look alive, and stateRone that is only waiting for a processor; no total time bounds them (owner decision, see below). None of them sees a server that checks the push before it answers: that tree is in stateS, indistinguishable from a dead peer by its processes alone. What tells them apart is the connection: a process of the tree that holds a TCP socket in stateESTABLISHED(read from the kernel’s socket diagnostics,tcp_sockets.rs) has a remote that has not closed it, and a tree with such a socket is waited for, however long it shows nothing. The owner’s answer “Wait while connected (Recommended)” (2026-11-03) is the rule; the rest is pick 5 ofdoc/planning/cli-git-shipped-limits-audit.md, which the coordinator made under the owner’s delegation (“Pick options as if I had picked them”, 2026-11-03) and the owner may veto: no stall window for a connected push, no ceiling for any push, no low-speed abort. What the code cannot observe: a peer that vanished without a reset leaves the socketESTABLISHEDuntil TCP gives up (retransmission for unacknowledged data, keepalive probes for an idle connection), which the owner accepted. Where/proc/<pid>/iodoes not exist (no/proc, or a kernel without task I/O accounting) nothing is measured and the stall window is a fixed deadline, as before. - push_
retry - The service’s own retry of a push it stopped or that failed: the pauses, the waiting branches, the final failure.
What:
The service’s own retry of a push it stopped or that failed: the pause before each new try, the list of branches waiting
for one, and the test for the one failure no retry can mend.
Why:
The owner’s rule is that auto-push must land (
SPEC.md, “Merges and commits never fail”). A push stopped by the stall window, by the network, by an LFS server error or by a reference lock passes by itself, so the service tries again on its own, with pauses that grow, until the push lands or a conflict needs the person. Nothing waits for the next commit: the work loop is woken when the pause is over. The list is owned by the service’s work loop and kept on disk beside the instance’s journal (push_retry_store.rs), so a restarted service rebuilds it; the person’s failure record is not the plan, since the person’s next command shows it and removes it. A conflict, a login the service does not have and a hook on the server that declined the push stop the retry until the next commit (owner decision, 2026-11-03). - push_
retry_ store - The retry plan on disk, in the service instance’s journal, so that a restarted service keeps trying the pushes it was trying.
What:
The retry plan on disk: one small file per branch whose push waits for a new try, in a directory of the service instance’s own
journal, written when a try is planned, removed when the plan ends, and read when a service starts.
Why:
A service that is stopped (a release, a restart of the unit, a crash) must not forget the pushes it keeps trying: the defect
the retry mends is a stopped push that is never tried again, and a restart that forgot the plan would bring it back. The
person’s failure record cannot carry the plan: the person’s next command shows it and removes it. The journal is the
instance’s own and survives a restart (
%S/cli-git/service/%i), and one instance is the only writer of its plan, so no lock is taken. A plan is replaced by publishing the new file whole before the old one is removed, so a kill leaves one of them, never none; when a kill leaves both, the newer one is the plan. - push_
stacking - An explicit push that lost a race: run as a child, merge in what landed on the remote and publish the merge.
What:
An explicit
git pushthat lost a race: the commits that landed on the remote since the local base are merged in the object store, and the merge is published by the wrapper pushing it again through its own gates. Why: Several sessions push to one branch all day; a push rejected because the remote moved cannot succeed again, and every caller would fetch, merge or rebase and retry by hand while racing the others. The wrapper runs an eligible push as a child with the caller’s streams, so Git’s own output is unchanged. Only a failed push is looked at again, and only a divergence of the destination is acted on; an authentication, offline, hook or rule refusal keeps the exit status and the text Git gave. The merge is pushed through the same push planning, run inside this process (stacked_merge_push) withSTACKED_PUSH_MARKERset in the planned environment, so the policies that gate any push read the integrated result and the marker stops that push from stacking again. The wrapper never starts itself for this. A push that carries the Git LFS upload also runs here, as a child, so this parent can explain a failed upload. - push_
trigger_ log - The record of each decision of the push trigger: handed over, or declined with its reason.
What:
The record of every decision of the push trigger after a command that adds commits:
handed over,
or declined with the step that declined and why.
Why:
A merge that was never pushed left nothing to look at,
because the trigger returned silently at eight places
and the reason sat only in the code.
The record is one JSON line appended to a size-capped file in the common Git directory beside the commit timing records,
and a debug line under
MONOCHROMATIC_VERBOSE=true; the person’s terminal shows neither. - stacked_
merge_ push - The push of a merge that integrated a lost race, planned and run inside the running wrapper.
What:
The push of a merge that integrated a lost race, planned and run inside the running wrapper.
Why:
The merge push must pass every gate a typed
git pushpasses, so the policies read the integrated result. An earlier version started the wrapper’s own executable with a marker variable to get those gates; the wrapper never starts itself, so the same planning (plan_invocation) now runs in this process, with the marker set only in the environment handed to the planner, never in the process environment, and the planned action is run here. The marker stops that planned push from stacking again. - start_
retry - Starting a child again when the operating system refuses it for lack of processes, within a bounded wait.
What:
Starting a child process again when the operating system refuses it for lack of processes.
Why:
A host that has run out of processes (a process limit, a cgroup limit) refuses a start with
EAGAIN, which Rust reports asErrorKind::WouldBlock. A refused start started nothing, so starting again is always safe, and such a shortage usually ends within moments. Without this the first refusal ends a commit that the next attempt would have finished. The wait is bounded, so the wrapper still never stalls: a shortage that outlasts the budget fails with the same error as before plus how often the start was refused. - tcp_
sockets - The connected TCP sockets of the machine with their byte counts, and the sockets a process holds.
What:
The TCP sockets of this machine that are connected, with the bytes each has sent and had acknowledged and the bytes it has
received, and the sockets a process holds.
Why:
rcharandwcharof/proc/<pid>/iocountreadandwrite, notsendandrecv, so a transfer done throughlibcurl(git-remote-http) moves no byte the push meter can see, and/proc/net/tcphas queue lengths and no running count. The kernel’s own socket diagnostics (NETLINK_SOCK_DIAG,man 7 sock_diag, the interfacessreads) return, for every socket, its state, its inode and astruct tcp_infowithtcpi_bytes_ackedandtcpi_bytes_received; a socket a process holds is thesocket:[<inode>]link of its/proc/<pid>/fd(man 5 proc_pid_fd). - wait_
progress - The shared clock of every wait notice: the grace, the repeat interval and a queued commit’s place in line.
What:
The shared clock of every wait notice:
when the first line of a wait is due,
how often it repeats,
and the place in line a queued commit holds.
Why:
A commit that queues behind other commits, a lock, a reservation or a child process must say so,
and say it again at a steady interval,
or the caller cannot tell a queued commit from a hung one (issue 1811).
Each wait loop feeds its elapsed time to a
Tickerand prints a line when the ticker says one is due, so the decision is a function of elapsed time alone and a test can drive it with chosen durations. The interval comes fromCLI_GIT_WAIT_NOTICE_SECONDSat the production entry of each wait.