Failure Handling

Exit code conventions, preflight checks, auto-retry on generation errors, and how failures interact with caching.

Overview

Not all failures are equal. When a generated script exits with non-zero code, the CLI needs to know: was the script wrong, or did the script correctly detect a real problem? The answer determines whether to retry, cache, or report.

Exit code convention

Generated scripts use exit codes as a protocol between the script and the CLI:

Exit code Meaning CLI behaviour
0 Success Cache the script, report success
1 Task failed legitimately Report failure, do NOT retry — script is correct but task found a real problem (tests fail, lint errors)
2 Precondition not met Report missing requirement, do NOT retry — environment isn’t ready
≥ 3 Script error / generation bug Auto-retry with error context (up to max_retries attempts)

The system prompt instructs the LLM to follow this convention when generating scripts. Exit 1 means “I ran the task correctly and it found a problem.” Exit 2 means “I checked and a required tool or version is missing.” Any other non-zero exit (3+) typically means the script itself is broken — a wrong command, bad flags, or a misunderstanding of the project.

Preflight checks

The system prompt instructs the LLM to emit a preflight section at the top of every generated script. This section verifies that required tools and versions are available before doing anything, and exits with code 2 if something is missing:

#!/bin/bash
set -euo pipefail

# --- preflight ---
command -v go >/dev/null 2>&1 || { echo "error: go is required but not installed"; exit 2; }

# --- task ---
go test -race -v ./...

For tasks that require a specific version, the LLM may add version checks (e.g. from go.mod, package.json):

# --- preflight ---
command -v go >/dev/null 2>&1 || { echo "error: go is required but not installed"; exit 2; }
go_version=$(go version | grep -oP 'go\K[0-9]+\.[0-9]+')
[[ "$(printf '%s\n' "1.25" "$go_version" | sort -V | head -1)" == "1.25" ]] || { echo "error: go >= 1.25 required (found $go_version)"; exit 2; }

The LLM infers what to check from project context — go.mod tells it the Go version, package.json tells it the Node version, and so on. Preflight checks verify but never install — the script should not run apt-get install or brew install for system-level dependencies. If a tool is missing, the script reports what’s needed and exits.

This gives the user a clear, actionable error message instead of a cryptic failure halfway through execution.

Auto-retry on generation errors

When a script exits with 3+, the CLI assumes the script is wrong and retries:

vibe run build
  → generate script (attempt 1)
  → execute → exit code 127 (command not found)
  → retry: send error output back to LLM
    "The script failed with: line 5: pnpm: command not found
     The project uses npm, not pnpm. Regenerate the script."
  → generate script (attempt 2)
  → execute → exit code 0
  → cache the fixed script

The retry prompt includes:

  • The original task recipe
  • The script that failed
  • The full stderr/stdout from the failed execution
  • An instruction to fix the problem

Retry behaviour is configurable:

max_retries = 2     # default: 1
vibe run build --no-retry   # disable retry, fail immediately

If all retries are exhausted, the CLI fails with the last error and does NOT cache the broken script.

Caching and failure interaction

Outcome Cache the script? Why
Exit 0 — success Yes Script works
Exit 1 — legitimate failure Yes Script is correct; task found a problem (user fixes code and reruns)
Exit 2 — precondition not met Yes Script is correct; environment needs setup (user installs tool and reruns)
Exit 3+ — retry succeeds Yes Cache the fixed version
Exit 3+ — retries exhausted No Script is broken; don’t persist

Key insight: exit 1 and 2 scripts are correct scripts. Caching them means the next vibe run skips the LLM — the user fixes their code or installs the tool, and reruns instantly.

Dependency failure

When a target’s dependency fails, the default behaviour is to stop the chain. Remaining dependencies are skipped and the dependent target does not run.

Example:

vibe run deploy        # depends on: test, build
  → run test → exit 1 (tests fail)
  → skip build (dependency test failed)
  → skip deploy
  ✗ deploy failed: dependency "test" failed

This is the safe default. A --continue-on-error flag may be added in the future for CI scenarios where you want to run all independent targets and collect all failures.