Template
feat: bootstrap meta — cross-project best-practices template
Extract patterns from 18 projects across two machines (12 local infra + 6 remote personal/business via ssh survey). Ship a reusable Gitea template with AGENTS.md, 5 Crush PreToolUse hooks, git pre-commit/pre-push, a generalized rules engine, shared bash library, Makefile, lifecycle scripts, and gardening loop. Includes the canonical BASELINE-PROMPT.md (13 sections) and PATTERNS.md (standardization scorecard). The repo self-applies: it passes its own shellcheck (zero info-level), make fast, and all check-rules.sh checks. 💘 Generated with Crush Assisted-by: Crush via Crush <crush@charm.land>
This commit is contained in:
Executable
+221
@@ -0,0 +1,221 @@
|
||||
#!/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 ./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 Makefile crush.json 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; JOURNAL.md has today's entry.
|
||||
# Warnings (not failures): staleness is a signal, not a break.
|
||||
# ----------------------------------------------------------------------------
|
||||
$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
|
||||
|
||||
if [ -f docs/JOURNAL.md ]; then
|
||||
LAST_ENTRY="$(grep -oE '^## [0-9]{4}-[0-9]{2}-[0-9]{2}' docs/JOURNAL.md | tail -1 | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || echo unknown)"
|
||||
if [ "$LAST_ENTRY" = "$TODAY" ]; then
|
||||
check "JOURNAL.md has an entry for today ($LAST_ENTRY)" "pass"
|
||||
else
|
||||
check "JOURNAL.md has no entry for today (last: $LAST_ENTRY) — add one" "warn"
|
||||
fi
|
||||
else
|
||||
check "docs/JOURNAL.md MISSING" "fail"
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 5. Git state — uncommitted changes are a warning (not a hard failure here;
|
||||
# the pre-push hook and audit-before-git crush hook harden 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 `make test` if present.
|
||||
# ----------------------------------------------------------------------------
|
||||
if [ "$RULE_FAST" = false ] && [ -f Makefile ] && grep -qE '^test:' Makefile; then
|
||||
$RULE_VERBOSE && log_step "Test suite (make test)"
|
||||
if make test >/dev/null 2>&1; then
|
||||
check "make test passes" "pass"
|
||||
else
|
||||
check "make test FAILS" "fail"
|
||||
fi
|
||||
fi
|
||||
|
||||
print_summary_and_exit
|
||||
Executable
+39
@@ -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
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# down.sh — bring down the project's docker-compose stack.
|
||||
# Wrapper around `docker compose down` so every project has the same verb.
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$HERE/lib/common.sh"
|
||||
REPO_ROOT="$(repo_root)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
|
||||
[ -f "$COMPOSE_FILE" ] || die "no $COMPOSE_FILE"
|
||||
|
||||
log_step "Bringing down stack ($COMPOSE_FILE)"
|
||||
docker compose -f "$COMPOSE_FILE" down
|
||||
log_ok "stack down"
|
||||
Executable
+65
@@ -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
|
||||
# `make garden`. 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 docs/JOURNAL.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
|
||||
@@ -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
|
||||
}
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# pre-commit — fast rule audit (< 1s typical).
|
||||
# Hot-path bypass: commits that ONLY touch STATUS.md / JOURNAL.md / WORKING.md
|
||||
# skip the audit so frequent status/journal 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|docs/JOURNAL.md|WORKING.md)$' || true)"
|
||||
|
||||
if [ -z "$HOT_PATHS" ]; then
|
||||
echo "hot-path files only (STATUS/JOURNAL/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
|
||||
Executable
+25
@@ -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
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/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, required files,
|
||||
doc freshness, WORKING.md completion, hygiene).
|
||||
Hot-path bypass for STATUS.md / JOURNAL.md / WORKING.md.
|
||||
pre-push full rule audit (includes make test) + clean-working-tree gate.
|
||||
|
||||
Bypass either with \`git commit --no-verify\` / \`git push --no-verify\`
|
||||
(emergencies only).
|
||||
EOF
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# up.sh — bring up the project's docker-compose stack.
|
||||
# Wrapper around `docker compose up -d` so every project has the same verb.
|
||||
# Add project-specific pre/post steps below.
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$HERE/lib/common.sh"
|
||||
REPO_ROOT="$(repo_root)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
|
||||
[ -f "$COMPOSE_FILE" ] || die "no $COMPOSE_FILE — copy docker-compose.yml.example to $COMPOSE_FILE first"
|
||||
|
||||
log_step "Bringing up stack ($COMPOSE_FILE)"
|
||||
docker compose -f "$COMPOSE_FILE" up -d
|
||||
log_ok "stack up"
|
||||
docker compose -f "$COMPOSE_FILE" ps
|
||||
Reference in New Issue
Block a user