mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
4d7ac76393
## What Single-command wrapper that **removes the operator's choice to skip the discipline.** Auto-detects inventory-touching changes (skills/, skills-codex/, docs/contracts/, schemas/, etc.) and forces the FULL pre-push gate. Runs regen sweep (sync-skill-counts, codex-hashes, domain-map, context-map, registry, sync-hooks) preemptively for inventory PRs. ## Why Closes the operator-discipline gap named in [`ship-loop/references/anti-patterns.md` #1](https://github.com/boshu2/agentops/blob/main/skills/ship-loop/references/anti-patterns.md): *"Running --fast pre-push on an inventory-touching PR."* Observed cost on PR #332: ~90 min of CI cycles burning through 15 inventory validators that the fast gate skipped. Before this PR: operator had to remember to run `scripts/pre-push-gate.sh` (no `--fast`) for inventory PRs. The skill names the rule; nothing enforced it. After this PR: `scripts/ship.sh` is the canonical entry. It detects inventory touch and routes to the right gate. **No operator choice.** ## Files | File | Change | |---|---| | `scripts/ship.sh` (NEW, 212 lines) | Detection + regen + gate routing | | `tests/scripts/ship.bats` (NEW, 98 lines) | 10 structural tests (10/10 PASS) | | `skills/ship-loop/SKILL.md` | Step 5 promotes `ship.sh` as recommended entry | | `skills-codex/ship-loop/SKILL.md` | Codex twin mirrored | | `skills-codex/.agentops-manifest.json`, `skills-codex/ship-loop/.agentops-generated.json` | Hash regen | | `registry.json` | Auto-regenerated | ## Recursive close Three PRs to fix one operator-discipline gap: | PR | Role | |---|---| | #332 | Burned ~90 min on anti-pattern #1; surfaced it | | #331 | Documented it as a durable learning | | **This PR** | Makes it mechanically impossible to hit | The compounding tax is now paid. Future sessions inherit `ship.sh` as the default and the anti-pattern stops costing time. ## Validation - ✅ `shellcheck scripts/ship.sh`: clean - ✅ `bats tests/scripts/ship.bats`: 10/10 PASS - ✅ **Dogfood**: ran `ship.sh` on this very PR. Correctly detected inventory touch (skills/, skills-codex/, registry.json) and routed to full gate. The wrapper closing the discipline gap shipped through itself. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Codex <codex@example.invalid>
213 lines
7.8 KiB
Bash
Executable File
213 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ship.sh — single-command bot-paired fast-lane PR cycle.
|
|
#
|
|
# Removes the choice to skip the discipline encoded in
|
|
# skills/ship-loop/SKILL.md. In particular, anti-pattern #1 of that skill
|
|
# ("Running --fast pre-push on an inventory-touching PR") is enforced
|
|
# mechanically here: this script DETECTS inventory-touching changes and
|
|
# routes through the FULL pre-push gate (no --fast) automatically. The
|
|
# operator no longer chooses; the script chooses.
|
|
#
|
|
# What it does:
|
|
# 1. Detect inventory touch from working-tree + staged diff
|
|
# 2. If inventory: run the regen scripts (sync-skill-counts, codex-hashes,
|
|
# domain-map, context-map, registry, sync-hooks) preemptively so the
|
|
# gates see consistent state
|
|
# 3. Run pre-push-gate.sh in full mode (inventory PR) or --fast (routine)
|
|
# 4. On green: stage all changes (regens included), exit 0 with a clean
|
|
# "ready to commit + push" next-step report
|
|
# 5. On red: exit 2 with a clear reason — same as the gate itself
|
|
#
|
|
# What it does NOT do (yet):
|
|
# - commit (operator writes the message; tracked separately)
|
|
# - push or open PR (one extra command after green)
|
|
# - close the bead
|
|
#
|
|
# Usage:
|
|
# scripts/ship.sh # auto-detect, auto-regen, auto-gate
|
|
# scripts/ship.sh --dry-run # show plan, no execution
|
|
# scripts/ship.sh --force-fast # escape hatch: use --fast even on
|
|
# # inventory diff (NOT recommended;
|
|
# # accept full responsibility for the
|
|
# # registries-drift class if you use it)
|
|
# scripts/ship.sh --no-regen # skip the regen sweep (useful if you
|
|
# # have committed regens already)
|
|
# scripts/ship.sh --gate full|fast # force the gate mode explicitly
|
|
#
|
|
# Exit codes:
|
|
# 0 Ready to commit + push (or already committed; gate passed)
|
|
# 2 Pre-push gate BLOCKED — see gate output for which check failed
|
|
# 3 Preconditions failed (on main branch, no remote, etc.)
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
|
|
DRY_RUN=false
|
|
FORCE_GATE="" # "" | "full" | "fast"
|
|
SKIP_REGEN=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-h|--help)
|
|
sed -nE 's/^# ?(.*)/\1/p' "$0" | head -40
|
|
exit 0
|
|
;;
|
|
--dry-run) DRY_RUN=true; shift ;;
|
|
--force-fast) FORCE_GATE="fast"; shift ;;
|
|
--no-regen) SKIP_REGEN=true; shift ;;
|
|
--gate)
|
|
case "${2:-}" in
|
|
full|fast) FORCE_GATE="$2"; shift 2 ;;
|
|
*) echo "ship: --gate requires full|fast" >&2; exit 3 ;;
|
|
esac
|
|
;;
|
|
*) echo "ship: unknown flag: $1" >&2; exit 3 ;;
|
|
esac
|
|
done
|
|
|
|
# --- Preconditions ---------------------------------------------------------
|
|
|
|
branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
|
|
if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
|
|
echo "ship: not on a named branch (detached HEAD?). Create a feature branch first." >&2
|
|
exit 3
|
|
fi
|
|
if [[ "$branch" == "main" || "$branch" == "master" ]]; then
|
|
echo "ship: on '$branch' — branch off main before shipping." >&2
|
|
exit 3
|
|
fi
|
|
|
|
# Inventory-touch detection patterns. Each surface here triggers ~1-15
|
|
# inventory validators that --fast mode would skip. Anti-pattern #1 in
|
|
# skills/ship-loop catalogs the empirical cost (PR #332 burned ~90 min
|
|
# of CI cycles on this class).
|
|
INVENTORY_PATTERNS=(
|
|
'^skills/'
|
|
'^skills-codex/'
|
|
'^skills-codex-overrides/'
|
|
'^docs/contracts/'
|
|
'^docs/learnings/'
|
|
'^docs/reference/'
|
|
'^schemas/'
|
|
'^SKILL-TIERS\.md$'
|
|
'^PRODUCT\.md$'
|
|
'^README\.md$'
|
|
'^registry\.json$'
|
|
'^\.github/workflows/'
|
|
'^cli/docs/COMMANDS\.md$'
|
|
)
|
|
|
|
detect_inventory_touch() {
|
|
local files
|
|
files="$(
|
|
{
|
|
# Modified tracked files (staged + working tree)
|
|
git diff --name-only HEAD 2>/dev/null
|
|
git diff --name-only --cached 2>/dev/null
|
|
# New untracked files (e.g., skills/new-skill/SKILL.md before first add)
|
|
git ls-files --others --exclude-standard 2>/dev/null
|
|
# Commits on this branch not yet on origin/main
|
|
git diff --name-only "$(git merge-base HEAD origin/main 2>/dev/null || echo HEAD)..HEAD" 2>/dev/null
|
|
} | sort -u | sed '/^[[:space:]]*$/d'
|
|
)"
|
|
if [[ -z "$files" ]]; then
|
|
return 1 # no changes at all
|
|
fi
|
|
local pattern
|
|
for pattern in "${INVENTORY_PATTERNS[@]}"; do
|
|
if echo "$files" | grep -qE "$pattern"; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# --- Mode selection --------------------------------------------------------
|
|
|
|
case "$FORCE_GATE" in
|
|
full)
|
|
gate_mode="full"
|
|
reason="--gate full (operator override)"
|
|
;;
|
|
fast)
|
|
gate_mode="fast"
|
|
reason="--force-fast or --gate fast (operator override; ANTI-PATTERN #1 if inventory)"
|
|
;;
|
|
*)
|
|
if detect_inventory_touch; then
|
|
gate_mode="full"
|
|
reason="inventory-touching diff detected (per ship-loop anti-pattern #1)"
|
|
else
|
|
gate_mode="fast"
|
|
reason="routine diff (no inventory surfaces touched)"
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
echo "ship: branch=$branch gate=$gate_mode ($reason)"
|
|
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
regen_msg="$([[ "$gate_mode" == "full" ]] && echo "yes" || echo "no")"
|
|
gate_msg="$([[ "$gate_mode" == "fast" ]] && echo "pre-push-gate.sh --fast" || echo "pre-push-gate.sh (full)")"
|
|
echo "ship: --dry-run — would run regen sweep: $regen_msg; then $gate_msg"
|
|
exit 0
|
|
fi
|
|
|
|
# --- Regen sweep (inventory PRs only) --------------------------------------
|
|
|
|
if [[ "$gate_mode" == "full" && "$SKIP_REGEN" != "true" ]]; then
|
|
echo "ship: running regen sweep (inventory PRs need synced derivatives)"
|
|
|
|
run_regen() {
|
|
local name="$1" cmd="$2"
|
|
if eval "$cmd" >/dev/null 2>&1; then
|
|
echo " ok: $name"
|
|
else
|
|
echo " WARN: $name failed (non-fatal; gate may catch it)"
|
|
fi
|
|
}
|
|
|
|
[[ -x scripts/sync-skill-counts.sh ]] && \
|
|
run_regen "sync-skill-counts" "bash scripts/sync-skill-counts.sh"
|
|
[[ -x scripts/regen-codex-hashes.sh ]] && \
|
|
run_regen "regen-codex-hashes" "bash scripts/regen-codex-hashes.sh"
|
|
[[ -x scripts/generate-skill-domain-map.sh ]] && \
|
|
run_regen "generate-skill-domain-map" "bash scripts/generate-skill-domain-map.sh"
|
|
[[ -x scripts/generate-context-map.sh ]] && \
|
|
run_regen "generate-context-map" "bash scripts/generate-context-map.sh"
|
|
[[ -x scripts/generate-registry.sh ]] && \
|
|
run_regen "generate-registry" "bash scripts/generate-registry.sh"
|
|
if [[ -f cli/Makefile ]] && grep -q '^sync-hooks:' cli/Makefile; then
|
|
run_regen "cli sync-hooks" "(cd cli && make sync-hooks)"
|
|
fi
|
|
fi
|
|
|
|
# --- Pre-push gate ---------------------------------------------------------
|
|
|
|
gate_args=()
|
|
if [[ "$gate_mode" == "fast" ]]; then
|
|
gate_args=(--fast)
|
|
fi
|
|
|
|
echo "ship: running pre-push-gate.sh ${gate_args[*]:-(full)}"
|
|
if bash scripts/pre-push-gate.sh "${gate_args[@]}"; then
|
|
echo ""
|
|
echo "ship: pre-push gate PASSED."
|
|
echo ""
|
|
echo "Next steps (operator):"
|
|
echo " 1. Review changes: git status && git diff"
|
|
echo " 2. Commit: git add -A && git commit -m '...'"
|
|
echo " 3. Push + auto-merge: git push && gh pr create + gh pr merge <num> --squash --auto"
|
|
echo " 4. Close bead on merge: bd close <bead-id> --reason 'Merged via PR #<num>'"
|
|
exit 0
|
|
else
|
|
rc=$?
|
|
echo "" >&2
|
|
echo "ship: pre-push gate BLOCKED. Address the failing check(s) above and re-run." >&2
|
|
echo "ship: in inventory-PR mode, --fast skips ~15 validators — DO NOT use --force-fast to bypass." >&2
|
|
exit "$rc"
|
|
fi
|