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/AGENTS.md b/AGENTS.md index 518b225..950df0e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,6 +60,7 @@ Replace this bracketed text.> - **Redmine** is the single system of record for ALL project work — tickets, schedules, Gantt, dependencies. Use the `redmine-cli`. **Do not use Gitea issues.** Reference tickets as `[#NNN]`. - **NEVER close a Redmine ticket without explicit user permission.** You may SUGGEST a close when the result is clearly scoped and delivered. If it's ambiguous whether the work is truly complete, don't suggest a close — leave that decision to the user. This applies to ALL tickets, no exceptions. +- **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`. - **Discourse** is the single system of record for documentation. Use the `discourse-cli`. **Do not author long-form docs in gitea.** - **Git-tracked `.md` files are stubs** that point to the relevant Discourse topic URL. Operational files that must live next to code (`AGENTS.md`, `STATUS.md`, `questions-v*.md`) are the documented exceptions. - `STATUS.md` is a scratchpad for token efficiency, **not** a system of record. 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