Skip to content

feat(gates): fail the local preflight when the branch is behind its base #985

Description

@tvna

ACM: draft, not pre-verified -- any reader must independently re-check each row against this issue's own stated Facts before acting on it.

Facts

Verified live against origin/main at 836318d this session, not recalled.

Requested outcome

Implement the behind-base check the 17 retrospectives above have proposed and carried without ever building: a deterministic gate, discovered by gitapex_gate_local_preflight.py through its registry entry, that fetches the base branch and fails when the current branch is behind it.

Acceptance Criteria Map

Criterion Interpretation Planned ops Proof method Residual risk
Fail the local preflight when the branch is behind its base A new gate script computes git rev-list --left-right --count between the base ref and HEAD and exits non-zero when the behind count is greater than zero, printing the count and the concrete remedy (merge or rebase). Exit 0 when the count is zero. Base ref is origin/main, hardcoded, matching exception-handler-gap's own existing local_stdin convention rather than resolving a per-branch upstream -- main is the only base branch in this repository's flow Add .github/scripts/gitapex_gate_behind_base.py; register it in .gitapex/ssot.json with planes containing local and a local_invocation argv Run the runner on a branch deliberately left behind its base and confirm one FAIL naming the behind count; run it on an up-to-date branch and confirm PASS. Both as tests in tests/test_gitapex_gate_behind_base.py, not only by hand Hardcoding origin/main is wrong the day this repository grows a second long-lived base branch. Named rather than solved; the cost of getting it wrong is a false FAIL with an obvious message, not a silent pass
The gate fetches its own base ref before comparing Requester's recorded decision. The gate runs a git fetch for the base branch as its first step, so the comparison is always against real remote state rather than whatever the contributor last pulled. This is the first network call in an otherwise fully offline preflight runner, and that posture change is deliberate, not incidental Fetch the base ref inside the gate script before the rev-list call; document the posture change in the script docstring and in the runner's own docstring bullet that currently describes the origin/main staleness behavior, which this gate falsifies for its own case A test that stubs the fetch and asserts it ran before the comparison; a second that asserts a stale local ref still produces a correct verdict once the fetch has run Adds wall-clock to every push. Unquantified here -- measure it against the runner's existing sequential total (mypy-type-check and cyclomatic-complexity-floor already dominate) and report the real number in the PR rather than asserting it is negligible
A failed fetch never becomes a silent pass An offline machine, an unreachable remote, or an auth failure must not let the gate report PASS. It fails closed with a message naming the fetch failure as the reason, distinct from the message for a genuinely behind branch, so a contributor can tell "cannot check" from "you are behind". This is evaluating-deterministic-gate-quality dimension 15 applied to this gate's own dominant failure mode Non-zero exit plus a distinct message on fetch failure; no fallback to comparing against the local ref A test that simulates fetch failure and asserts a non-zero exit with the fetch-failure message, and specifically that the behind-count path is not reached An offline contributor is blocked at push time with no override. Whether that needs an escape hatch (an environment variable, a documented skip flag) is unknown, pending the requester's call once the FAIL has been seen in practice -- deliberately not designed in advance here
Plane registration is a stated decision, not a default local is required (that is where the runner discovers it). Whether a ci plane is added alongside is unknown, pending the implementing pass -- this would be the repository's first local-only gate, and GitHub already surfaces behind-ness on the PR itself, so a CI copy may be redundant rather than defensive Set planes in the registry entry, and state in the PR body which choice was made and why gitapex_scan_ssot_schema.py passes (it makes local_invocation required exactly when planes contains local, and local_exclusion required exactly when it does not); gitapex_gate_registry_wiring.py passes Choosing local-only sets a precedent for a plane combination nothing else uses. Low blast radius, but it should be an argued choice in the PR body rather than an unremarked first
The change satisfies this repository's own gate bar A new .github/scripts/gitapex_gate_*.py is itself a deterministic gate script, so the implementing PR owes the deterministic-gate-quality and checker-script-adversarial-review disclosures its own gates require, and the script must pass mypy --strict, ruff, and the per-file coverage floor Write the script, its tests, and the registry entry together; run gitapex_gate_skill_audit_disclosure.py --check-diff against the drafted PR body before opening Local preflight all wired gates PASS; pytest green; the skill-audit-disclosure CI check green on the PR A gate that ships fail-open in its own first version is the exact defect PR #651 shipped and issue #673 built a disclosure check for; the third ACM row above is where that risk concentrates

Constraints

  • Do not modify gitapex_gate_local_preflight.py's discovery logic. Wiring is a registry entry; that module deliberately contains no gate names.
  • Keep GitHub artifacts ASCII.
  • Cite this issue number in every commit.

Non-goals

  • Any of the other gates carried forward by the same 17 retrospectives (angle-bracket placeholder preflight, provenance-hook auto-classification, skill-audit-disclosure local-plane registration, markdownlint wiring, flaky-test detection). Each is separately proposed and stays separately tracked.
  • Closing the 17 retrospective issues. They are the evidence trail for this gate and for other unbuilt gates; whether any of them closes when this lands is the owner's call, not this issue's.
  • A behind-base check on the CI side, unless the plane decision in row four concludes otherwise.
  • Rebasing or merging on the contributor's behalf. The gate reports; the contributor acts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions