From 95f475c2f9fb6044e0f5d74f97b51755300e7daf Mon Sep 17 00:00:00 2001 From: reachableceo Date: Tue, 11 Aug 2026 11:56:42 -0500 Subject: [PATCH] =?UTF-8?q?feat(hooks):=20add=20ticket-gate=20Crush=20hook?= =?UTF-8?q?=20=E2=80=94=20enforce=20ticket-first=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mechanically enforces the Agent Authority policy: blocks all modifying operations (bash, edit, write, multiedit) until an active ticket is set via: echo '#NNN' > .crush/active-ticket Exempts read-only tools, ticket management (redmine-cli/discourse-cli), repo hygiene (check-rules, shellcheck), and monitoring commands. Also: un-ignore .crush/crush.json so the hook config is tracked in git. 💘 Generated with Crush Assisted-by: Crush:glm-5.2 --- .crush/.gitignore | 3 ++ .crush/crush.json | 11 +++++++ .gitignore | 8 +++-- AGENTS.md | 5 +++ WORKING.md | 9 ++--- hooks/ticket-gate.sh | 78 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 .crush/.gitignore create mode 100644 .crush/crush.json create mode 100755 hooks/ticket-gate.sh diff --git a/.crush/.gitignore b/.crush/.gitignore new file mode 100644 index 0000000..23abd10 --- /dev/null +++ b/.crush/.gitignore @@ -0,0 +1,3 @@ +* +!crush.json +!.gitignore diff --git a/.crush/crush.json b/.crush/crush.json new file mode 100644 index 0000000..aecec0e --- /dev/null +++ b/.crush/crush.json @@ -0,0 +1,11 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "^(bash|edit|write|multiedit|lsp_replace_symbol|lsp_rename)$", + "command": "./hooks/ticket-gate.sh", + "timeout": 5 + } + ] + } +} diff --git a/.gitignore b/.gitignore index a2b3250..67aa997 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ -# Crush internal state -.crush/ +# Crush internal state (track crush.json config, ignore session data) +.crush/crush.db +.crush/crush.db-* +.crush/logs/ +.crush/memory/ +.crush/active-ticket # OS/editor .DS_Store diff --git a/AGENTS.md b/AGENTS.md index ef96d35..120d323 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,6 +130,11 @@ for a full audit or `--fast` for pre-commit speed. Bypass with `--no-verify` - **Redmine is the system of record for all work.** - **NEVER close a ticket without explicit user permission.** Suggest a close when clearly scoped/delivered; if ambiguous, don't suggest. +- **Ticket-first enforcement (mechanically enforced).** Before starting + any work, set the active ticket: `echo '#NNN' > .crush/active-ticket`. + The Crush hook (`hooks/ticket-gate.sh`) blocks modifying operations + until this file exists. If no ticket exists, CREATE ONE FIRST via + redmine-cli, then set it. Clear when done: `> .crush/active-ticket`. - **WORKING.md** is the only in-repo task tracker — a scratchpad for the current session. The pre-commit hook blocks commits while any task remains unchecked. - Clear WORKING.md before responding to the user. diff --git a/WORKING.md b/WORKING.md index 7a495ea..aacedd8 100644 --- a/WORKING.md +++ b/WORKING.md @@ -7,10 +7,5 @@ A commit is blocked while any task below remains unchecked. (all done — session complete) -- [x] Build VM MAC→IP mapping from ARP table + Proxmox data -- [x] Generate dhcpd.conf host reservations (18 production VMs, current IPs) -- [x] Deploy reservations to netinfra-01 (syntax check passed, DHCP reloaded) -- [x] Sync to netinfra-02 (manual trigger, confirmed) -- [x] Add PTR records in Technitium (18 PTR-only records, verified) -- [x] Pull updated dhcpd.conf into repo -- [x] Create #421 (inotify DHCP sync, someday/maybe) +- [x] Add ticket-gate Crush hook (blocks work without active ticket) +- [x] Document active-ticket workflow in AGENTS.md + meta template diff --git a/hooks/ticket-gate.sh b/hooks/ticket-gate.sh new file mode 100755 index 0000000..6a09fa9 --- /dev/null +++ b/hooks/ticket-gate.sh @@ -0,0 +1,78 @@ +#!/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