mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
3ad43d3051
## 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>
198 lines
6.0 KiB
Bash
Executable File
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
|