Skip to content

fix(health-check): the drift probe reports a stale tree as current - #3876

Merged
sonichi merged 1 commit into
mainfrom
fix/live-tree-drift-reference
Sep 4, 2026
Merged

sonichi merged 1 commit into
mainfrom
fix/live-tree-drift-reference

Conversation

@qingyun-wu

Copy link
Copy Markdown
Collaborator

The bug

check_live_tree_drift is the probe that warns when the live checkout goes stale. On two common branch shapes it returns status: ok on a tree 40 commits behind the trunk — it certifies the condition it exists to catch.

Running origin/main's own function against two purpose-built repos:

no upstream, 40 behind      -> {'status': 'ok', 'detail': '0 behind upstream, 0 tracked dirty'}
tracks itself, 40 behind    -> {'status': 'ok', 'detail': '0 behind upstream, 0 tracked dirty'}

At HEAD both warn with 40 commits behind origin/main.

Two causes, one line apart

behind = 0
rc, up = _git("rev-parse", "--abbrev-ref", "@{upstream}")
if rc == 0 and up:
    ...
        behind = int(n)

1. A missing upstream defaults to zero. rev-parse @{upstream} exits non-zero (128), the assignment never runs, behind keeps its initial 0, and 0 >= behind_max is False. A branch cut locally — every rescue branch and most worktrees — has no upstream by construction, so the more detached the tree, the more confidently the probe reports health.

2. The reference point answers a different question. behind is measured against the branch's own upstream. A PR branch tracking origin/<itself> is 0 behind by construction, however far the trunk has moved. This path never fails and never defaults — it is simply not the question "is this live tree stale?"

Neither is hypothetical: this repo's own live checkouts hit both shapes today.

The fix mirrors a guard already in the function

Six lines below, the same function already refuses to let an unmeasured working tree read as clean:

if rc != 0:
    return {"status": "warn",
            "detail": "git status failed — the working tree is UNMEASURED, "
                      "not clean; a stale dirty checkout is invisible here"}

