Files
TSYSGroupAIOS/hooks/global/ticket-gate.sh
T
mrcharles 8bff9567b7 feat(enforcement): global house rules + tier-wide mechanical gates
Machine-optimal global AGENTS.md (symlinked at ~/.config/AGENTS.md, picked
up by every crush lane) covering the alpha/beta/uat/prod doctrine
(prod = human release only), strict SDLC (red/green TDD, adversarial
review, code/tests/docs/ticket sync), gitea-redmine-discourse
cross-linking, and the 2026-08-31 rulings (no JOURNAL.md, sparse
STATUS.md, rolling table HUD).

Global enforcement layer: repo-scoped ticket-gate (crush PreToolUse,
all lanes) + universal git pre-commit/pre-push (core.hooksPath) that
delegate to repo-local check-rules.sh and chain installed .git/hooks.
2026-08-31 14:42:00 -05:00

62 lines
2.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# hooks/global/ticket-gate.sh — GLOBAL crush PreToolUse gate (all lanes).
#
# Ticket-first policy, repo-scoped: inside a git repository, modifying
# operations are blocked until the repo's .crush/active-ticket is set
# echo '#NNN' > .crush/active-ticket (set)
# > .crush/active-ticket (clear)
# Outside git repos (scratch space) everything is allowed: the gate
# governs governed work, not throwaway experiments.
#
# Env provided by crush: CRUSH_TOOL_NAME, CRUSH_TOOL_INPUT_COMMAND,
# CRUSH_TOOL_INPUT_FILE_PATH, CRUSH_PROJECT_DIR, PWD.
set -u
TOOL="${CRUSH_TOOL_NAME:-}"
CMD="${CRUSH_TOOL_INPUT_COMMAND:-}"
FILE_PATH="${CRUSH_TOOL_INPUT_FILE_PATH:-}"
# Read-only tools — always allowed, everywhere.
case "$TOOL" in
view|ls|grep|glob|agent|sourcegraph|fetch|agentic_fetch|download|lsp_diagnostics|lsp_symbols|lsp_definition|lsp_references|lsp_call_hierarchy|crush_info|crush_logs|question|todos)
exit 0
;;
esac
# Not inside a git repo? Scratch space — allow.
REPO_ROOT="$(git -C "${PWD:-.}" rev-parse --show-toplevel 2>/dev/null || true)"
if [ -z "$REPO_ROOT" ]; then
exit 0
fi
# bash tool: exempt read-only + management commands.
if [ "$TOOL" = "bash" ]; then
case "$CMD" in
*"redmine-cli"*|*"discourse-cli"*|*"dns-cli"*|*"technitium"*) exit 0 ;;
*"git status"*|*"git log"*|*"git diff"*|*"git show"*|*"git branch"*) exit 0 ;;
*"check-rules"*|*"setup-hooks"*|*"shellcheck"*|*"run-tests"*|*"promote.sh"*) exit 0 ;;
*"tailscale status"*|*"access-matrix"*|*"docker ps"*|*"docker logs"*|*"docker inspect"*) exit 0 ;;
*active-ticket*) exit 0 ;;
esac
fi
# edit/write: policy and wiring files ARE the policy — exempt them.
case "$FILE_PATH" in
*/AGENTS.md|*/questions-v*.md|*/check-rules.sh|*/crush.json|*/crushrc|*/hooks/*|*/.crush/*)
case "$TOOL" in write|edit|multiedit) exit 0 ;; esac
;;
esac
# Enforce: repo root's .crush/active-ticket must exist and be non-empty.
TICKET_FILE="$REPO_ROOT/.crush/active-ticket"
if [ -f "$TICKET_FILE" ] && [ -s "$TICKET_FILE" ]; then
TICKET="$(tr -d '\n' < "$TICKET_FILE")"
printf '{"context":"Active ticket: %s"}\n' "$TICKET"
exit 0
fi
cat >&2 <<'EOF'
{"error":{"message":"TICKET GATE: no active ticket in this repo. Set one first: echo '#NNN' > .crush/active-ticket (create the ticket in Redmine first if none exists). Clear with: > .crush/active-ticket"}}
EOF
exit 2