chore: adopt TSYSGroupAIOS framework (git hooks, rules engine, SoR policy)

Brings in the enforcement layer from ~/daytoday/meta:
- Makefile, scripts/ (check-rules.sh, setup-hooks.sh, pre-commit/pre-push,
  docker-run.sh, garden.sh, lib/common.sh)
- WORKING.md, questions-v1.md, .env.example
- Git hooks installed (pre-commit: fast audit, pre-push: full audit)

Fixes to pass rule audit:
- Pin Pi-hole/autoheal Docker images (no :latest tags)
- Fix shellcheck SC2001 in probe-vm-dns.sh
- Prune vendor/ and archive/ from shellcheck + Discourse pointer checks
- Add Quick Start, Enforcement Model, Task Tracking, Working Style
  sections to AGENTS.md from template

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
2026-08-07 12:29:36 -05:00
parent 25a71c0265
commit ec6e228b05
14 changed files with 746 additions and 3 deletions
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env bash
# check-rules.sh — project rule audit engine.
#
# Usage:
# bash scripts/check-rules.sh # full audit (verbose, includes slow checks)
# bash scripts/check-rules.sh --fast # fast audit (quiet, skips slow checks) — for pre-commit
# bash scripts/check-rules.sh --quiet # full audit, only prints failures
#
# Exit code: 0 = all rules pass (warnings are non-fatal), 1 = one or more FAILED.
#
# This is a generalized version of the rules engine proven in the
# RCEO-PersonalAssistant project. Add project-specific checks by appending
# `check "<desc>" "<pass|warn|fail>"` calls below.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$HERE/lib/common.sh"
REPO_ROOT="$(repo_root)"
cd "$REPO_ROOT"
# --- argument parsing ---
RULE_FAST=false
RULE_VERBOSE=true
for arg in "$@"; do
case "$arg" in
--fast) RULE_FAST=true; RULE_VERBOSE=false ;;
--quiet) RULE_VERBOSE=false ;;
*) die "check-rules.sh: unknown argument '$arg'" ;;
esac
done
export RULE_FAST RULE_VERBOSE
init_counters
$RULE_VERBOSE && echo "=== Project Rule Audit ==="
TODAY="$(date +%Y-%m-%d)"
# ----------------------------------------------------------------------------
# 1. Shellcheck — every .sh/.bash must pass (zero warnings, incl. info-level).
# Runs in Docker so the host stays clean (no native shellcheck required).
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Shell scripts (shellcheck)"
mapfile -d '' SH_FILES < <(find . -path ./.git -prune -o -path ./.tmp -prune -o -path ./vendor -prune -o -path ./archive -prune -o -path ./node_modules -prune -o \( -name '*.sh' -o -name '*.bash' \) -print0 2>/dev/null)
if [ "${#SH_FILES[@]}" -gt 0 ]; then
if have shellcheck; then
if shellcheck "${SH_FILES[@]}" >/dev/null 2>&1; then
check "All shell scripts pass shellcheck (host)" "pass"
else
check "shellcheck reports violations — run: shellcheck <file>" "fail"
fi
elif have docker; then
MNT_FILES=()
for f in "${SH_FILES[@]}"; do MNT_FILES+=("/mnt/${f#./}"); done
if docker run --rm -v "$REPO_ROOT:/mnt" koalaman/shellcheck:stable "${MNT_FILES[@]}" >/dev/null 2>&1; then
check "All shell scripts pass shellcheck (docker)" "pass"
else
check "shellcheck (docker) reports violations" "fail"
fi
else
check "No shellcheck or docker available to lint scripts" "warn"
fi
else
check "No shell scripts to lint" "pass"
fi
# ----------------------------------------------------------------------------
# 2. Docker image pinning — no ':latest' tags in compose or Dockerfiles.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Docker image pinning"
if grep -rqE '(image:|FROM).*:latest' --include='docker-compose*.y*ml' --include='Dockerfile*' . 2>/dev/null; then
check "No ':latest' image tags (pin everything)" "fail"
else
check "No ':latest' image tags" "pass"
fi
# ----------------------------------------------------------------------------
# 2b. Container naming — every service in a docker-compose file MUST set an
# explicit container_name (never rely on Docker's default <dir>_<n>).
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Container naming"
COMPOSE_FILES="$(find . -path ./.git -prune -o \( -name 'docker-compose*.yml' -o -name 'docker-compose*.yaml' -o -name 'compose.yml' -o -name 'compose.yaml' \) -print 2>/dev/null || true)"
if [ -n "$COMPOSE_FILES" ]; then
BAD=0
while IFS= read -r cf; do
[ -n "$cf" ] || continue
# Count top-level service keys (2-space indent under services:) and
# compare against the number of container_name: declarations.
svc_count=$(awk '/^services:/{f=1;next} f&&/^[^[:space:]]/{f=0} f&&/^[[:space:]]{2}[[:alnum:]_-]+:[[:space:]]*$/{c++} END{print c+0}' "$cf")
cn_count=$(grep -cE '^[[:space:]]*container_name:' "$cf" 2>/dev/null || echo 0)
if [ "${svc_count:-0}" -gt 0 ] && [ "$cn_count" -lt "$svc_count" ]; then
BAD=$((BAD + 1))
fi
done <<EOF
$COMPOSE_FILES
EOF
if [ "$BAD" -eq 0 ]; then
check "All compose services set container_name" "pass"
else
check "$BAD compose file(s) with services missing container_name" "fail"
fi
else
check "No compose files (container-name check skipped)" "pass"
fi
# ----------------------------------------------------------------------------
# 3. Required-files manifest — the files every project using this template owns.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Required files"
REQUIRED_FILES="AGENTS.md STATUS.md questions-v1.md .env.example scripts/check-rules.sh scripts/setup-hooks.sh"
REQUIRED_FILES="$REQUIRED_FILES ${PROJECT_REQUIRED_FILES:-}"
for f in $REQUIRED_FILES; do
if [ -f "$f" ]; then check "$f exists" "pass"; else check "$f MISSING" "fail"; fi
done
# ----------------------------------------------------------------------------
# 4. Doc freshness — STATUS.md touched today.
# Warning (not failure): staleness is a signal, not a break.
# Redmine is the system of record for work; Discourse for docs. STATUS.md is
# a scratchpad only — see BASELINE-PROMPT.md §3, §8.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Doc freshness"
if [ -f STATUS.md ]; then
STATUS_DATE="$(grep -oE 'Last updated: [0-9]{4}-[0-9]{2}-[0-9]{2}' STATUS.md | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || echo unknown)"
if [ "$STATUS_DATE" = "$TODAY" ]; then
check "STATUS.md updated today ($STATUS_DATE)" "pass"
else
check "STATUS.md is stale (last: $STATUS_DATE, today: $TODAY) — update it" "warn"
fi
else
check "STATUS.md MISSING" "fail"
fi
# ----------------------------------------------------------------------------
# 4b. Discourse pointer-header policy (MANDATORY).
# Discourse is the system of record for documentation. In-repo .md files are
# stubs that point to a Discourse topic URL. Operational files exempt.
# Override exemptions via PROJECT_DOC_EXEMPT (space-separated globs of
# basenames) and the Discourse host via PROJECT_DISCOURSE_HOST.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Discourse pointer-header"
DISCOURSE_HOST="${PROJECT_DISCOURSE_HOST:-community.turnsys.com}"
DOC_EXEMPT="${PROJECT_DOC_EXEMPT:-AGENTS.md STATUS.md WORKING.md README.md ADOPTING.md LICENSE .env.example questions-v*.md BASELINE-PROMPT.md PATTERNS.md}"
POINTER_MISSING=0
while IFS= read -r -d '' f; do
base="$(basename "$f")"
exempt=false
for pat in $DOC_EXEMPT; do
# shellcheck disable=SC2254
case "$base" in $pat) exempt=true; break ;; esac
done
[ "$exempt" = true ] && continue
if ! grep -qF "$DISCOURSE_HOST" "$f" 2>/dev/null; then
if [ "$POINTER_MISSING" -eq 0 ]; then
$RULE_VERBOSE && printf ' %s\n' "Missing $DISCOURSE_HOST URL in:"
fi
POINTER_MISSING=$((POINTER_MISSING + 1))
$RULE_VERBOSE && printf ' %s\n' "$f"
fi
done < <(find . -path ./.git -prune -o -path ./.tmp -prune -o -path ./vendor -prune -o -path ./archive -prune -o -name '*.md' -print0 2>/dev/null)
if [ "$POINTER_MISSING" -eq 0 ]; then
check "All non-exempt .md cite Discourse ($DISCOURSE_HOST)" "pass"
else
check "$POINTER_MISSING .md file(s) missing Discourse pointer (see BASELINE-PROMPT.md §3)" "fail"
fi
# ----------------------------------------------------------------------------
# 5. Git state — uncommitted changes are a warning (the pre-push hook hardens
# this where it matters).
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Git state"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
if git diff --quiet && git diff --cached --quiet; then
check "Working tree clean" "pass"
else
check "Uncommitted changes present" "warn"
fi
else
check "Not a git repo (git checks skipped)" "pass"
fi
# ----------------------------------------------------------------------------
# 6. Hooks installed — self-check that git hooks were set up.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Git hooks"
if [ -f .git/hooks/pre-commit ]; then
check "pre-commit hook installed" "pass"
else
check "pre-commit NOT installed (run: bash scripts/setup-hooks.sh)" "warn"
fi
if [ -f .git/hooks/pre-push ]; then
check "pre-push hook installed" "pass"
else
check "pre-push NOT installed (run: bash scripts/setup-hooks.sh)" "warn"
fi
# ----------------------------------------------------------------------------
# 7. WORKING.md completion — no unchecked tasks may remain at commit time.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Task completion"
if [ -f WORKING.md ]; then
UNCHECKED="$(grep -cF -- '- [ ]' WORKING.md || true)"
if [ "$UNCHECKED" -eq 0 ]; then
check "WORKING.md has no unchecked tasks" "pass"
else
check "WORKING.md has ${UNCHECKED} unchecked task(s) — finish them before committing" "fail"
fi
else
check "WORKING.md absent (no active task tracker)" "pass"
fi
# ----------------------------------------------------------------------------
# 8. CNW markers — empty `CNW:` markers flag unresolved questions for the human.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Unresolved questions"
EMPTY_CNW="$(grep -rn 'CNW:$' . --include='*.md' 2>/dev/null | head -20 || true)"
if [ -z "$EMPTY_CNW" ]; then
check "No empty CNW: markers (unresolved questions)" "pass"
else
CNW_COUNT="$(printf '%s\n' "$EMPTY_CNW" | grep -c . || true)"
check "${CNW_COUNT} unresolved CNW: marker(s) — needs user input" "warn"
fi
# ----------------------------------------------------------------------------
# 9. Hygiene — merge-conflict markers and trailing whitespace must never land.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "File hygiene"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
CONFLICT="$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | xargs -r grep -lE '^(<<<<<<<|=======|>>>>>>>)' 2>/dev/null || true)"
if [ -z "$CONFLICT" ]; then check "No merge-conflict markers staged" "pass"; else check "Merge-conflict markers staged: $CONFLICT" "fail"; fi
fi
# ----------------------------------------------------------------------------
# 10. (slow, skipped in --fast) Project test suite via scripts/test.sh.
# ----------------------------------------------------------------------------
if [ "$RULE_FAST" = false ] && [ -x scripts/test.sh ]; then
$RULE_VERBOSE && log_step "Test suite (scripts/test.sh)"
if bash scripts/test.sh >/dev/null 2>&1; then
check "scripts/test.sh passes" "pass"
else
check "scripts/test.sh FAILS" "fail"
fi
fi
print_summary_and_exit
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# docker-run.sh — canonical ephemeral-container wrapper.
#
# Keeps the host clean: every build/test/generation runs inside a pinned image.
# Ensures output files are owned by the invoking user (not root).
#
# Usage:
# docker-run.sh <image> [command...]
# Runs <command> in <image> with the repo mounted at /data, cwd /data.
# With no command, drops into the image's default entrypoint.
# docker-run.sh --shell <image>
# Interactive shell inside the container (for debugging).
#
# Examples:
# docker-run.sh python:3.12-slim python3 -m pytest
# docker-run.sh pandoc/extra report.md -o report.pdf
# docker-run.sh --shell node:20
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$HERE/lib/common.sh"
SHELL_MODE=false
case "${1:-}" in
--shell) SHELL_MODE=true; shift ;;
-h|--help)
sed -n '2,18p' "$0"; exit 0 ;;
esac
[ "$#" -ge 1 ] || { sed -n '2,18p' "$0"; exit 1; }
if [ "$SHELL_MODE" = true ]; then
# ${SHELL:-sh} must expand inside the container, not in this outer shell.
# shellcheck disable=SC2016
docker_run "$1" sh -c 'exec "${SHELL:-sh}"'
else
docker_run "$@"
fi
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# garden.sh — the gardening loop.
#
# Reports doc sprawl and files that violate the "Discourse is the system of
# record for documentation; gitea .md files are stubs" policy. Run via
# `bash scripts/garden.sh`. Findings are WARNINGS (advisory); fix them at a natural break.
#
# What it checks:
# 1. Markdown sprawl: count of .md files per directory (top-10 by count).
# 2. Oversized .md files (default >300 lines) that don't cite a Discourse URL
# — candidates to migrate to Discourse, leaving a stub.
# 3. .md files with no Discourse link at all (informational; exempt: the
# operational files in EXEMPT_FILES).
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$HERE/lib/common.sh"
REPO_ROOT="$(repo_root)"
cd "$REPO_ROOT"
SIZE_LIMIT="${GARDEN_MD_LINE_LIMIT:-300}"
# Operational files that legitimately live next to code, not in Discourse.
EXEMPT_FILES="${GARDEN_EXEMPT:-AGENTS.md STATUS.md WORKING.md questions-v.*.md PATTERNS.md BASELINE-PROMPT.md README.md}"
log_step "Gardening report for $REPO_ROOT"
# --- 1. sprawl by directory -------------------------------------------------
log_info "Markdown file count by directory (top 10):"
find . -path ./.git -prune -o -name '*.md' -print 2>/dev/null \
| sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -10 | sed 's/^/ /'
# --- 2. oversized .md without a Discourse link ------------------------------
log_info "Oversized .md (>${SIZE_LIMIT} lines) lacking a Discourse URL — migrate candidates:"
OVERSIZED=0
while IFS= read -r -d '' f; do
# skip exempt files (glob match against basename and relative path)
exempt=false
base=$(basename "$f")
rel=${f#./}
for pat in $EXEMPT_FILES; do
# shellcheck disable=SC2254 # glob match is intentional
case "$base" in $pat) exempt=true; break ;; esac
# shellcheck disable=SC2254
case "$rel" in $pat) exempt=true; break ;; esac
done
[ "$exempt" = true ] && continue
lines=$(wc -l < "$f" 2>/dev/null || echo 0)
if [ "$lines" -gt "$SIZE_LIMIT" ]; then
if ! grep -qiE 'community\.turnsys\.com|discourse' "$f" 2>/dev/null; then
printf ' %-60s %s lines\n' "$f" "$lines"
OVERSIZED=$((OVERSIZED + 1))
fi
fi
done < <(find . -path ./.git -prune -o -name '*.md' -print0 2>/dev/null)
[ "$OVERSIZED" -eq 0 ] && echo " (none)"
# --- 3. summary -------------------------------------------------------------
log_step "Gardening summary"
echo " Oversized non-Discourse .md files: $OVERSIZED"
if [ "$OVERSIZED" -eq 0 ]; then
log_ok "no migration candidates"
else
log_warn "$OVERSIZED file(s) to migrate to Discourse"
fi
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
# lib/common.sh — shared helpers for shell scripts and hooks in this repo.
#
# Source it from any script:
# #!/usr/bin/env bash
# set -euo pipefail
# HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# # shellcheck source=lib/common.sh
# source "$HERE/lib/common.sh" # or the appropriate relative path
#
# This library exists to drive a known cross-project inconsistency to zero:
# every repo used to re-paste the ANSI color block, redefine log_* helpers,
# pick one of three incompatible shebangs, and roll its own docker wrapper.
# Import this once instead.
# Do NOT set -euo pipefail here unconditionally — some callers (git hooks)
# source this file and rely on controlling their own shell options. We only
# guarantee the functions below are defined.
###############################################################################
# Config — override via environment before sourcing if needed
###############################################################################
: "${TEMPLATE_ROOT:=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
export TEMPLATE_ROOT
###############################################################################
# ANSI colors (defined once, used everywhere)
###############################################################################
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; BLUE=''; BOLD=''; NC=''
fi
export RED GREEN YELLOW BLUE BOLD NC
###############################################################################
# Logging
###############################################################################
log_info() { printf "${BLUE}${NC} %s\n" "$*"; }
log_ok() { printf "${GREEN}${NC} %s\n" "$*"; }
log_warn() { printf "${YELLOW}${NC} %s\n" "$*" >&2; }
log_error() { printf "${RED}${NC} %s\n" "$*" >&2; }
log_step() { printf "\n${BOLD}== %s ==${NC}\n" "$*"; }
die() { log_error "$*"; exit 1; }
###############################################################################
# Predicates
###############################################################################
# have <cmd> — return 0 if <cmd> is on PATH
have() { command -v "$1" >/dev/null 2>&1; }
###############################################################################
# Path helpers
###############################################################################
repo_root() {
# Prefer git's notion of the repo root, fall back to $TEMPLATE_ROOT, then pwd.
if git rev-parse --show-toplevel >/dev/null 2>&1; then
git rev-parse --show-toplevel
else
printf '%s\n' "${TEMPLATE_ROOT:-$(pwd)}"
fi
}
###############################################################################
# Privilege helpers
###############################################################################
# as_root — run the remaining args as root via sudo, or directly if already root.
as_root() {
if [ "$(id -u)" -eq 0 ]; then "$@"; else sudo "$@"; fi
}
###############################################################################
# Docker wrapper
###############################################################################
# docker_run <image> <args...>
# Ephemeral container, host-uid ownership, repo mounted at /data, cwd /data.
# Drives the "host stays clean; everything runs in containers" policy and
# ensures output files are owned by the invoking user, not root.
docker_run() {
[ "$#" -ge 1 ] || die "docker_run: image required"
local image="$1"; shift
have docker || die "docker not found on PATH"
local root
root="$(repo_root)"
docker run --rm \
--user "$(id -u):$(id -g)" \
-e HOME=/tmp \
-v "$root:/data" \
-w /data \
"$image" "$@"
}
###############################################################################
# Rule-audit accumulator (used by scripts/check-rules.sh)
# Globals read/written: RULE_PASS RULE_WARN RULE_FAIL
###############################################################################
init_counters() { RULE_PASS=0; RULE_WARN=0; RULE_FAIL=0; }
# check <description> <pass|warn|fail>
check() {
local desc="$1" result="$2"
case "$result" in
pass)
RULE_PASS=$((RULE_PASS + 1))
if [ "${RULE_VERBOSE:-true}" = true ]; then printf " ${GREEN}PASS${NC} %s\n" "$desc"; fi
;;
warn)
RULE_WARN=$((RULE_WARN + 1))
if [ "${RULE_VERBOSE:-true}" = true ]; then printf " ${YELLOW}WARN${NC} %s\n" "$desc"; fi
;;
fail)
RULE_FAIL=$((RULE_FAIL + 1))
printf " ${RED}FAIL${NC} %s\n" "$desc"
;;
*)
die "check(): invalid result '$result' (use pass|warn|fail)"
;;
esac
}
# print_summary_and_exit
print_summary_and_exit() {
if [ "${RULE_VERBOSE:-true}" = true ]; then
printf "\n=== Summary ===\n PASS: %s\n WARN: %s\n FAIL: %s\n\n" \
"$RULE_PASS" "$RULE_WARN" "$RULE_FAIL"
fi
if [ "$RULE_FAIL" -gt 0 ]; then
if [ "${RULE_VERBOSE:-true}" = true ]; then
printf "AUDIT FAILED — %s rule(s) violated.\n" "$RULE_FAIL"
fi
exit 1
fi
if [ "${RULE_VERBOSE:-true}" = true ]; then printf "AUDIT PASSED.\n"; fi
exit 0
}
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# pre-commit — fast rule audit (< 1s typical).
# Hot-path bypass: commits that ONLY touch STATUS.md / WORKING.md skip the
# audit so frequent status/task commits stay frictionless.
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"
CHANGED="$(git diff --cached --name-only)"
HOT_PATHS="$(printf '%s\n' "$CHANGED" | grep -vE '^(STATUS.md|WORKING.md)$' || true)"
if [ -z "$HOT_PATHS" ]; then
echo "hot-path files only (STATUS/WORKING) — skipping rule audit"
exit 0
fi
if ! bash scripts/check-rules.sh --fast; then
echo ""
echo "pre-commit audit FAILED. Fix the violations above before committing."
echo "Full audit: bash scripts/check-rules.sh"
echo "Bypass: git commit --no-verify (emergencies only)"
exit 1
fi
exit 0
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# pre-push — full rule audit + clean-working-tree gate before pushing.
# Installed via: bash scripts/setup-hooks.sh
#
# Combines two proven policies observed across projects:
# - KNEL-AIMiddleware: block push if the working tree is dirty.
# - RCEO-PersonalAssistant: block push if the full test suite fails.
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"
echo "pre-push: running full rule audit..."
# Full audit (non-fast): runs the slow test suite via `make test` if present.
if ! bash scripts/check-rules.sh --quiet; then
echo ""
echo "pre-push audit FAILED. Push blocked."
echo "Re-run with output: bash scripts/check-rules.sh"
echo "Bypass: git push --no-verify (emergencies only)"
exit 1
fi
echo "pre-push: all rules and tests passed."
exit 0
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# setup-hooks.sh — install this repo's git hooks.
#
# Mechanism: copy scripts/pre-commit and scripts/pre-push into .git/hooks/ and
# make them executable. This is the most portable pattern (works on any clone,
# no `git config core.hooksPath` mutation, survives config resets, idempotent).
#
# Run once after cloning: bash scripts/setup-hooks.sh
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$HERE/lib/common.sh"
REPO_ROOT="$(repo_root)"
cd "$REPO_ROOT"
[ -d .git ] || die "no .git directory here — run this from a git checkout"
HOOKS_DIR=".git/hooks"
HOOK_NAMES="pre-commit pre-push"
log_step "Installing git hooks"
for name in $HOOK_NAMES; do
src="scripts/$name"
dst="$HOOKS_DIR/$name"
[ -f "$src" ] || die "source hook not found: $src"
cp "$src" "$dst"
chmod +x "$dst"
log_ok "installed $dst"
done
cat <<EOF
Git hooks installed. The following now run automatically:
pre-commit fast rule audit (shellcheck, image pinning, container naming,
required files, doc freshness, Discourse pointers, WORKING.md
completion, hygiene).
Hot-path bypass for STATUS.md / WORKING.md.
pre-push full rule audit (includes scripts/test.sh) + clean-working-tree gate.
Bypass either with \`git commit --no-verify\` / \`git push --no-verify\`
(emergencies only).
EOF