feat: bootstrap meta — cross-project best-practices template

Extract patterns from 18 projects across two machines (12 local infra +
6 remote personal/business via ssh survey). Ship a reusable Gitea template
with AGENTS.md, 5 Crush PreToolUse hooks, git pre-commit/pre-push, a
generalized rules engine, shared bash library, Makefile, lifecycle scripts,
and gardening loop. Includes the canonical BASELINE-PROMPT.md (13 sections)
and PATTERNS.md (standardization scorecard).

The repo self-applies: it passes its own shellcheck (zero info-level),
make fast, and all check-rules.sh checks.

💘 Generated with Crush

Assisted-by: Crush via Crush <crush@charm.land>
This commit is contained in:
2026-08-07 11:17:53 -05:00
commit 374288a105
27 changed files with 1612 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
# .env.example — copy to .env and fill in real values. .env is gitignored.
#
# Convention (observed across projects, standardized here):
# - Dummy placeholder for secrets that have a safe stand-in.
# - Empty value for keys with no safe placeholder.
# - Real default for non-secret configuration.
# - Inline comment explaining WHY each value is what it is.
# --- Secrets (NEVER commit real values) ---
EXAMPLE_API_TOKEN=your_token_here
EXAMPLE_API_KEY=
# --- Non-secret configuration ---
# Override per environment; these are safe defaults.
LOG_LEVEL=info
TZ=America/Chicago
+17
View File
@@ -0,0 +1,17 @@
# Secrets / local config
.env
*.env
!.env.example
# Crush local state (db, logs) — keep the directory, ignore its contents
.crush/crush.db
.crush/crush.db-*
.crush/logs/
# Scratch / survey data
.tmp/
# OS / editor noise
.DS_Store
*.swp
*~
+194
View File
@@ -0,0 +1,194 @@
# <Project Name> — Agent Guidelines
> **Active agent:** Crush running <model> (<provider>). Permission mode: <yolo|normal>.
> **Read [`STATUS.md`](STATUS.md), [`.crush/memory/operational.md`](.crush/memory/operational.md), and [`questions-v1.md`](questions-v1.md) first, every session.**
> **Governing baseline:** [`BASELINE-PROMPT.md`](BASELINE-PROMPT.md) (parent project).
This file is the project-level policy. The hooks and `scripts/check-rules.sh`
enforce the mechanical rules below; this document states the policy and intent.
Inherit from the baseline; specialize here.
---
## Quick Start
**You are an AI agent working on this project. Your first actions, in order:**
1. **Set up the environment:** `make setup` (installs git hooks — idempotent).
2. **Read [STATUS.md](STATUS.md)** — current state, inbox, blockers, tactical notes.
3. **Read [`.crush/memory/operational.md`](.crush/memory/operational.md)** — access details, key IDs, gotchas.
4. **Read [questions-v1.md](questions-v1.md)** — open questions awaiting human input.
5. **Read [docs/JOURNAL.md](docs/JOURNAL.md)** — decisions and patterns already established.
6. **Check current state:** `git log --oneline -10` and `make status`.
## Project Overview
<13 paragraphs: what this project is, the stack, and the hard constraints.
Replace this bracketed text.>
## Phase
> We have exited the "move fast and loose" phase. This is production
> infrastructure. The bar is the bar. (See `BASELINE-PROMPT.md`.)
<Adjust per project: is this in production? serving what? what's the blast radius?>
## Repository Layout
```
<root>/
├── AGENTS.md ← THIS FILE — project policy
├── STATUS.md ← agent scratchpad (token-efficiency; NOT the system of record)
├── WORKING.md ← the ONLY task tracker (todos tool is banned)
├── questions-v1.md ← git-tracked questions for the human (version up per round)
├── crush.json ← Crush PreToolUse hooks (behavioral guardrails)
├── .crush/memory/ ← operational memory read each session
├── hooks/ ← Crush agent hooks (block-todos, enforce-*, audit-*, exit-protocol)
├── scripts/
│ ├── setup-hooks.sh ← install git hooks into .git/hooks/
│ ├── pre-commit ← fast rule audit (hot-path bypass for status/journal)
│ ├── pre-push ← full audit + clean-tree gate
│ ├── check-rules.sh ← the rule audit engine (pass/warn/fail accumulator)
│ ├── docker-run.sh ← canonical ephemeral-container wrapper
│ ├── up.sh / down.sh ← docker compose lifecycle wrappers
│ ├── garden.sh ← doc-sprawl / Discourse-migration report
│ └── lib/common.sh ← shared bash library (colors, log_*, docker_run, check)
├── docs/
│ └── JOURNAL.md ← append-only decision & pattern log
├── Makefile ← standard targets: setup/validate/lint/test/garden/up/down/status
├── docker-compose.yml.example ← lifecycle template (copy to docker-compose.yml)
└── .env.example ← copy to .env, fill in secrets
```
## Systems of Record (do not duplicate)
- **Redmine** is the single system of record for ALL project work — tickets, schedules, Gantt, dependencies. Use the `redmine-cli` tool. **Do not use Gitea issues.**
- **Discourse** is the single system of record for documentation. Use the `discourse-cli` tool. **Do not author long-form docs in gitea.**
- **Git-tracked `.md` files are stubs** that point to the relevant Discourse URL. Operational files that must live next to code (`AGENTS.md`, `STATUS.md`, `questions-v*.md`, `docs/JOURNAL.md`) are the documented exceptions.
- `STATUS.md` is a scratchpad for token efficiency, **not** a system of record.
## Git Workflow
- **Use the `tea` CLI for pull requests.**
- **Work smart off master.** Branches on the workstation are encouraged for moving fast, exploring, and avoiding stash churn.
- **Once work leaves the workstation, it goes through a PR.**
- **Commit & push policy:** ALWAYS commit and push automatically; NEVER wait to be asked. This overrides any default "never commit unless asked" behavior.
1. **Atomic commits** — one logical change per commit.
2. **Commit after every logical unit of work; push immediately.**
3. **Never leave uncommitted or unpushed changes at session end.**
4. **If blocked:** commit and push what IS done, then report the blocker.
- **Conventional commit format:**
```
<type>(<scope>): <subject — 50 chars max, imperative, no period>
<body: WHAT changed, WHY, and the context>
💘 Generated with Crush
Assisted-by: <AI-Model> via Crush <crush@charm.land>
```
Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `security`.
- **Never interrupt current work** when the human tosses new work mid-task. Log it in the **Inbox** section of `STATUS.md`. If materially different, spin up a Redmine ticket.
## Task Tracking
- **WORKING.md is the ONLY task tracker.** The `todos` tool is banned by hook.
- Only mark `[x]` after the work is verified complete.
- A commit is **blocked** (pre-commit + audit-before-git hook) while any task remains unchecked.
- Clear WORKING.md (to "all done") before responding to the user; the exit-protocol hook will not let you stop otherwise.
- **The human decides when the work is done and when to deploy.** Never declare "done" unilaterally.
## Questions
- Capture questions for the human in a git-tracked `questions-v(N).md` file.
- The human reviews and edits it inline. Version up when a round of answers lands.
- Questions, answers, and the reasoning behind decisions are often more important than the code. Synthesize resolved Q&A into Discourse (decisions) and Redmine (work items).
## Working Style
- **Stop over-thinking.** Get to code and output faster. Explore with code; gather ground truth. Do not burn tokens reasoning about things a quick command answers.
- **Ask questions early** via `questions-v(N).md`. Don't ruminate or self-debate at length.
- **Farm work out to deterministic tooling:** linters, LSPs, formatters, test runners. If Crush has an LSP wired up, use it; otherwise pull a Docker image and lint inside it. Do not parse huge code blocks in context.
- **Use sub-agents as subcontractors** for well-defined parallel deliverables, not as staff augmentation.
## CI/CD
- The **local workstation must be able to run the same CI/CD** the hosted infrastructure runs. Maintain them in lockstep.
- **Shift left:** catch issues at `make fast` (pre-commit) before push, before PR, before merge.
## Conventions
- **Docker/Kubernetes for everything** — cluster of 1 or 100 is the same. Don't presume scale.
- **All development work happens in containers** — custom, off-the-shelf, or a mix. `docker pull` freely without asking. Use `scripts/docker-run.sh <pinned-image>` for one-offs.
- **Container naming: never use Docker's default.** Always name with a project prefix (e.g. `<project>-<service>`). Enforced by `check-rules.sh`.
- **Pin every Docker image** — no `:latest` tags (enforced).
- **Use Docker Compose with lifecycle scripts** (`scripts/up.sh`, `scripts/down.sh`) to bring services up/down.
- **Shell scripts** use `#!/usr/bin/env bash` + `set -euo pipefail` and must pass `shellcheck` with zero warnings, including info-level.
- **Shared helpers** live in `scripts/lib/common.sh`; source it rather than re-pasting boilerplate.
- **Names:** lowercase, hyphen-separated.
- **Secrets:** NEVER commit secrets. Credentials come from env vars / `.env` (gitignored). Use placeholders in `.env.example`.
- **IAC testing:** test against the corresponding `sectestbed-` VM (snapshot to base state). `preprod-` VMs for vendor-upgrade testing. Do not require AWX as a prerequisite.
## Key Commands
```bash
make setup # install git hooks (run once after clone)
make fast # fast rule audit (pre-commit equivalent)
make validate # full audit (includes the test suite)
make lint # shellcheck via docker
make test # run the test suite (define per project)
make garden # doc-sprawl / Discourse-migration report
make up # bring up the docker-compose stack
make down # bring it down
make status # repo status snapshot
```
## Status & Journal Maintenance
After any work session, you MUST:
1. **Update [STATUS.md](STATUS.md)** — reflect completed work, inbox, blockers. Update "Last updated". (check-rules.sh warns if stale.)
2. **Append to [docs/JOURNAL.md](docs/JOURNAL.md)** — one section per change (what, why, pattern, challenges, commit hash). Never delete or reorder. (check-rules.sh warns if no today-entry.)
3. **Synthesize to Redmine/Discourse** — status and journal are not the system of record; push decisions and durable docs there.
4. **Grep for stale paths** after any rename/restructure: `grep -rn 'old/path'` and fix in the same commit.
5. **Run `make garden`** periodically — migrate oversized `.md` to Discourse, leaving stubs.
## Enforcement Model (belt and suspenders)
Policy is enforced in two complementary layers:
- **Git hooks** (`.git/hooks/`, installed from `scripts/` by `make setup`) — *mechanical* checks at commit/push: shellcheck, image pinning, container naming, required files, doc freshness, WORKING.md completion, hygiene, full test suite on push.
- **Crush hooks** (`hooks/`, wired in `crush.json`) — *behavioral* guardrails at agent-tool-call time: ban the todos tool, block banned commands and host language tools, block edits to banned file types, run the fast audit before any commit/push, enforce the exit protocol.
Bypass with `--no-verify` in genuine emergencies only.
## TDD & Linting
- **Red/green TDD for all code.** Write the failing test first.
- **Linters on all code, as early as possible.** Let deterministic tools find the issues.
## DO
- Read STATUS.md, questions file, operational memory, and JOURNAL.md BEFORE starting work.
- Write a failing test first (TDD).
- Read files before editing. Use exact text matching.
- Run `make validate` before committing.
- Use sub-agents to parallelize scoped work.
- Log interruptions to the STATUS.md Inbox.
## DO NOT
- Wait to be asked before committing/pushing.
- Batch unrelated changes into one commit.
- Edit files you haven't read.
- Install language toolchains on the host.
- Use `:latest` image tags or Docker's default container naming.
- Author long-form docs in gitea — use Discourse.
- Commit secrets.
- Run destructive git operations without explicit instruction.
- Declare the work "done" — the human decides that.
- Pivot to new mid-task requests — log them in the Inbox.
---
**Last Updated:** TEMPLATE-REPLACE-ME
+105
View File
@@ -0,0 +1,105 @@
# Global Baseline Prompt
> The canonical set of working principles for every agent (AI or human) across
> every project. This is the source of truth; project `AGENTS.md` files inherit
> and specialize it. Derived from the owner's operating notes, deduplicated and
> stripped of project-specific detail.
>
> **Phase context:** we have exited the "move fast and loose" phase. This is
> production infrastructure — it is in production right now. The bar is the bar.
---
## 1. You are an employee, not a lone genius
- **Stop over-thinking.** Get to code and output faster. Explore with code; gather ground truth. Do not burn tokens reasoning about things a quick command can answer.
- **Ask questions early.** Use a git-tracked `questions-v(N).md` file (see §9) that the human reviews inline. Questions, answers, and the reasoning behind decisions are often more important than the code. Capture and synthesize them to Discourse/Redmine.
- **Don't ruminate or self-debate** at length in context — gather data from the human, from code, or both, and proceed.
- **You are not alone.** Ask for guidance when you need it.
## 2. Token efficiency is a hard constraint
- The owner has a quota. Burning tokens to parse huge code blocks or reason about easily-answerable questions is unacceptable.
- **Farm work out to deterministic tooling:** linters, LSPs, formatters, test runners — run them, read their output, don't reason about what they can tell you.
- If Crush has an LSP wired up for the language (code or docs), use it. If not, pull a Docker image and lint inside it. Prose linting belongs in a Docker image too.
- Use `STATUS.md` as a durable, git-tracked scratchpad for high-fidelity tactical notes (see §8) so context doesn't have to be re-derived.
- Use sub-agents as **subcontractors** for well-defined parallel deliverables (see §12), not as staff augmentation.
## 3. Systems of record (do not duplicate)
- **Redmine is the single system of record for ALL project work** — tickets, tasks, schedules, Gantt, dependency modeling. Use the `redmine-cli` tool. Gitea issues are not used.
- **Discourse is the single system of record for documentation.** It is all Markdown. Use the `discourse-cli` tool. Do not author long-form docs in gitea.
- **Git-tracked `.md` files should be stubs** that point to the relevant Discourse URL. Short operational files that must live next to code (e.g. `AGENTS.md`, `STATUS.md`) are the exception.
- Engineering already works this way. Operations now does too.
## 4. Git workflow
- **Use the `tea` CLI for pull requests.**
- **Work smart off master** generally. Branches on the workstation are encouraged for moving fast, exploring ideas, and avoiding stash churn.
- **Once work leaves the workstation, it goes through a PR.**
- Branching strategy is open to per-project discussion.
## 5. CI/CD — shift left, keep in lockstep
- Strong preference that the **local workstation can run the same CI/CD** that the hosted infrastructure runs. Maintain them in lockstep across all projects.
- **The further left CI/CD runs, the better.** Catch it before push, before PR, before merge.
- We have a mix of developers and agents, some inside the hosted security boundary, some on beefy workstations. CI/CD must work for all of them.
## 6. Docker and Kubernetes for everything
- Use Docker and Kubernetes for everything — a cluster of 1 or 100 is the same. Don't presume scale. Containers are containers; k8s is k8s.
- **All development work happens in containers** — custom, off-the-shelf, or a mix. `docker pull` freely without asking.
- **Container naming: never use Docker's default.** Always name with a project prefix (e.g. `<project>-<service>`).
- Use Docker Compose with hook scripts to bring services up/down (lifecycle scripts). See `~/projects` for established examples.
## 7. Infrastructure-as-Code testing
- When working on IAC, test against the corresponding **`sectestbed-` VM**. These are snapshot-able to a known base state (Tailscale-joined, Beszel-registered, SSH keys in place). The base state evolves; the delta of tested code shrinks over time.
- A new functional-area VM starts in that ultra-basic base state and has roles applied on top.
- **`preprod-` VMs** are for testing upgrades to new vendor software versions — they carry a snapshot of current prod. Snapshot/rollback semantics are work-stream-specific and need explicit discussion.
- Compliance mitigations may need to flow through both `sectestbed-` and `preprod-` testing, in lockstep. Redmine Gantt and dependency-relationship modeling are heavily used here.
- **Portability and reproducibility by anyone** — do not require AWX as a prerequisite (optional nice-to-have; not mandatory for bootstrap).
## 8. STATUS.md — scratchpad, not system of record
- **STATUS.md is a durable, git-tracked scratchpad for token efficiency.** It is not the system of record (Redmine is).
- The agent fully owns STATUS.md; the human only consumes it.
- Use it for high-fidelity tactical notes as you work — input for commit logs, PRs, and Redmine updates.
- The harness todo tool is fine for tracking *current* work; STATUS.md is the durable cross-session record.
- **STATUS.md has an Inbox section.** When the human tosses new work mid-task in another conversation turn, do NOT pivot. Log it in the Inbox. If it's materially different, spin up a Redmine ticket.
## 9. Questions file — `questions-v(N).md`
- Capture questions in a git-tracked, versioned file: `questions-v1.md`, `questions-v2.md`, …
- The human reviews and edits it inline. Version it when a round of answers goes in.
- Synthesize resolved Q&A into Discourse (decisions/rationale) and Redmine (work items).
## 10. Belt-and-suspenders protocol enforcement
- Enforce the rules in **two layers**: prose policy in `AGENTS.md` and mechanical enforcement in git `pre-commit` / `pre-push` hooks (and Crush `PreToolUse` hooks where applicable).
- Never rely on memory or prose alone.
## 11. Gardening — keep docs from sprawling
- Run a routine **gardening loop.** Agents are disciplined with code but tend to sprawl `.md` files everywhere.
- Keep docs, code, and tests in sync at all times.
- `make garden` reports `.md` sprawl and files that should be migrated to Discourse.
## 12. Sub-agents as subcontractors
The owner has a quota; the driving context is the expensive one. The biggest
token cost is prefix mutation, not per-call work — so keep the driving prefix
stable and push volatility into side-channels (sub-agents, STATUS.md).
- **Mandate:** use sub-agents for any non-trivial search, audit, parallel review, or large-output read. **Never read 10+ files sequentially** — batch them into 2-3 agent calls.
- **Self vs. delegate:** read the 3-4 files you will immediately edit yourself (you need their content in-context for the edit anyway); dispatch agents for everything else.
- **Output contract:** request **distilled findings only**, never raw file contents. Specify the output format in the prompt. A sub-agent that returns a 500-line file dump has failed the contract.
- **Parallelize independent work; chain dependent work** (one agent's distilled summary feeds the next).
- **Why:** keeps the main context lean and preserves the cached prompt prefix.
This is scoped, contract-style work with a clear handback — not staff augmentation.
## 13. TDD and linting
- **Red/green TDD for all code.** Write the failing test first.
- **Linters on all code, as early as possible.** Be token-efficient — let deterministic tools find the issues.
+60
View File
@@ -0,0 +1,60 @@
# Makefile — the single standard task-entry surface for this project.
#
# Every project using the template exposes the SAME targets, so CI, hooks,
# and humans all know one set of verbs regardless of the underlying stack.
# Each target delegates to scripts/ so the logic is shellcheck-able and
# runnable anywhere (the Makefile is pure dispatch).
#
# Targets:
# make setup install git hooks (idempotent)
# make validate run the full rule audit
# make fast run the fast rule audit (pre-commit equivalent)
# make lint lint (shellcheck via docker)
# make test run the test suite <-- override for your stack
# make garden doc-sprawl / Discourse-migration report
# make up bring up the docker-compose stack
# make down bring it down
# make status show repo status snapshot
# make clean remove build/test artifacts <-- override for your stack
#
# Override `test` and `clean` per project; the rest are stable.
.PHONY: setup validate fast lint test garden up down status clean help
help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
setup: ## Install git hooks
@bash scripts/setup-hooks.sh
validate: ## Full rule audit (includes tests)
@bash scripts/check-rules.sh
fast: ## Fast rule audit (pre-commit equivalent)
@bash scripts/check-rules.sh --fast
lint: ## Lint shell scripts (shellcheck via docker)
@docker run --rm -v "$$(pwd):/mnt" koalaman/shellcheck:stable \
$$(find . -path ./.git -prune -o -path ./.tmp -prune -o -path ./vendor -prune -o -path ./node_modules -prune -o \( -name '*.sh' -o -name '*.bash' \) -print | sed 's|^\./|/mnt/|') || true
test: ## Run the test suite (override per project)
@echo "make test: no test target defined yet — override this in your project's Makefile." >&2
@exit 1
garden: ## Doc-sprawl / Discourse-migration report
@bash scripts/garden.sh
up: ## Bring up the docker-compose stack
@bash scripts/up.sh
down: ## Bring down the docker-compose stack
@bash scripts/down.sh
status: ## Show a repo status snapshot
@echo "== branch =="; git branch --show-current 2>/dev/null || echo "(no branch)"
@echo "== last commit =="; git log --oneline -1 2>/dev/null || true
@echo "== working tree =="; git status --short 2>/dev/null || echo "(not a git repo)"
@echo "== STATUS.md head =="; sed -n '1,12p' STATUS.md 2>/dev/null || echo "(no STATUS.md)"
clean: ## Remove build/test artifacts (override per project)
@echo "make clean: nothing to clean — override this in your project's Makefile."
+247
View File
@@ -0,0 +1,247 @@
# Cross-Project Pattern Extraction
**Scope:** 18 projects surveyed across two machines:
- **Local (`ultix-streaming`):** 12 infra/stack projects — `dotfiles`, `EngineeringWorkstation`, `EngStack`, `football`, `hermes-rceo-streaming`, `KNEL-AIMiddleware`, `KNELIAC`, `netbird`, `PFVCluster`, `TSYSDevStack-SupportStack-Cloudron`, `WorkstationStack`, `TSYSDevStack-SupportStack-LocalWorkstation`.
- **Remote (`ultix-offstage`):** 6 personal/business projects — `CharlesNWybleResume`, `rceo-automation-espanso-private`, `RCEO-PersonalAssistant`, `ReachableCEO-Profile-FullTimeEmployment`, `RevGen-TimeForMoney`, `ThreeYearPlan`.
This document records the patterns that recur, the inconsistencies between them,
and the decisions the repo makes to drive inconsistency to zero.
---
## 1. AGENTS.md — recurring structure
A canonical section set emerges from the 12 files that have one (8 local + 4 remote):
| Section | Prevalence |
|---|---|
| Git commit & push policy | 12/12 |
| Project overview / context | 11/12 |
| Key commands / build cheat sheet | 10/12 |
| Repository layout (ASCII tree) | 9/12 |
| Status / journal maintenance | 9/12 |
| Conventions & naming | 8/12 |
| DO / DON'T | 7/12 |
| Validation / testing | 7/12 |
| Key files reference | 6/12 |
| Quick start / onboarding | 5/12 |
**Decision:** the template's `AGENTS.md` ships all ten sections in a fixed order,
so every project has the same skeleton. Filler is bracketed for replacement.
---
## 2. Shared cross-project preferences (the house style)
These recur strongly and are baked into the template:
1. **Auto-commit + push is mandatory and non-negotiable.** Stated with ALL-CAPS force in nearly every file. The template states the override-once and lets the hooks enforce it.
2. **Atomic commits — one logical change per commit.** Universal.
3. **Conventional-commit format** (`feat:`/`fix:`/`docs:`/…). Universal, but the **💘 Crush attribution footer is inconsistent**: mandated only by `football` and `KNELIAC`; absent on the remote entirely. **Decision:** the template mandates the footer once, in the AGENTS.md commit block.
4. **Docker-only host hygiene.** The single most consistent convention across *both* machines (`KNEL-AIMiddleware`, `EngStack`, `LocalWorkstation`, `RCEO-PersonalAssistant`, `ThreeYearPlan`, `CharlesNWybleResume`). **Decision:** enforced by hook (`hooks/enforce-bash.sh`) and rule, and supported by `scripts/docker-run.sh`.
5. **Sub-agents encouraged.** Explicit in `football`, `ThreeYearPlan`, `RCEO-PersonalAssistant`. **Decision:** standard section in the template.
6. **No secrets in git (infra genre).** Inverted on the remote private repos (`RCEO-PersonalAssistant` deliberately commits credentials). **Decision:** the template keeps the infra stance (`.env` gitignored) as the default; private repos may opt out.
**Inconsistencies the template resolves:**
- `shellcheck` is mandated by `PFVCluster` and `football` but unmentioned elsewhere → the template mandates it for all shell, via Docker.
- "Keep the host clean" is phrased four different ways → one canonical phrasing.
- Auto-commit override language varies in strength → stated once, authoritatively.
---
## 3. Git hooks — the biggest gap, now standardized
**Finding:** custom git hooks are essentially unused. Only **3 of 18** projects have any:
| Project | Hook | Mechanism | Installer |
|---|---|---|---|
| `football` | `pre-commit` (4-check SDLC gate: shellcheck, unit tests, coverage, doc-sync) | `core.hooksPath githooks/` | `scripts/setup-githooks.sh` |
| `KNEL-AIMiddleware` | `pre-push` (block on dirty tree) | `core.hooksPath .githooks/` | **none** (manual `git config`) |
| `RCEO-PersonalAssistant` | `pre-commit` (fast audit) + `pre-push` (full Docker tests) | **copy** into `.git/hooks/` | `scripts/setup-hooks.sh` |
Three different install mechanisms; one project has no installer at all.
**Decision:** the template uses the **copy** approach (most portable: works on any clone, no config mutation, idempotent) with a single `scripts/setup-hooks.sh`, and combines both policy philosophies:
- `pre-commit` = fast audit with a **hot-path bypass** for `STATUS.md` / `JOURNAL.md` / `WORKING.md` (so frequent status commits stay frictionless) — proven in RCEO.
- `pre-push` = full audit (incl. `make test`) — proven in RCEO — plus the dirty-tree gate — proven in KNEL-AIMiddleware.
---
## 4. Crush hooks — an entirely untapped capability (except one project)
**Finding:** Crush `hooks` are used by **exactly one project**`RCEO-PersonalAssistant`. Every other `crush.json` (and most projects have none) defines only `lsp`/`mcp`/`options`, never `hooks`. All process enforcement elsewhere is prose in AGENTS.md — manually enforced, violable.
RCEO's crush.json wires five `PreToolUse` hooks that form a behavioral guardrail layer complementing the git hooks:
| Hook | Matcher | Effect |
|---|---|---|
| `block-todos.sh` | `^todos$` | bans the todos tool; WORKING.md is the only tracker |
| `enforce-bash.sh` | `^bash$` | blocks banned commands + host language toolchains |
| `enforce-rules.sh` | `^(edit\|write\|multiedit)$` | blocks banned file types; TDD reminder on source edits |
| `audit-before-git.sh` | `^bash$` | runs the fast audit before any `git commit`/`git push` |
| `exit-protocol.sh` | `.*` | blocks stopping while WORKING.md has unchecked tasks |
**Decision:** the template generalizes all five into `hooks/`, configurable via env vars (`PROJECT_BANNED_COMMANDS`, `PROJECT_BANNED_SUFFIXES`, `PROJECT_SOURCE_SUFFIXES`, …) so they are project-agnostic. This is the template's highest-leverage feature: it converts the one-off RCEO invention into a reusable baseline.
---
## 5. The rules engine — `check-rules.sh`
RCO's `check-rules.sh` is the best reusable artifact found. Its core abstractions:
- A `check()` accumulator classifying each rule as **pass / warn / fail**, with `FAIL > 0 ⇒ exit 1` and `WARN` non-fatal.
- A `--fast` mode (used by pre-commit) that silences per-check output and skips slow checks.
- Self-checking categories: shellcheck, Docker image pinning (no `:latest`), required-files manifest, doc freshness (STATUS.md/JOURNAL.md touched today), git state, hooks-installed, WORKING.md completion, unresolved `CNW:` markers.
**Decision:** the template's `check-rules.sh` generalizes this — language-specific checks (Go test coverage, gofmt) are dropped or made opt-in via `PROJECT_*` env; universal checks (shellcheck, image pinning, required files, doc freshness, hygiene, WORKING.md, CNW markers, merge-conflict markers) are kept. The `check()` accumulator and `--fast`/`--quiet` flags move into `scripts/lib/common.sh` so any script can reuse them.
---
## 6. Scripts — driving the shebang/boilerplate chaos to zero
**Findings:**
- **Three incompatible shebang variants** across projects: `#!/usr/bin/env bash` (best), `#!/bin/bash`, `#!/usr/bin/bash` (non-portable).
- **Four different `set`-flag policies**: `set -euo pipefail` (best), `set -e` only, `set -uo pipefail`, none. KNEL-AIMiddleware uses `set -e` only — piped failures silently swallowed; `BuildAll.sh`'s `docker compose … | tail` can report green on failure.
- **The ANSI color block is copy-pasted into ~10 scripts.**
- **`log_*` helpers are redefined per project** (and per-script in KNEL-AIMiddleware, where they aren't even functions).
- **Repo-root resolution** is re-rolled in nearly every script.
- **No shared cross-project library exists.** Only `EngStack` factors helpers (`scripts/lib.sh` build-time + `lib/common.sh` runtime).
**Decision:** `scripts/lib/common.sh` provides the deduplicated primitives once: ANSI colors, `log_info/ok/warn/error/step`, `have()`, `die()`, `repo_root()`, `as_root()`, `docker_run()`, and the `init_counters`/`check`/`print_summary_and_exit` audit helpers. All template scripts use `#!/usr/bin/env bash` + `set -euo pipefail`.
---
## 7. Task runners — five conventions, standardized to one
**Findings:** no `Makefile`/`Taskfile`/`justfile` exists anywhere. Five incompatible conventions for the same semantic verbs:
| Project | Convention |
|---|---|
| football | `./run.sh <subcommand>` |
| KNEL-AIMiddleware | `scripts/<Verb>.sh` |
| EngStack | `scripts/<verb>-<noun>.sh` |
| PFVCluster | `tests/<name>.sh` |
| hermes-agent | npm workspace scripts |
The recurring semantic targets — **build, test, lint, validate, status, clean, setup** — map cleanly onto one target set that doesn't exist.
**Decision:** a single `Makefile` provides `setup`, `validate`, `fast`, `lint`, `test`, `status`, `clean`, `help`. It is pure dispatch to `scripts/`; projects override `test`/`clean` for their stack. CI, hooks, and humans now share one set of verbs.
---
## 8. CI — present in exactly one project
Only `hermes-agent` has CI (22 GitHub Actions workflows — an exemplar: change-detection orchestrator, reusable-workflow lanes, SHA-pinned actions, supply-chain/OSV scans, live PR-comment bot). The other 17 have none.
**Decision:** the template doesn't ship CI (it's stack-dependent), but `make validate` + `make lint` give any future workflow a uniform entry point. The hermes-agent `ci.yml` orchestrator is the documented growth path.
---
## 9. Crush configuration layers — documented but unused
Crush defines a priority chain (`$HOME/.config/crush/crush.json``crush.json``.crush.json`). In practice:
- The **global** config (`dotfiles`) is trivial (attribution style only) — no shared LSP/MCP baseline.
- Only **2 projects** have a project-local `crush.json` (KNEL-AIMiddleware: 4 LSP + 36 MCP via wrappers; RCEO-PersonalAssistant: hooks only).
- The highest-priority `.crush.json` layer is **unused anywhere**.
- **No project sets `model` or `provider`** in config.
**Decision:** the template ships only the `hooks` block in `crush.json` (the universally-applicable part). LSP/MCP remain project-local concerns. A future shared global config could baseline common LSPs (bash, markdown) and universal MCPs (context7) once.
---
## 10. Crush memory — a singleton, now a template
Only `PFVCluster` has operational memory (`.crush/memory/operational.md`). Its format — Identity → Tracking Systems (with READY/PARTIAL/BLOCKED status) → Tooling → Access chokepoints → Key commands → Mandatory rules → Credential TODOs — is strong and reusable.
**Decision:** the template ships `.crush/memory/operational.md` as a fill-in skeleton.
---
## 11. KNEL-AIMiddleware wrapper ecosystem — a standardization opportunity (not in the template)
`KNEL-AIMiddleware` hand-maintains **38 near-identical** wrapper scripts (`mcp-*-wrapper.sh`, `lsp-*-wrapper.sh`) for its MCP/LSP fleet. ~90% of each file is duplicated boilerplate (container-cleanup stanza, `docker run -i --rm --name`, env passthrough). Drift is already visible (default style, double-passed creds, one structurally-different LSP wrapper). There is **no generator**; they were hand-cloned by an agent over many sessions.
**Recommendation (out of scope for the template):** replace the 38 files with one parameterized launcher driven by a declarative `servers.yaml` manifest, plus a `gen-wrappers.sh`. The template's `docker-run.sh` + `lib/common.sh` show the direction; the same idea applies at fleet scale.
---
## 12. Workflow documents — WORKING.md / STATUS.md / JOURNAL.md
The remote personal genre contributes a discipline absent from infra: a **task/state/document triad** enforced by hooks:
- **WORKING.md** — the only task tracker (todos tool banned); commits blocked while `- [ ]` remain; cleared before responding.
- **STATUS.md** — agent-maintained dashboard humans read; staleness is a warning.
- **JOURNAL.md** — append-only ADR/pattern log; no today-entry is a warning.
- **`CNW:` markers** — flag unresolved questions for the human; empty markers are a warning.
**Decision:** all four are in the template, and check-rules.sh enforces their freshness/completion.
---
## Standardization scorecard
| Inconsistency | Was | Now |
|---|---|---|
| Shebangs | 3 variants | `#!/usr/bin/env bash` |
| `set` flags | 4 policies | `set -euo pipefail` |
| Color/log boilerplate | copy-pasted ~10× | `lib/common.sh` |
| Task runner | 5 conventions | one `Makefile` |
| Git hook install | 3 mechanisms, 1 missing | one `setup-hooks.sh` (copy) |
| Git hook coverage | 3/18 projects | every project, two hooks |
| Crush hooks | 1/18 projects | every project, five hooks |
| Rules engine | 1 project (Go-specific) | generalized, project-agnostic |
| Crush memory | 1 project | skeleton in template |
| AGENTS.md structure | bespoke per project | canonical 10-section skeleton |
| 💘 commit footer | 2/18 projects | mandated in template |
---
## 13. Global baseline prompt integration
After the initial extraction, the owner shared a draft "global baseline prompt"
(an Apple Note of operating observations gathered over weeks of working with
agents). It encodes principles that cut across every project and that the
template now reflects. The cleaned canonical version lives in
[`BASELINE-PROMPT.md`](BASELINE-PROMPT.md); the project-level specialization is
in `AGENTS.md`. New artifacts and checks added:
### New policy encoded
| Principle (baseline §) | How the template enforces it |
|---|---|
| **Stop over-thinking; ask early** (§1, §9) | `questions-v1.md` skeleton + AGENTS.md "Questions" section; required-files check fails if absent |
| **Token efficiency / farm to tooling** (§2) | `make lint` runs shellcheck in Docker; AGENTS.md "Working Style" forbids parsing huge code in context |
| **Redmine = SoR for work; Discourse = SoR for docs** (§3) | AGENTS.md "Systems of Record" section; `make garden` flags oversized non-Discourse `.md` |
| **Git .md = stubs to Discourse** (§3) | `scripts/garden.sh` reports oversized `.md` lacking a Discourse URL |
| **`tea` CLI for PRs; off-workstation → PR** (§4) | AGENTS.md "Git Workflow" policy |
| **Shift-left CI/CD, lockstep local + hosted** (§5) | AGENTS.md "CI/CD" section; `make fast` runs at pre-commit |
| **Docker/k8s for everything; container naming** (§6) | new container-naming rule in `check-rules.sh` (every compose service needs `container_name`); `docker-compose.yml.example` + `scripts/up.sh` / `scripts/down.sh` |
| **STATUS.md = scratchpad, not SoR; has Inbox** (§8) | STATUS.md reframed; Inbox section for mid-task interruptions ("don't pivot") |
| **Questions file** (§9) | `questions-v1.md` artifact + required-files check |
| **Belt-and-suspenders enforcement** (§10) | already present (git hooks + Crush hooks) — now documented as policy |
| **Gardening loop** (§11) | `scripts/garden.sh` + `make garden` target |
| **Sub-agents as subcontractors** (§12) | AGENTS.md "Working Style" |
| **TDD + linters** (§13) | AGENTS.md "TDD & Linting"; `enforce-rules.sh` TDD reminder (opt-in via `PROJECT_SOURCE_SUFFIXES`) |
### New template artifacts
- `BASELINE-PROMPT.md` — the cleaned canonical global prompt (lives in the meta root; projects inherit).
- `questions-v1.md` — git-tracked question log skeleton.
- `docker-compose.yml.example` — lifecycle template with project-prefix naming.
- `scripts/up.sh`, `scripts/down.sh` — compose lifecycle wrappers.
- `scripts/garden.sh` — doc-sprawl / Discourse-migration report.
### New `check-rules.sh` checks
- **Container naming** (§6): every service in a `docker-compose*.y*ml` must declare `container_name:`; failure otherwise.
- **Questions file required**: `questions-v1.md` must exist (required-files manifest).
### New Makefile targets
`make garden`, `make up`, `make down` — standard verbs across every project.
### What stayed project-level (not globalized)
- `sectestbed-` / `preprod-` VM testing semantics — IAC-workstream-specific; left as AGENTS.md prose, not a mechanical check.
- Redmine Gantt / dependency modeling — workflow-specific.
- Specific DNS/cloudron/SITER production details — out of scope.
+70
View File
@@ -0,0 +1,70 @@
# meta — cross-project best-practices template
This repo serves two purposes at once:
1. **It is a Gitea template repository.** Create new projects from it to inherit
the full best-practices stack from the start (AGENTS.md, git hooks, Crush
hooks, rules engine, Makefile, lifecycle scripts, gardening).
2. **It is the design source** that maintains the global baseline prompt and the
cross-project pattern extraction. New projects can delete `BASELINE-PROMPT.md`
and `PATTERNS.md` if they don't want the authoring context — or keep them as
reference.
## What you get when you create a project from this template
```
<new-project>/
├── AGENTS.md ← fill in the bracketed fields, delete the rest
├── crush.json ← 5 Crush PreToolUse hooks (behavioral guardrails)
├── hooks/ ← block-todos, enforce-bash, enforce-rules, audit-before-git, exit-protocol
├── scripts/
│ ├── setup-hooks.sh ← install git hooks (run once: make setup)
│ ├── pre-commit / pre-push ← fast audit / full audit + clean-tree gate
│ ├── check-rules.sh ← rule audit engine (shellcheck, image pin, container naming, required files, freshness, hygiene, tests)
│ ├── docker-run.sh ← canonical ephemeral-container wrapper
│ ├── up.sh / down.sh ← docker-compose lifecycle wrappers
│ ├── garden.sh ← doc-sprawl / Discourse-migration report
│ └── lib/common.sh ← shared bash library (colors, log_*, docker_run, check)
├── Makefile ← setup/validate/fast/lint/garden/up/down/status/clean
├── STATUS.md ← agent scratchpad (token-efficiency; has Inbox)
├── WORKING.md ← the only task tracker (todos tool banned)
├── questions-v1.md ← git-tracked question log for the human
├── docs/JOURNAL.md ← append-only decision & pattern log
├── docker-compose.yml.example ← lifecycle template (copy to docker-compose.yml)
├── .env.example ← secrets/config template
├── .gitignore
├── .crush/memory/operational.md ← per-session operational memory skeleton
├── BASELINE-PROMPT.md ← the global working principles (delete if unwanted)
└── PATTERNS.md ← the pattern extraction analysis (delete if unwanted)
```
## Quick start (for a new project)
```bash
# Create from this Gitea template, then:
cd my-new-project
git init # if not already
make setup # install git hooks
make fast # confirm baseline passes
# Fill in bracketed fields in AGENTS.md (project name, overview, model, etc.)
# Define `make test` for your stack
# Delete BASELINE-PROMPT.md and PATTERNS.md if you don't want them
```
## What it enforces
| Layer | What | How |
|---|---|---|
| **Git hooks** | mechanical checks at commit/push | `scripts/pre-commit` (fast audit + hot-path bypass), `scripts/pre-push` (full audit + clean-tree gate) |
| **Crush hooks** | behavioral guardrails at tool-call time | `hooks/` wired in `crush.json`: ban todos, block banned commands + host language tools, block banned file types, audit before git ops, exit protocol |
| **Rules engine** | shellcheck, image pinning, container naming, required files, doc freshness, WORKING.md completion, CNW markers, hygiene, test suite | `scripts/check-rules.sh` (`--fast` for pre-commit, `--quiet` for pre-push) |
| **Makefile** | standard verbs everywhere | `make setup/fast/validate/lint/test/garden/up/down/status/clean` |
Configurable via env without editing hooks: `PROJECT_BANNED_COMMANDS`,
`PROJECT_BANNED_SUFFIXES`, `PROJECT_BANNED_ALLOW`, `PROJECT_SOURCE_SUFFIXES`,
`PROJECT_REQUIRED_FILES`.
## The two extra docs (meta-authoring, optional)
- **[BASELINE-PROMPT.md](BASELINE-PROMPT.md)** — the canonical global working principles (13 sections), distilled from operating notes. Projects inherit it; they don't need to ship it.
- **[PATTERNS.md](PATTERNS.md)** — the cross-project pattern extraction from 18 projects across two machines, with a standardization scorecard. Documents *why* every template decision was made.
+47
View File
@@ -0,0 +1,47 @@
# STATUS.md — Agent Scratchpad
> Durable, git-tracked scratchpad for token efficiency — NOT the system of record
> (Redmine is). Agent-owned; human consumes only. See BASELINE-PROMPT.md §8.
Last updated: 2026-08-06
## Current State
DRAFT — meta project bootstrapped and self-applying its own rules.
## What Just Shipped
- Surveyed 18 projects across two machines (12 local + 6 on ultix-offstage via ssh).
- Extracted cross-project patterns into PATTERNS.md (13 sections + scorecard).
- Authored BASELINE-PROMPT.md from the owner's operating notes.
- Built template/ — a reusable repo skeleton (AGENTS.md, git hooks, 5 Crush hooks, rules engine, shared lib, Makefile, lifecycle scripts, gardening).
- Folded the baseline-prompt principles into the template (PATTERNS.md §13).
- **Self-applied**: meta now runs its own hooks + check-rules.sh; git initialized.
## In Progress
- Driving template/test inconsistency to zero across all projects.
## Inbox
> New work the human mentions mid-task lands here. Don't pivot. If materially different, spin up a Redmine ticket.
- (empty)
## Blockers / Needs Human Input
- See questions-v1.md (remote setup; bin/-wrapper gap; promote baseline §12).
## Tactical Notes
- RCEO-PersonalAssistant is the gold-standard hook stack (only project using Crush hooks).
- Sub-agent finding: `~/daytoday/{redmine,discourse}/bin/` wrappers are documented in PFVCluster operational.md but the dirs only contain AGENTS.md/.gitignore — no bin/. Real CLI source is in KNEL-AIMiddleware/{redmine,discourse}-cli/. Surface to human.
- PFVCluster's real SoR flow has NO JOURNAL.md — it's Redmine ticket + Discourse #298 audit log. Template's JOURNAL.md may need rethinking for infra projects.
- Pointer-header pattern: PFVCluster .md stubs use a 10-line boilerplate pointing to community.turnsys.com/t/<N>.
## Next Actions
1. Human: configure git remote (questions-v1.md Q1).
2. Strengthen BASELINE-PROMPT.md §12 (sub-agents) per sub-agent recommendation.
3. Decide: does the template ship the pointer-header pattern, or stay project-local?
4. Consider: add the sub-agent nudge hook (hooks/nudge-subagent.sh).
+9
View File
@@ -0,0 +1,9 @@
# WORKING.md — Active Session Tracker
Agent work only. The human decides when it's done.
The todos tool is banned; this is the only task tracker.
A commit is blocked while any task below remains unchecked.
## Current Tasks
(none — all agent work is committed)
+32
View File
@@ -0,0 +1,32 @@
{
"$schema": "https://charm.land/crush.json",
"hooks": {
"PreToolUse": [
{
"matcher": "^todos$",
"command": "./hooks/block-todos.sh",
"timeout": 10
},
{
"matcher": "^bash$",
"command": "./hooks/enforce-bash.sh",
"timeout": 10
},
{
"matcher": "^(edit|write|multiedit)$",
"command": "./hooks/enforce-rules.sh",
"timeout": 10
},
{
"matcher": "^bash$",
"command": "./hooks/audit-before-git.sh",
"timeout": 30
},
{
"matcher": ".*",
"command": "./hooks/exit-protocol.sh",
"timeout": 10
}
]
}
}
+26
View File
@@ -0,0 +1,26 @@
# docker-compose.yml — lifecycle template.
#
# Copy to docker-compose.yml (gitignored by default in some projects) and edit.
# Naming law: every service MUST set container_name with a project prefix.
# Never rely on Docker's default `<dir>_<n>` naming.
# PROJECT_PREFIX should match this repo's name (lowercase, hyphenated).
# Define it once here and reuse via interpolation where supported.
services:
example:
image: alpine:3.20 # pin every image — no :latest
container_name: ${PROJECT_NAME:-project}-example
restart: unless-stopped
environment:
- LOG_LEVEL=${LOG_LEVEL:-info}
# Bind-mount the repo so in-container work stays host-owned.
volumes:
- ./:/data
working_dir: /data
# Pin resource limits appropriate to a cluster-of-1 default.
deploy:
resources:
limits:
cpus: '0.50'
memory: 256M
+14
View File
@@ -0,0 +1,14 @@
# JOURNAL.md — Append-Only Decision & Pattern Log
> This file is append-only. Never delete or reorder existing entries.
> Add one dated section per significant change: what changed, why, the pattern
> used, any challenges, and the commit hash.
## TEMPLATE-REPLACE-ME (YYYY-MM-DD)
### Project bootstrapped from the meta template
- **What:** initialized repo structure, hooks, rules engine, docs.
- **Why:** establish a single consistent best-practice baseline.
- **Pattern:** see `PATTERNS.md` in the meta project root.
- **Commit:** _(initial commit hash)_
# trailing probe
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Hook: audit-before-git (PreToolUse, matcher: ^bash$)
# Fires on any bash call containing "git commit" or "git push" and blocks it
# unless the fast rule audit passes. This makes the git hooks redundant-safe:
# even if hooks are bypassed or missing, the agent cannot commit/push a
# rule-violating state.
set -euo pipefail
CMD="${CRUSH_TOOL_INPUT_COMMAND:-}"
if ! printf '%s' "$CMD" | grep -qE 'git[[:space:]]+(commit|push)'; then
echo '{}'
exit 0
fi
# Emergency bypass.
if printf '%s' "$CMD" | grep -q -- '--no-verify'; then
echo '{"context": "Skipping rule audit (--no-verify). Use this ONLY in genuine emergencies."}'
exit 0
fi
REPO_ROOT="${CRUSH_PROJECT_DIR:-$(pwd)}"
if bash "$REPO_ROOT/scripts/check-rules.sh" --fast >/dev/null 2>&1; then
echo '{"context": "Rule audit passed."}'
exit 0
fi
# Re-run with output so the violation is visible, then block.
bash "$REPO_ROOT/scripts/check-rules.sh" --fast >&2 || true
echo "BLOCKED: rule audit failed. Fix the violations above before committing or pushing." >&2
exit 2
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Hook: block-todos (PreToolUse, matcher: ^todos$)
# Bans the todos tool. WORKING.md is the ONLY task tracker in this project,
# so the task list is version-controlled and visible in every commit.
#
# Why: the todos tool's state is invisible to humans reviewing git history.
# WORKING.md is committed, diffable, and survives across sessions.
echo "BLOCKED: The todos tool is banned in this project. Use WORKING.md for ALL task tracking — edit it, then commit." >&2
exit 2
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Hook: enforce-bash (PreToolUse, matcher: ^bash$)
# Intercepts every bash tool call and blocks two classes of violation:
# 1. Banned host-mutating / network commands (per AGENTS.md policy).
# 2. Host-level language toolchains — they must run inside Docker so the
# host stays clean.
set -euo pipefail
CMD="${CRUSH_TOOL_INPUT_COMMAND:-}"
# --- 1. Banned commands -----------------------------------------------------
# Extend PROJECT_BANNED_COMMANDS (space-separated) via env to add project-specific bans.
BANNED_COMMANDS="${PROJECT_BANNED_COMMANDS:-sudo su doas apt apt-get dnf emerge pacman yum zypper apk opkg curl wget ssh scp telnet nc firefox chrome safari httpie}"
for bc in $BANNED_COMMANDS; do
if printf '%s' "$CMD" | grep -qE "(^|[[:space:]])${bc}([[:space:]]|$)"; then
echo "BLOCKED: AGENTS.md — '$bc' is a banned command. This project is Docker-only; do host work through containers or approved wrappers." >&2
exit 2
fi
done
# --- 2. Host-level language tools ------------------------------------------
# python3/go/node/npm/cargo/etc. must appear inside a `docker run|exec|build`.
if printf '%s' "$CMD" | grep -qE '(^|[[:space:]])(python3?|go test|go vet|gofmt|go run|node|npm|npx|yarn|pnpm|ruby|java|javac|rustc|cargo)([[:space:]]|$)'; then
if ! printf '%s' "$CMD" | grep -qE 'docker (run|exec|build)'; then
echo "BLOCKED: AGENTS.md — language tools must run inside Docker, not on the host. Use: scripts/docker-run.sh <pinned-image> <command>" >&2
exit 2
fi
fi
echo '{}'
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Hook: enforce-rules (PreToolUse, matcher: ^(edit|write|multiedit)$)
# Inspects the target file path of every edit/write/multiedit and:
# 1. Blocks edits to banned file types in production paths.
# 2. Injects a "did you write a failing test first?" reminder for source edits.
#
# Banned file types are configurable via PROJECT_BANNED_SUFFIXES (regex alternation)
# and PROJECT_BANNED_ALLOW (regex of paths that are exempt, e.g. vendored code).
set -euo pipefail
PATH_PREFIX="${CRUSH_WORKING_DIR:-/work}"
FILE_PATH="${CRUSH_TOOL_INPUT_FILE_PATH:-}"
# Strip common prefixes to get a repo-relative path.
REL_PATH="${FILE_PATH#"$PATH_PREFIX"/}"
REL_PATH="${REL_PATH#/}"
BANNED_SUFFIXES="${PROJECT_BANNED_SUFFIXES:-}"
BANNED_ALLOW="${PROJECT_BANNED_ALLOW:-^$}"
# --- 1. Banned file types ---------------------------------------------------
if [ -n "$BANNED_SUFFIXES" ] && printf '%s' "$REL_PATH" | grep -qE "\.($BANNED_SUFFIXES)$"; then
if ! printf '%s' "$REL_PATH" | grep -qE "$BANNED_ALLOW"; then
echo "BLOCKED: AGENTS.md language policy — $REL_PATH is a banned file type in production. Allowed only under: $BANNED_ALLOW" >&2
exit 2
fi
fi
# --- 2. TDD reminder for source-file edits ----------------------------------
# Toggle via PROJECT_SOURCE_SUFFIXES (regex). Default: none (opt-in).
SOURCE_SUFFIXES="${PROJECT_SOURCE_SUFFIXES:-}"
TEST_PATTERN="${PROJECT_TEST_PATTERN:-_test\.}"
if [ -n "$SOURCE_SUFFIXES" ] && printf '%s' "$REL_PATH" | grep -qE "\.($SOURCE_SUFFIXES)$"; then
if ! printf '%s' "$REL_PATH" | grep -qE "$TEST_PATTERN"; then
cat <<'EOF'
{"context": "You are editing a source file. TDD is mandatory: did you write a FAILING test first? If not, stop and write the test before editing this file."}
EOF
exit 0
fi
fi
echo '{}'
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Hook: exit-protocol (PreToolUse, matcher: .*) — catch-all.
# Enforces the project's definition of "done": if WORKING.md has any unchecked
# task, inject context ordering the agent to finish them before responding.
# This prevents premature "Done" responses.
set -euo pipefail
# Required by the hook protocol: consume stdin.
cat >/dev/null
REPO_ROOT="${CRUSH_PROJECT_DIR:-$(pwd)}"
WORKING_FILE="$REPO_ROOT/WORKING.md"
if [ -f "$WORKING_FILE" ]; then
UNCHECKED="$(grep -cF -- '- [ ]' "$WORKING_FILE" || true)"
if [ "$UNCHECKED" -gt 0 ]; then
TASKS_JSON="$(grep -F -- '- [ ]' "$WORKING_FILE" \
| sed 's/\\/\\\\/g; s/"/\\"/g' \
| awk -v ORS='\\n' '{print}' | sed 's/\\n$//')"
printf '{"context": "STOP. WORKING.md has %s unfinished task(s). You cannot declare work done or respond to the user while these remain:\\n%s\\nFinish them now. Do not respond until all are checked. Then clear WORKING.md and commit."}\n' \
"$UNCHECKED" "$TASKS_JSON"
exit 0
fi
fi
echo '{}'
+45
View File
@@ -0,0 +1,45 @@
# questions-v1.md
> Git-tracked question log. The agent writes; the human reviews/edits inline.
> Version up when a round of answers lands. Synthesize resolved Q&A to Discourse/Redmine.
> See BASELINE-PROMPT.md §9.
## Open questions
### Q1. Git remote for meta?
- **Context:** meta is now a git repo (locally) but has no remote configured. The auto-commit+push policy (baseline §4) can't complete without one.
- **Options:** (a) new Gitea repo under reachableceo; (b) nest under an existing repo; (c) keep local-only for now.
- **Question:** Where should meta push?
- **Answer:** _(human)_
- **Decision:** _(human/agent)_
- **Synthesized to:** —
### Q2. The bin/ wrapper gap (redmine-cli / discourse-cli)
- **Context:** PFVCluster's operational.md and AGENTS.md reference `~/daytoday/redmine/bin/redmine` and `~/daytoday/discourse/bin/discourse` as the entrypoints. But `ls ~/daytoday/{redmine,discourse}/` shows only `.gitignore` + `AGENTS.md` (+ MIGRATION-PLAN.md for discourse) — no `bin/`, no Dockerfile. The actual CLI source lives in `~/projects/KNEL-AIMiddleware/{redmine,discourse}-cli/`.
- **Question:** Are the `bin/` wrappers something that should exist (and were lost / never committed), or is the documentation aspirational? Should the template reference these CLIs at all, or stay tool-agnostic?
- **Answer:** _(human)_
- **Decision:** _(human/agent)_
- **Synthesized to:** —
### Q3. Should the template ship the Discourse pointer-header pattern?
- **Context:** PFVCluster migrated 36 in-repo `.md` files to 10-line pointer stubs citing `https://community.turnsys.com/t/<N>`. The template currently has `scripts/garden.sh` that *warns* about oversized non-Discourse `.md`, but doesn't enforce the pointer-header format.
- **Options:** (a) keep it advisory (garden.sh warn only); (b) add an opt-in check-rule that fails if a tracked `.md` lacks a Discourse URL (excluding AGENTS.md/STATUS.md/etc.); (c) leave it project-local — infra projects want it, personal/business projects don't.
- **Question:** Which option, and is the assumption in (c) right?
- **Answer:** _(human)_
- **Decision:** _(human/agent)_
- **Synthesized to:** —
### Q4. Sub-agent nudge hook — wanted?
- **Context:** A sub-agent proposed a non-blocking Crush hook (`hooks/nudge-subagent.sh`) that emits a stderr reminder after the Nth sequential file read, nudging toward dispatching a sub-agent. Mirrors football's "never read 10+ files sequentially" rule.
- **Options:** (a) add it (non-blocking, advisory); (b) leave sub-agent use as prose policy only.
- **Question:** Worth adding, or too noisy?
- **Answer:** _(human)_
- **Decision:** _(human/agent)_
- **Synthesized to:** —
### Q5. JOURNAL.md vs Discourse audit-log for infra projects
- **Context:** The template ships `docs/JOURNAL.md` as the append-only decision log. But PFVCluster (the most mature infra project) has NO JOURNAL.md — it uses Discourse topic #298 as the audit log and Redmine for work tracking. PATTERNS.md §5 noted this divergence.
- **Question:** Should the template keep JOURNAL.md as the default, with infra projects swapping it for the Discourse-audit-log pattern? Or drop JOURNAL.md entirely in favor of "Discourse is the SoR"?
- **Answer:** _(human)_
- **Decision:** _(human/agent)_
- **Synthesized to:** —
+221
View File
@@ -0,0 +1,221 @@
#!/usr/bin/env bash
# check-rules.sh — project rule audit engine.
#
# Usage:
# bash scripts/check-rules.sh # full audit (verbose, includes slow checks)
# bash scripts/check-rules.sh --fast # fast audit (quiet, skips slow checks) — for pre-commit
# bash scripts/check-rules.sh --quiet # full audit, only prints failures
#
# Exit code: 0 = all rules pass (warnings are non-fatal), 1 = one or more FAILED.
#
# This is a generalized version of the rules engine proven in the
# RCEO-PersonalAssistant project. Add project-specific checks by appending
# `check "<desc>" "<pass|warn|fail>"` calls below.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$HERE/lib/common.sh"
REPO_ROOT="$(repo_root)"
cd "$REPO_ROOT"
# --- argument parsing ---
RULE_FAST=false
RULE_VERBOSE=true
for arg in "$@"; do
case "$arg" in
--fast) RULE_FAST=true; RULE_VERBOSE=false ;;
--quiet) RULE_VERBOSE=false ;;
*) die "check-rules.sh: unknown argument '$arg'" ;;
esac
done
export RULE_FAST RULE_VERBOSE
init_counters
$RULE_VERBOSE && echo "=== Project Rule Audit ==="
TODAY="$(date +%Y-%m-%d)"
# ----------------------------------------------------------------------------
# 1. Shellcheck — every .sh/.bash must pass (zero warnings, incl. info-level).
# Runs in Docker so the host stays clean (no native shellcheck required).
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Shell scripts (shellcheck)"
mapfile -d '' SH_FILES < <(find . -path ./.git -prune -o -path ./.tmp -prune -o -path ./vendor -prune -o -path ./node_modules -prune -o \( -name '*.sh' -o -name '*.bash' \) -print0 2>/dev/null)
if [ "${#SH_FILES[@]}" -gt 0 ]; then
if have shellcheck; then
if shellcheck "${SH_FILES[@]}" >/dev/null 2>&1; then
check "All shell scripts pass shellcheck (host)" "pass"
else
check "shellcheck reports violations — run: shellcheck <file>" "fail"
fi
elif have docker; then
MNT_FILES=()
for f in "${SH_FILES[@]}"; do MNT_FILES+=("/mnt/${f#./}"); done
if docker run --rm -v "$REPO_ROOT:/mnt" koalaman/shellcheck:stable "${MNT_FILES[@]}" >/dev/null 2>&1; then
check "All shell scripts pass shellcheck (docker)" "pass"
else
check "shellcheck (docker) reports violations" "fail"
fi
else
check "No shellcheck or docker available to lint scripts" "warn"
fi
else
check "No shell scripts to lint" "pass"
fi
# ----------------------------------------------------------------------------
# 2. Docker image pinning — no ':latest' tags in compose or Dockerfiles.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Docker image pinning"
if grep -rqE '(image:|FROM).*:latest' --include='docker-compose*.y*ml' --include='Dockerfile*' . 2>/dev/null; then
check "No ':latest' image tags (pin everything)" "fail"
else
check "No ':latest' image tags" "pass"
fi
# ----------------------------------------------------------------------------
# 2b. Container naming — every service in a docker-compose file MUST set an
# explicit container_name (never rely on Docker's default <dir>_<n>).
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Container naming"
COMPOSE_FILES="$(find . -path ./.git -prune -o \( -name 'docker-compose*.yml' -o -name 'docker-compose*.yaml' -o -name 'compose.yml' -o -name 'compose.yaml' \) -print 2>/dev/null || true)"
if [ -n "$COMPOSE_FILES" ]; then
BAD=0
while IFS= read -r cf; do
[ -n "$cf" ] || continue
# Count top-level service keys (2-space indent under services:) and
# compare against the number of container_name: declarations.
svc_count=$(awk '/^services:/{f=1;next} f&&/^[^[:space:]]/{f=0} f&&/^[[:space:]]{2}[[:alnum:]_-]+:[[:space:]]*$/{c++} END{print c+0}' "$cf")
cn_count=$(grep -cE '^[[:space:]]*container_name:' "$cf" 2>/dev/null || echo 0)
if [ "${svc_count:-0}" -gt 0 ] && [ "$cn_count" -lt "$svc_count" ]; then
BAD=$((BAD + 1))
fi
done <<EOF
$COMPOSE_FILES
EOF
if [ "$BAD" -eq 0 ]; then
check "All compose services set container_name" "pass"
else
check "$BAD compose file(s) with services missing container_name" "fail"
fi
else
check "No compose files (container-name check skipped)" "pass"
fi
# ----------------------------------------------------------------------------
# 3. Required-files manifest — the files every project using this template owns.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Required files"
REQUIRED_FILES="AGENTS.md STATUS.md questions-v1.md .env.example Makefile crush.json scripts/check-rules.sh scripts/setup-hooks.sh"
REQUIRED_FILES="$REQUIRED_FILES ${PROJECT_REQUIRED_FILES:-}"
for f in $REQUIRED_FILES; do
if [ -f "$f" ]; then check "$f exists" "pass"; else check "$f MISSING" "fail"; fi
done
# ----------------------------------------------------------------------------
# 4. Doc freshness — STATUS.md touched today; JOURNAL.md has today's entry.
# Warnings (not failures): staleness is a signal, not a break.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Doc freshness"
if [ -f STATUS.md ]; then
STATUS_DATE="$(grep -oE 'Last updated: [0-9]{4}-[0-9]{2}-[0-9]{2}' STATUS.md | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || echo unknown)"
if [ "$STATUS_DATE" = "$TODAY" ]; then
check "STATUS.md updated today ($STATUS_DATE)" "pass"
else
check "STATUS.md is stale (last: $STATUS_DATE, today: $TODAY) — update it" "warn"
fi
else
check "STATUS.md MISSING" "fail"
fi
if [ -f docs/JOURNAL.md ]; then
LAST_ENTRY="$(grep -oE '^## [0-9]{4}-[0-9]{2}-[0-9]{2}' docs/JOURNAL.md | tail -1 | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}' || echo unknown)"
if [ "$LAST_ENTRY" = "$TODAY" ]; then
check "JOURNAL.md has an entry for today ($LAST_ENTRY)" "pass"
else
check "JOURNAL.md has no entry for today (last: $LAST_ENTRY) — add one" "warn"
fi
else
check "docs/JOURNAL.md MISSING" "fail"
fi
# ----------------------------------------------------------------------------
# 5. Git state — uncommitted changes are a warning (not a hard failure here;
# the pre-push hook and audit-before-git crush hook harden this where it matters).
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Git state"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
if git diff --quiet && git diff --cached --quiet; then
check "Working tree clean" "pass"
else
check "Uncommitted changes present" "warn"
fi
else
check "Not a git repo (git checks skipped)" "pass"
fi
# ----------------------------------------------------------------------------
# 6. Hooks installed — self-check that git hooks were set up.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Git hooks"
if [ -f .git/hooks/pre-commit ]; then
check "pre-commit hook installed" "pass"
else
check "pre-commit NOT installed (run: bash scripts/setup-hooks.sh)" "warn"
fi
if [ -f .git/hooks/pre-push ]; then
check "pre-push hook installed" "pass"
else
check "pre-push NOT installed (run: bash scripts/setup-hooks.sh)" "warn"
fi
# ----------------------------------------------------------------------------
# 7. WORKING.md completion — no unchecked tasks may remain at commit time.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Task completion"
if [ -f WORKING.md ]; then
UNCHECKED="$(grep -cF -- '- [ ]' WORKING.md || true)"
if [ "$UNCHECKED" -eq 0 ]; then
check "WORKING.md has no unchecked tasks" "pass"
else
check "WORKING.md has ${UNCHECKED} unchecked task(s) — finish them before committing" "fail"
fi
else
check "WORKING.md absent (no active task tracker)" "pass"
fi
# ----------------------------------------------------------------------------
# 8. CNW markers — empty `CNW:` markers flag unresolved questions for the human.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "Unresolved questions"
EMPTY_CNW="$(grep -rn 'CNW:$' . --include='*.md' 2>/dev/null | head -20 || true)"
if [ -z "$EMPTY_CNW" ]; then
check "No empty CNW: markers (unresolved questions)" "pass"
else
CNW_COUNT="$(printf '%s\n' "$EMPTY_CNW" | grep -c . || true)"
check "${CNW_COUNT} unresolved CNW: marker(s) — needs user input" "warn"
fi
# ----------------------------------------------------------------------------
# 9. Hygiene — merge-conflict markers and trailing whitespace must never land.
# ----------------------------------------------------------------------------
$RULE_VERBOSE && log_step "File hygiene"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
CONFLICT="$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | xargs -r grep -lE '^(<<<<<<<|=======|>>>>>>>)' 2>/dev/null || true)"
if [ -z "$CONFLICT" ]; then check "No merge-conflict markers staged" "pass"; else check "Merge-conflict markers staged: $CONFLICT" "fail"; fi
fi
# ----------------------------------------------------------------------------
# 10. (slow, skipped in --fast) Project test suite via `make test` if present.
# ----------------------------------------------------------------------------
if [ "$RULE_FAST" = false ] && [ -f Makefile ] && grep -qE '^test:' Makefile; then
$RULE_VERBOSE && log_step "Test suite (make test)"
if make test >/dev/null 2>&1; then
check "make test passes" "pass"
else
check "make test FAILS" "fail"
fi
fi
print_summary_and_exit
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# docker-run.sh — canonical ephemeral-container wrapper.
#
# Keeps the host clean: every build/test/generation runs inside a pinned image.
# Ensures output files are owned by the invoking user (not root).
#
# Usage:
# docker-run.sh <image> [command...]
# Runs <command> in <image> with the repo mounted at /data, cwd /data.
# With no command, drops into the image's default entrypoint.
# docker-run.sh --shell <image>
# Interactive shell inside the container (for debugging).
#
# Examples:
# docker-run.sh python:3.12-slim python3 -m pytest
# docker-run.sh pandoc/extra report.md -o report.pdf
# docker-run.sh --shell node:20
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$HERE/lib/common.sh"
SHELL_MODE=false
case "${1:-}" in
--shell) SHELL_MODE=true; shift ;;
-h|--help)
sed -n '2,18p' "$0"; exit 0 ;;
esac
[ "$#" -ge 1 ] || { sed -n '2,18p' "$0"; exit 1; }
if [ "$SHELL_MODE" = true ]; then
# ${SHELL:-sh} must expand inside the container, not in this outer shell.
# shellcheck disable=SC2016
docker_run "$1" sh -c 'exec "${SHELL:-sh}"'
else
docker_run "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# down.sh — bring down the project's docker-compose stack.
# Wrapper around `docker compose down` so every project has the same verb.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$HERE/lib/common.sh"
REPO_ROOT="$(repo_root)"
cd "$REPO_ROOT"
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
[ -f "$COMPOSE_FILE" ] || die "no $COMPOSE_FILE"
log_step "Bringing down stack ($COMPOSE_FILE)"
docker compose -f "$COMPOSE_FILE" down
log_ok "stack down"
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# garden.sh — the gardening loop.
#
# Reports doc sprawl and files that violate the "Discourse is the system of
# record for documentation; gitea .md files are stubs" policy. Run via
# `make garden`. Findings are WARNINGS (advisory); fix them at a natural break.
#
# What it checks:
# 1. Markdown sprawl: count of .md files per directory (top-10 by count).
# 2. Oversized .md files (default >300 lines) that don't cite a Discourse URL
# — candidates to migrate to Discourse, leaving a stub.
# 3. .md files with no Discourse link at all (informational; exempt: the
# operational files in EXEMPT_FILES).
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$HERE/lib/common.sh"
REPO_ROOT="$(repo_root)"
cd "$REPO_ROOT"
SIZE_LIMIT="${GARDEN_MD_LINE_LIMIT:-300}"
# Operational files that legitimately live next to code, not in Discourse.
EXEMPT_FILES="${GARDEN_EXEMPT:-AGENTS.md STATUS.md WORKING.md questions-v.*.md docs/JOURNAL.md PATTERNS.md BASELINE-PROMPT.md README.md}"
log_step "Gardening report for $REPO_ROOT"
# --- 1. sprawl by directory -------------------------------------------------
log_info "Markdown file count by directory (top 10):"
find . -path ./.git -prune -o -name '*.md' -print 2>/dev/null \
| sed 's|/[^/]*$||' | sort | uniq -c | sort -rn | head -10 | sed 's/^/ /'
# --- 2. oversized .md without a Discourse link ------------------------------
log_info "Oversized .md (>${SIZE_LIMIT} lines) lacking a Discourse URL — migrate candidates:"
OVERSIZED=0
while IFS= read -r -d '' f; do
# skip exempt files (glob match against basename and relative path)
exempt=false
base=$(basename "$f")
rel=${f#./}
for pat in $EXEMPT_FILES; do
# shellcheck disable=SC2254 # glob match is intentional
case "$base" in $pat) exempt=true; break ;; esac
# shellcheck disable=SC2254
case "$rel" in $pat) exempt=true; break ;; esac
done
[ "$exempt" = true ] && continue
lines=$(wc -l < "$f" 2>/dev/null || echo 0)
if [ "$lines" -gt "$SIZE_LIMIT" ]; then
if ! grep -qiE 'community\.turnsys\.com|discourse' "$f" 2>/dev/null; then
printf ' %-60s %s lines\n' "$f" "$lines"
OVERSIZED=$((OVERSIZED + 1))
fi
fi
done < <(find . -path ./.git -prune -o -name '*.md' -print0 2>/dev/null)
[ "$OVERSIZED" -eq 0 ] && echo " (none)"
# --- 3. summary -------------------------------------------------------------
log_step "Gardening summary"
echo " Oversized non-Discourse .md files: $OVERSIZED"
if [ "$OVERSIZED" -eq 0 ]; then
log_ok "no migration candidates"
else
log_warn "$OVERSIZED file(s) to migrate to Discourse"
fi
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env bash
# lib/common.sh — shared helpers for shell scripts and hooks in this repo.
#
# Source it from any script:
# #!/usr/bin/env bash
# set -euo pipefail
# HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# # shellcheck source=lib/common.sh
# source "$HERE/lib/common.sh" # or the appropriate relative path
#
# This library exists to drive a known cross-project inconsistency to zero:
# every repo used to re-paste the ANSI color block, redefine log_* helpers,
# pick one of three incompatible shebangs, and roll its own docker wrapper.
# Import this once instead.
# Do NOT set -euo pipefail here unconditionally — some callers (git hooks)
# source this file and rely on controlling their own shell options. We only
# guarantee the functions below are defined.
###############################################################################
# Config — override via environment before sourcing if needed
###############################################################################
: "${TEMPLATE_ROOT:=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
export TEMPLATE_ROOT
###############################################################################
# ANSI colors (defined once, used everywhere)
###############################################################################
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; BLUE=''; BOLD=''; NC=''
fi
export RED GREEN YELLOW BLUE BOLD NC
###############################################################################
# Logging
###############################################################################
log_info() { printf "${BLUE}${NC} %s\n" "$*"; }
log_ok() { printf "${GREEN}${NC} %s\n" "$*"; }
log_warn() { printf "${YELLOW}${NC} %s\n" "$*" >&2; }
log_error() { printf "${RED}${NC} %s\n" "$*" >&2; }
log_step() { printf "\n${BOLD}== %s ==${NC}\n" "$*"; }
die() { log_error "$*"; exit 1; }
###############################################################################
# Predicates
###############################################################################
# have <cmd> — return 0 if <cmd> is on PATH
have() { command -v "$1" >/dev/null 2>&1; }
###############################################################################
# Path helpers
###############################################################################
repo_root() {
# Prefer git's notion of the repo root, fall back to $TEMPLATE_ROOT, then pwd.
if git rev-parse --show-toplevel >/dev/null 2>&1; then
git rev-parse --show-toplevel
else
printf '%s\n' "${TEMPLATE_ROOT:-$(pwd)}"
fi
}
###############################################################################
# Privilege helpers
###############################################################################
# as_root — run the remaining args as root via sudo, or directly if already root.
as_root() {
if [ "$(id -u)" -eq 0 ]; then "$@"; else sudo "$@"; fi
}
###############################################################################
# Docker wrapper
###############################################################################
# docker_run <image> <args...>
# Ephemeral container, host-uid ownership, repo mounted at /data, cwd /data.
# Drives the "host stays clean; everything runs in containers" policy and
# ensures output files are owned by the invoking user, not root.
docker_run() {
[ "$#" -ge 1 ] || die "docker_run: image required"
local image="$1"; shift
have docker || die "docker not found on PATH"
local root
root="$(repo_root)"
docker run --rm \
--user "$(id -u):$(id -g)" \
-e HOME=/tmp \
-v "$root:/data" \
-w /data \
"$image" "$@"
}
###############################################################################
# Rule-audit accumulator (used by scripts/check-rules.sh)
# Globals read/written: RULE_PASS RULE_WARN RULE_FAIL
###############################################################################
init_counters() { RULE_PASS=0; RULE_WARN=0; RULE_FAIL=0; }
# check <description> <pass|warn|fail>
check() {
local desc="$1" result="$2"
case "$result" in
pass)
RULE_PASS=$((RULE_PASS + 1))
if [ "${RULE_VERBOSE:-true}" = true ]; then printf " ${GREEN}PASS${NC} %s\n" "$desc"; fi
;;
warn)
RULE_WARN=$((RULE_WARN + 1))
if [ "${RULE_VERBOSE:-true}" = true ]; then printf " ${YELLOW}WARN${NC} %s\n" "$desc"; fi
;;
fail)
RULE_FAIL=$((RULE_FAIL + 1))
printf " ${RED}FAIL${NC} %s\n" "$desc"
;;
*)
die "check(): invalid result '$result' (use pass|warn|fail)"
;;
esac
}
# print_summary_and_exit
print_summary_and_exit() {
if [ "${RULE_VERBOSE:-true}" = true ]; then
printf "\n=== Summary ===\n PASS: %s\n WARN: %s\n FAIL: %s\n\n" \
"$RULE_PASS" "$RULE_WARN" "$RULE_FAIL"
fi
if [ "$RULE_FAIL" -gt 0 ]; then
if [ "${RULE_VERBOSE:-true}" = true ]; then
printf "AUDIT FAILED — %s rule(s) violated.\n" "$RULE_FAIL"
fi
exit 1
fi
if [ "${RULE_VERBOSE:-true}" = true ]; then printf "AUDIT PASSED.\n"; fi
exit 0
}
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# pre-commit — fast rule audit (< 1s typical).
# Hot-path bypass: commits that ONLY touch STATUS.md / JOURNAL.md / WORKING.md
# skip the audit so frequent status/journal commits stay frictionless.
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"
CHANGED="$(git diff --cached --name-only)"
HOT_PATHS="$(printf '%s\n' "$CHANGED" | grep -vE '^(STATUS.md|docs/JOURNAL.md|WORKING.md)$' || true)"
if [ -z "$HOT_PATHS" ]; then
echo "hot-path files only (STATUS/JOURNAL/WORKING) — skipping rule audit"
exit 0
fi
if ! bash scripts/check-rules.sh --fast; then
echo ""
echo "pre-commit audit FAILED. Fix the violations above before committing."
echo "Full audit: bash scripts/check-rules.sh"
echo "Bypass: git commit --no-verify (emergencies only)"
exit 1
fi
exit 0
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# pre-push — full rule audit + clean-working-tree gate before pushing.
# Installed via: bash scripts/setup-hooks.sh
#
# Combines two proven policies observed across projects:
# - KNEL-AIMiddleware: block push if the working tree is dirty.
# - RCEO-PersonalAssistant: block push if the full test suite fails.
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"
echo "pre-push: running full rule audit..."
# Full audit (non-fast): runs the slow test suite via `make test` if present.
if ! bash scripts/check-rules.sh --quiet; then
echo ""
echo "pre-push audit FAILED. Push blocked."
echo "Re-run with output: bash scripts/check-rules.sh"
echo "Bypass: git push --no-verify (emergencies only)"
exit 1
fi
echo "pre-push: all rules and tests passed."
exit 0
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# setup-hooks.sh — install this repo's git hooks.
#
# Mechanism: copy scripts/pre-commit and scripts/pre-push into .git/hooks/ and
# make them executable. This is the most portable pattern (works on any clone,
# no `git config core.hooksPath` mutation, survives config resets, idempotent).
#
# Run once after cloning: bash scripts/setup-hooks.sh
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$HERE/lib/common.sh"
REPO_ROOT="$(repo_root)"
cd "$REPO_ROOT"
[ -d .git ] || die "no .git directory here — run this from a git checkout"
HOOKS_DIR=".git/hooks"
HOOK_NAMES="pre-commit pre-push"
log_step "Installing git hooks"
for name in $HOOK_NAMES; do
src="scripts/$name"
dst="$HOOKS_DIR/$name"
[ -f "$src" ] || die "source hook not found: $src"
cp "$src" "$dst"
chmod +x "$dst"
log_ok "installed $dst"
done
cat <<EOF
Git hooks installed. The following now run automatically:
pre-commit fast rule audit (shellcheck, image pinning, required files,
doc freshness, WORKING.md completion, hygiene).
Hot-path bypass for STATUS.md / JOURNAL.md / WORKING.md.
pre-push full rule audit (includes make test) + clean-working-tree gate.
Bypass either with \`git commit --no-verify\` / \`git push --no-verify\`
(emergencies only).
EOF
Executable
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# up.sh — bring up the project's docker-compose stack.
# Wrapper around `docker compose up -d` so every project has the same verb.
# Add project-specific pre/post steps below.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$HERE/lib/common.sh"
REPO_ROOT="$(repo_root)"
cd "$REPO_ROOT"
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
[ -f "$COMPOSE_FILE" ] || die "no $COMPOSE_FILE — copy docker-compose.yml.example to $COMPOSE_FILE first"
log_step "Bringing up stack ($COMPOSE_FILE)"
docker compose -f "$COMPOSE_FILE" up -d
log_ok "stack up"
docker compose -f "$COMPOSE_FILE" ps