vibey / rules
Cursor rule3 starsChanged 25 days agoMITView on GitHub
Onion layers for vibey (domain, application, infrastructure, cli, tui), import-linter, where new code belongs, the workspace tenants, and how to file a sub-doctrine. Use before adding a module under src/ or writing a standing rule.
We split this vibey-architecture.mdc into the rules it gives an agent
5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 38
When in doubt, push logic inward.
- Distinctive: line 90
If a capability exists inside this family, use it.
- Very specific: line 147
If a thing can be declared in the repository and reconciled from it, that is how it is done.
- Distinctive: line 168
Every change uses Specification-Driven Development (SDD) for intent and acceptance criteria, Test-Driven Development (TDD) for executable …
- Very specific: line 265
If it fails, the error names the contract and the violating import.
≈ “Common” is an estimate: at least 1 in 5 of the 6,350 Cursor rule files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
--- description: Onion layers for vibey (domain, application, infrastructure, cli, tui), import-linter, where new code belongs, the workspace tenants, and how to file a sub-doctrine. Use before adding a module under src/ or writing a standing rule. alwaysApply: false --- > **Cursor rule mirror** of `.claude/skills/vibey-architecture/SKILL.md`. When this guidance changes, update Claude, Cursor, Codex, and Antigravity in the same PR. # vibey architecture Dependencies point inward. `import-linter` enforces this in CI via four contracts: onion-layers, domain-independence, application-independence, and interfaces-declare-only. ``` src/vibey/ ├── domain/ # PURE. stdlib only. No I/O, no async, no third-party. ├── application/ # Protocol ports + use cases. Imports domain + stdlib. ├── infrastructure/ # Adapters. ONLY layer that may import asyncpg, httpx, etc. ├── cli/ # Typer. Calls application via bootstrap. ├── tui/ # Textual. Calls application via bootstrap. └── bootstrap.py # Composition root — the ONE module that sees every layer. ``` ## Where does new code go? 1. **Touches FS, network, clock, database, or an SDK?** → `infrastructure/`, behind a `Protocol` in `application/interfaces/<area>.py` (`azure`, `build`, `design`, `engines`, `gates`, `ledger`, `observability`, `projects`, `queue`, `review`, `system`, `visual`). `application/ports.py` is a compatibility re-export shim of seven names — never add to it. Never `import asyncpg` or vendor SDKs elsewhere. 2. **Pure decision, zero I/O?** → `domain/`. Examples: `phase.py`, `rotation.py`, `noloss.py`, `circuit.py`, `effort.py`. 3. **Orchestration (port → domain → port)?** → `application/`. 4. **Argument parsing / terminal formatting?** → `cli/` or `tui/`. 5. **Wiring concrete implementations to Protocols?** → `bootstrap.py` ONLY. When in doubt, push logic inward. `lint-imports` names the broken contract when you violate the onion. ## Shape: classes, behind interfaces Code lives in **class objects**. A module-level function is the method of last resort — a package's `__all__` façade, a `__main__` entry point, a bare function some library contract requires — and where you use one, write the reason at the definition. "It is only a few lines" is not a reason. **Every class has an interface beside it**, in a mirrored `interfaces/` directory (illustrative paths — there is no `services/` package): ``` src/vibey/services/github_service.py src/vibey/services/interfaces/github_service_interface.py ``` This is the target shape, not the current one. Today's interfaces packages (`application/interfaces/`, `cli/interfaces/`, and the empty `infrastructure/interfaces/`) are mostly organised by area; `cli/interfaces/early_signals_interface.py` is the one module that already follows the `<module>_interface.py` mirror. The mapping is mechanical: the directory gains an `interfaces/` child, the module gains an `_interface` suffix. Interfaces **declare**; they never consume — an interface module imports the standard library and other interfaces, nothing else from its own tree. The rule applies to every `interfaces/` package; the `interfaces-declare-only` contract in `.importlinter` enforces part of it today, for `application/interfaces` only (it forbids imports of `vibey.infrastructure`, `vibey.cli`, `vibey.tui` and `vibey.bootstrap`). Why: a class behind an interface is substituted at its seam. The caller takes the interface, the test passes a double, and nothing is patched — so the test depends on the contract, which is meant to be stable, rather than on the import graph, which is not. With a 100% branch floor that is not a style preference: a branch reachable only by `monkeypatch.setattr` is a branch whose test breaks for reasons unrelated to its subject. This applies to **new and changed code**. The existing tree (289 module-level functions, and 278 module-level classes outside an `interfaces/` package, 277 of them with no mirrored `_interface` module — AST walk of `src/vibey/**/*.py`, top-level definitions only, measured 2026-09-15) converges module by module, and the absorbed subtrees under `src/vibey_runners/` and `src/vibey_tools/` converge as they are touched — rewriting them on import would destroy the property their import exists to create. `domain/` gets no exemption: a pure function becomes a method on a stateless class, and purity is preserved, because purity was never about the absence of a class. See ADR-0016 and sub-doctrine 9.b (the declared seam). ## Dogfood the family first If a capability exists inside this family, use it. Do not reimplement it, and do not reach for a third-party equivalent. The bar is not "is ours better" — it is "does ours do this at all". Between `vibey_bootstrap`'s retry and a retry loop, use theirs. Between its dead-letter routing and a hand-rolled failure path, use its. Between its correlation scope and threading a request id by hand, use its. When the choice is between using a family feature and not using it, **always prefer using it**. The shape is the one this codebase already uses: a Protocol in `application/interfaces/`, an adapter in `infrastructure/` that satisfies it with the family package, and a line in `bootstrap.py`. Dogfooding is not a new pattern here — it is the existing one, applied to ourselves. A new implementation of something the family already ships needs a written reason at the call site, and the reason must be a capability gap. If ours is missing something, the fix is to add it to ours. Measured 2026-09-15: `src/vibey` imports **zero** family packages. See ADR-0017 (and sub-doctrine 10.e) for the parity backlog. ## The workspace tenants This repository is a uv workspace (`[tool.uv.workspace] members = ["src/vibey_runners/*", "src/vibey_tools/*"]`, ADR-0021). `vibey` is the root package; the tenants are absorbed packages that ship inside the one `vibey` distribution rather than under their own PyPI names (ADR-0037). Their former GitHub repositories (`the-vibey-project/claudeloop`, `vibey-gh`, and the rest) and their former PyPI projects no longer exist — never link either; link the directory in this repository instead. | Directory | Package | Python floor | Own checks | |---|---|---|---| | `src/vibey_runners/claude` | `claudeloop` | 3.12 | pytest, ruff, mypy, import-linter | | `src/vibey_runners/codex` | `codexloop` | 3.12 | pytest, ruff, mypy, import-linter | | `src/vibey_runners/qwen` | `qwenloop` | 3.12 | pytest, ruff, mypy, import-linter | | `src/vibey_runners/common` | `vibey-runners-common` | 3.12 | ruff, mypy, import-linter (no test directory yet) | | `src/vibey_tools/gh` | `vibey-gh` | 3.12 | pytest (100% branch), black, isort, mypy, bandit | | `src/vibey_tools/skills` | `vibey-skills` | 3.12 | manifest/link validators, unittest | | `src/vibey_tools/bootstrap` | `vibey-bootstrap` | 3.12 | pytest (100% line), black, isort, mypy, bandit | Rules for working inside a tenant: - **Its own configuration governs it.** Each tenant's `pyproject.toml` carries its own tool sections and test directory (`test/` for `gh` and `bootstrap`, `tests/` for the rest). Run its checks from its own directory; the root `pytest` (`testpaths = ["tests"]`) and the root `mypy`/`bandit`/coverage gates never reach it. Root `ruff check .` and `ruff format --check .` do (`include = ["src/**/*.py", "tests/**/*.py"]`). Absorbed packages keep the gates they arrived with (ADR-0022); see the `vibey-quality-gates` skill for the commands. - **Converge as touched.** ADR-0016 and ADR-0017 apply to tenants only as their code is changed. Do not restyle or restructure a tenant on import. - **The root layers do not import tenants,** except the dependency-free family packages `domain/` may use (below) and adapters in `infrastructure/`. ## Everything-as-code, and never less of it If a thing can be declared in the repository and reconciled from it, that is how it is done. Branch protection and the repository profile live in `.vibey-gh.toml` (`[rulesets]`, `[repository_profile]`) and are reconciled by `vibey-gh`; pipelines are rendered from templates; policy is `.importlinter` and the coverage gates. No settings page, no one-off `gh api`, no runbook step that says "then set X". And **everything that can be generic and configurable must be.** A hard-coded value that could have been a key, a special case that could have been a rule, a path that could have been discovered — each takes a decision away from whoever adopts this next, silently. Never change anything to a state that is less generic or less configurable. A default is fine; a default is configurability with an opinion. A constant is not. Worked example: `find_root` walked to `.git`, which broke the moment a project lived inside a repository belonging to something else. The fix was not an explicit root at the two call sites that noticed — it was that a directory carrying its own `.vibey-gh.toml` stops the walk. Same work, aimed at the general shape. See ADR-0018. ## Convergence-Driven Development is the delivery loop Every change uses Specification-Driven Development (SDD) for intent and acceptance criteria, Test-Driven Development (TDD) for executable checks, and Convergence-Driven Development (CDD) above them to ground, implement, test, review and repair against the actual tracked repository. At every iteration, classify the trajectory as converging, neutral or diverging against the remaining criteria and failing checks. A divergence is allowed only when it is small, bounded and has a concrete path to reconvergence; large or pathless divergence is abandoned. Never treat activity, a marker or a generated file as completion evidence. See sub-doctrine 9.c and ADR-0039. Project atoms can compose chemical structures, and a suite of suites can be alive in the digital realm as a software organism when identity, feedback, adaptation, repair and exchange are evidenced; inspect that higher-order trajectory whenever the change participates in it. ## A standing rule goes in the canon This project has law, not just decision records: the constitutional cluster in `src/vibey_tools/gh/docs/` — the Constitution, the Twelve Doctrines, the Ten Commandments, the Bill of Rights, SD-01 — indexed by `vibey-gh corpus-index` so drift between published law and its index is one hash comparison. The Twelve are **sealed**. Anything new files as a **sub-doctrine** under one of them, in the established form, and is ratified by the operator's merge (Article II.3). The ratchet applies: a refinement may strengthen, never weaken. So if what you are writing binds future decisions, survives a rewrite, and is about conduct rather than mechanism — it is a sub-doctrine, and writing it only as an ADR leaves it with no ratification, no ratchet protection and no chunk in the corpus. Write both: the sub-doctrine states the rule, the ADR argues it. "PostgreSQL, not SQLite" is a mechanism and stays an ADR. "Dogfood the family" is a rule and belongs in the canon. See ADR-0020 and sub-doctrine 12.b. ### Filing a sub-doctrine 1. **Pick the parent.** Find the one of the twelve `## N — title` headings in `src/vibey_tools/gh/docs/doctrines.md` the rule stands under. Use the next free letter (existing entries: 2.a–2.b, 4.a, 7.a, 8.a, 9.a–9.b, 10.a–10.e, 12.a–12.c). 2. **Write it in the established form** — one bold run-in paragraph under the parent heading: ``` **9.c — short title** *(ratified by the merge that carried this entry)*: the rule, stated as conduct, in present tense. ``` The ratification note becomes a date only after the fact; do not pre-date it. 3. **Write the ADR** that argues it and cite the sub-doctrine by number (ADR-0016 through ADR-0020 pair with 9.b, 10.e, 12.c, 2.b and 12.b). 4. **Regenerate and check the corpus index** from the tenant directory: ```bash cd src/vibey_tools/gh uv run vibey-gh corpus-index # rewrites corpus-index.json uv run vibey-gh corpus-index --check # "corpus intact: N chunk(s)" ``` The index lives at `src/vibey_tools/gh/corpus-index.json` (32 chunks on 2026-09-15); there is no root copy. Commit it with the doctrine change. 5. **Ratify by human merge.** Article II.3: the doctrines' sub-entries are ratified by humans through reviewed pull requests — a machine may draft, a human ratifies. Article IV.2 (the ratchet): a change may clarify, strengthen or extend; it may never weaken. 6. **Know the blast radius.** Article V.4: a ratified change to the canon yanks every prior release of the code. A sub-doctrine merge is a release-level event. A standing subdoctrine carried verbatim in agent files (such as SD-01 in `CLAUDE.md`) is amended only by the operator, with a version bump — never edited in passing. ## The forbidden imports `domain/` must never import: - Any layer: `vibey.application`, `vibey.infrastructure`, `vibey.cli`, `vibey.tui` - Third-party: `asyncpg`, `psycopg`, `httpx`, `typer`, `structlog`, `pydantic`, `textual` - `vibey_bootstrap` — named rather than categorised: it carries the Azure SDK and OpenTelemetry, so importing it here pulls a third-party graph in transitively - I/O, async and wall-clock from stdlib: `tests/domain/test_domain_purity.py` walks the AST and rejects imports of `asyncio`, `asyncpg`, `subprocess`, `socket` and `pathlib` (plus the third-party names above), calls to `open()`, `os.environ`, `os.getenv`, `datetime.now()`/`today()`, `time.time()`, and any `async def`, `await`, `async with` or `async for` `domain/` **may** import a family package that is itself dependency-free — `vibey_gh`, `vibey_skills`, `vibey_runners.common` all declare `dependencies = []`, so they add nothing to the graph that stdlib-only did not already allow. This is the ADR-0017 caveat to the stdlib-only rule: a first-party package is not third-party. `application/` must never import: - `vibey.infrastructure`, `vibey.cli`, `vibey.tui` ## Verify before committing ```bash uv run lint-imports ``` If it fails, the error names the contract and the violating import. Fix it by moving the offending code to a layer that can legally import the dependency, or by abstracting the dependency behind a Protocol in `application/interfaces/<area>.py` (`ports.py` is a compatibility re-export shim — never add to it). The onion layering has no ADR of its own: its authority is `.importlinter` and `docs/plans/architecture-and-roadmap.md`. ADR-0016 governs the class/interface shape; ADR-0021 records the monorepo layout.
Where files are headingNew Cursor rule files have more rules about planning and tracking tasks, and fewer about code style.
Of the Cursor rule files we read, the 3,978 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository23 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .cursor/rules/typesafe-ai.mdcCursor rule
- .cursor/rules/vibey-domain-model.mdcCursor rule
- .cursor/rules/vibey-engine-adapters.mdcCursor rule
- .cursor/rules/vibey-quality-gates.mdcCursor rule
- .cursor/rules/vibey-releasing.mdcCursor rule
- .cursor/rules/vibey-testing.mdcCursor rule
- docs/llms.txtllms.txt
- typesafe-aiSkill
- vibey-architectureSkill
- vibey-domain-modelSkill
- vibey-engine-adaptersSkill
- vibey-quality-gatesSkill
- vibey-releasingSkill
- vibey-testingSkill
- typesafe-aiSkill
- vibey-architectureSkill
- vibey-domain-modelSkill
- vibey-engine-adaptersSkill
- vibey-quality-gatesSkill
- vibey-releasingSkill
- vibey-testingSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

