From 374288a105805804232686fce5ef19f05857d713 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Fri, 7 Aug 2026 11:17:53 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20bootstrap=20meta=20=E2=80=94=20cross-pr?= =?UTF-8?q?oject=20best-practices=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.example | 16 +++ .gitignore | 17 +++ AGENTS.md | 194 +++++++++++++++++++++++++++++ BASELINE-PROMPT.md | 105 ++++++++++++++++ Makefile | 60 +++++++++ PATTERNS.md | 247 +++++++++++++++++++++++++++++++++++++ README.md | 70 +++++++++++ STATUS.md | 47 +++++++ WORKING.md | 9 ++ crush.json | 32 +++++ docker-compose.yml.example | 26 ++++ docs/JOURNAL.md | 14 +++ hooks/audit-before-git.sh | 32 +++++ hooks/block-todos.sh | 9 ++ hooks/enforce-bash.sh | 30 +++++ hooks/enforce-rules.sh | 42 +++++++ hooks/exit-protocol.sh | 26 ++++ questions-v1.md | 45 +++++++ scripts/check-rules.sh | 221 +++++++++++++++++++++++++++++++++ scripts/docker-run.sh | 39 ++++++ scripts/down.sh | 17 +++ scripts/garden.sh | 65 ++++++++++ scripts/lib/common.sh | 137 ++++++++++++++++++++ scripts/pre-commit | 25 ++++ scripts/pre-push | 25 ++++ scripts/setup-hooks.sh | 43 +++++++ scripts/up.sh | 19 +++ 27 files changed, 1612 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 BASELINE-PROMPT.md create mode 100644 Makefile create mode 100644 PATTERNS.md create mode 100644 README.md create mode 100644 STATUS.md create mode 100644 WORKING.md create mode 100644 crush.json create mode 100644 docker-compose.yml.example create mode 100644 docs/JOURNAL.md create mode 100755 hooks/audit-before-git.sh create mode 100755 hooks/block-todos.sh create mode 100755 hooks/enforce-bash.sh create mode 100755 hooks/enforce-rules.sh create mode 100755 hooks/exit-protocol.sh create mode 100644 questions-v1.md create mode 100755 scripts/check-rules.sh create mode 100755 scripts/docker-run.sh create mode 100755 scripts/down.sh create mode 100755 scripts/garden.sh create mode 100644 scripts/lib/common.sh create mode 100755 scripts/pre-commit create mode 100755 scripts/pre-push create mode 100755 scripts/setup-hooks.sh create mode 100755 scripts/up.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0a0e415 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..916fcc9 --- /dev/null +++ b/.gitignore @@ -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 +*~ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..14ab9dc --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,194 @@ +# โ€” Agent Guidelines + +> **Active agent:** Crush running (). Permission mode: . +> **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 + +<1โ€“3 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`.) + + + +## Repository Layout + +``` +/ +โ”œโ”€โ”€ 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:** + ``` + (): + + + + ๐Ÿ’˜ Generated with Crush + + Assisted-by: via Crush + ``` + 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 ` for one-offs. +- **Container naming: never use Docker's default.** Always name with a project prefix (e.g. `-`). 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 diff --git a/BASELINE-PROMPT.md b/BASELINE-PROMPT.md new file mode 100644 index 0000000..3f29668 --- /dev/null +++ b/BASELINE-PROMPT.md @@ -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. `-`). +- 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..23e6733 --- /dev/null +++ b/Makefile @@ -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." diff --git a/PATTERNS.md b/PATTERNS.md new file mode 100644 index 0000000..727e330 --- /dev/null +++ b/PATTERNS.md @@ -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 ` | +| KNEL-AIMiddleware | `scripts/.sh` | +| EngStack | `scripts/-.sh` | +| PFVCluster | `tests/.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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f27cb08 --- /dev/null +++ b/README.md @@ -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 + +``` +/ +โ”œโ”€โ”€ 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. diff --git a/STATUS.md b/STATUS.md new file mode 100644 index 0000000..2437caf --- /dev/null +++ b/STATUS.md @@ -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/. + +## 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). diff --git a/WORKING.md b/WORKING.md new file mode 100644 index 0000000..59c38a5 --- /dev/null +++ b/WORKING.md @@ -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) diff --git a/crush.json b/crush.json new file mode 100644 index 0000000..f9fc12f --- /dev/null +++ b/crush.json @@ -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 + } + ] + } +} diff --git a/docker-compose.yml.example b/docker-compose.yml.example new file mode 100644 index 0000000..f943c20 --- /dev/null +++ b/docker-compose.yml.example @@ -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 `_` 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 diff --git a/docs/JOURNAL.md b/docs/JOURNAL.md new file mode 100644 index 0000000..043aaac --- /dev/null +++ b/docs/JOURNAL.md @@ -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 diff --git a/hooks/audit-before-git.sh b/hooks/audit-before-git.sh new file mode 100755 index 0000000..71acda5 --- /dev/null +++ b/hooks/audit-before-git.sh @@ -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 diff --git a/hooks/block-todos.sh b/hooks/block-todos.sh new file mode 100755 index 0000000..2ecadd4 --- /dev/null +++ b/hooks/block-todos.sh @@ -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 diff --git a/hooks/enforce-bash.sh b/hooks/enforce-bash.sh new file mode 100755 index 0000000..c9c27f1 --- /dev/null +++ b/hooks/enforce-bash.sh @@ -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 " >&2 + exit 2 + fi +fi + +echo '{}' diff --git a/hooks/enforce-rules.sh b/hooks/enforce-rules.sh new file mode 100755 index 0000000..c726a52 --- /dev/null +++ b/hooks/enforce-rules.sh @@ -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 '{}' diff --git a/hooks/exit-protocol.sh b/hooks/exit-protocol.sh new file mode 100755 index 0000000..c759ffa --- /dev/null +++ b/hooks/exit-protocol.sh @@ -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 '{}' diff --git a/questions-v1.md b/questions-v1.md new file mode 100644 index 0000000..769d2bd --- /dev/null +++ b/questions-v1.md @@ -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/`. 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:** โ€” diff --git a/scripts/check-rules.sh b/scripts/check-rules.sh new file mode 100755 index 0000000..ac6bfe4 --- /dev/null +++ b/scripts/check-rules.sh @@ -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 "" ""` 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 " "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 _). +# ---------------------------------------------------------------------------- +$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 </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 diff --git a/scripts/docker-run.sh b/scripts/docker-run.sh new file mode 100755 index 0000000..2e0ce38 --- /dev/null +++ b/scripts/docker-run.sh @@ -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 [command...] +# Runs in with the repo mounted at /data, cwd /data. +# With no command, drops into the image's default entrypoint. +# docker-run.sh --shell +# 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 diff --git a/scripts/down.sh b/scripts/down.sh new file mode 100755 index 0000000..b5af9ab --- /dev/null +++ b/scripts/down.sh @@ -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" diff --git a/scripts/garden.sh b/scripts/garden.sh new file mode 100755 index 0000000..0241dfe --- /dev/null +++ b/scripts/garden.sh @@ -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 diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh new file mode 100644 index 0000000..a3dd5e4 --- /dev/null +++ b/scripts/lib/common.sh @@ -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 โ€” return 0 if 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 +# 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 +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 +} diff --git a/scripts/pre-commit b/scripts/pre-commit new file mode 100755 index 0000000..7d65d35 --- /dev/null +++ b/scripts/pre-commit @@ -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 diff --git a/scripts/pre-push b/scripts/pre-push new file mode 100755 index 0000000..7c5bf7d --- /dev/null +++ b/scripts/pre-push @@ -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 diff --git a/scripts/setup-hooks.sh b/scripts/setup-hooks.sh new file mode 100755 index 0000000..238b879 --- /dev/null +++ b/scripts/setup-hooks.sh @@ -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 <