Skip to content

fix(pm): make remote reachability a measured precondition of the governed-merge sweep - #13421

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-13307-governed-merge-reachability
Aug 30, 2026
Merged

fix(pm): make remote reachability a measured precondition of the governed-merge sweep#13421
os-project-manager merged 3 commits into
mainfrom
claude/issue-13307-governed-merge-reachability

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #13307

The defect, and the precondition the card's repro needs

A governed repo whose checkout is present and healthy but whose remote is unreachable was audited from the local snapshot. Every clause of the row was literally true about that snapshot, and the conclusion a reader draws — no governed merge happened there in this window — was unsupported. It could never correct itself: the remedy the row prescribed, git fetch origin main, is exactly the command that fails.

⚠️ The card's headline artefact does not reproduce in a container that has no /home/user/cloud clone at all — there the row reads no git checkout, an honest refusal. Baseline measured at this branch's base, unmodified: exit 1, all four rows refusing (two on the #9902 shallow-history guard, two on absent checkouts). So the symptom was reproduced synthetically, against the unmodified script, with a checkout whose origin bare repo had been deleted:

✓ audited  objectstack-ai/cloud — tip d90d78760 @ 2026-08-30T11:01:37+00:00; 2 mainline
           commit(s) in window; window date; history complete clone
✅  clean window — no governed surface was merged in any governed repo.

exit 0, while git ls-remote against that same remote exits 128. The sweep did not merely call the repo quiet — it reported having scanned two commits and found the window clean, over a remote it never consulted.

The shape: a parallel leg in the same refusal register

The PM's premise note read the existing shallow-history refusal as the path to extend, flagged as a hypothesis from one run rather than from the code. Reading the code, the register is the right thing to reuse and historyHorizon is the wrong thing to extend — so this takes the parallel leg, as that note's Zone 2 allowed. Three reasons, in the order that decided it:

  1. The horizon guard is not asked of every repo. historyHorizon runs only where a DATE window is in force — a topological --since-ref window answers its own completeness question and skips it. Keyed onto that branch, reachability would be silently absent from exactly the invocation the card's own re-check command uses.
  2. historyHorizon lives in git-history.mjs, shared with other adopters precisely so the predicate cannot drift, and zero-network by construction.
  3. The questions fail independently. A complete clone can be a dead mirror; a fresh mirror can be too shallow.

What is reused is the register itself: status: 'unaudited' plus a stated reason, rendered ⚠️ UNAUDITED, counted by the #4690 note, classified INCOMPLETE by the existing exit contract. No second mechanism, no new exit code, no new report section, no new flag.

What it measures

One git ls-remote --exit-code per otherwise-auditable repo — git wire protocol, so the API budget is unchanged — asked before enumeration, in every window mode. It answers both halves at once:

reading verdict
remote unreachable UNAUDITED, reason opens NOT MEASURED
reached, names a different commit UNAUDITED — the mirror is behind
reached, tip matches audited, and the row says its zero is a MEASURED zero

⭐ The second row is the card's requirement 3 answered more sharply than it was asked. A tip date older than the window cannot separate a quiet repo from a dead mirror — which is why the old row could only hedge, "if that tip predates your last fetch, run git fetch". Tip identity separates them exactly, so that hedge is now a reading and the advisory is gone.

⛔ Not done, per the card's point 4 and the dispatch ruling: dropping cloud from the governed-repo list. That trades a loud hole for a silent one. Whether the repo is still in scope is a maintainer question, and until it is answered the audit's honest reading is NOT MEASURED.

⚠️ Stated rather than left to be discovered: the sweep now needs network reachability to each governed remote, and an offline run refuses every repo (exit 1). That is the intended reading, and there is deliberately no flag to suppress the probe — a flag that let rows read ✓ audited without it would reintroduce the false green.

Non-vacuity control — synthetic fixtures, five legs

⚠️ Synthetic fixtures, stated explicitly. None of the card's three named live fixtures can serve here: objectos has no checkout, objectui is behind the shallow refusal, and the stale cloud snapshot is absent. The fixtures are local bare repos whose paths end in the GitHub-shaped segments, so the governed slug parses while git ls-remote can actually reach them. ⛔ No --unshallow or --shallow-since was run against any shared checkout.

All five at head 47535e81:

leg invocation result
unreachable ⇒ NOT MEASURED, non-zero cloud (deleted bare) + objectui exit 2 — cloud UNAUDITED/NOT MEASURED, objectui ✓ audited
reachable + zero governed merges ⇒ true zero objectos, date window exit 0, ✅ clean window
same, topological window objectos, --since-ref exit 0, 0 mainline commit(s) in window … a MEASURED zero
reachable + a governed merge ⇒ reported objectui, AGENTS.md + skills/** commit exit 0, the entry is listed
reachable but mirror BEHIND remote objectos against an advanced bare exit 1, this mirror is BEHIND its remote naming both tips

The third leg matters twice: it is the true-zero requirement, and it is the proof that the reachability leg runs in topological mode, where the horizon guard is skipped entirely.

Ablations — both legs rebuilt, both mutations proven on disk, restore proven

Run from a committed state, mutated by an anchor-checked splicer that aborts on anything but exactly one match, restored by a trap pinned to an absolute path and HEAD (never a bare checkout, which reads the index). Each mutation proven on disk by a git hash-object comparison with both sides non-empty; each restore proven by the blob equalling the HEAD blob and git diff HEAD being empty.

  • A — verdict gutted to return null: self-test exit 1, 11 named failures across the pure verdicts, the report words, the live prober and the end-to-end sweep.
  • B — the sweep's call site removed, verdict left intact: self-test exit 1, exactly 1 failure, the end-to-end one.

⭐ Ablation B is the measurement that earned an extra pin. Every assertion on a pure verdict or on the prober stays green when main() simply stops consulting them — an instrument that cannot fail in the direction it exists to detect, which is this card's own subject one level down. So the call site is now pinned by running the sweep against the fixtures, both directions. That claim is written into the source as measured, and B is the measurement.

Predictions I got wrong, recorded

  1. The unmodified sweep would print 0 mainline commit(s) in window plus the stale-tip advisory. It printed 2 mainline commit(s) in window with no advisory — the seed commits were inside the window. The false green was stronger than predicted, not weaker.
  2. Ablation A would produce a clean list of named failures. It threw a TypeError at the third new assertion instead, because two assertions dereferenced .reason on a verdict whose job is to be non-null. Still red, but a self-test that dies at the first casualty reports neither green nor red for everything after it — the [finding] lint.yml's shallow-history step runs three self-tests under bash -e — a failure in the first silently prevents the other two from running at all #10814 collector lesson in a different hat. Fixed in the second test commit; A then produced the 11-failure list above.

A third correction came from a red run worth keeping: the end-to-end assertion first failed because the refusal's own reason quotes the string ✓ audited … 0 mainline commit(s) in window while explaining what it refuses to print, so a bare substring test read the explanation as the symptom. The row test is now line-anchored, matching the file's existing "assert on the tick, not the phrase" pin.

One wording fix came out of the fixtures too: git's remote failures end on boilerplate, and quoting the last stderr line put the fragment (and the repository exists.) inside a NOT MEASURED row. The reason now quotes the first line git marks as the fault.

Verification

  • pnpm check:pm-governed-merges (this script's own self-test, and its whole test suite): 228 assertions, exit 0 — up from 204 on the base file measured the same way.
  • Gate families derived on this final head via node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack at 47535e81, not from the dispatch brief. 17 green, including both convention-triggered obligations the path derivation cannot name and the brief did not carry — bare-root-worklist --self-test and check:pm-dispatch-gates, both incurred by editing a gate script.
  • node scripts/check-test-completeness.mjs exits 3, its own PREREQUISITE NOT MET — it grades a saved turbo test log that only CI produces. Not a finding, and not a measurement either way.
  • pnpm lint (repo-wide eslint . --no-inline-config): exit 0, 68 s, run under the shared verify lock at this head. No narrowing, so no narrowing argument is owed.
  • Control-character scan over the diff: no hits.

No changeset: the diff is exactly one file under scripts/pm/, which publishes nothing from any package — the skip-changeset closed list. The label is applied on this PR.


Generated by Claude Code

claude added 3 commits August 30, 2026 11:08
…rned-merge sweep

A governed repo whose checkout is present and healthy but whose remote is
unreachable was audited from the local snapshot and reported `0 mainline
commit(s) in window` — a false zero that could never advance, because the
remedy the row prescribed was the command that failed. One `git ls-remote`
per repo now answers reachability and mirror freshness together, feeding the
existing UNAUDITED refusal register rather than a second mechanism.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
…own call site

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 30, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 30, 2026 11:39
@os-project-manager
os-project-manager added this pull request to the merge queue Aug 30, 2026
Merged via the queue into main with commit a40ab8d Aug 30, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-13307-governed-merge-reachability branch August 30, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants