Files
Bo 5644f2ba71 fix(gates): fail closed on ratchet collection failure and pin first-parent head scope
Repairs the two fresh-Sol ADR-0016 review findings. Turns the RED witnesses
recorded in the previous commit green without touching the governed regex, the
growth guard, the allowlist, or scripts/.skill-python-grandfather.

1. check-skill-python-ratchet.sh no longer swallows the collector contract.

scripts/lib/ratchet.sh gives every chosen collection command an explicit
"|| return 2" because the gate runs in a shell with no ambient strict mode. The
caller then discarded exactly that: ratchet_changed_files_status ... 2>/dev/null
|| true turned a dead Git read into an EMPTY changed set, the governed loop ran
zero times, and the gate printed its green banner over a diff nobody read.
Measured before this change, with a diff-tree shim over a tree carrying a real
unpinned violation: "PASS: skill-python ratchet" / exit 0. The rc is now
preserved and the gate exits 2, with stderr no longer sent to /dev/null so the
library's own reason survives.

2. head scope now HAS the first-parent semantics it declared.

git diff-tree -m --first-parent does not mean "diff only against parent 1":
-m splits a merge into one diff block PER PARENT and emits them all, while
--first-parent constrains history traversal rather than which parent blocks
print. The parent-2 block lists every path already settled on the first-parent
line, so a merge commit re-attributed long-landed files to itself. Sol measured
this on the live integration lineage: at 88c2692f2 the old form emitted 14
governed rows, 12 from the second-parent block, none introduced by that merge.

Head collection becomes one owner, ratchet__head_diff, doing an explicit
two-tree HEAD^1..HEAD diff, with the --root empty-tree diff kept for root
commits (HEAD^1 does not resolve there, and a root commit's contents are exactly
what it introduced). An unreadable parent list fails closed at rc 2 rather than
returning an empty set. The helper replaces all SIX open-coded copies of the old
command -- head scope plus the upstream-without-upstream and auto fallbacks, in
both the name-only and name-status collectors -- so the same defect cannot
survive in a path nobody looked at. diff-tree is retained (not git diff) to
preserve the existing rename-detection posture; only ref selection changes.

The lib header no longer claims -m --first-parent means first-parent.

Verification:
  - tests/scripts/check-skill-python-ratchet.bats  15/15 (N7, N8 were RED)
  - tests/scripts/ratchet-lib.bats                 41/41 (merge case was RED)
  - all six other ratchet-lib consumer suites: unchanged from base
  - go test ./internal/gates/...                   ok (negative-witness closure)
  - shellcheck -S warning on both changed scripts: clean
  - git diff --check: clean
  - live gate sweep over all eight consumers, run identically at the unmodified
    base 0ba51a5f6 and here: byte-identical results. The one pre-existing
    failure (check-atomic-write-ratchet stale-entry rc=1, and its bats case 13)
    reproduces at base with real history and is NOT caused by this change.

Refs: /tmp/agentops-adr0016-go-promotion-review-sol.md (REQUEST_CHANGES,
"Two ratchet corrections are still required"); ADR-0016 section 3.
2026-07-27 23:52:16 -04:00
..