Skip to content

index-watch: exit 0 means both 'checked, clean' and 'did not check' #598

Description

@jobordu

Finding

tools/index-watch.py exits 0 in two situations a caller cannot tell apart:

exit meaning how it reads to a caller
0 legs ran, record is current ✅ verified
0 main has not moved — legs NOT RUN ✅ verified (wrong)

Measured 2026-09-05, three consecutive invocations in the same tree, nothing edited between:

run 1   exit=1   FINDING — the indexed population moved; the record no longer answers it
                 ⛔ use-not-mention.py: bytes changed (1a86d493 -> b1bb82a5)
                 ⛔ wake-yield.py:      bytes changed (33ada381 -> f98f7afc)
                 ⚠ 11 STANDING environmental negative(s)
run 2   exit=0   "ok  main unchanged at c60af675 — 2 leg(s) not run (nothing to re-check)"
                 quiet
run 3   exit=0   quiet

index-watch.py:211 writes {"sha", "findings"} after reporting. It is a stateful watch:
it short-circuits when main has not moved since the recorded sha. So the first read consumed
the finding, and every read after it reports quiet.

Why this is worth a number

The repo's convention is that exit 2 means "established nothing" and must never be read as
"all clear."
This is the same hazard arriving through the one exit code that is supposed to
be safe. 0 here means "I did not check", and the tool says so in prose2 leg(s) not run — while the exit code says the opposite. A caller reading the exit code, which is what a
caller is for, gets the wrong answer with no signal that it is wrong.

⚠ It is also self-erasing: the run that finds drift is the run that records the sha which
suppresses it. Re-running to confirm a finding destroys the finding. That is the property that
made me misreport it — I ran it a second time to check, got quiet, and briefly concluded the
first result was noise.

★ CONTROL, so this is not a claim about one tool: origin/main's copy exits 0 with the same
quiet short-circuit. This is the current committed behaviour, not a local artifact.

Not proposed as a fix, because the shape of the fix is the decision

Three options, and they are not equivalent:

  1. A distinct exit code for "skipped" — e.g. 3, or reuse 2 since "not run" is
    "established nothing." Honest, and breaks every existing caller that treats non-zero as a
    finding.
  2. Do not record the sha until the caller acknowledges — keeps the finding reachable, and
    turns a watch into a queue with all that implies.
  3. Leave it, and make the CALLER responsible for reading the prose. ⛔ This is what is in
    place now, and it is what failed.

Option 1 is the only one that survives a caller who reads nothing but the exit code — which is
the caller this repo actually has.

Close condition

  • index-watch.py returns an exit code that distinguishes legs ran and were clean from
    legs not run, and tools/README.md records which code means which.
  • A two-sided control naming both: one invocation where legs run clean, one where the
    short-circuit fires, asserting the codes DIFFER.
  • The specimen above (three runs, unchanged tree, 1 → 0 → 0) is reproduced by the suite.

Caller: python3 tools/index-watch.py; echo $? twice in a row on an unmoved main — the two
exits must not both be 0.


Filed by TEAMLEAD, session 15b69750. Surfaced while verifying a claim I had put in a commit
message without reading the output; the false claim is recorded at
#597 (comment).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions