vibey / rules
Cursor rule3 starsChanged 25 days agoMITView on GitHub
How vibey drives claudeloop, codexloop, and the local engines — gptossloop (the sovereign default), and the opt-in qwenloop and claudeloop-local — the engine adapter pattern, tiers, argv building, and the conformance suite.
We split this vibey-engine-adapters.mdc into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
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 89
Never hand-roll a kill followed by an unbounded process.wait() (#283, ADR-0017).
- Very specific: line 130
Before build.implement seeds a fresh run, it can ask the vibey-skills CLI for a skills-context packet …
- Very specific: line 201
If an engine saturates, the descriptor sets achieved to the tier it actually delivers.
≈ “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: How vibey drives claudeloop, codexloop, and the local engines — gptossloop (the sovereign default), and the opt-in qwenloop and claudeloop-local — the engine adapter pattern, tiers, argv building, and the conformance suite.
alwaysApply: false
---
> **Cursor rule mirror** of `.claude/skills/vibey-engine-adapters/SKILL.md`. When this guidance changes, update Claude, Cursor, Codex, and Antigravity in the same PR.
# vibey engine adapters
Vibey drives three autonomous session runners through five engine ids:
`claudeloop` and `codexloop` (tier PAID) make up the default pool,
`DEFAULT_DESCRIPTORS` (cursorloop and agyloop were retired by ADR-0078);
three local engines (tier LOCAL,
`LOCAL_DESCRIPTORS`) join them, each behind its own switch (ADR-0064):
`gptossloop` — the local runner on GPT-OSS 20B, the sovereign default, **on**
unless `VIBEY_FEATURE_GPTOSSLOOP=0` / `[features] gptossloop = false`; `qwenloop`
— the same runner on a Qwen model (`qwen3:14b`), opt-in by
`VIBEY_FEATURE_QWENLOOP=1` / `[features] qwenloop = true` (ADR-0015); and
`claudeloop-local` — the claudeloop binary run with a local backend profile
(`--profile NAME`), opt-in by
`VIBEY_FEATURE_CLAUDELOOP_LOCAL` / `[features] claudeloop_local` (ADR-0038).
Under sub-doctrine 8.a local engines are **preferred first**: selection runs
SWRR within the LOCAL tier and falls back to PAID only when no local engine is
eligible. gptossloop's model is also the sovereign DESIGN/DECOMPOSE provider
(`gptossloop_design.py`, `gptossloop_decompose.py`, ADR-0027), the default
`--provider` (`--provider qwenloop` is read as gptossloop). The runners' source lives in this
repository under `src/vibey_runners/{claude,codex,qwen,common}`
(ADR-0021); vibey drives the installed binaries, not those packages' Python
APIs. Each has its own CLI surface, effort vocabulary,
state directory, and done marker. Vibey abstracts these differences via
`EngineAdapter`, a `Protocol` defined in `application/interfaces/engines.py`
and re-exported from `application/ports.py`.
## The adapter pattern
`EngineAdapter` (read the real definition before relying on this summary —
it's short):
- `descriptor: EngineDescriptor` — a property, the engine's static facts
- `async def preflight() -> PreflightResult` — runs `<engine> doctor`;
classifies auth + availability
- `async def start(spec: RunSpec) -> RunHandle` — builds argv from
`descriptor.effort_projection` + isolation flags, spawns the runner,
returns a handle over its run directory
- `def tail(handle: RunHandle) -> AsyncIterator[EngineEvent]` — streams the
runner's `events.jsonl`, translated into vibey's own event vocabulary
through `loop_events.py::LOOP_EVENT_MAP`. Only the runner's own turn
boundary maps to `TurnCompleted`, one per real turn, because the budget
brake counts it; chatter and stream deltas that echo a turn's text map
to `TranscriptRecorded`
- `async def send_prompt(handle, text, *, now: bool) -> None` — writes the
runner's control-plane inbox
- `async def stop(handle: RunHandle) -> StopSummary` — soft-stops the run;
collects `stop-summary.md` and the final snapshot
- `async def snapshot(handle: RunHandle) -> SnapshotRef | None`
- `def classify(raw: Mapping[str, object]) -> CapacityState` — maps a raw vendor
error shape to `Available | WindowExhausted | CreditsExhausted | AuthenticationFailed`
- `def attribute(exit_code: int, tail: str) -> FailureClass` — attributes a dead
process to a `FailureClass` (`capacity`, `engine`, `work`, `vibey`)
An engine session never inherits the worker's environment: every spawn (the run,
`--version`, `doctor`, `--help`) builds it through
`LoopProcessAdapter.environment`, an `EngineEnvironmentPolicy`
(`infrastructure/engines/engine_environment.py`) over the one builder,
`infrastructure/process/child_environment.py::ChildEnvironment`. It is an
allow-list: the system basics, the descriptor's `env_passthrough` and `auth_env`,
and the project's `engine_environment` additions, declared in vibey.toml's
`[engine_environment]` (copied into the record by `vibey new`) or the
`VibeyProject` spec's `engineEnvironment` — never hand-edited into the record.
`VIBEY_*`, `PG*` and anything DSN- or password-shaped can never be on it, so
`VIBEY_PG_URL` never reaches a model-driven process; a descriptor or overlay that
tries is refused when the adapter is built. The worker's startup preflight and
`vibey doctor --record` probe with the project's policy, so a declared credential
reaches the auth check too. A new engine declares what its runner and vendor CLI
read in `env_passthrough`; do not add a spawn path that passes `env=` itself.
vibey's own git calls (`infrastructure/git/clean_env.py`) and the `az` adapter
build their environments the same way, never from a copy.
`LoopProcessAdapter.env_overlay` is laid over that last (run and preflight alike) —
how gptossloop gets `GPTOSSLOOP_BASE_URL`/`GPTOSSLOOP_MODEL`, and qwenloop only
`QWENLOOP_BASE_URL` (never a model), from the one setting `VIBEY_OLLAMA_URL`
(`local_engines.py::LocalEndpointEnvironment`).
Preflight runs `<binary> doctor` plus `descriptor.doctor_args`
(claudeloop-local: `--profile NAME`).
Preflight's `--version` and `doctor` probes each lead a process group of their own.
On a timeout or a cancellation, `infrastructure/process/reaper.py::ProcessReaper`
kills the whole group and reaps it within `LoopProcessAdapter.kill_grace_seconds`,
logging `engine_process_not_reaped` if a descendant that left the group still holds
the pipes. The gate runner and the skills-context compiler use the same reaper.
Never hand-roll a kill followed by an unbounded `process.wait()` (#283, ADR-0017).
The run's environment strips the interpreter's prefix only when it is a venv
(`infrastructure/process/python_env.py::OrchestratorPythonEnv`, shared with the gate
runner), so a system-Python install keeps `/usr/bin`.
`start()` internally calls `infrastructure/engines/argv.py::build_argv()` —
that's a plain function, not an adapter method; it takes both the descriptor
and the `RunSpec` (`build_argv(descriptor, spec)`), not just the spec.
See `application/interfaces/engines.py::EngineAdapter` and
`infrastructure/engines/scripted.py::ScriptedEngine` (the fake runner used in
tests). `infrastructure/engines/loop_process_adapter.py::LoopProcessAdapter`
is the production adapter (it supersedes `ClaudeLoopProcess`): it spawns the
loop process, streams `events.jsonl`, detects `done_marker`, treats exit code 75
as wind-down, and classifies capacity through `classify.py` — one class driven
by descriptors, not one class per engine.
Engine choice for engine-driven BUILD jobs goes through
`SelectingEngineProvider → application/engine_selector.py::EngineSelector`
(SWRR over `domain/rotation.py`), wired in `bootstrap.py`. The selector builds a
candidate per eligible engine and `domain/rotation.py::preferred_tier` offers SWRR
only the first tier in `TIER_PREFERENCE` (LOCAL, PAID) holding a candidate that
can win. The provider passes its pool (adapters ∩ `--engines`) as the allow-list,
and refreshes every enabled local engine's health from its `doctor` before each
selection. Selection requires populated `engine_health` rows, so an engine with
no recorded conformance is never selected.
Hybrid dispatch (ADR-0079, sub-doctrine 8.k) sits on top: `[engines] mode` is
`singleton` (exactly the above), `hybrid` or `auto` (the default: `hybrid` only from a
current `EngineDispatchMeasured` ledger event, else `singleton`). Under `hybrid`,
`domain/engine_dispatch.py::EngineDispatcher` plans over the same candidates: local
engines fill their `[engines.slots]` first (1 local, 2 paid by default); with every
eligible local slot occupied a job is held (`SlotHeld` → `CapacityDeferred`, recorded once
per attempt as `EngineSlotWaitStarted`) until it has waited `overflow_after_seconds`, then
a paid engine with a free slot takes it as overflow — reserved under the project row's
lock against `paid_daily_cap`, counted from `EngineOverflowSelected` events for the UTC
day. Slots in use are the job table's unexpired leases per `assigned_engine`. The provider
takes `dispatch=` (`application/engine_dispatch_service.py::EngineDispatchService`, wired
by `bootstrap._engine_dispatch`); `dispatch=None` is today's selection, unconditionally.
Counts reach the domain as data; never read the queue or a clock from `domain/`.
Before `build.implement` seeds a fresh run, it can ask the `vibey-skills` CLI for
a skills-context packet (`infrastructure/skills_context.py::VibeySkillsContextCompiler`,
modes `off`/`shadow`/`inject`, budget 1,000–32,000 with a 6,000 default;
ADR-0031). The packet is recorded as a `vibey_skills_context_packet` artifact.
## Engine descriptors
`infrastructure/engines/descriptors.py` defines `CLAUDELOOP`, `CODEXLOOP`,
`GPTOSSLOOP`, `QWENLOOP`, `CLAUDELOOP_LOCAL` — one
`EngineDescriptor` per engine. claudeloop-local's is *built* from `[engines.claudeloop_local]`
(`profile`, `context_window`, `structured_verdict`) by
`ClaudeloopLocalDescriptors.build()`; the constant is the default profile `local`.
`GPTOSSLOOP` is `QWENLOOP` with its own id, binary and `GPTOSSLOOP_*`
passthrough: one runner, whose run layout (`.qwenloop/runs/`), done marker and
events both engines share.
`DEFAULT_DESCRIPTORS` is the default pool of two, `LOCAL_DESCRIPTORS` the three
local ones, `ALL_DESCRIPTORS` all five, and `BY_ENGINE_ID` maps every
`EngineId`. The worker
adds adapters for the local engines that are switched on through
`local_engines.py::LocalEngineSettings` — the one resolver bootstrap, `worker`,
`work` and `doctor` share.
Each descriptor (`domain/engine.py::EngineDescriptor`) declares:
- `engine_id`, `binary` — the executable name (e.g., `"claudeloop"`), `min_version`
- `state_dir` — where runs are stored (e.g., `".claudeloop/"`)
- `done_marker` — the text signaling completion (e.g., `"CLAUDELOOP_TASK_FULLY_COMPLETE"`)
- `capabilities` — which features it supports (`savepoints`, `unwind`, etc.). A
claim must agree with the facts below (`test_descriptors.py`): `mid_run_prompt`
exactly where `controls.prompt` is declared, `attachments` only where `images`
or `paste_images` is proven `True`, `web_search` only where the runner's `run`
takes `--web-search`
- `effort_projection` — how vibey's 5-level ladder (`TRIVIAL, LOW, STANDARD, HIGH, MAX`)
maps to the engine's native flags
- `auth_env` — environment variables that must be set (empty for gptossloop and qwenloop)
- `env_passthrough` — the variables the engine's runner and vendor CLI read, passed
to its sessions (a trailing `*` names a prefix); nothing else reaches a session
unless the project declares it
- `session_verb`, `isolation_flags` (per `IsolationLevel`)
- `cost_per_mtok_in`/`cost_per_mtok_out`, `context_window`, `base_weight` (rotation weight)
- `tier` (`EngineTier.PAID` default, `LOCAL` for gptossloop, qwenloop and claudeloop-local) and
`doctor_args` (extra `doctor` arguments; claudeloop-local: `--profile NAME`)
- `supports_cwd_flag` (default `True`; codexloop: `False`) and `plan_flag`
(default `None` = positional plan path; no current engine sets one)
- `affordances` — `EngineAffordances`: `images`, `files`, `paste_text`,
`paste_images`, `plugins` (`PluginSystem`) and `mcp`, each `True`, `False` or
`None` (unknown, the default). `evidence` names, for every value that is set,
where the runner's own code shows it; a set value without evidence fails a
test. Never set one from memory or a vendor's marketing
- `controls` — `EngineControls`: `stop`, `wind_down` and `prompt` as argv
templates after the binary (`{run_id}`, `{cwd}`, `{text}`), `None` where the
runner has no such verb or does not act on it. Tests read every template against
the runner's own Typer app the way click parses it
- `events` — `EventLog`: the `path` template of its `events.jsonl` and its
`envelope` (`EventEnvelope`: `type`, `event_type+payload`, or `event_type`)
**Effort projection example** (claudeloop, as in `descriptors.py`):
```python
effort_projection={
Effort.TRIVIAL: EngineInvocation(("--preset", "low", "--effort", "low"), achieved=Effort.TRIVIAL),
Effort.LOW: EngineInvocation(("--preset", "low", "--effort", "medium"), achieved=Effort.LOW),
Effort.STANDARD: EngineInvocation(("--preset", "medium", "--effort", "high"), achieved=Effort.STANDARD),
Effort.HIGH: EngineInvocation(("--preset", "high", "--effort", "high"), achieved=Effort.HIGH),
Effort.MAX: EngineInvocation(("--preset", "high", "--effort", "max"), achieved=Effort.MAX),
}
```
gptossloop and qwenloop project effort onto `--max-turns` (8, 16, 40, 64, 96).
claudeloop-local passes `--profile NAME --preset low|low|medium|high|high` and
never `--effort`; HIGH and MAX report `achieved=STANDARD`, its honest ceiling.
If an engine **saturates**, the descriptor sets `achieved` to the tier it
actually delivers. codexloop is the extreme case: its `run` has no effort flag,
so every level projects to empty argv with `achieved=Effort.STANDARD`.
`domain/rotation.py::fidelity_factor(descriptor, requested)` computes the
`Candidate.fidelity_factor` that scales the effective weight of engines that
saturate below the requested effort; `application/engine_selector.py` applies
it.
See ADR-0006 (normalized effort ladder).
## argv building
`infrastructure/engines/argv.py::build_argv(descriptor, spec)` takes an
`EngineDescriptor` and a `RunSpec` (fields: `run_id`, `worktree_path`,
`prompt`, `effort`, `isolation`, optional `session_id` to resume) and
produces the command line — read the real function, it is short. As of
2026-09-15 it emits, in order:
1. `<binary> run`, or `<binary> resume <session_id>` when `spec.session_id` is set.
2. For `run` only: the plan path `<worktree>/.vibey/plans/<run_id>.md`, prefixed
by `descriptor.plan_flag` when an engine sets one,
then `--run-id <run_id>` (so the adapter finds the run directory the process
writes to).
3. `descriptor.invoke(effort).argv`.
4. `descriptor.isolation_flags.get(isolation, ())`.
5. `--cwd <worktree_path>`, only when `descriptor.supports_cwd_flag` (codexloop:
`False`).
30 golden files under `tests/infrastructure/engines/golden/` (5 engines × 6
efforts, ULTRA included; `test_argv.py` parametrizes over `ALL_DESCRIPTORS`) capture the
expected argv for each combination — the source of truth for the exact current
shape.
`argv.py::RUN_ARGV_TEMPLATE` (`RunArgvTemplate`) is `build_argv`'s `run` line with
its per-run values as placeholders: `{binary}`, `run`, `{plan_flag?}` (only with a
`plan_flag`), `{plan}`, `--run-id`, `{run_id}`, `{effort_argv...}`, and `--cwd
{cwd}` when `supports_cwd_flag`. It is what `vibey loops` publishes for a caller
that starts a runner itself, such as the VS Code extension. Tests compare it with
`build_argv` for every engine and effort, and read it, filled in at every effort
and isolation level, against the runner's own `run` definition.
## What `vibey loops` reports
`vibey loops [--json]` (`application/loops.py::LoopCatalog`, `cli/loops.py`) lists
the two loops of sub-doctrine 8.c, each engine under the loop its tier puts it in,
with every effort's argv, achieved effort and model, and the descriptor's run
template, `affordances`, `controls`, `events` and env names (names only). It reads
the local switches (and whether each is `on_by_default`) and each local runner's
model through `local_engines.py`, the resolvers
`vibey doctor` reads. An engine canon 8.b repeals (`REPEALED_FROM_LOOPS`; empty
since OpenCode's engine was deleted) is listed with `repealed: true` and left out of
every by-effort view. The document for one
fixed environment is committed as `tests/cli/golden/vibey-loops.json`: after
changing a descriptor, regenerate it with
`VIBEY_UPDATE_GOLDENS=1 uv run pytest tests/cli/test_loops_cli.py -k golden` and
commit it with the change.
## Capacity classification
`infrastructure/engines/classify.py::classify_capacity(engine_id, raw)` dispatches
to one private classifier per engine (claudeloop-local shares claudeloop's) and
maps vendor-specific error shapes to vibey's `CapacityState`. claudeloop writes
its capacity as the class *name* (`"capacity": "CreditsExhausted"`); both that and
the `{"state": ...}` mapping are read, and `BackendMisconfigured` maps to
`AuthenticationFailed` (terminal, never credits). Exit 78 (EX_CONFIG,
`EXIT_CODE_BACKEND_MISCONFIGURED`) is attributed to ENGINE, and an incomplete run
that exited 78 parks its BUILD job on an `engine_misconfigured` gate
(`RunOutcome.misconfiguration_gate`) instead of retrying:
```python
Available | WindowExhausted | CreditsExhausted | AuthenticationFailed
```
**The critical distinction:** `WindowExhausted` has a `resets_at` deadline;
`CreditsExhausted` does not. A window exhaustion is waitable; credits
exhaustion requires a human top-up.
Each engine's classifier pattern is versioned in `CREDITS_FIXTURES`,
`WINDOW_FIXTURES`, `AUTH_FIXTURES`, and `AVAILABLE_FIXTURES` — shared between
the classifier's own tests and the conformance suite.
**Note:** these are synthesized fixture payloads, not captured real vendor
errors. If you get access to real `*loop` binaries or real captured error
payloads, this is the first thing to replace.
## The conformance suite
`application/conformance.py::run_conformance()` runs 9 named checks per
engine (grep the file for the check-name strings if this list drifts):
`binary` (installed + version), `flags` (`run --help` exposes what the
descriptor claims), `state_dir`, `run_dir_shape` (a real run produces
`meta.json`/`events.jsonl`/`snapshots/latest.json` — this one races a real
subprocess's own startup time, not just an instant file check; read the
function if you're touching it), `snapshot_schema`, `capacity_map`,
`done_marker`, `control_plane`, `structured_verdict`.
A failing conformance check marks that engine **ineligible** rather than
letting it fail mid-cycle.
```bash
vibey doctor --conformance --record
```
`--record` persists the preflight and conformance result to `engine_health`.
Without a recorded pass the worker warns and never selects the engine for
engine-driven jobs.
Live runs of the suite are split in two modes (ADR-0030): `tests/live/` with
`@pytest.mark.live` runs conformance and rotation against `ScriptedEngine`
and scripted stand-in binaries, with no model calls; `@pytest.mark.paid` spawns real binaries
against real models and is excluded by default.
See ADR-0001 (orchestrate, do not reimplement).
## When to read this skill
Before:
- Adding a new engine.
- Changing effort mappings.
- Changing what an engine declares it can take, control or emit (`affordances`,
`controls`, `events`), or anything `vibey loops` reports.
- Debugging why rotation is skipping an engine.
- Updating capacity classification patterns after a vendor API change.
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-architecture.mdcCursor rule
- .cursor/rules/vibey-domain-model.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.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
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.

