CLI reference

Every vibey command and subcommand, written by hand against src/vibey/cli/main.py and checked against it as of 2026-09-29. Nothing generates this page. If it and the code disagree, the code wins. vibey <command> --help prints the code's own help text, which is shorter than this page and in places less complete (for example, vibey work --help does not say that qwenloop is still accepted as the old name of gptossloop).

Top-level commands, in vibey --help order: new, serve, sabbath, projects, gates, answer, work, watch, recover, status, engines, loops, cost, install, doctor, migrate, operator, worker, and the command groups design, visual, deploy, ledger, queue, budget, ultra, driver, supervisor, state, hub. Bare vibey, and each bare command group, prints help.

Commands that read or write project state need VIBEY_PG_URL (see Environment variables). doctor needs it only with --record or --cluster.

vibey budget is a command group too, and the one whose bare form does not print help: bare vibey budget shows the latest project's budget.

Global options

These apply to every command; they must come before the subcommand name.

Option Default What it does
--version — Print vibey <version> and exit.
--verbose / -v (repeatable) 0 -v debug, -vv also third-party libraries, -vvv full payloads.
--quiet / -q off Warnings and errors only.
--log-level LEVEL unset DEBUG, INFO, WARNING, ERROR, or CRITICAL (case-insensitive). Overrides the level -v or -q would pick.
--log-file PATH unset Also write redacted JSON lines to this file.
--workflows / -w off Run the command on this repository's GitHub-hosted runners instead of here (below).
--install-completion — Install shell completion for the current shell.
--show-completion — Print the completion script for the current shell.
--help — Show help and exit.

-v and -q are mutually exclusive: together they print Error: --quiet and --verbose are mutually exclusive and exit 2. An unknown --log-level exits 2 with Error: invalid log level .... --log-level fixes the level, but the -v count still widens scope, so --log-level WARNING -vvv means warnings from third-party libraries too, with payloads.

Running a command on the workflows

vibey -w <command> (or --workflows) sends the command line, without the flag, to .github/workflows/vibey-remote.yml, which runs it with vibey on a GitHub-hosted runner and hands back what it printed and its exit code (ADR-0085). vibey -w prints them as the command would have (stdout to stdout, stderr to stderr), says where it ran on stderr, and exits with the command's own code: vibey -w status --json | jq . reads exactly as vibey status --json | jq ..

The flag is read only among the global options before the command: vibey status -w passes -w to status. It needs the GitHub CLI (gh), logged in, with actions: write on the repository. The runner starts from an empty, freshly migrated PostgreSQL unless a private repository declares VIBEY_WORKFLOWS_PG_URL as a secret (a public one never uses it: its runs can be read by others), and holds no engine key or model, so a command that needs either fails there as it would on a host without them. Its exit codes beyond the command's own: 2 for a command line that cannot be sent (a nested -w, a NUL, more than 4,000 characters) or invalid VIBEY_WORKFLOWS_* settings; 1 when GitHub refused the dispatch or the run ended without a report; 124 when the wait ran out, with the run's address so it can be followed. Every krypton interface reaches the same path through the hub's /api/v1/workflows/runs routes, which need the workflows scope and the scope of what the command does (view for status, answer and spend for answer, every scope for a command the policy does not name), and refuse vibey migrate and the vibey budget commands that change caps. A device reads back only the runs it started: the hub binds each request id it mints to the device that asked, and answers any other id 404, as it answers an unknown one. The host reads any run.

Exit codes and errors

Code Meaning
0 Success. Also a guarded command whose reader closed the pipe early.
1 Nothing to act on, or a check failed: no project exists (no projects found; create one with `vibey new` first); an explicit PROJECT_ID is unknown in watch, cost, gates (said on stderr), abandon, or deploy *; recover without --project or --all; doctor --engine with an unknown name; doctor --conformance with a failing engine; doctor --install-postgres or install --postgres could not install/start a supported server; doctor --cluster with a failing check; operator without the operator extra; worker --azure az without a logged-in Azure CLI.
2 Usage error: a bad global flag (see above); typer's own validation (missing argument, malformed UUID, a value outside an option's minimum or maximum, unknown option); install without --postgres; doctor --install-postgres with --cluster; new --skills-context-mode outside off/shadow/inject; answer mode conflicts or a --raw value that is not a JSON object; abandon without --reason, or with a --reason or --by that cannot be recorded; worker with an unknown --engines id, an --engines list matching none of the worker's engines, an unknown --provider, or an unknown --azure value; doctor --cluster with an unknown --engines id or --provider; doctor --engines or --provider without --cluster; doctor --record whose target project declares a forbidden engine_environment entry; new whose vibey.toml declares a malformed or forbidden [gates] or [engine_environment] entry.
3 Blocked by a domain rule, in a guarded command. Prints Error: <message> on stderr, plus a next-step hint for some error types.
130 Interrupted with Ctrl-C, in a guarded command (prints Interrupted.).
78 A supervisor setting no service could run with (EX_CONFIG): supervisor install or supervisor status with an unreadable [supervisor] table, a path on volatile storage or inside a linked worktree, vibey not on PATH, or no delivery bridge under --repo; supervisor exec with a missing or malformed environment file.
75 Resting for the Sabbath (sub-doctrine 8.i, ADR-0072): new or work declined, said why and when it resumes, and changed nothing. Paused, not failed.

Guarded and unguarded commands

Fourteen commands run inside vibey.cli.errors.guard(): new, projects, gates, design resume, design accept, work, loops, visual accept, visual waive, queue bump, queue unbump, queue list, queue reap, and worker. For these, any VibeyError — an unknown project or provider, a wrong phase, an invalid spec, no eligible engine, a rejected handoff, an unset VIBEY_PG_URL — becomes the one-line Error: message and exit 3. Ctrl-C exits 130 and a closed pipe exits 0. Exceptions that are not VibeyError keep their Python traceback.

budget, budget show, budget set and budget clear run inside guard() as well, and so does abandon.

The other commands (answer, watch, recover, status, engines, cost, ledger show, every deploy subcommand, doctor, operator) are not guarded. Their own checks exit 1 or 2 as listed above; any other error, including an unset VIBEY_PG_URL, surfaces as a Python traceback.

Known gaps in unknown-project handling:

  • status <unknown-id> raises ValueError: unknown project ... as a traceback.
  • engines <unknown-id> prints no engines recorded for project and exits 0.
  • ledger show <unknown-id> prints nothing and exits 0.

vibey install

Install local dependencies that vibey can manage explicitly.

Option Default What it does
--postgres off Install and start the current stable PostgreSQL major using Homebrew, apt, or dnf.

The installer targets PostgreSQL 18 today. The application floor is PostgreSQL 14, and the same SQL/runtime contract is tested against 14, 15, 16, 17, and 18 in CI. Installation does not invent or persist a database DSN: set VIBEY_PG_URL to a database you own after the server is ready.

Next-step hints

guard() appends a hint for these error types. Every command a hint names exists, and a test asserts it (tests/cli/test_errors_and_logging.py).

Error Hint printed Notes
NoEligibleEngine Every engine is excluded, circuit-open, or missing a capability; run vibey engines or vibey doctor.
BudgetExceeded A tripped cap parks a budget_exhausted gate; raise it with vibey answer GATE_ID --raw '{"max_dollars": 25}' (or "max_turns"), and vibey cost shows where the spend went. Ends by naming vibey gates, which lists the gate. Nothing in src/vibey raises this error today, and no runtime code reads [budget] from vibey.toml — which is why the hint names neither.
EscalationExhausted The work item failed at every rung of the effort ladder and parked a human gate; vibey answer GATE_ID it. Ends by naming vibey gates.
HandoffRejected The no-loss gate refused the handoff and parked a human gate whose prompt says what could not be carried over; vibey answer GATE_ID it. Ends by naming vibey gates.
IllegalTransitionError The project is not in a phase this command applies to; vibey status shows the phase.
InvalidSpecError Run vibey design to finish the spec before building.
InvalidPhaseError Likely a bug in vibey rather than the project.
GateAlreadyAnswered A gate is answered once and the first answer stands; the hint names vibey ledger search --kind GateAnswered and says a --request-id makes a retry of the same answer a no-op.
UnknownGate No gate has that id; the hint names vibey gates.
CheckoutHeld Nothing was created; vibey abandon PROJECT_ID --reason TEXT lets the checkout go if the holding project should stop, otherwise start the new one elsewhere with --repo; vibey projects --json lists every project with its checkout and phase. Raised by vibey new when a live project holds the checkout.

vibey new NAME

Create a project and enqueue its first DESIGN interview.

Option Default What it does
--repo PATH . Repository the project builds against.
--intake TEXT unset Initial issue or task text to seed the DESIGN ledger with. Before the DESIGN job is enqueued it is appended as one TranscriptRecorded event with untrusted provenance and the payload {"text": TEXT, "source": "github-issue"}, so the interview starts from it. The triaged-delivery bridge (scripts/triaged_delivery.py) hands a GitHub issue over this way, and only an issue whose author, editors and triage-label curators are trusted, quoted through PromptShield (runbook).
--max-cycles N 10 Cap on delivery cycles before the project stops (min 1).
--max-cycle-dollars F unset Per-cycle dollar cap (min 0.01), stored as max_cycle_dollars in project config. Tripping it parks a budget_exhausted gate instead of starting more sessions (ADR-0024). Unset means no dollar cap.
--max-cycle-turns N unset Per-cycle engine-turn cap (min 1), stored as max_cycle_turns.
--skills-context-mode {off,shadow,inject} off vibey-skills retrieval mode (ADR-0031) — see Configuration reference. Any other value exits 2.
--skills-context-budget N 6000 Token budget for skills retrieval (1,000–32,000). Stored only when the mode is not off.
--design-default-scope {narrowest,model} unset Whose appetite a DESIGN question's default follows — see [design.interview]. Stored as design.interview.default_scope, over whatever vibey.toml declares; unset keeps the file's value, and with neither the default is narrowest. Any other value exits 2. The triaged-delivery bridge always passes narrowest.
--accept-spec-json FILE unset Skip DESIGN: the reviewed spec JSON (the file vibey design accept --spec-json takes) is the design. The project is created, moved into DESIGN with no interview job, the spec saved and accepted at once (visual design declined; the ledger names new-with-spec), and BUILD's decomposition enqueued. No interview, no research and no spec synthesis, so nothing waits for a person or a model. A spec that cannot be read is a usage error before any project exists; one the acceptance refuses leaves the project in DESIGN with the spec saved, so vibey design accept PROJECT --spec-json FILE can finish it. Prints design skipped; entered build where the interview prints its job.

Prints project <id> and design job <id>. The project id is the PROJECT_ID the other commands take; vibey projects prints it again later.

One checkout holds at most one live project. --repo is stored resolved to an absolute path, and a checkout a project in any phase but abandoned holds -- done included -- is refused: Error: checkout <path> is held by live project <id> (phase <phase>); two live projects never share a checkout, so no project was created, with a hint naming vibey abandon, and exit 3. Nothing is created. A checkout whose every earlier project was abandoned is free: the new project is created beside them, and they stay readable by id.

vibey serve

The hub (ADR-0068): the HTTP API every krypton client reaches, built on vibey_bootstrap. Install it with pip install 'vibey-engine[hub]'. It serves until stopped.

Option Default What it does
--host ADDRESS 127.0.0.1 Address to listen on. Any address that is not loopback needs [hub] lan = true in vibey.toml; without it the command prints why and exits 2 before opening anything.
--port PORT [hub] port, else 8765 Port to listen on.
--openapi off Print the OpenAPI 3.1 document and exit. Needs no database. The committed copy is hub-api.json.

At start it prints the API's address and where the host token is. Every route is under /api/v1 and needs Authorization: Bearer <token>, where the token is the content of <state_dir>/token (owner-only; state_dir defaults to the platform's state directory, e.g. ~/Library/Application Support/vibey/hub on macOS). A request whose Host is not one the hub answers is refused with 421 (the DNS-rebinding defence), and no response ever carries a CORS header. The routes, scopes and refusals are in the hub API reference.

While it runs it keeps <state_dir>/serving.json (address, port, process id, and whether it serves TLS), which vibey doctor's hub-exposure line and vibey hub read.

With [hub] lan = true the hub serves its own self-signed certificate (made on first run in <state_dir>, owner-only), prints its SHA-256 fingerprint, and advertises itself on the LAN as _vibey._tcp (mDNS/DNS-SD) with that fingerprint. On loopback it serves plain HTTP and announces nothing.

vibey hub

The host's side of pairing devices with the running hub (ADR-0071). Each subcommand reads <state_dir>/serving.json, presents the host token, and -- when the hub serves TLS -- trusts only the hub's own certificate. With no hub running they exit 3.

Command What it does
vibey hub pair --scope SCOPE [--scope SCOPE ...] Offers a pairing of the named scopes (view, answer, spend, run, bump; at least one) and shows a QR code, the 6-digit code (valid two minutes, once), the pairing URI and the certificate fingerprint.
vibey hub devices Lists the paired devices: id, name, scopes. Never a key.
vibey hub revoke DEVICE_ID Revokes a device. It is refused from its very next request.

vibey sabbath

Print the Sabbath window on this host (sub-doctrine 8.i, ADR-0072): whether it is enabled, the zone, where the location came from and how accurate it is, and when the current or next rest ends. It only reads, and is never held. From sundown Friday to sundown Saturday vibey new and vibey work decline with exit code 75, the worker claims no lease, and vibey doctor reports the location source (FAIL when no source could place the host). --ignore-sabbath on vibey new, vibey work and vibey worker runs that one command through the window and says so on stderr; no file can supply it. See [sabbath] and the guide.

vibey projects

List every project, newest first (by creation time, then id), with its phase, its cycle, and how many gates are waiting for your answer.

Option Default What it does
--json off Print a JSON array instead of the table.

The table has one row per project — NAME, PHASE, CYCLE (cycle/max_cycles), OPEN GATES, CREATED (UTC), PROJECT ID — then a line counting them that, when any gate is open, points at vibey gates. PHASE is the phase's name, such as BUILD. A phase a newer vibey wrote, which this one has no name for (vibey#287), is shown as its stored text; it never fails the listing.

--json prints an array, newest first, of one object per project with exactly these keys:

[
  {
    "project_id": "0b5c9a4e-1d4c-4c47-9a2a-3c1d2b8f9e10",
    "name": "greeter",
    "phase": "BUILD",
    "cycle": 1,
    "max_cycles": 3,
    "repo_path": "/Users/me/src/greeter",
    "created_at": "2026-09-24T09:00:00.123456+00:00",
    "open_gates": 2
  }
]

phase follows the table's rule, created_at is ISO-8601 with its offset, and open_gates counts the project's unanswered gates. With no projects both forms exit 0: the table form prints no projects yet; create one with `vibey new <name> --repo <path>` and --json prints []. An empty list is an answer, not an error — unlike vibey status, which needs a project to report on and exits 1 without one.

vibey gates [PROJECT_ID]

List every open gate — a job parked until a person answers it (ADR-0009) — oldest first (by raise time, then gate id), across every project or only PROJECT_ID's. Each gate shows its project's name, its kind, its prompt as one wrapped paragraph, and the exact command that answers it:

1 open gate, oldest first -- each is a job waiting for your answer:

1. greeter: approval gate, raised 2026-09-24 12:03 UTC
   Review artifacts ready for cycle 1. Accept, request changes, or ask
   questions.
   answer with: vibey answer 5e1f0c7a-8b2d-4e5f-9a61-2c3d4e5f6a7b --verdict accept
Argument / option Default What it does
PROJECT_ID every project Only this project's open gates. An unknown id exits 1 with unknown project <id> on stderr and nothing on stdout.
--json off Print {"gates": [...]} instead.
--remind off Send the gate notices now due instead of listing: see Reminding about waiting gates.
--dry-run off With --remind: list the notices that are due and send and record nothing. Without --remind it exits 2 (--dry-run applies only with --remind).

--json prints one object whose gates array holds, oldest first, one object per gate with exactly these keys:

{
  "gates": [
    {
      "gate_id": "5e1f0c7a-8b2d-4e5f-9a61-2c3d4e5f6a7b",
      "project_id": "0b5c9a4e-1d4c-4c47-9a2a-3c1d2b8f9e10",
      "project_name": "greeter",
      "job_id": "9d2a7b1c-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
      "kind": "approval",
      "prompt": "Review artifacts ready for cycle 1. Accept, request changes, or ask questions.",
      "options": ["accept", "changes", "cancel"],
      "default_answer": null,
      "raised_at": "2026-09-24T12:03:04.567890+00:00",
      "timeout_at": null,
      "answer_with": "vibey answer 5e1f0c7a-8b2d-4e5f-9a61-2c3d4e5f6a7b --verdict accept"
    }
  ]
}

job_id, default_answer and timeout_at are null when the gate has none; prompt is the gate's own text, line breaks included. With nothing waiting, both forms exit 0: the text form prints no open gates: nothing is waiting for your answer and --json prints {"gates": []}.

Reminding about waiting gates

vibey gates --remind runs one gate-reminder sweep -- the one every idle vibey worker runs for its project at most every [notifications] sweep_interval_seconds -- so a supervisor can schedule it (cron, a systemd timer, a Kubernetes CronJob) where no worker idles. Each open gate gets at most one notice: its raise notice when none is on record, else the reminder its project's schedule says is now due ([notifications]). Every notice is recorded on the ledger as GateNotified or GateNoticeUndeliverable, once per gate and notice number, so running it twice sends nothing twice. A project whose notifications are off is said once per gate -- UNDELIVERABLE (disabled) -- and not reminded about.

2 open gates; 2 notices sent
  told    reminder 1 gate 5e1f0c7a-8b2d-4e5f-9a61-2c3d4e5f6a7b (approval)
  UNDELIVERABLE (disabled) raise notice gate 9d2a7b1c-3e4f-4a5b-8c6d-7e8f9a0b1c2d (defect)
          [notifications] enabled is false, so nobody will be told this gate is waiting; enable a channel, or watch `vibey gates`

--json prints {"project_id", "dry_run", "waiting", "sent": [...], "planned": [...], "unreadable": [...], "ok"}; each sent entry is the recorded payload (gate_id, gate_kind, job_id, notice, and channels or reason and detail) with project_id and delivered. It exits 1 when a project's notices on record could not be read: nothing was sent for that project, and it is named under unreadable.

How each kind of gate is answered

answer_with is one line a shell runs as printed: every word is quoted as the shell needs it. Its form depends on the gate's kind, declared once in src/vibey/cli/gate_answers.py; a test fails when a gate kind is raised anywhere in src/vibey without an entry there.

Kind answer_with (ID is the gate id) Raised by
question vibey answer ID --defaults The DESIGN interview. Accepts every question's default -- under the default narrowest scope, a question proposing an artefact the intake does not name defaults to "No".
approval vibey answer ID --verdict accept REVIEW. Its other options are changes and cancel.
deploy_demo_review vibey answer ID --verdict approve The Phase ⑥ demo. Or request_changes.
choice vibey answer ID --choice local_only The deployment opt-in after REVIEW. deploy opts in.
deploy_interview vibey answer ID --choice accept_defaults The Phase ④ interview.
deploy_acceptance vibey answer ID --choice reject Phase ④ spec consent; see below.
deploy_failure_triage vibey answer ID --choice LOOP_DEPLOY_DESIGN The Phase ⑥ triage. Or RETRY_DEPLOY_EXECUTE, ABORT_DEPLOYMENT.
bus_dead_lettered vibey answer ID --choice replay, or dismiss when the message cannot be replayed A dead-lettered bus message.
budget_exhausted vibey answer ID --raw '{"max_dollars": N}' The budget brake.
escalation_exhausted, attempts_exhausted vibey answer ID --raw '{"max_attempts": N}' A spent effort ladder; a job out of attempts whose last failures were not all alike.
publish_interview vibey answer ID --choice skip, --choice accept_defaults, or --raw '{"choice": "custom", ...}' The last step of a delivered project, with [publish] mode = "interview": its prompt shows exactly what would be pushed and where. accept_defaults publishes that; skip finishes locally and is the declared default, so nothing is published by silence; custom changes only the fields you name (title, body, base, draft, visibility, repo, owner, description). It never times out to a default.
defect vibey answer ID --choice requeue, or --choice abandon A job out of attempts whose last [queue.defect] failures were all one failure: no more attempts are offered. requeue runs it again once a fix has landed (a same-failure run parks here again at once); abandon cancels it.
verify_repair_exhausted, integrate_repair_exhausted vibey answer ID --raw '{"max_rounds": N}' BUILD's verify and integrate repair loops.
delivery_exhausted, research_evidence, engine_misconfigured vibey answer ID --raw '{}' Any answer retries, once the cause outside vibey is fixed.
foreign_branch vibey answer ID --raw '{}' BUILD found a branch it cannot prove this project created and touched nothing. Check whose it is, move it aside (git branch -m), then answer: the retry cuts a fresh branch (ADR-0008).
handoff_gate_failed, too_many_wind_downs, and any kind not listed vibey answer ID --raw '<json>' Nothing reads a particular answer; you write it.

A verdict or choice uses the gate's declared default, else its first option; options in --json lists the rest. N and <json> are placeholders, and the text form says what to put there. Neither is valid JSON, so a command pasted without filling it in is refused by vibey answer (exit 2), never sent. How much more money or how many more tries to grant is a person's decision, so no number is suggested; the gate's prompt usually proposes one.

deploy_acceptance prints its declared default, reject. Accepting the deployment spec also takes explicit consent to change real infrastructure, which no flag sends and answer_with never suggests: vibey answer ID --raw '{"verdict": "accept", "explicit_mutation_authorized": true}'.

vibey answer GATE_ID [QUESTION_ID=ANSWER ...]

Answer a parked human gate. Exactly one of the following modes is required (or --defaults alone for interview gates):

Form Used for
QUESTION_ID=ANSWER [QUESTION_ID=ANSWER ...] Interview gates, answered per question. Combine with --defaults to fill in every other question's default; the explicit pairs win.
--defaults Interview gates: accept every question's default. Question keys are model-minted per run, so this needs none.
--choice VALUE Deployment and triage gates: sends {"choice": VALUE}.
--verdict VALUE Review gates: sends {"verdict": VALUE} — accept, changes, cancel, approve, or request_changes.
--raw JSON Any other gate shape, e.g. raising a tripped budget cap: --raw '{"max_dollars": 25}' or --raw '{"max_turns": 50}'.
Option What it does
--by NAME the name the answer is recorded under, for a tool that runs the command (the VS Code extension says vibey-vscode). Defaults to the account running the command. It is a label for the record, not a permission: the account is always recorded beside it.
--request-id ID names this request so a retry is safe. The same id with the same answer is a no-op once it has landed. Without one, every run is a new request.

vibey gates prints, beside each open gate, the form that answers it.

Prints answered <gate_id> as <name>. The answer is written in a transaction that sets answered_at only if it was NULL; of two answers racing for one gate exactly one lands, the first answer stands, and the answer and its GateAnswered ledger event are written together. If the same request-id is used again with the same answer it prints already answered <gate_id> by this request; nothing changed and exits 0. The command exits 2 with a one-line message when --defaults is combined with --choice, --verdict, or --raw; when no mode is given or more than one is given; or when --raw is not valid JSON or not a JSON object. Exit 3 produces Error: … and a hint, never a traceback: the gate was already answered (GateAlreadyAnswered); the same --request-id was used with a different answer (also GateAlreadyAnswered); no gate exists (UnknownGate); the --by label is invalid (InvalidActorLabel); or the --request-id is invalid (InvalidAnswer). A positional argument without = raises InvalidAnswer before connection (this is still exit 3).

