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.
This commit is contained in:
2026-08-31 14:42:00 -05:00
parent 3e0cdffd07
commit 8bff9567b7
4 changed files with 266 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
# AGENTS.md — GLOBAL HOUSE RULES
Loaded by EVERY crush session (all lanes, all repos) via crush's global
context path (~/.config/AGENTS.md -> symlink into the TSYSGroupAIOS
repo, the tracked home). Machine-consumable distillation of
TSYSGroupAIOS/BASELINE-PROMPT.md + workspace rulings; the human docs
stay canonical. Layering: THIS FILE < workspace AGENTS.md < repo
AGENTS.md. The more specific file wins; on conflict, stop and record a
question (see Questions). Effective 2026-08-31.
## Promotion tiers (alpha -> beta -> uat -> prod; each slower than the last)
- alpha: build + vet + test in the digest-pinned builder (`./dev.sh check`). Nothing deployed.
- beta: candidate image on the isolated beta lane (own Postgres, loopback bind; gateway: compose.beta.yaml, 127.0.0.1:4002) + scripted UAT.
- uat: the REAL client against the beta lane (crush: `crushgw-uat`; gateway: `./promote.sh uat`). Spend lands in beta accounting only.
- prod: the live stack. **HUMAN RELEASE ONLY** (gateway: `./promote.sh release`, typed by human hands). An agent NEVER promotes to prod, NEVER restarts prod for convenience. If prod must bounce, drain-aware: quiet logs, 30s graceful, in-flight turns may retry.
- Verified in one tier does not mean verified in the next. Each tier re-proves.
## SDLC doctrine (violating this invalidates the work)
1. Red/green TDD: failing test first, then code, then refactor.
2. Extensive suites: happy path AND sad path AND edge cases for every change.
3. Adversarial review before done: attack your own change (malformed input, null/empty, oversize, race, timeout, auth bypass, resource exhaustion).
4. Two review passes on non-trivial code: friendly (does it work?) and hostile (how does it break?).
5. STRICT sync, same commit: code, tests, docs, AND the Redmine ticket state move together. Never leave any out of sync. Ticket notes/status updated as milestones land (never close/done-ratio without human ruling).
6. Human UAT gates "done". Agent verifies technically (services, wiring, APIs responding); the human accepts. "Technically wired but blank dashboard" is NOT done. Never set done-ratio 100, never close tickets without explicit permission.
7. Verify the TRAIL, never just the answer: prove requests landed (gateway logs, spend rows in the RIGHT Postgres, wire captures). A passing answer from the wrong lane/endpoint is a FAIL.
## Hard rules
- ALL builds/tests in Docker via each repo's pinned builder. NEVER run toolchains on the host. Host runs only git, docker, unix utils.
- Secrets: ONE place — ~/.creds (0600 env files). NEVER in repos, images, compose files, app databases, logs, or error strings. Configs hold refs (env:/file:), never material.
- curl is banned. HTTP from host = python3 urllib.
- 100% compose lifecycle: every container is a compose service with container_name; storage in named volumes; no ad-hoc docker run for services, no manual renames. Scoped ops only — NEVER bare `down`.
- Image tags: digests or pinned versions, NEVER :latest.
- Container naming: `ukrrs-<fleet>-<function>[-beta]` (prod) / same with -beta (beta lane). Compose project mirrors the lane.
- DNS names over IP literals (documented exception: the gateway's tailscale service bind).
- Every command touching a remote system gets `timeout`: 30s reads / 120s standard / 300s deploys. A hung command is a FAILED command — diagnose, never blind-retry.
- Time-box: if stuck twice on the same approach, change approach or record a question.
- NEVER push to remotes unless the human asked (repo policies may demand commits; pushes stay human-gated).
## Enforcement (mechanical, belt and suspenders)
- Crush PreToolUse ticket-gate (all lanes): inside a git repo, modifying tools are BLOCKED until `echo '#NNN' > .crush/active-ticket` at the repo root. Clear when done: `> .crush/active-ticket`. Scratch outside repos is exempt.
- Git hooks are GLOBAL: `git config --global core.hooksPath` points at TSYSGroupAIOS/hooks/global (pre-commit/pre-push). They run universal checks (secret patterns, conflict markers, :latest tags), delegate to the repo's own scripts/check-rules.sh when present, and chain the repo's .git/hooks/<name> when it exists. In genuine emergencies bypass with --no-verify AND note it in the repo JOURNAL the same day.
- New fleet repos come from the go-cli-mcp-template Gitea template (enforcement layer baked in). Adopting an existing repo: copy TSYSGroupAIOS scripts/{check-rules.sh,setup-hooks.sh,pre-commit,pre-push} per ADOPTING.md.
- If a hook blocks you, fix the cause. Never disable enforcement to proceed.
## Systems of record
- Redmine = ALL work tracking (tickets, schedule, decisions-as-work-items). Reference `[#NNN]` in every commit. NEVER close a ticket without explicit human permission. Ticket-first inside repos (see gate above).
- Discourse = long-form docs + durable decisions/rationale. Repo .md files are stubs pointing there (exceptions: AGENTS.md, STATUS.md, questions-v*.md, runbooks that must live beside code).
- No per-repo JOURNAL.md (ruling 2026-08-31): work lives in Redmine, decisions in Discourse. Existing repo JOURNALs are read-only history — do not extend.
- STATUS.md = agent-owned, git-tracked, SUPER SPARSE: active ticket #s + one-line state each + an Inbox for mid-task interrupts. No narrative, no history — Redmine has that. Point-in-time only.
## Cross-linking (gitea ↔ redmine ↔ discourse — always in lockstep)
- Every gitea repo README links its Redmine project AND its Discourse doc/category.
- Every Redmine project overview links its gitea repo(s) AND Discourse category.
- Every Discourse doc/category links its gitea repo AND Redmine project.
- Per work item: ticket ↔ Discourse doc ↔ PR/commit ([#NNN] in subject or body).
- Links are created AT BIRTH of any artifact and updated in the same turn the work lands. Anyone landing on ANY of the three must reach the other two in one click. An artifact without its links is not done.
## Working style
- ROLLING TABLE HUD (every client — crush, openwebui): ONE markdown table, updated IN PLACE at each significant checkpoint (step done, blocker, pivot). Never append a second table. Max 10 rows. States: done ✅ / doing 🔄 / blocked ⛔ / next ⏳.
| ticket | tier | state | item |
|---|---|---|---|
| #587 | beta | 🔄 | promote pipeline UAT |
| #588 | alpha | ⏳ | JSON LSP container |
- Sub-agents are subcontractors: scoped ask in, DISTILLED findings out. NEVER read 10+ files sequentially; batch into 2-3 agent calls. Read only the 3-4 files you will edit.
- Farm out to deterministic tooling first: LSP, linters, formatters, test runners. Do not reason about what a tool can tell you.
- Interruptions mid-task: log to STATUS.md Inbox, do not pivot.
- Questions: NEVER harness question/modal tools. Write questions in the repo's git-tracked questions-v(N).md; human answers inline; version up per round. Ask early; do not ruminate.
## Crush wiring (v0.91.2 — learned the hard way)
- Provider wiring lives in the `crushrc` NEXT TO crush.json (`provider add ... --extra-header X-Consumer <label>`). A providers block in crush.json alone does NOT register a provider.
- Lane wrappers set CRUSH_GLOBAL_CONFIG + CRUSH_GLOBAL_DATA per lane:
`crushgw` = PROD (gateway :4000, X-Consumer crushgw) — default lane for all real work.
`crushgw-alpha` / `crushgw-beta` / `crushgw-uat` = beta lane (:4002) with per-tier attribution labels; uat is the human soak tier before any release.
Bare `crush` = DIRECT to z.ai, break-glass only (never gateway-locked).
- X-Consumer is an accounting label, never a credential. Check the RIGHT Postgres for attribution (beta vs prod).
- LSP fleet: always-hot `ukrrs-mopac-lsp-<lang>` containers via wrapper scripts (KNEL-AIMiddleware/lsp/lsp-*-wrapper.sh); wired in each lane's crush.json `lsp` block. LSPs start lazily — view the file, then check diagnostics (do not conclude "not installed" from a cold start; that was a false negative once, never again).
## LLM gateway (MOPAC) — fleet rules
- All LLM traffic flows through the gateway (z.ai plan is the shared, metered budget: demand shaping is fleet-wide; per-request policy ladder normal -> peak -> cooldown -> hard; /status publishes the signal).
- There are NO per-key quotas; never mint credentials. Identity is platform-level; X-Consumer is attribution only.
- Master key stays env-only (~/.creds/gateway.env), ops/bootstrap only.
- Gateway repo work follows its own promote.sh pipeline; see tiers above.
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# hooks/global/pre-commit — GLOBAL git hook (core.hooksPath for every
# repo on this host). Runs universal checks always, delegates to the
# repo's own enforcement when present, and chains the repo's installed
# .git/hooks/<name> so copy-installed hooks keep working.
#
# Layers, in order:
# 1. Universal: staged conflict markers, secret-looking material,
# :latest image tags in staged compose/Dockerfiles.
# 2. Repo-local: scripts/check-rules.sh --fast (if the repo ships it)
# 3. Repo chain: .git/hooks/pre-commit (if present)
# Bypass: --no-verify in genuine emergencies ONLY, and note it in the
# repo JOURNAL the same day.
set -uo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || exit 0)"
FAIL=0
# --- 1. Universal checks on staged content -------------------------------
while IFS= read -r -d '' f; do
# skip binary-looking blobs
case "$f" in
*.png|*.jpg|*.jpeg|*.gif|*.ico|*.woff|*.woff2|*.ttf|*.db|*.zip|*.gz) continue ;;
esac
[ -f "$REPO_ROOT/$f" ] || continue
if grep -nE '^(<<<<<<<|=======|>>>>>>>)' "$REPO_ROOT/$f" >/dev/null 2>&1; then
echo "FAIL conflict markers staged in $f" >&2; FAIL=1
fi
case "$f" in
*Dockerfile*|*docker-compose*|*compose*.y*ml)
if grep -nE 'image:[[:space:]]*[A-Za-z0-9._/-]+:latest' "$REPO_ROOT/$f" >/dev/null 2>&1; then
echo "FAIL :latest image tag in $f (pin digests or versions)" >&2; FAIL=1
fi ;;
esac
if grep -nE '(sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----)' "$REPO_ROOT/$f" >/dev/null 2>&1; then
echo "FAIL secret-looking material staged in $f (secrets live ONLY in ~/.creds)" >&2; FAIL=1
fi
done < <(git diff --cached --name-only -z 2>/dev/null)
# --- 2. Repo-local rule engine, when adopted -----------------------------
if [ -x "$REPO_ROOT/scripts/check-rules.sh" ]; then
if ! (cd "$REPO_ROOT" && bash scripts/check-rules.sh --fast --quiet); then
echo "FAIL repo-local check-rules.sh (fast)" >&2; FAIL=1
fi
fi
# --- 3. Chain the repo's own installed hook ------------------------------
if [ -x "$REPO_ROOT/.git/hooks/pre-commit" ]; then
if ! "$REPO_ROOT/.git/hooks/pre-commit"; then
echo "FAIL repo .git/hooks/pre-commit" >&2; FAIL=1
fi
fi
exit "$FAIL"
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# hooks/global/pre-push — GLOBAL git hook (core.hooksPath for every
# repo on this host). Runs universal checks always, delegates to the
# repo's own enforcement when present, and chains the repo's installed
# .git/hooks/<name> so copy-installed hooks keep working.
#
# Layers, in order:
# 1. Universal: staged conflict markers, secret-looking material,
# :latest image tags in staged compose/Dockerfiles.
# 2. Repo-local: scripts/check-rules.sh --fast (if the repo ships it)
# 3. Repo chain: .git/hooks/pre-push (if present)
# Bypass: --no-verify in genuine emergencies ONLY, and note it in the
# repo JOURNAL the same day.
set -uo pipefail
# pre-push receives refs on stdin; drain it so delegating hooks inherit a clean fd
cat >/dev/null 2>&1 || true
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || exit 0)"
FAIL=0
# --- 1. Universal checks on staged content -------------------------------
while IFS= read -r -d '' f; do
# skip binary-looking blobs
case "$f" in
*.png|*.jpg|*.jpeg|*.gif|*.ico|*.woff|*.woff2|*.ttf|*.db|*.zip|*.gz) continue ;;
esac
[ -f "$REPO_ROOT/$f" ] || continue
if grep -nE '^(<<<<<<<|=======|>>>>>>>)' "$REPO_ROOT/$f" >/dev/null 2>&1; then
echo "FAIL conflict markers staged in $f" >&2; FAIL=1
fi
case "$f" in
*Dockerfile*|*docker-compose*|*compose*.y*ml)
if grep -nE 'image:[[:space:]]*[A-Za-z0-9._/-]+:latest' "$REPO_ROOT/$f" >/dev/null 2>&1; then
echo "FAIL :latest image tag in $f (pin digests or versions)" >&2; FAIL=1
fi ;;
esac
if grep -nE '(sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----)' "$REPO_ROOT/$f" >/dev/null 2>&1; then
echo "FAIL secret-looking material staged in $f (secrets live ONLY in ~/.creds)" >&2; FAIL=1
fi
done < <(git diff --cached --name-only -z 2>/dev/null)
# --- 2. Repo-local rule engine, when adopted -----------------------------
if [ -x "$REPO_ROOT/scripts/check-rules.sh" ]; then
if ! (cd "$REPO_ROOT" && bash scripts/check-rules.sh --quiet); then
echo "FAIL repo-local check-rules.sh (fast)" >&2; FAIL=1
fi
fi
# --- 3. Chain the repo's own installed hook ------------------------------
if [ -x "$REPO_ROOT/.git/hooks/pre-push" ]; then
if ! "$REPO_ROOT/.git/hooks/pre-push"; then
echo "FAIL repo .git/hooks/pre-push" >&2; FAIL=1
fi
fi
exit "$FAIL"
+61
View File
@@ -0,0 +1,61 @@
#!/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