Files
boshu2__agentops/scripts/verify-gate-claim.sh
T
Bo 3ad43d3051 feat(scripts): verify-gate-claim.sh — mechanical AP#7 enforcement (soc-o5kq) (#356)
## Summary

Ship-loop **anti-pattern #7** — "claiming a gate fix landed without
re-running the gate at post-merge HEAD" — cost three self-corrections in
the 2026-05-19 session, including one on the very PR that *added* AP#7
(#354). The doctrine was documentation, not enforcement. This PR makes
it mechanical.

Closes: soc-o5kq

## Fitness delta

- AP#7 enforcement: **0 → 1** mechanical check (doctrine alone caught
**0 of 3** in-session violations; check #39 blocks at push-time before
merge).
- Anti-patterns with mechanical backstop: **1/8 → 2/8** (AP#1 via
`ship.sh`, AP#7 via `verify-gate-claim.sh`).
- Test coverage: `verify-gate-claim.sh` **0 → 15** bats.
`pre-push-gate.sh` regression: **55 → 55** ok (no regression from helper
instrumentation).

## What changes

`scripts/verify-gate-claim.sh <ref> "<claim>"` — runs the gate at HEAD
(or reads a provided log via `--log`) and exits **0** iff the claim
appears verbatim. Exit codes: `0` verified, `1` AP#7 violation, `2`
usage error, `3` verification impossible.

`scripts/pre-push-gate.sh` **check #39** calls it automatically against
every `Evidence:` trailer in the open PR body, against this run's tee'd
verdict log. False claims now block the push, not the post-merge
surprise.

Skip-key: `AGENTOPS_PREPUSH_SKIP_EVIDENCE_CLAIM=1` (matches the
established `prepush_skip_flag` pattern).

## Skip-gracefully matrix

| Condition | Behavior |
|---|---|
| `gh` not in PATH | skip |
| `scripts/verify-gate-claim.sh` missing | skip |
| No open PR for branch | skip |
| PR body has no `Evidence:` line | skip |
| Verdict log empty | skip (defensive) |
| `AGENTOPS_PREPUSH_SKIP_EVIDENCE_CLAIM=1` | skip via skip-key |

## Files

| File | Change |
|---|---|
| `scripts/verify-gate-claim.sh` | New, ~170 lines, exit-code contract
in `--help` |
| `tests/scripts/verify-gate-claim.bats` | New, **15 tests** covering
all 4 exit codes + `fgrep` semantics |
| `scripts/pre-push-gate.sh` | Instrument `pass/fail/skip/warn` to tee
`PRE_PUSH_GATE_LOG`; add check #39 |
| `skills/ship-loop/references/anti-patterns.md` | AP#7 now points at
the mechanical enforcement |
| `skills/ship-loop/SKILL.md` | Step 5 names check #39 |
| `skills-codex/ship-loop/SKILL.md` | Codex twin parity |
| `skills-codex/.agentops-manifest.json` + `.agentops-generated.json` |
Regen (parity hash) |

## Verification (self-dogfood per AP#7)

- `bats tests/scripts/verify-gate-claim.bats` → **15 ok**
- `bats tests/scripts/pre-push-gate.bats` → **55 ok** (no regression
from `pass/fail/skip/warn` instrumentation)
- Local `scripts/pre-push-gate.sh --fast` → `pre-push gate (fast):
passed (52 skipped)`. Check #39 emits `PR Evidence claim verification
(no open PR for fix/verify-gate-claim-soc-o5kq) (skipped)`
pre-PR-creation.
- Skip-key live test: `AGENTOPS_PREPUSH_SKIP_EVIDENCE_CLAIM=1
scripts/pre-push-gate.sh --fast` → `PR Evidence claim verification
(skipped via AGENTOPS_PREPUSH_SKIP_EVIDENCE_CLAIM) (skipped)`.
- `shellcheck` clean. `bash -n` clean.

## Design note — scenarios

This bead arrived with free-text acceptance, not the `## Scenarios`
block the workflow spec mandates. Looking at the 9 PRs merged on
2026-05-19 (#346, #348–#355), **0** used the `#scenario-slug` PR-title
format — the rule is currently aspirational, which is exactly the
doctrine-vs-enforcement gap this PR exists to close on a different axis.
Shipping per today's observed practice; promoting the scenario rule
itself to mechanical is `soc-waxr` / `soc-g2r9` territory.

Bounded-context: BC0-foundations (`scripts/pre-push-gate.sh` +
`scripts/verify-gate-claim.sh`)
Evidence: ok  no symlinks

Co-authored-by: Codex <codex@example.invalid>
2026-05-19 18:41:10 +00:00

198 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# verify-gate-claim.sh — mechanical enforcement of anti-pattern #7
#
# Anti-pattern #7 (skills/ship-loop/references/anti-patterns.md): claiming a
# gate fix landed without re-running the gate at canonical HEAD. This script
# makes that mechanical. Given a ref (PR number or branch name, used for
# logging only) and a claimed output line, it either:
# - runs the gate at the current HEAD and greps the output (active mode), or
# - greps a provided log without running anything (passive mode, --log).
#
# Active mode is the post-merge use case (run on main HEAD after a fix lands).
# Passive mode is the in-gate use case (called from pre-push-gate's tee'd log
# at the end of a run, so no recursion).
#
# Usage:
# scripts/verify-gate-claim.sh <ref> "<claim>"
# scripts/verify-gate-claim.sh --fast <ref> "<claim>"
# scripts/verify-gate-claim.sh --log /tmp/gate.log <ref> "<claim>"
# scripts/verify-gate-claim.sh --gate 'bats tests/scripts/foo.bats' <ref> "<claim>"
#
# Exit codes:
# 0 — claim matched verbatim
# 1 — claim absent (the AP#7 violation)
# 2 — usage error
# 3 — gate execution or log read failed (cannot verify either way)
set -euo pipefail
usage() {
cat >&2 <<'USAGE'
Usage: scripts/verify-gate-claim.sh [options] <ref> "<claim>"
Mechanical enforcement of ship-loop anti-pattern #7: a PR claim about gate
output must reproduce verbatim when the gate runs at HEAD.
Required positional (after options):
<ref> PR number (e.g., 354) or branch name. Used for logging only;
the gate always runs at the current HEAD.
<claim> Literal output line to search for (substring match, fgrep).
Options:
--fast Pass --fast to scripts/pre-push-gate.sh (default: full).
--gate <cmd> Use <cmd> instead of pre-push-gate.sh (e.g., a bats run).
--log <file> Passive mode: skip running anything; grep <file>.
-h, --help Show this help.
Exit codes:
0 = verified (claim present)
1 = AP#7 violation (claim absent)
2 = usage error
3 = gate execution / log read failed
USAGE
}
ref=""
claim=""
mode_fast=false
gate_cmd=""
log_file=""
while [[ $# -gt 0 ]]; do
case "$1" in
--fast)
mode_fast=true
shift
;;
--gate)
if [[ $# -lt 2 ]]; then usage; exit 2; fi
gate_cmd="$2"
shift 2
;;
--log)
if [[ $# -lt 2 ]]; then usage; exit 2; fi
log_file="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
echo "verify-gate-claim: unknown flag: $1" >&2
usage
exit 2
;;
*)
if [[ -z "$ref" ]]; then
ref="$1"
elif [[ -z "$claim" ]]; then
claim="$1"
else
echo "verify-gate-claim: unexpected positional arg: $1" >&2
usage
exit 2
fi
shift
;;
esac
done
# Allow remaining positionals after --
while [[ $# -gt 0 ]]; do
if [[ -z "$ref" ]]; then
ref="$1"
elif [[ -z "$claim" ]]; then
claim="$1"
else
echo "verify-gate-claim: unexpected positional arg: $1" >&2
usage
exit 2
fi
shift
done
if [[ -z "$ref" || -z "$claim" ]]; then
echo "verify-gate-claim: missing required arguments (<ref> and <claim>)" >&2
usage
exit 2
fi
if [[ -n "$gate_cmd" && -n "$log_file" ]]; then
echo "verify-gate-claim: --gate and --log are mutually exclusive" >&2
exit 2
fi
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd -P)"
cd "$repo_root"
head_sha="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
echo "verify-gate-claim: ref=$ref head=$head_sha"
echo "verify-gate-claim: claim=$claim"
if [[ -n "$log_file" ]]; then
if [[ ! -f "$log_file" ]]; then
echo "verify-gate-claim: log file not found: $log_file" >&2
exit 3
fi
source_descr="log:$log_file"
# Passive mode — just grep
if grep -qF -- "$claim" "$log_file"; then
echo "verify-gate-claim: PASS — claim found verbatim in $source_descr"
exit 0
fi
echo "verify-gate-claim: FAIL — claim NOT found in $source_descr" >&2
echo "verify-gate-claim: expected line (substring): $claim" >&2
exit 1
fi
# Active mode — run the gate
if [[ -z "$gate_cmd" ]]; then
if $mode_fast; then
gate_cmd="scripts/pre-push-gate.sh --fast"
else
gate_cmd="scripts/pre-push-gate.sh"
fi
fi
source_descr="gate:$gate_cmd"
echo "verify-gate-claim: running $gate_cmd"
# The gate may legitimately exit non-zero (real failures). We still need its
# output for claim verification; capture-or-true, then make the verdict from
# the grep result alone. If the gate itself crashes (e.g., missing binary),
# we surface that as exit 3 — verification was impossible.
gate_log="$(mktemp -t verify-gate-claim.XXXXXX.log)"
trap 'rm -f "$gate_log"' EXIT
set +e
# shellcheck disable=SC2086 # gate_cmd is intentionally word-split
$gate_cmd >"$gate_log" 2>&1
gate_status=$?
set -e
# Exit 127 (command not found) or 126 (not executable) = cannot verify
if [[ $gate_status -eq 126 || $gate_status -eq 127 ]]; then
echo "verify-gate-claim: gate command failed to execute (status=$gate_status)" >&2
echo "verify-gate-claim: gate output follows (tail):" >&2
tail -20 "$gate_log" >&2 || true
exit 3
fi
if grep -qF -- "$claim" "$gate_log"; then
echo "verify-gate-claim: PASS — claim found verbatim in $source_descr"
echo "verify-gate-claim: gate exit status was $gate_status (informational; verdict is from claim match)"
exit 0
fi
echo "verify-gate-claim: FAIL — claim NOT found in $source_descr" >&2
echo "verify-gate-claim: expected line (substring): $claim" >&2
echo "verify-gate-claim: gate exit status was $gate_status" >&2
echo "verify-gate-claim: gate output (last 40 lines):" >&2
tail -40 "$gate_log" >&2 || true
exit 1