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

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.