The ticket gate required the single .crush/active-ticket file, so two
concurrent sessions overwrote each other's ticket mid-work. The gate now
accepts any non-empty .crush/active-ticket* file; each session keeps its
own (e.g. active-ticket-plant, active-ticket-core). Also prune .crush/
session scratch from both shellcheck scanners so one session's throwaway
probe scripts cannot block the other session's commits. Documented in
AGENTS.md Task Tracking.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
84 lines
2.7 KiB
Bash
Executable File
84 lines
2.7 KiB
Bash
Executable File
#!/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
|
|
|
|
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. Parallel sessions each use their OWN file:
|
|
# .crush/active-ticket (default / single session)
|
|
# .crush/active-ticket-<name> (suffixed per session; any non-empty one
|
|
# satisfies the gate — sessions never fight
|
|
# over one file)
|
|
GATE_OK=0
|
|
for f in "$CRUSH_PROJECT_DIR"/.crush/active-ticket*; do
|
|
[ -f "$f" ] && [ -s "$f" ] || continue
|
|
GATE_OK=1
|
|
printf '{"context":"Active ticket(s): %s -> %s"}\n' "$(basename "$f")" "$(cat "$f")"
|
|
done
|
|
[ "$GATE_OK" -eq 1 ] && exit 0
|
|
|
|
# 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
|