Configuration reference: vibey.toml

[!IMPORTANT] Work in progress — not a final product: The vibey.toml configuration schema is actively evolving and not yet a final product. Folks will be notified on the Discord server once the product is finally done.

The schema below is fully implemented and unit-tested in src/vibey/domain/config.py (VibeyConfig, parse_config, parse_toml_string) and src/vibey/infrastructure/config_loader.py (load_config_from_path). vibey new reads the [notifications], [telemetry], [gates], [engine_environment] and [design] tables from the repository's vibey.toml and stores them in the project record; the worker and lifecycle repository consume those stored tables. The other schema tables remain documented inputs for future wiring.

What is read at runtime today

Input Read by What it controls
./vibey.toml, keys [features].gptossloop, [features].qwenloop, [features].claudeloop_local vibey doctor and vibey loops (cli/main.py _local_engines_from_toml, through LocalEngineSettings) Which local engines are added to the health sweep and reported as switched on. The file is read from the current directory with parse_toml_string; a missing or malformed file leaves every switch at its default: gptossloop on, the others off (ADR-0064).
./vibey.toml, [notifications], [telemetry], [gates], [engine_environment], [design] and [human_gates] vibey new (infrastructure/config_loader.py) Copies project notification channels, the telemetry switch, how gate commands run, what an engine session may see of the environment, how the DESIGN interview declares its defaults ([design.interview]) and which human gates may resolve to their default after waiting ([human_gates]) into the stored project config. [gates] and [engine_environment] are validated first: a forbidden entry stops vibey new before a project exists.
<repo>/vibey.toml, [queue.priority] sources — the project's own repository root, never the current directory vibey queue bump / unbump, vibey design resume --priority, via QueuePriorityService (infrastructure/queue_priority_grant.py ProjectPriorityGrantReader) Which automations besides the operator may reorder the project's queue ([queue.priority]); the file's owner is the operator. Read fresh on every request; only the [queue] table is parsed. A missing file declares none; a malformed one refuses every request, recorded.
./vibey.toml, [queue.reap], [queue.defect] and [bus] -- or, with no ./vibey.toml, the environment alone (VIBEY_QUEUE_REAP_*, VIBEY_QUEUE_DEFECT_*, VIBEY_BUS_*) bootstrap.build_app (every command that opens the queue), via load_config_from_path or EnvironmentConfigLoader The queue reaper's thresholds and broker policy ([queue.reap]), when an exhausted job is a defect ([queue.defect]), and the bus it inspects. A cluster pod has no vibey.toml in its working directory, so the chart's environment is what composes both there (ADR-0056). A malformed environment value fails the start; a ./vibey.toml that does not parse is skipped, as build_app has always skipped it, and the environment alone is read. [notifications] sweep_interval_seconds is read from the same ./vibey.toml, for the gate-reminder sweep every idle worker runs.
./vibey.toml, [queue.reap], [queue.defect] and [bus] -- or, with no ./vibey.toml, the environment alone (VIBEY_QUEUE_REAP_*, VIBEY_QUEUE_DEFECT_*, VIBEY_BUS_*) bootstrap.build_app (every command that opens the queue), via load_config_from_path or EnvironmentConfigLoader The queue reaper's thresholds and broker policy ([queue.reap]), when an exhausted job is a defect ([queue.defect]), and the bus it inspects. A cluster pod has no vibey.toml in its working directory, so the chart's environment is what composes both there (ADR-0056). A malformed environment value fails the start; a ./vibey.toml that does not parse is skipped for the surfaces, but its [queue.reap] table is read strictly and malformed values fail the start rather than falling back to defaults.
./vibey.toml, [design.research] -- or, with no ./vibey.toml, the environment alone (VIBEY_DESIGN_RESEARCH_ON_UNAVAILABLE) bootstrap.build_app, handed to every worker's design.research handler What DESIGN research does when its provider can obtain no evidence ([design.research]): wait for a person (the default) or record the topic as not researched. A ./vibey.toml that does not parse is skipped, and the environment alone is read, so the policy falls back to the default -- a person waits -- unless the environment declares otherwise.
The project's stored record (the project row: max_cycles column and config JSON) vibey worker, lifecycle repository, and job handlers Cycle cap, per-cycle spend and turn caps, skills-context policy, gate commands, what an engine session may see of the environment, notification delivery, telemetry, and (in principle) the features local-engine switches — see below.
Environment variables See Environment variables Database DSN, the migration-lock wait, the local-engine switches, the sovereign DESIGN provider's evidence directory.

The project record is written once, at creation, by one of two paths:

  • vibey new (see the CLI reference): --max-cycles is stored in the project.max_cycles column; --max-cycle-dollars, --max-cycle-turns, --skills-context-mode and --skills-context-budget are stored in the config JSON as max_cycle_dollars, max_cycle_turns and skills_context (the last only when the mode is not off). When the repo contains vibey.toml, its [notifications], [telemetry], [gates], [engine_environment] and [design] tables, and [engines]'s dispatch keys, are copied into that same JSON record; --design-default-scope then overrides design.interview.default_scope.
  • The Kubernetes operator (ADR-0025): a VibeyProject spec's maxCycles sets the column (default 10); repo, maxCycleDollars, maxCycleTurns and skillsContext are stored in the config JSON, and gates and engineEnvironment as its gates and engine_environment objects (validated the same way). spec.engines is stored as a flat engines list that nothing reads back yet.

Neither path passes through parse_config. One command updates any of these values on an existing project, and only two of them: vibey budget set and vibey budget clear rewrite max_cycle_dollars and max_cycle_turns (see Per-cycle caps). No command changes the rest after creation. The operator applies its spec at creation only, so later edits to a VibeyProject's maxCycleDollars or maxCycleTurns change nothing; use vibey budget.

Per-cycle caps: max_cycle_dollars, max_cycle_turns

The budget brake's caps. They are top-level keys of the project's stored config, and that is the only place the brake reads them (LedgerBudgetSource.caps_from_config).

Key Type Unset means Set by
max_cycle_dollars number above zero no dollar cap vibey new --max-cycle-dollars, the operator's spec.maxCycleDollars, vibey budget set --max-cycle-dollars
max_cycle_turns integer above zero no turn cap vibey new --max-cycle-turns, the operator's spec.maxCycleTurns, vibey budget set --max-cycle-turns
  • Read at every BUILD session, not once when a worker starts. A cap changed while a worker runs binds its next BUILD session. A project created uncapped can be capped without a restart.
  • Uncapped is absence. vibey budget clear removes the key, leaving the config as if the cap had never been set. A key that is not a number, or a true or false, is also no cap. It is never a default.
  • Every change is on the ledger. Each set or clear that changes a cap appends one BudgetCapChanged event (field, old, new, by, account) in the same transaction as the config write. vibey budget --json reads its history back from those events. A value set at creation has no event.
  • A grant is not a cap change. Answering a budget_exhausted gate with --raw '{"max_dollars": N}' raises the cap for that one job and leaves the stored cap as it is.

Neither path writes a features key either, so for projects created today the worker finds no stored switch and each local engine sits at its default — gptossloop on, qwenloop and claudeloop-local off (ADR-0064): the VIBEY_FEATURE_* variables are the switches that reach the worker.

Environment variables

Variable Read by Effect
VIBEY_PG_URL bootstrap.database_url() (every command that opens the queue) The application role's PostgreSQL DSN (database roles). Required; there is no default — vibey exits with DatabaseNotConfigured if it is unset.
VIBEY_PG_MIGRATE_URL vibey migrate only The owner's DSN: migrations run on it, and the application role's grants are reconciled from it (database roles). Give it to that one command (VIBEY_PG_MIGRATE_URL=… vibey migrate); never export it, and nothing else reads it.
VIBEY_MIGRATION_LOCK_TIMEOUT_SECONDS bootstrap.build_app() via PostgresMigrator.from_environ (every command that opens the queue) How long a start waits for another process's migration before failing with MigrationLockTimeout, which names the backend pid holding the lock. Seconds, fractions allowed and rounded up to the next millisecond; default 300; 0 waits indefinitely. Unset or blank means the default; anything that is not a number from 0 to 2147483.647 fails the start with InvalidMigrationLockTimeout before the pool opens, rather than falling back. See the migration lock.
VIBEY_FEATURE_GPTOSSLOOP vibey worker, vibey doctor and vibey loops, through LocalEngineSettings (infrastructure/engines/local_engines.py) Overrides features.gptossloop. 1, true, yes, on (case-insensitive, surrounding whitespace ignored) enable; any other set value — 0 included — disables. When set it wins over both the stored project record and ./vibey.toml; when neither sets the switch, gptossloop is on (ADR-0064). For the worker, gptossloop on means a gptossloop adapter in the LOCAL tier, preferred first for BUILD (ADR-0038).
VIBEY_FEATURE_QWENLOOP as VIBEY_FEATURE_GPTOSSLOOP Overrides features.qwenloop, with the same values. Off when nothing sets it. Enabling it adds a qwenloop adapter — the same runner on a Qwen model — to the LOCAL tier beside gptossloop, and makes the worker and vibey doctor print a note: that qwenloop runs a Qwen model since ADR-0064.
VIBEY_LLM_WIRE_LOG vibey with -vvv sets it when unset; the sovereign DESIGN and DECOMPOSE client reads it (OllamaChatClient.from_environment); vibey llm tail reads it to find the file Path of the wire log: every request to the local model and its reply as it streams, one JSON object per line (request, chunk, response, error), redacted by the ledger's redactor and created readable by its owner alone. Set, the client asks Ollama for a streamed reply (stream: true) and writes each piece as it arrives; unset, it makes the one blocking request it always made. -vvv sets it to ~/.local/state/vibey/llm-wire.jsonl unless it is already set, so a path you chose is never overridden. Covers the sovereign providers' traffic to Ollama; a BUILD engine's own conversation is in its run directory's events.jsonl. The file is never rotated: remove it when it is large.
VIBEY_CONFORMANCE_POLL_SECONDS vibey doctor --conformance (infrastructure/engines/conformance_window.py) How long conformance waits for an engine's run directory (meta.json, events.jsonl, snapshots/latest.json) to appear before failing run_dir_shape, and with it snapshot_schema, done_marker and structured_verdict. Seconds, fractions allowed; unset or blank keeps the default of 30. A local model on ordinary hardware needs far longer than a hosted engine: raise it for the host, not for the engine. A value that is not a finite number above zero is refused with exit 2, naming the variable, before any engine is started.
VIBEY_EVIDENCE_DIR vibey work --provider gptossloop, vibey worker --provider gptossloop (the default) Directory of reading that the sovereign DESIGN provider's research stage draws from (ADR-0027). Unset, research refuses rather than inventing a source; what the research job does then is [design.research] on_unavailable — by default it parks for a person.
VIBEY_ENGINE_MAX_RUN_MINUTES vibey worker, for every build.implement and build.verify session (bootstrap._engine_run_deadline) Overrides [engines] max_run_minutes: the wall-clock limit on one BUILD engine session, in whole minutes. Set, it beats the project's stored config. A value that is not a positive whole number is ignored, never read as "no limit"; the default is 240.
VIBEY_ENGINES_MODE, VIBEY_ENGINES_OVERFLOW_AFTER_SECONDS, VIBEY_ENGINES_PAID_DAILY_CAP vibey worker (bootstrap._engine_dispatch, through EngineDispatchConfigLoader), and every whole-vibey.toml load Override [engines] mode, overflow_after_seconds and paid_daily_cap. Set, they beat the project's stored config; empty counts as unset; a value that is not valid for its key fails building that project's worker. No value of VIBEY_ENGINES_PAID_DAILY_CAP means "uncapped".
VIBEY_DESIGN_RESEARCH_ON_UNAVAILABLE bootstrap.build_app (every worker), through the environment overlay Overrides [design.research] on_unavailable: gate or record_gap, exactly. Set, it beats ./vibey.toml; empty counts as unset; any other value fails the start. How an unattended driver (scripts/triaged_delivery.py --record-research-gaps) opts a worker in without writing a file.
VIBEY_WORKFLOWS_REPOSITORY, VIBEY_WORKFLOWS_WORKFLOW, VIBEY_WORKFLOWS_REF, VIBEY_WORKFLOWS_POLL_SECONDS, VIBEY_WORKFLOWS_TIMEOUT_SECONDS vibey -w and the hub's /api/v1/workflows/runs routes (RemoteWorkflowsSettingsLoader, infrastructure/workflows/gh_workflows.py) Where a command sent to the workflows runs and how long to wait for it (ADR-0085): the repository as OWNER/NAME (default: the working directory's git remote), the workflow file (default vibey-remote.yml), the branch (default: the repository's default branch), seconds between looks (default 10) and seconds before giving up waiting (default 3600). A wait that is not a positive number is refused with exit 2; on the hub it leaves the routes answering 503.
VIBEY_STATE_REPOSITORY, VIBEY_STATE_BRANCH, VIBEY_STATE_PATH, VIBEY_STATE_PG_URL, VIBEY_STATE_KEY, VIBEY_STATE_KEY_FILE, VIBEY_STATE_CONFLICTS, VIBEY_STATE_ATTEMPTS vibey state (StateSyncSettingsLoader, infrastructure/state/settings.py) Where the sealed state lives, which database it syncs, the key, the conflict rules and the retries (State sync, ADR-0086).

Database roles

The ledger is append-only by the database (ADR-0055). Triggers refuse every UPDATE, DELETE and TRUNCATE of event and its partitions, even by the owner. The application connects as a role that could not rewrite the ledger anyway. There are two roles and two DSNs:

Role DSN Holds
The owner VIBEY_PG_MIGRATE_URL Every table. Runs migrations and reconciles grants, nothing else: vibey migrate, run by hand or as the chart's migrate init container. No other command reads this variable, so no worker, engine session or gate command ever holds the owner's DSN.
The application role VIBEY_PG_URL Exactly APP_ROLE_GRANTS (infrastructure/db/ledger_guard.py). On event that is SELECT and INSERT. It holds no DELETE or TRUNCATE anywhere, owns nothing, and is not a superuser. Every worker, CLI command, operator and KEDA scaler connects as it.

build_app() never reads the owner's DSN:

  • When the application's role may migrate (a superuser, or a member of the migration catalog's owner), it migrates as that role. This is a single-DSN install.
  • Otherwise it only verifies the schema, and refuses to start while migrations are pending (SchemaNotMigrated): run vibey migrate first.

Upgrading a single-DSN install. Existing installs connect as one role, usually a superuser. They keep running, but vibey doctor fails ledger-guard, vibey worker prints error: ledger guard NOT in force on every start, and vibey migrate exits 1, until the roles are split:

  1. Set VIBEY_PG_URL to the same server with a new role and password, for example postgresql://vibey_app:<password>@host:5432/vibey.
  2. Run VIBEY_PG_MIGRATE_URL=<the DSN you use today> vibey migrate, giving the owner's DSN to that one command rather than exporting it. It creates vibey_app (from the DSN's password) if it does not exist, grants it exactly the declared privileges, and prints ledger guard in force.
  3. Remove the owner's DSN from every other environment: a worker, an engine session or a gate command that holds it can disable the triggers.
  4. Run vibey doctor, and require scram-sha-256 for every connection (sub-doctrine 10.j, ADR-0061): never trust, peer, ident, md5 or a password in clear. The split protects the ledger only once local-auth passes. SECURITY.md §7 gives the required pg_hba.conf lines.

With the Helm chart this is postgres.appRole (default vibey_app) and, for an external database, dsn.existingSecretKey (the application's DSN) with dsn.existingSecretMigrateKey (the owner's). existingSecretMigrateKey is empty by default, which is a single-DSN install.

State sync

vibey state keeps this database and a sealed copy of it on a branch of the repository the same, in both directions (ADR-0086). Every setting is an environment variable, read by StateSyncSettingsLoader (infrastructure/state/settings.py). Surrounding whitespace is ignored, and an empty value means the default. There is no vibey.toml table: under the supervisor, set them in state_sync_env_file.

Variable Default Effect
VIBEY_STATE_REPOSITORY empty: the working directory's GitHub repository (gh repo view) The repository whose branch holds the state, as OWNER/NAME. Also the macOS keychain account the key is kept under; empty, that account is default.
VIBEY_STATE_BRANCH vibey-state The branch the sealed state lives on. It holds one file and nothing else. A name that starts with -, or holds .. or a space, is refused (exit 2).
VIBEY_STATE_PATH state.vibey The file on that branch.
VIBEY_STATE_PG_URL VIBEY_PG_URL The database to sync. The sync writes every synced table and its own watermark, state_sync, which the application role is not granted (database roles), so give it its own DSN rather than widening the one the worker holds. Neither set: exit 2.
VIBEY_STATE_KEY empty: the keychain, then VIBEY_STATE_KEY_FILE The key itself: 32 bytes as base64url, padding optional. A runner gets it from the repository's VIBEY_STATE_KEY secret.
VIBEY_STATE_KEY_FILE $XDG_CONFIG_HOME/vibey/state.key, else ~/.config/vibey/state.key Where the key is kept when there is no keychain. Refused while its group or others can read it: chmod 600 it.
VIBEY_STATE_CONFLICTS empty: the declared rules Overrides for the conflict rule of a row both ends changed differently, as table=rule,table=rule. A rule is newest, mine, theirs or refuse. An unknown table, an unknown rule, or a rule other than refuse on event, which is append-only, is refused (exit 2).
VIBEY_STATE_ATTEMPTS 5 How many times one sync starts again when the database or the branch moved under it. A whole number, at least 1.

The key is looked for in VIBEY_STATE_KEY, then the macOS keychain (service dev.vibey.state), then VIBEY_STATE_KEY_FILE. vibey state key --new makes one where it would be found first on this machine: the keychain on macOS, the key file elsewhere. It refuses while a key exists anywhere, because replacing a key makes every state sealed under it unreadable.

The declared rules (TABLES in domain/state_sync.py) are:

Rule Tables
newest, by updated_at project, job, triaged_ticket
newest, by answered_at human_gate
mine engine_health, rotation_cursor
refuse job_dependency, work_item, open_item, handoff, artifact, budget_ledger
none: append-only event. Divergence is reported, never resolved.

newest keeps the row with the later time. A missing time counts as the oldest. Equal times, a time with an offset against one without, or a row deleted at one end and changed at the other are conflicts. mine keeps this database's row and theirs the branch's, deletion included. Any conflict stops the sync, which then writes nothing at either end. schema_migration, event_seq and state_sync are never synced.

From any workflow

Any GitHub workflow can open the state, use it with vibey, and hand it back, with the composite action .github/actions/vibey-state and the reusable workflow .github/workflows/vibey-state-write.yml. The action runs scripts/vibey_state_action.py.

Whether a run may open it at all is decided by one gate (StateGate). It needs the VIBEY_STATE_KEY secret, and either a private repository or a public one that declares so in .github/vibey-state.toml:

[state]
public = true

Without the file, or with public = false, a public repository never decrypts the state: open says why, opens nothing, and the job goes on. Any other table or key, or a public that is not true or false, is refused by name (exit 2). On a public repository, whatever a workflow prints from the decrypted state is public. This repository declares public = true; its operator accepted that (ADR-0086).

Step What it does Outputs
open Gives the job a database: pg-url (an owner's DSN, as from services: postgres), or else the runner's preinstalled PostgreSQL, started for the job. Migrates it, fills it with vibey state sync --no-push, and sets VIBEY_PG_URL (the application role) and VIBEY_STATE_PG_URL (the owner) for the job's later steps. opened
close Exports the state, sealed, and uploads it as an artifact unique to the run and job (artifact names another), kept one day. With push: "true", in a job that already holds contents: write, syncs the branch directly instead. state, artifact, path, pushed
write-back What the reusable workflow runs: a fresh database, vibey state import, then vibey state sync. none

The action's inputs are step, key (pass ${{ secrets.VIBEY_STATE_KEY }}), token (default github.token), pg-url, app-pg-url (default: derived from pg-url), push, file, artifact and python-version (3.12). They reach the script through env:, and every state command runs with the system basics, VIBEY_STATE_*, the key, the repository and the token, never the runner's whole environment. A workflow that keeps its own jobs read-only lets the reusable workflow, whose one job holds contents: write, write the state back:

jobs:
  work:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      state: ${{ steps.close.outputs.state }}
      artifact: ${{ steps.close.outputs.artifact }}
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with:
          persist-credentials: false
      - uses: ./.github/actions/vibey-state
        with:
          step: open
          key: ${{ secrets.VIBEY_STATE_KEY }}
      - run: vibey status            # reads and writes the restored database
      - id: close
        uses: ./.github/actions/vibey-state
        with:
          step: close
          key: ${{ secrets.VIBEY_STATE_KEY }}

  write:
    needs: work
    if: needs.work.outputs.state == 'true'
    permissions:
      contents: write
    uses: ./.github/workflows/vibey-state-write.yml
    with:
      artifact: ${{ needs.work.outputs.artifact }}
    secrets:
      VIBEY_STATE_KEY: ${{ secrets.VIBEY_STATE_KEY }}

vibey -w (vibey-remote.yml) uses the same gate and commands, and its sync-back job calls the same reusable workflow. The AI-driven daily lanes do not open the state; any of them may, by calling the action.

Operational surface environment variable overlay

Every operational surface setting can be configured or overridden via environment variables. An environment variable always takes precedence over the corresponding key in vibey.toml. An empty or whitespace-only value counts as unset.

Variable Target Table & Key Description
VIBEY_TRACKER_URL [tracker].url Plane API endpoint URL
VIBEY_TRACKER_TOKEN [tracker].token Plane user or API token
VIBEY_TRACKER_WORKSPACE_SLUG [tracker].workspace_slug Plane workspace slug
VIBEY_TRACKER_PROJECT_ID [tracker].project_id Plane project UUID
VIBEY_DOCS_URL [docs].url BookStack base URL
VIBEY_DOCS_TOKEN_ID [docs].token_id BookStack API token ID
VIBEY_DOCS_TOKEN_SECRET [docs].token_secret BookStack API token secret
VIBEY_DOCS_BOOK_ID [docs].book_id BookStack book integer ID
VIBEY_SECRETS_URL [secrets].url OpenBao server URL
VIBEY_SECRETS_TOKEN [secrets].token OpenBao access token
VIBEY_FILES_URL [files].url Nextcloud WebDAV URL
VIBEY_FILES_USER [files].user Nextcloud WebDAV username
VIBEY_FILES_PASSWORD [files].password Nextcloud WebDAV password
VIBEY_EMAIL_SMTP_HOST [email].smtp_host Postfix/SMTP host
VIBEY_EMAIL_SMTP_PORT [email].smtp_port SMTP port (e.g. 587 or 465)
VIBEY_EMAIL_USERNAME [email].username SMTP username
VIBEY_EMAIL_PASSWORD [email].password SMTP password
VIBEY_EMAIL_FROM [email].from_email Sender address
VIBEY_SMS_URL [sms].url Kannel sendsms gateway URL
VIBEY_SMS_USERNAME [sms].username Kannel sendsms username
VIBEY_SMS_PASSWORD [sms].password Kannel sendsms password
VIBEY_SMS_SENDER [sms].sender SMS sender ID/number
VIBEY_MESSAGING_URL [messaging].url Matrix Synapse URL
VIBEY_MESSAGING_TOKEN [messaging].token Matrix access token
VIBEY_MESSAGING_ROOM_ID [messaging].room_id Matrix target room ID
VIBEY_CONFIG_STORE_URL [config_store].url Infisical URL
VIBEY_CONFIG_STORE_TOKEN [config_store].token Infisical token
VIBEY_CONFIG_STORE_PROJECT_ID [config_store].project_id Infisical project UUID
VIBEY_CONFIG_STORE_ENVIRONMENT [config_store].environment Infisical environment slug
VIBEY_CACHE_URL [cache].url Redis URL (redis://...)
VIBEY_BUS_URL [bus].url RabbitMQ Management URL
VIBEY_BUS_USERNAME [bus].username RabbitMQ username
VIBEY_BUS_PASSWORD [bus].password RabbitMQ password
VIBEY_BUS_VHOST [bus].vhost RabbitMQ vhost the bus and the reaper are scoped to
VIBEY_BLOB_URL [blob].url Garage S3 URL
VIBEY_BLOB_ACCESS_KEY [blob].access_key Garage S3 access key
VIBEY_BLOB_SECRET_KEY [blob].secret_key Garage S3 secret key
VIBEY_BLOB_REGION [blob].region Garage S3 region
VIBEY_SIEM_URL [siem].url Wazuh indexer endpoint
VIBEY_SIEM_USERNAME [siem].username Wazuh indexer username
VIBEY_SIEM_PASSWORD [siem].password Wazuh indexer password
VIBEY_SIEM_INDEX [siem].index Wazuh index name
VIBEY_SURFACES_ADAPTER_TIMEOUT_SECONDS [surfaces].adapter_timeout_seconds Seconds each surface adapter waits on one network call

The queue reaper's keys have the same overlay, and the same precedence ([queue.reap]). A boolean takes 1, true, yes or on and 0, false, no or off, in any case; anything else fails the start, naming the variable.

Variable Target Table & Key
VIBEY_QUEUE_REAP_ENABLED [queue.reap].enabled
VIBEY_QUEUE_REAP_INTERVAL_SECONDS [queue.reap].interval_seconds
VIBEY_QUEUE_REAP_LEASE_GRACE_SECONDS [queue.reap].lease_grace_seconds
VIBEY_QUEUE_REAP_STALE_READY_SECONDS [queue.reap].stale_ready_seconds
VIBEY_QUEUE_REAP_DEAD_LETTER_MIN_DEPTH [queue.reap].dead_letter_min_depth
VIBEY_QUEUE_REAP_DEAD_LETTER_PEEK_LIMIT [queue.reap].dead_letter_peek_limit
VIBEY_QUEUE_REAP_OWNED_QUEUE_PATTERN [queue.reap].owned_queue_pattern
VIBEY_QUEUE_REAP_DEAD_LETTER_QUEUE_PATTERN [queue.reap].dead_letter_queue_pattern
VIBEY_QUEUE_REAP_POLICY_NAME [queue.reap].policy_name
VIBEY_QUEUE_REAP_POLICY_PRIORITY [queue.reap].policy_priority
VIBEY_QUEUE_REAP_CONSUMER_TIMEOUT_SECONDS [queue.reap].consumer_timeout_seconds
VIBEY_QUEUE_REAP_DELIVERY_LIMIT [queue.reap].delivery_limit
VIBEY_QUEUE_DEFECT_IDENTICAL_FAILURES [queue.defect].identical_failures
VIBEY_DESIGN_RESEARCH_ON_UNAVAILABLE [design.research].on_unavailable
VIBEY_TELEMETRY_MAX_SPANS [telemetry].max_spans
VIBEY_TELEMETRY_MAX_SAMPLES [telemetry].max_samples

Schema semantics

domain/config.py is a pure, stdlib-only module with no filesystem access of its own (reading the file is an infrastructure concern). Every table below is optional; omit any of them and the listed defaults apply.

Validation is partial. The Type column is the intended shape. parse_config type-checks every key except [budget].*, [phases.*].engines (not even checked to be a list — a bare string is split into single-character engine ids), [phases.*].parallelism, the elements of [isolation].egress and [provision].plugins, and the values of [engines].weights; those are stored as written. No numeric range is checked outside [qwenloop] (idle_timeout_seconds ≥ 0, startup_timeout_seconds > 0, context_window > 0), booleans pass integer checks (max_cycles = true is accepted), and [deploy].target / [deploy].iac accept any string.

Engine names: an unknown engine name in [engines].enabled or as a key of [engines].weights fails validation with a ConfigError naming the offending path, as does a local engine in [engines].enabled or any [phases.*].engines list while its switch is off: qwenloop before features.qwenloop = true (the message adds that qwenloop runs a Qwen model since ADR-0064 and that the gpt-oss engine it used to be is gptossloop), claudeloop-local before features.claudeloop_local = true, and gptossloop while features.gptossloop = false. [phases.*].engines entries are otherwise not validated — unknown names and engines outside [engines].enabled are accepted as written — and [engines].weights may name a local engine whose switch is off. Unknown tables (for example [skills_context]) are silently ignored. All of this applies only when something calls load_config_from_path/parse_config, which nothing in this codebase does outside tests.

[project]

Field Type Default Notes
name string required Project name.
repo string "." Path to the repository this project builds against.
max_cycles integer 10 Cap on delivery cycles. Not range-checked. The live value is the project.max_cycles column set by vibey new --max-cycles or spec.maxCycles.
strict_loopback boolean false When true, tightens review-loopback routing (ADR-0010).
[project]
name = "my-app"
repo = "~/src/my-app"

[isolation]

Field Type Default Notes
level string "worktree" One of worktree, container, vm.
allow_push boolean false Whether a worktree may push to a remote.
egress array of strings [] Allowed egress destinations when network isolation is otherwise closed. Element types are not validated.
[isolation]
level = "worktree"
allow_push = true
egress = ["github.com"]

worktree isolation is the one active runtime behavior today: every job and phase runs in an isolated ephemeral git worktree. container and vm are schema-valid values, and the container hardening path (infrastructure/container/config.py's ContainerConfig, infrastructure/container/runtime.py's OciContainerExecutor) is implemented and unit-tested, but it is never constructed by bootstrap.py or anything else outside infrastructure/container/ and its tests, and [isolation] itself is never read from a real vibey.toml. Setting level = "container" has no effect today; see SECURITY.md for the same disclosure.

[budget]

Field Type Default Notes
max_dollars_per_cycle float or unset unset (no cap) Intended per-cycle spend cap. Not validated.
max_dollars_total float or unset unset (no cap) Intended total spend cap across the project's lifetime. Not validated.
max_turns_per_item integer or unset unset (no cap) Intended per-work-item turn cap; the implemented cap (max_cycle_turns) is per cycle. Not validated.
ultra_no_cap boolean false Written by vibey budget no-cap and vibey budget cap as the record of the no-cap declaration for ULTRA runs (ADR-0063). The worker reads the trusted UltraNoCapChanged ledger event, never this key: a true here with no event behind it declares nothing.
[budget]
max_dollars_per_cycle = 15.0
max_dollars_total = 250.0

None of these keys is read at runtime. The live brake is the project's stored max_cycle_dollars / max_cycle_turns (set by vibey new --max-cycle-dollars / --max-cycle-turns or the operator's spec.maxCycleDollars / spec.maxCycleTurns, and changed afterwards by vibey budget set / clear; see Per-cycle caps). LedgerBudgetSource reads the caps at every BUILD session and sums the spend live from the current cycle's TurnCompleted and BudgetSpent ledger events, never estimating it ahead of time. Tripping either cap parks a budget_exhausted gate. With neither set, spend is uncapped.

vibey cost and vibey budget show those stored caps and that ledger sum. A budget key in the stored project config is not read by either.

[verify]

Field Type Default Notes
require_independent_review bool false When true, build.verify always fails as VIBEY if the reviewing engine is the implementer, even when the configured pool has nobody else. When false (the default) a pool that cannot supply a second reviewer gets a self-review, recorded as a DecisionRecorded in the ledger so the weakened independence is visible. See ADR-0035.
[verify]
require_independent_review = true

Unlike [budget] above, this key is read at runtime, by bootstrap.build_full_worker.

[publish]

What happens to a finished project's work after REVIEW. Nothing leaves the machine unless mode says so: the default finishes locally, as it always has. The guide walks through it.

Field Type Default Notes
mode "off", "interview" or "auto" "off" off finishes locally. interview raises a publish_interview gate that shows exactly what would be pushed and where, and waits for you (or for a worker started with --auto-publish). auto publishes the proposal with no gate.
visibility "private" or "public" "private" For a new GitHub repository. A public repository is always an explicit choice, here or in the interview.
draft bool true Whether an opened pull request is a draft.
base string the remote's default branch, else main The branch a pull request targets, or the branch a new repository's first push becomes.
remote string "origin" The git remote to push to.
owner string the logged-in gh user The user or organisation a new repository is created under.
[publish]
mode = "interview"
visibility = "private"
draft = true
base = "develop"
remote = "origin"
owner = "my-org"

Names are checked: a base, remote or owner that could be read as a command-line option (leading -) or holds a space or shell character is refused, as is an unknown key (visibilty), and the project fails to start rather than publishing with a setting nobody meant. The table is read from the project's stored configuration, the same way [verify] is.

[engines]

Field Type Default Notes
enabled array of strings ["gptossloop"], plus every other local engine whose switch is on Must be a subset of the known engines below. gptossloop, the sovereign default (sub-doctrine 8.b, DEFAULT_ENGINES), is on without declaration: an explicit list that leaves it out is extended with it. It leaves the pool only by features.gptossloop = false (ADR-0064). If the list is omitted, each switched-on local engine (qwenloop, claudeloop-local) is appended too.
max_run_minutes positive integer 240 The wall-clock limit on one BUILD engine session (build.implement or build.verify). Past it the session is stopped and the job fails as ENGINE: three in a row open that engine's circuit so the retry rotates away, and the job's bounded attempts still end in a park, never a loop. A hung session used to keep its job's lease alive forever. There is no "off": a value that is not a positive whole number is ignored. Read by the worker from the project's stored config, into which vibey new copies only [engines]'s dispatch keys, so set it with VIBEY_ENGINE_MAX_RUN_MINUTES unless your project record was written another way.
weights table of string→int {} Per-engine weight for smooth weighted round robin (ADR-0005). Keys must be known engines; values are not validated.
mode "singleton", "hybrid" or "auto" "auto" How BUILD jobs are dispatched across the tiers (hybrid engine dispatch, ADR-0079). VIBEY_ENGINES_MODE overrides it.
overflow_after_seconds integer ≥ 0 600 How long a BUILD job must have been held for a local slot before a paid engine may take it as overflow. VIBEY_ENGINES_OVERFLOW_AFTER_SECONDS overrides it.
paid_daily_cap integer ≥ 0 10 The most paid overflows one project may take per UTC day, counted from the ledger. 0 means no overflow; there is no value that means "uncapped" (sub-doctrine 8.b). VIBEY_ENGINES_PAID_DAILY_CAP overrides it.
slot_poll_seconds integer ≥ 1 30 How soon a job held for a local slot is looked at again (never later than its remaining wait).
auto_window_hours integer ≥ 1 168 How much of the project's BUILD history auto measures.
auto_min_sessions integer ≥ 1 20 Fewer local sessions than this in the window and auto chooses singleton.
auto_min_contention number 0–1 0.25 The share of local sessions that must have found every local slot occupied before auto may choose hybrid.
auto_max_age_hours integer ≥ 1 24 How long a recorded measurement decides before it is taken again.
[engines]
enabled = ["claudeloop", "codexloop"]
max_run_minutes = 120
mode = "hybrid"
paid_daily_cap = 5

[engines.weights]
claudeloop = 3
codexloop = 1

Known engine ids: claudeloop, codexloop, gptossloop (valid unless features.gptossloop = false), qwenloop (valid in enabled and [phases.*].engines only once features.qwenloop = true), and claudeloop-local (only once features.claudeloop_local = true). opencode is no longer an engine: its engine and runner were deleted, and a vibey.toml that names it is refused as an unknown engine. cursorloop and agyloop were retired by ADR-0078: a vibey.toml that names either — in [engines], [phases.*].engines or [engine_environment] — is refused with an error that names the ADR.

[engines.slots]

Field Type Default Notes
engine id integer ≥ 1 1 for a local engine, 2 for a paid one The engine's concurrent slots under hybrid: how many leased BUILD jobs it may hold at once. Keys must be known engines. One is what a default Ollama serves; declare more where the host measured it can carry them (sub-doctrine 8.j).

Hybrid engine dispatch

Sub-doctrine 8.a makes the local tier the preference, and under singleton that is all selection knows: any local engine that can take a job gets it, however many jobs it is already running, and BUILD queues behind the one local model slot. hybrid adds one narrow path, sub-doctrine 8.k's overflow (ADR-0079):

  1. A local engine with a free slot always takes the job.
  2. When every eligible local slot is occupied and a paid engine with a free slot exists and the cap allows one, a job that has waited less than overflow_after_seconds is held in the queue (a capacity defer: no attempt is spent) and looked at again after slot_poll_seconds. Its first hold on each attempt is an EngineSlotWaitStarted ledger event.
  3. Once it has waited long enough, a paid engine takes it as overflow, reserved against paid_daily_cap under the project row's lock and recorded as an EngineOverflowSelected event naming the occupied slots, the wait and what is left of the cap.
  4. When overflow cannot happen — no paid engine free, or the cap reached — the job is assigned to local and waits there, exactly as under singleton.

Slots in use are the queue's unexpired leases with an assigned engine, across every project. auto (the default) chooses hybrid only from an EngineDispatchMeasured event: over the last auto_window_hours, at least auto_min_sessions local BUILD sessions, at least auto_min_contention of them begun with every local slot occupied, and a median wait for a slot of at least overflow_after_seconds. A measurement for other engines, slots, thresholds or vibey version, or older than auto_max_age_hours, is taken again; one that cannot be taken means singleton. vibey ledger shows every measurement, hold and overflow.

The worker reads these keys from the project's stored config: vibey new copies [engines]'s dispatch keys (and only those) from vibey.toml, and the VIBEY_ENGINES_* variables overlay them.

[engines.claudeloop_local]

Which claudeloop backend profile the claudeloop-local engine runs, and what vibey may claim about it (ADR-0038, ClaudeloopLocalConfig). The engine itself is switched on by features.claudeloop_local; this table only describes it.

Field Type Default Notes
profile string "local" Names a [profiles.NAME] table in claudeloop's own config; that profile, not vibey, carries the local server's base_url and the model tiers. Must not be empty. VIBEY_CLAUDELOOP_LOCAL_PROFILE, when set and non-empty, overrides it.
context_window integer 32768 The engine descriptor's context window. Must be a positive integer. Keep it equal to the profile's own context_window and the server's OLLAMA_CONTEXT_LENGTH.
structured_verdict boolean false Claims the structured-verdict capability. A local model earns it only when vibey doctor --conformance proves it for the model the profile configures; a claim conformance cannot prove makes the engine ineligible rather than trusted.

vibey doctor and vibey loops read this table from ./vibey.toml, as they read [features]; a malformed table makes vibey loops exit 3. The worker reads it from the project's stored config, which no creation path writes today (vibey new copies only the tables listed above, and the Kubernetes operator stores engines as a flat list, which is read as "not configured"), so a worker runs claudeloop-local on these defaults unless VIBEY_CLAUDELOOP_LOCAL_PROFILE names another profile. The local models guide shows a working profile.

[phases.design], [phases.build], [phases.review]

Each phase table accepts the same three fields:

Field Type Default (per phase) Notes
effort string design = high, build = low, review = high One of trivial, low, standard, high, max.
engines array of strings or unset unset (falls back to [engines].enabled) Engine ids this phase may use. Not validated against known engines or [engines].enabled, and not checked to be a list; only a retired engine id is refused (see above).
parallelism integer or unset unset Per-phase worker concurrency override. Not validated.
[phases.build]
effort = "standard"
engines = ["claudeloop", "codexloop"]
parallelism = 4

[design.interview]

How the DESIGN interview declares each question's default. vibey new copies the [design] table into the project record, and the design.interview handler reads it from there (DesignConfig.from_data) every time it records a question batch.

Field Type Default Notes
default_scope "narrowest" or "model" "narrowest" Whose appetite a declared default follows. Any other value is refused by vibey new; a malformed value in a stored record fails the interview job rather than reading as narrowest. vibey new --design-default-scope overrides the file.

A default is what vibey answer ID --defaults takes, and what an unanswered non-blocking question becomes as an assumption. When the model writes both a question and its default, accepting defaults accepts the model's appetite: live on issue #998, a pure README insertion answered "Yes" to "Should we add a unit test ... / commit the generation script / run it in the existing CI pipeline?" and grew a CI change that the delegated approver may never approve.

  • narrowest — a yes/no question (it opens with should, do, will, is ...) that proposes to add, also, include, extend, enforce, automate, introduce, create, commit, integrate, provide or run something as part of another thing, and names an artefact the intake does not name — a test, CI, pipeline, workflow, script, hook, lint, check, comment, badge, changelog, generator, tool, automation, configuration, dependency or Makefile target — has an affirmative default rewritten to No (the intake does not ask for this; out of scope). A default that already declines is left alone, and so is every question that proposes no new artefact: "Should the anchor generation follow GitHub's algorithm exactly?" is about how to do what was asked, and "Do we need to include sub-headings?" is about the granularity of the requested table of contents, not a new thing beside it. The intake is the issue text that seeded the ledger (vibey new --intake). Both providers are also asked, in their prompt, for the narrowest default at source (QUESTION_DEFAULT_CONTRACT); the rewrite is the deterministic half, since a model can ignore prose.
  • model — the model's default is recorded as the model wrote it.

A rewritten default is recorded, never silent: the QuestionAsked payload carries the narrowed default, the model's own as model_default, and a one-sentence default_reason naming the artefacts beyond the intake; the gate prompt shows [default: No (...); narrowed from the model's 'Yes']. A person who answers a question explicitly is never overridden. The triaged-delivery bridge creates every project with --design-default-scope narrowest, whatever the repository declares: accepting defaults unattended is bounded only under it.

[design.interview]
default_scope = "narrowest"

[provision]

Field Type Default Notes
plugins array of strings [] Reserved for agent-surface provisioning plugins (ADR-0011). Parsed and stored only; infrastructure/provision/agent_surface.py does not read it today.
[provision]
plugins = []

[deploy]

Field Type Default Notes
enabled boolean false Opts the project into the DEPLOY_DESIGN / DEPLOY_EXECUTE / DEPLOY_REVIEW stage set.
target string "azure" Deployment target. Azure is the only implemented target today; other strings are accepted by parse_config.
iac string "bicep" Infrastructure-as-code format used by the deploy adapters; other strings are accepted by parse_config.
[deploy]
enabled = true
target = "azure"
iac = "bicep"

[notifications]

Notifications are opt-in because desktop alerts and outbound webhooks are side effects, and no channel reaches a person with zero configuration: a worker in a pod or under launchd has no desktop to show an alert on, and a webhook needs a public destination. So enabled stays false by default. vibey new copies this table into the project's stored config; build_app() constructs one service and applies the project policy when a worker raises a gate or a project changes phase.

Whether anyone was told is recorded, never assumed. Every notice about a gate -- notice 0 when it is raised, reminder 1, 2, ... while it waits -- ends in exactly one ledger event: GateNotified when at least one channel took it (with what each channel reported), or GateNoticeUndeliverable with the reason -- disabled, no_channel (enabled with desktop off and no webhook), config_invalid, unwired (a process composed without a sink) or failed -- and a gate.notice_undeliverable warning. A project nobody can hear is said once per gate, at its raise notice, and not reminded about. One event per gate and notice number, fleet-wide: a replayed sweep records nothing. Both kinds are withheld from ledger publication.

A gate still waiting is reminded about. An idle vibey worker sweeps its project's open gates at most every sweep_interval_seconds; vibey gates --remind runs the same sweep on demand, so a supervisor can schedule it where no worker idles (CLI reference). A gate with no raise notice on record -- raised before this release, or by the queue reaper -- gets one; a gate open remind_after_seconds after it was raised gets a reminder every remind_every_seconds, at most max_reminders times. A sweep that was down sends the one reminder now due, not every one it missed. vibey doctor reports how many open gates are in projects nobody will be told about (WARN gate-notices ... gates waiting, nobody will be told).

Field Type Default Notes
enabled boolean false Enables delivery for this project.
desktop boolean true Sends desktop alerts when enabled.
webhooks array of tables [] Each table requires url and may include a secret; URLs must be public http:// or https:// destinations at publish time (loopback, private, link-local, reserved, local-name, credential-bearing, and redirecting endpoints are rejected). Secrets sign the JSON payload with X-Vibey-Signature: sha256=.... The payload names the gate, its kind, its job and the notice number -- never the gate's prompt.
remind_after_seconds int ≥ 1 86400 How long a gate waits before its first reminder: a day.
remind_every_seconds int ≥ 1 86400 The time between reminders after the first.
max_reminders int ≥ 0 7 The most reminders one gate gets -- a week of daily ones at the defaults. 0: none; the raise notice is still sent and recorded.
sweep_interval_seconds int ≥ 1 300 How often an idle worker looks for due reminders. Read from ./vibey.toml by build_app, like [queue.reap], since the sweep is the process's; the other keys are read from each project's stored config.
[notifications]
enabled = true
desktop = true
remind_after_seconds = 43200   # first reminder after 12 hours
max_reminders = 4

[[notifications.webhooks]]
url = "https://ops.example/vibey"
secret = "replace-me"

[telemetry]

build_app() constructs the in-process tracer and metrics recorder. Workers record job spans, queue latency, phase duration, engine selections, engine turns, handoff-gate failures, and cost spend. The recorder is available to the running application but has no external exporter yet.

Field Type Default Notes
enabled boolean true Set false to disable runtime tracing and metrics for this project.
export_path string or unset unset Reserved for a future file/exporter integration; it is validated and stored.
max_spans integer 10000 How many finished spans the in-process tracer keeps. Only the newest are kept; the oldest are dropped first. Must be at least 1. VIBEY_TELEMETRY_MAX_SPANS overrides it.
max_samples integer 1000 How many latency or duration samples the metrics recorder keeps per series (queue latency per job kind, phase duration per phase). Only the newest are kept. Must be at least 1. Counters (engine selections, handoff-gate failures, cost spend) are not capped. VIBEY_TELEMETRY_MAX_SAMPLES overrides it.
[telemetry]
enabled = true
export_path = "~/.local/state/vibey/telemetry.jsonl"
max_spans = 10000
max_samples = 1000

[features]

Field Type Default Notes
gptossloop boolean true The sovereign default engine: the local runner on GPT-OSS 20B (ADR-0064). On unless set false; false removes it from the default pool and refuses it in [engines].enabled and [phases.*].engines.
qwenloop boolean false The same runner on a Qwen model (qwen3:14b unless QWENLOOP_MODEL or its own config names another). Must be true before qwenloop can appear in [engines].enabled or any [phases.*].engines list. Before ADR-0064 this switch turned on the engine that ran gpt-oss:20b; that engine is now gptossloop, on by default.
claudeloop_local boolean false The claudeloop binary on a local backend profile ([engines.claudeloop_local]). Must be true before claudeloop-local can be requested.
[features]
qwenloop = true
claudeloop_local = false

Runtime: vibey doctor and vibey loops read these keys from ./vibey.toml. vibey worker reads them from the project's stored config record, which vibey new and the operator never write, so for the worker the VIBEY_FEATURE_GPTOSSLOOP, VIBEY_FEATURE_QWENLOOP and VIBEY_FEATURE_CLAUDELOOP_LOCAL variables are currently the only switches; each overrides both. With none set, the worker runs gptossloop and no other local engine.

[qwenloop]

Describes the local runner package that both gptossloop and qwenloop run (the table keeps its historical name). In engine-driven BUILD rotation the local engines form the LOCAL tier, preferred first (ADR-0038, amending the standby of ADR-0015). The sovereign DESIGN and DECOMPOSE providers are gptossloop's, the default for vibey work and vibey worker (ADR-0027, ADR-0064). These keys mirror the runner's own QwenConfig (src/vibey_runners/qwen/src/qwenloop/domain/config.py, which additionally has max_turns, default 40, must be positive); parse_config validates them into VibeyConfig, but nothing passes them to the runner.

Field Type Default Notes
backend string "auto" One of auto, llama.cpp, vllm.
portable_profile string "qwen2.5-coder-14b-q5-k-m" Model profile used on the portable (CPU/quantized) backend path.
nvidia_profile string "qwen2.5-coder-14b-bf16" Model profile used when an NVIDIA GPU backend is selected.
idle_timeout_seconds integer 900 Must be non-negative; how long an idle local model stays warm.
startup_timeout_seconds integer 180 Must be positive.
context_window integer 32768 Must be positive.

Shared model turns

The runner keeps direct dispatch as the default. To let multiple lanes share one model server, opt in explicitly:

[qwenloop]
turn_dispatch_mode = "rabbitmq"
turn_queue_url = "amqp://user:password@broker/vhost"
turn_queue_name = "vibey.llm.turns"

turn_queue_url is sensitive connection material and should be supplied through operator-controlled configuration, never committed or printed in logs. Start qwenloop server turn-worker (or gptossloop server turn-worker) for the model host. turn_queue_name defaults to vibey.llm.turns and permits separate model pools to use distinct durable queues. TurnDispatcherInterface is the runner seam: DirectTurnDispatcher preserves local operation, HybridTurnMultiplexer shares one resident server among bounded in-process lanes, and RabbitMqTurnDispatcher publishes correlated requests to the worker.

[failover]

The driver's failover and handback (ADR-0070), read by vibey driver from the worktree's vibey.toml (or --config). Every key is optional. An unknown key or a wrong type is refused by name.

Field Type Default Notes
enabled boolean true false plans no failover; the hook then records nothing.
target_engine string "gptossloop" The engine the work moves to.
target_effort string "ULTRA" An Effort name (ADR-0063).
probe_interval_seconds integer 1800 How long after a failover the first probe may run, and the timer's period. A window's known reset time replaces the first; credits never have one.
sovereign_argv list of strings ["gptossloop", "run", "{brief}", "--run-id", "{run_id}", "--cwd", "{cwd}"] Started detached on failover.
sovereign_wind_down_argv list of strings ["gptossloop", "wind-down", "{run_id}", "--cwd", "{cwd}"] Run before handback.
probe_argv list of strings ["claude", "-p", "Reply with the single word OK.", "--output-format", "json", "--max-turns", "1"] Success is exit 0 with a JSON result whose is_error is false.
resume_argv list of strings ["claude", "-p", "--resume", "{session_id}", "{prompt}"] Started detached on handback.
[failover]
enabled = true
target_engine = "gptossloop"
target_effort = "ULTRA"
probe_interval_seconds = 900

The argv templates fill {brief}, {cwd}, {run_id}, {session_id} and {prompt}; any other text is passed through unchanged.

[supervisor]

What vibey supervisor install renders, and what vibey supervisor status and vibey doctor ask about (#1189). Read from <repo>/vibey.toml by install (or --config) and from ./vibey.toml by status and doctor. Every key is optional; an unknown key or a wrong type is refused by name.

Field Type Default Notes
log_dir string ~/Library/Logs/vibey (macOS), $XDG_STATE_HOME/vibey/logs else ~/.local/state/vibey/logs (Linux) Each service appends to <log_dir>/worker.log, delivery.log or state-sync.log. Refused on volatile storage or in a linked worktree.
env_file string ~/Library/Application Support/vibey/supervisor.env (macOS), $XDG_CONFIG_HOME/vibey/supervisor.env else ~/.config/vibey/supervisor.env (Linux) The environment the worker and the delivery bridge start with, through vibey supervisor exec. Created from a commented template (mode 0600) when missing. The state sync has its own (state_sync_env_file).
vibey string vibey on PATH The vibey each unit runs.
python string the interpreter running vibey supervisor install The Python that runs the delivery bridge.
delivery boolean true false supervises the worker alone.
delivery_interval_seconds integer 300 The bridge's --interval; at least 1.
delivery_args list of strings [] Appended to the bridge's command, e.g. ["--answer-design-defaults"].
worker_args list of strings [] Appended to vibey worker --all-projects, e.g. ["-j", "2", "--provider", "gptossloop"].
restart_seconds integer 30 How long after a failed exit the service manager restarts a service (launchd ThrottleInterval, systemd RestartSec).
label_prefix string "dev.vibey" Units are <label_prefix>.worker and <label_prefix>.delivery, and <label_prefix>.state-sync with state_sync = true.
required boolean false true turns vibey doctor's WARN for a missing or stopped service into a FAIL.
state_sync boolean false true adds a third service, state-sync, which runs vibey state sync --every <state_sync_interval_seconds> (State sync, ADR-0086). Off unless declared.
state_sync_interval_seconds integer 300 Seconds between the service's syncs; at least 1 when state_sync is on.
state_sync_env_file string state-sync.env in the directory of env_file's platform default The state sync's own environment, read by vibey supervisor exec: set VIBEY_STATE_PG_URL and the other VIBEY_STATE_* variables here. Created from a commented template (mode 0600) when missing. Never the worker's file: the sync's DSN writes every synced table, more than the application role may (ADR-0055). Refused on volatile storage or in a linked worktree.
state_sync_args list of strings [] Appended to vibey state sync --every N, e.g. ["--no-push"].
[supervisor]
delivery = true
delivery_interval_seconds = 600
worker_args = ["-j", "2", "--provider", "gptossloop"]
restart_seconds = 30
required = true
state_sync = false

[hub]

The hub, vibey serve (ADR-0068). Every key is optional, and the defaults are the closed ones: with no [hub] table the hub listens on loopback only. An unknown key, or a key of the wrong type, is refused rather than ignored, so a misspelt lan can never silently mean "not declared".

Key Default Meaning
lan false true declares that vibey serve --host <LAN address> may listen off loopback. Without it such an address is refused (exit 2), and vibey doctor fails a hub found listening on one.
port 8765 The port vibey serve listens on when --port is not given.
names [] Extra names a request may carry in Host when the LAN is declared, e.g. "studio.local". The computer's own host name, its .local name and its addresses are always admitted then.
state_dir the platform state directory + /hub Where the host token (token, 0600), the scoped agent token (agent.token, 0600; ADR-0100), the key workflows request ids are bound to their starters under (run.key, 0600; ADR-0085) and the runtime record (serving.json) live.
lane_roots [] (the directory vibey serve runs in) Where /api/v1/lanes looks for lanes: each root and every directory directly inside it.
projects_root "" (nowhere) The one folder a paired device holding run may start projects in: each project's folder is <projects_root>/<name>, made by the host (ADR-0091). Absolute, or starting with ~. With none declared a device can pause, resume and unwind a project but cannot start one, and is told so. A device never names a path.
agent_scopes ["view", "answer", "run"] Scopes granted to the agent principal (<state_dir>/agent.token) used by vibey mcp when a frontier LLM or IDE connects over MCP (ADR-0100). Never includes spend, bump or workflows by default, so a host model cannot answer a spending gate (budget_exhausted, deploy_*). Set agent_scopes = [] to disable agent token authentication entirely.
[hub]
lan = true
port = 8765
names = ["studio.local"]

A bump from the hub is requested as the queue source vibey-hub; it lands only when [queue.priority] sources names it.

[queue.priority]

Who besides the operator may move a job to the front of the queue (ADR-0054, sub-doctrines 12.h and 12.j). Read from the vibey.toml at the root of the project's own repository — the reviewed file — and never from the current directory or a path given on the command line.

Field Type Default Notes
sources list of strings [] The automations admitted to vibey queue bump / unbump (and --priority), each exactly as it names itself with --source. Matched exactly, case included. Each entry must be a non-blank string, may not repeat, and may not be operator.

The operator is the account that owns this file (or the repository root, when there is no file), checked by the process's uid — not a name. A declared source is admitted only when it also runs as that account: a name is not a credential. Empty — the default — admits the operator alone: the absence of a grant is refusal (12.f). Every other request is refused and recorded on the ledger as JobPriorityRefused. Declare a source only for an automation that decides for itself; a source that relays other people's words (a label anyone may set, an issue comment) admits everyone who can reach it.

[queue.priority]
sources = ["storm"]

[queue.reap]

When queued or held work counts as stuck, and what bounds it (ADR-0056). The same thresholds judge the PostgreSQL job queue and every queue on the broker, so both backends reap identically. The worker runs the reaper when idle; vibey queue reap runs it on demand (CLI reference). Every key must have the type of its default -- true is not a number -- and an unknown key is refused.

Field Type Default Notes
enabled bool true Whether the worker runs the reaper's own pass (claimable-work and broker sightings, dead letters) on its own. vibey queue reap runs regardless. The lease reap -- JobRepository.reap(), in every idle worker iteration -- is not switched off: it is the job queue's crash recovery, and without it every dead worker's job would stay leased forever; its bound, parking a job whose attempts are spent, comes with it.
interval_seconds int ≥ 1 60 The most often one worker runs a pass beyond the lease reap.
lease_grace_seconds int, 0–86,400 0 How far past lease_expires_at a lease may run before it is reaped. The lease is already the heartbeat's bound (renewed every third of it), so the default adds nothing. Condition (a)/(b) on PostgreSQL.
stale_ready_seconds int ≥ 1 900 How long ready work may wait with nobody taking it before it is surfaced. On PostgreSQL: how long the oldest job has been claimable and unclaimed, in every project, a project with no worker included -- measured from the later of run_after (which the lease reap, a gate answer and vibey recover move up to now) and its last dependency's success, never from updated_at, which a bump writes. On the broker: the head message's age on a queue with no consumer. Condition (d). A message without a timestamp property, and any message on a quorum queue (which reports no head timestamp), is not measured and never reported as old; the pass notes it instead.
dead_letter_min_depth int ≥ 1 1 Dead letters on a dead-letter queue before it is acted on. Condition (e).
dead_letter_peek_limit int, 1–1,000 100 The most new dead letters one pass parks off one queue. A read returns each message to its place and the reaper removes none, so the parked ones stay at the head: the read reaches past them (at most 10,000 in all) to the next unparked. Any still beyond it are counted and surfaced; the count is part of the sighting, so its growth is recorded again.
owned_queue_pattern regex ^vibey\. Queues vibey owns. Only an owned dead-letter queue's messages are parked, only owned queues get the policies below, and a dead letter is replayed only into an owned queue; every other queue on the broker -- Plane's Celery queues -- is measured and surfaced, never touched. Refused: a pattern that matches every name ('', .*) or owns a known foreign one (celery, celery.pidbox, amq.gen-…).
dead_letter_queue_pattern regex (\.dlq\|\.dead)$ Which queues are dead-letter queues: the bus port's <queue>.dlq and ADR-0044's vibey.jobs.dead / vibey.runs.<engine>.dead. Refused: a pattern that matches every name.
policy_name string vibey-reap The broker policies the reaper reconciles onto owned queues: <name> for quorum queues (consumer-timeout, delivery-limit) and <name>-classic for classic queues (consumer-timeout alone -- on RabbitMQ 4.3 a policy holding delivery-limit does not attach to a classic queue at all). Each is read back, and verified only when every owned queue reports carrying it.
policy_priority int ≥ 0 0 Those policies' priority, against any other policy matching the same queues. An owned queue carrying another policy is reported, and the pass is not verified.
consumer_timeout_seconds int ≥ 7,200 21600 The policy's consumer-timeout: how long a consumer may hold a delivery from an owned queue before the broker closes its channel and requeues. Six hours: at least the longest job lease (two hours for BUILD), with room. Condition (a). The broker-wide value for everything else is the chart's surfaces.rabbitmq.consumerTimeoutMs.
delivery_limit int ≥ 1 20 The policy's delivery-limit: deliveries of one message on an owned quorum queue before the broker dead-letters it (condition (c)). Twenty, not three, because a draining worker's requeues count too (ADR-0044 §11). Only the quorum policy carries it.

A reap is a gate a number crossed, never a judgement (12.d): every verdict is recorded as a QueueReaped ledger event with the object, the condition, the measured value, the threshold and the action. A surfaced condition is recorded once for the whole fleet -- the ledger holds it open until a pass that read its source whole no longer sees it and records it cleared -- and a broker sighting is recorded untrusted, since the broker's names are not vibey's words. What each condition does is set out in ADR-0056: an expired lease is requeued while attempts remain and parked with a delivery_exhausted gate once they are spent; a dead letter on an owned queue becomes a parked bus.dead_letter job and a bus_dead_lettered gate, and is never deleted.

[queue.reap]
stale_ready_seconds = 600
dead_letter_peek_limit = 200

[queue.defect]

When an exhausted job is a defect rather than bad luck. A job that burns its last attempt parks on a gate (ADR-0024), and that gate -- attempts_exhausted from the worker, escalation_exhausted from build.implement's effort ladder -- offers more attempts. When the job's last identical_failures failures were all the same failure, more attempts cannot change the outcome, so the worker raises a defect gate instead, which offers none.

Every failed run of a handler is recorded as a JobFailed ledger event (withheld from publication) with its signature: the failure's class and its detail with ids, timestamps, durations, addresses, temporary paths and attempt counters normalized away, hashed (domain/defect.py). The defect gate names the signature and how many attempts shared it, and is answered --choice requeue -- run it again once a fix has landed; it parks on a new defect gate at once if it fails the same way, and goes on within its remaining attempts if the failure changed -- or --choice abandon, which cancels the job when the answer lands (work that depends on it will not run). Varied failures keep the grant. History that cannot be read keeps the grant too, and is said at warning.

Read like [queue.reap]: from ./vibey.toml by build_app, or from the environment alone (VIBEY_QUEUE_DEFECT_IDENTICAL_FAILURES) in a pod with none.

Field Type Default Notes
identical_failures int: 0, or ≥ 2 3 How many of the most recent failures must share one signature. Two could be a coincidence; three is a pattern. 0 switches the check off and every exhaustion offers its grant; 1 would call every failure a defect, so it is refused. An unknown key is refused.
[queue.defect]
identical_failures = 4

[build.slicing]

How build.decompose groups a large plan into the four CDD layers -- stories, epics, phases, project -- before any of it runs (ADR-0092). A plan of at most max_children items stays stories only. A larger one is grouped into epics, more than max_children epics into phases, and so on, never past four layers. Each group runs to completion before the next begins.

Copied into the project's stored config by vibey new and read when the plan is made.

Field Type Default Notes
max_children integer, at least 2 8 The most children one group holds. Lower slices finer and serialises more across group boundaries; higher keeps a plan flatter. A boolean, a string, or anything below 2 is refused at load, as is an unknown key in [build.slicing] or an unknown table under [build].
[build.slicing]
max_children = 6

[design.research]

What a DESIGN research job does when its provider can obtain no evidence. After the interview, DESIGN queues one design.research job per topic (prior-art, libraries, api-docs), and synthesis waits for all of them. The sovereign provider has no web access: it summarises reading the operator supplied in VIBEY_EVIDENCE_DIR, and with none it refuses rather than invent a source (ADR-0027). Refusing to fabricate is not negotiable (sub-doctrine 10.f); this key only decides what happens next.

  • gate (the default). The job parks on a research_evidence gate that names the file to supply, and DESIGN waits for a person. This is the behaviour before the key existed.
  • record_gap. The topic is recorded as not researched, and DESIGN proceeds without a person. Nothing is invented: the job writes a ResearchGapRecorded ledger event (trusted; topic, reason, cycle -- no source, no content) and succeeds. Synthesis reads the cycle's gaps from the ledger -- not from the model -- into the spec, and the published spec.md ends with a Research not performed section naming each topic and why; open-items.md lists them too, so REVIEW and a person see the gap rather than a silence. The synthesizer is told a gap means no findings exist for that topic.

Either way, evidence that exists is used. And evidence the operator supplied that cannot be attributed -- a file with no source: first line, or an empty body -- still parks for a person under record_gap: that is a mistake to fix, and recording it as "not researched" would silently set the operator's material aside.

Read like [queue.defect]: from ./vibey.toml by build_app, or from the environment alone (VIBEY_DESIGN_RESEARCH_ON_UNAVAILABLE), which beats the file when set. The triaged-delivery bridge opts its worker in with --record-research-gaps (VIBEY_TRIAGED_DELIVERY_RECORD_RESEARCH_GAPS=1); it is off by default there too.

Field Type Default Notes
on_unavailable string: gate or record_gap gate Exactly one of the two, lower case. Any other value -- a typo, true, another case -- is refused at load, so a malformed policy can never read as record_gap, nor silently as the default. An unknown key in [design.research], or an unknown table under [design], is refused.
[design.research]
on_unavailable = "record_gap"

Concurrent local runs: [local_models] in .vibey-gh.toml

How many runs of one local model run at once on a device is not a vibey.toml key. It is declared once per repository in .vibey-gh.toml [local_models] concurrent_runs, and checked against that device's own calibration evidence (ADR-0058, sub-doctrines 8.c and 8.j). The default, 1, is 8.c as written and needs no evidence. "measured" takes what the device's evidence supports. A number above one runs only if the device measured that number inside every bound. Missing or stale evidence (another runner version, model digest, memory or context window) means one, and a calibration is requested. The storm runner reads the answer from vibey-gh slots allowed, and the queue runs one lane at a time until it says more. Every key, the bounds and the calibration are documented in the vibey-gh configuration reference.

# .vibey-gh.toml
[local_models]
concurrent_runs = "measured"
model = "gpt-oss:20b"
context_window = 65536

Operational surfaces (sovereign defaults, declared-only paid relays)

Each table is optional: an omitted table (or an omitted key) leaves the surface on its in-memory default, so a project runs with no external service configured at all. A real endpoint declares a self-hosted sovereign service (or a paid relay). parse_config validates all eight into VibeyConfig; bootstrap.build_app wires the concrete adapter only when every required key for that surface is present, otherwise the in-memory default.

[tracker] (sovereign default: Plane)

Field Type Default Notes
url string unset Base URL of the Plane instance, e.g. https://plane.example.com.
token string unset API token (X-API-Key, or Bearer for OAuth).
workspace_slug string unset Workspace slug from the Plane URL, e.g. my-team. Required with project_id: Plane's work-item routes are workspace-scoped (/api/v1/workspaces/{workspace_slug}/projects/{project_id}/work-items/).
project_id string unset Plane project UUID. Required with workspace_slug.
[tracker]
url = "https://plane.example.com"
token = "REPLACE_ME"
workspace_slug = "my-team"
project_id = "REPLACE_ME"

[docs] (sovereign default: BookStack)

Field Type Default Notes
url string unset Base URL of the BookStack instance.
token_id string unset API token ID (Token <id>:<secret> auth).
token_secret string unset API token secret.
book_id integer unset Book pages are created under. Must be an integer, never a boolean.
[docs]
url = "https://bookstack.example.com"
token_id = "REPLACE_ME"
token_secret = "REPLACE_ME"
book_id = 1

[secrets] (sovereign default: OpenBao)

Field Type Default Notes
url string unset Base URL of the OpenBao instance, e.g. http://localhost:8200.
token string unset OpenBao root or service token (X-Vault-Token).
[secrets]
url = "http://localhost:8200"
token = "REPLACE_ME"

[files] (sovereign default: Nextcloud)

Field Type Default Notes
url string unset Base URL of the Nextcloud instance (WebDAV).
user string unset WebDAV username.
password string unset WebDAV password or app token.
[files]
url = "https://nextcloud.example.com"
user = "vibey"
password = "REPLACE_ME"

[email] (sovereign default: Forward Email)

Field Type Default Notes
smtp_host string unset SMTP relay host.
smtp_port integer unset 587 for STARTTLS submission, 465 for implicit TLS (SMTP_SSL). Must be an integer, never a boolean.
username string unset SMTP username (also the default sender).
password string unset SMTP password. Login and STARTTLS are skipped without it.
from_email string unset Sender address; defaults to username, then vibey@localhost.
[email]
smtp_host = "smtp.example.com"
smtp_port = 587
username = "vibey@example.com"
password = "REPLACE_ME"
from_email = "vibey@example.com"

[sms] (gateway: Kannel; handsets: Fossify Messages)

Field Type Default Notes
url string unset Base URL of the Kannel smsbox instance (/cgi-bin/sendsms).
username string unset Kannel sendsms username.
password string unset Kannel sendsms password.
sender string unset Sender phone number or alphanumeric origin.
[sms]
url = "http://localhost:13013"
username = "vibey"
password = "REPLACE_ME"
sender = "+15550100"

[messaging] (sovereign default: Matrix)

Field Type Default Notes
url string unset Base URL of the Matrix homeserver.
token string unset Access token (Bearer).
room_id string unset Default room, e.g. !room:example.org.
[messaging]
url = "https://matrix.example.org"
token = "REPLACE_ME"
room_id = "!room:example.org"

[config_store] (sovereign default: Infisical)

Field Type Default Notes
url string unset Base URL of the Infisical instance.
token string unset API access token (Bearer) or machine identity token.
project_id string unset Infisical project ID.
environment string "dev" Environment slug secrets are read from and written to.
[config_store]
url = "https://infisical.example.com"
token = "REPLACE_ME"
project_id = "REPLACE_ME"
environment = "dev"

[cache] (sovereign default: Redis)

Field Type Default Notes
url string unset Redis connection URL, e.g. redis://localhost:6379/0.
[cache]
url = "redis://localhost:6379/0"

[bus] (sovereign default: RabbitMQ)

Field Type Default Notes
url string unset Base URL of the RabbitMQ Management API, e.g. http://localhost:15672. It must not carry credentials (http://user:pw@host is refused): they go in username and password, and are never quoted in an error.
username string unset RabbitMQ management username.
password string unset RabbitMQ management password.
vhost string / The vhost the bus declares its queues in and the queue reaper reads ([queue.reap]).
mode auto, singleton, multiplexer, or hybrid auto Dispatch policy. auto uses the durable per-machine benchmark winner and falls back to singleton when no valid result exists.
hybrid_concurrency positive integer 4 Maximum concurrent bus operations in hybrid mode.
[bus]
url = "http://localhost:15672"
username = "vibey"
password = "REPLACE_ME"
vhost = "/"
mode = "auto"
hybrid_concurrency = 4

The bus port consumes at most once: its consume acknowledges on take, so no delivery is ever held -- and one whose consumer dies after consume returns is lost. The job queue's transport is ADR-0044's AMQP client, not this port.

All bus-backed surfaces follow ADR-0074: singleton is the serial path, multiplexer permits concurrent operations, and hybrid bounds concurrency. The benchmark and winner are per surface and per machine; mode remains an explicit override when an operator needs one.

[blob] (sovereign default: Garage, S3 API)

Field Type Default Notes
url string unset Garage S3 endpoint URL, e.g. http://localhost:3900.
access_key string unset S3 API access key ID.
secret_key string unset S3 API secret access key.
region string "us-east-1" S3 signing region (must match server configuration).
[blob]
url = "http://localhost:3900"
access_key = "REPLACE_ME"
secret_key = "REPLACE_ME"
region = "us-east-1"

[siem] (sovereign default: Wazuh Indexer)

Field Type Default Notes
url string unset Wazuh indexer REST endpoint, e.g. http://localhost:9200.
username string unset Indexer username for Basic auth (omitted when security is disabled).
password string unset Indexer password.
index string "vibey-audit" Target audit index name.
[siem]
url = "http://localhost:9200"
username = "vibey"
password = "REPLACE_ME"
index = "vibey-audit"

[surfaces]

Settings every sovereign surface adapter shares.

Field Type Default Notes
adapter_timeout_seconds integer, 1 to 600 10 The bound on each blocking network call a surface adapter makes, so a server that accepts the connection and never answers cannot hang its caller for ever (doctrine 10). VIBEY_SURFACES_ADAPTER_TIMEOUT_SECONDS overrides it. A value outside 1 to 600, or one that is not an integer, is refused. The adapters ship with this default as their timeout; the key is declared and validated here, and nothing yet passes it to them from vibey.toml.
[surfaces]
adapter_timeout_seconds = 30

Skills context (project config record — not a vibey.toml table)

VibeyConfig has no skills_context field; a [skills_context] table in vibey.toml is silently ignored by parse_config. The values live in the project's stored config record and are written by vibey new --skills-context-mode / --skills-context-budget (only when the mode is not off; see the CLI reference) or by the operator's spec.skillsContext object (copied verbatim). compiler_from_config (infrastructure/skills_context.py) reads them when bootstrap.build_full_worker builds the worker (ADR-0031).

Field Type Default Notes
mode string "off" off, shadow (measure only, never changes prompts), or inject (append successful packets to BUILD prompts). Any other value raises when the worker is built.
budget integer 6000 Token budget for retrieval, 1,000–32,000 (enforced by the vibey new flag, the operator CRD, and VibeySkillsContextCompiler).
timeout_seconds number 120.0 Skills compile timeout; must be positive. Settable only through spec.skillsContext.
command array of non-empty strings unset (<python> -m vibey_skills.cli) Override for the vibey-skills command. Read by compiler_from_config but not declared in the VibeyProject CRD schema, so neither creation path sets it today.
index_path string .vibey/skills-context/index under the repo Path to the skills index; relative paths resolve under the repo. Read by compiler_from_config but not declared in the CRD schema, so neither creation path sets it today.

Automated review checks (project config record — not a vibey.toml table)

VibeyConfig has no review field; a [review] table in vibey.toml is silently ignored by parse_config, and vibey.toml is never loaded by the worker anyway. The values live in the project's stored config record under a review object, and SubprocessAutomatedReviewRunner.from_config (infrastructure/build/automated_review_runner.py) reads them when bootstrap.build_full_worker builds the worker. They are the commands review.demo shells out to: a non-zero exit becomes a Severity.HIGH security finding or a Severity.MEDIUM code_review finding, which sends REVIEW back to BUILD.

Neither vibey new nor the operator's VibeyProject spec writes this object today, the same way skills_context.command and skills_context.index_path are read but never written; the record is written directly. Until one of them does, an unconfigured project runs REVIEW's code-review check and no security check at all — which is what the empty security_commands default states plainly, rather than running a scan that inspects nothing and reports success. vibey's own bandit -q -r src/vibey is enforced for real as gate 6 of ci.yml, on every pull request, independently of this.

Field Type Default Notes
security_commands array of arrays of non-empty strings [] (no security check runs) Security checks. There is deliberately no default: any baked-in command names both a tool and a layout, and bandit -q -r <path that does not exist> exits 0 — a wrong default reports a passing security check that examined zero files. Configure this to get one.
code_review_commands array of arrays of non-empty strings [["ruff", "check", ".", "--exclude", ".vibey", "--exclude", ".claudeloop", "--exclude", ".codexloop"]] Code-review checks. The default excludes vibey's own machinery inside the repo — worktrees under .vibey/ and the engines' state dirs — which are not the product. An explicit [] disables the check.

A malformed review object (not an object, a command list that is not a list of non-empty string arrays) raises when the worker is built, rather than silently running nothing.

[gates]

SubprocessGateRunner.from_config (infrastructure/build/gate_runner.py) reads the record's gates object when bootstrap.build_full_worker builds the worker. One runner runs build.verify's gates, build.integrate's gates and REVIEW's automated checks. Declare it in vibey.toml's [gates] table, which vibey new copies into the record, or in the VibeyProject spec's gates object; do not edit the record's JSON by hand (sub-doctrines 12.c and 12.h).

Field Type Default Notes
timeout_seconds number 1800 Per command; one that overruns is killed and fails as exit 124. Must be finite and positive.
kill_grace_seconds number 5 How long a killed command's reap may take before it is abandoned and logged.
isolate_python_env bool true false passes vibey's own Python environment (VIRTUAL_ENV, PYTHONPATH, its venv on PATH) to gates that rely on tools installed beside vibey.
env_allow array of strings [] Environment variables a gate command may receive beyond the system basics listed under engine_environment. A trailing * names a prefix (GRADLE_*). A gate command is decomposer-produced argv running engine-written code, so nothing else in the worker's environment reaches it. VIBEY_*, PG* and GIT_* can never be declared. A project's own test database can be (TEST_DATABASE_URL).

A malformed or forbidden gates object is refused by vibey new and by the operator before the project is created, and raises when the worker is built.

[gates]
timeout_seconds = 1800
env_allow = ["JAVA_HOME", "GRADLE_*", "TEST_DATABASE_URL"]

[engine_environment]

An engine session runs model-chosen shell commands, unattended in BUILD and DEPLOY_EXECUTE. It never inherits the worker's environment. Every engine process (the run, its --version, doctor and --help probes, and the claudeloop DESIGN/DECOMPOSE sessions) starts from an allow-list built by EngineEnvironmentPolicy (infrastructure/engines/engine_environment.py). The allow-list has three parts:

  1. The system basics, for every engine and every gate command: PATH (with vibey's own venv removed), HOME, USER, LOGNAME, SHELL, TMPDIR, TMP, TEMP, LANG, LANGUAGE, LC_*, TZ, TERM, COLORTERM, NO_COLOR, COLUMNS, LINES, SSL_CERT_FILE, SSL_CERT_DIR, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, NODE_EXTRA_CA_CERTS, HTTP_PROXY, HTTPS_PROXY, NO_PROXY, ALL_PROXY (and their lower-case forms), XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME, XDG_RUNTIME_DIR, and __CF_USER_TEXT_ENCODING (macOS).
  2. What the engine's descriptor declares. This is its env_passthrough and its API credential, auth_env:
Engine Declared
claudeloop ANTHROPIC_API_KEY, CLAUDELOOP_*, CLAUDE_CODE_*, CLAUDE_CONFIG_DIR, ANTHROPIC_*
claudeloop-local CLAUDELOOP_*, CLAUDE_CODE_*, CLAUDE_CONFIG_DIR, ANTHROPIC_*
codexloop OPENAI_API_KEY, CODEXLOOP_*, CODEX_*, OPENAI_*, AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT
gptossloop GPTOSSLOOP_* (plus the GPTOSSLOOP_BASE_URL/GPTOSSLOOP_MODEL vibey derives from VIBEY_OLLAMA_URL)
qwenloop QWENLOOP_* (plus the QWENLOOP_BASE_URL vibey derives from VIBEY_OLLAMA_URL; vibey hands qwenloop no model)
  1. What the project declares. This is the record's engine_environment object, declared in vibey.toml's [engine_environment] table (copied into the record by vibey new) or the VibeyProject spec's engineEnvironment object:
Field Type Default Notes
allow array of strings [] Added for every engine. A trailing * names a prefix.
engines object of engine id → array of strings {} Added for one engine only, for example {"claudeloop": ["GH_TOKEN"]}. Keys must be known engine ids.

A GitHub token or a cloud credential is on no default list. It reaches only the engine it is declared for. Some things need declaring:

  • claudeloop's GitHub issue import needs GH_TOKEN or GITHUB_TOKEN for a private repository.

Some names can never be declared, by anyone. For every engine: vibey's own VIBEY_* variables (VIBEY_PG_URL, the queue and ledger DSN, among them), libpq's PG*, and any name containing DSN, DATABASE_URL, PASSWORD or PASSWD. A malformed object, an unknown key or engine, or a forbidden entry is refused by vibey new and the operator before the project is created, and raises when the worker is built. A descriptor's own env_passthrough is held to the same rule when its adapter is built.

The worker's startup preflight probes each engine with the project's policy, so a declared credential reaches the auth check the same way it reaches a session. vibey doctor probes with the defaults, because it reads no project record, unless --record names one (--project, default the latest): then it probes and runs conformance with that project's policy, since the health it records is that project's.

[engine_environment]
allow = ["JAVA_HOME"]

[engine_environment.engines]
claudeloop = ["GH_TOKEN"]
"claudeloop-local" = ["GH_TOKEN"]

[human_gates]

Which human gates may resolve to their own default answer once they have waited, and how long each waits first. Nothing times out unless it is declared here. A gate's default is never applied on silence: sub-doctrine 12.d says silence is not consent, and some defaults act for you -- deploy_demo_review defaults to approve. Declaring a kind here is the consent, given in advance, in the repository (the operator's ruling of 2026-09-30).

[human_gates.timeout_defaults]
choice = 1440          # REVIEW's deployment opt-in resolves to "local_only" after a day
deploy_acceptance = 60 # an unanswered deployment acceptance resolves to "reject"
Field Type Default Notes
timeout_defaults table of gate kind → minutes {} Each key is a gate kind; each value is a positive whole number of minutes. A gate of a declared kind that carries a default and has waited at least that long is answered with its default by the worker's idle sweep (application/gate_timeouts.py), through the same path as vibey answer: the ledger records it as GateAnswered by gate-timeout. Every other gate waits for a person, however long, and is still reminded about on the [notifications] schedule.

Only these kinds can be declared, because only these carry a default this table can turn into the answer their handler reads: choice (default local_only), deploy_interview (accept_defaults), deploy_acceptance (reject), deploy_demo_review (approve) and deploy_failure_triage (LOOP_DEPLOY_DESIGN). REVIEW's approval gate has no default and can never time out. Naming any other kind, or a wait that is not a positive whole number, is refused by vibey new before the project exists; a stored declaration that cannot be honoured makes the sweep answer nothing for that project and log gate.timeout_config_invalid.

[sabbath]

Sub-doctrine 8.i: from sundown Friday to sundown Saturday nothing writes, merges, tests or ships code (ADR-0072). Sundown is computed for the machine the process runs on, with the NOAA algorithm. The same table is read from a local vibey.toml (by the engine) and from .vibey-gh.toml (by the merge train, the promotion and the heartbeat). Never commit coordinates: put them only in this machine's own vibey.toml, in local_config, or in VIBEY_SABBATH_LATITUDE and VIBEY_SABBATH_LONGITUDE.

Key Default Meaning
enabled true 8.i has no exception. Turning it off is a declared act, never a missing key.
timezone "" The IANA zone the civil day is read in. Empty reads the host's own zone ($TZ, then /etc/localtime).
latitude, longitude unset An explicit override. Set both or neither, and only in a file that is never committed.
local_config ~/.config/vibey/sabbath.toml A per-host TOML file with latitude and longitude, read when the table has none.
location_service true Ask CoreLocation (CoreLocationCLI, macOS) or GeoClue (where-am-i, Linux) when installed.
offset_minutes 0 Widens every window toward rest, both edges (0–240). It never narrows one.
coarse_margin_minutes 90 Extra widening when the location is only the zone's reference city (0–240).
fallback_opens, fallback_closes "14:00", "23:00" Friday open and Saturday close, local, when no sundown can be computed (no location, or a polar day).
resume_dispatch true At the first heartbeat after the window, re-fire the held merge train and promotion.
lanes_dir ~/.local/state/vibey/sabbath-lanes Where lanes register "paused for the Sabbath, resume with ...".

Where the host stands is resolved in this order, and every window names its source:

  1. the override;
  2. the operating system's location service;
  3. the reference city of the host's zone from the system zone1970.tab, which is coarse and so widened.

There is no IP lookup. The answer is cached for a week and dropped when the zone changes.

VIBEY_SABBATH_ENABLED=false (also 0, no, off; truthy values re-enable) declares the Sabbath off for one process without editing a file -- what the Helm cluster-smoke step uses so vibey new does not lawfully decline every Friday evening. It overrides the table's enabled in either direction; unset, the table decides.

# This machine's own vibey.toml -- never committed. EXAMPLE coordinates only.
[sabbath]
latitude  = 34.97
longitude = -82.44

Full example

This is a valid file exercising most of the schema that parse_config validates. vibey new also copies its [notifications], [telemetry], [gates] and [engine_environment] tables into the project's stored config (see the top of this page).

[project]
name = "my-app"
repo = "."
max_cycles = 15

[isolation]
level = "container"
allow_push = false

[budget]
max_dollars_per_cycle = 15.0
max_dollars_total = 250.0

[engines]
enabled = ["claudeloop", "codexloop"]
weights = { claudeloop = 3, codexloop = 1 }

[phases.design]
effort = "high"

[phases.build]
effort = "low"
parallelism = 2

[phases.review]
effort = "high"

[deploy]
enabled = true
target = "azure"
iac = "bicep"

[notifications]
enabled = true
desktop = false

[[notifications.webhooks]]
url = "https://ops.example/vibey"
secret = "replace-me"

[telemetry]
enabled = true

[features]
gptossloop = true   # the default; false switches the sovereign engine off
qwenloop = true     # opt in to the same runner on a Qwen model

[qwenloop]
backend = "auto"
idle_timeout_seconds = 600

[queue.priority]
sources = ["storm"]

[gates]
env_allow = ["JAVA_HOME"]

[engine_environment.engines]
claudeloop = ["GH_TOKEN"]

[queue.reap]
stale_ready_seconds = 900