# 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 via the repo's compose `check` service (pinned builder). 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 through the repo's compose lifecycle services (pinned-builder `check`). NEVER run toolchains on the host. Host runs only git, docker, unix utils. NO dev.sh, NO Makefile — docker compose is the only lifecycle interface (k8s/gitops-portable; template: go-cli-mcp-template #578). - 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. - Host hygiene (ruling 2026-08-31, RETIRES the old curl ban): common shell commands on the host are fine, curl included. NOT fine: package installs on the host (apt-get/pip/etc) or piling work/toolchains onto it — ALL work runs in containers (python inside dev/build containers is fine). Keep the host a thin, clean layer. - Missing CLI on the host? DOCKER PULL IT (ruling 2026-08-31, standing permission): agents may pull tooling images freely — anything that makes the work easier (jq, ripgrep, etc. already fine from host; grab whatever else via `docker run --rm `). No permission needed; never apt/pip instead. - New gitea repos default to PUBLIC (ruling 2026-08-31): create with `--private=false`; go private only when the human asks or the content demands it (secrets are never repo content anyway — ~/.creds is the only home for those). - NO Python from agents, ever (ruling 2026-08-31, human preference): ALL text/JSON/CSV processing = bash, sed, awk, perl, cut, jq, sort/uniq. Never python one-liners; HTTP = curl. Same rule for anything you hand the human: shell only. - 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--[-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. - PUSH AS YOU GO (ruling 2026-08-31, standing permission): agents commit AND push work as it lands — do not wait for the human. The human's primary monitoring surface is Redmine/Gitea/Discourse activity, not ssh to hosts or running crush. Never force-push; respect branch protections. - pfv-tsys5 fleet rolls (ruling 2026-08-31): crush may reboot OTHER VMs on pfv-tsys5 (sectestbed fleet) via `qm` as needed. VM 5111 (the human workstation hosting crush sessions) reboots only when the human pulls the trigger. The PVE host itself NEVER reboots casually (NFS server for -02). Hardware window slipped to ~OCT 2026: presume VM 5111 stays 8 vCPU / 48G until that window — plan on interim values as the operating values. - Locale (ruling 2026-08-31): human + fleet sit in TEXAS, America/Chicago (Central; UTC-5 in summer). Times the human says ("1638 CST") are Central local. Never re-derive weekday/date from UTC log strings — docker/journal print UTC = local +5h; e.g. 2026-08-31 is a Monday, "Friday" = 2026-09-04. - Repo naming (ruling 2026-09-06): every repo in the KNEL org carries the KNEL prefix (CamelCase per KNELBMS/KNELIAC; KNELk8s is the sanctioned k8s spelling). Executed 2026-09-06 under [#819]; new repos MUST follow. Gitea keeps permanent redirects for old names, but canonical URLs are the prefixed ones. ## Production change management (CR gating — ruling 2026-09-06) - EVERY production change files a GLPI CR BEFORE the change lands. CR deep link format: `https://cmdb.knownelement.com/front/change.form.php?id=N`. - The PR body MUST reference the CR deep link; the Redmine ticket MUST reference the CR deep link. A PR without its CR link is not ready for review. - CRs are solved only WITH verification evidence as a followup (what was checked, logs/URLs). Closed is human-only. - Physical-plant / live-infra changes additionally need a Uptime Kuma maintenance window before apply. ## 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/ 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). Every commit carries the full Redmine comment URL (`https://projects.knownelement.com/issues/NNN#note-M`) — never a bare `#NNN` (see Reference format below). 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 ↔ glpi — 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 ↔ CR. Redmine ticket comments MUST carry the full Gitea commit/PR URLs (clickable). Discourse posts link the CR, the commit and the ticket. - Links are created AT BIRTH of any artifact and updated in the same turn the work lands. Anyone landing on ANY of the systems must reach the others in one click. An artifact without its links is not done. ## Reference format (ruling 2026-09-06 — no bare `#NNN` in Gitea) - In Gitea (commit messages, PR bodies, comments), a bare `#NNN` resolves against Gitea's OWN issue tracker — it NEVER links Redmine. All Redmine references in Gitea are FULL URLs, and the URL of record is the issue COMMENT anchor: `https://projects.knownelement.com/issues/NNN#note-M`. - In Redmine, bare `#NNN` is correct (resolves locally). Gitea artifacts referenced FROM Redmine use full URLs: `https://git.knownelement.com/KNEL//commit/`. - Rule of thumb: whichever system you are writing IN gets the short form; every OTHER system gets the full clickable URL. ## Commit messages (ruling 2026-09-01) - SHORT: subject <=72 chars; body a FEW lines max, wrapped at 72. NEVER cram a paragraph into one long line. - The MEAT (detail: what/why/how/verification) goes in a Redmine ticket COMMENT on the work item, in the same turn the work lands — not in the commit body. - The commit body carries a CLICKABLE full URL to that comment: https://projects.knownelement.com/issues/NNN#note-M - All markdown written for humans (Redmine comments, Discourse posts, commit bodies) uses real clickable links and human formatting (headings, bullets, short wrapped lines) — no walls of text, no unclickable references. ## 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