Finding a gate id

vibey gates lists every open gate with its id, its prompt, and the vibey answer command that answers it; vibey gates PROJECT_ID lists one project's. The worker also sends NOTIFY vibey_gate_raised with the gate id when one is raised, for a program that would rather listen than poll.

vibey abandon PROJECT_ID --reason TEXT

Abandon a project that is not going to finish -- built on the wrong spec, superseded, no longer wanted, or stuck in intake because its dispatch never reached DESIGN. Any phase short of done can be abandoned. Nothing in vibey abandons a project on its own; until a person does, anything waiting on it waits (the triaged-delivery bridge holds its one delivery slot for a dispatched project until it is done or abandoned).

Option Default What it does
--reason TEXT required Why. Recorded on the project's ledger with the move. Visible text, up to 2000 characters.
--by NAME the account running it The name the abandonment is recorded under, for a tool that runs the command. A label for the record, not a permission: the account is always recorded beside it.
--dry-run off List the jobs that would be cancelled and the gates that would be withdrawn; write nothing. It checks what the real run checks, so a refusal is the same refusal.
--json off Print JSON instead (below).

In one transaction, under a lock on the project's row:

  • every open human gate of the project is withdrawn: the gate row records {"withdrawn": true, "reason": "project abandoned"} as its answer, the name above as answered_by and abandon:<project_id> as its request id, so a later vibey answer to it is refused as a second answer. No row is deleted;
  • every job that could still run -- ready, leased, awaiting_human, awaiting_capacity -- is cancelled, its lease cleared, so a worker still running one finds its lease gone and nothing it reports revives the job. Succeeded, failed and cancelled jobs are left as they are;
  • the project moves into abandoned through the phase machine's own guard, and its ledger gets one PhaseTransitioned (guard: "operator abandoned", with reason, by, account, cancelled_jobs and withdrawn_gates) and one GateWithdrawn per withdrawn gate.

A failure anywhere leaves all of it as it was. The claim never hands out a job of an abandoned project, so a follow-up job a still-running handler enqueues after the abandonment never runs.

Abandoning lets the project's checkout go: vibey new can start a fresh project with the same --repo, beside the abandoned one, which stays readable by id. Nothing on disk is removed. The files the abandoned project wrote under the checkout's .vibey/ (runs/<cycle>/..., context/, deploy/) stay where they are, and the new project's own writes replace them cycle by cycle; they are not keyed by project, so start the new project on a clean checkout when the old files must not be read.

Abandoned greeter (9692abab-...): build -> abandoned, cycle 1.
  by adam (account adam)
  reason: built on a foreign spec
  cancelled 2 jobs:
    6f1c... build.implement (was ready)
    8a07... build.verify (was awaiting_human)
  withdrew 1 gate:
    c3d9... defect

With --dry-run the block starts Dry run: nothing was written. and says Would abandon, would cancel and would withdraw. --json prints one object: project_id, name, from, to, cycle, dry_run, already_abandoned, written, reason, by, account, cancelled_jobs (job_id, kind, state as it was) and withdrawn_gates (gate_id, kind, job_id).

Abandoning a project that is already abandoned changes nothing, prints <name> (<id>) is already abandoned; nothing was changed. and exits 0. A project that is done is refused (exit 3, Error: the project is done ...): it finished, which is a different ending. So is one in a phase this vibey does not know. An unknown PROJECT_ID exits 1; a missing, empty or control-bearing --reason, or an unusable --by, exits 2 before the database is opened.

vibey work PROJECT_ID

Process one ready DESIGN job, or, when the project is in VISUAL_DESIGN, one visual-inventory job. Live engine use is explicit and capped. Prints processed one job or no ready job.

Option Default What it does
--provider {scripted,claudeloop,gptossloop} gptossloop gptossloop runs the sovereign local DESIGN provider on Ollama (ADR-0015, ADR-0027, ADR-0064) and reads research material from $VIBEY_EVIDENCE_DIR; with that unset, research refuses rather than inventing a source, and DESIGN stops there. claudeloop runs a real, paid session capped by --max-turns and --max-dollars; its spend is recorded as budget_spent ledger events so the budget brake counts it. scripted needs no live engine. qwenloop is still accepted: it is read as gptossloop, with --provider qwenloop is now --provider gptossloop (ADR-0064) ... on stderr. Any other value exits 3 with Error: provider must be 'scripted', 'claudeloop', or 'gptossloop'.
--max-turns N 1 Turn cap for this one job (min 1).
--max-dollars F 0.25 Dollar cap for this one job (0.01–10).
--ollama-model NAME $VIBEY_OLLAMA_MODEL, else gpt-oss:20b Local model for --provider gptossloop; ignored by the other providers. The server is $VIBEY_OLLAMA_URL.

In VISUAL_DESIGN only --provider scripted works; any other provider exits 3 with Error: no live VisualInventoryProducer is implemented yet; use --provider scripted. An unknown PROJECT_ID exits 3.

vibey design

Bare vibey design prints help. Subcommands:

Subcommand What it does
design resume PROJECT_ID [--priority] Enqueue or resume the project's DESIGN interview. Prints design job <id>. --priority enqueues it bumped, so it runs next after whatever is running — the same grant and ledger record as vibey queue bump. On an interview that has already finished, --priority is a recorded no-op, as plain resume is a no-op.
design accept PROJECT_ID [--spec-json PATH] [--visual/--no-visual] Accept the synthesized spec (optionally importing JSON first) and choose whether to enter the VISUAL_DESIGN interstitial. Defaults to --no-visual; the choice is never implicit. Refused while any DESIGN job of the cycle (research, synthesis, spec) is unsettled. Prints accepted design for <id>; entered <phase>; context under <repo_path>.

--spec-json expects a JSON object with:

  • objective and walking_skeleton — required strings;
  • criteria — required list of objects with criterion_id, given, when, then, fit;
  • constraints — optional list of {"text": ..., "kind": "hard" | "soft"};
  • non_goals — optional list of strings;
  • research_gaps — optional list of {"topic": ..., "reason": ...}: the topics that were not researched. Keep them when you edit a synthesized spec, since an import replaces the whole spec and a gap you leave out is no longer stated in it;
  • nfrs — optional list of objects with nfr_id, attribute, scale, meter, must, wish (string or null), fit_criterion.

The shapes are the dataclasses in src/vibey/domain/spec.py. A missing required key or an unreadable file currently surfaces as a traceback, because guard() renders only VibeyError.

vibey visual

Bare vibey visual prints help. Subcommands:

Subcommand What it does
visual accept PROJECT_ID Accept the reviewed visual plan and enter BUILD. Prints accepted visual design for <id>; entered <phase>.
visual waive PROJECT_ID Explicitly waive the visual plan (the inventory must still be complete) and enter BUILD. Prints waived visual design for <id>; entered <phase>.

vibey watch [PROJECT_ID]

Live TUI dashboard: current phase, queue, engine circuits, active worktrees, and a ledger tail. Defaults to the most recently created project; an unknown id prints unknown project <id> and exits 1.

Option Default What it does
--replay off Replay the project's historical ledger events instead of following live state.
--speed F 1.0 Replay rate in ledger states per second while playing (--speed 4 advances four states per second). 0 or a negative value disables auto-advance. The option's --help text calls it a "multiplier"; it is a rate.

Replay starts paused. Keys: Space play/pause, Right or n next step, Left or p previous step, q quit.

vibey llm

The conversation with the local model, as it happens. It only reads.

Subcommand Options What it does
llm tail --file PATH, --all, --no-follow, --raw, --max-chars N Follows the wire log in every phase: each request DESIGN and DECOMPOSE send the model, its reasoning and answer as they stream back, and the model's own counts and timings; and, for BUILD and REVIEW, each engine session the worker started: every tool call and its result, the streamed answer, the reasoning, and each turn's time and tokens. It starts at the latest call or session and switches to each new session by itself (a new run id means a new events file). --all starts from the beginning, with every session; --raw prints the JSON lines as written.

The log is written when a command runs with -vvv, or with VIBEY_LLM_WIRE_LOG=path (see VIBEY_LLM_WIRE_LOG). Without either, nothing is written and the model is asked exactly as before. The default file is ~/.local/state/vibey/llm-wire.jsonl, readable by its owner alone. With it on, the worker also notes each engine session in the log (a session record naming that run's events.jsonl), which is how the tail knows where BUILD and REVIEW write; the records hold ids and paths only.

vibey search and vibey fetch

vibey search "QUERY" [--json] [--limit N]
vibey fetch URL [--json] [--max-chars N]

The web, for any loop that can run a command, and for the sovereign DESIGN research stage. search lists results (title, URL, snippet) from the backend you configure; fetch reads one public page as plain text. Both print under an untrusted banner: what comes back is other people's text, to be read as data and never followed as instructions. Evidence handed to a DESIGN stage is the quoted retrieved text with its URLs, never a model's summary of it.

fetch refuses a private, loopback, link-local or metadata address, re-checks every redirect hop, follows at most three, reads text types only and stops at 1 MB. A local file path in a query is removed before the query leaves the machine.

Configuration is environment only (never vibey.toml, so a key cannot be committed). A search with no backend configured exits 2 and says how to configure one.

Backend Set Reaches
SearXNG (keyless, self-hosted) VIBEY_SEARXNG_URL the DESIGN research stage and every engine session
Brave Search VIBEY_BRAVE_API_KEY the DESIGN research stage only: the key is never passed to a session

vibey worker tells every session that can run a command how to search (an appended "Web access" note in its plan, naming the absolute path of the running vibey), and passes the non-secret WEBSEARCH_* settings through to it. With only a Brave key configured, sessions are told nothing and have no search. The worker prints one line on stderr saying which of the two is in force.

gptossloop and qwenloop also have web_search and web_fetch as tools of their own, which run the same two commands; every other engine uses the command from its plan.

vibey recover

Recover stuck leased jobs (e.g. after a worker crash): every job in state leased is set back to ready, and its lease owner, lease expiry, and assigned engine are cleared.

Option Default What it does
--project ID unset Recover jobs for one project.
--all off Recover jobs for every project. Wins if --project is also given.

| --stop-workers | off | First stop every vibey worker this user is running, and everything each started (an engine runner, a uv run or caffeinate wrapper), then reset the leases. The processes are listed before anything is signalled. SIGTERM, then SIGKILL for what is still there after --grace. It matches vibey command lines with worker in them, in this user's processes only, and never itself or what started it. If anything survives SIGKILL it exits 1 without resetting a lease, because a live worker holds its leases by heartbeat and resetting them would run its job twice. | | --grace SECONDS | 10 | How long --stop-workers waits after SIGTERM before SIGKILL. | | --dry-run | off | Change nothing: with --stop-workers, list the processes that would be stopped, and print would recover N stuck job(s) instead of resetting them. |

One of --project or --all is required; with neither, it prints Must specify either --project <id> or --all and exits 1.

A worker that died leaves its jobs leased until the lease expires, and a worker that is still alive keeps them; so the one command for a stuck queue is vibey recover --project ID --stop-workers, then start the worker again.

Prints Recovered N stuck job(s)., where N is the number of rows actually reset, read from PostgreSQL's UPDATE n status tag.

vibey retry PROJECT_ID

Get a stalled project moving again in one command. A project that stalls on a bad model answer parks its job on an attempts_exhausted gate; this does what used to be recover --stop-workers, gates, answer --raw '{"max_attempts": N}' and a hand-written worker restart, in that order.

Option Default What it does
--max-attempts N 10 Attempts to grant on each of the project's attempts_exhausted gates.
--keep-workers off Do not stop this user's running workers first.
--start-worker off Then start vibey worker --project PROJECT_ID detached, with every argument after -- passed to it.
--log PATH $XDG_STATE_HOME/vibey/worker-<project>.log Where the started worker's output is appended.
--grace SECONDS 10 How long a worker gets after SIGTERM before SIGKILL.
--dry-run off Change nothing: list the workers that would be stopped, the gates that would be granted and the worker that would be started.
vibey retry 6b45e169-4653-4a8d-941c-7440249dc8ef --start-worker -- --engines gptossloop --auto-answer

Workers are stopped exactly as recover --stop-workers stops them, and if one survives SIGKILL nothing else is done (a live worker holds its leases). Only attempts_exhausted gates are answered, each as a named request (retry-<gate>-<N>, by vibey retry) so running the command twice answers a gate once. Any other open gate is a decision: it is counted and pointed at vibey gates PROJECT_ID, never answered. The started worker is its own session, so closing the terminal does not stop it, and vibey recover --stop-workers finds it by its command line. Updating vibey itself (git pull, uv sync) is not part of this.

vibey status [PROJECT_ID]

Show the project's name, phase, cycle, visual and deployment decisions, repository path, queue depth per job state, and engine circuits (circuit state, consecutive failures, cycle cost). Defaults to the most recently created project.

Option Default What it does
--json off Emit JSON instead: project_id, name, phase, cycle, max_cycles, repo_path, visual_decision, deployment_decision, queue_depth, circuits (each with engine_id, installed, version, conformance_ok, circuit, capacity_state, consecutive_fail, cost_usd_cycle, selected_count), active_worktrees, and integration_branch (the project's own integration branch for the cycle, vibey/<project8>/<cycle>/integration).

vibey engines [PROJECT_ID]

Show recorded engine health for a project as a table: engine, version, circuit-breaker state, consecutive failures, selection count, and cycle cost. Rows exist only for engines that vibey doctor --record or a worker's startup preflight has recorded; with none it prints no engines recorded for project. Defaults to the most recently created project.

vibey loops

List the family's two loops (sub-doctrine 8.c), every engine each one holds, what each effort level passes to each engine, and what each engine can do. sovereignloop is tier local, and canon 8.b makes it the default loop. A local engine in it runs only while its switch is on. paidloop is tier paid, and canon 8.b makes it declared only, with Claude through claudeloop as its default. The canon is reported as the canon: vibey's selector does not read a paid declaration yet. Today it prefers an eligible local engine and, when none is eligible, picks a paid one by weighted round robin (ADR-0038). Each engine is listed under the loop its descriptor's tier puts it in.

The command needs no database and no network. Local switches are read the way vibey doctor reads them, from the environment and then ./vibey.toml. It exits 0. It exits 3 when a setting it must read is malformed: VIBEY_OLLAMA_URL, VIBEY_OLLAMA_TIMEOUT while VIBEY_OLLAMA_URL is set, or the [engines.claudeloop_local] table in ./vibey.toml. That is the same configuration that would stop the worker. The message names the setting and never its value, because a URL can carry user:token@.

Option Default What it does
--json off Print the whole document instead of the tables.

Each loop gets a small table with one column per engine. Its rows are switched on, $ per Mtok in/out, model (the model vibey hands the engine; see default_model below), and one row per effort. An effort's row shows the flags the engine is passed, without their dashes. It is followed by -> STANDARD (or the level it really reaches) when the engine runs at a different effort. After each table come the variable that switches each local engine — gptossloop is on unless switched off by VIBEY_FEATURE_GPTOSSLOOP=0, or by its key under [features] in vibey.toml for the on-by-default one, <engine> is switched on by <VARIABLE>=1, ... for the others — and any notes.

--json prints one object:

  • efforts: the five levels, in order: TRIVIAL, LOW, STANDARD, HIGH, MAX.
  • default_loop: sovereignloop. paid_default_engine and its alias paid_default: claudeloop, the paid default canon 8.b names.
  • ladder: phase_base (each phase's starting effort, by phase name), build_attempts (BUILD's effort at attempts 1 to 6), exhausted_after (6; attempt 7 parks a human gate), and rotates_when_effort_rises (true).
  • loops: two objects, sovereignloop first. Each has loop, tier, default, declared_only (canon 8.b's rule, as above), engines, and by_effort.

Each engine object has these keys:

Key What it holds
engine_id, binary, state_dir, done_marker, plan_flag, supports_cwd_flag, base_weight, cost_per_mtok_in, cost_per_mtok_out The engine's descriptor, as declared.
enabled Whether the engine would run right now. A local engine follows its switch. An engine with no switch is true: the selector may pick it whenever it is eligible. Being listed is not being selected.
switch The variable that switches a local engine (VIBEY_FEATURE_GPTOSSLOOP, VIBEY_FEATURE_QWENLOOP, VIBEY_FEATURE_CLAUDELOOP_LOCAL), or null.
on_by_default true for a local engine that is on when neither its variable nor [features] sets its switch: gptossloop, the sovereign default (ADR-0064). false for every other engine.
repealed true for an engine canon 8.b repeals from both loops while vibey still carries its code; none today, since OpenCode's engine was deleted. Such an engine stays listed, and is left out of by_effort, so nothing that selects from by_effort picks it. false for every other engine.
default_model The model vibey hands the engine, or null. For gptossloop this follows how the model actually reaches it. It is GPTOSSLOOP_MODEL when set. Otherwise it is vibey's model (VIBEY_OLLAMA_MODEL, else gpt-oss:20b), but only while VIBEY_OLLAMA_URL is set, because that is the only path by which vibey's model reaches the session. A worker started with --ollama-model hands that model instead, which this command cannot see. Otherwise it is null, and gptossloop's own configuration chooses the model. For qwenloop it is QWENLOOP_MODEL when set, else null: vibey hands qwenloop only its endpoint, and qwenloop runs the Qwen model it names itself (qwen3:14b unless its configuration names another).
efforts All five levels, in order. Each has effort, argv (from the descriptor's projection), achieved (the level it really reaches), and model. model is the value of a --model the level passes, else default_model, else null. notes gives the descriptor's own note and, when model is null, what chooses the model instead (claudeloop preset high, qwenloop's own configuration chooses the model). qwenloop's notes also say what it became: since ADR-0064 qwenloop runs a Qwen model (qwen3:14b unless QWENLOOP_MODEL names another); the gpt-oss engine it used to be is gptossloop.
capabilities images, files, paste_text, paste_images, plugins (skills-context or claude-plugins), and mcp. Each is true, false, or null for unknown, and a menu belongs only beside a value that is not null. evidence names, for each value that is set, where the runner's own code shows it. skills-context applies when the project sets skills_context.mode = inject: vibey then appends the vibey-skills context packet to the plan. A local model's own abilities come from Ollama at run time, so an image menu needs both this loop's images and the model's vision.
run The argv template build_argv fills for a run: {binary}, run, {plan_flag?} (only when plan_flag is set: put that flag there), {plan}, --run-id, {run_id}, {effort_argv...}, and --cwd {cwd} when supports_cwd_flag. Tests compare it with build_argv for every engine at every effort. They also read it, filled in at every effort, against the runner's own run definition.
controls stop, wind_down, and prompt: argv templates after the binary, with {run_id}, {cwd} and {text} to fill in. Each is null where the runner has no such verb, or does not act on it. A test reads each template against the runner's own Typer definition.
events path ({cwd}/{state_dir}/runs/{run_id}/events.jsonl) and envelope, one of three values: type (a top-level "type"), event_type+payload, or event_type (a top-level "event_type" with no payload wrapper; no engine writes it today).
env auth and passthrough: the descriptor's auth_env and env_passthrough, names only and in declared order. The command never reads a value.
notes A list of strings: anything the canon says otherwise, such as a note that 8.b repeals an engine the code still lists. Empty for every engine today.

by_effort maps each level to every engine in the loop that is not repealed, each with its engine_id, model and achieved. Engines that reach exactly that level come first, then those that go higher, then those that fall short. Ties go to the lower output price, then to the engine id.

The document for one fixed environment is committed as tests/cli/golden/vibey-loops.json. In that environment every variable the command reads is cleared, then VIBEY_OLLAMA_URL=http://127.0.0.1:11434 is set, so gptossloop (on by default) runs vibey's model and qwenloop (off) reports its own. The test suite produces the document and fails when the command's output drifts from the file, so a parser tested against the file reads what the command prints. After an intended change, regenerate it with VIBEY_UPDATE_GOLDENS=1 uv run pytest tests/cli/test_loops_cli.py -k golden and commit it with the change.

What the runners' own code shows today:

Engine Capabilities shown stop / wind_down / prompt Envelope
claudeloop, claudeloop-local files, pasted text, claude-plugins (run --plugin), MCP (run --connector); images unknown --run-id and --cwd for each; prompt TEXT --now event_type+payload
codexloop files, pasted text, skills-context; images and MCP unknown --run-id for each and no --cwd: run it in the worktree; prompt TEXT --now type
gptossloop, qwenloop files, pasted text, skills-context; no images, no pasted images, no MCP (text messages and a fixed tool set) each takes the run id positionally, with --cwd; prompt RUN_ID TEXT, which its runner adds to the conversation at the next turn boundary type

vibey cost [PROJECT_ID]

Show the current cycle's spend against the caps the budget brake enforces, then each engine's metered BUILD spend. Defaults to the most recently created project; an unknown id prints unknown project <id> and exits 1.

  • Cycle spend: is the ledger sum the brake checks before every BUILD session: TurnCompleted cost and BudgetSpent dollars, and one turn per TurnCompleted plus BudgetSpent turns. DESIGN's spend is in it.
  • Cycle dollar cap: and Cycle turn cap: are the project's stored max_cycle_dollars and max_cycle_turns, read through the brake's own parser, or none (uncapped) and none. A reached cap adds Cap reached: the next BUILD session parks a budget_exhausted gate.
  • Per-engine (BUILD sessions, all cycles): is each engine's metered spend from engine_health, which accumulates across cycles, and how often rotation selected it. That count is not a turn count.

The spend and caps are the budget vibey budget shows. That command also changes the caps.

vibey budget [PROJECT_ID]

Show a project's per-cycle caps and this cycle's spend against them. Add, change or remove the caps after the project exists. The caps are max_cycle_dollars and max_cycle_turns in the project's stored config, the one place the budget brake reads them. The spend is the same ledger sum vibey cost and the worker use. Bare vibey budget, or vibey budget show, shows the most recently created project.

Subcommand Option Default What it does
budget [show] [PROJECT_ID] --json off Print the budget as JSON (below).
--all off Every project, newest first (by creation time, then id). With --json, an array. Not together with PROJECT_ID (exit 2).
budget set [PROJECT_ID] --max-cycle-dollars F unset Set the dollar cap: a finite number above zero.
--max-cycle-turns N unset Set the turn cap: a whole number above zero. At least one of the two is required.
--by NAME the account running it The name the change is recorded under. A tool that runs the command names itself; the VS Code extension says vibey-vscode.
budget clear [PROJECT_ID] --dollars, --turns, --all — Remove the dollar cap, the turn cap, or both. At least one is required. The project is then uncapped for it, as if it had never been set.
--by NAME the account running it As for set.

The text form is one short block per project:

greeter (0b5c9a4e-1d4c-4c47-9a2a-3c1d2b8f9e10), cycle 2
  dollars: $3.21 spent this cycle; cap $15.00
  turns:   41 spent this cycle; no cap
  last change: dollar cap none -> $15.00, by adam, 2026-09-24 19:02 UTC (1 change in all)

When a cap is reached, the block says so plainly: The dollar cap is reached: the next BUILD session will park a budget_exhausted gate.

--json prints one object with exactly these keys. With --all it prints an array of them, newest project first:

{
  "project_id": "0b5c9a4e-1d4c-4c47-9a2a-3c1d2b8f9e10",
  "name": "greeter",
  "cycle": 2,
  "caps": {"max_cycle_dollars": 15.0, "max_cycle_turns": null},
  "spend": {"dollars": 3.21, "turns": 41},
  "exhausted": false,
  "history": [
    {"at": "2026-09-24T19:02:11.482113+00:00", "by": "adam", "field": "max_cycle_dollars", "old": null, "new": 15.0}
  ]
}
  • A cap of null is no cap. The dollar cap is a number and the turn cap an integer.
  • spend.dollars is the ledger sum, not rounded. spend.turns is the brake's count: one per TurnCompleted, plus BudgetSpent turns.
  • exhausted is true when a cap is reached.
  • history lists every change budget set or budget clear recorded, oldest first. Each entry has at (ISO-8601 with its offset), by, field, old and new, where null is uncapped. A cap set at creation (by vibey new or the Kubernetes operator) is not a change, so it has no entry.

With no projects, bare vibey budget prints no projects found; create one with `vibey new` first and exits 1. --all exits 0, printing [] with --json.

Changing a cap. set and clear print what changed, then the budget block:

Changed by adam:
  dollar cap: none -> $15.00

A request that leaves every cap as it is prints Nothing changed: the caps were already as asked., and writes and records nothing, so running it again is harmless. A cap at or below this cycle's spend is allowed. The block then says the next BUILD session will park a budget_exhausted gate.

Each change writes the new caps into the project's config and appends one BudgetCapChanged event per changed cap to the ledger, in one transaction. The event carries field, old, new, by and account. vibey ledger search --kind BudgetCapChanged lists them, and vibey ledger export withholds them (What gets published). --by is a label for the record, not a permission. account is recorded beside it: the account the command ran as, from the password database, never $USER.

The brake reads the caps at every BUILD session, so a worker that is already running applies a change to its next session. No restart is needed. A job already parked on a budget_exhausted gate stays parked until the gate is answered. The command lists each such gate with the answer that resumes the job under the stored caps, vibey answer GATE_ID --raw '{}'. Answering --raw '{"max_dollars": N}' still raises the cap for that job alone, as before.

Exit codes: 0 on success, including an empty --all. 1 when there is no project or PROJECT_ID names none. 2 for a usage error, which writes nothing: a value that is not a cap, nothing to set or clear, a --by label that is empty, longer than 200 characters or holds control or formatting characters, or PROJECT_ID with --all. 3 for a change to a project in a phase this vibey does not know, because nothing can be recorded under it. Showing that project still works.

vibey budget no-cap and vibey budget cap

no-cap declares no cap for ULTRA runs through sub-doctrine 8.b's whole path, in a terminal on the host:

  1. a full-screen warning with the measured cost per hour ("unknown" when nothing has been measured);
  2. the typed phrase I accept unlimited spending;
  3. a second warning, whose default answer keeps a cap;
  4. the declaration: [budget] ultra_no_cap = true in --toml (default ./vibey.toml), and a trusted UltraNoCapChanged ledger event naming who, when and which device.
Subcommand Option Default What it does
budget no-cap [PROJECT_ID] --toml PATH vibey.toml The vibey.toml that records the declaration.
--by NAME the account running it As for set: a label for the record, not a permission; the account is recorded beside it.
budget cap [PROJECT_ID] --toml PATH, --by NAME as no-cap As for no-cap.

Both default to the most recently created project. Run off a terminal, no-cap refuses with exit 2 and records nothing. A wrong phrase exits 1. The default answer to the second warning prints Kept the cap and exits 0. No environment variable can declare it. cap withdraws the declaration in one command, binding at the next ULTRA pass: it records UltraNoCapChanged with enabled: false, and sets the key to false when the file exists. The worker reads only the ledger event. The file records the declaration.

vibey ultra

ULTRA is effort without a ceiling (ADR-0063). While a project's ULTRA run is on, every BUILD pass runs at ULTRA: the attempt ladder is not consulted, so a pass never parks for running long, and qwenloop, gptossloop and claudeloop get no --max-turns. A pass that ends with a done verdict is a checkpoint. The checks (build.verify) are enqueued as always, an UltraPassCompleted event is recorded, and the next pass is enqueued after the checks, under its own job key. Two things end the run: the operator's Stop, and the budget brake at a declared cap. A run with no dollar cap parks an ultra_needs_cap gate unless no cap was declared (vibey budget no-cap).

Subcommand Option What it does
ultra start [PROJECT_ID] --by NAME Starts the run: records a trusted UltraStarted event.
ultra stop [PROJECT_ID] --by NAME Stops it: records UltraStopped. No further pass starts; a queued pass ends without running.
ultra status [PROJECT_ID] --json Running or stopped, passes completed, the dollar cap, this cycle's spend and the measured cost per hour (null, shown as "unknown", when nothing has been measured).

Each event records by, account and device (the host's name). These commands run only on the host. start after a stop does not resume a stopped item's chain by itself: the stopped pass ended without a successor, so the next pass starts when the item's BUILD job is enqueued again.

vibey driver

The driver's failover and handback (ADR-0070). The driver is the Claude Code session steering the work. When it hits a usage limit or runs out of credit, the work continues on [failover] target_engine (gptossloop) at target_effort (ULTRA). When a probe of the paid model is recorded as successful, the work goes back to the same session.

Subcommand Option What it does
driver hook --config PATH The command Claude Code's StopFailure hook runs; reads the hook's JSON on stdin. On error rate_limit or billing_error it gates a brief into <cwd>/.vibey/driver/, appends EngineFailedOver and starts the sovereign engine detached. Any other error is ignored. A second hook for an active failover does nothing. Prints the outcome as JSON; exit 3 when the gate parked it (Claude Code ignores this hook's exit code).
driver probe --cwd PATH, --config PATH Run by the timer. When a failover is active and its probe is due, runs probe_argv and appends EngineProbed (ok or not). Only after a recorded ok probe: winds the sovereign engine down, gates the return brief (it lists the commits made meanwhile), appends EngineHandedBack and resumes the session with resume_argv (claude -p --resume <session-id>).
driver status --cwd PATH, --config PATH The state, read from <cwd>/.vibey/driver/ledger.jsonl, as JSON.
driver timer --out DIR, --platform launchd\|systemd, --cwd PATH Writes a launchd agent (macOS default) or a systemd user service and timer that run vibey driver probe --cwd <worktree> every probe_interval_seconds, and prints the launchctl / systemctl --user command that loads it. vibey never loads it for you.
driver hook-config Prints the settings.json block that runs vibey driver hook on StopFailure, with no matcher.

Both directions pass the no-loss gate: STRICT up to three times (the brief's transcript SHA-256 must equal the transcript's now), then FULL_TRANSCRIPT (the transcript is copied into the worktree), then HUMAN: a PARKED-*.md brief is left in .vibey/driver/ and nothing is started. CreditsExhausted has no reset time; a window's reset only schedules the probe, and only a recorded successful probe hands back.

vibey supervisor

Keeps vibey worker --all-projects and the triaged-delivery bridge (scripts/triaged_delivery.py --interval N) running (#1189): a launchd agent on macOS, a systemd user service on Linux, rendered from [supervisor] by the same renderer as vibey driver timer. Each unit starts its process at load, restarts it restart_seconds after it exits with a failure (never after a clean exit, so a worker drained by SIGTERM stays down), and appends its output to <log_dir>/<name>.log. vibey never loads a unit for you: install prints the commands.

Subcommand Option What it does
supervisor install --repo PATH (.), --out DIR, --platform launchd\|systemd, --config PATH Writes <label_prefix>.worker and <label_prefix>.delivery (and <label_prefix>.state-sync with [supervisor] state_sync = true) into the service manager's directory (~/Library/LaunchAgents, or $XDG_CONFIG_HOME/systemd/user), or --out. Creates the log directory, and the environment file from a commented template, readable by you alone, when it does not exist (an existing one is kept); with state_sync, the state sync's own state-sync.env the same way. Prints the launchctl bootstrap or systemctl --user enable --now commands that load them. Refuses, exit 78, a log directory, environment file, vibey, Python or --repo under a directory a reboot empties or inside a linked worktree (10.h), vibey not on PATH, and a --repo with no scripts/triaged_delivery.py unless delivery = false. --config defaults to <repo>/vibey.toml.
supervisor status --platform launchd\|systemd, --config PATH One line per service: running, stopped, not loaded or not installed, read from the unit file and launchctl print / systemctl --user is-active. Exit 0 only when every service runs, else 1. --config defaults to ./vibey.toml.
supervisor exec --env-file PATH -- COMMAND... What every unit runs. Reads KEY=VALUE lines from the file (# comments, an optional export, one pair of matching quotes removed, nothing expanded; a declared value wins over the inherited one), then replaces itself with COMMAND. Exit 2 with no command, 78 when the file is missing or a line is not a pair (named by line number, never quoted), 127 when COMMAND cannot run.

vibey doctor prints one supervisor-worker and one supervisor-delivery line, and a supervisor-state-sync line with state_sync = true: PASS when the service runs; WARN when it is not installed, not loaded or stopped, naming what is missing; FAIL instead of WARN with [supervisor] required = true. An unreadable [supervisor] table is a FAIL.

vibey state

Keeps this database and a sealed copy of it on the repository's vibey-state branch the same, in both directions (ADR-0086). The branch holds one file, encrypted with AES-256-GCM under the state key, and nothing else. Every setting is a VIBEY_STATE_* variable (State sync); the database is VIBEY_STATE_PG_URL, else VIBEY_PG_URL. Bare vibey state prints help. Subcommands, in vibey state --help order: sync, status, export, import, forget, key.

Subcommand Option Default What it does
state sync --no-push off Merge this database with the branch's head, three-way against the commit it last synced with, and write the result to each end that differs. With --no-push, write only here: the branch is read, never written.
--every SECONDS unset Sync every this many seconds (a number above 0), for as long as the process lives. Each round's line starts with its UTC time. A failed round is said on stderr and the next one runs. What [supervisor] state_sync runs.
state status --json off What a sync would pull and push. Writes nothing at either end. --json prints one object: state, remote, commit, rows, to_pull, to_push, conflicts.
state export --out FILE required Write this database, sealed under the state key, with the commit it last synced with, to FILE (mode 0600, written whole or not at all). For carrying it to an empty database, as the remote-command workflow does.
state import FILE Load a sealed export into this database, which must hold no rows in any synced table and have applied the same migrations as the export. Records the export's commit as this database's, so the next sync stays three-way.
state forget Forget the commit this database last synced with, so the next sync merges both ends as a first sync would, against no base. Only for a branch that was rewritten or deleted: the sync refuses until then, and says so.
state key Say where the state key is: VIBEY_STATE_KEY, the macOS keychain or the key file. Exit 1 when there is none.
--new off Make a key and keep it where it would be found first here: the keychain on macOS, the key file elsewhere. Refused while a key exists anywhere: replacing one makes every state sealed under it unreadable.
--show off Print the key, and only the key, for another machine's VIBEY_STATE_KEY or for a private repository's runners: vibey state key --show \| gh secret set VIBEY_STATE_KEY.

sync and status print one line: OWNER/NAME:vibey-state: in sync at <commit>, N row(s); synced at <commit>: N row change(s) pulled, pushed; N row(s); or, from status, N row change(s) to pull, changes here to push; N row(s). A row both ends changed differently is settled by its table's rule (newest, mine, theirs or refuse). A conflict no rule settles stops the sync before anything is written: it prints N conflict(s); nothing was changed at either end: to stderr, then one line per row (table key: reason), and exits 1. The ledger is never settled by a rule: two ends that appended different events at the same seq have diverged, and a person decides. Two databases that applied different migrations are refused: run vibey migrate on the one behind. A sync that found the database or the branch moved under it starts again, up to VIBEY_STATE_ATTEMPTS times. Run twice in a row, the second sync pulls nothing and pushes nothing.

Exit codes: 0 done or in sync; 1 a conflict, a refusal (no key, a key that does not open the branch, a base the branch no longer reaches) or a failure, each said on stderr; 2 a setting that cannot be used. vibey state is never offered by the hub, and the hub refuses it as a command for the workflows (RESERVED_COMMANDS). On the runner the remote-command workflow runs it itself, and only for a private repository (ADR-0086).

vibey ledger

Bare vibey ledger prints help. Subcommands, in vibey ledger --help order: search, export, site, show. What export and site produce can be browsed in the ledger explorer.

Subcommand Option Default What it does
ledger search [PROJECT_ID] --id UUID unset Exactly this record.
--digest SHA256 unset Records whose payload has this full 64-character SHA-256 digest. A digest names a payload, not a record, so several records can match.
--actor ACTOR unset Who produced it: an engine id, a provenance (trusted, agent, untrusted), or vibey for events vibey wrote itself.
--since TIME unset Produced at or after this ISO-8601 time; no zone means UTC.
--until TIME unset Produced before this ISO-8601 time; no zone means UTC. Must be after --since.
--kind KIND (repeatable) unset Event kind, by name or value, any case; repeat it for any of several. A kind this vibey does not know (a newer one wrote it) is matched exactly, with a note on stderr.
--text TEXT unset Appears in the payload's JSON text, literally, any case. Must not be empty.
--limit N / -n 50 At most this many, the latest matches (min 1).
--json off Print JSON: every field of every event.
ledger show [PROJECT_ID] --limit N / -n 50 Show the most recent N events (min 1).
--phase PHASE unset Filter to one phase, by name or value, case-insensitive (BUILD, deploy_design).
--kind KIND unset Filter to one event kind, by name or value, case-insensitive.
ledger export PROJECT_ID --out FILE / -o required Write the public, redacted ledger projection.
--billing off Write the operator-scoped billing projection consumed by vibey-gh forecast; it keeps metered spend fields and operational event kinds only.
ledger site --from FILE required A shard file written by vibey ledger export. Needs no database.
--out DIR / -o required The directory to write the site into.
--json-only off Build the JSON surface: records/<event_id>.json, index.json, manifest.json. Required until the human-first record pages exist; without it the command exits 2. An invalid shard exits 1.

ledger search applies every criterion it is given, in Postgres, to one project's ledger (the latest when PROJECT_ID is omitted). A malformed criterion is a usage error (exit 2), checked before any connection is opened; an unknown PROJECT_ID prints unknown project <id> and exits 1. It prints one line per event, oldest first, then a line saying how many matched and whether older matches were cut. What gets published covers export and site.

ledger show prints one line per event, oldest first: #<seq> <YYYY-MM-DD HH:MM:SS> [<PHASE>] <kind> [<engine>]. Filters apply before --limit. Defaults to the most recently created project.

vibey queue

See the job queue in claim order, move a job to the front of it (ADR-0054), and reap what is stuck (ADR-0056). Bare vibey queue prints help.

Subcommand Option Default What it does
queue list [PROJECT_ID] --json off Every unfinished job: running work first (running), then waiting work numbered in the order the claim will take it. A bumped job is marked bumped #N, one pulled forward for another job says which, and a job still waiting on unfinished dependencies says how many. A job in a phase or state this vibey does not know is listed with - in place of a number, marked as not claimable by this vibey (claimable_here: false in JSON). Defaults to the latest project; an unknown id prints unknown project <id> and exits 1.
queue bump JOB_ID --project PROJECT_ID latest project Run the job next: after whatever is running, ahead of every un-bumped waiting job, behind anything bumped before it. Its unfinished dependencies move forward with it, dependencies first. Prints every job moved with its new place and any already ahead.
--source NAME unset An automation naming itself (see below).
--json off Print the change as JSON.
queue reap --project PROJECT_ID latest project One pass of the queue reaper: every expired lease (requeued while attempts remain, parked with a delivery_exhausted gate once they are spent), work claimable and unclaimed for [queue.reap] stale_ready_seconds in every project, and -- with a broker configured -- vibey's two policies reconciled onto the queues it owns and checked on them, every queue measured, and each dead letter on an owned queue parked as a bus.dead_letter job (once, whichever project's pass reads it). A lease or claimable-work sighting is recorded under its own job's project; a broker sighting once, under this project.
--dry-run off Judge everything and change nothing: no requeue, no park, no ledger event, no policy write. Prints what it would do.
--json off Print the report as JSON: ok, acted, surfaced (each with object, queue, condition, measured, threshold, unit, action), policy, notes, unreadable.
queue unbump JOB_ID --project, --source, --json as bump Take the job out of the lane. The lane is always the jobs bumped by name plus their unfinished dependencies, so every pulled-in job no remaining named job needs leaves with it; the output and the ledger list exactly what was removed. Refused while another named job depends on this one.

A bump changes order only. It never interrupts the running job or touches its lease, never makes a job claimable before its dependencies succeed, and never shortens a run_after a capacity deferral set; phases and human gates apply as before. A job that is running can be bumped: it keeps its place if the attempt returns it to the queue.

Who may reorder is decided by the project's own vibey.toml — at the root of the repository the project record names, never the current directory — and nothing on the command line can widen it. With no --source, the caller must be the operator: the account that owns that file (or the repository root, when there is none), checked by uid. With --source NAME, NAME must be declared in that file's [queue.priority] sources (see the configuration reference) and the caller must still be that account. Anything else is refused: nothing moves and the command exits 3 with the reason.

Every request is recorded on the project ledger — JobPriorityBumped, JobPriorityUnbumped or JobPriorityRefused — whether it moved something, moved nothing (bumping a job already bumped or already finished, un-bumping one that is not: exit 0 with nothing moved or nothing to move), or was refused. The grant is checked before the job is looked up, so a refused request is recorded whether or not the job exists. vibey ledger search --kind JobPriorityRefused lists the refusals. Refused with exit 3, and recorded:

  • a caller that is not the operator, or a source not declared or not run as the operator;
  • a vibey.toml or repository this process may not read, or one that is not valid TOML;
  • a job that does not exist in the project, or is in a state or phase this vibey does not know; an un-bump of a finished job;
  • a bump whose dependency can never finish (failed or cancelled), or a dependency ring — the message names it;
  • an un-bump of a job that another named job still depends on — the message names them;
  • a request the database aborted to break a lock cycle (retry it).

queue reap prints what it reaped (reaped:), what is stuck and was only surfaced (stuck, surfaced, nothing moved:), what was stuck and no longer is (no longer stuck, closed on the ledger:), the broker policies' check (verified or NOT VERIFIED, naming each owned queue that does not carry its policy), notes, and every source it could not read (UNREAD:). A surfaced condition is recorded once for the whole fleet -- every pod and every queue reap share the ledger -- until a pass that read its source whole records it cleared. Each verdict line names the action, the condition, the object, the queue, the measured value and the threshold. It exits 0 when every source was read and the broker policy, where one was reconciled, read back as written; otherwise 1 -- a reaper never reports success it did not observe. It never deletes a dead letter: answer the parked job's gate with vibey answer GATE_ID --choice replay (publish it back to the queue it died on, at least once -- offered only when that queue is one vibey owns, since its name comes from the message's own headers) or --choice dismiss; the broker's copy stays on the dead-letter queue either way. Its thresholds are [queue.reap].

vibey-gh slots

How many runs of one local model may run at once on this device, measured per device (ADR-0058). This is a vibey-gh command, part of the same distribution:

Subcommand What it does
vibey-gh slots corpus --pool FILE --out FILE Draw a stratified corpus of turn segments from a turn pool (the storm builds one with storm_turn_pool.py).
vibey-gh slots calibrate --corpus FILE Sweep N = 1, 2, 3, ... on this device beside an idle production runner. Stops at a broken bound or a plateau, checkpoints every step, and records the evidence keyed to the device's fingerprint.
vibey-gh slots allowed Print the number a queue may run at once here, with the reason on stderr. Missing or stale evidence prints 1 and requests a calibration.

Every option is in the vibey-gh CLI reference.

vibey-gh sabbath

The Sabbath as the release tooling keeps it (sub-doctrine 8.i, ADR-0072). vibey-gh merge-train and vibey-gh promote stand down inside the window: they print the hold, write it to the job summary, and exit 0. vibey-gh sovereign --beat keeps beating, recording "resting until ...".

Subcommand What it does
vibey-gh sabbath status The window, the zone, the location source, and every lane paused for the Sabbath.
vibey-gh sabbath register-lane --name NAME [--cwd DIR] -- COMMAND ... Pause a lane for the Sabbath. The heartbeat runs COMMAND after sundown Saturday and forgets the lane once it succeeds.
vibey-gh sabbath resume [--dispatch] Outside the window, resume paused lanes now. --dispatch also re-fires the held merge train and promotion.

vibey deploy

Phases ④–⑥. Bare vibey deploy prints help. Each subcommand takes an optional PROJECT_ID, defaulting to the latest project; an unknown id prints unknown project <id> and exits 1.

Subcommand What it does
deploy status Prints the project name, its current phase, cycle, and the endpoint recorded by the most recent deployment_verification artifact ((none) if there is none).
deploy inspect Prints spec id, scope digest, and monthly budget from the most recent deployment_spec_accepted decision. When no spec has been accepted it prints the placeholders default / none / $100.00.
deploy plan Placeholder, not yet implemented: prints Status: NOT EVALUATED with every check marked "not checked". Reads no IaC changeset and evaluates no budget.
deploy cancel Placeholder, not yet implemented: prints a notice and cancels nothing. No cloud resource, ledger event, or job or phase state is touched.
deploy rollback Placeholder, not yet implemented: prints a notice and performs no rollback.

Only deploy status and deploy inspect read real state today; the other three reserve the command surface. Deployments themselves run through vibey worker (see --azure) after an explicit opt-in at REVIEW.

vibey doctor

Check local PostgreSQL readiness, engine installs and auth; optionally install PostgreSQL, run the conformance suite, or run the in-cluster preflight instead.

Option Default What it does
--conformance off Run the 9-check conformance suite against each checked engine that is installed.
--engine ENGINE unset Check one engine: claudeloop, codexloop, gptossloop, qwenloop, or claudeloop-local. An unknown name prints Unknown engine: <name> and exits 1.
--record off Persist preflight (and conformance, with --conformance) results to engine_health. Exits 1 if no project exists.
--project ID latest Project to record health for, with --record.
--cluster off Run the in-cluster preflight instead of the engine checks — see Kubernetes guide.
--engines LIST unset With --cluster: the worker's own --engines allow-list (chart value worker.engines). engine-auth requires exactly these. Parsed as the worker parses it; empty means unset.
--provider NAME scripted With --cluster: the worker's own --provider (scripted, claudeloop, gptossloop, or the old name qwenloop; chart value worker.provider). claudeloop adds claudeloop to what engine-auth requires.
--install-postgres off Install and start local PostgreSQL when it is missing or stopped. This is explicit; the default doctor never changes the host. It cannot be combined with --cluster.
--sovereign-fit off Run the GPT-OSS capacity probe (scripts/sovereign_probe.py) against $VIBEY_OLLAMA_URL (default http://127.0.0.1:11434) and $VIBEY_OLLAMA_MODEL (default gpt-oss:20b), and persist the measured fit; see Local models. Prints sovereign fit PASS — recorded <path>, or sovereign fit FAIL — no measured capacity fit recorded, which makes doctor exit 1.
--fit-output PATH ~/.local/state/vibey/sovereign-fit.json Where --sovereign-fit writes the fit record. Its parent directory is created.

With --engine unset, doctor checks the two paid engines (claudeloop, codexloop) whether or not they are installed — missing ones print NOT INSTALLED — and adds each local engine that is switched on: gptossloop unless VIBEY_FEATURE_GPTOSSLOOP is set to anything but a truthy value or, if that variable is unset, ./vibey.toml in the current directory has [features] gptossloop = false; qwenloop and claudeloop-local when their variable is truthy or, if it is unset, ./vibey.toml sets [features] qwenloop / claudeloop_local = true. --engine gptossloop and --engine qwenloop work regardless of the switch. When qwenloop is switched on, doctor first prints note: qwenloop is switched on, and since ADR-0064 it runs a Qwen model ....

Each engine line shows install state, version, and auth. Auth is the exit status of <binary> doctor; if that command cannot run, doctor falls back to the engine's API-key variable (see Environment variables). Each probe starts from the engine's allow-listed environment, never doctor's own: the defaults, or with --record the target project's engine_environment, so a credential that project declares reaches the auth check and the conformance run as it reaches a session. A forbidden declaration in that project exits 2.

The final postgresql line shows READY, NOT READY, UNSUPPORTED, or NOT INSTALLED. READY means a local PostgreSQL server at port 5432 accepts connections and is at least version 14. --install-postgres exits 1 if the package manager or service start fails, or if verification still does not reach that state.

The db-passwordless line after it asks whether the app DSN's database admits a login with no password at all: as the DSN's role and as the OS user doctor runs as, on the DSN's host and, when that host is local, on each local socket directory. It prints FAIL when one is let in, and doctor then exits 1. That happens with trust or peer authentication: any process running as that user, an engine session included, can open the database without VIBEY_PG_URL. Sub-doctrine 10.j (ADR-0061) requires scram-sha-256 for every connection; see SECURITY.md §5 and §7. It prints PASS when every attempt was refused, and UNKNOWN when none reached the server or VIBEY_PG_URL is unset. UNKNOWN does not change the exit code.

With --conformance, the command exits 1 if any engine fails a check. The worker does not select an engine for engine-driven jobs until a doctor --conformance --record run has passed for it.

When VIBEY_PG_URL is set, doctor ends with two database checks (ADR-0055). Each prints PASS, FAIL or UNKNOWN, and doctor exits 1 if either fails:

  • ledger-guard fails when the role VIBEY_PG_URL connects as could rewrite the ledger: it is a superuser, owns event, holds UPDATE, DELETE or TRUNCATE on it or a partition, or finds a guard trigger missing or disabled. A single-DSN install fails here until its roles are split (database roles).
  • local-auth fails when the server lets a password-less connection in as the owner or a superuser. It attempts one on the DSN's host and, for a local host, on the local socket directories. It also fails when pg_hba.conf has a trust, peer, ident, md5 or password rule that can match them, and when password_encryption is not scram-sha-256 (sub-doctrine 10.j, ADR-0061). It is UNKNOWN (not a failure, never a pass) when it could neither get in nor read pg_hba_file_rules.

With VIBEY_PG_URL unset, ledger-guard prints UNKNOWN and nothing is checked.

The last line, hub-exposure, asks whether a running hub (vibey serve) listens where vibey.toml says it may: PASS on loopback or on a LAN address [hub] lan = true declares, FAIL on an undeclared one (the exit is then 1), and UNKNOWN when no hub is running or its runtime record names a process that is gone. A [hub] table that cannot be read is a FAIL.

When a local engine is switched on, the ollama line asks the server (/api/ps, three seconds, host and port only in what it prints) what it is holding. WARN when the configured model (VIBEY_OLLAMA_MODEL, default gpt-oss:20b) is loaded at a context above VIBEY_OLLAMA_CONTEXT (default 8192): Ollama reloads a model whose context differs from a request's, so the first request pays for the reload and can time out, and a warm-up at a larger window is how that happens. The line names the fix, ollama stop MODEL. OK when the model is loaded within the ceiling or not loaded at all, SKIP when no server answered. Never a failure: the exit code is not changed.

After the Sabbath lines, gate-notices counts the open gates in projects nobody will be told about -- [notifications] off (the default), on with desktop alerts off and no webhook, or a table that does not parse -- and prints them as WARN gate-notices N gates waiting, nobody will be told: <project> <count> (<reason>), ..., with how many more will be. PASS when there are none, or when every waiting gate's project has a channel; UNKNOWN when VIBEY_PG_URL is unset or the gates cannot be read. It is a report, never a failure: those gates are still listed by vibey gates, and each is recorded on the ledger as undeliverable ([notifications]).

--cluster ignores --conformance, --engine, --record and --project, runs up to eight checks, and exits 1 if any fails: the DSN host resolves beyond its own namespace, the process is not root, the workspace (current directory) is writable, the engines the worker uses have API keys, the database accepts a connection, no migrations are pending, and the two database checks above (ledger-guard, local-auth). The last three are skipped when the database connection fails.

The engine check (engine-auth) judges what the worker was told to run, not what is on PATH — every runner ships in the image since ADR-0037, so a binary's presence says nothing. It requires each engine in --engines, plus claudeloop under --provider claudeloop, to be on PATH with one of its API-key variables set (gptossloop and qwenloop take none). With neither, nothing is required: the check passes and reports which engines in the worker's default pool have a key, so a default install says plainly when no engine-driven job can run. --engines and --provider without --cluster exit 2.

vibey migrate

Apply migrations as the schema's owner, then make the application role's privileges exactly the declared ones (ADR-0055). It reads the owner's DSN from VIBEY_PG_MIGRATE_URL and the application's from VIBEY_PG_URL. It takes no options.

  1. Applies pending migrations under the migration lock, as the owner.
  2. Creates the role VIBEY_PG_URL names if it is missing and the DSN carries a password. A role that is a superuser, the owner, or a member of the owner is refused.
  3. Revokes everything that role holds and grants exactly APP_ROLE_GRANTS.
  4. Attaches the TRUNCATE guard to any partition of event that lacks it.
  5. Connects as the application role and prints the ledger guard.

It exits 2 when VIBEY_PG_MIGRATE_URL is unset. It exits 1 when VIBEY_PG_URL is unset (nothing reconciled, guard unchecked), or when the guard is not in force: for instance while VIBEY_PG_URL still names the owner. The Helm chart runs it as the migrate init container of the worker and the operator, the only place the owner's DSN is mounted.

vibey operator

Run the Kubernetes operator, reconciling VibeyProject custom resources (ADR-0025). Requires the optional extra, pip install 'vibey-engine[operator]'; without it the command prints operator support is not installed: pip install 'vibey-engine[operator]' and exits 1.

Option Default What it does
--namespace NS unset (cluster-wide) Watch a single namespace instead of the whole cluster.

vibey worker

Long-running worker: listens on vibey_job_ready and dispatches jobs across every phase for one project, or with --all-projects for every project.

Option Default What it does
--engines LIST the two paid engines plus every local engine switched on (gptossloop by default) Comma-separated allowlist of engine ids (claudeloop, codexloop, gptossloop, qwenloop, claudeloop-local) for engine-driven jobs. An unknown id prints Invalid engine: ... and exits 2; a retired one (cursorloop, agyloop) is refused the same way, the message naming ADR-0078. A local engine joins the pool only while its switch is on (see below): gptossloop unless switched off, qwenloop and claudeloop-local only when switched on. A list that matches none of the worker's engines — --engines qwenloop with its switch off, say — is refused at startup with --engines <list> matches none of this worker's engines (...) and exits 2, rather than starting a worker with no engine that would defer every engine-driven job forever.
--parallelism N / -j N 1 Concurrent job loops, 1–16. The effective count is clamped to twice the number of allowed engines and to the CPU count, and is never below 1.
--once off Process one job and exit (processed one job or no ready job), instead of running forever.
--provider {scripted,claudeloop,gptossloop} gptossloop DESIGN and decomposition providers. gptossloop uses the sovereign local DESIGN and decomposition providers on Ollama (GptossloopDesignProvider, GptossloopWorkPlanProducer; reads $VIBEY_EVIDENCE_DIR), recorded in the ledger as gptossloop. claudeloop uses a live session for both DESIGN and decomposition, capped by --max-turns / --max-dollars. scripted is fully offline. qwenloop is still accepted and read as gptossloop, with a notice on stderr (ADR-0064). Any other value exits 2.
--max-turns N 25 Turn cap per claudeloop DESIGN or decomposition session (min 1).
--max-dollars F 2.0 Dollar cap per claudeloop DESIGN or decomposition session (0.01–10).
--ollama-model NAME $VIBEY_OLLAMA_MODEL, else gpt-oss:20b Local model for --provider gptossloop, and the model the worker hands gptossloop (below); ignored by the other providers.
--project ID latest Project to work on.
--all-projects off Serve every project (#1189). Each pass asks the queue which projects have a job the claim would hand out now, ordered by each project's next job in the claim's own order (bump, priority, due time), and claims through that project's own loop — the same FOR UPDATE SKIP LOCKED claim, budgets, Sabbath gate, capacity circuits, phase gates and engine selection a single-project worker uses. A project's loops are built, and its engines preflighted, the first time it has work. A project that cannot be served (an unknown phase, an --engines list matching none of its engines, a forbidden engine_environment) prints project <name> refused: its jobs stay queued for a worker that can serve them and the worker goes on with the others. With nothing queued anywhere it waits on any project's vibey_job_ready instead of exiting, so a supervised worker never restart-loops. Parallel loops (-j) are clamped to the CPU count only. Exits 2 with --project, --wait-for-project or --auto-answer. This is what vibey supervisor install runs.
--wait-for-project SECONDS unset (min 1.0) Poll every N seconds for a project instead of exiting 1 when none exists yet — for long-lived deployments, where exiting means a restart loop.
--ignore-sabbath off Claim jobs through a Sabbath window that would otherwise leave this worker claiming nothing. For this worker only, said on stderr when a window would have held, never stored (see the Sabbath guide).
--auto-answer off Answer this project's waiting gates for you, so an unattended run keeps going: the DESIGN interview's questions with their own defaults (accept_defaults), {"max_attempts": 10} for an escalation_exhausted gate, {"max_rounds": 6} for verify_repair_exhausted and integrate_repair_exhausted, and {} to deliver a delivery_exhausted job once more. Never a spending gate (budget_exhausted), engine_misconfigured, research_evidence, an approval or a review: those wait for you, and each sweep names them. The consent is the flag, for this one worker and one project (exits 2 with --all-projects), and nothing is stored. Each answer is recorded on the ledger as GateAnswered by auto-answer, under a request id derived from the gate so a replay is a no-op.
--auto-answer-limit N 30 The most answers --auto-answer gives before it says so and leaves every gate to a person (min 1), so a job that cannot finish does not retry for ever.
--local-max-turns N\|unlimited unset The turn limit each local runner (gptossloop, qwenloop) session is started with, in place of its per-effort 8, 16, 40, 64 or 96: a whole number, or unlimited (handed over as 100000, a count no session reaches). A turn limit is a cost control, and a local model costs nothing per turn; a small model that needs many tool calls to read a repository fails at 16 for the limit's reason, not the work's. A session still ends when it reports the task done, is stopped (vibey controls) or fails. A bad value exits 2. Paid engines keep their limits; ULTRA keeps its own (no flag). Same as VIBEY_LOCAL_MAX_TURNS, which the flag sets for this worker.
--auto-record-research-gaps off DESIGN research that can obtain no evidence (no search backend, no VIBEY_EVIDENCE_DIR) records the topic as not researched and goes on, instead of parking a research_evidence gate; a gate already parked is answered with nothing, which runs the research again. The spec says what was not researched, so nothing is invented. Sets VIBEY_DESIGN_RESEARCH_ON_UNAVAILABLE=record_gap for this worker (beating ./vibey.toml and an earlier gate). One project: refused with --all-projects.
--autonomous off Every switch that is safe to leave on for an unattended run: --auto-answer, --auto-accept-all and --auto-record-research-gaps. Never --auto-publish (it sends the work to GitHub, so it is its own flag), never a deployment gate and never a spending gate. One worker, one project.
--auto-accept-design off Accept this project's DESIGN spec yourself, once DESIGN has finished (no DESIGN job of the cycle unsettled, a spec present), instead of waiting for vibey design accept. That is your decision that the spec is right, given in advance and only for this worker and one project (not with --all-projects). The acceptance guards still apply, so an open blocking question refuses it and the worker asks again. It never opts into VISUAL_DESIGN, and it accepts once. The ledger's visual-design event carries accepted_by: auto-accept-design. Without the flag nothing changes: --auto-answer still never answers an acceptance.
--accept-spec-json PATH unset With --auto-accept-design: import this reviewed spec (the file vibey design accept --spec-json takes) just before accepting, instead of accepting what the synthesizer wrote. A file that cannot be read exits 2 before any engine starts.
--auto-accept-review off Accept this project's REVIEW approval gate yourself: the verdict accept, never changes or cancel, and only when the gate offers accept. Your decision that the review is good, given in advance, for one worker and one project (not with --all-projects). Recorded on the ledger as GateAnswered by auto-answer, and the startup line names it.
--auto-finish-local off Answer the deployment choice after REVIEW with local_only, the gate's own default, so the project finishes without deploying. It never answers deploy.
--auto-accept-all off --auto-accept-design, --auto-accept-review and --auto-finish-local together: every acceptance from DESIGN through REVIEW, finishing local. It is not --auto-answer: the interview and retry gates stay yours unless you add that flag too.
--auto-publish off Answer this project's publish_interview gate accept_defaults: publish exactly what its prompt shows (a draft pull request, or a new private repository for a project with no remote), never custom. It sends the finished work to GitHub, so it is its own flag and no other --auto-accept flag implies it, --auto-accept-all included. Only meaningful for a project whose [publish] mode is interview; one worker and one project.
--azure {memory,az} memory Azure client for the deploy stage set. memory is an in-memory adapter that touches no real infrastructure. az uses the real Azure CLI and mutates real resources on consented deploys; the worker runs az account show first and exits 1 if you are not logged in. Any other value exits 2.

What no acceptance flag answers. The publish interview is answered only by --auto-publish. The deployment gates (deploy_interview, deploy_acceptance, deploy_demo_review, deploy_failure_triage) and every spending gate (budget_exhausted) are never answered by any of these flags, together or alone. deploy_acceptance is explicit consent to change real infrastructure, which no flag sends. --auto-accept-design never opts into VISUAL_DESIGN, so that stage is never entered by an unattended run.

The VISUAL_DESIGN stage always uses the scripted visual provider.

On start the worker preflights every allowed engine — including each local engine that is switched on, so a local engine is visible in vibey engines like every other. Engines with no passing recorded conformance produce warning: no recorded conformance for <names> -- engine-driven jobs will not select them until `vibey doctor --conformance --record` passes. It then prints worker started: project=<name> engines=<list or all> parallelism=<n> provider=<p> (worker started: all projects ... with --all-projects, which preflights each project when it is first served and prints serving project=<name> (<id>) then).

Local engines (ADR-0015, ADR-0038, ADR-0064): the worker runs gptossloop unless VIBEY_FEATURE_GPTOSSLOOP is set to a value that is not truthy, and qwenloop or claudeloop-local only when VIBEY_FEATURE_QWENLOOP or VIBEY_FEATURE_CLAUDELOOP_LOCAL is truthy. Unlike doctor, the worker does not read [features] from vibey.toml; it falls back to a features table in the project's stored config, which no creation path writes today, and then to each engine's default. In practice the environment variables are the only switches for the worker. When qwenloop is switched on the worker prints note: qwenloop is switched on, and since ADR-0064 it runs a Qwen model ... at start. With VIBEY_OLLAMA_URL set, the worker hands gptossloop GPTOSSLOOP_BASE_URL=<url>/v1 and GPTOSSLOOP_MODEL (--ollama-model, else VIBEY_OLLAMA_MODEL, else gpt-oss:20b), and qwenloop QWENLOOP_BASE_URL only — never a model; a variable the operator set already is left alone.

qwenloop shared-turn worker

qwenloop server turn-worker hosts one inference server behind the durable RabbitMQ turn queue so multiple lane processes can share it. Configure turn_dispatch_mode = "rabbitmq", turn_queue_url, and optionally turn_queue_name in the qwenloop configuration before starting it. The command refuses direct mode, starts or verifies the configured inference server, and then consumes turns until stopped. Keep the broker URL in operator-owned secret configuration; use a TLS AMQP URL and least-privilege vhost/queue credentials when the broker is not local. If the worker stops, restart the same command after RabbitMQ is available; durable requests remain on the queue for another worker.

qwenloop server benchmark runs a measured direct-versus-hybrid-versus-RabbitMQ comparison against the configured local server and stores the per-mode rates and winner in the user cache. Set turn_dispatch_mode = "auto" to use that persisted winner on subsequent runs; missing or invalid benchmark data safely falls back to direct dispatch. Use --samples and --concurrency to control the experiment size.

The queue reaper (ADR-0056) runs in the same idle iterations: after the lease reap, at most once per [queue.reap] interval_seconds across all of the worker's loops, the worker surfaces claimable, unclaimed work in every project and -- with a broker configured -- reaps the broker, as vibey queue reap does. Its findings go to the ledger (QueueReaped) and to stderr (queue.reaped, queue.stuck, queue.unstuck, queue.reap_unreadable). A reap that fails is reported (lease reap failed:, queue reap failed:) and the worker carries on.

Shutdown (ADR-0026): SIGTERM drains the worker — it finishes the job in hand, claims no more, and exits. A SIGTERM that arrives during startup, before the handler is installed, is latched at import time and honoured as soon as the event loop starts. Ctrl-C aborts immediately with exit 130. The worker writes diagnostic lines to stderr (sigterm handler registered, drive[<i>] iter=...); they are temporary instrumentation, not errors.

Environment variables

Variables read by code under src/vibey:

Variable Read by Effect
VIBEY_PG_URL every command that opens the database; recover; doctor; migrate The application role's PostgreSQL 14+ DSN (database roles). There is no default: when unset, vibey refuses with VIBEY_PG_URL is not set. vibey will not guess a database. (exit 3 from guarded commands, a traceback from the others). vibey install --postgres installs the server but does not set this variable for the parent shell.
VIBEY_PG_MIGRATE_URL migrate only The owner's DSN. Migrations run on it and the application role's grants are reconciled from it. Give it to that one command (VIBEY_PG_MIGRATE_URL=… vibey migrate); never export it.
VIBEY_STATE_PG_URL, VIBEY_STATE_KEY and the other VIBEY_STATE_* state only The database the state sync reads and writes (default VIBEY_PG_URL), the state key, and where the sealed state lives; every one in State sync.
VIBEY_EVIDENCE_DIR work --provider gptossloop, worker --provider gptossloop Directory of reading material for the gptossloop DESIGN provider's research stage. Unset means research refuses and DESIGN stops there.
VIBEY_FEATURE_GPTOSSLOOP doctor, worker, loops 1, true, yes, or on (case-insensitive) enables gptossloop; any other set value, 0 included, disables it. When set it overrides config. When unset, doctor and loops fall back to [features] gptossloop in ./vibey.toml and worker to the project's stored config; with neither, gptossloop is on (ADR-0064).
VIBEY_OLLAMA_URL work and worker with --provider gptossloop; doctor --sovereign-fit; loops; the worker's local engines The Ollama server, default http://127.0.0.1:11434. Only an http(s) URL with a host is accepted.
VIBEY_OLLAMA_MODEL as VIBEY_OLLAMA_URL The local model, default gpt-oss:20b; --ollama-model beats it.
VIBEY_OLLAMA_TIMEOUT the sovereign DESIGN and DECOMPOSE providers Seconds per request, default 900.
VIBEY_OLLAMA_CONTEXT as VIBEY_OLLAMA_TIMEOUT Context ceiling in tokens (num_ctx), default 8192, at least 4096.
VIBEY_OLLAMA_OUTPUT as VIBEY_OLLAMA_TIMEOUT Output ceiling in tokens (num_predict), default 2048.
VIBEY_OLLAMA_THINK as VIBEY_OLLAMA_TIMEOUT The reasoning level asked for on the first request: low, medium, high, max, true, false or none. Unset or none sends no think, leaving it to the model, which for gpt-oss is a long reasoning pass that can spend the whole output budget before the answer begins. low makes the first attempt succeed where it would otherwise be cut short and retried. The retry has its own, VIBEY_OLLAMA_RETRY_THINK (default low).
VIBEY_OLLAMA_TEMPERATURE as VIBEY_OLLAMA_TIMEOUT Sampling temperature, 0 to 2, default 0. At 0 a job's retries are the same request and fail the same way, so a plan that is rejected once is rejected every attempt (seen: seven identical build.decompose attempts). Raise it (0.6 is a start) when attempts keep failing for the same reason; the answer to an unchanged question then varies, so the DESIGN and DECOMPOSE results are no longer reproducible. A bad value exits 2.
VIBEY_OLLAMA_RETRY_CONTEXT as VIBEY_OLLAMA_TIMEOUT The largest context window the one retry of a cut-short reply may ask for, default 32768, at least 4096. A prompt that leaves too little of VIBEY_OLLAMA_CONTEXT for the reasoning and the answer fills the window, and asking again inside the same window only fails again; the retry instead opens as much as it needs, up to this. A configured VIBEY_OLLAMA_CONTEXT above it is never lowered. Set it equal to VIBEY_OLLAMA_CONTEXT to keep the retry inside that ceiling. Ollama reloads a model whose context changes, so a retry that grows the window pays a reload: raise VIBEY_OLLAMA_CONTEXT itself when that is the usual case.
VIBEY_OLLAMA_FIT as VIBEY_OLLAMA_TIMEOUT Path to a fit record written by vibey doctor --sovereign-fit. When it matches, its measured context and output replace the two ceilings above; see Local models. Unset, unreadable or mismatched means the configured ceilings.
VIBEY_REVISION as VIBEY_OLLAMA_FIT When set, a fit record is used only if its revision equals this value.
VIBEY_SEARXNG_URL search, fetch, worker, the sovereign DESIGN research stage Base URL of a SearXNG instance with its JSON format enabled. Passed to engine sessions as WEBSEARCH_SEARXNG_URL. Queries go to that instance, so run your own if they are private.
VIBEY_BRAVE_API_KEY as VIBEY_SEARXNG_URL A Brave Search key. Used by vibey's own processes only; never passed to a session.
VIBEY_SEARCH_PROVIDER as VIBEY_SEARXNG_URL searxng or brave; needed only when both are configured (SearXNG wins otherwise).
VIBEY_SEARCH_TIMEOUT as VIBEY_SEARXNG_URL Seconds per search or fetch request. A bad value exits 2 before any engine starts.
VIBEY_SEARCH_RESULTS as VIBEY_SEARXNG_URL How many results a search returns.
VIBEY_FEATURE_QWENLOOP doctor, worker, loops 1, true, yes, or on (case-insensitive) enables qwenloop, the runner on a Qwen model; any other set value disables it. When set it overrides config. When unset, doctor and loops fall back to [features] qwenloop in ./vibey.toml and worker falls back to the project's stored config; with neither, qwenloop is off.
ANTHROPIC_API_KEY doctor auth fallback; doctor --cluster (which also accepts ANTHROPIC_AUTH_TOKEN) claudeloop credentials.
OPENAI_API_KEY doctor auth fallback; doctor --cluster (which also accepts AZURE_OPENAI_API_KEY, CODEX_API_KEY) codexloop credentials.
VIRTUAL_ENV engine session launch Removed, together with VIRTUAL_ENV_PROMPT, PYTHONHOME, PYTHONPATH, and the matching PATH entries, from the environment passed to engine sessions, so an engine does not install into or run vibey's own interpreter.

Engine sessions otherwise inherit the caller's environment. Build-gate and git subprocesses inherit it minus every GIT_* variable. The engine CLIs read further variables of their own; see each runner under src/vibey_runners/.