#!/usr/bin/env bash # hooks/ticket-gate.sh — enforce ticket-first work policy # # Blocks modifying operations until an active ticket is established. # The agent sets the active ticket via: echo '#NNN' > .crush/active-ticket # And clears it when done: > .crush/active-ticket # # Exempts read-only and management commands (so you can create tickets, # run audits, check status, etc.). set -euo pipefail TICKET_FILE="${CRUSH_PROJECT_DIR}/.crush/active-ticket" TOOL="${CRUSH_TOOL_NAME:-}" CMD="${CRUSH_TOOL_INPUT_COMMAND:-}" # Read-only tools — always allowed 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 # For bash tool: exempt read-only and management commands if [ "$TOOL" = "bash" ]; then # Ticket/doc/dns management — always allowed case "$CMD" in *"redmine-cli"*|*"discourse-cli"*|*"dns-cli"*|*"technitium"*) exit 0 ;; esac # Read-only git case "$CMD" in *"git status"*|*"git log"*|*"git diff"*|*"git show"*|*"git branch"*) exit 0 ;; esac # Repo hygiene scripts case "$CMD" in *"check-rules"*|*"setup-hooks"*|*"shellcheck"*|*"run-tests"*) exit 0 ;; esac # Monitoring/probe commands case "$CMD" in *"tailscale status"*|*"access-matrix"*) exit 0 ;; esac # Setting/clearing the active ticket case "$CMD" in *active-ticket*) exit 0 ;; esac fi # For edit/write: exempt policy/hook files (these ARE the policy) FILE_PATH="${CRUSH_TOOL_INPUT_FILE_PATH:-}" case "$FILE_PATH" in */AGENTS.md|*/check-rules.sh|*/crush.json|*/hooks/*) if [ "$TOOL" = "write" ] || [ "$TOOL" = "edit" ] || [ "$TOOL" = "multiedit" ]; then exit 0 fi ;; esac # Check for active ticket if [ -f "$TICKET_FILE" ] && [ -s "$TICKET_FILE" ]; then TICKET=$(cat "$TICKET_FILE") printf '{"context":"Active ticket: %s"}\n' "$TICKET" exit 0 fi # No active ticket — block cat >&2 <<'MSG' TICKET GATE: No active ticket set. This project requires ticket-governed work (AGENTS.md Agent Authority). Before modifying systems or code, set the active ticket: echo '#NNN' > .crush/active-ticket If no ticket exists yet, create one first (redmine-cli create), then set it. Clear the ticket when work is complete: > .crush/active-ticket MSG exit 2