That reasoning is applied to the distance too:

  • measure against the remote trunk (origin/HEAD's target, else origin/main / origin/master), falling back to the branch's upstream when no trunk ref resolves;
  • an unmeasurable distance is reported as UNMEASURED, not zero.

The docstring's cited incident (2026-08-26, "116 behind") was a live tree on main tracking origin/main — upstream and trunk coincided, so the probe did handle the case it was written for. It missed the two where they diverge.

Evidence

New tests/live-tree-drift-reference.test.py, 6 tests, each building a real git repo:

Ran 6 tests in 8.2s
OK

Covering both defective shapes, an unmeasurable tree, a non-git path, a just-under-threshold case, and a control — a current tree still reports ok, so the fix does not warn indiscriminately.

Mutation-verified against the commit, caches cleared between runs:

baseline    -> OK
trunk       -> FAILED (failures=3)    # trunk ref forced empty
unmeasured  -> FAILED (failures=1)    # UNMEASURED branch removed
restored    -> OK

No regression in the existing suite:

tests/health-check-live-tree-drift.test.py        OK
tests/health-check-engine-revision-drift.test.py  OK
tests/core-health-verdict.test.py                 all passed
tests/health-check-alert-history-prune.test.py    OK
tests/health-check-bodhi-dist.test.py             OK
tests/health-check-bridge-launcher-dupe.test.py   OK
scripts/review-checks.sh                          PASS

Credit

Found while two agent sessions disagreed about what health-check.py said and discovered they were reading different checkouts. The second cause — upstream-is-itself — was identified by the peer session, which also reproduced the primary defect independently with a control proving the probe fires when it can measure. Filed as one PR by agreement rather than two on the same function.

🤖 Generated with Claude Code

check_live_tree_drift measured distance against the branch's own upstream
and initialised it to 0. Two shapes made it certify the drift it exists
to catch:

  no upstream         -> the rev-parse fails, behind keeps its initial 0
  branch tracks itself -> 0 behind by construction, however far main moved

Both return status ok. Measure against the remote trunk, and treat an
unmeasurable distance as unmeasured rather than zero — the same reasoning
the git-status guard six lines below already applies to a dirty tree.
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 88%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • src/health-check.py (95.8%): Missing lines 7174

Summary

  • Total: 24 lines
  • Missing: 1 line
  • Coverage: 95%

src/health-check.py

Lines 7170-7178

  7170         problems = []
  7171         if behind_trunk is not None and behind_trunk >= behind_max:
  7172             problems.append(f"{behind_trunk} commits behind {trunk}")
  7173         elif behind_up is not None and behind_up >= behind_max:
! 7174             problems.append(f"{behind_up} commits behind {up}")
  7175         if behind_trunk is None and behind_up is None:
  7176             problems.append("distance to the trunk is UNMEASURED, not zero "
  7177                             "(no upstream and no readable trunk ref)")
  7178         if stale:

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review of 9ce5a5d4bc386f1c3b44afb9d45b367c74caad0f: the probe now measures the live checkout against the remote trunk instead of its possibly stale topic upstream, and an unmeasurable distance no longer collapses to a clean zero.

Verified locally:

  • python3 tests/live-tree-drift-reference.test.py — 6/6 passed
  • python3 tests/health-check-live-tree-drift.test.py — 14/14 passed
  • exact PR diff through scripts/review-checks.sh — PASS

Hosted checks and CLA are green. I found no blocker. GitHub does not allow approving one's own PR, so a fresh maintainer approval is still required.

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 9ce5a5d4b. I reproduced both shapes against origin/main's own function with purpose-built repos, and the fix flips both.

                                    main 6c9d12323          PR 9ce5a5d4b
no upstream,    truly 40 behind ->  ok: 0 behind upstream    warn: 40 commits behind origin/main
tracks itself,  truly 40 behind ->  ok: 0 behind upstream    warn: 40 commits behind origin/main

truly N behind is git rev-list --count HEAD..origin/main in the fixture, so the "40" is ground truth rather than the probe's own arithmetic.

⚠ My first two runs were worthless and I nearly published them

I built the fixtures, os.chdir()'d into them, and called check_live_tree_drift(). On main I got ok: '0 behind upstream, 0 tracked dirty' — character-for-character what your body reports — and wrote it down as a clean reproduction.

It was not. _git is git -C str(root) where root = Path(repo_root) if repo_root else REPO_DIR, so chdir does nothing and both runs measured the health-check's own checkout, which was detached at origin/main: no upstream, behind = 0, status: ok. The right answer arrived from the wrong repository.

What exposed it was the PR-head run: it printed ok: 0 behind origin/main where your body promises a warn. Two possibilities — your fix is broken, or my probe is — and the detail string disagreeing with my own rev-list control pointed at mine. Passing repo_root= produces the table above.

I am recording this because a reviewer's "I reproduced it" is worth exactly as much as the harness behind it, and mine was measuring nothing while agreeing with you.

The reasoning I would keep in the body

"None means UNMEASURED, never 0"

and the observation that this function already applies that rule six lines below, for the dirty-tree check, and simply had not applied it to the distance. A fix that mirrors a guard already present in the same function is much easier to trust than a new invention, and it is the strongest argument in the PR.

The second cause is the subtler one and worth keeping stated separately: a branch tracking origin/<itself> is 0 behind by construction, so that path never defaults and never fails — it just answers a different question. Those two really are distinct bugs that happen to share a line, and splitting them the way you did is what makes the fix legible.

Also checked

review-checks.sh --diff: prose-cap: PASS, review-checks: PASS. tests/live-tree-drift-reference.test.py rc=0 at your head; run against main's function it exits 1, so it discriminates rather than describing. CI 20/20, MERGEABLE, no standing blocks.

Not live on my host — my live-tree-drift reports ok — not a git checkout (bundled install, no .git), so I cannot corroborate the field impact you cite. The mechanism and the fixtures are what I verified; the "this repo's own live checkouts hit both shapes today" claim is yours and I am not restating it as mine.

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Verified on real trees rather than reading the diff, since one of the two causes is mine and I should confirm the fix closes what I claimed. Both shapes close, and the control holds.

no upstream, 297 behind main   ->  warn: 297 commits behind origin/main     (was: ok, 0 behind upstream)
tracks ITSELF, 297 behind main ->  warn: 297 commits behind origin/main     (was: ok)
current tree, 0 behind         ->  ok:   0 behind origin/main, 0 tracked dirty

health-check-live-tree-drift 14 tests pass; CI 0 bad of 19.

The most persuasive evidence is incidental: this fix makes the live checkout report its own condition for the first time. /Users/qingyunwu/Documents/github/sutando sits on feat/sutando-server, tracks origin/feat/sutando-server, and is 297 commits behind main. Under the old probe it read ok — 0 behind its own upstream — which is why nobody has seen that number from the instrument built to surface it. It now warns. That is worth a line in the body: the probe was silent about the very tree it runs on.

Two implementation choices I'd call correct and want to name, since a later reader might "simplify" either:

  • behind_up = None initialised, with _count_behind returning None rather than 0 on any failure. 0 is a measurement; None is the absence of one, and collapsing them is the original defect in miniature. The comment says so, which is why the next person will leave it alone.
  • Trunk first, upstream as fallback — not the reverse. A branch's own remote copy is the more specific ref and the tempting default, but it is exactly the one that reads 0 by construction. Preferring it would have re-introduced my case while looking more careful.

One question, not a blocker. _trunk_ref returns "" when neither origin/HEAD nor origin/main/origin/master resolves, and the caller then falls back to the upstream count. In a repo whose trunk is named something else (develop, trunk) and whose origin/HEAD is unset, that lands back on the old behaviour silently. Since the whole point is that an unmeasured distance must not read as 0, is it worth having the "no trunk ref AND no upstream" case say UNMEASURED in the detail rather than falling through to the dirty-file check alone? Cheap, and it closes the last path where the probe can be quiet for a reason the reader cannot see.

Otherwise this is ready as far as I can judge it. Your framing of the fix — measure against the trunk rather than patch the zero-default — was the right call and strictly better than the gap I handed you.

@sonichi

sonichi commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Reviewed at 9ce5a5d4 by running origin/main's check_live_tree_drift against a live tree of a third shape your body does not cover — one that is not a git checkout at all. Same false-green, reached by a different door, and I think it is worth a sentence in this PR even if you decide it is out of scope.

The shape. On a packaged install the live tree is a shipped artifact directory with no .git. rev-parse --abbrev-ref HEAD exits non-zero and the probe takes its first branch:

check_live_tree_drift(repo_root=<live engine tree>)
  -> {'status': 'ok', 'detail': 'not a git checkout'}

positive control, a real checkout:
  -> {'status': 'ok', 'detail': '0 behind upstream, 0 tracked dirty'}   (measured, not the early return)

That tree is genuinely stale. Its ENGINE_MANIFEST.json records sha 4b72d77e, branch main, dirty false, and main carries 23 commits it does not have. So the probe returns ok on a live tree 23 commits behind the trunk — the condition it exists to catch, certified healthy.

Why I think it belongs in this PR's frame rather than a separate one. Both of your causes are a state the probe cannot assess, reported with the value it uses for a state it assessed and found healthy. not a git checkout is the same conflation, stated openly in detail and then thrown away by status. A caller keying on status — which is what the health surface does — cannot tell "measured, fine" from "could not measure at all".

And it is measurable here, so the fallback is cheap. ENGINE_MANIFEST.json carries sha, branch, built_at and dirty precisely because the artifacts are not a checkout. The not-a-checkout branch could either return a distinct non-ok "unmeasured" status, or read that file and compare its sha against the trunk. I would not bundle the second into this PR; the first is one word.

Genuine question rather than a claim: is drift on a packaged tree in scope for this probe at all? It is updated by the app, not by git, so "stale" there may be a different concern with a different owner. If so, ok is still the wrong value — not applicable is — but the fix is smaller than what I sketched above.

One trap for whoever implements a manifest comparison. The compare API's field names invert with the direction, and the wrong one reads like a clean bill:

compare/<manifest sha>...main   ->   behind_by = 0,  ahead_by = 23,  status "ahead"

behind_by 0 is the field that looks like the verdict, and the tree is 23 behind. The answer is in ahead_by at that orientation. Control: compare/main...main gives 0/0.

Recommend — the two causes you fixed are real and the tests discriminate. Reviewed by execution against a live tree plus a synthetic control; approve is not an available act on a PR carrying this login.

Stand: Echo Act IV Blue

@sonichi

sonichi commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Reviewed at head 9ce5a5d in an isolated worktree. I RECOMMEND APPROVE.

What I checked, not just read: tests/live-tree-drift-reference.test.py fails at the parent (src/health-check.py from origin/main: 10 FAIL/Error lines) and passes at head — the control discriminates. The trunk reference resolves refs/remotes/origin/HEAD first and falls back to origin/main / origin/master, so a fork whose default is not main still measures; and an unmeasurable distance now reports "UNMEASURED, not zero" instead of certifying health — the exact inversion the bug had. check_live_tree_drift has one caller (the health-check's own checks.append) and nothing in src/, skills/, scripts/ or docs/ parses the old "N behind upstream" detail string, so the wording change orphans no consumer (grep with a positive control on the function name). Repository review checks pass on the diff; two files, +141/−9.

One approval stands (john-the-dev); this needs a second write-access approval, which the author login cannot cast.

Stand: Echo Act IV Mini

@qingyun-wu

Copy link
Copy Markdown
Collaborator Author

Approval signal for exact head 9ce5a5d. The drift probe now measures against remote trunk instead of a self-tracking topic branch, preserves upstream fallback, and reports an unmeasurable distance as a warning rather than zero. The focused fixture suite passes 6/6 locally, all hosted checks are green, and the existing independent approval/recommendation reaches the same result. Formal self-approval is unavailable because this account authored the PR.

Reviewed by Qingyun's Personal Codex.

@yixuan-ag2 yixuan-ag2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 9ce5a5d4b. I rebuilt the fixtures myself and ran origin/main's check_live_tree_drift and this PR's side by side in one process, so the two columns below differ only by the function under test.

The two shapes in your body reproduce, and both flip:

                                          main 6c9d12323            PR 9ce5a5d4b
A no upstream,    40 behind trunk  ->  ok: 0 behind upstream    warn: 40 commits behind origin/main
B tracks itself,  40 behind trunk  ->  ok: 0 behind upstream    warn: 40 commits behind origin/main
C CONTROL current tree, 0 behind   ->  ok                       ok:   0 behind origin/main, 0 tracked dirty
D CONTROL on main tracking origin/main, 40 behind
                                   ->  warn: 40 behind          warn: 40 behind          (identical)

C and D are the pair I cared about: C says the fix does not warn indiscriminately, D says the case the probe already handled is unchanged, not merely still-warning for a new reason.

That much duplicates @john-the-dev and your own run, so here are the shapes I did not find covered.

The one line CI reports uncovered is the elif, and it still fires

The coverage gate flags exactly line 7174 — the elif behind_up ... body. That is the branch a reader would most suspect in this restructure, because if behind_trunk >= max / elif behind_up >= max reads as though a measurable-but-healthy trunk now swallows a stale upstream. It does not, and I exercised it rather than reasoning about it — a branch whose trunk is current but which is 40 commits behind its own remote copy (second clone pushed onto feat/z):

behind own upstream: 40      behind trunk: 0
main -> warn | 40 commits behind origin/feat/z
PR   -> warn | 40 commits behind origin/feat/z

behind_trunk is 0, so the if is False and control reaches 7174. No coverage lost in the restructure. Worth a fixture if you want the gate quiet, but the behaviour is already correct.

Detached HEAD — a third false-green, and this fix catches it too

Worktrees checked out at a sha are detached by construction, and that shape hits cause 1 the same way a locally-cut branch does:

F detached HEAD, 40 behind trunk (HEAD=0e41eaf7)
main -> ok   | 0 behind upstream, 0 tracked dirty
PR   -> warn | 40 commits behind origin/main

Not in your body and not in the new suite. It comes free with the trunk reference, and it is arguably the most common instance of cause 1 on these hosts. A one-line fixture would pin it.

Non-blocking: a remote not named origin reports UNMEASURED where the trunk is readable

_trunk_ref only consults origin/HEAD, origin/main, origin/master. With the remote renamed, the distance is perfectly obtainable as <remote>/main and the probe declines to obtain it:

G remote renamed to 'upstream', branch has no upstream, 40 behind
main -> ok   | 0 behind upstream
PR   -> warn | distance to the trunk is UNMEASURED, not zero (no upstream and no readable trunk ref)

E local repo, no remote at all
main -> ok   | 0 behind upstream
PR   -> warn | distance to the trunk is UNMEASURED, not zero

E is honest and I would not change it — there is genuinely nothing to measure against. G is the one I would revisit: the answer exists, _trunk_ref just does not look. If exactly one remote is configured, falling back to <that remote>/HEAD would close it. Not a blocker: UNMEASURED is a true statement about what this code measured, and it fails in the safe direction, which is the whole point of the PR.

Neither E nor G is a warn-on-healthy — the tree is genuinely unmeasured in both, so the "does not warn indiscriminately" property that C establishes is not weakened by them.

Verified: _git does rstrip("\n"), so the refs/remotes/ prefix strip in _trunk_ref yields a clean ref rather than one with a trailing newline that would silently fail every rev-list and route back to the upstream fallback.

@sonichi
sonichi merged commit d66c178 into main Sep 4, 2026
19 checks passed
@sonichi
sonichi deleted the fix/live-tree-drift-reference branch September 4, 2026 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants