Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Vibe Factory

Vibe Factory is an open, extensible framework for autonomous multi-agent software development, written in Rust.

You describe what you want. A pipeline of cooperating AI agents assesses the request, explores your codebase, writes a specification, plans the work, implements it in an isolated git workspace, reviews the result against the acceptance criteria, fixes what the review found, and hands you a branch ready to merge. Everything it did stays visible afterwards: the history of each task, the activity of every task in one feed, and the complete trace of every tool call, from the command line, a terminal UI, a local web UI or a macOS app.

   task ──▶ assess ──▶ spec ──▶ plan ──▶ build ──▶ qa ──▶ fix ──▶ merge
                                          │          ▲       │
                                          └──────────┴───────┘

Why another agent framework?

Most autonomous coding tools are products: a fixed set of agents, one model vendor, one user interface. Vibe Factory is a framework. Everything that could reasonably differ between teams is an extension point with a small, documented trait:

You want to…Implement or configure
use a different model vendor or a local modelModelProvider
give agents a new capabilityTool
change how an agent thinks or what it may touchAgentSpec (TOML, no code)
isolate work in a container instead of a worktreeWorkspaceProvider
remember lessons across runsMemoryStore
audit, veto or enrich what agents doHook
ship all of the above as a packagePlugin (in-process); tools, agents and hooks also from any language over stdio

The default pipeline is itself built only from these extension points, so anything the framework can do, a plugin can do too.

What is in the box

  • Four interfaces on one engine — the vibe command line, a terminal UI (vibe tui), a local web UI and HTTP API (vibe serve), and a native macOS app built on that API; they all read the same events, so they always agree. A VS Code extension is a client of vibe serve too.
  • See what was done — vibe history for what each task delivered (runs, commits, changed files, validations, QA verdict, tokens, active time, and a cost when you give prices), vibe events for the activity of every task, live or replayed, and vibe trace for every tool call of a run with its complete arguments, exit code and output.
  • Human control — approvals of the spec, the plan or the merge, token and time budgets, cancellation and resume from any interface.
  • Providers — Anthropic, any OpenAI-compatible API (OpenAI, Groq, Mistral, xAI, OpenRouter, Ollama), and a scriptable mock for tests and dry runs.
  • Tools — read, write, edit, list, glob, grep and a sandboxed shell, all confined to the workspace and governed by a security policy.
  • Workspaces — git worktree isolation with fast-forward, merge and conflict reporting, or in-place mode for quick experiments.
  • Agents — complexity assessor, spec gatherer, researcher, writer and critic, planner, coder, QA reviewer and fixer, merge resolver and commit-message writer, all overridable from .vibe/agents/*.toml.
  • Plugins — a JSON-RPC-over-stdio protocol shaped like the Model Context Protocol, so existing MCP servers work as tool plugins and new plugins can be written in any language.
  • Documentation — this book: a user guide, a design book with architecture decision records, and generated API docs.

Where to go next

Vibe Factory is licensed under MIT or Apache-2.0, at your option.

Installation

Vibe Factory is a single binary, vibe, built from source with cargo. It runs on macOS, Linux and Windows.

Requirements

RequirementVersionWhy
Rust toolchain1.88 or newer (edition 2024)to build vibe; install with rustup
gitany recent versiontask workspaces are git worktrees; also needed by cargo install --git
a model provideran Anthropic or OpenAI key, any OpenAI-compatible endpoint, or a local Ollama
Ollama (optional)to run models locally without a key

Check what you have:

rustc --version     # rustc 1.88.0 or newer
git --version

If rustc is older, run rustup update stable.

The project you point vibe at should be a git repository: the default workspace creates a worktree per task. For a directory without git, use the in_place workspace (see Workspaces and merging).

Installing from git

cargo install --git https://github.com/vincentlauriat/vibe-factory vibe-cli

This builds the vibe-cli package in release mode and places the vibe binary in ~/.cargo/bin (%USERPROFILE%\.cargo\bin on Windows), which rustup adds to your PATH. The first build downloads and compiles the dependencies and takes a few minutes.

To install a tagged release or a branch (replace the tag with one that exists):

cargo install --git https://github.com/vincentlauriat/vibe-factory --tag v0.5.1 vibe-cli
cargo install --git https://github.com/vincentlauriat/vibe-factory --branch main vibe-cli

Building from source

git clone https://github.com/vincentlauriat/vibe-factory
cd vibe-factory
cargo build --release -p vibe-cli
./target/release/vibe --version

To install your local checkout as vibe:

cargo install --path crates/vibe-cli

The rest of the workspace builds and tests the same way:

cargo build --workspace
cargo test --workspace
mdbook serve docs          # this book, on http://localhost:3000

The repository also contains an example plugin, built separately:

cargo build --release -p vibe-plugins --bin vibe-echo-plugin

Verifying the installation

vibe --version
vibe 0.5.1

Then, inside a project:

cd your-project
vibe init
vibe doctor

vibe doctor checks git, the project repository, the configuration, provider keys, plugins, the workspace setting and that .vibe/ is writable, one line per check. A missing key is expected at this point if you have not set one yet; the Quick start continues from here.

Setting up a model provider

With the default configuration, vibe uses anthropic/claude-sonnet-5 and reads the key from ANTHROPIC_API_KEY:

export ANTHROPIC_API_KEY=sk-ant-…          # bash, zsh
set -gx ANTHROPIC_API_KEY sk-ant-…         # fish
$env:ANTHROPIC_API_KEY = "sk-ant-…"        # PowerShell

For OpenAI, set OPENAI_API_KEY and a default model:

export OPENAI_API_KEY=sk-…
vibe config set default_model openai/gpt-5

For a local model with Ollama, no key is needed:

ollama pull qwen2.5-coder
ollama serve                                # if not already running as a service
vibe config set default_model ollama/qwen2.5-coder

Other vendors and gateways are configured in Providers and models.

Shell completions

vibe completions <shell> prints a completion script for bash, zsh, fish, elvish or powershell.

ShellInstall
bashvibe completions bash > ~/.local/share/bash-completion/completions/vibe
zshvibe completions zsh > ~/.zfunc/_vibe, with fpath+=~/.zfunc before compinit in ~/.zshrc
fishvibe completions fish > ~/.config/fish/completions/vibe.fish
PowerShellvibe completions powershell >> $PROFILE

Open a new shell afterwards. Regenerate the script after upgrading, since new options only appear in a fresh script.

Upgrading

Run the install command again with --force:

cargo install --force --git https://github.com/vincentlauriat/vibe-factory vibe-cli

From a checkout: git pull then cargo install --path crates/vibe-cli --force.

Project data is forward-compatible: fields added to persisted files have defaults, so tasks created by an older version still load. Read the changelog before upgrading across a minor version, and finish or pause running tasks first. Your .vibe/config.toml is never rewritten by an upgrade; vibe config show --default shows the new defaults so you can compare.

Uninstalling

cargo uninstall vibe-cli

This removes the binary only. Per-project data lives in each project’s .vibe/ directory; discard task worktrees first so that their branches are removed too, then delete the directory:

vibe task list --all              # tasks whose worktrees you may want to discard
vibe task discard <ref> --yes     # for each task
rm -rf .vibe
git worktree prune

User-level plugins live in your configuration directory (~/.config/vibe/ on Linux, ~/Library/Application Support/vibe/ on macOS, %APPDATA%\vibe\ on Windows); remove it if you installed any.

Platform notes

macOS and Linux. The bash tool runs commands with sh -c in the workspace, in their own process group, so a timeout kills the whole process tree. Any POSIX sh works; the commands agents write (cargo test, npm test, grep, …) must be installed on your machine, as for a human developer.

Windows. The bash tool runs commands with cmd /C, and a timeout kills the process tree with taskkill /T /F. The security policy still parses commands with POSIX shell rules, so constructs specific to cmd (for example %VAR% expansion or ^ escapes) may be rejected as unparseable; commands such as cargo test or git status work unchanged. Programs must be on PATH. Paths given to tools are resolved against the workspace and must stay inside it, as on other platforms, and a bare drive root (C:\) is never a valid target for rm. Long paths inside deep worktrees can hit the 260-character limit: enable long paths in git (git config --global core.longpaths true) and in Windows, or keep the project close to the drive root.

All platforms. Worktrees live inside the project, under .vibe/worktrees/, so they are on the same file system as your checkout and need roughly one extra working copy of disk space per open task (see Workspaces and merging).

Quick start

This walkthrough takes about ten minutes. It runs one small task on an existing repository, from vibe init to a merged branch. It assumes vibe is installed (Installation) and uses a small Rust command-line project as the example; any language works the same way.

1. Choose a model

With an Anthropic key:

export ANTHROPIC_API_KEY=sk-ant-…

Or, with no key at all, a local model through Ollama:

ollama pull qwen2.5-coder

You will point the project at Ollama in step 2. To try the pipeline without any model, jump to Try it with no API key.

2. Initialise the project

cd ~/src/export-tool
git status                  # a clean git repository is the easiest start
vibe init

vibe init writes .vibe/config.toml with every key at its default value and a commented [[plugins]] example, and adds worktrees/ and tool-output/ to .vibe/.gitignore (tasks/ stays committed on purpose). Inspect the effective configuration:

vibe config show
default_provider = "anthropic"
default_model = "anthropic/claude-sonnet-5"
plugins = []

[phases]

[providers.anthropic]
kind = "anthropic"
api_key_env = "ANTHROPIC_API_KEY"

[providers.ollama]
kind = "openai"
base_url = "http://localhost:11434/v1"
default_model = "qwen2.5-coder"

[providers.openai]
kind = "openai"
api_key_env = "OPENAI_API_KEY"

[pipeline]
max_qa_rounds = 3
max_subtask_attempts = 3
max_parallel_subtasks = 3
max_phase_retries = 2
workspace = "git_worktree"
auto_merge = false
merge_strategy = "manual"

[security]
blocked_commands = []
allowed_commands = []
command_timeout_secs = 120
allow_network = false
extra_read_paths = []

Every phase uses default_model; [phases] is empty until you give a phase its own model.

With Ollama, switch the default model:

vibe config set default_model ollama/qwen2.5-coder

Then check everything is in place:

vibe doctor

Every key is explained in Configuration.

3. Add a task

vibe task add "Add a --json flag to the export command" \
  -d "When --json is given, print the exported records as a JSON array on stdout instead of the table. Keep the table as the default. Add a test."
✓ created task #1 Add a --json flag to the export command
  id   6f1c3e0a-9b2d-4c47-8a51-0e3b8f2d7c19
  dir  /home/me/src/export-tool/.vibe/tasks/001-add-a-json-flag-to-the-export-command

Run it with: vibe run 1

The task is stored in .vibe/tasks/001-add-a-json-flag-to-the-export-command/task.json, in the backlog status. vibe task list shows it.

4. Plan without building

A dry run stops after planning, so you can read what the agents intend before they touch any code:

vibe run 1 --dry-run

It runs three phases:

PhaseAgent(s)Produces
assesscomplexity_assessor (unless the heuristic already knows)the complexity and profile, in task.json and run.json
specspec_gatherer, then spec_writer for a standard taskspec.json and spec.md: summary, requirements with acceptance criteria, relevant files
planplannerplan.json and plan.md: phases of subtasks with files, dependencies and verification

The files are in the task directory:

cd .vibe/tasks/001-add-a-json-flag-to-the-export-command
cat spec.md plan.md progress.md
cd -

progress.md has one timestamped note per step: the assessment and its source, the spec summary, the plan. The dry run leaves the run paused and the task back in backlog, ready to continue; the exit code is 0 because it stopped where you asked. If the spec or plan misses the point, edit spec.json and run vibe run 1 --from plan --dry-run, or discard the task and add it again with a better description.

5. Run it

vibe run 1 --resume

--resume continues the paused run at the build phase, reusing the spec and plan you just read. (vibe run 1 would start over from the assessment.) The output looks like this:

Resuming #1 Add a --json flag to the export command (model anthropic/claude-sonnet-5, workspace git_worktree)
▶ run 3b1f09c2

── build ──────────
  ▸ subtask 1/3 Add the --json option to the CLI definition: in_progress
  ● coder · subtask 1/3 Add the --json option to the CLI definition
  ⟶ read_file(path: "src/cli.rs")
  ⟵ ok (2 ms)
  ⟶ edit_file(new_string: "    /// Print records as JSON.\n    #[arg(long)]\n    pub json: bool,\n…
  ⟵ ok (4 ms)
  ⟶ bash(command: "cargo build")
  ⟵ ok (8412 ms)
  ● coder finished: 7 step(s), 31.4k in / 1.9k out tokens, completed
  ▸ subtask 1/3 Add the --json option to the CLI definition: done
  ▸ subtask 2/3 Serialise records as JSON in export: in_progress
  ● coder · subtask 2/3 Serialise records as JSON in export
  ⟶ grep(path: "src", pattern: "fn export")
  ⟵ ok (12 ms)
  ⟶ edit_file(new_string: "    if args.json {\n        serde_json::to_writer_pretty(std::io::st…
  ⟵ ok (3 ms)
  ⟶ bash(command: "cargo test export")
  ⟵ error (6120 ms): exit status 101 --- stdout --- running 4 tests test export::tests::json_out…
  ⟶ edit_file(new_string: "#[derive(Serialize)]\npub struct Record {", old_string: "pub stru…
  ⟵ ok (2 ms)
  ⟶ bash(command: "cargo test export")
  ⟵ ok (5810 ms)
  ● coder finished: 12 step(s), 58.0k in / 3.4k out tokens, completed
  ▸ subtask 2/3 Serialise records as JSON in export: done
  ▸ subtask 3/3 Test the --json output: in_progress
  …
  ▸ subtask 3/3 Test the --json output: done
✓ build: 3 done, 0 failed, 0 skipped (4 min 12 s)

── qa ──────────
  ● qa_reviewer
  ⟶ bash(command: "cargo test")
  ⟵ ok (9204 ms)
  ● qa_reviewer finished: 15 step(s), 39.8k in / 1.9k out tokens, completed
✓ qa: round 1: Approved (0 issue(s)) (1 min 03 s)

── merge ──────────
✓ merge: ready for human review and merge (auto_merge is off) (3 ms)
■ run finished: ready

phase  result  time          tokens in  tokens out  summary
build  ✓       4 min 12 s    176.3k     7.9k        3 done, 0 failed, 0 skipped
qa     ✓       1 min 03 s    39.8k      1.9k        round 1: Approved (0 issue(s))
merge  ✓       3 ms          0          0           ready for human review and merge (auto_mer…
status     ready
duration   5 min 16 s
tokens     216.1k in / 9.8k out
branch     vibe/add-a-json-flag-to-the-export-command-6f1c3e0a
worktree   /home/me/src/export-tool/.vibe/worktrees/add-a-json-flag-to-the-export-command-6f1c3e0a
task dir   /home/me/src/export-tool/.vibe/tasks/001-add-a-json-flag-to-the-export-command

Review the work, then merge it: git merge vibe/add-a-json-flag-to-the-export-command-6f1c3e0a

A ── header opens each phase and a ✓ (or ✗) line closes it with its summary and duration. ● lines mark agent sessions, ▸ lines subtask status changes. Each ⟶ line is a tool call with its arguments (cut at 80 characters) and each ⟵ line its result with the time it took. A failed tool call (error) is normal: the agent reads the error and tries again. With -v, the agents’ own text and informational logs are shown too. The final table sums up the phases of this invocation. Ctrl-C cancels; vibe run 1 --resume continues later.

Exit code 0 means the task is ready (or done with auto-merge).

6. Inspect the result

vibe task show 1

This prints the spec summary, the plan with each subtask’s status, the QA verdict, the state of the run and the workspace. The code is on the task branch, in its own worktree:

git log --oneline main..vibe/add-a-json-flag-to-the-export-command-6f1c3e0a
git diff main...vibe/add-a-json-flag-to-the-export-command-6f1c3e0a
cd .vibe/worktrees/add-a-json-flag-to-the-export-command-6f1c3e0a && cargo test && cd -

Each completed subtask was committed as vibe: complete subtask <n> - <title>.

7. Merge

The task is ready: nothing reached your branch yet. Merge it as any branch:

git switch main
git merge vibe/add-a-json-flag-to-the-export-command-6f1c3e0a

The worktree and the branch stay until you discard the task. vibe task discard 1 --yes removes them, together with the task directory and its traced tool outputs, so the task no longer appears in its history: run it once you no longer need to look back at the task (step 8).

To let Vibe Factory merge by itself as soon as QA approves, run with --auto-merge, or set it for the project:

vibe run 2 --auto-merge
vibe config set pipeline.auto_merge true

Auto-merge fast-forwards when it can, otherwise creates a merge commit. It refuses to run while tracked files of your checkout have uncommitted changes, and it leaves the task ready with the list of conflicting files when the branches conflict. Details in Workspaces and merging.

8. See what was done

Everything a run did stays readable after it ended, rebuilt from the task’s events and git:

vibe history                 # every finished task: runs, commits, files, tokens, time, cost
vibe history 1               # one task: its runs, commits, changed files, validations, QA
vibe trace 1                 # every tool call of the last run, with its result
vibe trace 1 --full          # … and the complete output of each call
vibe events --since 1d       # what happened on every task today, in time order
vibe events --follow         # the activity of every task from now on (Ctrl-C to stop)

vibe history shows a cost only once you give model prices in [pricing]. The complete tool outputs vibe trace --full reads are kept under .vibe/tool-output/, ignored by git; see Tool call trace for their disk usage and History for where each figure comes from.

The same views exist outside the command line:

  • vibe tui opens a terminal UI: the task board (T), the activity of every task (A), the history (H), and a Trace tab in the task detail;
  • vibe serve serves a web UI on http://127.0.0.1:7777 (open the link it prints, which carries the access token) with the Tasks, Activity, History and Evaluations views and a Trace tab per task, and the HTTP API they use;
  • the macOS app in apps/macos/VibeFactory opens a project folder and starts vibe serve for it, or connects to one already running.

Try it with no API key

The built-in mock provider answers every agent with canned, valid documents without calling any model. It edits no file, so it is only good for seeing the pipeline move:

vibe task add "Add a --json flag to the export command"
vibe run 1 --provider mock --dry-run
vibe task show 1

This is the CLI’s built-in mock, used whenever no [providers.mock] table is declared. A provider you declare yourself with kind = "mock" only answers plain text, so its structured phases fail. The mock assessor classifies the task as simple, so the spec is written by the gatherer alone, and the planner returns a one-subtask plan. Drop --dry-run to go through build, QA and merge as well.

To control what the “model” says, give a script with --script. A script is a JSON array of answers replayed in order, or an object with answers per agent role:

{
  "routes": {
    "planner": [
      {"json": {"approach": "Two steps.", "phases": [{"name": "Work", "parallel": false,
        "subtasks": [{"title": "Add flag", "description": "Add --json."},
                     {"title": "Test it", "description": "Add a test.", "depends_on": ["Add flag"]}]}]}}
    ],
    "qa_reviewer": [
      {"json": {"verdict": "changes_requested", "summary": "Missing test.",
                "issues": [{"severity": "high", "title": "No test for --json"}]}}
    ]
  },
  "fallback": true
}
vibe run 1 --script demo.json      # implies the mock provider on every phase

Each request takes the next answer routed to its role, else the next entry of steps, else, with fallback true (the default), the canned answer. An answer is a string (plain text), {"text": "…"}, {"json": …} (sent as a fenced JSON document, as structured agents answer), or {"tool": "read_file", "input": {"path": "src/main.rs"}} (a tool call, which really runs). With "fallback": false, an exhausted script fails the request. Roles are recognised from the first line of the agent’s system prompt, so routes work for the built-in prompts; an agent whose prompt you replaced is only served by steps and the fallback. The example above makes QA request changes once, so you can watch the fix loop: the fixer runs, QA reviews again with the canned approval, and the task ends ready.

Next steps

Concepts

This page introduces the vocabulary used throughout Vibe Factory. The Glossary has one-line definitions; this page explains how the pieces fit together.

Task

A task is the unit of work you hand to the framework: a title and a description, in plain language, of what you want. Tasks live on a board with the statuses backlog, planning, building, review, ready, done, failed and cancelled.

vibe task add "Add OAuth login with GitHub" \
  --description "Users should be able to sign in with their GitHub account…"

Complexity

Before doing anything expensive the pipeline assesses the task: trivial, simple, standard or complex. The class picks a profile: a trivial fix skips the specification; a complex feature adds research and a self-critique of the spec.

Specification

The spec captures what must be built and how success will be checked: a summary, a list of requirements (functional, non-functional, constraints) each with acceptance criteria, and the context gathered from your codebase (relevant files, conventions, assumptions). It is stored both as JSON and as a readable spec.md.

Plan and subtasks

The plan breaks the spec into ordered phases of subtasks. A subtask is small enough for one fresh agent session: a title, a precise description, the files to touch, the subtasks it depends on, and how to verify it. Subtasks without dependencies on each other can run in parallel.

Agents and roles

An agent is a model running in a loop with tools. Vibe Factory ships one agent per role: complexity assessor, spec gatherer, researcher, writer and critic, planner, coder, QA reviewer, QA fixer, merge resolver and commit-message writer. Each role has a system prompt, a set of allowed tools, a model and a thinking level. All of it is configurable from .vibe/agents/*.toml without writing code.

Tools

Tools are what agents can do: read, write and edit files, list and search the tree, and run shell commands. Tools are confined to the workspace and governed by a security policy. Plugins can add tools (a browser, a database client, an issue tracker…).

Workspace

Agents never work in your checkout by default. The workspace is a git worktree on a dedicated branch, created per task. When QA approves, the branch is merged (or left for you to review). in_place mode disables isolation for quick experiments.

Pipeline and phases

The pipeline runs the phases assess → spec → plan → build → qa → fix → merge. Each phase reads the artefacts of the previous ones and writes its own into .vibe/tasks/<task>/. A run can be watched live, interrupted, and resumed.

Events, trace and history

Everything a run does is published as an event (a phase started, an agent called a tool, a subtask was committed, the run finished with its totals) and appended to the task’s events.jsonl. Events are the contract between the engine and every interface: the live output of vibe run, vibe events, the terminal UI, the web UI and the macOS app all show events, and none of them keeps a state of its own (Events).

What does not fit in an event is referenced by one. The complete output of each tool call goes to the trace store, .vibe/tool-output/, local and ignored by git, and the tool_returned event points to it (Tool call trace).

A read layer turns events and git back into views: the activity of every task in time order, the history of what a task delivered (runs, commits, changed files, validations, QA verdict, tokens, active time, cost; see History), and the trace of a run’s tool calls. The command line, the terminal UI and vibe serve use the same read layer, so they always agree.

Providers and models

A provider is a model vendor or API (Anthropic, OpenAI, a local Ollama…). Models are referred to as provider/model, for example anthropic/claude-sonnet-5 or ollama/qwen2.5-coder. Each phase can use a different model.

Memory

Memory is what survives a single run: gotchas discovered while coding, conventions, decisions. The default store is a set of files in the task directory; plugins can provide richer back-ends.

Plugins and hooks

A plugin bundles extensions: tools, agents, providers, workspaces, memory stores and hooks. A hook observes or vetoes what agents do (for example “never let an agent run npm publish”) and can enrich prompts with project rules. In-process (Rust) plugins can contribute every kind of extension; out-of-process plugins, written in any language speaking the plugin protocol, contribute tools, agents and the before_tool hook.

Configuration

A project is configured by one file, .vibe/config.toml, written by vibe init. Every key is optional: a missing key takes the default listed below, and a missing file means “all defaults”. This page is the complete reference, generated from the VibeConfig type in crates/vibe-core/src/config.rs.

vibe config path             # where the file is
vibe config show             # the effective configuration (file + defaults)
vibe config show --default   # the built-in defaults only

Top-level keys

KeyTypeDefaultMeaning
default_providerstring"anthropic"provider name used when a model reference has no provider/ prefix
default_modelstring"anthropic/claude-sonnet-5"model used by every phase without a [phases.<phase>] entry
base_branchstringunsetbranch that task worktrees fork from and merge into; unset means the branch currently checked out
[phases.<phase>]tablenonemodel and thinking level for one phase
[providers.<name>]tablethe three built-insmodel back-ends
[pipeline]tablesee belowpipeline tuning
[security]tablesee belowtool security policy
[[plugins]]array of tablesnoneout-of-process plugins to start
[workspace.container]tablenonesettings of the container workspace; see below
[integrations]tablenoneGitHub and GitLab access for vibe task import and vibe pr; see below
[pricing."<provider>/<model>"]tablenonemodel prices, to show what a task cost in vibe history; see below

Unknown keys are ignored by the TOML reader, so check spelling with vibe config show: a key that does not appear there had no effect.

Models and thinking per phase

The phases are assess, spec, plan, build, qa, fix and merge.

[phases.plan]
model = "anthropic/claude-opus-5"   # required in the table
thinking = "high"                    # optional: overrides every agent of the phase
KeyTypeDefaultMeaning
modelstring, requirednoneprovider/model, or a bare model on default_provider, or provider/ for that provider’s default model
thinkingoff, low, medium, high, maxunsetwhen set, thinking level of every agent run during this phase

How a model is chosen for an agent:

  1. an agent that pins a model (model = "provider/model" in .vibe/agents/<role>.toml) uses it;
  2. otherwise [phases.<phase>].model;
  3. otherwise default_model.

How a thinking level is chosen: an agent uses its own level (the built-in table in Customising agents, or the thinking of an agent file) unless the current phase has a [phases.<phase>] table that sets thinking, in which case that value applies to every agent of the phase. A [phases.build] table that sets only model leaves each agent’s own thinking level untouched.

Model references and shorthands are described in Providers and models.

[providers.<name>]

KeyTypeDefaultMeaning
kindstring, requirednoneanthropic, openai, ollama, mock, or a kind registered by a plugin
api_keystringunsetthe key itself; wins over api_key_env; do not commit it
api_key_envstringunsetenvironment variable holding the key; an empty value counts as missing
base_urlstringthe vendor endpointgateway, proxy or self-hosted server
default_modelstringprovider-specificmodel used for provider/ or provider/default
extratableemptykind-specific settings (headers, auth mode, …)

When the file contains no [providers] table at all, these three are used:

[providers.anthropic]
kind = "anthropic"
api_key_env = "ANTHROPIC_API_KEY"

[providers.openai]
kind = "openai"
api_key_env = "OPENAI_API_KEY"

[providers.ollama]
kind = "openai"
base_url = "http://localhost:11434/v1"
default_model = "qwen2.5-coder"

As soon as you declare one [providers.<name>] table, the built-in list is replaced by the tables you wrote. Declare every provider you reference. The extra keys and ready-made recipes (Groq, OpenRouter, xAI, Mistral, mock) are in Providers and models.

[pipeline]

KeyTypeDefaultMeaning
max_qa_roundsinteger3QA reviews per invocation of vibe run; when reached without approval, the run pauses with the task in review (minimum 1)
max_subtask_attemptsinteger3attempts per subtask; the last one uses the coder_recovery agent; then the subtask is failed (minimum 1)
max_parallel_subtasksinteger3subtasks implemented at the same time (minimum 1)
max_phase_retriesinteger2extra planner attempts when the plan is invalid (so 3 attempts by default)
workspacestring"git_worktree"git_worktree, in_place, or a workspace provider registered by a plugin
auto_mergeboolfalsemerge after QA approval and required validations; otherwise stop in ready
validation_commandslist of strings[]mandatory shell checks before ready/merge; see below
max_validation_fix_attemptsinteger2automatic validation fixes over the whole run, including resumes; 0 disables them
merge_strategy"manual" or "assisted""manual"manual reports conflicts for a human; assisted lets a model resolve conflict markers first
isolate_subtasksbooltruewith git_worktree, one worktree per subtask attempt, integrated one at a time; see Workspaces
project_memorybooltruekeep lessons (pitfalls, conventions) in .vibe/memory.jsonl and recall the relevant ones in later tasks; see vibe memory
approvalslist of "spec", "plan", "merge"[]where the run waits for a human decision; see Human approvals
max_tokensintegernoneinput plus output tokens of the whole run, including resumes; the run pauses when reached
max_duration_secsintegernoneactive time of the whole run in seconds, including resumes; the run pauses when reached
trace_outputsbooltruekeep the complete output of every tool call in .vibe/tool-output/<task dir>/<run>/<call>.txt (ignored by git), referenced by the tool_returned events and read by vibe trace --full; see Tool call trace for disk usage and privacy
trace_max_charsinteger100000characters kept per traced tool output; longer outputs are cut and end with a marker line. Only the trace is affected: what the model sees is truncated separately

What these limits do at run time is described in The pipeline; workspaces and merging in Workspaces and merging.

Required validation commands

[pipeline]
validation_commands = ["cargo fmt --all -- --check", "cargo test --offline"]
max_validation_fix_attempts = 2

The default is [] for compatibility. Each configured command is mandatory and runs sequentially from the task workspace root at the start of the merge phase, after normal QA approval and before either marking the task ready or merging it automatically. The pipeline calls the registered bash tool directly, without asking a model to interpret its result. Tool hooks and the existing shell policy still apply. Custom bash tools must return structured exit_code: 0 metadata for success; tool registrations are trusted. The timeout is security.command_timeout_secs (bounded by the shell tool to 1–600 seconds).

A nonzero exit or timeout sends the command, exit status and captured output to the QA fixer. The fixer commits its changes, a fresh QA review runs, then every required command runs again before integration. This applies even to the trivial complexity profile. A favorable model verdict alone never satisfies a command check.

max_validation_fix_attempts defaults to 2 and limits automatic validation corrections over the whole run, including resumes. Set it to 0 for manual correction only. Attempts are reserved and saved before invoking the fixer, so an interrupted attempt still consumes one. The existing QA review/fix limits also remain in effect. After exhausting the validation budget, the run pauses with task status review. A denied/empty command, missing tool or execution error without an exit status pauses immediately for a human; it is not sent to an agent to work around the policy. Later validation commands do not run after a failure.

Results, output and metadata remain in run.json under validations; the persisted validation_fix_attempts counter and pending_validation_fix index track correction and recovery. Notes are also written to progress.md. Correct the workspace or configuration and use vibe run <task> --resume: the gate rechecks all commands without granting fresh automatic attempts. To grant more attempts explicitly, increase the configured limit. Cancellation is checked between commands and before merge; a running command remains bounded by its timeout.

Use checking commands, not formatters that rewrite sources. With auto_merge = true, the commands also run on an isolated integration candidate that combines the latest target branch and the task, after any assisted conflict resolution. Validation records include integration (false for task checks, true for integration checks) and workspace_root. A failed integration check pauses for human review without spending task-fixer attempts; the target branch is not updated. Resuming rebuilds a candidate from the current target.

The git provider rejects tracked changes, new non-ignored files or commits left by a check in the candidate. Build outputs must be ignored. It also refuses publication if the target branch, current checkout, tracked files or guarded repository configuration changed during validation. Only the already-tested commit can be fast-forwarded into the target. Temporary candidate worktrees are removed after success or failure; the persisted output and progress notes remain. This validates repository content, not production deployment.

With no validation commands the previous merge behavior is preserved. In-place mode has no separate integration: checks run again on the project itself, with no rollback guarantee. Third-party workspace providers must implement merge_validated to support gated automatic integration; the default refuses it rather than falling back to an unchecked merge. External manual git merge commands are outside this gate. Configuration is loaded by the host, not accepted from an agent’s QA report.

Run budgets

[pipeline]
max_tokens = 2_000_000      # input + output tokens over the whole run
max_duration_secs = 3600    # one hour of active work

Both limits are off by default. They apply to a run, not to one invocation of vibe run: run.json keeps the tokens (usage) and the active time (active_ms) of every invocation, and a resumed run starts from those totals. Time spent paused or between invocations is not counted.

Agents add the tokens of every model call to the budget as it happens. Once a limit is reached no new model call starts: running sessions stop before their next step, interrupted subtasks go back to pending, and the run pauses (exit code 2) at the phase it was in, with a note in progress.md naming the limit. A running shell command is not interrupted; it is bounded by its own timeout. Resuming without raising the limit pauses again immediately and spends nothing. Raise the limit in the configuration, or for one invocation with vibe run <REF> --resume --max-tokens N --max-duration 2h, to continue.

The token limit counts every model call of the run: the agents’ calls and the ones made by merge_strategy = "assisted" to resolve conflict markers.

Human approvals

[pipeline]
approvals = ["plan", "merge"]

Each listed gate pauses the run (exit code 2, task review) until a human decides:

GateThe run stopsA rejection goes back to
specafter the specification, before planning (only when the profile writes a spec)the spec phase
planafter the plan, before buildingthe planner
mergeafter QA approval, before the merge phase (validation, integration or marking ready)the fixer, through a QA report titled “Human review”, then QA again
vibe task show 3                       # read the spec, plan or QA report
vibe approve 3 --comment "ok"          # or:
vibe reject 3 --reason "Split the migration into its own subtask"
vibe run 3 --resume

The decision is stored in run.json (approvals, pending_approval, rejection) and logged as an approval_resolved event. A rejection needs a reason: it is handed to the agents that redo the work. Regenerating an artefact revokes its approval, so a new plan is approved again, and a merge is approved again after any new build or fix. Resuming without a decision pauses again at the same gate.

[workspace.container]

Settings of the container workspace (pipeline.workspace = "container"). Only image is required; unknown keys are refused.

KeyTypeDefaultMeaning
imagestringrequiredimage the commands run in
runtimestring"docker"docker or podman, or a path to one of them
networkstring"none"none, bridge or a named network; attached only when security.allow_network is true
mountslist of tables[]extra bind mounts: source, target, read_only (default true)
cpusnumbernone--cpus
memorystringnone--memory and --memory-swap, such as 4g
pids_limitinteger1024maximum number of processes
tmp_sizestringruntime defaultsize of the /tmp tmpfs
userstringowner of the worktree (Unix)uid[:gid] or name[:group]
envlist of names[]host environment variables passed through
mount_git_metadataboolfalsemount the repository’s git directory read-only (Unix only)

See Container workspace for what each setting allows.

[security]

KeyTypeDefaultMeaning
blocked_commandslist of strings[]programs denied in addition to the built-in list
allowed_commandslist of strings[]when non-empty, only these programs (plus harmless builtins) may run
command_timeout_secsinteger120default timeout of a shell command; an agent may ask for up to 600
allow_networkboolfalsegrant the network permission (curl, wget, ssh, … and network-using tools)
extra_read_pathslist of paths[]directories outside the workspace that agents may read, never write
web_allowed_domainslist of strings[]domains (and subdomains) web_fetch may reach; empty means any public host
search_urlstringnoneSearXNG-compatible search URL with {query}, e.g. https://searx.example/search?q={query}&format=json; registers web_search

The rules behind these keys are in Tools and security.

[integrations]

[integrations.github]
api_url = "https://github.example.com/api/v3"   # GitHub Enterprise; default https://api.github.com
token_env = "GHE_TOKEN"                         # default GITHUB_TOKEN, then GH_TOKEN

[integrations.gitlab]
api_url = "https://gitlab.example.com/api/v4"   # default https://gitlab.com/api/v4
token_env = "GITLAB_TOKEN"

Used by vibe task import and vibe pr. Tokens are read from the environment only, never from the configuration file.

[pricing]

[pricing."anthropic/claude-sonnet-5"]
input = 3.0          # USD per million prompt tokens
output = 15.0        # USD per million completion tokens
cache_read = 0.3     # optional: prompt tokens served from cache
cache_write = 3.75   # optional: prompt tokens written to cache
KeyTypeDefaultMeaning
inputnumber, requirednoneUSD per million prompt tokens
outputnumber, requirednoneUSD per million completion tokens
cache_readnumberunsetUSD per million prompt tokens read from the cache
cache_writenumberunsetUSD per million prompt tokens written to the cache

Prices are only used to show the cost of a task in vibe history and its web, terminal and API equivalents; they change nothing in a run. There are no built-in prices: without this table no cost is shown. Every price must be a finite number, zero or more; vibe refuses a configuration with any other value.

Events name a model by the provider’s model id, without the provider. A model id is matched first against a key equal to it, then against every key "<provider>/<model id>", the provider being the part of the key before its first /. So claude-sonnet-5 in a log matches "claude-sonnet-5", else "anthropic/claude-sonnet-5"; a model id that contains a / itself, such as meta-llama/llama-3 served by openrouter, matches "openrouter/meta-llama/llama-3". When two providers price the same model id differently, the model counts as unpriced. A local model needs a table too, at zero, or tasks that used it show no cost:

[pricing."ollama/qwen2.5-coder"]
input = 0.0
output = 0.0

A task’s cost is shown only when every agent session it ran has a price that covers the tokens it used: one model without a price, cache tokens without a cache_read or cache_write price, or a session logged before 0.5 (whose model is not recorded) and the cost is left out rather than estimated. The cost is marked incomplete, a lower bound, when a session started but never finished (a crash: its tokens were not logged) or when a run used more tokens than its sessions account for (a call outside any agent session, such as the repair of an invalid structured answer, whose model is not logged).

[[plugins]]

KeyTypeDefaultMeaning
namestring, requirednoneunique name for logs and vibe plugins list
commandlist of strings[]program and arguments speaking the plugin protocol on stdio
envtable of strings{}extra environment variables
cwdpaththe project rootworking directory of the process
capabilitieslist of tools, agents, hooks[]informational; the handshake decides
requiredboolfalsea failure to start stops the run instead of being skipped

See Plugins for manifests under .vibe/plugins/ and user-level plugins.

A complete example

# .vibe/config.toml

# Models ------------------------------------------------------------------
default_provider = "anthropic"
default_model = "anthropic/claude-sonnet-5"   # every phase without its own table
# base_branch = "develop"                     # default: the branch checked out

# A cheap, fast assessment; a strong planner and reviewer; a local coder.
[phases.assess]
model = "anthropic/haiku"
thinking = "low"

[phases.plan]
model = "anthropic/opus"
thinking = "high"

[phases.build]
model = "ollama/qwen2.5-coder:32b"
thinking = "off"

[phases.qa]
model = "anthropic/opus"
thinking = "high"

# Providers: declaring one table replaces the built-in list, so list them all.
[providers.anthropic]
kind = "anthropic"
api_key_env = "ANTHROPIC_API_KEY"

[providers.ollama]
kind = "ollama"
default_model = "qwen2.5-coder:32b"

# Pipeline ----------------------------------------------------------------
[pipeline]
max_qa_rounds = 3           # reviews per `vibe run` before pausing for a human
max_subtask_attempts = 3    # the third attempt uses the recovery agent
max_parallel_subtasks = 2   # keep the local model responsive
max_phase_retries = 2       # extra planner attempts on an invalid plan
workspace = "git_worktree"  # or "in_place"
auto_merge = false          # stop in `ready` and let me merge
merge_strategy = "manual"
max_tokens = 3_000_000      # pause the run beyond this, resumes included
max_duration_secs = 7200    # and beyond two hours of active work
trace_outputs = true        # keep every tool output under .vibe/tool-output/ (git-ignored)
trace_max_chars = 50_000    # at most this many characters per call

# Security ----------------------------------------------------------------
[security]
blocked_commands = ["docker", "terraform"]
allowed_commands = []                # empty: everything not blocked may run
command_timeout_secs = 300           # long test suites
allow_network = false
extra_read_paths = []

# Plugins -----------------------------------------------------------------
[[plugins]]
name = "policy"
command = ["./tools/vibe-policy-plugin"]
required = true                      # never run without the policy hook

Remember that in TOML a key written after a [table] header belongs to that table: put top-level keys such as base_branch and default_model before the first header.

Precedence

For each setting the first source that provides it wins:

  1. command-line flags of vibe run: --complexity, --provider, --model, --workspace, --auto-merge (see Command line reference). --provider and --model replace the model of every phase, but not a model pinned by an agent file;
  2. .vibe/config.toml;
  3. built-in defaults listed on this page.

Agent settings have their own layering, from lowest to highest: built-in agent, agents from plugins, .vibe/agents/*.toml in file-name order; see Customising agents. The thinking of a [phases.<phase>] table still overrides all of them for that phase.

Environment variables

VariableUsed for
ANTHROPIC_API_KEYkey of the built-in anthropic provider (api_key_env)
ANTHROPIC_WORKSPACE_IDworkspace sent as anthropic-workspace-id by every anthropic provider, for an API key that is not scoped to a workspace; an extra.headers entry of the same name wins
OPENAI_API_KEYkey of the built-in openai provider
any name in an api_key_envkey of that provider
RUST_LOGlog filter, for example RUST_LOG=vibe_pipeline=debug,vibe_plugins=debug; -v and -vv are shortcuts
NO_COLORdisables colours, like --no-color

Keys are read when a provider is built, at the start of each command. A provider whose key is missing still loads and fails with AuthFailed on its first call.

Changing values from the command line

vibe config set takes a dotted key and a value, edits the file in place and keeps the rest of it, including comments. Values are parsed as TOML when they are valid TOML (numbers, booleans, arrays, quoted strings) and as plain strings otherwise.

vibe config set pipeline.auto_merge true
vibe config set pipeline.max_parallel_subtasks 1
vibe config set default_model anthropic/claude-opus-5
vibe config set phases.plan.model openai/gpt-5
vibe config set phases.plan.thinking high
vibe config set security.blocked_commands '["docker", "kubectl"]'
vibe config set providers.ollama.base_url http://gpu-box:11434/v1

Missing tables are created. The whole configuration is parsed before the file is written, so a value of the wrong type, or a [phases.<phase>] table without its required model, is rejected (`phases.plan.thinking = high` makes the configuration invalid: …) and the file is left unchanged: set model before thinking. The key name itself is not checked, and a misspelled key is written and then ignored. Without a configuration file, set starts from the defaults and creates it.

Agents

Per-role settings (prompt, tools, pinned model, thinking, step and token budgets) do not live in config.toml but in .vibe/agents/*.toml. vibe agents export <role> writes the built-in definition of a role there as a starting point. The format is described in Customising agents.

Command line reference

The vibe binary drives everything: it creates tasks, runs them through the pipeline, shows their state and inspects the configuration, agents and plugins. vibe help <command> and vibe <command> --help print the same information as this page.

vibe [GLOBAL OPTIONS] <COMMAND>

  init          create .vibe/config.toml
  task          add | list | show | import | discard
  run           run a task through the pipeline
  approve       approve what a paused run waits for
  reject        reject it, with a reason for the agents
  cancel        stop the run of a task, from any terminal
  pr            push a ready task's branch and open a pull request
  memory        list | clear the project memory
  tui           terminal UI: board, activity, history, trace
  serve         HTTP API and web UI
  events        replay or follow the events of one task or of every task
  history       what finished tasks did: runs, commits, files, tokens, cost
  trace         the tool calls of a run: arguments, results, outputs
  status        project summary
  config        show | path | set
  agents        list | show | export
  plugins       list | check
  doctor        check the environment and the configuration
  completions   print a shell completion script

Global options

Global options may appear before or after the subcommand.

OptionEffect
-C, --project <DIR>start directory; the project is this directory (default: the current one) or its nearest parent that contains a .vibe directory, without leaving the git repository (the search stops at the first parent that contains .git) and never climbing up to your home directory; vibe init never searches parents
-v, --verbosemore logging on standard error: default warnings only; -v info, -vv debug, -vvv trace; a non-empty RUST_LOG wins
--jsonmachine-readable output: one JSON document, or one JSON event per line for vibe run and vibe events (see JSON output)
--no-colorno colours; also the case when NO_COLOR is set or the output is not a terminal
-h, --helphelp of the command
-V, --versionversion of vibe

Task references

Commands that act on one task take a <REF>:

FormExampleMatches
number (up to six digits, leading zeros allowed)3, 003the task numbered 3
directory name003-add-oauth-login-githubthe task stored in .vibe/tasks/003-…
id prefix6f1c, 6f1c3e0a-9b2dthe task whose id starts with it; an ambiguous prefix is an error

Numbers are tried first, then directory names, then id prefixes. A reference that matches nothing is an error.

vibe init

vibe init [--force]

Creates .vibe/config.toml in the project directory (the current directory unless -C is given; parents are not searched): a two-line header comment, every key at its default value, and a commented [[plugins]] example at the end, so you can append plugin tables. It refuses to overwrite an existing file unless --force is given, which rewrites the defaults. In a git repository it also writes .vibe/.gitignore, or completes an existing one without duplicating lines, with worktrees/, tool-output/ and a comment explaining that tasks/ is committed on purpose. Outside git it warns that worktree isolation needs git. It ends by printing the next steps; run vibe doctor afterwards. The file is described in Configuration.

vibe task

vibe task add

vibe task add <TITLE> [-d <TEXT> | --description-file <PATH>] [--label <LABEL>]...
OptionEffect
-d, --description <TEXT>the description; - reads it from standard input
--description-file <PATH>read the description from a file; - for standard input
--label <LABEL>add a label; repeat for several

--description and --description-file are mutually exclusive. The task is created in backlog and gets the next number.

vibe task add "Add a --json flag to the export command" \
  -d "Print the exported records as a JSON array when --json is given." --label cli
git log -1 --format=%B | vibe task add "Follow up on the last commit" -d -
vibe task add "Migrate the settings page" --description-file notes/settings.md

vibe task list

vibe task list [--status <STATUS>] [--all]

Lists tasks by number with status, complexity and title. With --json, the output is an array of {"number": …, "task": {…}} objects. done and cancelled tasks are hidden unless --all is given. --status keeps one of backlog, planning, building, review, ready, done, failed, cancelled.

vibe task show

vibe task show <REF>

Shows the task: description, status and complexity, the branch its last run recorded, spec summary and requirements, plan with subtask statuses, the latest QA verdict and issues, the state of the last run (run.json) and the workspace branch and directory. The files it reads are listed in Persistence layout.

vibe task import

vibe task import octo/app#12
vibe task import https://gitlab.com/group/project/-/issues/5
vibe task import gitlab:group/project#5

Creates a task from a GitHub or GitLab issue: its title, its body (followed by a link back) and its labels. The task remembers the issue (source), so importing it twice fails with already imported as …, and vibe pr closes it. Private repositories need a token in GITHUB_TOKEN (or GH_TOKEN) or GITLAB_TOKEN; see [integrations] for GitHub Enterprise and self-managed GitLab. --forge gitlab makes the short form refer to GitLab.

vibe task discard

vibe task discard <REF> [-y | --yes]

Removes the task’s workspace (worktree and branch; nothing for in_place), deletes the task directory and the task’s traced tool outputs (.vibe/tool-output/<task dir>/), so the task leaves the history too. It asks for confirmation on a terminal unless --yes is given; when standard input is not a terminal it refuses to run without --yes. Unmerged work on the task branch is lost.

vibe run

vibe run <REF> [OPTIONS]

Runs the task through the pipeline and renders its progress live.

OptionEffect
--complexity <C>trivial, simple, standard or complex; skips the assessment
--from <PHASE>start at this phase, reusing the persisted spec and plan
--until <PHASE>pause (resumably) once this phase is done
--dry-runonly assess, specify and plan; same as --until plan
--provider <NAME>use this provider for every phase, with its default model unless --model is given; mock selects the built-in mock when no [providers.mock] table exists
--model <PROVIDER/MODEL>use this model for every phase; a bare model name uses the default provider (or --provider)
--workspace <NAME>git_worktree, in_place, or a workspace provider from a plugin
--auto-mergemerge automatically once QA approves
--script <FILE>drive the mock provider with scripted responses from a JSON file; implies --provider mock on every phase and cannot be combined with --provider or --model
--resumecontinue the last run of the task where it stopped
--max-tokens <N>pause the run once it used N tokens, counted across resumes; overrides pipeline.max_tokens
--max-duration <D>pause the run after D of active work, counted across resumes: seconds, or 90s, 15m, 2h; overrides pipeline.max_duration_secs

Phases are assess, spec, plan, build, qa, fix, merge. These flags override the configuration for this run only; nothing is written back. --provider and --model replace default_model and the model of every [phases.*] table but keep their thinking levels; an agent that pins its own model in .vibe/agents/ keeps it. The script format is described in the Quick start.

Exit codes

CodeMeaning
0the task ended ready or done, or the run stopped where --dry-run or --until asked
2the run paused for a human: task in review (QA did not approve, escalation, inconclusive review), or a run budget was reached
1the run failed, or the command itself failed (unknown task, bad configuration, plugin or workspace error)
130the run was cancelled (Ctrl-C, or a hook that aborted a phase)

Cancelling. Ctrl-C cancels the run cooperatively: sessions stop before their next model call, interrupted subtasks go back to pending, and run.json is saved as cancelled. A second Ctrl-C exits immediately with code 130, leaving run.json as running. Either way, vibe run <REF> --resume continues later.

Resuming. --resume continues the last run from the phase recorded in run.json, keeping its run id, profile, spec, plan and completed subtasks. The other options still apply: --from overrides the phase to restart at, --until and --dry-run where to stop. A run that already finished cannot be resumed (already finished; start a new run, exit 1); start a new one. --from <PHASE> instead starts a new run at that phase, reusing the artefacts on disk; use it after fixing something by hand, for example vibe run 3 --from qa. Resume semantics are detailed in The pipeline.

vibe run 1                                   # full pipeline
vibe run 1 --dry-run                         # spec and plan only, then inspect them
vibe run 1 --resume                          # continue after the dry run
vibe run 1 --complexity trivial              # skip spec for a one-line fix
vibe run 1 --model anthropic/claude-opus-5   # one model for every phase
vibe run 1 --provider ollama                 # local model, provider's default
vibe run 1 --workspace in_place --auto-merge # no worktree, nothing to merge
vibe run 3 --from qa                         # review again after a manual fix

The run starts with a line naming the task, the build model and the workspace, renders the events live (see the Quick start for a sample), and ends with a table of the phases of this invocation, the final status, duration and tokens, the branch, worktree and task directory, and a hint for the next command.

With --json, vibe run prints the pipeline events as they happen, one JSON object per line, in the Envelope format of events.jsonl (see Domain model), then one last object of type summary with run_id, task_id, final_status, run_status, success, exit_code, duration_ms, usage, phases, last_error, branch, worktree and task_dir:

vibe run 1 --json | jq -r 'select(.event.type == "phase_finished") | "\(.event.phase): \(.event.summary)"'

vibe approve and vibe reject

vibe approve <REF> [--comment TEXT]
vibe reject <REF> --reason TEXT

Answer the approval a paused run is waiting for (see Human approvals). The run is not resumed: continue it with vibe run <REF> --resume. Both fail when the run is not waiting for an approval.

vibe cancel

vibe cancel <REF> [--wait]

Asks the process that runs the task (a vibe run in another terminal, for example) to stop after its current step, as Ctrl-C would. --wait returns once the run has stopped. It fails when no process runs the task. Only one process at a time can run a given task: a second vibe run fails with already being run by another process.

vibe pr

vibe pr <REF> [--repo OWNER/REPO] [--forge github|gitlab] [--remote origin] [--base BRANCH] [--draft] [--no-push]

For a task in ready with the git_worktree or container workspace: pushes its branch to the remote and opens a pull request (a merge request on GitLab) into the branch the task was forked from. The description summarises the spec (or the task), lists the subtasks, gives the last QA verdict and the required checks, and ends with Closes #N when the task was imported from an issue of the same repository. The repository is read from the remote URL unless --repo is given; the forge from the remote host (or the imported issue), else GitHub. A token is required (GITHUB_TOKEN or GITLAB_TOKEN). The URL is added to progress.md.

vibe memory

vibe memory list [--query TEXT]
vibe memory clear [--yes]

The project memory holds lessons learnt by earlier tasks: findings of the spec phase (conventions), subtasks that failed repeatedly and QA issues that fixes could not solve (pitfalls). They are kept in .vibe/memory.jsonl, once each, and the ones sharing words with a new task’s title and description are given to its agents as “Recalled from project”. list shows them newest first, or the ones relevant to --query; clear forgets them. Set pipeline.project_memory = false to turn the memory off; plugins can register other memory stores.

vibe tui

vibe tui

A terminal view of the project with three screens, listed in the title bar: Tasks (T), Activity (A) and History (H). q quits from any of them; Esc closes what is open, then goes back to Tasks, and quits only from Tasks (not from its Trace tab).

Tasks is the board: tasks on the left (a ● marks the ones a process is running, whichever terminal started them), the selected task on the right with its run state, a budget gauge and four tabs: Activity (phases, agents, tools, validations, approvals, and the text of the current step as the model writes it), Plan (subtasks and their status), Changes (the workspace diff summary) and Trace (the tool calls of the last run).

KeyAction
↑ ↓ (or k j)select a task
nnew task (type the title, Enter)
r / Rrun / resume the selected task
ccancel its run (also one started in another terminal)
a / xapprove / reject what the run waits for (a rejection asks for a reason)
Tabnext tab
gread the tasks again
qquit; runs started from the UI are cancelled and stay resumable

The Trace tab lists the calls of a run, one per line: seq role subtask tool duration exit err (error when the tool failed, timeout, or no result for a call that never returned). While it is shown, ↑ ↓ move among the calls instead of the tasks (Tab to another tab to select tasks again); the other task keys keep working.

Key (Trace tab)Action
↑ ↓select a call (in an expanded call or its output: scroll)
[ / ]previous / next run, when the task has several
Enterexpand the call: its arguments as JSON and a preview of its output
oload its complete output from .vibe/tool-output/ into a scrollable pane (PgUp PgDn); a note says when it was not traced or is gone, and when the call was paired with its result by order (logs from before 0.5)
Escclose the output, then collapse the call
gload the calls again (they are also loaded each time the tab is entered)

Activity is the feed of every task’s events, oldest first, each prefixed with the task number (#003). It opens on the last 200 events and follows new ones.

Key (Activity)Action
↑ ↓ (PgUp PgDn)select an event; the feed stops following
ffollow the newest event again, or stop
1 … 7show or hide agents, tools, phases, git, approvals, budget, logs
Enteropen the event’s task on the Tasks screen
gload the last 200 events again (also done each time the screen is entered)

A log that cannot be read is reported once in the status line; the other tasks keep coming.

History lists the finished tasks (ready and done; a adds failed and cancelled ones) as vibe history does: number, title, status, runs, commits, changed files, tokens, active time, cost (- without a [pricing] table), with the notation of vibe history: + marks a lower bound, ~ an approximate file count; finished is the end of the last run, else the last activity. Enter opens the task: its runs (state, start, duration, tokens), commits, changed files (status, where the list comes from, whether it is approximate), validations, last QA report and errors; ↑ ↓ scroll it and Esc closes it. The list is loaded each time the screen is entered; g loads it again.

History and trace data are loaded in the background on entry, so the screen stays responsive on large projects (“Loading…” shows meanwhile).

The UI reads the same files and events as the other commands, so vibe run, vibe approve or vibe cancel in another terminal show up in it. Runs use the project configuration (there are no --provider or --model flags; set default_model or [phases]). It needs an interactive terminal.

vibe serve

vibe serve [--bind 127.0.0.1] [--port 7777] [--provider NAME] [--model P/M] [--workspace NAME] [--script FILE] [--evals DIR] [--exit-on-stdin-eof]

Serves an HTTP API and a web UI for the project. It prints the address and a link that carries the access token (http://127.0.0.1:7777/#token=…); open it in a browser. The web UI shows the task board, the selected task with its run, budget, a live activity feed with the text as the model writes it, the plan, spec, QA reports, tool-call trace and workspace changes, and lets you create, run, resume and cancel tasks and approve or reject what a run waits for. Its views:

  • Tasks: the board and the selected task. Its Trace tab lists every tool call of a run (role, subtask, tool, duration, exit code, a badge for errors and for calls logged before 0.5 that were paired by order); a call expands to its arguments as JSON and its output preview, and Load complete output fetches the whole traced output. A selector picks the run when there are several; the footer counts calls and errors and lists the files written.
  • Activity: every task’s events as they happen, with toggles per group of event types (agents, tools, phases, git, approvals, budget, logs), a task filter and a pause button. Clicking a line opens its task.
  • History: what each finished task delivered (the columns of vibe history), with failed and cancelled tasks on demand; a row expands to its runs, commits, changed files (with their source and whether the list is approximate), validations, last QA verdict and problems.
  • Evaluations: with --evals DIR, every summary.json that evals/run_suite.py wrote under DIR (success rate, time, tokens per case and model).

The page reads one event stream, GET /api/stream, for the whole project; it resumes from the last event id when the connection comes back, and polls the task list every 10 s (every 2 s while the stream is down) since creating a task is not an event.

Security: the server listens on the loopback interface by default and then only accepts its own host names (127.0.0.1, localhost, [::1]), which blocks DNS rebinding. Every API call needs Authorization: Bearer <token>; the token is random per start, printed once and written to .vibe/server.token (owner-only on Unix), and removed on exit. The two event streams, /api/stream and /api/tasks/{ref}/stream, also accept ?token=, because browsers cannot set headers on them; no other route does. The API never returns the configuration or keys. --bind with another address prints a warning: anyone who reaches it with the token controls the agents. Ctrl-C stops the server and cancels the runs it started (they stay resumable).

--exit-on-stdin-eof makes the end of standard input stop the server exactly like Ctrl-C (runs cancelled, token file removed, exit code 0). An application that starts vibe serve as a child with a pipe on its standard input (the macOS app does) uses it so that the server does not outlive it, even when the application is killed or crashes. Without the flag, standard input is not read.

Method and pathEffect
GET /api/healthname and version
GET /api/tasksevery task with its number, whether it runs, and its last run’s status, phase and pending approval
POST /api/tasks {"title", "description"}create a task
GET /api/tasks/{ref}task, run state, spec, plan and QA reports
POST /api/tasks/{ref}/run {"resume": bool}start or resume a run (202)
POST /api/tasks/{ref}/cancelstop its run, from this server or another process
POST /api/tasks/{ref}/approve {"comment"}approve what the run waits for
POST /api/tasks/{ref}/reject {"reason"}reject it
GET /api/tasks/{ref}/changesworkspace changes summary
GET /api/tasks/{ref}/events?after=SEQ&all=boollogged events
GET /api/evalsevaluation summaries found under --evals
GET /api/tasks/{ref}/stream?after=SEQserver-sent events: logged events after SEQ, then new ones and streamed text; each SSE event is named by its type and carries the envelope, with the sequence number as id
GET /api/events?after=CURSOR&since=TIME&type=T&task=REF&limit=Nlogged events of every task as tagged envelopes ({"task", "number", "schema", "seq", "at", "event"}), oldest first, the last N (default 1000, at most 10000) after the filters; see below
GET /api/stream?after=CURSOR&since=TIME&type=T&task=REFserver-sent events of every task: logged events after the starting point, then new ones (tasks created later included) and the streamed text of runs this server started; see below
GET /api/history?all=boolhistory of every finished task (vibe history --json); all adds failed and cancelled ones
GET /api/history/{ref}history of one task, finished or not (vibe history REF --json)
GET /api/tasks/{ref}/trace?run=RUN&all=booltool calls of the last run, of the run whose id starts with RUN, or of every run with all (a list), as vibe trace --json; 404 when the task has not been run (where vibe trace --json prints null or []) or no run matches RUN, 400 when RUN matches several runs or comes with all
GET /api/tasks/{ref}/trace/{call}/outputcomplete output of a call, as text/plain, cut at 8 MiB (then marked at the end and with X-Vibe-Truncated: true); 400 for an id that is not 12 hex digits, 404 when the call is unknown or its output was not traced, is gone or cannot be read

{ref} is a task number, directory name or id prefix, as on the command line; a prefix that matches several tasks is a 400. Errors are {"error": "…"} with 400 (including an unknown event type, a malformed cursor, time or limit in /api/events and /api/stream), 401, 403, 404, 409, or 503 when too many event streams are open.

Project-wide events are ordered by an event cursor, <nanoseconds since the epoch>-<task number>-<seq> (1790000000123456789-3-17): by time, then task, then sequence number, so events of two tasks logged at the same instant are neither lost nor repeated on resume. In /api/events and /api/stream, after=CURSOR keeps the events after it and since=TIME those after that time (RFC 3339, or an age such as 30m, 2h, 1d, as vibe events --since); type (an event type) and task (a task reference) repeat, and each list keeps events that match one of its values. /api/events answers with two headers: X-Vibe-Cursor, the cursor of the last event read before the filters (the value to pass as after to go on from there, absent when nothing was read), and X-Vibe-Read-Errors, the number of task logs that could not be read (their events are missing; the server logs why). In /api/stream, each SSE event is named by its type and its data is the tagged envelope; logged events carry their cursor as id, and a client that reconnects with Last-Event-ID resumes after it (the header wins over after). Streamed text (agent_delta) has no id and is not replayed. Without after, since or Last-Event-ID, the stream starts from now. A comment line is sent every 15 s to keep an idle connection open. All streams share one reader of the logs, which runs while at least one is open; at most 32 streams are open at once (503 beyond).

Cursor order is the order of the at times each process writes. Events of one task always come in order, but a vibe run in another process whose clock reads earlier than the server’s can log an event with a cursor below one already sent for another task. An open stream still sends it, once, so ids are not always increasing; but a client that was disconnected while it was logged and resumes from its last id does not get it, since it sorts before that id. Reload the events of a task (/api/tasks/{ref}/events) when its state must be exact.

Trace outputs are only read from .vibe/tool-output/: paths recorded elsewhere are ignored, and a file there that is a link leading out of it is answered like a missing output (404, the reason in the server’s log).

vibe events

vibe events <REF> [--after SEQ] [--follow | --all] [--since TIME] [--type TYPE]...
vibe events [--follow] [--since TIME] [--type TYPE]... [--task REF]...

With a <REF>, replays the logged events of the task’s last run, rendered like vibe run (or as JSON envelopes with --json). --after SEQ skips events up to that sequence number, --follow keeps printing new events until the run ends (use it from another terminal while vibe run works), and --all shows every run of the task.

Without <REF>, it prints the activity of every task, merged in time order, each line prefixed with the task number (#3 ✓ plan: …). --follow prints the events logged from now on, including those of tasks created meanwhile, until Ctrl-C (exit code 0); with --since, it first prints what was logged since then. With --json, each line is an envelope carrying its task: {"task": "<id>", "number": 3, "schema": 2, "seq": …, "at": …, "event": {…}}. A task whose log cannot be read is reported once on standard error and the feed goes on with the others.

OptionEffect
--since TIMEonly events logged after TIME: RFC 3339 (2026-09-27T10:00:00Z) or an age (90s, 30m, 2h, 1d)
--type TYPEonly events of this type (repeatable), e.g. --type run_finished --type committed; an unknown type is an error
--task REFonly the events of this task (repeatable); with <REF> as well, nothing is printed unless <REF> is one of them

--after and --all need a <REF>. With --follow and a <REF>, the end of the run stops the command even when --type or --since hide the run_finished event; a run that paused and was resumed is followed to its new end. As with vibe run, some events are only rendered with -v (agent text, committed, artefact_written, subtask integrations without conflicts, informational logs): add -v, or use --json, to see them when you filter on them. The envelope and every event type are described in Events.

vibe history

vibe history [--all]
vibe history <REF>

What finished tasks did, read from their event logs, run.json and git (nothing is recomputed by a model). History of finished work explains each figure and where it comes from. Without <REF>, a table of the ready and done tasks, most recent first — --all adds failed and cancelled ones:

#  title               status  runs  commits  files  tokens  active      cost      finished
3  Add OAuth login     done    2     5        7      184.2k  6 min 12 s  1.84 USD  2 h ago
1  Fix typo in README  ready   1     1        1      2.9k    4.1 s       -         1 d ago

tokens counts input plus output tokens of every run, active the time the runs were working (pauses excluded); a + on them marks a lower bound: some run was logged before 0.5 (its totals are unknown) or did not finish. cost is - without a [pricing] table, or when a model the task used has no price in it; a + on it marks a lower bound: some tokens were used outside a finished agent session (a session cut by a crash, or calls such as the repair of an invalid structured answer) and are not priced. files is marked ~ when the list is approximate.

With <REF> (any task, finished or not), the detail:

  • the task, its branch (the one its run recorded, or the task branch git still has) and its totals;
  • one block per run: its state (running, finished, or interrupted when the process died), when it started, how many times it was resumed, its active time, tokens and cost, its phases with their result, the merge, a pending approval and the last error;
  • the commits (short sha, first line of the message, number of files, subtask);
  • the changed files with their status (A, M, D, R, C, ? when unknown) and where the list comes from: the recorded merge, the diff of the task branch against its base, the commit events, or the files the agents wrote (the last two are marked approximate);
  • the validation commands of the last run that ran them, and whether they passed;
  • the last QA verdict and its summary;
  • problems met while reading (a missing merge commit, an unreadable file).

With --json, the output is the TaskHistory document (an array of them without <REF>): task, number, runs (with state, usage, active_ms, totals_known, phases, commits, merged, validations, approvals, last_error), totals, changed_files (source, approximate, files), last_qa, validations_passed, cost, last_activity and errors.

vibe trace

vibe trace <REF> [--run RUN | --all] [--tool NAME] [--subtask ID] [--full]

The tool calls of the task’s last run (--run picks another by id or id prefix, --all shows every run), in the order they were made. Tool call trace describes the trace store, its disk usage and what it may contain. Each call is a block:

#4 coder · subtask 1/2 Write hello.txt · write_file  2 ms  ok  [call 5f0c2a9e1b7d]
    {
      "content": "Hello, world!\n",
      "path": "hello.txt"
    }
  ⟵ output
    Created `hello.txt` (14 bytes).

The number is the call’s position in the run (it stays the same when filters hide other calls), then the agent role, the subtask, the tool, the duration and the result: ok, exit N for commands, error, timed out, or no result for a call that never returned (the run was interrupted). The call id pairs the call with its result; logs recorded before 0.5 have none (-) and are paired by order, which is shown as paired: by order. Errors are shown in red.

The arguments are printed as JSON, with strings longer than 2 000 characters cut. The output is the preview logged with the event; --full prints the complete arguments and the complete output from the trace store (.vibe/tool-output/, see pipeline.trace_outputs), or says why it is not there: tracing was off or the run predates 0.5, or the file was removed (vibe task discard, manual cleanup). A footer counts the calls and the errors and lists the files written by write_file and edit_file.

--tool and --subtask (id or id prefix) keep only the matching calls. With --json, the output is the RunTrace document — run, calls (each with run, call, role, subtask, tool, input, called_at, returned_at, duration_ms, is_error, exit_code, timed_out, preview, output_chars, output_file, paired) and files_written — or an array of them with --all; --full does not change it (read output_file for the complete output). A task that has not been run prints Task has not been run yet. (null, or [] with --all, with --json).

vibe status

vibe status

Project summary: the number of tasks per status, a table of recent runs (task, run status, phase, last update) and the active task worktrees.

vibe config

vibe config show [--default]
vibe config path
vibe config set <KEY> <VALUE>
SubcommandEffect
showprints the effective configuration as TOML (file merged with defaults)
show --defaultprints the built-in defaults
pathprints the path of .vibe/config.toml
set <KEY> <VALUE>sets a dotted key; the value is a TOML literal (true, 3, ["a", "b"]) or else a plain string
vibe config set pipeline.auto_merge true
vibe config set phases.plan.model anthropic/claude-opus-5
vibe config set security.blocked_commands '["docker"]'

vibe agents

vibe agents list
vibe agents show <ROLE>
vibe agents export <ROLE> [--dir <DIR>] [--force]
SubcommandEffect
listevery agent with its tools, thinking level, model, source and description; the source is builtin, override (a built-in changed by .vibe/agents/) or custom
show <ROLE>the system prompt of the agent after overrides (the whole AgentSpec with --json)
export <ROLE>writes <role>.toml with every setting and <role>.md with the prompt into .vibe/agents (or --dir); refuses to overwrite either file without --force

These commands read the built-in agents and .vibe/agents/; they do not start plugins, so agents contributed by plugins are not listed. An invalid override file makes them fail with the file name, which makes vibe agents list a quick check of your overrides. Roles and override files are described in Customising agents.

vibe plugins

vibe plugins list
vibe plugins check

list shows the declared and discovered plugins with name, source (config for [[plugins]] entries, manifest for .vibe/plugins/ and the user directory), whether they are required, their declared capabilities and command. It starts nothing. check starts every plugin, prints its version, the capabilities from its handshake and its tools, or the error, then stops it; it exits with 1 when any plugin failed. Use it to debug a plugin before a run. See Plugins.

vibe doctor

vibe doctor

Checks the environment and the configuration and prints one line per check, marked ✓ (ok), ! (warning) or ✗ (failure):

CheckFails when
gitgit --version does not run
config.vibe/config.toml does not parse (a missing file is only a warning)
repositorythe project is not a git repository and the workspace is git_worktree
providers / providerthe provider table is invalid, a provider used by default_provider, default_model or a [phases.*] model is not declared, or a used provider has no key (an unused one only warns)
plugina plugin with required = true does not start (an optional one only warns)
workspacepipeline.workspace is neither built in nor possibly provided by a plugin
storage.vibe/ (or the project directory) is not writable

The exit code is 1 when any check fails, 0 otherwise; --json prints {"ok": …, "checks": [{"level", "name", "message"}]}. Agent files are not checked here; vibe agents list reports an invalid one. Run vibe doctor after vibe init and whenever a run fails before its first phase.

vibe completions

vibe completions <SHELL>

Prints a completion script for bash, zsh, fish, elvish or powershell. Installation is shown in Installation.

Exit codes

Every command exits with 0 when it did what was asked and 1 when it failed: an unknown task reference or run, a bad configuration, a storage or git error, a --run or --subtask prefix that matches nothing or several ids. A usage error (an unknown option, an invalid value such as an unknown --type, conflicting options) exits with 2 before anything runs. vibe run has its own codes (above): 2 there also means the run paused for a human, and 130 that it was cancelled. vibe doctor and vibe plugins check exit with 1 when a check fails. vibe history, vibe trace, vibe events and JSON documents exit with 0 when their output is closed early (vibe history | head).

JSON output

--json replaces human-oriented output with one JSON document on standard output, suitable for scripts; logs still go to standard error. Field names follow the persisted types (Domain model) and statuses are the snake_case names listed above. task add prints number, id, dir and task; task list an array of {number, task}; task show the number, task, dir, spec, plan, qa_reports, run and worktree (branch, path and whether each exists); history the TaskHistory documents and trace the RunTrace documents described above; init the paths it wrote. vibe run --json and vibe events --json are the exceptions: a stream of events, one per line (then the summary for vibe run).

History of finished work

vibe history answers “what did the agents deliver, and what did it cost?” for every task, long after the run that did the work. Nothing is asked of a model and nothing extra is stored: the history is rebuilt each time from what the runs already left in .vibe/ and in git (ADR-008).

The table

vibe history            # ready and done tasks, most recent first
vibe history --all      # failed and cancelled tasks too
#  title                      status  runs  commits  files  tokens  active  cost  finished
2  Write the greeting module  done    1     1        1      6.7k    251 ms  -     just now
1  Fix typo in README         done    1     0        0      2.9k    187 ms  -     just now
ColumnMeaning
runsruns of the task; a run and its resumes count once
commitscommits the pipeline made for the task (checkpoints, subtasks, fixes, integrations)
filesfiles the task changed, ~ when the list is approximate (see below)
tokensinput plus output tokens of every run, resumes included
activetime the runs were working, pauses excluded
costmoney spent, only with a [pricing] table; - otherwise
finishedend of the last run, else the last activity

A + after tokens, active or cost marks a lower bound: a run was logged before 0.5, whose totals are unknown, or has not finished, or some tokens were used outside a finished agent session and are not priced. A legend under the table repeats this when it applies.

One task in detail

vibe history 2
#2 Write the greeting module
  status    done
  branch    vibe/write-the-greeting-module-6fed323c
  tokens    6.3k in / 365 out
  active    251 ms
  activity  just now

Runs (1)
  4ef9f554 finished → done, started just now
    251 ms active, 6.3k in / 365 out tokens
    phases  assess ✓  spec ✓  plan ✓  build ✓  qa ✓  merge ✓
    merged  vibe/write-the-greeting-module-6fed323c into main (62a82acb)

Commits (1)
  62a82acb  vibe: complete subtask 1 - Implement the task (1 file(s)) · subtask 1/1 Implement the task

Files (1) from merge 62a82acb (fast-forward)
  A  hello.txt

QA round 1: approved, 0 issue(s)
  Approved by the mock provider.

Any task can be shown, finished or not. Each run shows its state: finished, running when a process holds the task’s run lock, or interrupted when the process died before the run ended (vibe run <REF> --resume continues it). The detail ends with the validation commands of the last run that ran them, the last QA verdict, and the problems met while reading (a merge commit git no longer has, a file that does not parse), if any.

Where the data comes from

PartSource
runs, phases, approvals, errorsthe run_started … run_finished events of events.jsonl; run.json completes the last run
tokens, active timethe totals of each run’s last run_finished (resumes included); run.json for the last run when its log predates 0.5 or it has not finished
commitsthe committed events (and subtask_integrated for older logs)
mergethe merged event
validationsthe validation_finished events
QA verdictthe last qa_report_<n>.json
changed filesgit, else the events (below)
costthe agent_started model and agent_finished usage of each session, and [pricing]

Changed files and the approximate marker

The list of changed files comes from the most exact source available, and the detail says which one it used (from …):

  1. the recorded merge (merge <sha>): the files the merge brought into the base branch. A fast-forward is compared from where the task’s first recorded commit started;
  2. the task branch (diff of <branch> against <base>), while it exists and differs from its base;
  3. the commit events (commit events): every file of every committed event, statuses unknown;
  4. the files written by the agents (files written by the agents): paths passed to write_file and edit_file calls that succeeded;
  5. nothing recorded.

Sources 3 and 4 are approximate: they can list a file a later commit reverted, and the fourth misses files that shell commands wrote. The first is approximate in one case: a fast-forward that contains none of the task’s recorded commits. Approximate lists are marked ~ in the table and approximate in the detail. Statuses are A added, M modified, D deleted, R renamed, C copied, ? unknown. The rules are detailed in Persistence layout.

A project outside git, or a task run with the in_place workspace, has no branch to diff: its files come from the events.

Tokens, time and cost

Tokens and active time are always shown. A money cost is shown only when you give prices:

[pricing."anthropic/claude-sonnet-5"]
input = 3.0
output = 15.0
cache_read = 0.3
cache_write = 3.75

The cost of a task is computed only when every agent session it ran used a model with a price that covers its tokens; otherwise it is -, never an estimate. Matching rules and the cases that make a cost a lower bound are in [pricing].

JSON and other interfaces

vibe --json history prints an array of TaskHistory documents, vibe --json history <REF> one of them; the fields are listed in the command line reference. The same documents are served by GET /api/history?all=bool and GET /api/history/{ref} (vibe serve), shown by the History view of the web UI (a table whose rows expand to the detail) and by the History screen of vibe tui (H). Clients of vibe serve, such as the macOS app in apps/macos/VibeFactory, read the same routes.

To see how a run got there, call by call, use vibe trace.

Tool call trace

vibe trace shows how a run did its work: every tool call an agent made, with its complete arguments, how long it took, how it ended, and its output. It is the answer to “which command failed, and what did it print?” once the live output of vibe run is gone.

Reading a trace

vibe trace 2                     # the last run of task 2
vibe trace 2 --full              # with complete arguments and outputs
vibe trace 2 --tool bash         # only the shell commands
vibe trace 2 --all               # every run of the task
vibe trace 2 --run 4ef9          # one run, by id or id prefix
vibe trace 2 --subtask 7c1e      # the calls made for one subtask
── run 4ef9f554 ──────────

#1 coder · subtask 1/1 Implement the task · write_file  1 ms  ok  [call 2e5c7f564a52]
    {
      "content": "Hello, world!\n",
      "path": "hello.txt"
    }
  ⟵ output
    Created `hello.txt` (14 bytes).

#2 coder · subtask 1/1 Implement the task · bash  7 ms  error, exit 1  [call 4b5fd969429a]
    {
      "command": "cat hello.txt; ls nope"
    }
  ⟵ output
    Hello, world!
    ls: nope: No such file or directory

    Exit code: 1

2 call(s), 1 error(s)
files written: hello.txt

Each block starts with the call’s position in the run, the agent role, the subtask (its position and title in the plan), the tool, the duration and the result: ok, exit N for a command, error, timed out, or no result when the run was interrupted before the tool returned. The call id at the end pairs the call with its result. The footer counts the calls and errors and lists the files written by write_file and edit_file. The options are described in the command line reference.

Without --full, the output is the 200-character preview logged with the event, and strings longer than 2 000 characters in the arguments are cut. --full reads the complete output from the trace store and prints the arguments whole.

The trace store

While a run works, the complete output of every tool call is written to

.vibe/tool-output/<task dir>/<run>/<call>.txt

at the project root, for example .vibe/tool-output/002-write-the-greeting-module/4ef9f554-…/4b5fd969429a.txt. The tool_returned event records that path (output_file) and the length of the complete output (output_chars), so the log stays small and the output stays one read away (Events).

Two settings control it ([pipeline]):

KeyDefaultEffect
trace_outputstruewrite the store; false keeps only the previews in the log
trace_max_chars100000characters kept per call; a longer output is cut and ends with [vibe: output cut to the first N of M characters]

The store is independent of what the model sees. An output longer than the runner’s limit is still truncated for the model, and its complete text is also written in the workspace (<workspace>/.vibe/tool-output/<uuid>.txt) where the agent can read it, so such outputs exist twice.

Disk usage

The store grows with what the tools print: at most trace_max_chars characters per call, usually far less (a write_file result is one line; a test suite’s output can reach the cap). Nothing is removed automatically. To see and reclaim the space:

du -sh .vibe/tool-output/*/                      # per task
rm -rf .vibe/tool-output/002-write-the-greeting-module/<run>   # one run's outputs
vibe task discard 2 --yes                        # the task, its workspace and its outputs

Removing files only affects --full and the complete output in the other interfaces: vibe trace then says the output file is missing and shows the preview. Lower trace_max_chars to keep less per call, or set trace_outputs = false to stop writing the store.

Privacy

Tool outputs can contain secrets: a command that prints an environment variable, a configuration file an agent read, a token in an error message. The trace store stays on your machine: vibe init puts tool-output/ in .vibe/.gitignore, so it is never committed, and nothing sends it anywhere.

Two things are worth knowing:

  • the event log, .vibe/tasks/*/events.jsonl, is committed with the task by default (tasks/ is project history on purpose), and it holds each call’s complete arguments and the 200-character preview of its output. Add tasks/*/events.jsonl to .vibe/.gitignore if that is too much to share (What to commit);
  • vibe serve returns complete outputs (GET /api/tasks/{ref}/trace/{call}/output) to anyone holding its token. It listens on the loopback interface by default; with another --bind address, anyone who can reach it with the token can read them.

Logs recorded before 0.5

Runs logged by an earlier version have no call ids and no stored outputs. vibe trace pairs their calls with their results by order, per role, tool and subtask, and shows paired: by order; --full reports that the output was not traced and shows the preview.

JSON and other interfaces

vibe --json trace <REF> prints the RunTrace document (an array of them with --all): run, calls and files_written, each call with its arguments, times, result, preview, output_chars and output_file. --full does not change it; read output_file for the complete output. The same documents are served by GET /api/tasks/{ref}/trace and the complete output by GET /api/tasks/{ref}/trace/{call}/output (vibe serve). The web UI shows them in the Trace tab of a task (a call expands to its arguments and output; Load complete output fetches the whole text), and vibe tui in the Trace tab of the task detail (Enter expands a call, o loads its complete output).

For what the task delivered in the end (commits, changed files, tokens, cost), see History.

Providers and models

A provider is a model back-end: Anthropic, any API speaking the OpenAI Chat Completions dialect (OpenAI, Groq, Mistral, xAI, OpenRouter, Ollama, most gateways), or the built-in mock. You declare providers in .vibe/config.toml, give each a name, and refer to models as <provider name>/<model id>.

Defaults

Without any configuration, Vibe Factory knows three providers and uses anthropic/claude-sonnet-5 for every phase with the medium thinking level:

default_provider = "anthropic"
default_model = "anthropic/claude-sonnet-5"

[providers.anthropic]
kind = "anthropic"
api_key_env = "ANTHROPIC_API_KEY"

[providers.openai]
kind = "openai"
api_key_env = "OPENAI_API_KEY"

[providers.ollama]
kind = "openai"
base_url = "http://localhost:11434/v1"
default_model = "qwen2.5-coder"

These built-in values apply when .vibe/config.toml does not exist and when the file contains no [providers.*] table at all. As soon as you declare one provider, the providers are exactly the tables you wrote: declare every provider you use (a file written from the defaults, as above, already contains the three).

A provider whose key is missing still loads. It fails with an authentication error on its first call only, so listing providers you do not use is harmless.

Provider keys

Every [providers.<name>] table accepts:

KeyTypeMeaning
kindstring, requiredanthropic, openai, ollama or mock (plugins may add kinds)
api_keystringthe key itself; avoid committing it, prefer api_key_env
api_key_envstringenvironment variable holding the key (an empty value counts as missing)
base_urlstringendpoint override (gateway, proxy, self-hosted server); trailing / is ignored
default_modelstringmodel used when a reference names no model, or names default
streamboolstream answers as they are generated (default true); set false for a gateway that mishandles server-sent events
extratablekind-specific settings, below

api_key wins over api_key_env when both are set.

An Anthropic API key that is not scoped to a workspace is refused with HTTP 400 (“must include the anthropic-workspace-id header”). Set ANTHROPIC_WORKSPACE_ID to the workspace id (wrkspc_…, from the console’s workspace settings) and every anthropic provider sends it as anthropic-workspace-id, or use a key created in a workspace. In the macOS app, add the variable in Settings like the key.

extra keys

KeyKindsTypeEffect
headersanthropic, openai, ollamatable of stringsextra HTTP headers sent with every request
thinkinganthropic"adaptive" or "budget"force the thinking form instead of choosing it from the model id (see Thinking levels)
authanthropic"api_key" (default) or "bearer"bearer sends the key as Authorization: Bearer with the OAuth beta header instead of x-api-key
require_api_keyopenai, ollamabooldefault false when base_url is set, true on the official OpenAI endpoint; with false, no Authorization header is sent when the key is missing
max_tokens_paramopenai, ollamastringname of the output-budget field; default max_completion_tokens on the official endpoint, max_tokens elsewhere
reasoningopenai, ollamaboolsend reasoning_effort; default on for the official endpoint only, since many compatible servers reject unknown fields
replymockstringfixed answer of the mock provider (default "Mock response.")

The ollama kind is the openai kind with base_url defaulting to http://localhost:11434/v1.

[providers.gateway]
kind = "openai"
base_url = "https://llm.internal.example.com/v1"
api_key_env = "GATEWAY_TOKEN"

[providers.gateway.extra]
max_tokens_param = "max_tokens"

[providers.gateway.extra.headers]
"X-Team" = "platform"

Referring to models

A model reference is provider/model, where provider is the name of a [providers.<name>] table, not its kind. A reference without / is attributed to default_provider.

ReferenceResolves to
anthropic/claude-opus-5provider anthropic, model claude-opus-5
claude-opus-5provider default_provider, model claude-opus-5
ollama/defaultprovider ollama, its default_model
openrouter/meta-llama/llama-4-maverickprovider openrouter, model meta-llama/llama-4-maverick (split at the first /)

On providers of kind anthropic these shorthands are expanded:

ShorthandModel id
opusclaude-opus-5
sonnetclaude-sonnet-5
haikuclaude-haiku-4-5-20251001
fableclaude-fable-5-1

An unknown provider name fails with unknown provider `x` for model `x/y` (configured: …), and an unknown kind with provider `x` has unknown kind `y` (known kinds: anthropic, mock, ollama, openai).

Models per phase

Each pipeline phase can use its own model and, optionally, its own thinking level. Phases without an entry use default_model, and every agent keeps its own thinking level.

default_model = "anthropic/sonnet"

[phases.assess]
model = "anthropic/haiku"
thinking = "low"

[phases.plan]
model = "anthropic/opus"
thinking = "high"

[phases.build]
model = "ollama/qwen2.5-coder"
thinking = "off"

The phase names are assess, spec, plan, build, qa, fix and merge. An agent can also pin a model regardless of the phase; see Customising agents.

Thinking levels

LevelBudgetAnthropic, current modelsAnthropic, older modelsOpenAI-compatible
offnoneno thinkingno thinkingno reasoning_effort
low1 024 tokensadaptive, effort lowbudget_tokens: 1024reasoning_effort = "low"
medium4 096adaptive, effort medium4 096"medium"
high16 384adaptive, effort high16 384"high"
max32 768adaptive, effort max32 768"high"

On OpenAI-compatible servers the level is only sent when reasoning is enabled (see above); otherwise it is ignored.

With Anthropic, current models (Claude 4.6 and later: Sonnet 5, Opus 5 and 5.5, Fable, Opus 4.6 to 4.8, Sonnet 4.6) take thinking: {type: "adaptive"} with output_config.effort and reject budget_tokens and sampling parameters, so vibe never sends a temperature to them. Older models (Haiku 4.5, Claude 4.5 and before) and unknown model ids keep budget_tokens, and thinking removes the temperature as the API requires. The form is chosen from the model id; set extra.thinking = "adaptive" or "budget" on the provider to force it, for example behind a gateway that renames models. Model ids are sent as is, so the choice follows claude-... names.

Recipes

Ollama (local, no key):

[providers.ollama]
kind = "ollama"
default_model = "qwen2.5-coder:32b"

Groq:

[providers.groq]
kind = "openai"
base_url = "https://api.groq.com/openai/v1"
api_key_env = "GROQ_API_KEY"
extra = { require_api_key = true }

OpenRouter:

[providers.openrouter]
kind = "openai"
base_url = "https://openrouter.ai/api/v1"
api_key_env = "OPENROUTER_API_KEY"
default_model = "anthropic/claude-sonnet-5"

[providers.openrouter.extra]
require_api_key = true

[providers.openrouter.extra.headers]
"X-Title" = "vibe-factory"

Reference it as openrouter/anthropic/claude-sonnet-5.

xAI:

[providers.xai]
kind = "openai"
base_url = "https://api.x.ai/v1"
api_key_env = "XAI_API_KEY"
extra = { require_api_key = true }

Mistral:

[providers.mistral]
kind = "openai"
base_url = "https://api.mistral.ai/v1"
api_key_env = "MISTRAL_API_KEY"
extra = { require_api_key = true }

Setting require_api_key = true on hosted services turns a forgotten environment variable into a clear no API key configured error instead of an anonymous request rejected by the server.

Mock (dry runs and tests, no network):

default_model = "fake/any"

[providers.fake]
kind = "mock"
extra = { reply = "Nothing to do." }

Streaming

Anthropic and OpenAI-compatible providers stream their answers: the text and, when the model exposes it, the reasoning arrive as agent_delta events while a step is being generated (vibe --json run prints them; the terminal output shows the complete text at the end of each step, as before). The final answer is identical to a non-streamed one.

A request that fails before any text arrived is retried as usual. A stream that breaks after text was sent is not retried by the provider (the text would be repeated): the step fails and the agent runtime’s own retry policy applies. An OpenAI-compatible server that refuses stream or stream_options with a 4xx error gets the same request without streaming. agent_delta events are not written to events.jsonl.

Retries and errors

HTTP providers retry transient failures on their own: four attempts in total, starting at 1 second and doubling up to 60 seconds, with ±25 % jitter. A Retry-After header from the server replaces the computed delay, still capped at 60 seconds. On top of that, the agent runtime retries a still-failing call twice more and publishes a retrying event you will see in the live output.

SituationClassified asRetried
HTTP 429 or “rate limit” wordingrate limitedyes
HTTP 5xx, including 529 overloadedserver erroryes
DNS, TLS, connection or timeout failure, HTTP 408networkyes
HTTP 401 / 403authentication failedno
billing, credit or quota wording, or HTTP 402 (even on a 429)invalid requestno
HTTP 400 / 413 mentioning the context lengthcontext too longno; the agent session stops and may continue from a summary
other 4xxinvalid requestno

Messages you may see:

AuthFailed: no API key configured for provider `anthropic` (set `api_key` or `api_key_env`)
AuthFailed: HTTP 401: invalid x-api-key sk-***
InvalidRequest: HTTP 429: You exceeded your current quota
RateLimited: HTTP 429: rate limit reached for requests
ServerError: malformed provider response: no choices
Network: request timed out: …

Keys, bearer tokens and token= / api_key= values are masked before anything is logged or shown. Requests time out after 30 seconds to connect and 300 seconds overall.

Prompt caching

With Anthropic, the system prompt is sent as a cached block, so the many turns of one agent session reuse it; cache reads and writes are reported in the token usage. OpenAI-compatible servers that cache automatically report cached prompt tokens the same way. Nothing needs to be configured.

Tools and security

Agents act on your project through tools. Every tool is confined to the task’s workspace and governed by a security policy you control from .vibe/config.toml.

The built-in tools

ToolWhat it doesNeeds
read_fileRead a text file with line numbers (offset, limit, default 2000 lines). Binary files are refused.read
write_fileCreate or overwrite a file, creating parent directories.write
edit_fileReplace an exact snippet (old_string → new_string, optional replace_all); returns a unified diff.read + write
list_dirIndented tree of a directory (depth 1–10, default 2), honouring .gitignore, skipping .git, target, node_modules.read
globFind files by pattern (**/*.rs), newest first, at most 2000 results.read
grepRegex search in text files (content, files or count mode, context lines, case-insensitive).read
bashRun a shell command inside the workspace with a timeout (default 120 s, max 600 s). Background jobs are killed when the command finishes.execute
web_fetchFetch a public http(s) page and return its text (HTML converted, 2 MB read at most, 30 000 characters returned). Hosts resolving to loopback, private, link-local or other internal addresses are refused, and each redirect is checked again; security.web_allowed_domains narrows it to some domains.network
web_searchSearch through the SearXNG-compatible endpoint of security.search_url and list the first 10 results. Only registered when that key is set.network

The web tools refuse to run unless security.allow_network = true. They are read-only tools, so read-only agents such as the researcher and the reviewers get them too.

Which tools an agent may use is decided per role (see Customising agents): the planner and the reviewers of the spec phase are read-only, the coder and the QA agents have everything.

Path containment

Every path an agent gives is resolved against the workspace root and must stay inside it:

  • ../secrets, absolute paths and symbolic links that point outside are refused;
  • a symbolic link whose target does not exist yet is refused as well, so a write can never create a file outside the workspace;
  • writing tools additionally refuse to write through any symbolic link.

Paths listed in security.extra_read_paths may be read but never written. Nothing under a .git directory can be written by the file tools, whatever the path used to reach it.

The shell policy

The bash tool does not run a command before the security policy has approved it. The command line is parsed into segments (pipes, &&, ||, ;, &, subshells, bash -c "…", eval, find -exec, xargs) and every segment is checked. Anything the parser cannot understand is denied.

Always blocked

System, disk, privilege and network administration:

shutdown reboot halt poweroff init mkfs fdisk parted gdisk dd sudo su doas chown
iptables ip6tables nft ufw nmap systemctl service crontab mount umount
useradd userdel usermod groupadd groupdel passwd visudo

Program names computed at run time ($CMD, $(…), globs, brace expansion) are denied, and so are shells reading their program from standard input (echo x | sh).

Guarded commands

CommandRule
rm, rmdir, unlink, shredNo --no-preserve-root; no target containing $, a backtick, *, ?, [, { or ~ (so rm -rf ${HOME%/}, rm *.log and rm -rf /U* are all refused); no ., .., the workspace root itself, or any absolute path outside the workspace (/Users/you/.ssh included). Symbolic links leading out of the workspace are followed for the check.
find … -delete / -exec rmThe start paths obey the rm rules.
… | xargs rmEvery upstream command’s path arguments obey the rm rules; xargs must name its program explicitly and cannot take it from its input.
Redirections >, >>, &>, teeThe target file must be inside the workspace and contain no expansion; /dev/null and friends are fine; /dev/tcp needs network access.
cd, pushdThe target must stay inside the workspace (no cd .. from the root, no cd -, no CDPATH).
chmodNo setuid/setgid bits (4xxx, 2xxx, 6xxx, +s).
gitgit config only for reads (get, list, --get*, -l); no -c overrides of identity, hooks, pagers, editors, includes, filters, credential or GPG helpers; no GIT_* environment overrides; no rebase -x, bisect run, filter-branch, submodule foreach, difftool/mergetool commands; no force-push to main/master (or without an explicit branch); no deletion of main/master; no --exec-path. Everything else is allowed.
kill, pkill, killallNo kill -1/kill 0; no system processes (launchd, systemd, sshd, dockerd, …).
Shells (bash, sh, zsh, fish, dash, ksh, csh, tcsh, rc, elvish, nu, pwsh, powershell)The inline command (-c, -Command) is validated recursively; reading the program from standard input and -EncodedCommand are refused.
Wrappers (env, time, nice, nohup, xargs, timeout, setsid, stdbuf, caffeinate, flock, watch, strace, script, busybox, …)Stripped, then the wrapped command is validated; a wrapper that writes a file (time -o, strace -o, script) obeys the write rules.
alias, hash, enable, expect -cRefused: they can give a blocked program another name or run code the policy cannot read.
Environment prefixesPATH, CDPATH, LD_PRELOAD, LD_LIBRARY_PATH, DYLD_*, IFS, BASH_ENV, ENV, PROMPT_COMMAND, PERL5OPT, PYTHONSTARTUP, NODE_OPTIONS, RUBYOPT and the GIT_* overrides cannot be set, in any form (VAR=x cmd, env VAR=x, export, declare, read, …).
psql, mysql, mariadb, redis-cli, mongosh, mongoNo DROP, TRUNCATE, DELETE without WHERE, FLUSHALL/FLUSHDB, .drop(, dropDatabase, deleteMany({}).
dropdb, dropuserOnly names containing test, dev, local, tmp, temp, scratch, sandbox or mock.
curl, wget, nc, ssh, scp, sftp, ftp, telnetOnly when network access is allowed (see below).
Anything touching .git/Writing tools, copies, links and redirections into .git/hooks, .git/config, .git/info, .git/worktrees or .git/modules are refused, and git worktree is limited to list.

What you can configure

[security]
# Programs to block in addition to the built-in list.
blocked_commands = ["docker", "terraform"]

# If non-empty, ONLY these programs (and these wrappers, e.g. `nohup`) may run,
# plus harmless builtins: cd, echo, printf, true, false, pwd, test, [, exit, :.
allowed_commands = ["cargo", "git", "ls", "cat"]

# Default timeout of a shell command, in seconds (an agent may ask up to 600).
command_timeout_secs = 120

# Let agents use network clients (curl, wget, ssh, …).
allow_network = false

# Directories outside the project that agents may READ.
extra_read_paths = ["/usr/share/doc"]

When a command is denied, the agent receives the reason as a full sentence (“Command denied by the security policy: rm may not target /”) and can choose another approach.

Output limits

Tool output shown to the model is capped (30 000 characters for grep and bash, 100 000 for any tool). The full text of a truncated output is saved under .vibe/tool-output/<uuid>.txt and the path is given to the agent.

Hooks: your own rules

The policy above is static. For dynamic rules (“no npm publish on Fridays”, “log every command to Slack”, “ask me before touching migrations”) write a hook, either in Rust or as a plugin in any language; see Plugins and Extension points.

What the policy is not

It is a denylist and a set of validators, not a sandbox. An agent that can run python -c or node -e can, in principle, do anything the interpreter can. For untrusted code or strict environments, combine the policy with an isolated workspace, keep allow_network = false, and use the allowlist. The built-in container workspace runs every shell command in a hardened, throw-away container with no network by default; a VM or remote sandbox can be plugged in as a WorkspaceProvider plugin. The file tools still act on the host worktree, inside the path containment described above.

Workspaces and merging

Agents never edit your checkout by default. Each task gets its own workspace: a git worktree on a dedicated branch. The agents are confined to that directory, you keep working in your checkout, and several tasks can run side by side. When the work is approved, the branch is merged into the base branch, or left for you to review. The design rationale is in ADR-003.

Choosing a workspace

[pipeline]
workspace = "git_worktree"   # default
# workspace = "in_place"     # no isolation
# workspace = "container"    # git worktrees, shell commands in containers
# workspace = "<name>"       # a WorkspaceProvider registered by an in-process plugin

base_branch = "main"         # top level; default: the branch currently checked out
ValueIsolationNeeds gitMerge
git_worktreeone worktree and branch per taskyesfast-forward, merge commit, or human review
in_placenone: agents work in the project directorynonothing to merge; changes are already there
containergit worktrees as above, and every shell command in a throw-away containeryes, and Docker or Podmanas git_worktree

Git worktrees

For a task titled “Add OAuth login (GitHub)” with id 6f1c3e0a-…, the workspace is:

WhatValue
name<slug>-<first 8 characters of the task id>, for example add-oauth-login-github-6f1c3e0a
directory<project>/.vibe/worktrees/add-oauth-login-github-6f1c3e0a
branchvibe/add-oauth-login-github-6f1c3e0a
forked frombase_branch, else the branch currently checked out, else main, else master

The slug is the title in lowercase, non-alphanumeric runs replaced by -, at most 48 characters. The directory .vibe/worktrees/ contains a .gitignore with *, so worktrees never appear in your own git status.

The base branch is recorded in the repository configuration under branch.vibe/<name>.vibebase, so a task always merges back into the branch it was forked from, even if you change base_branch later:

git config --get branch.vibe/add-oauth-login-github-6f1c3e0a.vibebase

Reopening. Running a task again reuses its worktree. Before that, git worktree prune forgets worktrees whose directory you deleted. If the directory exists but git does not know it (a leftover), it is removed and recreated. If the branch exists but its worktree is gone, a new worktree is created on the existing branch, keeping its commits.

The project must be a git repository; otherwise opening fails with <path> is not a git repository; use the in_place workspace or run `git init` .

One worktree per subtask attempt

With git_worktree (and pipeline.isolate_subtasks = true, the default), every attempt at a subtask also gets its own worktree, forked from the current commit of the task branch:

WhatValue
directory<task worktree>--s<N>-a<attempt>, for example .vibe/worktrees/add-oauth-login-github-6f1c3e0a--s2-a1
branch<task branch>--s<N>-a<attempt>, for example vibe/add-oauth-login-github-6f1c3e0a--s2-a1

Parallel coder sessions therefore never see each other’s unfinished edits, and a failed attempt is thrown away with its worktree instead of being reset in a shared directory.

When a session reports its subtask done, its work is committed on the attempt branch (vibe: complete subtask 2 - Routes) and merged into the task branch right away: a fast-forward when the task branch did not move, else a merge commit. Integrations happen one at a time; sessions that finish together are integrated in plan order, and a subtask starts only once the work of its depends_on subtasks is integrated. If the merge conflicts with work integrated since the attempt started, the merge is aborted (the task branch is left exactly as it was), the attempt counts as failed with the conflicting files in its notes, and the next attempt starts from the updated task branch. With merge_strategy = "assisted", the model of the merge phase first tries to resolve the conflict markers, as for the final merge; the attempt only fails when a file stays unresolved.

Attempt worktrees and branches are removed after each attempt and, to clean up after a crash, when the build starts and ends. vibe task discard removes any that remain. Before the first attempt the task worktree is committed as vibe: checkpoint before build, so attempts fork from everything already in it.

Set pipeline.isolate_subtasks = false to have parallel subtasks share the task worktree as in 0.1 (completed subtasks are then committed when no other session is running, and failed attempts are reset). in_place and plugin workspaces always share the task workspace.

Container workspace

The shell policy filters commands but is not a sandbox. With workspace = "container", the bash tool, and therefore the required validation commands, runs every command in a fresh container instead of on your machine:

[pipeline]
workspace = "container"

[workspace.container]
image = "rust:1.88"          # required
runtime = "docker"           # or "podman", or a path to either
network = "none"             # default; "bridge" or a named network
cpus = 2.0
memory = "4g"                # also the swap limit
pids_limit = 1024            # default
tmp_size = "1g"              # size of the /tmp tmpfs
# user = "1000:1000"         # default: owner of the worktree on Unix
env = ["CARGO_TERM_COLOR"]   # host variables passed through, by name only
mount_git_metadata = false   # mount the git directory read-only (Unix only)

[[workspace.container.mounts]]
source = "/home/me/.cargo/registry" # absolute, or relative to the project root; no `~`
target = "/usr/local/cargo/registry"
read_only = true             # default

Files still live in the task’s git worktree on the host: it is bind-mounted at /workspace and the command’s working directory is translated to the matching path inside the container. The file tools, the diff shown to reviewers, commits, merges and validated merges work exactly as with git_worktree, and so do subtask worktrees.

Every container is started with --rm, --cap-drop=ALL, --security-opt=no-new-privileges, a read-only root file system, a /tmp tmpfs, a process limit and a label io.vibe-factory.managed=true. There is no way to pass raw flags to the runtime; unknown keys in [workspace.container] are an error. One container runs per command and is removed with rm --force when the command ends, times out or is cancelled, which also kills its background processes. The start-up costs a few hundred milliseconds per command.

What the settings allow and refuse:

  • Network. none unless the configured network is bridge or a named network and security.allow_network = true. host and container:<id> are refused.
  • Mounts. The source must exist; the filesystem root and sockets (such as the Docker socket) are refused; a writable mount may not overlap the project’s .git or .vibe; targets must be absolute and outside /workspace, /tmp, /proc, /sys and /dev.
  • Git. The worktree’s .git pointer is mounted read-only, and the repository’s git directory is not mounted unless mount_git_metadata = true (then read-only), so git commands that write do not work inside the container; the framework commits on the host.
  • Failures of the runtime. An exit status of 125 (image missing, daemon down) is reported to the agent as a runner failure with sandbox_error: true in the tool metadata.

The settings are validated before a run starts. vibe doctor also checks that <runtime> info answers. The image must already contain the tools the agents need (compilers, test runners); nothing is installed for you.

In-place mode

pipeline.workspace = "in_place" makes the project directory itself the workspace. Use it for directories that are not git repositories, throw-away experiments, or when you want to watch files change live. There is no branch, merging does nothing (NoChanges), and discarding a task leaves your files untouched. Agents still cannot leave the project directory.

What changed

The workspace summary shown to reviewers (and to you) has two parts, both excluding .vibe/:

Committed changes since main:
 src/auth/github.rs | 84 +++++++++++++++++++++++
 src/routes.rs      | 12 +++-
 2 files changed, 94 insertions(+), 2 deletions(-)

Uncommitted changes:
 M src/routes.rs
?? tests/auth_github.rs

The first part is git diff --stat <base>...HEAD (changes since the fork point); the second is git status --short in the worktree.

Merge behaviour

With configured pipeline.validation_commands and auto_merge = true, integration is prepared in a detached temporary worktree. The commands run against the combined result, including assisted conflict resolutions. Only a passing, unchanged candidate is published by fast-forwarding the base branch to that exact commit. A failed check, unresolved conflict, changed target or unsupported provider pauses in review; the target is not updated by the integration. Resume builds and checks a fresh candidate. See Required validation commands.

Without configured validation commands, merging integrates the task branch into the base branch in your project checkout using the original algorithm:

  1. Anything left uncommitted in the worktree is committed as vibe: checkpoint, excluding .vibe/, with the neutral identity Vibe Factory <vibe-factory@localhost>.
  2. If tracked files of your checkout have uncommitted modifications, the merge is refused: cannot merge vibe/… : the project at … has uncommitted changes; commit or stash them first. Untracked files do not block it.
  3. If the task branch has no commit ahead of the base, the result is no changes.
  4. Your checkout is switched to the base branch if needed, then git merge --ff-only is tried; if the base moved, git merge --no-ff creates a commit Merge vibe/… (vibe).
  5. On conflicts, the merge is aborted, your checkout is switched back to the branch it was on, and the task is handed to you for human review with the list of conflicting files. Your checkout is left clean.

After a successful merge your checkout stays on the base branch. The worktree and branch are kept until you discard the task. With pipeline.auto_merge = true the merge runs as soon as QA and required validations approve; otherwise the task waits in ready after its task checks.

Assisted conflict resolution

The worktree provider can instead ask a model to resolve conflicts (MergeStrategy::Assisted). For each conflicting file it sends the content with its markers, asks for the fully merged file, and writes it back only if the answer contains no conflict marker. If every file is resolved, the merge is committed; if any file is not, the merge is aborted and reported for human review as above. Enable it with pipeline.merge_strategy = "assisted" in .vibe/config.toml; the model used is the one configured for the merge phase (or default_model).

Repository configuration is guarded

Agents may run git in the worktree, and a worktree shares the repository’s local configuration (.git/config) with your checkout. To make sure an agent cannot smuggle executable settings into your repository, the worktree provider:

  • runs every git command of the framework with hooks, fsmonitor, pagers, editors and SSH helpers neutralised (-c core.hooksPath=<empty dir> and friends);
  • takes a snapshot of the local configuration when the workspace is opened (kept in memory and in .vibe/worktrees/.snapshots/<task>.cfg);
  • refuses to merge when any key changed since, except the framework’s own branch.vibe/* sections. The error lists the added, removed or modified keys, with core.hooksPath named first when it is involved.

If you changed the configuration yourself in the meantime, review the listed keys and either restore them or accept them (GitWorktreeProvider::accept_config_changes from code; a CLI command is on the roadmap), then run the merge again.

Inspecting, merging and discarding by hand

Everything is plain git, so you can take over at any point:

git worktree list                                   # every task workspace
git log --oneline main..vibe/add-oauth-login-github-6f1c3e0a
git diff main...vibe/add-oauth-login-github-6f1c3e0a

cd .vibe/worktrees/add-oauth-login-github-6f1c3e0a  # run tests, edit, commit
cargo test
git commit -am "fix: handle missing email scope"

cd -                                                # back to your checkout
git switch main
git merge vibe/add-oauth-login-github-6f1c3e0a      # resolve conflicts as usual

To throw a task’s work away:

vibe task discard <task>                            # removes worktree and branch
# or by hand:
git worktree remove --force .vibe/worktrees/add-oauth-login-github-6f1c3e0a
git branch -D vibe/add-oauth-login-github-6f1c3e0a
git worktree prune

Discarding is idempotent: a worktree or branch that is already gone is not an error. The framework refuses to discard a workspace whose root is the project itself.

Disk usage

Each worktree is a full checkout of your tracked files (the object database is shared, so history is not duplicated), plus whatever the agents build inside it: target/, node_modules/, virtual environments. On large projects, budget one working copy and one build directory per open task. Truncated tool outputs are also saved under .vibe/tool-output/ inside the workspace. Discard finished tasks to reclaim the space, and check with:

du -sh .vibe/worktrees/*

Customising agents

Every step of the pipeline is carried out by an agent: a model running in a loop with a system prompt, a set of tools, a model choice and a thinking level. Vibe Factory ships twelve of them. You can change any of their settings, replace their prompts, or add your own, by dropping TOML files in .vibe/agents/. No code is needed.

The built-in roles

RoleWhat it does
complexity_assessorReads the task and a little of the code, then classifies it as trivial, simple, standard or complex. This picks how much of the pipeline runs.
spec_gathererExplores the codebase and turns your description into numbered, testable requirements with the relevant files and conventions.
spec_researcherChecks the external libraries and APIs the requirements rely on, so the spec does not assume functions that do not exist.
spec_writerWrites the specification: a readable spec.md and the matching JSON.
spec_criticReviews the specification for gaps and contradictions and returns a corrected version.
plannerBreaks the spec into ordered phases of small subtasks, each with the files to touch, its dependencies and how to verify it.
coderImplements one subtask in a fresh session, then runs its verification.
coder_recoveryTakes over a subtask that keeps failing, diagnoses why, and completes it another way.
qa_reviewerChecks every acceptance criterion against the code, runs the checks, and issues a verdict with a list of issues.
qa_fixerFixes the issues listed in a QA report.
merge_resolverResolves the conflict markers of one file.
commit_messageWrites a conventional commit message for a set of changes.

Their default settings:

RoleToolsThinkingEnds with JSONMax steps
complexity_assessorread onlylowyes30
spec_gathererread onlymediumyes100
spec_researcherread onlymediumno100
spec_writerread + write_filehighyes100
spec_criticread onlyhighyes60
plannerread onlyhighyes100
coderalllowno300
coder_recoveryallmediumno300
qa_reviewerallhighyes200
qa_fixerallmediumno200
merge_resolvernonelowno3
commit_messagenonelowno2

All of them use the model configured for the current phase (see Providers and models).

Overriding a role

Create .vibe/agents/<anything>.toml. The file names the role it applies to and sets only the keys you want to change; everything else keeps its current value.

# .vibe/agents/qa_reviewer.toml
role = "qa_reviewer"
model = "anthropic/claude-opus-5"
thinking = "max"
max_steps = 250
KeyTypeMeaning
rolestring, requireda built-in role name, or a new name for a custom agent
descriptionstringshown in listings
system_promptstringthe whole system prompt, replacing the current one
system_prompt_filepathread the prompt from a file, relative to the TOML file
tools"none", "read_only", "read_write", "all", or a list of tool namestools the agent may use
model"phase" or "provider/model"phase follows the phase configuration; a reference pins one model
thinkingoff, low, medium, high, maxreasoning budget
max_stepsinteger ≥ 1maximum number of model round trips
max_tokensinteger ≥ 1output token budget per round trip
structured_outputboolthe agent must end with a JSON document

Rules:

  • Unknown keys are an error, so a typo such as max_step is reported instead of ignored.
  • system_prompt and system_prompt_file are mutually exclusive.
  • A tool list only keeps tools that exist; an unknown name is silently dropped. read_only means read_file, glob, grep and list_dir; read_write adds write_file; all includes tools from plugins.
  • model must contain a / unless it is phase.
  • Errors name the file: .vibe/agents/qa.toml: TOML error: unknown field `max_step` ….

Precedence

Files are read in file-name order. Each file applies on top of the agent already known for its role: the built-in agent, an agent provided by a plugin, or an earlier file in the same directory. Two files for the same role therefore layer, the later one winning on the keys both set. Keep one file per role unless you have a reason not to.

Replacing a prompt

Copy the built-in prompt from crates/vibe-agents/src/prompts/<role>.md as a starting point and point system_prompt_file at your copy:

# .vibe/agents/coder.toml
role = "coder"
system_prompt_file = "prompts/coder.md"   # .vibe/agents/prompts/coder.md

Prompts are templates: {{name}} is replaced by a variable, and unknown variables become empty text. Every agent gets {{task_title}}, {{task_description}}, {{workspace_root}} and {{date}}. The built-in prompts start with an HTML comment listing the other variables their role receives ({{spec}}, {{plan}}, {{subtask}}, {{progress}}, {{memory}}, {{qa_report}}, {{prior_context}}, …); that comment is stripped before the prompt reaches the model, so you can keep it as documentation. The full table is in Agent runtime.

If a role ends with JSON, your prompt must still ask for the same JSON shape: the pipeline parses it into typed data.

Adding a custom role

Any role that is not a built-in name defines a new agent. It must provide a prompt; the other keys default to all tools, the phase model, medium thinking, 200 steps and 8 192 output tokens.

# .vibe/agents/doc_writer.toml
role = "doc_writer"
description = "Keeps the user guide in sync with the code"
system_prompt_file = "doc_writer.md"
tools = ["read_file", "glob", "grep", "write_file"]
thinking = "medium"
max_steps = 80
<!-- .vibe/agents/doc_writer.md -->
You maintain the documentation of the project at {{workspace_root}}.
Task: {{task_title}}

{{task_description}}

Update only files under docs/. Finish with a short list of the pages you changed.

The default pipeline drives the built-in roles; a custom role is available to plugins and programs that look agents up by name in the registry. Plugins can also contribute agents over the plugin protocol (see Plugins).

Writing prompts that return valid JSON

Agents with structured_output = true must end their session with one JSON document. The runtime is tolerant, but these habits keep the retries (and their cost) away:

  • Show the exact shape in the prompt, as a fenced json block with every key, and list allowed enum values literally ("approved | changes_requested | inconclusive"). Enum values are snake_case and must match exactly.
  • Ask for the JSON last, in a single json fence, with nothing after it. The extractor takes the whole answer if it is pure JSON, else the last json fence, else the last untagged fence, else the last balanced {…} or […] in the text. An example block quoted earlier in the answer is harmless; a second document after the real one is not.
  • Objects or arrays only. A bare string or number is never accepted as the answer.
  • No comments, no trailing commas. They are the usual reason for the repair step.
  • Give enough output budget. Plans and specs are long; the built-ins use 32 000 tokens. If an answer is cut by the output limit, the agent is asked once to continue, which can split the JSON across two messages.
  • Let it converge. Structured agents are told “Converge now” at 75 % of max_steps; keep max_steps large enough that exploration fits before that point.

When extraction fails, the runtime makes one cheap repair call without tools, then resumes the agent once with “Your previous answer was not valid JSON”. A second failure is reported as an error; for the QA reviewer it counts as an inconclusive round.

Plugins

A plugin is a separate program that gives agents new tools, adds agents, or vetoes tool calls. It can be written in any language: Vibe Factory starts it, talks to it over its standard input and output, and stops it at the end of the run. Existing Model Context Protocol (MCP) servers that run over stdio work as tool plugins without changes. The wire contract is described in Plugin protocol.

A plugin can provideShown to agents as
toolsextra tools, offered to every agent whose tool selection is all or names them
agentsextra roles, or replacements for built-in ones
a before_tool hooka policy that sees every tool call and may block it

Declaring a plugin in the configuration

Add a [[plugins]] entry to .vibe/config.toml for each plugin:

[[plugins]]
name = "jira"
command = ["jira-vibe-plugin", "--project", "PAY"]
env = { JIRA_URL = "https://jira.example.com", JIRA_TOKEN_FILE = "/run/secrets/jira" }
cwd = "tools/jira"
capabilities = ["tools"]
required = false
KeyTypeDefaultMeaning
namestringrequiredunique name, used in logs and listings
commandlist of stringsrequiredprogram followed by its arguments
envtable of stringsnoneextra environment variables; the rest of the environment is inherited
cwdpaththe directory vibe runs in (normally the project root)working directory of the process
capabilitieslist of tools, agents, hooksnoneinformational only: the plugin’s handshake decides
requiredboolfalsewhen true, a plugin that fails to start stops the run

A program written as a relative path with a separator (./bin/plugin, bin/plugin) is resolved against cwd. A bare name (python3, jira-vibe-plugin) is looked up on PATH.

Installing a plugin from a manifest

A plugin can also ship its own manifest. Put it in a directory under .vibe/plugins/:

.vibe/plugins/
  echo/
    vibe-plugin.toml
    bin/vibe-echo-plugin
# .vibe/plugins/echo/vibe-plugin.toml
name = "echo"
version = "0.1.0"
description = "Echoes its input"
command = ["./bin/vibe-echo-plugin"]
required = false
capabilities = ["tools", "agents", "hooks"]

[env]
ECHO_PREFIX = ">"

The keys are those of [[plugins]] plus version and description; there is no cwd, because the plugin always runs in the manifest’s directory. capabilities may also be written as a table ([capabilities] with tools = true, …). An invalid manifest is skipped with a warning naming the file.

User-level plugins live in the same layout under your configuration directory and are available in every project:

PlatformDirectory
Linux~/.config/vibe/plugins/<name>/vibe-plugin.toml
macOS~/Library/Application Support/vibe/plugins/<name>/vibe-plugin.toml
Windows%APPDATA%\vibe\plugins\<name>\vibe-plugin.toml

When the same name is declared in several places, the first one wins, in this order: [[plugins]] in .vibe/config.toml, then .vibe/plugins/, then the user directory. This lets a project pin or replace a plugin you installed globally.

Using an MCP server as a plugin

Point a declaration at the server’s stdio command. For example, with a hypothetical filesystem server mcp-fs-server that serves one directory:

[[plugins]]
name = "fs"
command = ["mcp-fs-server", "--stdio", "--root", "."]

Vibe Factory performs the MCP handshake, lists the server’s tools (following pagination) and registers them under their own names. Things to know:

  • MCP servers provide tools only; they never add agents or hooks.
  • A tool is treated as read-only only if the server marks it with the readOnlyHint annotation. Other tools are treated as mutating and never run concurrently with other calls.
  • The framework passes the task’s workspace root and permissions in the request metadata, but MCP servers ignore it. The server works on whatever you configured it for: here --root . resolves against cwd, that is your project checkout, not the task’s worktree. Give such servers the narrowest scope that works, and prefer read-only servers when agents run in isolated worktrees.
  • A tool whose name matches a built-in tool replaces it (a warning is logged). Rename the server’s tools, or avoid servers that shadow read_file, bash and friends.

The echo example

The repository ships vibe-echo-plugin, a complete plugin used by the tests. It offers:

  • a tool echo that returns its text argument (with "workspace": true it returns the workspace root it received instead);
  • an agent with the custom role echo_agent that may only use echo;
  • a before_tool hook that blocks any call to a tool named forbidden.

Build it and declare it:

cargo build --release -p vibe-plugins --bin vibe-echo-plugin
[[plugins]]
name = "echo"
command = ["./target/release/vibe-echo-plugin"]

You can also talk to it by hand to see the protocol:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocol_version":"1","host":{"name":"me","version":"0"}}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"text":"hi"}}}' \
  '{"jsonrpc":"2.0","id":3,"method":"shutdown"}' \
  | ./target/release/vibe-echo-plugin

Troubleshooting

Where are the logs? Everything a plugin writes to its standard error is logged by Vibe Factory at debug level, prefixed with [<plugin name>]. Log notifications sent over the protocol are logged at their own level. Enable debug logging to see both. A plugin must never print diagnostics to standard output: that stream carries the protocol, and a stray line is reported as ignoring invalid line from plugin.

The plugin does not start. The message names the program and the directory it was started in:

skipping plugin `jira`: Plugin: cannot start plugin `jira` (`jira-vibe-plugin` in `/repo`): No such file or directory

Check the program is on PATH or use a path relative to cwd, and that it is executable.

Required or optional? An optional plugin (the default) that fails to start is skipped with a warning and the run continues without it. A plugin with required = true that fails stops loading: every plugin already started is shut down and the run reports required plugin `x` failed to start: …. Mark a plugin required when running without it would be unsafe, typically a policy plugin whose hook blocks dangerous commands.

Timeouts. A tool call may take up to 600 seconds; every other request, including the handshake, 60 seconds. A plugin that does not answer in time produces plugin `x` did not answer `tools/call` within 600s, which the agent sees as a failed tool call. At the end of the run each plugin gets 2 seconds to answer shutdown and 2 more to exit before it is killed.

Every tool call is blocked. A plugin hook that crashed or stopped answering blocks the calls it guards (hook plugin unavailable: …): hooks fail closed on purpose. Restart the run after fixing the plugin, or remove it.

Tools are missing. The handshake is authoritative: a plugin that does not report the tools capability is never asked for its tools, whatever capabilities says in your configuration. Also check the agent’s tool selection: roles with read_only or read_write tools never see plugin tools unless you list them explicitly (see Customising agents).

The same plugin twice. A name declared twice is loaded once; the duplicate is ignored with a warning.

Troubleshooting

Each section starts from what you see, explains the cause, and gives the fix. Two commands answer most questions:

vibe task show <ref>     # status, last run, QA verdict, workspace
vibe doctor              # environment, configuration, keys, plugins

Where to look

SourceWhat it tells you
.vibe/tasks/NNN-slug/progress.mda timestamped note for every step: assessment, spec, plan, each subtask attempt with its failure reason, each QA round, merge result, pause reasons
.vibe/tasks/NNN-slug/run.jsonthe last run: status, current_phase, completed_phases, qa_round, last_error
.vibe/tasks/NNN-slug/events.jsonlevery event of every run: tool calls with their input, tool results, retries, agent stops
.vibe/tasks/NNN-slug/plan.mdeach subtask’s status, attempts and notes
.vibe/tasks/NNN-slug/qa_report_<n>.mdthe issues of each QA round
vibe history <ref>what each run did: phases, commits, changed files, validations, QA verdict, errors (History)
vibe trace <ref> [--full]every tool call of a run with its arguments, exit code and complete output (Tool call trace)
vibe events --since 1hthe recent activity of every task, in time order
standard error with -v, -vv, -vvvlogs at info, debug, trace level; RUST_LOG overrides
vibe trace 3 --tool bash --full      # every shell command of the last run, with its output
jq -r 'select(.event.type == "tool_returned" and .event.is_error) | "\(.event.tool): \(.event.preview)"' \
  .vibe/tasks/003-*/events.jsonl
RUST_LOG=vibe_agents=debug,vibe_plugins=debug vibe run 3 --resume 2> run.log

The file layout is described in Persistence layout.

The first model call fails with AuthFailed

AuthFailed: no API key configured for provider `anthropic` (set `api_key` or `api_key_env`)
AuthFailed: HTTP 401: invalid x-api-key sk-***

Cause. The provider’s key is missing, empty or wrong. Providers load without a key and only fail on their first call, so this appears at the start of assess or spec.

Fix. Export the variable named by api_key_env in the shell that runs vibe (ANTHROPIC_API_KEY or OPENAI_API_KEY for the built-ins), or switch to a provider you have (vibe run 3 --provider ollama). vibe doctor lists every declared provider, whether the configuration uses it, and whether their keys are set. Then resume: vibe run 3 --resume.

InvalidRequest: HTTP 429: You exceeded your current quota is a billing problem, not a rate limit: it is not retried. Check your account.

Retrying lines and rate limits

  ↻ retrying model call (claude-sonnet-5): HTTP 429: rate limit reached for requests (attempt 2, in 30.0 s)

Cause. A transient failure: rate limit (429), overloaded or server error (5xx), network error or timeout. Providers first retry silently, four attempts with exponential backoff (1 s doubling up to 60 s, honouring Retry-After); if the call still fails, the agent runtime retries it twice more, and those retries are the lines you see. Nothing to do unless it keeps happening.

Fix, if it persists. Lower pipeline.max_parallel_subtasks (each parallel subtask is a concurrent session), use a smaller model for high-volume phases ([phases.build]), or run fewer tasks at once. If all retries fail, the phase fails; resume the run later.

Retrying also appears for pipeline-level retries: plan: <validation error> when the planner produced an invalid plan, and subtask `…` before another attempt of a subtask.

ContextTooLong and continuation

Cause. A session grew past the model’s context window (many large files read, long command outputs). The provider reports it as ContextTooLong and the session stops with context_window.

What happens. Coder sessions (build) continue automatically: the transcript is summarised into handover notes and a fresh session resumes from them, up to five times. Other agents do not continue; their session ends with whatever they produced, which usually leads to the JSON problem below.

Fix. Use a model with a larger context for the phase, split the task, or make subtasks smaller by asking for it in the description. Large tool outputs are already truncated and saved under .vibe/tool-output/.

A model returns invalid JSON

agent `planner` did not produce valid structured output after a retry: no JSON object or array found in the output
agent `qa_reviewer` did not produce valid structured output after a retry: JSON does not match the expected shape: invalid type: string "none", expected a sequence

Cause. Structured agents must end with a JSON document. When extraction fails, the runtime makes one repair call, then resumes the agent once asking for valid JSON. A second failure is an error. What follows depends on the role:

RoleEffect of the error
complexity_assessorthe task falls back to standard
spec_gathererthe spec phase fails; the run is failed
spec_writer, spec_criticignored; the spec keeps the gatherer’s requirements (or the writer’s, for the critic)
plannercounts as a failed attempt; retried with the error as feedback, up to max_phase_retries more times
coder, coder_recoverya failed attempt of the subtask
qa_reviewerthe round is inconclusive: the run pauses with the task in review

Fix. Small local models are the usual cause: use a stronger model for the phase ([phases.plan], [phases.qa]), raise max_tokens of the agent if answers are cut, and see Writing prompts that return valid JSON if you replaced a prompt. Then vibe run <ref> --resume.

A subtask fails three times

  [2/4] Wire the export route                              failed
✓ build  3 done, 1 failed, 0 skipped

Cause. Each attempt ended without a coder report saying done. The third attempt (max_subtask_attempts) used the recovery agent. The reasons are in plan.md (the subtask’s notes) and progress.md; memory/gotchas.md records the failure for later runs. Subtasks that depend on it are skipped.

What happens next. If at least one subtask is done and none is pending, the run goes on to QA, which will likely request changes for the missing part. If nothing is done, the run fails with build failed: ….

Fix. Read why it failed. Often a verification command cannot pass in the worktree (missing service, missing tool, command denied). Fix the cause, then:

vibe run 3 --resume     # a failed build retries failed and skipped subtasks with a fresh budget

To finish the subtask yourself, do it in the worktree, commit, set its status to "done" in plan.json, and run vibe run 3 --from qa.

QA never approves

⏸ paused: QA did not approve after 3 round(s); human review needed
⏸ paused: issue `export ignores --limit with --json` was reported in 3 consecutive QA rounds; escalating to a human
⏸ paused: QA round 2 was inconclusive; human review needed

Cause. The run pauses with the task in review (exit code 2) when QA still requests changes after max_qa_rounds reviews in this invocation, when the same issue title appears in three consecutive reports, when a review is inconclusive, or when a trivial task, which has no fix phase, gets changes requested.

Fix. Read the latest qa_report_<n>.md. Then either:

  • let the agents try again: vibe run 3 --resume resumes at QA with a fresh round budget. An escalated issue that is still reported three rounds in a row pauses again, so change something first (a hint in progress.md, a stronger model for fix);
  • fix it by hand: edit and commit in the worktree (.vibe/worktrees/<slug>-<id>/), then vibe run 3 --from qa for a new review;
  • accept it as is: merge the branch yourself (see Workspaces).

If QA is wrong about a requirement, correct the requirement in spec.json before re-running QA; the reviewer checks acceptance criteria literally.

Merge conflicts

✓ merge  merge conflicts in 2 file(s)
Task 3 is ready · branch vibe/add-oauth-login-github-6f1c3e0a

Cause. With auto-merge, the base branch moved and the task branch conflicts with it. The workspace provider returned NeedsHumanReview: the merge was aborted, your checkout was put back on its branch and left clean, and the task is ready with the conflicting files listed in progress.md. The worktree and branch are left in place.

Fix. Merge by hand and resolve as usual:

git switch main
git merge vibe/add-oauth-login-github-6f1c3e0a

The merge is refused because the project is dirty

Automatic merge failed: cannot merge vibe/… : the project at /repo has uncommitted changes; commit or stash them first

Cause. Auto-merge works in your project checkout, and tracked files there have uncommitted modifications. Untracked files do not block it. The task ends ready.

Fix. Commit or stash your changes, then merge the branch by hand, or run vibe run 3 --from merge --auto-merge.

The worktree already exists

Cause. A task keeps its worktree between runs; running it again reuses it. A worktree directory that git does not know about (for example restored from a backup) is removed and recreated; a branch whose worktree was deleted gets a new worktree with its commits.

Fix. Usually nothing. If git itself complains that the branch is checked out elsewhere or the worktree is locked, clean up and retry:

git worktree prune
git worktree list
git worktree remove --force .vibe/worktrees/<slug>-<id>   # only if you want to start over

A project that is not a git repository fails before the run starts with /path is not a git repository: run `git init`, or use `--workspace in_place` .

A plugin fails to start

skipping plugin `jira`: Plugin: cannot start plugin `jira` (`jira-vibe-plugin` in `/repo`): No such file or directory
required plugin `policy` failed to start: …

Cause. The program is not on PATH, not executable, or crashes during the handshake. An optional plugin is skipped with a warning and the run continues without its tools; a plugin with required = true stops the run. The handshake times out after 60 seconds, a tool call after 600.

Fix. Run vibe plugins check: it starts each plugin, prints what it offers or the error, and stops it. Plugin standard error is logged at debug level, so add -vv. Details in Plugins.

A command is denied by the security policy

bash ⟵ error: Command denied by the security policy: `rm` may not target `/`

Cause. The shell policy blocked a segment of the command. The agent receives the reason and usually finds another way, so a single denial is not a problem. Repeated denials of a command the task really needs (a network client, docker, a database client) will make subtasks or QA fail.

Fix. Allow what is needed in [security]: allow_network = true for curl and friends, remove the program from blocked_commands, or add it to allowed_commands if you use an allowlist. System administration programs (sudo, mount, …) are always blocked. The rules are listed in Tools and security.

vibe says your home directory is not a git repository

error: /Users/me is not a git repository: run `git init`, or use `--workspace in_place`

Cause. Before 0.4.0, vibe looked for the nearest parent holding a .vibe directory without limit, and a ~/.vibe left by another tool made it take your home directory for the project. Since 0.4.0 the search stops at the git repository and never climbs up to the home directory; running vibe from the home directory itself still uses it, like any other directory.

Fix. Check which binary runs (vibe --version, which -a vibe) and reinstall if it is older than 0.4.0. With a current version, the project is the directory you are in (or the nearest parent with .vibe inside the same repository): run vibe from inside the repository, or name it with -C <dir>. The other tool’s ~/.vibe does not need to be removed.

.vibe/tool-output/ keeps growing

Cause. The trace store keeps the complete output of every tool call of every run, up to pipeline.trace_max_chars characters per call (100 000 by default), and nothing removes it automatically. Long test suites and verbose builds fill it fastest.

Fix.

du -sh .vibe/tool-output/*/                  # which tasks use the space
vibe task discard 3 --yes                   # a task you no longer need, with its outputs
rm -rf .vibe/tool-output/003-*/<run>        # the outputs of one old run only

To keep less, lower pipeline.trace_max_chars; to keep nothing but the previews in the log, set pipeline.trace_outputs = false. Removed outputs only make vibe trace --full fall back to the preview. The directory is ignored by git. Details in Tool call trace.

A vibe serve started by an application is still running

error: cannot listen on 127.0.0.1:7777
  caused by: Address already in use (os error 48)

Cause. An application (an editor, the macOS app, a script) started vibe serve as a child process and died without stopping it, for example after a crash or a kill -9. The orphan server keeps its port, its token file .vibe/server.token, and possibly runs.

Fix. Find it and stop it with SIGINT, which cancels its runs cleanly (they stay resumable) and removes the token file:

pgrep -fl "vibe.*serve"
kill -INT <pid>

An application that starts vibe serve should pass --exit-on-stdin-eof and keep a pipe on the child’s standard input: when the application ends, however it ends, the pipe closes and the server stops as on Ctrl-C. The macOS app does this. For a server you start by hand, --port 0 picks a free port.

vibe history shows ~, + or -

#  title          status  runs  commits  files  tokens  active      cost  finished
4  Add a cache    done    2     3        5~     84.1k+  3 min 10 s+ -     2 d ago

These marks are not errors:

  • ~ on files: the list of changed files is approximate. The task branch is gone and no merge was recorded, so the list comes from the commit events or from the files the agents wrote. vibe history <ref> names the source.
  • + on tokens or active time: a lower bound. A run was logged before 0.5 (its totals are unknown) or has not finished; vibe history <ref> shows which run.
  • + on the cost: some tokens were used outside a finished agent session (a session cut by a crash, or the repair of an invalid structured answer) and are not priced.
  • - as the cost: there is no [pricing] table, or a model the task used has no price in it (a local model needs a price of zero), or the task ran before 0.5 and its models are not recorded.

The detail also lists problems met while reading, such as a merge commit that git no longer has (after a rebase or a garbage collection): the history then falls back to the next source. See History.

Windows specifics

  • Shell commands run through cmd /C but are validated with POSIX shell rules, so cmd syntax such as %VAR% or ^ escapes may be denied as unparseable. Ask for plain commands in the task description, or pin the project’s commands in .vibe/agents/coder.toml.
  • Deep worktree paths can exceed 260 characters: git config --global core.longpaths true and enable long paths in Windows.
  • Antivirus scanners can lock files in target/ and make renames fail; exclude the project’s .vibe/worktrees/ directory.

The run was interrupted

A crash or a killed terminal leaves run.json with "status": "running". Ctrl-C leaves it cancelled. Both are resumable:

vibe run 3 --resume

Subtasks that were in progress go back to pending; completed ones are kept.

Resetting a task

GoalDo
re-run QA after manual changesvibe run 3 --from qa
re-plan from the same specvibe run 3 --from plan
redo the spec after editing the descriptionedit description in task.json, then vibe run 3 --from spec
re-assess the complexityset "complexity": null in task.json, or pass --complexity, then vibe run 3
forget the last rundelete run.json; artefacts stay
start completely overvibe task discard 3 --yes, then vibe task add again

A task keeps its complexity once assessed, so a plain vibe run 3 never calls the assessor again. --from starts a new run that reuses the files already on disk; delete spec.json or plan.json if you want them regenerated rather than reused by later phases.

Upgrading

From 0.4 to 0.5

Version 0.5 is about seeing everything that was done: the history of finished work, the activity of every task in one feed, the complete trace of each tool call, and a native macOS app on vibe serve. Existing .vibe/config.toml files keep working, and runs started with 0.4 can be resumed with 0.5.

Upgrade

cargo install --git https://github.com/vincentlauriat/vibe-factory --tag v0.5.1 vibe-cli --force
vibe --version    # vibe 0.5.1
vibe doctor

Nothing on disk needs converting and the event schema stays 2: 0.5 only adds optional fields and two event types (committed, merged), which older clients must ignore anyway. Logs written by 0.4 still load, with what they lack shown as such: vibe trace pairs their tool calls with their results by order (paired: by order) and has only the 200-character previews of their outputs, and vibe history marks their token and time totals as lower bounds (+) unless run.json still describes the run, and shows no cost for them (their model is not recorded). See Logs recorded before 0.5.

task.json gains a branch field, recorded the next time a run opens the task’s workspace. Until then vibe pr, vibe task show and vibe task discard compute the branch from the title as before.

Behaviour that changes by default

Complete tool outputs are kept. Every tool call’s output is written to .vibe/tool-output/<task dir>/<run>/<call>.txt, at most 100 000 characters per call, so the directory grows with every run. It is ignored by git (vibe init has always put tool-output/ in .vibe/.gitignore; check that yours has it), removed per task by vibe task discard, and can be limited or turned off:

[pipeline]
trace_max_chars = 20_000    # keep less per call
trace_outputs = false       # or keep nothing but the previews in the log

Outputs may contain secrets printed by tools; they never leave your machine. See Tool call trace for disk usage and privacy.

vibe events <REF> --follow follows a resumed run to its end. It used to stop at the run_finished left by an earlier pause of the same run. It also stops at the end of the run when --after (or the new --type and --since) hides the run_finished event, where an --after beyond it used to follow forever.

The web UI reads one event stream. The page opens a single GET /api/stream for the whole project (after loading the recent events with GET /api/events) instead of one stream per selected task. The per-task routes (/api/tasks/{ref}/events and /api/tasks/{ref}/stream) are unchanged, so the VS Code extension and other clients keep working. A task reference that matches several tasks is now answered with 400 instead of 500 on every route.

vibe tui has three screens (T Tasks, A Activity, H History) and a Trace tab. Esc still quits from the Tasks board; on the other screens it closes what is open and goes back. In the Trace tab, ↑ ↓ select calls instead of tasks.

New settings

KeyWhat it doesPage
pipeline.trace_outputskeep the complete output of every tool call (default true)Tool call trace
pipeline.trace_max_charscharacters kept per call (default 100000)same
[pricing."<provider>/<model>"]prices per million tokens, to show a cost in the history (none by default)[pricing]

New commands and options: vibe history [REF] [--all], vibe trace <REF>, vibe events without a task (with --since, --type, --task, --follow), --since and --type for vibe events <REF>, and vibe serve --exit-on-stdin-eof. New server routes: GET /api/events, GET /api/stream, GET /api/history[/{ref}], GET /api/tasks/{ref}/trace and GET /api/tasks/{ref}/trace/{call}/output; see vibe serve.

The macOS app

apps/macos/VibeFactory is a SwiftUI client of vibe serve for macOS 14 and later. It starts vibe serve --exit-on-stdin-eof for the project it opens, so it needs the 0.5 vibe on the machine. A signed, notarized DMG is attached to each release since v0.5.0; the app then updates itself (Check for Updates…). Its README explains how to build it from source.

For library users

The Rust API is pre-1.0 and 0.5 changes it in a few places. Code that only uses the vibe command line is not affected.

  • vibe_core::Event has two new variants, Committed and Merged, and new fields: AgentStarted::model, ToolCalled::{call, subtask}, ToolReturned::{call, subtask, exit_code, timed_out, output_chars, output_file} and RunFinished::{usage, active_ms, started_at}. A match needs the new cases (or a wildcard) and .. in struct patterns; code that builds these events must fill the fields. vibe_core::CallId identifies a tool call.
  • Task has a new branch field, PipelineConfig new trace_outputs and trace_max_chars fields, and VibeConfig a pricing table of ModelPrice: use ..Default::default() rather than full struct literals.
  • vibe_agents::ToolTrace and AgentRunner::tool_trace write the complete output of each call; RunContext has a new tool_trace field and RunContext::commit takes the subtask the commit belongs to (None for checkpoints and fixes).
  • PipelineStore has three new methods with defaults, task_dir_name, task_numbers and is_running; implement them in a custom store to name trace directories after the task and to report running tasks.
  • vibe_pipeline has a read layer (ADR-008): the modules events_log (EventReader, TaggedEnvelope, EventCursor, AllEventsFollower), history (TaskHistory, task_history, project_history) and trace (RunTrace, Call, run_trace, read_output).
  • vibe-pipeline now depends on vibe-workspace (read-only git queries of the history).

From 0.2 to 0.4

Version 0.4 includes the work planned for 0.3 (streaming, numbered events, human approvals, the run manager and the terminal UI) and for 0.4 (vibe serve, the web UI, integrations). There was no 0.3 release. Existing .vibe/config.toml files keep working, and runs started with 0.2 can be resumed with 0.4.

Upgrade

cargo install --git https://github.com/vincentlauriat/vibe-factory --tag v0.4.0 vibe-cli --force
vibe --version    # vibe 0.4.0
vibe doctor

Nothing on disk needs converting. Events written by 0.2 in events.jsonl have no schema or seq field: they read as schema 1 and are not replayed by vibe events; a resumed run numbers its new events from 1. run.json gains approval fields that load empty.

Behaviour that changes by default

Answers are streamed. Anthropic and OpenAI-compatible providers now use server-sent events, and vibe --json prints agent_delta lines while a step is generated (they are not kept in events.jsonl). For a gateway that mishandles streaming, set stream = false in its [providers.<name>] table. See Streaming.

Project memory is on. Lessons of a finished task are kept in .vibe/memory.jsonl and the relevant ones are recalled by later tasks. Turn it off with pipeline.project_memory = false; vibe memory list and vibe memory clear inspect and reset it.

One process per task. A second vibe run on a task that is already running fails instead of racing the first one; vibe cancel <task> stops a run from another terminal.

Anthropic thinking. Current Anthropic models (Claude 4.6 and later, including the default claude-sonnet-5) get adaptive thinking with an effort level and no temperature, which they require. Older models keep the token budget form; extra.thinking forces either.

New settings, off by default

KeyWhat it doesPage
pipeline.approvalspause for vibe approve / vibe reject after spec, plan or before mergeHuman approvals
security.web_allowed_domains, security.search_urllimit web_fetch to some domains; the SearXNG-compatible endpoint that registers web_searchConfiguration
[integrations.github], [integrations.gitlab]vibe task import and vibe prCLI

New commands: vibe events, vibe approve, vibe reject, vibe cancel, vibe tui, vibe serve, vibe memory, vibe task import and vibe pr. A run paused for an approval exits with code 2, like any run that waits for a human.

For library users

The Rust API is pre-1.0 and 0.4 changes it in a few places. Code that only uses the vibe command line is not affected.

  • vibe_core::Event has new variants (AgentDelta, ValidationFinished, SubtaskIntegrated, BudgetUpdated, ArtefactWritten, ApprovalRequested, ApprovalResolved); a match on it needs a wildcard arm or the new cases.
  • vibe_core::Envelope has new schema and seq fields: build one with Envelope::now.
  • ModelProvider::complete_streaming has a default that calls complete; implement it to stream. StreamDelta and DeltaSink are new.
  • PipelineConfig has new approvals and project_memory fields, the provider configuration a stream field, SecurityConfig web_allowed_domains and search_url, and Config an integrations table: use ..Default::default() rather than full struct literals.
  • vibe_pipeline::RunManager starts, resumes, cancels and lists runs and is the seam every interface uses (ADR-007). RunState has new approval fields.

From 0.1 to 0.2

Version 0.2 is about reliability. Existing .vibe/config.toml files keep working, and runs started with 0.1 can be resumed with 0.2. This page lists what changes when you upgrade and what you may want to turn on.

Upgrade

cargo install --git https://github.com/vincentlauriat/vibe-factory --tag v0.2.0 vibe-cli --force
vibe --version    # vibe 0.2.0
vibe doctor

Nothing on disk needs converting. run.json gains new fields (validations, validation_fix_attempts, pending_validation_fix, usage, active_ms); files written by 0.1 load with empty or zero values, so a resumed 0.1 run starts its budgets from zero.

Behaviour that changes by default

One worktree per subtask attempt. With the default git_worktree workspace, every subtask attempt now runs in its own worktree and branch (<task branch>--s<N>-a<attempt>) and is merged into the task branch as soon as it is done. You will see:

  • a vibe: checkpoint before build commit when the task worktree had uncommitted work;
  • one vibe: complete subtask N - <title> commit per subtask, and merge commits when parallel subtasks finished out of order, instead of grouped commits (vibe: complete subtasks 2, 3 - …);
  • temporary …--s2-a1 worktrees under .vibe/worktrees/ and branches while a build runs, removed after each attempt.

A subtask whose changes conflict with work integrated while it ran is retried from the updated task branch, which can cost one more attempt than in 0.1. To keep the 0.1 behaviour (parallel subtasks share the task worktree), set:

[pipeline]
isolate_subtasks = false

in_place and plugin workspaces are unchanged. See One worktree per subtask attempt.

Exit code 2 has one more meaning. vibe run also exits with 2 when a run budget is exhausted (see below). Scripts that treat 2 as “QA needs a human” should read run_status and last_error in the --json summary.

Plugin timeouts no longer close the plugin. A request that times out while its line is being written used to close the plugin’s input. Writes now go through a single writer task that never leaves a partial line, so the plugin stays usable after a timeout. A request cancelled before its turn is not sent at all.

New settings, off by default

All of these are optional; leaving them out keeps the 0.1 behaviour.

KeyWhat it doesPage
pipeline.validation_commandsshell checks that must pass before ready or a merge, run by the pipeline itselfRequired validation commands
pipeline.max_validation_fix_attemptsautomatic fixes of failed checks over the whole run (default 2)same
pipeline.max_tokenspause a run after this many tokens, resumes includedRun budgets
pipeline.max_duration_secspause a run after this much active time, resumes includedsame
pipeline.workspace = "container"run the agents’ shell commands in a containerContainer workspace

vibe run gains --max-tokens and --max-duration to set the budgets for one invocation.

A reasonable starting point for a Rust project:

[pipeline]
validation_commands = ["cargo fmt --all -- --check", "cargo test --offline"]
max_tokens = 3_000_000
max_duration_secs = 7200

For library users

The Rust API is pre-1.0 and 0.2 changes it in a few places. Code that only uses the vibe command line is not affected.

  • vibe_core::PipelineConfig has new public fields (validation_commands, max_validation_fix_attempts, isolate_subtasks, max_tokens, max_duration_secs). Build it with PipelineConfig { …, ..PipelineConfig::default() } rather than a full struct literal.
  • vibe_pipeline::PipelineDeps has a new subtask_workspaces field. Pass None to share the task workspace between subtasks, or Some(Arc::new(GitSubtaskWorkspaces::new())) with a git worktree workspace.
  • vibe_pipeline::RunContext has new budget and subtask_workspaces fields, and RunState new validations, validation_fix_attempts, pending_validation_fix, usage and active_ms fields.
  • WorkspaceProvider has a new merge_validated method used when validation commands are configured with auto_merge. Its default fails closed: a custom provider must implement it to support validated automatic merges.
  • New traits and types: vibe_core::SubtaskWorkspaces and SubtaskIntegration (implemented by vibe_workspace::GitSubtaskWorkspaces), MergeValidator, and RunBudget, BudgetLimits, BudgetExceeded. AgentRunner::budget attaches a budget to an agent run; a reached limit stops it like a cancellation.

Architecture overview

Vibe Factory is a Cargo workspace of eight crates arranged in strict layers. Every arrow points downwards; no crate depends on a crate above it.

                        ┌─────────────┐
                        │  vibe-cli   │   command line, terminal UI, server: wires everything together
                        └──────┬──────┘
        ┌──────────┬───────────┼───────────┬────────────┐
        │          │           │           │            │
        │   ┌──────▼──────┐    │           │            │
        │   │vibe-pipeline│    │           │            │   orchestration, task store, run state, read layer
        │   └──────┬──────┘    │           │            │
        │   ┌──────▼──────┐    │           │            │
        │   │ vibe-agents │    │           │            │   agentic loop, prompts, structured output
        │   └──────┬──────┘    │           │            │
 ┌──────▼─────┐    │    ┌──────▼─────┐ ┌───▼─────────┐ ┌▼─────────────┐
 │vibe-provid.│    │    │ vibe-tools │ │vibe-workspac│ │ vibe-plugins │
 └──────┬─────┘    │    └──────┬─────┘ └───┬─────────┘ └┬─────────────┘
        └──────────┴───────────┴───────────┴────────────┘
                         ┌──────▼──────┐
                         │  vibe-core  │   domain model, traits, registry, events
                         └─────────────┘

Only vibe-pipeline depends on vibe-agents (and on vibe-workspace, whose git helpers its read layer uses to rebuild a task’s history); every other crate depends on vibe-core alone and reaches the others through the traits in the Registry. The CLI is the only place where concrete implementations meet.

The three ideas

1. The core is data and traits, nothing else. vibe-core has no I/O, no HTTP, no git. It defines what a task, a spec, a plan, a QA report and a message are, and what a provider, a tool, an agent, a workspace, a memory store and a hook do. Everything above it is an implementation of those traits; everything can be replaced.

2. Agents are declarative. An agent is an AgentSpec: a role, a system prompt template, a tool selection, a model selection and a thinking level. It can be loaded from TOML, built in code or handed over by a plugin. The runtime (vibe-agents) executes any spec the same way: the agentic loop calls the model, executes the tools it asks for, feeds results back, and stops on a well-defined set of conditions.

3. The pipeline is a state machine over persisted artefacts. Each phase reads artefacts written by the previous one (task.json, spec.md, plan.json, qa_report.json, progress notes) from the task directory and writes its own. Because state lives on disk, a run can be inspected, resumed after a crash, or continued by a human.

Request flow

  1. The CLI loads .vibe/config.toml, builds the Registry (built-in providers, tools, agents and workspaces, then plugins), and opens the task store.
  2. Pipeline::run(task) publishes RunStarted, then walks the phases chosen by the complexity profile.
  3. For each phase the pipeline picks the model for that phase, resolves the agent spec, builds an AgentRunner, and runs it with a kickoff message containing the relevant prior artefacts.
  4. Structured outputs (JSON) are parsed into domain types and saved; free-form outputs are saved as markdown.
  5. QA loops with the fixer until approval or the round limit; merge integrates the worktree or leaves the branch for a human.
  6. Every step publishes events on the EventBus; the CLI renders them live and the task store appends them to events.jsonl.

Reading order

Crates

Vibe Factory is a cargo workspace of eight crates. This page describes what each one owns, the public types you will meet first, what it is allowed to depend on, and how it is tested. The Architecture overview explains why the layering exists; this page is the map.

Dependency graph

The graph below is the one declared in the Cargo.toml files. The implementation crates depend on vibe-core and on nothing else from the workspace: vibe-agents reaches models, tools and hooks only through the core traits, so it does not link any concrete provider, tool set, workspace or plugin transport. vibe-pipeline builds on vibe-agents, and on vibe-workspace for the git reads of its read layer (the history of a task: its branch, its merge commit, the files it changed); the pipeline itself still reaches workspaces through the WorkspaceProvider trait. Wiring concrete implementations together is the job of vibe-cli.

   vibe-cli        (binary `vibe`: commands, terminal UI, HTTP server and web UI)
      │   depends on every crate below
      ├──────────────────────────────┬────────────────┬──────────────┐
      │                              │                │              │
   vibe-pipeline                     │                │              │
      │                              │                │              │
      ├──────────────┐               │                │              │
      │              │               │                │              │
   vibe-agents   vibe-workspace   vibe-providers   vibe-tools   vibe-plugins
      │              │               │                │              │
      └──────────────┴───────┬───────┴────────────────┴──────────────┘
                             │
                         vibe-core      (types + traits, no I/O)
CrateWorkspace dependenciesNotable external dependencies
vibe-corenoneserde, serde_json, toml, tokio (sync only), chrono, uuid, indexmap, thiserror, async-trait
vibe-providersvibe-corereqwest (rustls), regex, tokio; wiremock in tests
vibe-toolsvibe-coreglobset, ignore, regex, similar
vibe-workspacevibe-coretokio (process); shells out to git
vibe-pluginsvibe-coretokio (process, io), futures, dirs, indexmap
vibe-agentsvibe-corefutures, tokio, chrono, uuid, toml
vibe-pipelinevibe-core, vibe-agents, vibe-workspacetokio, fs4 (file locks), futures, chrono, uuid, regex
vibe-clievery crate aboveclap, ratatui, axum, reqwest, tokio, console, toml_edit

Workspace-wide rules apply to every crate: unsafe_code = "forbid", missing_docs = "warn" (promoted to an error in CI), clippy::all = "warn", edition 2024. See ADR-001.

vibe-core

Responsibility. The vocabulary of the framework: domain types, the extension traits, the Registry that collects implementations, the EventBus, the configuration model and the prompt template engine. The crate documentation states the rule bluntly: no I/O and no network code. The only filesystem access is VibeConfig::load/save and the lenient canonicalisation used by ToolContext::resolve_path.

Key public types.

ModuleTypes
task, spec, plan, qaTask, TaskStatus, Complexity, TaskSource, Spec, Requirement, SpecContext, Plan, PlanPhase, Subtask, SubtaskStatus, QaReport, QaIssue, QaVerdict, Severity
message, providerMessage, ContentBlock, Role, CompletionRequest, CompletionResponse, StopReason, Usage, ModelRef, ToolSpec, ModelProvider
toolTool, ToolContext, ToolOutput, Permissions, ToolRegistry
agentAgentSpec, AgentRole, ToolSelection, ModelSelection, ThinkingLevel, AgentStop, AgentOutcome
workspace, memory, storeWorkspaceProvider, Workspace, MergeOutcome, InPlaceWorkspace, MemoryStore, InMemoryStore, TaskStore
plugin, eventPlugin, Hook, HookDecision, Registry, Event, Envelope, EventBus, EventSink
config, prompt, errorVibeConfig, ProviderConfig, PipelineConfig, SecurityConfig, PluginConfig, PromptTemplate, Error, ErrorKind

Must never depend on any other workspace crate, an HTTP client, git, or a process API. Adding such a dependency would force every plugin author to compile it.

Tests. Unit tests next to each module: serde round trips (Task, ids, ContentBlock tags), Plan::validate on cycles and unknown dependencies, ToolSelection::resolve, path containment in ToolContext::resolve_path (including .. and absolute paths), the EventBus reaching both subscribers and sinks, VibeConfig TOML round trip and per-phase overrides.

vibe-providers

Responsibility. Every ModelProvider shipped with the framework and the plumbing they share: one pooled HTTP client, error classification, secret scrubbing, retry with exponential backoff, and a registry that builds named providers from [providers.<name>].

Key public types. AnthropicProvider (Messages API, extended thinking, prompt caching, AnthropicAuth::ApiKey or AnthropicAuth::Bearer), OpenAiCompatibleProvider (Chat Completions for OpenAI, Groq, Mistral, xAI, OpenRouter, Ollama and gateways), MockProvider and MockStep, ProviderRegistry and ProviderRegistryBuilder (with register_kind for new kinds), RetryPolicy and with_retry, classify_http_error, scrub_secrets, expand_model_shorthand.

Must never depend on vibe-agents or anything above it. A provider sees a CompletionRequest and nothing about agents, tasks or phases.

Tests. Unit tests for classification rules (billing wording wins over a 429, context length on 400/413), backoff arithmetic and jitter bounds, header parsing and scrubbing. Integration tests in tests/anthropic_http.rs and tests/openai_http.rs run each provider against a wiremock server: tool use round trip, 429 then success, retries exhausted with retry_after preserved, 401 and billing errors not retried, malformed JSON, bearer mode and custom headers, and replay of signed thinking blocks on the next tool turn.

vibe-tools

Responsibility (from ARCHITECTURE_EN.md; the crate is still being written). The built-in tools read_file, write_file, edit_file, list_dir, glob, grep and bash, the shell command parser and security policy, and output truncation. The policy is described in ADR-004.

Must never depend on vibe-agents or above. Tools implement vibe_core::Tool and are confined through ToolContext.

vibe-workspace

Responsibility. Where a task’s agents work and how their work reaches the main line. Implements WorkspaceProvider with git worktrees, re-exports InPlaceWorkspace, and offers commit helpers and an optional model-assisted conflict resolver.

Key public types. GitWorktreeProvider (fields base_branch, worktrees_dir, merge_strategy), MergeStrategy (Manual or Assisted { provider, model }), resolve_conflicts and Resolution, commit_all, has_uncommitted, the Git wrapper, and provider_by_name("git_worktree" | "in_place", base_branch).

Must never depend on vibe-providers concretely: the assisted merge takes any SharedProvider. It must not depend on vibe-agents either; its merge prompt is a constant (MERGE_SYSTEM_PROMPT) rather than an agent spec.

Tests. tests/worktree.rs creates real repositories in temporary directories and exercises: creation and reuse of a worktree, recreation of a stale directory, refusal on a non-repository, changes output, fast-forward with checkpoint commit, merge commit when the base moved, conflicts reported for human review with the project left clean and on its original branch, NoChanges, refusal on a dirty project, idempotent discard, and an assisted merge driven by a MockProvider-style provider.

vibe-plugins

Responsibility. The Vibe Plugin Protocol (VPP): wire types, a client that drives a plugin process, adapters turning a connection into Tool, Hook and AgentSpec contributions, manifest parsing and discovery, the PluginHost failure policy, and a PluginServer helper for writing plugins in Rust. Ships the vibe-echo-plugin example binary. See Plugin protocol.

Key public types. PluginHost, LoadedPlugin, RemotePlugin, RemoteTool, RemoteHook, PluginProcess, PluginManifest, PluginServer, PluginLogger, ToolCallContext, Capabilities, constants PROTOCOL_VERSION, MCP_PROTOCOL_VERSION, DEFAULT_TIMEOUT, TOOL_CALL_TIMEOUT, SHUTDOWN_GRACE.

Must never depend on vibe-agents, vibe-tools or vibe-providers. A plugin is a source of extensions, not a consumer of the built-in ones.

Tests. Unit tests in client.rs use tokio::io::duplex to script a fake peer (timeouts, errors, pagination, notifications). tests/echo_process.rs spawns the real vibe-echo-plugin binary: tools, agents and hooks are contributed, manifest discovery finds it, a broken optional plugin does not block the others, and a relative program resolves against cwd.

vibe-agents

Responsibility. The runtime that turns an AgentSpec into a conversation: the AgentRunner loop, structured-output extraction and repair, continuation after context exhaustion, the twelve built-in agents and their prompts, and TOML overrides. See Agent runtime.

Key public types. AgentRunner, run_structured, run_structured_with_hint, extract_json, repair_json, run_with_continuation, ContinuationPolicy, builtin_agents, register_builtin_agents, load_agent_overrides, apply_agent_overrides, project_agents_dir, documented_variables, and the test_support module (ScriptedProvider, EchoTool, FailingTool, ConcurrencyProbe).

Must never depend on a concrete provider, tool, workspace or plugin crate. The lib documentation calls this out: models come through ModelProvider, capabilities through Tool, policies through the hooks of a Registry.

Tests. tests/agent_loop.rs drives the loop with ScriptedProvider and mock tools, no network: parallel read-only calls, sequential mutating calls, MaxSteps, hook vetoes seen by the model, the 85 % warning and 90 % stop, every structured-output path (fenced, unfenced, repair, retry, give up), continuation and its limit, unknown, failing and panicking tools, truncation to .vibe/tool-output/, cancellation, provider error handling and retries, the max-tokens nudge, convergence at 75 %, prompt variables and hook augmentation, published events, and a smoke run of every built-in agent.

vibe-pipeline

Responsibility. The file-backed TaskStore under .vibe/tasks/NNN-slug/ (FileTaskStore), complexity profiles, phase orchestration, parallel subtask execution honouring depends_on, the QA/fix loop with escalation, human approvals, budgets, run state and resume, and the event log. RunManager is the seam every interface uses to start, resume, cancel and list runs (ADR-007). See The pipeline.

Read layer (ADR-008): events_log (EventReader, TaggedEnvelope, EventCursor, AllEventsFollower) reads one log incrementally or every task’s log in cursor order; history rebuilds what a task did (TaskHistory, project_history, task_history); trace pairs the tool calls of a run (RunTrace, Call, run_trace, read_output). They are described in Persistence layout.

Tests. tests/pipeline.rs runs whole pipelines on the scripted provider and a temporary git repository; tests/history.rs builds histories from logs of both schemas and from real git histories (branch, fast-forward, merge, reset).

vibe-cli

Responsibility. The vibe binary: every command of the Command line reference, the live rendering of events, the terminal UI (vibe tui, module tui) and the HTTP server with its embedded web UI (vibe serve, module server). Every interface goes through RunManager and the read layer; none keeps a state of its own.

Where to add code

You want to…Put it in
add a field to a persisted artefactvibe-core (with #[serde(default)] so old files still load)
support a new model APIvibe-providers, or out of tree via ProviderRegistryBuilder::register_kind
change how the loop reacts to a stop reasonvibe-agents::runtime
change a built-in promptcrates/vibe-agents/src/prompts/<role>.md
add a protocol methodvibe-plugins::protocol, then client, server and remote adapters
isolate work somewhere other than a worktreea new WorkspaceProvider, in any crate

Domain model

Every artefact the framework produces or exchanges is a plain Rust type in vibe-core, serialisable with serde. This page describes those types and shows how they look on disk. All types derive Serialize, Deserialize, Clone, Debug and PartialEq; most enums use #[serde(rename_all = "snake_case")], so TaskStatus::InProgress is written "in_progress".

Task ──1:1── Spec ──1:1── Plan ──1:n── PlanPhase ──1:n── Subtask
  │                                                        │
  └──1:n── QaReport (one per round) ──1:n── QaIssue        └── depends_on: [SubtaskId]

Run: RunId ──publishes──▶ Event (wrapped in Envelope) ──▶ EventBus
Agent session: SessionId, Message[] of ContentBlock, Usage

Identifiers

TaskId, SubtaskId, SessionId and RunId are generated by one macro in ids.rs: a newtype over a v4 uuid::Uuid, #[serde(transparent)], so each serialises as a bare string.

MethodPurpose
new() / default()fresh random id
parse(&str) / FromStrparse the canonical UUID form
short()first 8 characters, used in branch and directory names
Displayfull UUID

The ids are distinct types, so passing a TaskId where a SubtaskId is expected does not compile.

Task

A Task is the unit of work handed to the pipeline.

FieldTypeSerde
idTaskIdrequired
titleStringrequired
descriptionStringrequired
statusTaskStatusrequired
complexityOption<Complexity>required key, null until assessed
labelsVec<String>default
sourceTaskSourcedefault (manual)
created_at, updated_atDateTime<Utc>RFC 3339 strings
branchOption<String>default, omitted when None; the workspace branch recorded by the last run

Task::new creates a backlog task; set_status and touch bump updated_at; slug() applies slugify (lowercase ASCII alphanumerics separated by -, at most 48 characters, "task" when empty).

TaskStatusMeaningis_terminal()
backlog (default)created, not startedno
planningspec and plan being producedno
buildingsubtasks being implementedno
reviewQA reviewing or fixingno
readywaiting for a human to review and mergeno
donemerged or acceptedyes
failedthe pipeline gave upyes
cancelledcancelled by the useryes

Complexity is ordered (trivial < simple < standard < complex) and exposes two thresholds: needs_spec() is true from simple upwards, needs_research() from standard upwards.

TaskSource is internally tagged with kind: {"kind": "manual"}, {"kind": "issue", "provider": "github", "reference": "412"} or {"kind": "derived", "parent": "<task id>"}.

A task as serialised by serde_json:

{
  "id": "6f1c3e0a-9b2d-4c47-8a51-0e3b8f2d7c19",
  "title": "Add OAuth login with GitHub",
  "description": "Users should be able to sign in with their GitHub account.",
  "status": "planning",
  "complexity": "standard",
  "labels": ["auth"],
  "source": { "kind": "issue", "provider": "github", "reference": "412" },
  "created_at": "2026-09-27T08:12:44.120Z",
  "updated_at": "2026-09-27T08:15:02.871Z"
}

Spec

TypeFields
Spectask_id, summary, requirements (default []), context (default), body (default "", free markdown)
Requirementid ("R1"…), description, kind (default functional), priority (default 1, 1 = must have), acceptance (default [])
RequirementKindfunctional, non_functional, constraint
SpecContextrelevant_files, findings, assumptions, all default []
{
  "task_id": "6f1c3e0a-9b2d-4c47-8a51-0e3b8f2d7c19",
  "summary": "Add a GitHub OAuth sign-in flow next to the existing password login.",
  "requirements": [
    {
      "id": "R1",
      "description": "A 'Sign in with GitHub' button starts the OAuth flow.",
      "kind": "functional",
      "priority": 1,
      "acceptance": ["GET /auth/github redirects to github.com/login/oauth/authorize"]
    },
    {
      "id": "R2",
      "description": "Password login keeps working unchanged.",
      "kind": "constraint",
      "priority": 1,
      "acceptance": ["existing tests in tests/auth pass"]
    }
  ],
  "context": {
    "relevant_files": ["src/auth/mod.rs", "src/routes.rs"],
    "findings": ["sessions are stored with tower-sessions"],
    "assumptions": ["client id and secret come from environment variables"]
  },
  "body": ""
}

Because every field but task_id and summary has a default, a minimal spec is two keys.

Plan

TypeFields
Plantask_id, approach (default ""), phases (default [])
PlanPhasename, parallel (default false), subtasks (default [])
Subtaskid, title, description, files, depends_on: Vec<SubtaskId>, verification, status (default pending), attempts (default 0), notes (default "")
SubtaskStatusMeaning
pending (default)not started
in_progressan agent is working on it
doneimplemented
failedgave up after retries
skippeddependency failed or no longer needed

Phases always run in order; the subtasks of a phase may run concurrently when parallel is true. Plan offers subtasks() / subtasks_mut() (plan order), subtask(id), len(), is_empty() and is_complete() (every subtask done or skipped).

Plan::validate() rejects two kinds of broken plans with an ErrorKind::Config error:

  1. a depends_on entry naming a subtask that is not in the plan ("subtask <id> depends on unknown subtask <id>");
  2. a dependency cycle, found with Kahn’s algorithm: every subtask starts with an in-degree equal to its number of dependencies, subtasks at zero are dequeued, and each dequeue decrements the subtasks that depend on it. If fewer subtasks are dequeued than exist, the remainder forms a cycle ("plan contains a dependency cycle").

Dependencies may cross phase boundaries; validation looks at the whole plan.

{
  "task_id": "6f1c3e0a-9b2d-4c47-8a51-0e3b8f2d7c19",
  "approach": "Add an OAuth client module, then wire routes and the login page.",
  "phases": [
    {
      "name": "Foundations",
      "parallel": false,
      "subtasks": [
        {
          "id": "0b7e2f4c-1d3a-4e8b-9c6f-5a2d8e1f3b70",
          "title": "OAuth client",
          "description": "Create src/auth/github.rs exposing authorize_url() and exchange_code() (R1).",
          "files": ["src/auth/github.rs", "src/auth/mod.rs"],
          "depends_on": [],
          "verification": ["cargo test auth::github"],
          "status": "done",
          "attempts": 1,
          "notes": "Uses reqwest already present in Cargo.toml."
        }
      ]
    },
    {
      "name": "Wiring",
      "parallel": true,
      "subtasks": [
        {
          "id": "c41a9e07-6b52-4f1d-a3e8-2d9f0b7c6e15",
          "title": "Routes",
          "description": "Add GET /auth/github and GET /auth/github/callback (R1, R2).",
          "files": ["src/routes.rs"],
          "depends_on": ["0b7e2f4c-1d3a-4e8b-9c6f-5a2d8e1f3b70"],
          "verification": ["cargo test --test auth"],
          "status": "pending",
          "attempts": 0,
          "notes": ""
        }
      ]
    }
  ]
}

QA report

TypeFields
QaReporttask_id, round (1-based), verdict, summary (default), issues (default)
QaIssueseverity, title, detail, and optional requirement, file, line, suggested_fix
QaVerdictapproved, changes_requested, inconclusive
Severitylow < medium < high < critical (ordered)

issues_at_least(Severity::High) filters by severity; is_blocking() is true for any verdict other than approved; to_markdown() renders the human-readable report.

Messages

A conversation is a Vec<Message>; each Message has a role (user or assistant) and a list of ContentBlocks. There is no system role: the system prompt travels separately in CompletionRequest::system. Tool results are sent back in a user message.

ContentBlock (tag type)Fields
texttext
thinkingtext (only when the provider exposes reasoning)
tool_useid, name, input (JSON matching the tool schema)
tool_resulttool_use_id, content (text), is_error (default false)
{"role": "assistant", "content": [
  {"type": "text", "text": "Reading the router first."},
  {"type": "tool_use", "id": "toolu_01", "name": "read_file", "input": {"path": "src/routes.rs"}}
]}

Helpers: Message::user, Message::assistant, Message::tool_results, text() (joins text blocks), tool_uses(), has_tool_use(), and estimate_tokens(), a deliberately rough 4-characters-per-token estimate used for context budgeting without a tokenizer.

Usage counts input_tokens, output_tokens, cache_read_tokens and cache_write_tokens (the last two default to 0). It supports combined, += and total() (input plus output).

Phase

Phase lists the pipeline stages in canonical order: assess, spec, plan, build, qa, fix, merge (Phase::ALL). as_str() gives the stable lowercase name used in [phases.<name>] configuration and CLI flags; FromStr accepts it case-insensitively and returns a Config error otherwise. The enum does not impose an order at run time: a pipeline may skip spec or loop fix → qa.

Events

Event is internally tagged with type. Every variant except log carries the run id.

VariantExtra fields
run_startedtask
phase_startedphase
phase_finishedphase, success, summary
agent_startedrole, subtask (nullable)
agent_textrole, text
tool_calledrole, tool, input
tool_returnedrole, tool, is_error, duration_ms, preview
agent_finishedrole, steps, usage, stop (the AgentStop serialised as a JSON string)
subtask_updatedsubtask, status
retryingwhat, attempt, delay_ms
pausedreason
run_finishedsuccess, status (a TaskStatus)
logrun (nullable), level (info, warn, error), message

EventBus::publish wraps the event in an Envelope stamped with the current time; that is the shape appended to events.jsonl:

{"at":"2026-09-27T08:16:10.002Z","event":{"type":"tool_returned","run":"a3f0…","role":"coder","tool":"read_file","is_error":false,"duration_ms":3,"preview":"use axum::Router;…"}}

Errors

Every crate returns vibe_core::Result<T> with a single Error { kind, message, retry_after, source }. ErrorKind drives behaviour rather than display: RateLimited, ServerError and Network are the only kinds for which is_retryable() is true; ContextTooLong ends an agent run with AgentStop::ContextWindow; Denied is what policy checks return. io::Error converts to Storage, serde_json::Error to InvalidRequest, toml::de::Error to Config.

JSON is the source of truth

Complexity assessments, specs, plans and QA reports are produced by agents as JSON, parsed into these types, and saved as JSON. Markdown (Spec::to_markdown, QaReport::to_markdown) is rendered from the typed value for humans and is never parsed back. There is therefore one source of truth per artefact, and a malformed model answer is caught at the boundary by serde instead of deep in the pipeline. The rationale is in ADR-006; the extraction and repair strategy is in Agent runtime.

The JSON documents the built-in prompts ask for are the agent-facing shapes, not the persisted ones: they omit fields only the framework knows. The planner, for example, returns subtasks without id, status or attempts and names dependencies by subtask title, and the QA reviewer returns a report without task_id or round. Assigning ids, resolving titles to SubtaskIds and filling task_id and round is the caller’s job before a value becomes a Plan or a QaReport and before Plan::validate() runs.

The pipeline

vibe-pipeline turns a task into a sequence of agent sessions. It owns no provider, tool or workspace: everything concrete is injected, so the same driver runs under the CLI, in tests with a scripted provider, or inside another host. This page describes the driver as implemented in crates/vibe-pipeline/src/; the on-disk artefacts it reads and writes are described in Persistence layout.

Entry points

#![allow(unused)]
fn main() {
let pipeline = Pipeline::new(PipelineDeps { registry, providers, store, workspace,
                                            tools, events, config, project_root, committer });
let report: RunReport = pipeline.run(task_id, RunOptions::default()).await?;
let report = pipeline.resume(task_id).await?;            // continue the last run
let report = pipeline.resume_with(task_id, options).await?;
}
PipelineDeps fieldRole
registry: Arc<Registry>plugins, agent overrides, hooks, memory stores, plugin tools
providers: Arc<dyn ProviderResolver>turns a ModelRef into a provider and a model id
store: Arc<dyn PipelineStore>tasks, artefacts, run state, memory, event log
workspace: Arc<dyn WorkspaceProvider>opens the task workspace, lists changes, merges
tools: ToolRegistrybuilt-in tools, merged with the registry’s tools (the registry is cloned first, so a tool in tools wins on a name clash)
events: EventBuswhere every event is published
config: VibeConfigthe parsed .vibe/config.toml
project_root: PathBufthe project directory
committer: Option<Committer>commits the workspace after subtasks and fixes
resetter: Option<Resetter>discards what a failed attempt left in a shared workspace
subtask_workspaces: Option<Arc<dyn SubtaskWorkspaces>>one workspace per subtask attempt (the CLI plugs GitSubtaskWorkspaces for git_worktree)
RunOptions fieldEffect
complexity_overrideforces a complexity; the heuristic and the assessor are skipped
from_phasestarts at this phase instead of assess, reusing persisted spec and plan
until_phasepauses the run once this phase is done
dry_runsame as until_phase = plan (or an earlier until_phase if one is given)
cancela watch::Receiver<bool>; the run stops when it becomes true

RunReport carries run_id, the saved task, final_status (a TaskStatus), every PhaseResult in execution order (phase, success, summary, usage, next), the token usage and wall-clock duration of this invocation, and the final RunState. is_success() is true for ready and done.

A phase error does not make run return Err: the run ends as failed, last_error is recorded in run.json, and a report is returned. Only infrastructure errors come back as Err: the task cannot be loaded, the store cannot write, or the workspace cannot be opened (in that last case run_finished is still published with success: false).

Profiles

The complexity picks a Profile (complexity.rs): the phases run after assess, and which spec steps run.

ComplexityPhases after assessSpec steps
trivialplan, build, qa, mergenone: the planner works from the task description
simplespec, plan, build, qa, fix, mergegatherer only (light spec)
standardspec, plan, build, qa, fix, mergegatherer, writer
complexspec, plan, build, qa, fix, mergegatherer, researcher, writer, critic

fix is never reached by walking the profile forward; only a QA verdict jumps to it.

The complexity is settled in this order:

  1. RunOptions::complexity_override (vibe run --complexity);
  2. the complexity already stored on the task (a task assessed once keeps its class);
  3. heuristic_complexity: at most 30 words across title and description and a known small-change pattern. Typos, version or dependency bumps, colour, text, label, wording, title or copy changes and copyright-year updates are trivial; renames, removal of unused code, lint or compiler warning fixes and “add a comment” are simple;
  4. the complexity_assessor agent. Aliases are accepted (low, easy → simple, medium → standard, hard, high → complex, …). Its needs_research and needs_critique flags switch on the researcher and critic, but only for profiles that write a full spec (standard and complex);
  5. standard, when the assessor fails or answers an unknown class.

A run that starts after assess (from_phase) reuses the profile of the previous run when there is one and no override is given; otherwise it applies steps 1–3 and 5 without calling the assessor. default_profile(task, override) exposes the same computation for displays.

Flow

            ┌────────┐   ┌──────┐   ┌──────┐   ┌───────┐   ┌────┐  approved  ┌───────┐
 task ────▶ │ assess │──▶│ spec │──▶│ plan │──▶│ build │──▶│ qa │──────────▶│ merge │──▶ ready / done
            └────────┘   └──────┘   └──────┘   └───────┘   └────┘            └───────┘
                         (trivial                   │        │ ▲
                          skips it)                 │        │ │ changes requested
                                                    │        ▼ │
                                          no subtask done  ┌─────┐
                                                    │      │ fix │
                                                    ▼      └─────┘
                                                  failed      qa: round limit, inconclusive,
                                                              no fix phase; fix: same issue 3×
                                                              ──▶ paused, task in review

For every phase the driver:

  1. checks the cancel token (cancelled → run cancelled);
  2. sets the task status for the phase and saves task.json and run.json;
  3. runs the before_phase hooks; an Abort ends the run as cancelled with the reason in last_error and in the progress notes;
  4. publishes phase_started, runs the phase, publishes phase_finished;
  5. runs the after_phase hooks and appends the phase to completed_phases;
  6. follows the phase’s Transition: Continue (next phase of the profile), Goto (QA ⇄ fix) or Stop { status, reason, pause };
  7. applies until_phase, then saves run.json with the next phase as current_phase.

Each agent is resolved with RunContext::agent_spec: the registry’s agent for the role (plugins and .vibe/agents/*.toml overrides) or the built-in one. When [phases.<current phase>] exists in the configuration, its thinking replaces the agent’s thinking level. The model is the agent’s fixed model, else config.model_for(phase). Tools run with ToolContext rooted at the workspace and the permissions read, write and execute, plus network when security.allow_network is set.

Kickoff messages

The first user message of each session is built by kickoff_for from delimited sections. When the agent’s system prompt already consumes a section’s placeholder ({{spec}}, {{plan}}, …), the section is replaced by a one-line pointer, so the text is not sent twice; custom prompts without placeholders still receive everything.

RoleTASKSUBTASKSPECPLAN, PROGRESS NOTESCHANGESQA REPORTMEMORYPRIOR CONTEXT
complexity_assessoryesyes (empty)
spec_gatherer, spec_researcher, spec_writeryesyesyes
spec_criticyesyesyesyes
planneryesyesyesyes
coder, coder_recoveryyesyesyesyesyesyes
qa_revieweryesyesyesyesyesyes
qa_fixeryesyesyesyesyes

A final INSTRUCTIONS section restates the JSON document the role must end with. PRIOR CONTEXT carries: the gatherer JSON and research report for later spec steps; the rejection reason for a planner retry; the subtask’s failure history for a coder; a summary of earlier rounds for QA and the fixer. Sizes are capped: prior context 12 000 characters, progress notes 8 000 (most recent kept), memory 6 000, workspace changes 20 000 (beginning and end kept), spec and plan 24 000. The memory section is the task’s memory/*.md plus up to 10 entries recalled from each registered MemoryStore.

The phases

assess records the complexity on the task, the profile in run.json, and a progress note naming the source (override, stored on the task, heuristic, assessor …, fallback …).

spec runs the gatherer (mandatory: its failure fails the phase), then the researcher, writer and critic as the profile says. Their failures are logged as warnings and the best spec so far is kept. The writer also writes a markdown copy inside the workspace at .vibe/specs/<task short id>-spec.md. A critic verdict revised with a non-empty spec replaces the spec. An empty spec (no summary, no requirement) fails the phase. The spec’s context.findings are appended to memory/patterns.md.

plan runs the planner and converts its output with plan_from_output: subtask ids are generated, depends_on entries are resolved by title (case-insensitive) or 1-based position, and the plan is validated (no empty or duplicate title, no self or unknown dependency, no cycle, at least one subtask). A flat subtasks list becomes one sequential phase called Implementation. An invalid plan is retried with the error as feedback, up to max_phase_retries more attempts, each announced by a retrying event.

build is the scheduler below. qa and fix are the loop below.

merge stops the run in all cases:

SituationFinal task status
auto_merge = falseready
merged (fast-forward or merge commit), or nothing to mergedone
MergeOutcome::NeedsHumanReview { files }ready, conflicting files in progress.md
merge error (for example a dirty project checkout)ready, error in progress.md

The conflict strategy is a property of the workspace provider the host builds; the pipeline only records pipeline.merge_strategy in the progress notes.

Build scheduler

A pending subtask is ready when every subtask in its depends_on is done, every subtask of earlier plan phases is finished (done, failed or skipped), and, in a phase whose parallel is false, the previous subtask of the same phase is finished. Up to max_parallel_subtasks ready subtasks run at once, each in a fresh session spawned on its own tokio task (sessions are aborted if the build ends early or its future is dropped). Coder sessions use run_with_continuation, so a session that fills its context window is summarised and continued.

A session succeeds when it ends with a JSON report whose status is done (or success, completed, ok). Anything else is a failed attempt: the reason is appended to the subtask’s notes, which the next attempt receives as prior context. The attempt numbered max_subtask_attempts uses coder_recovery instead of coder (when more than one attempt is allowed). After the last attempt the subtask is failed, a line is added to memory/gotchas.md, and every pending subtask depending on it through depends_on, transitively, becomes skipped. Failures do not skip later phases by ordering alone. Pending subtasks that can never become ready are skipped as “blocked by the plan ordering”.

Isolated subtasks. When subtask_workspaces is set and pipeline.isolate_subtasks is true, launch opens a workspace labelled s<N>-a<attempt> forked from the task workspace and the session’s tools are confined to it (RunContext::runner_in). The scheduler awaits one finished session, drains every other session that already finished (now_or_never), and handles the batch in plan order. A successful attempt is integrated right away with SubtaskWorkspaces::integrate; a Conflict or an error turns it into a failed attempt with the reason in its notes. Every attempt workspace is discarded after its outcome is known; discard_all runs when the build starts (after a vibe: checkpoint before build commit) and when it ends or is cancelled. Nothing is deferred: the task workspace only changes through integrations, which the scheduler serialises.

Shared workspace. Otherwise commits are deferred: the committer stages the whole workspace, so completed subtasks are committed only when no other session is running, possibly several in one commit (vibe: complete subtask 2 - Routes, vibe: complete subtasks 2, 3 - Routes; Login page). A failed commit is logged and noted, never fatal.

The build continues to QA when at least one subtask is done and none is pending; its success flag is false if any subtask failed or was skipped. Otherwise the run fails with build failed: 0 done, 2 failed, 1 skipped. Subtasks found in_progress when the build starts (a crash) go back to pending.

QA loop

Each QA invocation increments qa_round in run.json, gives the reviewer the workspace change summary and the titles of earlier rounds, and saves qa_report_<round>.json/.md. A reviewer that fails to produce valid JSON after repair and resume yields an inconclusive report.

VerdictNext
approvedmerge
changes_requested, profile without fix (trivial)pause, task review
changes_requested, max_qa_rounds reviews already done in this invocationpause, task review
changes_requested otherwisefix
inconclusive (including unknown verdict words)pause, task review

The fixer first checks for escalation: if one issue title (lowercased, whitespace collapsed) appears in each of the last 3 reports, it records the issue in memory/gotchas.md and pauses the run with the task in review. Otherwise the fixer runs, the workspace is committed as vibe: address QA round <n>, and QA runs again. A pause during fix leaves current_phase = qa so a resumed run reviews before fixing again. The round limit counts reviews of the current invocation only, so each resume gets a fresh budget, while qa_round keeps numbering reports across invocations.

Statuses

Phase runningTask.status
assess, spec, planplanning
buildbuilding
qa, fix, mergereview
End of runTask.statusRunStatus
merge, auto_merge off, conflicts or merge errorreadyfinished
merge succeeded or nothing to mergedonefinished
QA pause or escalationreviewpaused
until_phase or dry run reachedbacklogpaused
phase error, build with nothing donefailedfailed
cancel token, hook abortcancelledcancelled

A RunStatus of running in run.json after the process ended means it died mid-phase.

Resume

resume loads run.json and refuses when there is none or its status is finished (start a new run). running, paused, failed and cancelled are resumable. The run keeps its run_id and profile, restarts at current_phase (or from_phase if given), clears last_error, and reuses the persisted spec and plan. Subtasks already done are never redone; interrupted ones are retried. When a failed run resumes at build, failed and skipped subtasks go back to pending with their attempt counters reset, and a progress note says how many.

Run manager and locking

RunManager is the seam interfaces use (ADR-007): start and resume spawn a run on its own tokio task and return a RunHandle to await, cancel flips that run’s cancellation token, active lists running tasks and subscribe returns the live event stream. vibe run goes through it; the terminal UI runs several tasks at once with it.

Pipeline::execute first calls PipelineStore::lock_run: FileTaskStore takes an exclusive OS lock on run.lock in the task directory (and writes its pid to run.owner), so one process at a time runs a task, and a crashed process leaves no stale lock. The run’s cancellation token combines the caller’s token with PipelineStore::take_cancel_request, polled every 500 ms: vibe cancel writes cancel.request, which the running process consumes. Index updates take .index.lock in the tasks directory as well as the in-process mutex, so a CLI and another process never interleave them.

Approval gates

pipeline.approvals lists ApprovalGates. Before a phase runs, the driver computes the gate it must pass (Plan needs Spec when a spec exists, Build needs Plan, Merge needs Merge). A gate without a valid approval sets RunState::pending_approval, publishes approval_requested once, then paused, and ends the run as paused (task review) at that phase. RunState::resolve_approval, called by vibe approve and vibe reject, records the decision. On resume an approved gate lets the phase run; a rejected one redirects to the producer: Spec and Plan take the reason through RunContext::take_rejection and put it in their prompt, Merge becomes a QA report with one high severity “Human review” issue and forces the fix phase (pending_human_fix), even in profiles without it. A completed Spec revokes the spec and plan approvals, Plan the plan approval, Build and Fix the merge approval.

Budgets

run.json accumulates the tokens (usage) and active milliseconds (active_ms) of every invocation of a run. The driver builds a vibe_core::RunBudget from those totals and the pipeline.max_tokens / pipeline.max_duration_secs limits, stores it in the RunContext and hands it to every AgentRunner. The runner adds each model call’s usage to it and treats a reached limit like a cancellation: no new step starts. RunContext::is_cancelled is true as well, so the build scheduler stops launching sessions and puts interrupted subtasks back to pending. The driver tells the two apart with RunBudget::exceeded: a cancelled phase whose budget is exhausted ends the run as paused (task backlog) at the same current_phase, and the budget is also checked before every phase. The totals are written to run.json before and after each phase and at the end of the run, so a crash loses at most the accounting of the phase in progress.

run with from_phase is different: it starts a new run id at that phase, keeping only the previous profile. It is how you re-run QA after fixing things by hand (--from qa).

Events

The driver publishes run_started, phase_started, phase_finished, subtask_updated (in_progress, done, pending after a failed attempt, failed, skipped), retrying (plan retries with delay_ms: 0, subtask retries), paused and run_finished (success true for ready and done); agent sessions add their own events (see Agent runtime). An internal router forwards each event carrying a run id to that run’s sink from the store, which appends it to events.jsonl.

Seams

ProviderResolver maps a ModelRef to (Arc<dyn ModelProvider>, model id). An empty model (ollama/) means the provider’s default. RegistryResolver looks providers up by name in a Registry; the CLI adapts its provider registry, tests plug a scripted provider.

Committer is Arc<dyn Fn(PathBuf, String) -> BoxFuture<'static, Result<Option<String>>>>: workspace root and message in, commit id (or None when there was nothing to commit) out. Without a committer, nothing is committed during the run and the worktree provider commits leftovers as vibe: checkpoint at merge time (see Workspaces and merging).

Agent runtime

An agent is data (AgentSpec); vibe-agents executes it. One runtime, AgentRunner, runs every agent the same way: it renders the system prompt, offers the selected tools, calls the model, executes the tools the model asks for, feeds the results back, and stops on a fixed set of conditions. This page describes the spec, the loop, and the helpers built on top of it (structured output and continuation).

AgentSpec

FieldTypeDefaultMeaning
roleAgentRolerequiredbuilt-in role or Custom(String); serialised as a snake_case string
descriptionString""human-readable purpose
system_promptStringrequireda PromptTemplate with {{variable}} placeholders
toolsToolSelectionallwhich registered tools are offered
modelModelSelectionphasemodel to use
thinkingThinkingLevelmediumreasoning budget
max_stepsu32200maximum model round trips
structured_outputboolfalsethe agent must end with a JSON document
max_tokensu328192output token budget per step

Builders: AgentSpec::new(role, prompt), with_tools, with_thinking, with_max_steps, with_structured_output, with_description.

ToolSelection

ToolSelection::resolve(available) turns the selection into concrete names, always intersected with what the runner’s ToolRegistry actually contains and in registry order:

VariantTOML / JSONResolves to
None"none"nothing
ReadOnly"read_only"available names in READ_ONLY_TOOLS (read_file, glob, grep, list_dir, web_fetch, web_search)
ReadWrite"read_write"ReadOnly plus write_file
All"all"every available tool, including plugin tools
Named(names){"named": [...]}the listed names that exist; unknown names are dropped silently

ThinkingLevel

Levelbudget()Anthropic (adaptive / budget form)OpenAI-compatible reasoning_effort
offNonethinking not sentnot sent
low1 024effort low / budget_tokens: 1024low
medium4 096effort medium / budget_tokens: 4096medium
high16 384effort high / budget_tokens: 16384high
max32 768effort max / budget_tokens: 32768high

The Anthropic provider picks the form from the model id (ThinkingMode::for_model): adaptive thinking for Claude 4.6 and later, a token budget for older models.

When a level has a budget, the runner raises the step’s output budget to at least budget + 4096 (effective_max_tokens), because providers with extended thinking require the output budget to exceed the thinking budget.

ModelSelection

phase (the default) uses the model the caller configured for the current pipeline phase; {"fixed": {"provider": "anthropic", "model": "claude-opus-5"}} pins one. The runner itself receives a concrete provider and model id; honouring ModelSelection is the caller’s responsibility when it builds the runner.

Building a runner

use std::sync::Arc;
use vibe_agents::{AgentRunner, builtin_agent};
use vibe_core::{AgentRole, EventBus, Permissions, Registry, ToolContext, ToolRegistry};

let runner = AgentRunner::new(provider, "claude-sonnet-5".into(), tools, Arc::new(registry), EventBus::default())
    .context_window(200_000)
    .task(task.clone())
    .tool_context(ToolContext::new(&workspace.root))
    .permissions(Permissions::local())
    .var("spec", spec.to_markdown())
    .cancel_token(cancel_rx);

let spec = builtin_agent(&AgentRole::Coder).expect("built-in");
let outcome = runner.run(&spec, "Implement subtask 2.".into()).await?;
BuilderDefaultEffect
context_window(tokens)DEFAULT_CONTEXT_WINDOW = 200 000basis of the 85 % / 90 % thresholds
run_id(id)fresh RunIdattached to every event
task(task)noneprovides task_title, task_description, task_id for tools and hooks
subtask(id)nonereported in agent_started
tool_context(ctx)current directoryworkspace root and permissions of tool calls
permissions(p)Permissions::default() (local, no network)applied to the tool context
max_tool_output_chars(n)DEFAULT_MAX_TOOL_OUTPUT_CHARS = 100 000truncation threshold
var / extra_varsnoneprompt variables; override the built-in ones
cancel_token(rx)nonetokio::sync::watch::Receiver<bool>
max_provider_retries(n)2runner-level retries of retryable provider errors
retry_base_delay(d)1 sbase of the runner’s exponential backoff

A runner is cheap to clone; every run starts a fresh conversation, and resume(spec, history, message) continues an existing transcript.

The loop

Before the first step the runner renders the system prompt (template plus the augment_prompt output of every hook, each separated by a blank line), resolves the tool selection, estimates the size of the tool schemas, sets ctx.agent to the role name and ctx.task_id from the task, and publishes agent_started.

loop:
  cancelled?                                   ─ yes ─▶ stop Cancelled
  steps >= max_steps?                          ─ yes ─▶ stop MaxSteps
  converging agent, steps >= ceil(75% max)?    ─ once ─▶ append CONVERGE_MESSAGE
  estimate >= 90% of context window?           ─ yes ─▶ stop ContextWindow
  estimate >= 85% of context window?           ─ once ─▶ append CONTEXT_WARNING_MESSAGE

  complete (with runner retries)
      ContextTooLong ─▶ stop ContextWindow
      Cancelled      ─▶ stop Cancelled
      other error    ─▶ stop Error{kind, message}
  steps += 1, usage += response.usage
  publish agent_text (if the answer has text)

  tool calls?  yes ─▶ execute, append assistant + one user message of results, loop
               no  ─▶ stop_reason MaxTokens and not yet nudged ─▶ append CONTINUE_NUDGE, loop
                      otherwise                                ─▶ stop Completed

The estimate is Message::estimate_tokens over the system prompt and transcript plus the tool schemas (4 characters per token). Injected messages are merged into the last user message when there is one, because providers reject two consecutive user turns.

ConstantValue
CONTEXT_WARNING_PERCENT85
CONTEXT_STOP_PERCENT90
CONVERGE_STEP_PERCENT75
CONTEXT_WARNING_MESSAGE“You are near the context limit: finish now, write your final output.”
CONVERGE_MESSAGE“Converge now: produce your final structured answer.”
CONTINUE_NUDGE“continue (your previous answer was cut off by the output limit; resume exactly where it stopped)”

A converging agent is any agent with structured_output = true, plus qa_reviewer and spec_critic. Coders are never told to converge.

Stop conditions

AgentStop (tag reason)Causeis_success()
completedthe model ended a turn without tool callsyes
max_stepsmax_steps round trips doneno
context_window90 % threshold reached, or the provider answered ContextTooLongno
cancelledcancel token set, or cancelled during a retry waitno
error (kind, message)non-retryable provider error or retries exhaustedno

run returns Err only for failures outside the conversation; a provider failure is an AgentStop::Error inside Ok(AgentOutcome), so the transcript is never lost. AgentOutcome holds role, stop, final_text (text of the last assistant message), messages, steps, tool_calls and usage.

Tool execution

The calls of one step are split into runs. Consecutive calls to non-mutating tools (Tool::is_mutating() == false) run concurrently with join_all; a call to a mutating tool runs alone, after everything before it and before everything after it. Results are always returned in the order of the calls, as one user message of tool_result blocks.

For each call:

  1. publish tool_called;
  2. unknown tool: error result Unknown tool `x`. Available tools: …;
  3. otherwise run every hook’s before_tool in registration order; the first Abort wins and yields an error result telling the model not to repeat the call;
  4. otherwise call the tool, catching both Err (Tool `x` failed: …) and panics (Tool `x` crashed: …), then run every hook’s after_tool;
  5. truncate the output if needed and publish tool_returned with a 200-character preview.

Tool failures never stop the run: the model sees them and can adapt.

Truncation and .vibe/tool-output/

An output longer than max_tool_output_chars characters is cut to that length. The full text is written to <workspace_root>/.vibe/tool-output/<uuid>.txt and a note is appended: [Output truncated: showing the first N of M characters. The full output was saved to `…`; read it in smaller pieces if you need more.]. If the file cannot be written, the note says so instead.

Trace of tool outputs

Independently of that truncation, a runner given a ToolTrace (dir, reference, max_chars) writes the complete output of every call to <dir>/<call>.txt, cut at max_chars characters with a final marker line, and reports it in tool_returned (output_file = <reference>/<call>.txt, output_chars = the untruncated length). The pipeline passes .vibe/tool-output/<task dir>/<run>/ of the project when pipeline.trace_outputs is on. The call id is generated before the call and repeated on the return, so parallel read-only calls pair even when they return out of order; exit_code and timed_out are copied from the tool’s metadata (bash). A trace file that cannot be written is logged as a warning and leaves output_file empty; the call itself is not affected. vibe trace and the read layer’s trace module read the store back (Tool call trace, ADR-008).

Provider errors and retries

Providers retry transient failures themselves (see Providers and models). The runner adds a last line of defence: a retryable error (RateLimited, ServerError, Network) is retried up to max_provider_retries times, waiting the provider’s retry_after when present, else retry_base_delay * 2^(attempt-1). Each retry publishes retrying. The cancel token is checked after each wait.

Events

EventWhen
agent_startedbefore the first step
agent_deltawhile a step streams: text or reasoning pieces, merged when several arrive together; ephemeral (not in events.jsonl)
agent_textafter each step whose answer contains text
tool_called / tool_returnedaround each tool call
retryingbefore each runner-level retry
agent_finishedat the end, with steps, usage and the serialised stop

Cancellation

Cancellation is cooperative: the watched value is checked before every step and after each retry delay. A tool call already in flight completes; the run then stops with cancelled before the next model call.

Structured output

Agents with structured_output = true end their answer with a JSON document. run_structured::<T>(runner, spec, message) runs the agent and deserialises it into any serde type, escalating from cheap to expensive:

  1. extract_json(final_text): the whole trimmed text; else the last fenced block tagged json; else the last untagged fence; else balanced top-level {…} / […] spans from last to first (brackets inside strings are skipped). Only objects and arrays are accepted.
  2. repair_json: one completion without tools, temperature 0, at most 8 192 output tokens, asking the model to return only valid JSON for a schema hint (the type name of T, or the hint given to run_structured_with_hint).
  3. If the run was cancelled or ended in error, stop here with that error.
  4. Resume the agent once with Your previous answer was not valid JSON: <error>. Return only JSON., then parse again. Steps, tool calls and usage of both runs are summed.

A second failure returns ErrorKind::InvalidRequest. What the caller does with it (for QA, count the round as inconclusive) is the caller’s policy.

Continuation

run_with_continuation(runner, spec, message, policy) handles long sessions. While a run stops with context_window and fewer than policy.max_continuations continuations happened (default 5), it summarises the transcript and starts a fresh run whose first message is the original message followed by Summary of your previous session: and Continue where you left off.

summarize_transcript renders the transcript as plain text (tool inputs cut to 400 characters, results to 1 500, the whole to the most recent 300 000 characters) and asks for handover notes of at most summary_max_words (default 600) words with the sections Goal, Done so far, Key findings, Problems, Next steps. If summarisation fails, the last outcome is returned as is. The combined outcome appends transcripts and sums counters.

Built-in agents

RoleToolsThinkingStructuredmax_stepsmax_tokens
complexity_assessorread_onlylowyes308 192
spec_gathererread_onlymediumyes10016 384
spec_researcherread_onlymediumno10016 384
spec_writerread_writehighyes10032 000
spec_criticread_onlyhighyes6032 000
plannerread_onlyhighyes10032 000
coderalllowno30016 384
coder_recoveryallmediumno30016 384
qa_reviewerallhighyes20032 000
qa_fixerallmediumno20016 384
merge_resolvernonelowno332 000
commit_messagenonelowno24 096

Every built-in agent uses model = phase. Prompts live in crates/vibe-agents/src/prompts/<role>.md and are embedded with include_str!.

Prompt variables

Each prompt starts with an HTML comment listing its variables; strip_doc_comment removes it before rendering and documented_variables parses it. The runner always provides the COMMON_VARIABLES task_title, task_description, workspace_root and date (YYYY-MM-DD); the rest come from AgentRunner::var. Unknown placeholders render as empty strings.

VariableUsed by
specspec_critic, planner, coder, coder_recovery, qa_reviewer, qa_fixer, merge_resolver
plancoder, coder_recovery, qa_reviewer
subtaskcoder, coder_recovery, commit_message
progresscoder, coder_recovery, qa_reviewer
memoryspec_gatherer, spec_researcher, spec_writer, planner, coder, coder_recovery, qa_reviewer, qa_fixer
prior_contextevery role except commit_message
qa_reportqa_fixer
spec_pathspec_writer
file_path, conflictmerge_resolver
diffcommit_message

commit_message and merge_resolver do not document workspace_root; commit_message does not document date either.

TOML overrides

load_agent_overrides(dir) reads every *.toml file of a directory (normally project_agents_dir(root) = .vibe/agents) in file-name order; apply_agent_overrides applies them to a Registry. Unknown keys are rejected (deny_unknown_fields). A file naming an existing role changes only the keys it sets; any other role defines a new agent and must provide system_prompt or system_prompt_file (relative to the TOML file), never both.

role = "qa_reviewer"
model = "anthropic/claude-opus-5"   # or "phase", or {fixed = {provider = "…", model = "…"}}
thinking = "max"
max_steps = 250
tools = ["read_file", "grep", "glob", "bash"]   # or "none" | "read_only" | "read_write" | "all"
system_prompt_file = "qa_reviewer.md"

max_steps and max_tokens are clamped to at least 1. The user-facing guide is Customising agents.

Extension points

Everything replaceable in Vibe Factory is a small trait in vibe-core, and every implementation, built-in or third-party, is collected in one Registry (ADR-002). This page gives, for each trait, its signature (abridged), the contract implementations must honour, a minimal example and the way it reaches the registry. Agents are not a trait but data (AgentSpec); they are covered at the end, together with the EventBus.

All traits are Send + Sync and async methods use #[async_trait::async_trait]. Implementations are shared as Arc<dyn Trait> (SharedProvider, SharedTool, SharedWorkspaceProvider, SharedMemory, SharedTaskStore, SharedHook), so they must be safe to call from several tasks at once. Errors are vibe_core::Error; pick the ErrorKind that matches the failure, because callers branch on it.

The Registry

pub struct Registry {
    pub tools: ToolRegistry,                               // by name, insertion order
    pub providers: BTreeMap<String, SharedProvider>,
    pub agents: BTreeMap<String, AgentSpec>,               // by role name
    pub workspaces: BTreeMap<String, SharedWorkspaceProvider>,
    pub memories: BTreeMap<String, SharedMemory>,
    pub hooks: Vec<SharedHook>,                            // registration order
}
MethodBehaviour
add_tool, add_provider(name, p), add_agent, add_workspace, add_memory(name, m)insert, replacing an entry with the same key
add_hookappend; hooks never replace each other
agent(&role), provider(name)lookups
before_phase, before_toolrun hooks in order; the first Abort(r) wins and is returned as Abort("<hook name>: r")
after_phase, after_toolrun every hook
augment_prompt(role, task)collect every non-None augmentation, in hook order

Because later registrations replace earlier ones, registration order is precedence. Within PluginHost::register_all, native plugins register before out-of-process plugins, so a remote plugin’s tool overrides a native one of the same name (a warning is logged).

ModelProvider

#[async_trait]
pub trait ModelProvider: Send + Sync {
    fn info(&self) -> ProviderInfo;   // name, supports_tools, supports_thinking, default_model
    async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse>;
    async fn health_check(&self) -> Result<()> { Ok(()) }
}

Contract.

  • Accept the provider-neutral request: system, messages of ContentBlocks, tools, max_tokens, temperature, thinking_budget, stop_sequences and opaque extra fields. Never leak a vendor wire format out of the provider.
  • An empty request.model means “your default model”.
  • Retry transient failures yourself. Return RateLimited (with retry_after when known), ServerError or Network only once retries are exhausted.
  • Map failures precisely: AuthFailed for missing or rejected credentials (report a missing key on the first call, not at construction), ContextTooLong when the prompt does not fit (the runner turns it into AgentStop::ContextWindow), InvalidRequest for anything the caller must fix, including billing problems.
  • Return tool calls as ContentBlock::ToolUse with a unique id and StopReason::ToolUse.
  • Scrub secrets from error messages (vibe_providers::scrub_secrets does it).

Example.

#![allow(unused)]
fn main() {
use vibe_core::provider::ProviderInfo;
use vibe_core::{CompletionRequest, CompletionResponse, Message, ModelProvider, Result, StopReason, Usage};

/// Answers every request with the last user text, reversed.
struct Reverse;

#[async_trait::async_trait]
impl ModelProvider for Reverse {
    fn info(&self) -> ProviderInfo {
        ProviderInfo { name: "reverse".into(), supports_tools: false,
                       supports_thinking: false, default_model: "reverse-1".into() }
    }

    async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
        let last = request.messages.last().map(Message::text).unwrap_or_default();
        let message = Message::assistant(last.chars().rev().collect::<String>());
        Ok(CompletionResponse { message, stop_reason: StopReason::EndTurn,
                                usage: Usage::default(), model: "reverse-1".into() })
    }
}
}

Registration. registry.add_provider("reverse", Arc::new(Reverse)). To make a new kind usable from [providers.<name>], teach the provider registry a factory: ProviderRegistry::builder().register_kind("reverse", |_name, _cfg| Ok(Arc::new(Reverse) as SharedProvider)).build(&config)?, then install_into(&mut registry).

Tool

#[async_trait]
pub trait Tool: Send + Sync {
    fn name(&self) -> &str;                       // unique, snake_case
    fn description(&self) -> &str;                // shown to the model
    fn input_schema(&self) -> serde_json::Value;  // JSON schema of the input object
    fn is_mutating(&self) -> bool { false }
    async fn call(&self, ctx: &ToolContext, input: serde_json::Value) -> Result<ToolOutput>;
    fn spec(&self) -> ToolSpec { /* name + description + schema */ }
}

Contract.

  • Resolve every user-supplied path with ctx.resolve_path(path), which normalises . and .., resolves symlinks of the existing prefix and returns Denied when the path escapes ctx.workspace_root (or the allowed extra_read_paths).
  • Honour ctx.permissions (read, write, execute, network) and return Error::denied(…) otherwise.
  • Return true from is_mutating if the tool writes files or runs arbitrary commands. The runner runs consecutive non-mutating calls concurrently, so a wrong false creates races.
  • Prefer ToolOutput::error("sentence the model can act on") for expected failures. An Err or a panic is caught by the runner and reported as Tool `x` failed/crashed: …, but a precise sentence helps the model more.
  • ToolOutput::metadata is for observers and is never shown to the model.

Example.

#![allow(unused)]
fn main() {
use serde_json::{Value, json};
use vibe_core::{Error, Result, Tool, ToolContext, ToolOutput};

/// Counts the words of a file inside the workspace.
struct WordCount;

#[async_trait::async_trait]
impl Tool for WordCount {
    fn name(&self) -> &str { "word_count" }
    fn description(&self) -> &str { "Count the words of a text file." }
    fn input_schema(&self) -> Value {
        json!({"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]})
    }

    async fn call(&self, ctx: &ToolContext, input: Value) -> Result<ToolOutput> {
        if !ctx.permissions.read {
            return Err(Error::denied("word_count needs read permission"));
        }
        let Some(path) = input["path"].as_str() else {
            return Ok(ToolOutput::error("missing string argument `path`"));
        };
        let full = ctx.resolve_path(path)?;
        let text = tokio::fs::read_to_string(&full).await?;
        Ok(ToolOutput::ok(text.split_whitespace().count().to_string()))
    }
}
}

Registration. registry.add_tool(Arc::new(WordCount)). Agents see it when their ToolSelection is all or names it.

WorkspaceProvider

#[async_trait]
pub trait WorkspaceProvider: Send + Sync {
    fn name(&self) -> &str;                                               // e.g. "git_worktree"
    async fn open(&self, project_root: &Path, task: &Task) -> Result<Workspace>;
    async fn merge(&self, workspace: &Workspace) -> Result<MergeOutcome>;
    async fn merge_validated(&self, workspace: &Workspace,
        validator: &mut dyn MergeValidator) -> Result<MergeOutcome>; // default: error
    async fn discard(&self, workspace: &Workspace) -> Result<()>;
    async fn changes(&self, _workspace: &Workspace) -> Result<String> { Ok(String::new()) }
}

Contract.

  • open is idempotent: calling it again for the same task returns the same workspace, so an interrupted run can resume.
  • Workspace::root is the directory agents are confined to; it becomes ToolContext::workspace_root.
  • merge returns Merged { commit }, NoChanges, or NeedsHumanReview { files }. It must not leave the project half-merged: abort and restore before reporting conflicts. Use Error::workspace for failures that are not conflicts.
  • discard is idempotent; already-missing resources are not an error.
  • changes is a human-readable summary for reviewers.

When mandatory validation commands are configured, the pipeline calls merge_validated. Implementations must prepare the combined candidate without changing the target, call validator.validate(&candidate).await, reject candidate/target changes during validation, and publish exactly the checked result. Validation errors must leave the target unchanged. The default implementation returns an error; it never calls the unchecked merge method. The in-place implementation validates the existing workspace and has no rollback boundary.

Example. A provider handing every task a plain scratch directory, merged by hand:

#![allow(unused)]
fn main() {
use std::path::Path;
use vibe_core::{MergeOutcome, Result, Task, Workspace, WorkspaceKind, WorkspaceProvider};

struct Scratch;

#[async_trait::async_trait]
impl WorkspaceProvider for Scratch {
    fn name(&self) -> &str { "scratch" }

    async fn open(&self, project_root: &Path, task: &Task) -> Result<Workspace> {
        let root = project_root.join(".vibe").join("scratch").join(task.slug());
        tokio::fs::create_dir_all(&root).await?;
        Ok(Workspace { root, project_root: project_root.to_path_buf(),
                       kind: WorkspaceKind::Custom("scratch".into()), branch: None, base_branch: None })
    }

    async fn merge(&self, _ws: &Workspace) -> Result<MergeOutcome> {
        Ok(MergeOutcome::NeedsHumanReview { files: vec![] })
    }

    async fn discard(&self, ws: &Workspace) -> Result<()> {
        let _ = tokio::fs::remove_dir_all(&ws.root).await; // already gone is fine
        Ok(())
    }
}
}

Registration. registry.add_workspace(Arc::new(Scratch)) (keyed by name()), selected with pipeline.workspace = "scratch". The built-in providers come from vibe_workspace::provider_by_name.

MemoryStore

#[async_trait]
pub trait MemoryStore: Send + Sync {
    async fn remember(&self, entry: MemoryEntry) -> Result<()>;
    async fn recall(&self, query: &str, limit: usize) -> Result<Vec<MemoryEntry>>;
    async fn all(&self) -> Result<Vec<MemoryEntry>>;
}

Contract. recall returns at most limit entries, most relevant first, with an implementation-defined ranking; all returns every entry newest first. A MemoryEntry has a kind (gotcha, pattern, decision, discovery, dead_end, or other), content, files, tags, optional task_id and recorded_at. Use Error::storage for back-end failures. The reference implementation is InMemoryStore (term-overlap ranking).

Example. A store that keeps entries in memory and matches by substring:

#![allow(unused)]
fn main() {
use vibe_core::{MemoryEntry, MemoryStore, Result};

struct Recent(tokio::sync::Mutex<Vec<MemoryEntry>>);

#[async_trait::async_trait]
impl MemoryStore for Recent {
    async fn remember(&self, entry: MemoryEntry) -> Result<()> { self.0.lock().await.push(entry); Ok(()) }
    async fn recall(&self, query: &str, limit: usize) -> Result<Vec<MemoryEntry>> {
        let v = self.0.lock().await;
        Ok(v.iter().rev().filter(|e| e.content.contains(query)).take(limit).cloned().collect())
    }
    async fn all(&self) -> Result<Vec<MemoryEntry>> { Ok(self.0.lock().await.iter().rev().cloned().collect()) }
}
}

Registration. registry.add_memory("recent", Arc::new(Recent(Default::default()))).

TaskStore

#[async_trait]
pub trait TaskStore: Send + Sync {
    async fn save_task(&self, task: &Task) -> Result<()>;
    async fn load_task(&self, id: TaskId) -> Result<Task>;
    async fn list_tasks(&self) -> Result<Vec<Task>>;              // newest first
    async fn delete_task(&self, id: TaskId) -> Result<()>;        // with every artefact
    async fn save_spec(&self, spec: &Spec) -> Result<()>;
    async fn load_spec(&self, id: TaskId) -> Result<Option<Spec>>;
    async fn save_plan(&self, plan: &Plan) -> Result<()>;
    async fn load_plan(&self, id: TaskId) -> Result<Option<Plan>>;
    async fn save_qa_report(&self, report: &QaReport) -> Result<()>;   // one per round
    async fn load_qa_reports(&self, id: TaskId) -> Result<Vec<QaReport>>; // oldest first
    async fn append_progress(&self, id: TaskId, note: &str) -> Result<()>;
    async fn load_progress(&self, id: TaskId) -> Result<String>;
}

Contract. Saving is an upsert. A missing optional artefact is Ok(None), not an error; a missing task is Error::storage. Writes must be durable when the call returns, because resuming a run relies on what is on disk. The default file-backed implementation belongs to vibe-pipeline (see Persistence layout). The trait is not stored in the Registry; the host hands a SharedTaskStore to the pipeline directly.

Hook

#[async_trait]
pub trait Hook: Send + Sync {
    fn name(&self) -> &str;
    async fn before_phase(&self, _phase: Phase, _task: &Task) -> HookDecision { Continue }
    async fn after_phase(&self, _phase: Phase, _task: &Task, _success: bool) {}
    async fn before_tool(&self, _ctx: &ToolContext, _tool: &str, _input: &Value) -> HookDecision { Continue }
    async fn after_tool(&self, _ctx: &ToolContext, _tool: &str, _output: &ToolOutput) {}
    async fn augment_prompt(&self, _role: &AgentRole, _task: &Task) -> Option<String> { None }
}

Contract. Every method has a no-op default; override what you need. Hooks sit on the hot path of every tool call, so keep them fast. An Abort(reason) from before_tool becomes an error result the model reads, so write the reason as a sentence. augment_prompt output is appended to the rendered system prompt, separated by a blank line.

Example.

#![allow(unused)]
fn main() {
use serde_json::Value;
use vibe_core::{AgentRole, Hook, HookDecision, Task, ToolContext};

struct NoPublish;

#[async_trait::async_trait]
impl Hook for NoPublish {
    fn name(&self) -> &str { "no-publish" }

    async fn before_tool(&self, _ctx: &ToolContext, tool: &str, input: &Value) -> HookDecision {
        let cmd = input["command"].as_str().unwrap_or_default();
        if tool == "bash" && cmd.contains("npm publish") {
            HookDecision::Abort("publishing packages is reserved for humans".into())
        } else {
            HookDecision::Continue
        }
    }

    async fn augment_prompt(&self, role: &AgentRole, _task: &Task) -> Option<String> {
        (*role == AgentRole::Coder).then(|| "Never bump package versions.".to_string())
    }
}
}

Registration. registry.add_hook(Arc::new(NoPublish)). Out-of-process plugins can only provide before_tool (see Plugin protocol).

Plugin

pub trait Plugin: Send + Sync {
    fn name(&self) -> &str;
    fn version(&self) -> &str { "0.0.0" }
    fn register(&self, registry: &mut Registry) -> Result<()>;
}

Contract. register is synchronous and may be called more than once (the host probes a native plugin once into a scratch registry to list its contributions), so it must only add entries, never perform side effects. An in-process plugin may contribute anything the Registry holds: tools, providers, agents, workspaces, memories and hooks.

struct TeamRules;

impl Plugin for TeamRules {
    fn name(&self) -> &str { "team-rules" }
    fn register(&self, registry: &mut Registry) -> Result<()> {
        registry.add_tool(Arc::new(WordCount)).add_hook(Arc::new(NoPublish));
        Ok(())
    }
}

let mut host = PluginHost::load(&configs).await?;   // out-of-process plugins
host.add_native(Box::new(TeamRules));
host.register_all(&mut registry)?;

AgentSpec

Agents are added as data: registry.add_agent(spec) keyed by spec.role.name(). A spec for a built-in role replaces the built-in agent; any other role adds a new one. Specs can come from code (AgentSpec::new), from TOML files in .vibe/agents/ (vibe_agents::apply_agent_overrides) or from a plugin’s agents/list. See Agent runtime and Customising agents.

EventBus and EventSink

#[async_trait]
pub trait EventSink: Send + Sync {
    async fn on_event(&self, envelope: &Envelope);   // must not block for long
}

EventBus::new(capacity) (default 1 024) wraps a tokio::sync::broadcast channel plus a list of sinks. publish(event) stamps an Envelope, sends it to every live subscribe() receiver (a lagging receiver loses old events, as with any broadcast channel) and then awaits every sink in order. Sinks are therefore the right place for things that must see every event, such as the events.jsonl writer; subscribers suit live UIs.

#![allow(unused)]
fn main() {
use std::sync::Arc;
use vibe_core::{Envelope, EventBus, EventSink};

struct Stderr;

#[async_trait::async_trait]
impl EventSink for Stderr {
    async fn on_event(&self, e: &Envelope) {
        eprintln!("{} {:?}", e.at, e.event);
    }
}

async fn wire(bus: &EventBus) { bus.add_sink(Arc::new(Stderr)).await; }
}

Plugin protocol

The Vibe Plugin Protocol (VPP) lets a program written in any language contribute tools, agent specs and a before_tool hook to a run. It is JSON-RPC 2.0 over the plugin’s stdin and stdout, shaped like the Model Context Protocol (MCP) so that an MCP stdio server works unchanged as a tool plugin (ADR-005). The reference implementation is crates/vibe-plugins: protocol.rs (wire types), client.rs (host side), server.rs (plugin side), remote.rs (adapters), host.rs, manifest.rs and discovery.rs.

Framing

  • One JSON object per line, UTF-8, terminated by \n (newline-delimited JSON). A message must not contain a raw newline.
  • The host writes requests and notifications to the plugin’s stdin and reads responses and notifications from its stdout.
  • stderr is free-form diagnostics; the host logs each line at debug level as [<plugin name>] <line>. Never write diagnostics to stdout.
  • Blank lines are ignored. An unparsable line from a plugin is logged and skipped.

Envelope

KindShapeRule
Request{"jsonrpc":"2.0","id":N,"method":"…","params":{…}}has method and a non-null id; expects exactly one response
Notification{"jsonrpc":"2.0","method":"…","params":{…}}has method, no id; never answered
Response{"jsonrpc":"2.0","id":N,"result":…} or {…,"error":{"code","message","data?"}}exactly one of result and error

id may be a number or a string; the host generates increasing integers starting at 1 and only matches numeric ids. Responses may arrive in any order; the plugin may handle requests concurrently. params is omitted when there are none (shutdown).

Error codeConstantMeaning
-32700PARSE_ERRORinvalid JSON received
-32600INVALID_REQUESTnot a valid request
-32601METHOD_NOT_FOUNDunknown method (mandatory answer for methods you do not implement)
-32602INVALID_PARAMSbad parameters (also used by PluginServer for an unknown tool)
-32603INTERNAL_ERRORinternal failure

An error response is reported by the host as plugin `<name>` returned an error: <message> (code <code>).

Lifecycle

host                                          plugin
 │  spawn command in cwd, env, piped stdio      │
 │── initialize ───────────────────────────────▶│   60 s
 │◀──────────── {name, version, capabilities} ──│
 │── notifications/initialized ────────────────▶│   (ignore it)
 │── tools/list  (if tools)   ─────────────────▶│   60 s, follows nextCursor
 │── agents/list (if agents)  ─────────────────▶│   60 s
 │        … run: tools/call (600 s), hooks/before_tool (60 s) …
 │── shutdown ─────────────────────────────────▶│   2 s
 │   close stdin, wait 2 s for exit, then kill  │

If the handshake or a listing fails, the process is shut down and the plugin counts as not started. The host only calls tools/*, agents/list and hooks/before_tool when the matching capability was advertised. All host writes go through one writer task that owns the plugin’s stdin and writes whole queued lines one at a time, so a caller that times out or is cancelled never leaves a partial line: its line is written whole, or skipped if its turn had not come yet. A failed write marks the connection dead and fails every pending and later call fast; after shutdown closed stdin, writes fail with plugin `<name>` is closed.

ConstantValueApplies to
DEFAULT_TIMEOUT60 severy request except tools/call
TOOL_CALL_TIMEOUT600 stools/call
SHUTDOWN_GRACE2 sanswer to shutdown, then again for process exit

A timeout surfaces as plugin `<name>` did not answer `<method>` within <n>s.

Methods

initialize

The host sends the VPP fields and, for MCP servers, the MCP fields:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
  "protocol_version":"1",
  "host":{"name":"vibe-factory","version":"0.5.1"},
  "protocolVersion":"2025-06-18",
  "clientInfo":{"name":"vibe-factory","version":"0.5.1"},
  "capabilities":{}}}
{"jsonrpc":"2.0","id":1,"result":{"name":"echo","version":"0.1.0",
  "capabilities":{"tools":true,"agents":true,"hooks":true}}}

protocol_version is PROTOCOL_VERSION ("1"), protocolVersion is MCP_PROTOCOL_VERSION ("2025-06-18"). Each capability flag is a boolean; when reading, an object ({"tools": {}}) counts as true and null or absence as false. If name or version is absent, the MCP serverInfo object is used.

tools/list

{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
{"jsonrpc":"2.0","id":2,"result":{"tools":[
  {"name":"echo","description":"Return the given text unchanged.",
   "input_schema":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]},
   "mutating":false}]}}
Descriptor fieldNotes
namerequired; registered under this name, overriding a same-named tool
descriptiondefault ""
input_schemainputSchema accepted; default {"type":"object"}
mutatingoptional; when absent, !annotations.readOnlyHint; when that is absent too, true

If the result carries nextCursor, the host requests {"cursor": "<value>"} again, up to 100 pages.

tools/call

{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{
  "name":"echo","arguments":{"text":"hi"},
  "_meta":{"vibe":{"workspace_root":"/repo/.vibe/worktrees/add-oauth-6f1c3e0a",
                   "task_id":"6f1c3e0a-9b2d-4c47-8a51-0e3b8f2d7c19","agent":"coder",
                   "permissions":{"read":true,"write":true,"execute":true,"network":false}}}}}
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"hi"}],"is_error":false}}

_meta.vibe is the ToolCallContext built from the agent’s ToolContext: the workspace root the tool must stay inside, the task id (omitted when there is none), the calling agent’s role name and its permission flags. A plugin tool that touches files must resolve paths against workspace_root and refuse to leave it; the host cannot enforce containment inside another process. MCP servers ignore _meta.

The result’s text blocks are joined with newlines; any non-text block becomes [<type> content omitted]. is_error (or isError) turns the result into an error the model sees. A tool failure should be a result with is_error: true, not a JSON-RPC error; PluginServer does this for handlers that return Err.

agents/list

{"jsonrpc":"2.0","id":4,"method":"agents/list","params":{}}
{"jsonrpc":"2.0","id":4,"result":{"agents":[
  {"role":"echo_agent","system_prompt":"You repeat what you are told using the `echo` tool.",
   "description":"Echoes its instructions","tools":{"named":["echo"]},"thinking":"low","max_steps":20}]}}

Each entry is the JSON form of AgentSpec. Only role and system_prompt are required; tools, model, thinking, max_steps, structured_output and max_tokens take the AgentSpec defaults. A built-in role name replaces the built-in agent.

hooks/before_tool

{"jsonrpc":"2.0","id":5,"method":"hooks/before_tool","params":{"tool":"forbidden","input":{}}}
{"jsonrpc":"2.0","id":5,"result":{"decision":"abort","reason":"the echo plugin forbids this tool"}}

decision is continue (default) or abort; reason defaults to "vetoed by plugin". The hook sees every tool call of every agent. Remote hooks fail closed: if the plugin times out, has exited or answers with an error, the call is aborted with hook plugin unavailable: …. before_phase, after_phase, after_tool and augment_prompt have no protocol method; they are available to in-process plugins only.

shutdown and ping

shutdown has no params and is answered with {}; the plugin then exits. An MCP server that does not know shutdown is tolerated: the host closes stdin and kills the process after the grace period. ping is answered with {} by either side; it is the only request a plugin may send to the host (any other gets -32601).

Notifications

DirectionMethodParams
host → pluginnotifications/initializednone; sent once after initialize, may be ignored
plugin → hostlog{"level": "error"|"warn"|"info"|"debug"|"trace", "message": "…"}
plugin → hostnotifications/message (MCP){"level": "…", "data": …}; data is used when message is absent

Log levels map onto tracing levels (warning is accepted, critical, alert and emergency map to error, anything else to info).

MCP compatibility

MCP fieldAccepted as
serverInfofallback for name and version
capability objects {"tools": {}}true
inputSchemainput_schema
annotations.readOnlyHintinverse of mutating
nextCursorpagination of tools/list
isErroris_error
notifications/messagelog

MCP servers expose neither agents nor VPP hooks, so only their tools are registered.

Manifests and declarations

A plugin is declared inline in .vibe/config.toml ([[plugins]], see Plugins) or by a manifest vibe-plugin.toml:

name = "echo"                            # required, unique
version = "0.1.0"
description = "Echoes its input"
command = ["./bin/vibe-echo-plugin"]     # required; program then arguments
required = false                         # true: a failure to start aborts loading
capabilities = ["tools", "agents", "hooks"]   # or [capabilities] tools = true …; informational

[env]
ECHO_PREFIX = ">"

The process runs in the manifest’s directory. A program written as a relative path (containing a separator, such as ./bin/p) is resolved against that directory; a bare name (python3) is looked up on PATH. Declared capabilities are informational: the initialize answer is authoritative.

Discovery precedence

  1. Inline [[plugins]] entries of .vibe/config.toml.
  2. <project>/.vibe/plugins/*/vibe-plugin.toml, sorted by directory name.
  3. <user config dir>/vibe/plugins/*/vibe-plugin.toml (dirs::config_dir(), for example ~/.config/vibe/plugins on Linux, ~/Library/Application Support/vibe/plugins on macOS).

The first declaration of a name wins; later ones are skipped with a debug log. Invalid manifests are logged and skipped. discovery::plugin_configs(project, &config) returns the merged list.

Host failure policy

PluginHost::load starts every plugin concurrently.

SituationResult
optional plugin fails to spawn or handshakewarning skipping plugin …, run continues
required = true plugin failsevery started plugin is shut down; ErrorKind::Plugin error required plugin `x` failed to start: …
same name declared twicethe duplicate is ignored with a warning
tool call times out or errorserror result to the model; the run continues
hook plugin unavailablethe guarded tool call is aborted (fail closed)

Writing a plugin in Rust

Add vibe-plugins, vibe-core, tokio and serde_json as dependencies and build a PluginServer:

use serde_json::{Value, json};
use vibe_core::{AgentRole, AgentSpec, HookDecision, ToolOutput, ToolSelection};
use vibe_plugins::{PluginServer, ToolCallContext};

#[tokio::main]
async fn main() {
    let server = PluginServer::new("todo-scan", "0.1.0");
    let log = server.logger();
    let server = server
        .tool_with_context(
            "count_todos",
            "Count TODO markers in a file of the workspace.",
            json!({"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}),
            move |args: Value, ctx: Option<ToolCallContext>| {
                let log = log.clone();
                async move {
                    let root = ctx.map(|c| c.workspace_root).unwrap_or_else(|| ".".into());
                    let path = root.join(args["path"].as_str().unwrap_or_default());
                    let text = tokio::fs::read_to_string(&path).await?;
                    log.log("debug", format!("scanned {}", path.display()));
                    Ok(ToolOutput::ok(text.matches("TODO").count().to_string()))
                }
            },
        )
        .agent(
            AgentSpec::new(AgentRole::Custom("todo_auditor".into()), "List the TODOs of {{task_title}}.")
                .with_tools(ToolSelection::Named(vec!["count_todos".into()])),
        )
        .before_tool(|tool, _input| async move {
            if tool == "bash" { HookDecision::Abort("todo-scan forbids the shell".into()) } else { HookDecision::Continue }
        });
    let code = if server.run_stdio().await.is_ok() { 0 } else { 1 };
    std::process::exit(code);
}

The example joins paths without a containment check for brevity; a real tool must reject .. and absolute paths. Notes:

  • tool / tool_with / tool_with_context register a handler; an Err becomes a result with is_error: true. Use tool_with(ToolDescriptor { mutating: Some(false), .. }) to let the host run the tool concurrently.
  • Capabilities are derived from what you registered.
  • Requests run concurrently; shutdown or end of input stops the server.
  • Call std::process::exit after run_stdio returns: Tokio reads stdin on a blocking thread that would otherwise keep the runtime alive. vibe-echo-plugin does this.
  • PluginServer::serve(reader, writer) and PluginServer::handle(method, params) let you test the plugin without a process.

Build it, then add .vibe/plugins/todo-scan/vibe-plugin.toml with command = ["/path/to/target/release/todo-scan"].

Writing a plugin in Python

Any language that can read lines and write JSON works. This sketch implements the contract with one read-only tool and a hook:

#!/usr/bin/env python3
import json, sys

TOOLS = [{"name": "upper", "description": "Uppercase a text.", "mutating": False,
          "input_schema": {"type": "object", "properties": {"text": {"type": "string"}},
                           "required": ["text"]}}]

def handle(method, params):
    if method == "initialize":
        return {"name": "upper", "version": "0.1.0",
                "capabilities": {"tools": True, "agents": False, "hooks": True}}
    if method == "tools/list":
        return {"tools": TOOLS}
    if method == "tools/call":
        text = params.get("arguments", {}).get("text", "")
        return {"content": [{"type": "text", "text": text.upper()}], "is_error": False}
    if method == "hooks/before_tool":
        if params.get("tool") == "bash" and "rm -rf" in json.dumps(params.get("input")):
            return {"decision": "abort", "reason": "no recursive deletes"}
        return {"decision": "continue"}
    if method in ("ping", "shutdown"):
        return {}
    raise KeyError(method)

for line in sys.stdin:
    if not line.strip():
        continue
    msg = json.loads(line)
    if "id" not in msg or "method" not in msg:
        continue                                   # notification or stray response
    try:
        reply = {"jsonrpc": "2.0", "id": msg["id"], "result": handle(msg["method"], msg.get("params") or {})}
    except KeyError:
        reply = {"jsonrpc": "2.0", "id": msg["id"],
                 "error": {"code": -32601, "message": "method not found: " + msg["method"]}}
    print(json.dumps(reply), flush=True)
    if msg["method"] == "shutdown":
        break

Declare it with command = ["python3", "upper.py"] in its manifest directory.

Security model

Vibe Factory lets language models edit files and run commands in a developer’s repository. The security model answers one question: what is the worst a confused or manipulated model can do, and how do we bound it? It is built in layers; each layer is independent and any one of them can be tightened without touching the others.

 ┌───────────────────────────────────────────────────────────┐
 │ 5. Isolation      git worktree (default) / plugin sandbox │
 │ 4. Hooks          before_tool veto, audit, prompt rules   │
 │ 3. Shell policy   parser + blocked programs + validators  │
 │ 2. Permissions    read / write / execute / network        │
 │ 1. Path containment   every path inside the workspace     │
 └───────────────────────────────────────────────────────────┘

1. Path containment (vibe-core)

ToolContext::resolve_path is the single entry point for paths. It joins relative paths to the workspace root, normalises . and .. lexically, canonicalises the longest existing prefix (resolving symbolic links), re-appends the non-existing suffix and checks that the result starts with the canonical workspace root (or one of Permissions::extra_read_paths).

Two subtleties are handled explicitly:

  • a dangling symbolic link cannot be canonicalised; the path is denied, because its eventual target is unknown and a write would create a file at that target;
  • the writing tools (write_file, edit_file, the bash working directory) walk every component with symlink_metadata and refuse any symbolic link, even one that points inside the workspace, so that a later retarget cannot redirect a write.

extra_read_paths are honoured for reading only; the writing tools require the canonical path to be inside the workspace root proper.

2. Permissions (vibe-core)

Permissions { read, write, execute, network, extra_read_paths } travel with every ToolContext. The built-in tools check them before doing anything. Presets: read_only(), local() (default: everything but network) and all(). The pipeline derives them from SecurityConfig and from the agent’s ToolSelection: a read-only agent never receives a writing tool in the first place, and even if it did, the permission check would refuse.

3. Shell policy (vibe-tools)

The bash tool hands the command line to SecurityPolicy::validate_with_network before spawning anything. The policy is a denylist with per-command validators, optionally narrowed by an allowlist (ADR-004).

Parsing

command_parser::parse_command splits on |, ||, &&, ;, & and newlines, descends into $(…), backticks and ( … ), strips env/time/nice/nohup/xargs/timeout/ exec/command/builtin wrappers and VAR=value prefixes, and returns one CommandSegment { program, argv } per command, up to MAX_NESTING_DEPTH (16). It fails closed on case/function constructs, env -S, here-documents containing substitutions, redirects without target and unbalanced quotes or parentheses. Program names that would be computed at run time ($, backtick, *, ?, [, {, }, ^, %) are denied.

Rules, in order

  1. BLOCKED_PROGRAMS and SecurityConfig::blocked_commands → deny.
  2. SecurityConfig::allowed_commands non-empty and program not in it (nor in ALWAYS_ALLOWED, the harmless builtins) → deny.
  3. NETWORK_PROGRAMS and network not permitted → deny.
  4. Per-command validators: rm, chmod, git, kill, pkill/killall, the shells (-c validated recursively; stdin-fed shells denied), eval, find -exec, database clients, dropdb/dropuser. The exact rules are listed in the user guide.

Every denial is a sentence returned to the model as an error tool result (ToolOutput::error, metadata.denied = true), never a framework error, so the agent can adapt instead of crashing the run.

Execution

Commands run through sh -c (Unix) or cmd /C (Windows) in the workspace, with stdin closed, GIT_TERMINAL_PROMPT=0 and pagers disabled. A timeout kills the whole process group (taskkill /T /F on Windows). Output is merged, capped at 30 000 characters keeping head and tail, and ends with the exit code.

4. Hooks (vibe-core, plugins)

Hook::before_tool runs before every tool call with the ToolContext, tool name and input; an Abort(reason) becomes an error tool result. after_tool sees every output. augment_prompt injects project rules into the system prompt. Remote hooks (plugins) fail closed: if the plugin cannot answer, the call is aborted. This is where teams put policies the static layer cannot express.

5. Isolation (vibe-workspace, plugins)

The default workspace is a git worktree on its own branch (ADR-003); the project checkout is never modified until the merge step, which refuses to run on a dirty tree. With the git worktree workspace each subtask attempt also gets its own worktree, integrated into the task branch one at a time.

The container workspace (vibe_workspace::ContainerWorkspace) keeps the worktrees and changes where commands run. vibe_core::CommandRunner is the seam: the bash tool checks the policy and the paths as usual, then asks the runner for the process to spawn (PreparedCommand: program, arguments, a cleanup command and the exit status that means “the runner failed”). ContainerRunner returns docker run --rm (or podman run --rm) with fixed hardening flags (--cap-drop=ALL, --security-opt=no-new-privileges, --read-only, a /tmp tmpfs, --pids-limit), the worktree bind-mounted at /workspace, its .git pointer mounted read-only over itself so a command cannot redirect the host’s git to a crafted directory, and --network=none unless a network is both configured and granted. The cleanup (rm --force <name>) runs when the command ends, times out or its future is dropped, which kills everything the command started. Settings are validated before any container starts (ContainerSettings::from_config): namespace-sharing networks, sockets, writable mounts over .git or .vibe and reserved targets are refused, and there is no raw flag passthrough. File tools are unaffected: they act on the host worktree under path containment. VMs and remote sandboxes remain WorkspaceProvider plugins.

A worktree shares .git/config with the user’s checkout, so the provider treats the repository configuration as untrusted after agents have run:

  • every framework git command passes NEUTRAL_CONFIG (core.hooksPath pointing at an empty directory, core.fsmonitor=false, core.pager=cat, core.editor=true, core.sshCommand=ssh) and GIT_CONFIG_NOSYSTEM=1, so no hook or helper runs during a checkpoint commit, a merge or a checkout;
  • config_guard::ConfigSnapshot captures git config --local --list at open; merge diffs the current configuration against it and fails with the list of changed keys unless only volatile keys changed (branch.vibe/*, core.repositoryformatversion). A missing snapshot fails closed. accept_config_changes re-baselines after a human review.

The writing tools and the shell policy additionally refuse any path inside a .git directory, so hook scripts cannot be planted without going through the configuration.

Secrets

Provider layers scrub sk-…, Bearer …, token=, api_key= and access_token= from every logged message (vibe_providers::scrub_secrets). API keys are read from environment variables by default (api_key_env); inline keys in config.toml are supported but discouraged. Plugins receive only the environment you declare in their configuration.

Known limits

  • The shell policy is defence in depth, not a sandbox: interpreters (python -c, node -e, perl -e) and build tools that run arbitrary scripts are allowed.
  • On Windows the command is parsed with POSIX rules before being handed to cmd /C; cmd-specific tricks beyond ^ and % escapes may not be recognised.
  • Prompt injection through repository content (a README telling the agent to run something) is mitigated by the same layers, not by detection.

Report weaknesses privately to the maintainers before opening a public issue.

Persistence layout

Vibe Factory keeps all of its state in plain files under <project>/.vibe/. There is no database and no daemon: the artefacts on disk are the state machine. Each phase reads the files of the previous ones and writes its own, which makes a run inspectable with cat and jq, and resumable after a crash. This page lists every file, its shape, how it is written and which ones you may edit.

The .vibe/ directory

<project>/.vibe/
├── config.toml                     project configuration
├── agents/*.toml                   agent overrides and custom agents (+ prompt files)
├── plugins/<name>/vibe-plugin.toml plugin manifests
├── memory.jsonl                    project memory: one lesson per line, shared by tasks
├── server.token                    token of a running `vibe serve` (owner-only)
├── tool-output/<task dir>/<run>/<call>.txt   complete output of every tool call (trace store)
├── worktrees/                      one git worktree per task (git_worktree workspace)
│   ├── .gitignore                  "*"
│   └── <slug>-<id8>/               the task's workspace
│       └── .vibe/
│           ├── specs/<id8>-spec.md markdown copy written by the spec writer
│           └── tool-output/<uuid>.txt   full text of truncated tool outputs
└── tasks/
    ├── index.json                  task id → directory and number
    ├── .index.lock                 OS lock serialising index updates across processes
    └── NNN-<slug>/                 one directory per task
        ├── task.json
        ├── spec.json   spec.md
        ├── plan.json   plan.md
        ├── qa_report_1.json   qa_report_1.md      one pair per QA round
        ├── progress.md                           append-only, timestamped
        ├── memory/gotchas.md   memory/patterns.md
        ├── events.jsonl                          every event of every run
        ├── run.json                              state of the last run
        ├── run.lock   run.owner                  OS lock and pid of the process running it
        └── cancel.request                        present while a `vibe cancel` is pending
PathWritten byRead by
config.tomlvibe init, vibe config set, youevery command (Configuration)
agents/you, vibe agents exportagent loading (Customising agents)
plugins/you, plugin installersplugin discovery (Plugins)
worktrees/the git_worktree workspace provideragents, you (Workspaces)
tasks/FileTaskStore (vibe-pipeline)the pipeline, vibe task, vibe status

The tool-output/<uuid>.txt files and the specs/ directory live in the workspace, so with the in_place workspace they appear directly under <project>/.vibe/. The trace store <project>/.vibe/tool-output/<task dir>/<run>/ is always at the project root: one file per tool call, named by the call id of its tool_called/tool_returned events and capped at pipeline.trace_max_chars characters (see Events). pipeline.trace_outputs = false turns it off; vibe task discard removes the task’s directory. Workspace change summaries and the merge checkpoint commit both exclude .vibe/.

The task store

FileTaskStore::open(project_root) creates .vibe/tasks/ if needed. A task gets its directory the first time it is saved: the next sequence number, zero-padded to three digits, and the slug of its title (lowercase ASCII alphanumerics, other runs replaced by -, at most 48 characters, task if empty). The number and the directory never change afterwards, even if the title does.

index.json

{
  "next_number": 2,
  "tasks": {
    "6f1c3e0a-9b2d-4c47-8a51-0e3b8f2d7c19": { "dir": "001-add-oauth-login-github", "number": 1 },
    "a4b21f77-0c9e-4d3b-b8f1-2e6a9c0d4e58": { "dir": "002-fix-typo-in-readme", "number": 2 }
  }
}

A new number is max(next_number, highest number in use) + 1, so deleting the last task does not reuse its number. User references are resolved by find_by_prefix: a number of up to six digits (3, 003), an exact directory name (003-add-login), or a prefix of the task id; an ambiguous prefix is an error. A corrupt index is reported as corrupt task index <path>: … rather than silently rebuilt.

Files of a task

FileContentWritten
task.jsonthe Taskon creation and at every status change
spec.json, spec.mdthe Spec, and its renderingend of the spec phase
plan.json, plan.mdthe Plan with subtask statuses, attempts and notesend of plan, then after every subtask state change
qa_report_<n>.json, .mdthe QaReport of round nafter each QA round
progress.mdtimestamped notes from every phaseappended throughout
memory/patterns.mdspec findingsappended at the end of spec
memory/gotchas.mdrepeatedly failing subtasks, escalated QA issuesappended when they happen
events.jsonlone Envelope per lineappended by the run’s event sink
run.jsonthe RunState of the last runbefore and after every phase, and at the end
run.lock, run.ownerempty lock file, pidtaken by the process running the task, released when it ends or dies
cancel.requestmarkerwritten by vibe cancel, consumed by the running process

The shapes of Task, Spec, Plan and QaReport are documented in Domain model. A task.json:

{
  "id": "6f1c3e0a-9b2d-4c47-8a51-0e3b8f2d7c19",
  "title": "Add OAuth login (GitHub)",
  "description": "Users should be able to sign in with their GitHub account.",
  "status": "ready",
  "complexity": "standard",
  "labels": ["auth"],
  "source": { "kind": "manual" },
  "created_at": "2026-09-27T08:12:03.114Z",
  "updated_at": "2026-09-27T08:41:57.902Z",
  "branch": "vibe/add-oauth-login-github-6f1c3e0a"
}

branch is recorded when a run opens a workspace that has one (absent with in_place); vibe pr, vibe task show and vibe task discard use it instead of recomputing the branch from the title.

plan.md marks subtasks with [ ] pending, [~] in progress, [x] done, [!] failed and [-] skipped, and shows dependencies by title, the attempt count, verification steps and notes. Markdown files are always rendered from the JSON and never parsed back.

run.json

{
  "run_id": "a3f09c2e-5d71-4b8e-9f06-1c2d3e4f5a6b",
  "task_id": "6f1c3e0a-9b2d-4c47-8a51-0e3b8f2d7c19",
  "current_phase": "qa",
  "completed_phases": ["assess", "spec", "plan", "build", "qa", "fix", "qa", "fix"],
  "qa_round": 3,
  "profile": {
    "phases": ["spec", "plan", "build", "qa", "fix", "merge"],
    "research": false,
    "critique": false,
    "full_spec": true
  },
  "started_at": "2026-09-27T08:12:10.520Z",
  "updated_at": "2026-09-27T08:39:44.061Z",
  "status": "paused",
  "last_error": "issue `login button has no accessible label` was reported in 3 consecutive QA rounds; escalating to a human"
}
FieldMeaning
run_ididentifier of the run, kept across resumes
current_phasethe phase executing, or the next one to execute when the run is not running
completed_phasesphases that completed, in order; qa and fix may repeat
qa_roundQA reviews performed so far; numbers the report files
profilethe profile chosen by the assessment (absent until known)
statusrunning, paused, finished, failed or cancelled
last_errorwhy the run stopped, for any status other than finished

Only the last run of a task is kept: a new vibe run overwrites run.json. Its events remain in events.jsonl, which is never truncated and can be split by event.run:

jq -c 'select(.event.type == "phase_finished") | .event' .vibe/tasks/001-*/events.jsonl

Only events carrying the run’s id are written; log events without a run id are not.

Writing strategy

Every JSON and markdown artefact is written atomically: the content goes to a uniquely named temporary file in the same directory (.<name>.<uuid>.tmp), which is then renamed over the target. A crash therefore leaves either the old or the new file, never a truncated one; at worst a stray .tmp file, safe to delete. JSON is pretty-printed with a trailing newline.

Appends (progress.md, memory files) are serialised by locks inside one process; events.jsonl has its own lock per sink. Across processes, an OS lock on run.lock lets one process at a time run a given task (a second vibe run fails with already being run by another process), and .index.lock serialises updates of index.json, so a terminal, a terminal UI and vibe serve can work on the same project. Readers never hold a lock: the read layer below only consumes complete lines of the logs, and the history only probes a task’s run lock for an instant to tell a running run from an interrupted one.

Missing files are normal: a task without spec.json simply has no spec yet, and loading it returns None. A file that exists but does not parse is an error naming the path, so a bad hand edit is reported instead of ignored.

What you may edit

FileSafe to edit?Notes
config.toml, agents/*, plugins/*yesread at the start of each command
progress.mdyesagents read its last 8 000 characters; add guidance for the next run
memory/gotchas.md, memory/patterns.mdyesfed to every agent except the assessor (6 000 characters)
task.jsonwith careediting description changes what the next run sees; set complexity to null to have it re-assessed
spec.json, plan.jsonwith caremust stay valid against the types; edit the JSON, not the .md, which is regenerated; a plan must still pass validation
qa_report_<n>.jsonnothe escalation check reads the titles of the last three rounds
run.jsonnodelete it to forget the last run instead
index.json, events.jsonlnouse vibe task discard to remove a task

Stop any run of the task before editing its files.

What to commit

In a git repository, vibe init creates (or completes, without duplicating lines) .vibe/.gitignore:

# tasks/ is committed on purpose: specs, plans and QA reports are project history.
worktrees/
tool-output/

Task directories are therefore committed by default: reviewers see the spec, plan and QA reports next to the change they produced.

CommitIgnore
config.toml (without inline api_key values)worktrees/ (ignored by vibe init)
agents/tool-output/ (ignored by vibe init: the trace store, plus truncated outputs with in_place)
plugins/ manifests you want everyone to loadspecs/ (only appears with in_place)
tasks/ (the default)optionally tasks/*/events.jsonl and tasks/*/run.json

The event log is large and machine-oriented, and run.json changes on every run; add them to .vibe/.gitignore if the noise bothers you. To keep all task data local, add tasks/.

.vibe/worktrees/ also contains its own .gitignore with *, written when the first worktree is created, so worktrees never show up in your git status even without these entries.

Reading the store: the read layer

Interfaces never parse these files themselves; vibe-pipeline gives them a read layer on the same files (ADR-007, ADR-008). It stores nothing of its own: histories and traces are rebuilt from the events and git each time they are asked for, and followers only remember how far they have read.

APIReadsFor
EventReader (events_log)one events.jsonl, from a byte offsetfollowing a task without re-reading its whole log
FileTaskStore::all_events(after), AllEventsFollowerevery task’s events.jsonl, tagged with the task id and numberone stream for the whole project, ordered by time
history::project_history, history::task_historytask.json, events.jsonl, run.json, qa_report_*.json, gitwhat finished tasks did, cost and changed
trace::run_trace, trace::read_outputevents.jsonl, .vibe/tool-output/the tool calls of a run with their complete outputs

Reading a log while it grows. A writer appends whole lines, but a reader can see one half written: EventReader keeps an incomplete last line until its end arrives, and starts over when the file shrinks or is replaced. Complete lines that do not parse are skipped, as when a log is loaded whole.

All tasks at once. Each event is tagged {task, number} and serialised flat, the envelope’s fields next to them: {"task":"…","number":3,"schema":2,"seq":17,"at":"…","event":{…}}. Events are ordered by at, then task number, then seq: that triple is the event’s EventCursor, written <nanoseconds>-<number>-<seq> so it can serve as an SSE id. A client resumes after the cursor of the last event it saw, which does not lose events of other tasks published at the same instant. AllEventsFollower re-reads index.json at each poll, so tasks created later are followed too; a log that cannot be read is reported with the poll’s result and retried at the next poll, without losing the events of the other logs. vibe serve runs a single follower for all its open event streams, whatever their number.

Runs. A run and its resumes share one id: a run is summarised from its first run_started to its last run_finished, whose totals already cover the resumes. A run_finished logged before 0.5 has no totals; run.json supplies them for the last run, and older runs report their totals as unknown rather than as zeros. run.json also supplies the totals of a resumed run that has not finished again. A run without a run_finished since its last start is running when a process holds the task’s run lock, interrupted otherwise.

Changed files, from the most to the least exact source, one or two git commands per task after three or four for the whole project:

  1. a merged event. A true merge commit is compared with its first parent: exact. A fast-forward (one parent, or a merged commit that is one of the task’s own commits, such as the merge commit of a subtask integration at the tip of the task branch) is compared from the parent of the task’s oldest recorded commit that the merged commit contains. That is exact when the recorded commits reach back to where the task branch forked, and it has limits: commits discarded by a reset and a relaunch are skipped, so the diff starts at the relaunched work; commits git makes without an event (the checkpoint before merging) are covered only if an older recorded commit precedes them; and when no recorded commit is contained, only the merged commit itself is compared with its parent. That last case sets approximate. A merged branch that still exists diffs as empty against its base, so the merge goes first;
  2. the task branch still exists: git diff --name-status <base>...<branch>, with the base resolved as vibe pr does (branch.<b>.vibebase, then base_branch, then the default branch); an empty diff is skipped;
  3. the files of the committed events (statuses unknown);
  4. the paths written by write_file and edit_file calls (approximate: files written by commands are missing);
  5. nothing.

approximate means the list may miss files or list files the task did not change in the end; it is set for sources 3 and 4 and for a fast-forward without a contained commit. Revisions read from the log or the configuration are passed to git after --end-of-options, so a value that starts with - is never read as an option.

A git error on one task, or a task file that does not parse (run.json, a QA report), is recorded on that task; only an unreadable index fails the whole history. Git runs with the same neutralised configuration as every other framework command (no hooks, no external diff).

Tool calls pair by call id; calls logged before 0.5 pair in order per role, tool and subtask, and are marked as such. A trace file is only read under .vibe/tool-output/: a recorded path that is absolute or climbs out with .. is ignored, and read_output resolves symbolic links and refuses a file that resolves outside the trace store.

How a resume reads the state

Pipeline::resume needs only the task directory:

  1. task.json gives the task and its stored complexity;
  2. run.json gives the run id, the profile, the phase to restart at, and whether the run is resumable (anything but finished);
  3. spec.json and plan.json are loaded if present, so spec and plan are not redone;
  4. in plan.json, done subtasks are kept, in_progress ones go back to pending, and after a failed build failed and skipped ones get a fresh attempt budget;
  5. qa_report_*.json provide the earlier rounds to QA and to the escalation check;
  6. progress.md and memory/ feed the agents’ context as in any run.

The workspace is reopened by the workspace provider (the same worktree and branch), so the commits made by earlier attempts are still there. See The pipeline for the exact rules.

Architecture decision records

Significant decisions are recorded here in the lightweight ADR format: context, decision, consequences. A new ADR is added when a decision changes the shape of the framework; an existing ADR is never edited after acceptance, it is superseded by a new one. The one exception is a cross reference: a link to the later ADR that details or supersedes a point may be added to an accepted ADR, its text does not change otherwise.

ADRTitleStatus
001Rust and a cargo workspaceAccepted
002Traits as extension pointsAccepted
003Git worktrees for isolationAccepted
004Denylist shell policy with validatorsAccepted
005JSON-RPC over stdio plugin protocolAccepted
006Structured QA verdictsAccepted
007One seam for every user interfaceAccepted
008Trace store and read layerAccepted

ADR-001: Rust and a cargo workspace

Status: Accepted — 2026-09-26

Context

An autonomous coding framework runs for hours, spawns processes, talks to several network services concurrently and manipulates the user’s repository. It must be fast to start, cheap to run on a laptop, safe against memory errors, and distributable as a single binary on Linux, macOS and Windows. It must also be pleasant to extend by people who are not its authors.

Decision

The framework is written in Rust (edition 2024, MSRV 1.88) and organised as a cargo workspace of small, layered crates. Each crate has one responsibility and a documented public API; vibe-core holds only types and traits and has no I/O dependencies.

unsafe_code is forbidden workspace-wide, every public item is documented (missing_docs = warn with -D warnings in CI), and clippy runs on all targets.

Consequences

  • Single static binary, no runtime to install, fast start-up.
  • Strong typing of every artefact (spec, plan, report) catches malformed model output at the boundary instead of deep inside the pipeline.
  • Async everywhere via Tokio; agents and tools run concurrently without threads per task.
  • Higher barrier for casual contributors than a scripting language; mitigated by the out-of-process plugin protocol (ADR-005) that lets extensions be written in any language.

ADR-002: Traits as extension points

Status: Accepted — 2026-09-26

Context

The framework must be “very open”: teams will want other model vendors, other tools, other isolation strategies (containers, remote sandboxes), other memory back-ends, and their own agents. Hard-coding any of these makes the framework a product rather than a platform.

Decision

Every replaceable concern is a small trait in vibe-core:

TraitDefault implementation
ModelProviderAnthropic, OpenAI-compatible, mock
Toolread/write/edit/list/glob/grep/bash
WorkspaceProvidergit worktree, in place
MemoryStorein-memory, file-backed
TaskStorefile-backed under .vibe/tasks
Hooknone (hooks are opt-in)
Pluginnative and out-of-process adapters

Agents are not a trait but data (AgentSpec): a role, prompt, tool selection, model selection and thinking level. One runtime executes every spec.

Implementations are collected in a Registry at start-up. The default pipeline only ever talks to the registry, never to concrete types.

Consequences

  • Anything the built-in pipeline can do, a plugin can do.
  • Tests use scripted providers and fake tools without network or filesystem.
  • Trait objects (Arc<dyn …>) everywhere; a small runtime cost, irrelevant next to model latency.
  • Trait evolution must be backwards compatible: new methods get default bodies.

ADR-003: Git worktrees for isolation

Status: Accepted — 2026-09-26

Context

Agents edit files and run commands. If they work in the user’s checkout, a bad run leaves a broken tree, and two tasks cannot run at once. Containers give stronger isolation but require a daemon, images, and volume juggling that many developers do not want.

Decision

The default WorkspaceProvider creates a git worktree per task under .vibe/worktrees/<slug>-<id> on a branch vibe/<slug>-<id> from the configured base branch. Agents are confined to that directory by path containment. Integration tries a fast-forward, then a regular merge, and reports conflicts for a human (optionally after an AI-assisted resolution pass) instead of guessing.

in_place mode exists for experiments and for repositories that are not git.

Isolation is a trait, so containers or remote sandboxes can be added as plugins without touching the pipeline.

Consequences

  • Several tasks can run in parallel on the same repository.
  • The main branch is never touched until the user (or auto_merge) says so.
  • Worktrees consume disk space; vibe task discard removes them.
  • Non-git projects lose isolation unless a plugin provides it.

ADR-004: Denylist shell policy with validators

Status: Accepted — 2026-09-26

Context

Agents need a shell to build, test and inspect a project. A pure allowlist breaks on every new tool chain; a wide-open shell is dangerous. Real incidents come from a small set of commands: destructive rm, privilege escalation, disk and system management, identity tampering in git, and destructive database statements.

Decision

Every command line is parsed into segments (pipes, &&, ;, subshells, bash -c) and every segment is checked against:

  1. a built-in list of blocked programs (system, disk, privilege, network administration);
  2. per-command validators for rm, chmod, git, kill, pkill, database clients and dropdb/dropuser;
  3. the project configuration: extra blocked programs, an optional allowlist, and whether network clients (curl, wget) are permitted.

Unparsable input is denied. Every denial is a full sentence returned to the model so it can choose another approach. Commands always run inside the workspace with a timeout.

Consequences

  • Works out of the box on any stack.
  • Not a sandbox: a determined model could still find a harmful command. Containers or VM workspaces remain the answer for untrusted code; the policy is defence in depth.
  • Teams with stricter needs switch on the allowlist.

ADR-005: JSON-RPC over stdio plugin protocol

Status: Accepted — 2026-09-26

Context

Rust traits are the native extension mechanism, but requiring Rust for every extension contradicts the goal of openness. Meanwhile the Model Context Protocol (MCP) has made “JSON-RPC 2.0 over stdio, tools/list, tools/call” a de facto standard with hundreds of existing servers.

Decision

Out-of-process plugins speak the Vibe Plugin Protocol: newline-delimited JSON-RPC 2.0 over stdin/stdout. The method names and payload shapes for tools follow MCP so that an MCP stdio server works unchanged as a tool plugin. Additional methods (agents/list, hooks/before_tool) expose the other extension points.

Plugins are declared in .vibe/config.toml or discovered from vibe-plugin.toml manifests in .vibe/plugins/ and the user configuration directory. A PluginServer helper makes writing a plugin in Rust a few lines; any language can implement the wire contract directly.

Consequences

  • Immediate access to the MCP ecosystem for tools.
  • Plugins crash in their own process; a non-required plugin failure never stops a run.
  • Latency per call (process boundary, JSON); acceptable next to model latency.
  • Native in-process plugins remain available for performance-sensitive extensions.

ADR-006: Structured QA verdicts

Status: Accepted — 2026-09-26

Context

A review loop needs an unambiguous verdict to decide whether to fix again, stop, or ask a human. Parsing free text (“Status: passed”, “LGTM”, “approved with nits”) is brittle, and having two sources of truth (a markdown report and a JSON status) invites disagreement.

Decision

The QA reviewer ends its session with a JSON document that deserialises into vibe_core::QaReport (verdict, summary, issues[] with severity, requirement, file, line and suggested fix). That document is the only source of truth. The markdown report is rendered from it for humans, never parsed. Invalid JSON goes through a repair call and one retry; a still-invalid answer counts as Inconclusive and consumes a round.

The same rule applies to every structured artefact: complexity assessment, spec, plan.

Consequences

  • The pipeline can reason about severities (block on High+, tolerate Low).
  • Escalation is deterministic: the same issue title three rounds in a row stops the loop.
  • Prompts must be explicit about the schema; the runtime helps with extraction and repair.

ADR-007: One seam for every user interface

Status: Accepted — 2026-09-27

Context

Up to 0.2 the only interface is the vibe command line: vibe run builds a pipeline in its own process, prints events as they come, and the run ends with the process. A terminal UI, a local web UI and an editor extension are all wanted. Each needs the same things: start, resume and cancel runs; see live progress, including text as the model produces it; answer approval requests; reconnect to a run in progress without losing events.

Building each interface directly on Pipeline would duplicate run management three times, and each would have to guess the state of a run from files on disk.

Decision

  1. The engine exposes one seam for interfaces: a run manager in vibe-pipeline that starts, resumes, cancels and lists runs and hands out an event subscription. It never renders anything.
  2. Events are the interface contract. Every change an interface shows is an event with a per-run sequence number and a schema version, persisted in events.jsonl, so a client can replay from any sequence number and then follow live events. Interfaces keep no private view of engine state that events cannot rebuild.
  3. Human decisions are part of the pipeline, not of an interface: an approval request is persisted in run.json and resolved by a command (vibe approve, vibe reject) that any interface can issue.
  4. Clients attach in two ways: in process (the CLI and the terminal UI use the run manager directly) or through vibe serve, which exposes the same operations over HTTP and the event stream over server-sent events (web UI, editor extensions).
  5. .vibe/ stays the source of truth and is protected by a cross-process lock, so a CLI and a server can work on the same project.

Consequences

  • The terminal UI can ship before the server, and the server adds no engine logic.
  • Streaming, approvals and new events must land before any interface that relies on them.
  • The event schema becomes a public, versioned API; changing an event is a breaking change.
  • A crash loses no decision: approvals and sequence numbers are on disk.
  • Content too large for an event stays out of the log but not out of reach: the complete output of each tool call is written to the trace store, .vibe/tool-output/<task dir>/<run>/<call>.txt (ignored by git), and tool_returned references it by path. Commits and merges are events too (committed, merged), and run_finished carries the run’s totals, so a history of every run can be rebuilt from events and git alone (Events; decided in ADR-008).

ADR-008: Trace store and read layer

Status: Accepted — 2026-09-27

Context

Up to 0.4, interfaces show a run while it happens, but reconstructing what was done after the fact is hard. tool_returned carries a 200-character preview and nothing pairs it with its tool_called when read-only tools run in parallel; the complete output of a call is kept nowhere unless it exceeds the model’s limit. Commits other than subtask integrations and the final merge emit no event, and run.json, the only place with a run’s totals, is overwritten by the next run. Each interface that wanted a history, a project-wide activity feed or a trace of tool calls would have had to re-parse events.jsonl, guess, and ask git on its own.

ADR-007 makes events the interface contract and forbids private views that events cannot rebuild, so whatever these views need must be in the events, or referenced by them.

Decision

  1. Events carry what the views need, as optional fields and new types (schema stays 2): a call id on tool_called and tool_returned, the subtask, exit code, output length and output path on tool_returned, the model on agent_started, committed and merged events, and the run’s totals on run_finished (Events).
  2. Complete tool outputs go to a trace store, not into the log: .vibe/tool-output/<task dir>/<run>/<call>.txt at the project root, ignored by git, capped per call (pipeline.trace_max_chars), on by default (pipeline.trace_outputs), removed with the task. tool_returned.output_file references the file by a path relative to the project root; nothing else points to it.
  3. One read layer in vibe-pipeline turns events and git into views: events_log (incremental reading of one log, every task’s log merged by an event cursor), history (runs, commits, changed files, validations, QA, tokens, active time and cost of a task) and trace (the calls of a run, paired, with their outputs). It stores nothing: every view is rebuilt from .vibe/ and git when it is asked for. The command line, the terminal UI and the server all use it, so they agree.
  4. The server reads the logs once. vibe serve runs one follower of every task’s log, shared by all its open event streams, instead of one reader per client.

Consequences

  • vibe history, vibe trace and vibe events without a task, and the matching screens of the terminal UI, the web UI and the server routes, are thin renderings of the same documents (TaskHistory, RunTrace, TaggedEnvelope).
  • The trace store costs disk space in proportion to what the tools printed, bounded per call; it is local, git-ignored and can hold secrets that a command printed.
  • Logs written before 0.5 still load: their calls pair by order, their outputs are only the previews, and their totals are unknown rather than zero.
  • vibe-pipeline now depends on vibe-workspace for read-only git queries of the history; the pipeline itself still reaches workspaces through WorkspaceProvider.
  • Ordering across tasks relies on the clocks of the processes that log; within a task, seq stays the exact order.

Events

Everything the engine does is published as an event. Interfaces (the CLI, vibe events, vibe history, vibe trace, the terminal UI, vibe serve and its clients) are built on events only, so this page is a public, versioned contract (ADR-007, ADR-008).

Envelope

Each event travels in an envelope, one JSON object per line in .vibe/tasks/<task>/events.jsonl and in vibe --json output:

{"schema":2,"seq":17,"at":"2026-09-27T10:00:00Z","event":{"type":"phase_started","run":"…","phase":"build"}}
FieldMeaning
schemaevent format version, currently 2; logs written before 0.3 have none and read as 1
seqposition of the event in its run, from 1, without gaps and continued across resumes; absent for ephemeral events and events without a run
atpublication time, UTC
eventthe event, tagged by type

A client replays a run by reading its events in seq order and follows it by asking for the events after the last seq it saw: vibe events <task> --after <seq> --follow. Events of one run are delivered in seq order to live subscribers and to the log.

Ephemeral events are only sent live and never logged: they carry no seq and a client that reconnects does not get them back.

Adding an event type or an optional field is not a breaking change; clients must ignore types and fields they do not know. Removing or changing a field increments schema.

Event types

typeFieldsWhen
run_startedrun, taska run or a resumed run starts
phase_startedrun, phasebefore a phase
phase_finishedrun, phase, success, summaryafter a phase
agent_startedrun, role, subtask, modelan agent session starts; model is the provider’s model id
agent_deltarun, role, subtask, delta ({"kind":"text"|"thinking","text":…})ephemeral: text arriving while a step streams
agent_textrun, role, textthe complete text of a step
tool_calledrun, role, tool, input (complete arguments), call, subtaskbefore a tool runs
tool_returnedrun, role, tool, is_error, duration_ms, preview (first 200 characters), call, subtask, exit_code, timed_out, output_chars, output_fileafter a tool ran
agent_finishedrun, role, steps, usage, stopan agent session ends
subtask_updatedrun, subtask, statusa subtask changes state
subtask_integratedrun, subtask, commit, conflictsa finished attempt was merged into the task branch (conflicts empty) or conflicted
committedrun, subtask, commit, message, filesthe pipeline committed in the task workspace: checkpoint before the build, subtask or fix commit, subtask integration
mergedrun, commit, branch, basethe merge phase merged the task branch into its base (auto_merge); commit is the resulting commit of base; branch or base is "" when the workspace provider does not know it
validation_finishedrun, command, integration, passed, exit_codea required validation command finished
budget_updatedrun, tokens, token_limit, active_ms, duration_limit_msafter each phase and each subtask attempt
artefact_writtenrun, artefact ({"kind":"spec"}, {"kind":"plan"}, {"kind":"qa_report","round":n})the spec, the plan or a QA report was written
approval_requestedrun, gate (spec, plan, merge)the run pauses until a human decides (pipeline.approvals)
approval_resolvedrun, gate, approved, commentvibe approve or vibe reject answered; logged by that command
retryingrun, what, attempt, delay_msbefore a retry
pausedrun, reasonthe run stops and waits for a human
run_finishedrun, success, status, usage, active_ms, started_atthe run ends: always the last event of a run that published run_started, also after paused and after an error (then success is false, with status failed, or the unchanged task status when the workspace could not be opened); totals of the whole run, resumes included
logrun (optional), level, messagediagnostics

Identifiers (run, task, subtask) are UUIDs. role is the agent role (coder, qa_reviewer, …). Statuses and phases use the same snake case names as vibe task show. usage is {"input_tokens", "output_tokens", "cache_read_tokens", "cache_write_tokens"}.

Tool calls

call is a short random id (12 hex digits) generated for each call: it pairs a tool_called with its tool_returned, including when read-only calls of one step run in parallel and return out of order. subtask is the subtask the session works on, null outside the build.

On tool_returned, exit_code and timed_out come from tools that run a command (bash); other tools leave them null and false. output_chars is the length of the complete output, before any truncation. output_file points to the complete output (see below), or is null when tracing is off or the file could not be written.

Trace store

With pipeline.trace_outputs = true (the default), the complete output of every tool call is kept in .vibe/tool-output/<task dir>/<run>/<call>.txt, where <task dir> is the task’s directory name under .vibe/tasks/ (003-add-login). output_file gives that path relative to the project root. Each file holds at most pipeline.trace_max_chars characters (100 000 by default); a longer output is cut and ends with a marker line. The directory is ignored by git (.vibe/.gitignore, written by vibe init) and removed by vibe task discard. It is independent of what the model sees: an output longer than the runner’s limit is still truncated for the model, with the complete text in <workspace>/.vibe/tool-output/<uuid>.txt where the agent can read it.

Commits

committed.subtask is null for the checkpoint before the build, for fix commits, and for one commit shared by several subtasks when subtasks are not isolated. committed.files lists the paths changed by the commit against its first parent (for a merge commit, what it brought in), relative to the repository root; it is empty when the committer does not use git. message is the first line of the commit message. Commits the git workspace makes on its own are not announced: the checkpoint just before merging and the commit of an assisted conflict resolution; the merge itself is, by merged.

Logs recorded before 0.5

Every field added in 0.5 is optional when reading, so older logs still load under schema 2: call reads as 000000000000 (pair those calls by order and tool), subtask, exit_code and output_file as null, timed_out as false, output_chars as 0, model as "", and run_finished without totals reads usage as zeros, active_ms as 0 and started_at as 1970-01-01T00:00:00Z.

Events of every task

A task’s events.jsonl does not name its task: it is implied by the directory. Views over the whole project (vibe events without a task, GET /api/events, GET /api/stream, the Activity screens) read every task’s log and tag each envelope with its task, serialised flat, the envelope’s fields next to the tag:

{"task":"6f1c3e0a-9b2d-4c47-8a51-0e3b8f2d7c19","number":3,"schema":2,"seq":17,"at":"2026-09-27T10:00:00Z","event":{"type":"phase_started","run":"…","phase":"build"}}

task is the task id and number its number on the board. This shape is vibe_pipeline::TaggedEnvelope.

Tagged events are ordered by an event cursor: the time at, then the task number, then seq (0 when absent). It is written <nanoseconds since the epoch>-<number>-<seq>, for example 1790000000123456789-3-17, and serves as the id of the events of /api/stream. A client that resumes after the cursor of the last event it saw gets neither a repeat nor a loss of events of other tasks logged at the same instant; 0-0-0 means “from the start”.

The order across tasks is the order of the at times, and each process stamps its own events. Events of one task always come in order, but a vibe run in another process whose clock reads slightly earlier can log an event whose cursor sorts before one already delivered for another task. A live follower still delivers it, once, so cursors are not always increasing on the wire; a client that was disconnected while it was logged and resumes from a later cursor does not get it. When the state of one task must be exact, reload that task’s events (vibe events <REF>, GET /api/tasks/{ref}/events), which are ordered by seq.

Roadmap

Vibe Factory is young. The list below is ordered by priority, not by date.

0.1 — foundation (released)

  • Layered crate architecture with documented extension points
  • Anthropic and OpenAI-compatible providers, mock provider
  • Built-in tools with shell security policy and path containment
  • Git worktree isolation and merge
  • Agent runtime with parallel tools, hooks, context budgeting, continuation
  • Spec → plan → build → QA/fix pipeline with file persistence and resume
  • Plugin protocol (JSON-RPC over stdio, MCP-compatible tools) and Rust plugin helper
  • vibe CLI
  • User guide, design book, ADRs, API docs

0.2 — reliability (released)

  • Required commands before ready/merge, persistent results and replay on resume
  • Feed deterministic validation failures into the bounded QA/fix loop
  • Validate the integration candidate, including assisted conflict resolution, before publication
  • Ten reproducible Rust evaluation cases with independent oracles, a suite runner and a summary of success, time and tokens
  • Worktree per subtask, ordered integration and conflict tests
  • Container workspace provider with explicit network, mounts and resource limits
  • Persistent token and duration budgets across resumes
  • Cancellation-safe plugin writer task
  • Cross-platform release qualification and migration guide

Carried over: a published real-model baseline, done in 0.4.0 (Claude Sonnet 5, 2026-09-27: 30/30 runs over the ten cases).

0.3 — interaction foundations and terminal UI (released in 0.4.0)

The engine learns to stream, to wait for a human and to outlive the terminal that started it, then gets a terminal UI. See ADR-007.

  • Real-model evaluation baseline, published with the framework commit and model (Claude Sonnet 5: 30/30 runs over the ten cases)
  • Streaming completions and live agent text
  • Events v2: sequence numbers, schema version, validation, integration, budget and artefact events, replay and follow
  • Human approvals after spec, plan or before merge
  • Run manager and cross-process locking of .vibe/
  • Terminal UI: task board and live run view

0.4 — server, web UI and integrations (released)

  • vibe serve: HTTP API and server-sent events over the run manager
  • Local web UI: board, activity, plan, spec, QA, changes, approvals, evaluation dashboard
  • GitHub and GitLab issue import and pull request creation
  • Web fetch and search tools
  • Persistent project memory across tasks
  • VS Code extension on vibe serve

0.5 — visibility: history, global activity, full trace, macOS app (released)

See everything that was done, from every interface (ADR-008).

  • Events that tell the whole story: call ids, exit codes and output references on tool calls, the model of each session, committed and merged events, run totals on run_finished (schema stays 2)
  • Trace store: the complete output of every tool call, local and git-ignored
  • Read layer: incremental and project-wide event reading with a stable cursor, history rebuilt from events and git, traces of tool calls
  • vibe history, vibe trace, vibe events across every task, optional [pricing]
  • Server routes for the global event stream, history and trace; web views Activity, History and Trace
  • Terminal UI screens Activity and History and a Trace tab
  • Native macOS app on vibe serve (SwiftUI, macOS 14+): signed, notarized DMG with Sparkle auto-update

Later

  • OpenAPI description of the server API
  • Editing the plan from the web UI before approving it
  • Linear and Jira importers

Ideas

  • Semantic merge assistance for parallel tasks touching the same files
  • Prompt caching statistics and optimisation hints
  • A gallery of community agents and plugins

Contributions to any item are welcome — see Contributing.

Contributing

Thank you for considering a contribution. Vibe Factory is meant to be extended, and the best contributions are often plugins and agents rather than changes to the core.

Ways to contribute

  • Report bugs and propose features in GitHub issues. Include the vibe doctor output and, for pipeline problems, the events.jsonl of the task.
  • Write a plugin. See Plugins. Plugins live in their own repository; open an issue to have yours listed.
  • Improve an agent prompt. Prompts live in crates/vibe-agents/src/prompts/. Explain the failure mode you observed and how the change fixes it.
  • Improve the documentation. Every page of this book has an edit link.
  • Change the framework. Open an issue first for anything that touches a vibe-core trait; trait changes must stay backwards compatible (new methods get default bodies).

Development setup

git clone https://github.com/vincentlauriat/vibe-factory
cd vibe-factory
cargo build --workspace
cargo test --workspace
cargo install mdbook && mdbook serve docs

Quality gates

CI runs on Linux, macOS and Windows:

cargo fmt --all -- --check
cargo clippy --workspace --all-targets   # with RUSTFLAGS="-D warnings"
cargo test --workspace
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
mdbook build docs

Every public item needs a doc comment. unsafe is forbidden. Tests must not depend on network access or on the developer’s global git configuration.

Pull requests

  • Target develop; main is for releases.
  • One logical change per PR, with tests and documentation updates.
  • Use conventional commit messages (feat(tools): …, fix(pipeline): …, docs: …).
  • Significant design changes get an ADR in docs/src/design/adr/.

Code of conduct

This project follows the Contributor Covenant; see CODE_OF_CONDUCT.md.

Glossary

TermDefinition
AgentA model running in a loop with tools, described by an AgentSpec.
AgentSpecDeclarative agent definition: role, system prompt, tools, model, thinking level, step budget.
ArtefactA file produced by a phase (spec.md, plan.json, qa_report.json, …).
Base branchThe branch worktrees are created from and merged into.
Complexitytrivial, simple, standard or complex; selects the pipeline profile.
EventA structured notification published on the EventBus (phase started, tool called, …).
HookExtension that observes or vetoes phases and tool calls and can enrich prompts.
MemoryKnowledge retained across runs (gotchas, patterns, decisions).
PhaseOne stage of the pipeline: assess, spec, plan, build, qa, fix, merge.
PipelineThe state machine that drives a task through its phases.
PluginA bundle of extensions, in-process (Rust) or out-of-process (any language).
ProfileThe list of phases chosen for a complexity class.
ProviderA model back-end (ModelProvider), e.g. Anthropic, OpenAI-compatible, mock.
QA reportStructured verdict of the reviewer: approved, changes_requested or inconclusive, with issues.
RegistryThe collection of every provider, tool, agent, workspace, memory store and hook available to a run.
RunOne execution of the pipeline on a task, identified by a RunId.
SpecSpecification: summary, requirements with acceptance criteria, gathered context.
SubtaskSmallest unit of implementation work, executed by one coder session.
TaskThe unit of work handed to the framework.
Thinking levelReasoning budget requested from the model: off, low, medium, high, max.
ToolA capability an agent may invoke (read a file, run a command, …).
VPPVibe Plugin Protocol: JSON-RPC 2.0 over stdio, MCP-shaped.
WorkspaceThe directory a task’s agents are confined to; a git worktree by default.