Installs into .claude/skills of the current project.
Are you the author of Vibey Domain Model?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/the-vibey-project-vibey-domain-model)
---
name: vibey-domain-model
description: Phases, jobs, ledger, handoff, rotation, and the no-loss gate — the core domain types and invariants in vibey.
allowed-tools: Read
---
# vibey domain model
Read `docs/plans/domain-model.md` before touching `domain/`. This skill is a
compressed orientation; the full spec lives in that doc.
## Phases
Six numbered phases plus optional visual interstitial:
```
INTAKE → ① DESIGN → [optional VISUAL_DESIGN] → ② BUILD ⇄ ③ REVIEW
▲──────────────────────────┘ │
│
③ ── user declines deployment ──────────────────────→ DONE (local)
│ user opts into deployment
▼
④ DEPLOY_DESIGN → ⑤ DEPLOY_EXECUTE → ⑥ DEPLOY_REVIEW → DONE (deployed)
```
Interactive phases (talk to the user): ①, ③, ④, ⑥, VISUAL_DESIGN.
Autonomous phases (unattended): ②, ⑤.
**Loop-backs:**
- `BUILD → DESIGN` when blocked on ambiguity (spec insufficient).
- `REVIEW → DESIGN` (default) or `REVIEW → BUILD` (fast path when every
finding is `unambiguous`). See ADR-0010.
- `DEPLOY_REVIEW` can route to ④, ⑤, or back to ①/②/③ depending on the
failure classification.
**Opt-in gates:**
- Visual design: after ① accepts a buildable spec, vibey asks whether to
enter the visual-design interstitial. Declining takes the `DESIGN → BUILD`
edge directly.
- Deployment: after ③ has no open product findings, vibey asks whether to
work on deployment. Declining records `completion_mode = "local"` and
transitions to `DONE` without enqueueing any Azure job.
See ADR-0014 (optional visual-design and deployment opt-in).
## Jobs
Job kinds are strings registered against handlers in `bootstrap.py` (25 kinds
as of 2026-09-15):
- DESIGN: `design.interview`, `design.research`, `design.synthesize`, `design.spec`
- VISUAL_DESIGN: `visual.inventory`, `visual.plan`
- BUILD: `build.decompose` (alias `build.plan`), `build.implement`, `build.verify`,
`build.integrate`
- REVIEW: `review.demo`, `review.collect`, `review.triage`, `review.deployment_choice`
- DEPLOY_DESIGN: `deploy.design`, `deploy.interview`, `deploy.synthesize`,
`deploy.spec` (alias `deploy.accept`)
- DEPLOY_EXECUTE: `deploy.execute` (alias `deploy.graph`)
- DEPLOY_REVIEW: `deploy.demo`, `deploy.triage`, `deploy.route`
`docs/plans/phase-protocols.md` also names kinds that are not registered
(`media.generate.*`, `visual.review`, `deploy.discover`/`plan`/`validate`/`apply`/
`release`/`verify`/`recover`/`collect`); treat those as planned. Handoff is not a
job kind: it runs inside the engine-driven job through `application/wind_down.py`
and `application/handoff_orchestration.py`.
**Job lifecycle (`domain/job.py::JobState`):**
`ready → leased → succeeded | failed | awaiting_human | awaiting_capacity | cancelled`.
**Failure classes (`domain/job.py::FailureClass`):** `capacity` (opens the
circuit), `engine` (opens after 3), `work` (the code is wrong; circuit untouched),
`vibey` (our bug; circuit untouched).
**Bounded ladders park, they do not loop.** When `build.implement` or
`build.verify` exhausts its effort ladder (or a spend cap), the job parks behind a
human gate. The answer can grant a bound (`max_attempts` for implement attempts,
`max_rounds` for verify repair rounds, `max_dollars`/`max_turns` for spend);
further attempts run at the ladder's top effort until that grant is used up, then
park again (ADR-0024; `granted_limit`/`granted_amount` in
`application/build_verify_handler.py`).
**Idempotency:** every job is idempotent under replay via `idempotency_key` —
a deterministic hash of `(project_id, cycle, kind, subject)`. Re-enqueueing
the same logical work is a no-op. Handlers also guard their own side effects.
**Parallel vs serial:** some jobs run in parallel (e.g., `build.implement` —
each work item in its own git worktree). Others are serial (e.g.,
`build.integrate` — one integration branch, serialised per project and cycle by a
Postgres advisory lock in `infrastructure/db/advisory_lock.py`, ADR-0029).
See `domain/job.py` (`JobState`, `FailureClass`, `backoff()`,
`idempotency_key()`) and the handlers in `application/*_handler.py`.
## Ledger
The **append-only event log** in Postgres, written in a vendor-neutral schema.
Vendor transcripts (`~/.claude/projects/**/*.jsonl`, codex rollouts) are
copied in as *attachments* referenced by events — they are
evidence, not state.
**Event kinds (26 total, `domain/ledger.py::EventKind`):** `SessionSeeded`,
`TurnRequested`, `TurnCompleted`, `ToolInvoked`, `TranscriptRecorded`, `FileEdited`,
`VerdictRendered`,
`CapacityRejected`, `QuestionAsked`, `AnswerGiven`, `DecisionRecorded`,
`AssumptionStated`, `FindingRaised`, `FindingResolved`, `ArtifactProduced`,
`SavePointCreated`, `HandoffInitiated`, `HandoffAccepted`, `PhaseTransitioned`,
`BudgetSpent`, and the ADR-0014 opt-in gate events `VisualDesignOptedIn`,
`VisualDesignDeclined`, `VisualDesignAccepted`, `VisualDesignWaived`,
`DeploymentOptedIn`, `DeploymentDeclined`.
**Readers are forward compatible, writers strict (vibey#275):** a stored kind this vibey does not know is read as `UnrecognizedEventKind` (so `LedgerEvent.kind` is `EventKind | UnrecognizedEventKind`), kept in every range, full ledger and digest, skipped by every projection, and never written. Match kinds with `is EventKind.X` and narrow with `isinstance(kind, EventKind)` before using a member-only attribute.
**Closable events:** `QuestionAsked`, `DecisionRecorded`, `AssumptionStated`,
`FindingRaised` — these are the events the no-loss gate checks.
**Digest:** every event carries a `digest` (SHA-256 of canonical JSON payload).
`digest_range()` computes an order-sensitive Merkle-ish fold of a sequence of
events — this is what R6 (range integrity) checks.
See `domain/ledger.py` and `docs/plans/handoff-protocol.md`.
## Handoff and the no-loss gate
When an engine hits `CreditsExhausted`, vibey:
1. Produces a `HandoffBrief` (the outgoing engine writes it, or the incoming
engine synthesizes it if the outgoing is dead).
2. Verifies it against the **no-loss gate** (`domain/noloss.py::verify()`).
3. If the gate passes, writes the full ledger to
`<worktree>/.vibey/handoff/ledger.jsonl` and seeds the next engine.
4. If the gate fails, regenerates (up to 3 attempts), then escalates to
`full_transcript` mode, then raises a human gate. In that mode the gate waives
R1–R5, R7, R9; the design inlines the whole range, but as built the range is only
the `ledger.jsonl` file named in the seed prompt — do not describe it as inlined.
**The 10 rules (R1–R10):**
- R1: remaining-work closure
- R2: open-question closure
- R3: decision closure
- R4: assumption closure
- R5: finding closure
- R6: range integrity (digest match)
- R7: artifact closure
- R8: budget carry
- R9: constraint closure
- R10: containment (nothing in the brief contradicts the spec)
The gate is **pure and deterministic** — no model call. This is the reason
rotation is safe.
See ADR-0004 (no-loss gate on handoff) and `domain/noloss.py`.
## Rotation
**Smooth weighted round robin** (nginx-style SWRR), not naive modulo. Engines
are selected from the eligible set via `domain/rotation.py::select()`.
**Eligibility:** installed, authenticated (`doctor` passed within TTL),
circuit not `open`, capability requirements met, and per-phase allow-list
permits it.
**When rotation happens:** at boundaries only — never mid-turn. Triggers:
new work item starts, capacity rejection (forced, exclude the rejecting
engine), effort escalation, phase transition, operator-requested handoff.
**Properties (property-tested):**
- No starvation: over `sum(effective_weight)` consecutive selections, every
candidate with `effective_weight > 0` is selected at least once.
- Weight fidelity: selection counts converge to weight ratios within ±1 over
any full period.
- Smoothness: no candidate is selected twice consecutively while another with
`effective_weight > 0` has gone unselected longer.
- Determinism: identical candidate state produces an identical selection.
**Wiring:** `application/engine_selector.py::EngineSelector` calls `select()`
for engine-driven BUILD jobs, through `SelectingEngineProvider` in `bootstrap.py`.
Candidates come from `engine_health` rows, so an engine without a recorded
`vibey doctor --conformance --record` pass is never eligible. A local engine joins
only while its switch is on — gptossloop unless switched off, qwenloop and
claudeloop-local only when switched on — and is preferred first (ADR-0038,
ADR-0064).
See ADR-0005 (smooth weighted round robin), ADR-0007 (rotate at boundaries),
and `domain/rotation.py`.
## Effort ladder
Vibey speaks a normalized 5-level ladder: `TRIVIAL, LOW, STANDARD, HIGH, MAX`.
Each engine descriptor provides a **projection** onto native flags, which may
saturate.
**Phase base effort:**
- ① DESIGN: `HIGH`
- VISUAL_DESIGN: `HIGH`
- ② BUILD: `LOW` (auto-escalating to `STANDARD` → `HIGH` after failures)
- ③ REVIEW: `HIGH`
- ④ DEPLOY_DESIGN: `HIGH`
- ⑤ DEPLOY_EXECUTE: `LOW` (auto-escalating)
- ⑥ DEPLOY_REVIEW: `HIGH`
`Phase` also carries `DEPLOY` (a legacy single-phase bridge, base `LOW`) and
`ABANDONED`; the phase diagram omits them deliberately.
See ADR-0006 (normalized effort ladder) and `domain/effort.py`.
## Capacity states
`Available | WindowExhausted | CreditsExhausted | AuthenticationFailed`
**The one rule that matters:**
`CreditsExhausted` has **no `resets_at` field** and can never acquire one.
This is enforced at three independent layers:
1. Type-level: `domain/capacity.py`'s dataclass has no such attribute.
2. Property-tested: `schedule_probe(CreditsExhausted(...))` can only return a
`BackoffProbe`, never a `DeadlineProbe`.
3. Database-level: the `engine_health` table has a CHECK constraint
`credits_never_have_a_deadline` that rejects any `INSERT`/`UPDATE` with
`capacity_state = 'CreditsExhausted'` and non-null `resets_at`.
See `domain/capacity.py`, `domain/circuit.py`, the constraint in
`migrations/0007_engine_health_rotation.sql`, and the CLAUDE.md non-negotiable "A
capacity rejection always outranks a completion claim" (it has no ADR of its
own).