Unreleased docs — may describe features not in any release yet. See the latest release.
// docs/guides/troubleshooting.md docs online

Troubleshooting

The failures people actually hit, what each one means, and what to do.


Start here: vincent doctor

When the symptom is “nothing is happening” and you do not yet know which section below you are in, run one command:

vincent doctor          # 0 healthy · 1 problems found · 2 no daemon answered

It prints, in one pass: the config and data directories, whether config.yaml parses and whether either is readable beyond its owner (with the chmod that fixes it); the daemon’s status, pid, port, uptime and version; the daemon log’s size and last lines; the database’s size on disk (including its WAL sidecar), schema version, PRAGMA integrity_check, per-table row counts, workflow-snapshot bytes and how far back its event history reaches; every agent CLI with its path, version and logged_in; whether the GitHub integration can import and sync issues, and if not which piece is missing, and how each project’s last sync went; whether the skills vincent publishes are installed and current for your agents; whether a newer vincent has been released and whether the running daemon is older than the binary you just ran; whether scheduled backups are on, when the last one succeeded and when the next is due; free disk, worktree count and bytes, and any orphaned worktrees; and task counts by state — so “12 blocked” is visible without opening the board.

It works with no daemon, which is the point: the daemon being down is one of the answers. In that mode it exits 2, still prints everything it can read from disk, and reports the database and task rows — and, with scheduled backups on, how the last one went — as unknown — daemon not running rather than opening SQLite behind the daemon’s back.

The bottom of the report is a PROBLEMS table — the closed set that makes it exit 1: a config.yaml that does not parse, a daemon alive but unresponsive, a failed integrity_check, a database written by a newer vincent, orphaned worktrees, an unreconciled task, or — with backup.interval set — a scheduled backup whose last attempt failed. A missing or logged-out agent CLI is printed plainly and deliberately does not set the exit code, so doctor stays usable in a script on a machine that only ever installs one of the three adapters.

An unreconciled task is one whose state and step runs contradict each other — it is queued (or finished) while one of its step runs is still marked running. A fan_out step’s own row does not count: a parent between rounds is queued with that round’s row deliberately left open, which is normal operation rather than a contradiction. Crash recovery finalizes the old attempt before the task returns to the queue, so this means recovery could not complete: the task is refused by admission and will sit at queued forever. Recovery runs at startup, so restarting the daemon retries it; the daemon log says why it failed the first time.

vincent doctor --json > doctor.json   # the whole report, for a bug report
vincent doctor --fix                  # reclaim orphaned worktrees, compact the DB
vincent doctor --fix --force          # also remove orphans that have local changes

--fix needs a running daemon — every repair is a write, and only the daemon writes. It reclaims orphaned directories (an entry under a data root that no task row claims — the same set vincent gc reclaims, run by the same code) and runs a real VACUUM. Two refusals are by design and are reported rather than hidden: an orphan with local changes is skipped until you pass --force, and the VACUUM is skipped while any task is in flight, because it would stall that task mid-step.

Vincent does not run git worktree prune in your repositories. After doctor removes an orphan, the repo it came from may still carry a stale registration; git -C <repo> worktree prune clears it.

The daemon

Every subcommand exits 2

Exit 2 means nothing answered. Start one:

vincent daemon start
vincent daemon status      # 0 healthy, 1 not running, 2 unresponsive

Subcommands never auto-start a daemon by design — only the TUI does. If daemon status says unresponsive (exit 2 with a daemon present), the process is alive but not serving; vincent daemon stop --force clears it.

“A daemon is already running”

Single-instance is enforced with a lock file in the data directory that releases automatically when the process dies. If you get this after a hard kill, check whether the daemon really is running (vincent daemon status) before assuming a stale lock — an orphaned lock is rare, an orphaned daemon is not.

The TUI starts its own daemon even though the service is installed

Almost always: the service and your shell resolved different data directories, so the TUI looked for daemon.json where the service did not write one.

  • On Windows, this is the signature of a pre-Scheduled-Task install that ran as LocalSystem. See Running at login → upgrading.
  • Otherwise, check whether VINCENT_DATA_DIR is set in one context and not the other, and reinstall the service from the shell you actually use.

The daemon starts and immediately exits

Read the log — vincent daemon logs, which reads {data_dir}/logs/daemon.log from disk and so still answers here. An invalid config.yaml at startup is fatal and says which key and line. (An invalid config on hot reload is not fatal — the last good config keeps running and the rejection is logged.)

Crash recovery is fatal too. If it cannot close a previous run’s open step runs, the daemon refuses to start — startup failed: crash recovery, with the underlying error beside it, a storage failure nearly every time. Nothing is half-done: each task is reconciled whole or left exactly as it was found, so starting the daemon again simply retries it.

Agent CLIs

An agent CLI is not found

vincent agents and the TUI’s daemon view list what the daemon resolved. If your shell finds the binary and the daemon does not, it is a PATH problem — in this order:

  1. Service-installed daemons capture PATH at install time on macOS and Linux. Install a CLI after that, and the service cannot see it. Fix: vincent service install again, from a shell where which claude works.
  2. Point at it directly and skip PATH entirely:

    agents:
      claude: { path: "/opt/homebrew/bin/claude" }
    
  3. Cursor: vincent resolves cursor-agent, never cursor — cursor is the editor launcher. If only cursor is on your PATH, cursor-agent is not installed.

A task that runs in a container uses the CLI in the image, not yours, so none of the above applies to it: agents.*.path is ignored there, and agent_unavailable means the image has no claude, codex or cursor-agent on its own PATH. Check with docker run --rm <image> /bin/sh -c 'command -v claude'. On macOS, a containerized claude step that is not logged in while your own claude is has hit the Keychain gap — claude’s login does not reach the container; see Agents in a container.

Detection is cached by binary identity (path + mtime + version), so upgrading a CLI is picked up automatically. Force a re-probe with R in the new-task view or GET /v1/agents?refresh=true.

The adapter is found but every run fails

Suspect authentication. An installed-but-unauthenticated CLI probes as perfectly healthy and then fails at the first API call. vincent doctor prints the answer per adapter under AGENTS, re-probing each time:

AGENTS
codex   found  0.147.0  auth NOT LOGGED IN  /opt/homebrew/bin/codex

Run the CLI by hand once and log in (codex login, cursor-agent login, claude auth login or claude interactively), then run doctor again. All three answer this cheaply, so vincent asks them. A claude older than 2.1.41 has no auth status command, so it shows unknown rather than claiming fine. ok means the CLI found credentials configured, not that they were checked.

restricted_unsupported

The step asked for permission_mode: restricted and the adapter cannot restrict on this platform. Today that is cursor on Windows: its sandbox requires macOS or Linux.

Most of the time you will not see this reason at all, because task creation refuses it first: creating a task whose restricted step resolves to such an adapter answers 400 naming the step and the agent. Seeing the block reason means the task and its daemon parted company after creation — a data directory carried to Windows, or a workflow edited after the task was queued.

The step fails rather than running unrestricted, which is deliberate — a restricted mode that quietly isn’t restricted is worse than none. Use claude or codex for that step on Windows, or drop the step to full-auto knowingly.

mcp_unsupported

The step was to be wired to vincent’s own MCP server — the default — and the adapter cannot carry one for a single run.

All three shipped adapters can, so you will not meet this: claude takes --mcp-config, codex takes -c mcp_servers.… overrides, and cursor gets a .cursor/mcp.json written into the task worktree. None of them checks your CLI build for MCP support. The reason is reserved for a future adapter with no per-run MCP surface.

The step fails rather than starting an agent that silently has no vincent tools, because a prompt written against those tools would otherwise burn a run producing work premised on a channel that was never there. If the step does not need them, set mcp.wire_steps: false — it is hot-reloaded — and retry.

“untested version” beside an adapter

Not a fault, and nothing to fix. Vincent pins the CLI builds its parsers were captured against; anything else reads untested, which is the normal state a few weeks after any vincent release because agent CLIs ship far more often than vincent does. It changes nothing: no run is refused, and vincent doctor does not exit 1 over it.

It is worth knowing when something else is already wrong — a stream that parses oddly, a flag the adapter stopped finding — because it tells you whether you are on ground vincent has walked. incompatible version is different: that is a build vincent knows breaks. No such build has been observed for any adapter, so that list currently ships empty.

A workflow is listed but cannot be selected — platform_unsupported

The workflow declares a platforms: list this host is not in (schema). vincent workflow ls shows it with status unsupported and the platforms it needs, the TUI’s workflow view says “not on this platform”, and the new-task picker refuses it — creating the task would only produce a run that fails at the first cat or Get-ChildItem.

Three honest fixes, in order of preference:

  1. run the task on a host the workflow names;
  2. widen the list, if the workflow really is portable — [posix, windows] is the same as removing the key;
  3. write a host-specific copy under the project scope, which shadows the global one by name.

The block reason platform_unsupported on an existing task means the task was created elsewhere and its data directory has since moved between machines. The snapshot is fine; it is the host that changed.

An agent cannot be picked for a workflow — input_unsupported

The workflow has a step declaring on_input: require: it needs an agent that can stop mid-run and ask you something. Codex and cursor never can — neither CLI has a control channel — and claude can only inside the CLI version family vincent has verified the protocol against.

Where you meet it:

  • The new-task picker greys the agent out, and POST /v1/tasks answers 400 naming the step. Pick an agent whose row in vincent agents carries no no mid-run input note — or whose input_verdict in vincent agents --json (or GET /v1/agents) is supported.
  • The workflow fails to validate when a step pins agent: codex or agent: cursor outright. Change the pin, or drop require back to wait if the questions are optional after all.
  • A task blocks with input_unsupported when the answer changed after the task was created — almost always a claude upgrade past the verified family. Check vincent agents for the no mid-run input note beside its BUILD verdict; the fix is on the machine, not in the workflow, and retry refuses until it is done rather than reproducing the block.
  • A task in a container blocks with input_unsupported when the claude in its image is outside the verified family. Task creation and retry judge your host’s claude, so neither catches it; only the step checks the image’s, with claude --version inside the container, and vincent agents does not show that one. The fix is the image, and a retry blocks again until it is.

An agent that is not installed never triggers any of this: an unknown verdict is not a refusal, and you get agent_unavailable at run time instead if it is still missing.

Every cursor tool call is blocked on Windows

If cursor steps run, produce no edits, and report blocked tool calls, check what started the daemon. A daemon parented by Git Bash carries MSYSTEM, and cursor-agent imports Claude Code’s hooks and composes them as PowerShell while executing them with bash — so every hook errors, and an erroring hook blocks the call. It is a Cursor interop bug on Windows, not a vincent one.

unset MSYSTEM in the launching shell does not help: the MSYS runtime re-injects it into every child. Vincent sets the child’s environment block directly, so this does:

environment:
  unset: [MSYSTEM]

A daemon started by vincent service install (a Scheduled Task in your logon session) never had MSYSTEM in the first place.

The agent received C:/Program Files/Git/review instead of /review

You sent a skill invocation from Git Bash — vincent chat send 12 /review — and the agent got your Git install root with the skill’s name on the end. The MSYS2 runtime converts any argument that looks like a POSIX path into a Windows one before vincent starts, and says nothing when it does; vincent sends what it was given, and cannot tell it was ever /review. A /plugin:skill name is left alone, which is why only some invocations break.

Either fix works:

MSYS_NO_PATHCONV=1 vincent chat send 12 '/review the auth change'
printf '%s' '/review the auth change' | vincent chat send 12 --message-file -

--message-file takes the message out of argv entirely, so it works in every shell — and it is the fix for the other silent rewrite too, a $name inside double quotes that bash, zsh and pwsh expand. See quoting a skill invocation.

A restricted codex step cannot commit

In a linked worktree the real git directory lives under the main repository, outside --sandbox workspace-write. Move the commit into a command step, which runs outside the agent’s sandbox.

Projects and worktrees

project add refuses: no default branch

Vincent detects the default branch from origin/HEAD, then a local main, then master, then the current branch. A detached or unborn HEAD hits none of them:

vincent project add /path/to/repo --default-branch main

worktree_dirty

The task’s worktree has uncommitted changes at a point where vincent needs it clean — most often when archiving. Archiving removes the worktree, so uncommitted work would be lost; it refuses unless you force it.

Inspect the worktree first (its path is on the task detail view), commit or discard, then archive again.

branch_exists / worktree_path_occupied

The branch this task would use, or its worktree directory, already exists from an earlier run. Vincent never deletes a branch that carries a commit, so leftovers accumulate if you re-create tasks with the same title — or if a branch template has no discriminator in it, in which case the second task for the same input collides every time. (Archiving cleans up only the branches that received no commit at all, which is precisely the set that never collides with anything you would miss.)

Most collisions are caught at creation with a 400, but the check at creation is racy by nature, so this block is the authority.

Two ways out. Give the task a different name and let it re-admit, which keeps its history and transcripts:

curl -X POST .../v1/tasks/7/retry -H 'Content-Type: application/json' \
  -d '{"branch_override":"feat/second-attempt"}'

Or delete the leftover branch yourself, then retry:

vincent task ls --archived      # find the branches vincent made
git worktree list

A related failure is a ref hierarchy conflict: feat/foo cannot be created while feat/foo/bar exists, because git stores refs as a path hierarchy. The message names the branch that is in the way.

worktree_path_occupied has a second cause with a different fix: a crash between creating the worktree and recording its path leaves the directory on disk while the task claims nothing, so every later admission finds it occupied. Run vincent gc — it reclaims exactly the directories no task claims — then retry the task. vincent gc --dry-run shows you the path first, and --force is needed when git cannot judge the directory because the repository behind it is gone.

An issue’s main task has a third cause: it was about to receive the issue’s main worktree from the main task before it, and a chat was opened on that earlier task in the meantime. The block names the chat. Close the chat when you are done in it, then retry the task; vincent never hands the directory over while a chat is working in it.

base_branch_missing / project_path_missing

The base branch was deleted, or the repository moved. Re-point the project (vincent project edit <id> --path /new/path, add --default-branch if the stored one is gone, or edit it in the projects view) or set the task’s base branch to one that exists.

pull_fetch_failed / pull_branch_diverged / pull_branch_checked_out

These three belong to a task created from a pull request (--github-pull), whose branch is the pull request’s head rather than one vincent cut. All three happen at admission, before any step runs.

pull_fetch_failed means the head could not be fetched. Unlike the base-branch fetch — which degrades quietly, because the local base is always a valid answer — there is nothing to fall back to here: the fetched commit is where the branch has to be. Check the network and your GitHub credential (vincent doctor), then retry.

pull_branch_diverged means you already have a local branch of that name and it carries commits the pull request’s head does not. Vincent never reset --hards it — your unpushed commits are still there. Merge or rebase yours onto the head, or delete the branch if it is stale, then retry. A branch merely behind the head is fast-forwarded and never blocks.

pull_branch_checked_out means that branch is already checked out in another worktree — another vincent task’s, or your own main checkout. Git cannot put one branch in two worktrees. Switch that checkout to another branch, then retry. Within vincent a second task for the same head branch is refused earlier, with a 400 at creation.

branch_override is refused on such a task, so the branch_exists escape hatch above does not apply: renaming its branch would detach it from the pull request it was created for, and every later commit would go somewhere that pull request never sees.

adopt_branch_missing / adopt_branch_diverged / adopt_branch_checked_out

These three belong to a task or chat created on a branch that already exists (--existing-branch, existing_branch), which runs on that branch instead of cutting one.

adopt_branch_missing means the branch is gone. Creation checks that it is there, but a task can sit queued while you delete it; recreate the branch, or retry with branch_override pointing at one that exists.

adopt_branch_diverged means the branch and its own upstream have each moved. Nothing was moved — your commits and the remote’s are both still there. Merge or rebase, then retry. A branch merely behind its upstream is fast-forwarded, and one ahead is left exactly where it is, because those commits are yours to push.

adopt_branch_checked_out means the branch is checked out in one of vincent’s own worktrees, so another task or chat is working on it; wait for that one and retry. Your own main checkout is deliberately not this case: a task adopting a branch you have checked out runs in that checkout, alongside you. The detail view’s worktree row — worktree_path on the task over the API — is the project path itself when that happens.

Two things follow from running there. Vincent removes nothing when such a task is archived — the directory is yours — so worktree_dirty never applies to it. And the diff tab shows the working directory’s diff, so uncommitted work you already had when the task started reads as part of the task’s diff.

Workflows

A workflow file does not show up

  • vincent workflow ls shows built-in and global scopes only. Add --project <id> to include that repository’s .vincent/workflows/.
  • The daemon reloads on save; a file that fails to parse is reported as invalid and the previously loaded version keeps running. The workflows view shows validation status per entry, and vincent workflow validate <file> tells you exactly what is wrong.
  • Check the scope: a project file shadows a global file of the same name.

template_error

A template referenced something that does not exist. Rendering uses missingkey=error on purpose, so this fails the step before any process starts rather than writing a silent hole into a prompt.

The usual cause is an optional task field read directly. Read it defensively:

{{with index .Task.Fields "ticket"}}Ticket: {{.}}{{end}}

The other cause is .Steps.<id> for a step that has not completed — .Steps holds completed steps only.

condition_error

A step’s if: guard did not produce a verdict. Guards must render to exactly true or false; anything else is this error, including the two spellings a broken guard usually produces:

  • an empty string or <no value> — the guard is reading something that is not there. {{ .Steps.probe.ExitCode }} for a step that has not run yet does this;
  • a value that is merely truthy — yes, 1, 0, a number. Loose truthiness is deliberately not accepted, because it would treat the two cases above as decisions.

Write comparisons, not values:

if: '{{ ne (index .Steps "probe").ExitCode 0 }}'    # ✅ renders true or false
if: '{{ index .Steps "probe" }}'                    # ❌ renders a struct

This is the one block reason vincent does not retry. A guard is evaluated before the step becomes an attempt, so there is no attempt to retry, and re-rendering the same template against the same context cannot answer differently. Fix the workflow file and retry the task — the guard is re-evaluated from scratch, as it is on every pass.

vincent workflow validate <file> catches a guard that does not parse. vincent workflow render <file> goes one step further and executes it against a preview context, so a guard that references a field nothing supplies shows up there — it prints what each guard renders to, and warns when that is neither true nor false. Neither can tell you the guard renders the wrong answer for a particular task: that needs the task’s own context, which only exists at run time.

loop_limit

A loop step has more iterations ahead of it than max_iterations allows, so the task blocked instead of doing some of them. Two ways to get here:

  • a for_each: list longer than the ceiling. It blocks before the first iteration and names the count, so nothing has run yet;
  • a count: the ceiling moved under — loop.max_iterations was lowered while the task was queued or paused. The ceiling is read per loop, so a config reload reaches a task already in flight.

It blocks rather than truncating the list or advancing past the loop, because a loop that ran out of iterations did not do what it was looping for, and advancing would hand every later guard a .Steps saying the work is finished. condition (the step type) is what a workflow uses to stop and succeed; running out of tries is not that.

Three fixes, in the order worth trying:

  • filter the list at its source. git diff --name-only … | grep -v _test.go is usually what you wanted anyway, and it keeps the filtering where the output already is;
  • raise the ceiling for this loop with max_iterations: on the step;
  • raise it for the machine with loop.max_iterations in config.yaml. The default of 10 is low on purpose: ten iterations of a three-step body is thirty agent runs.

A loop ran fewer iterations than I expected after a retry

Working as intended. A loop’s position is derived from its step rows on every admission, so a retry resumes at the body step that failed, in the iteration it failed in — body steps that already succeeded in that iteration are not run again, and earlier iterations are not repeated. The detail view groups the rows by iteration, folded, with the latest one open, which is where to check what actually ran.

If you wanted the whole loop again, the loop is not the unit to retry — retry the task from an earlier step, or skip the loop and start over.

A workflow finished having run only half its steps

Almost certainly working as intended. Two things end a run early:

  • a condition step whose guard was false. The task is done, and the detail view shows a stopped row where it ended;
  • a chain of if: guards that all evaluated false, each leaving a skipped row with reason condition;
  • a break inside a loop, which ends the loop successfully and moves on — a stopped row on the break step, and the loop’s own iterations stopping short of its count:.

Both are visible in the task’s step list, which is the place to look: the board shows a finished task as finished, and does not try to say how much of the workflow it walked.

If it was not intended, the guard is the thing to read. .Steps holds only steps the workflow has moved past, so a guard naming a later step, or naming a step that was itself skipped, evaluates against something you did not mean.

shell_unavailable

The shell a command step asked for is not installed. On Windows vincent uses pwsh -NoProfile -Command and falls back to powershell; a step pinned to shell: sh on a machine with no POSIX shell fails here. Pin a shell that exists, or write the command so the platform default can run it.

container_image_unavailable

The task’s container.image is not on this machine and could not be pulled — a typo in the tag, a private registry the daemon is not logged in to, or no network. The task blocks before a worktree or a branch is created and without spending a retry, so nothing has to be cleaned up: fix the image or the login, then retry the task.

The check is deliberately not made when the task is created. Pulling a multi-gigabyte image inside the create request would run it against the API’s timeouts, and refusing anything not already on disk would reject every first run on a fresh machine.

Pull it by hand to see the runtime’s own error:

docker pull ghcr.io/acme/dev:latest

container_unavailable

The container runtime answered when the task was created and does not now — the desktop app quit, the socket was revoked, the binary was uninstalled — or it refused to create the container. Start the runtime and retry.

A step the container was going to run is never moved to the host because the runtime failed — the task blocks instead. A step that is not contained after asking to be contained has inverted the choice the workflow made, which is the same rule that makes an agent refuse an unsupported restricted mode rather than quietly running full-auto.

Validation warns about a model

A value in no catalog at all passes with a warning rather than an error: the CLI is the final authority on what your account can run. A value found in a different adapter’s catalog is an error — that is a claude effort reaching a codex step, and it is a real mistake.

Tasks that block

A step that exhausts its retries blocks the task and waits for a human. Nothing is ever silently abandoned. From blocked you can retry, edit-and-retry, repair with a one-off agent, skip the step, or cancel the task.

Repair is the one that can change files: it runs a throwaway agent, prompted by you and handed the blocked step’s failure context, in the task’s existing worktree and branch. It always returns the task to blocked at the same step with the same reason, so you look at what it did before deciding to retry. See repairing a blocked task.

The block reason names what happened:

Reason Meaning
check_failed The step ran but its check command exited non-zero
nonzero_exit A command step exited non-zero
agent_error The agent’s own event stream reported an error
agent_unavailable The adapter’s CLI could not be resolved or started
agent_unauthenticated The agent CLI is installed but not logged in (see below)
usage_limit The agent’s usage quota for the window is spent — not a failure. By default the task waits and re-runs itself; with usage_limit_auto_continue turned down it blocks here instead (see below)
retry_backoff Not a block reason: it is what a task shows while a step’s retry_backoff paces the next attempt (see below). The failure itself keeps its own reason
timeout The attempt exceeded its timeout and was killed
input_timeout A mid-run question went unanswered past input_timeout
template_error A template failed to render (see above)
condition_error A step’s if: guard rendered nothing usable (see below)
loop_limit A loop has more iterations to run than it is allowed (see below)
restricted_unsupported The adapter cannot restrict on this platform; refused at task creation, so this is a workflow or machine that changed afterwards
mcp_unsupported The adapter cannot be given vincent’s own MCP server for this step (see below)
platform_unsupported The workflow’s platforms: list does not admit this host
input_unsupported A step needs an agent that can answer questions mid-run, and this one cannot (see below)
transcript_limit The attempt’s transcript hit transcript_max_bytes
cost_limit The task has spent past max_task_cost_usd (see below)
tree_cost_limit The task’s fan-out tree has spent past max_tree_cost_usd (see below)
transcript_io_error The attempt’s transcript could not be written or closed (see below)
agent_protocol_error Vincent could not read the agent’s stream to the end (see below)
rejected You rejected a manual gate
shell_unavailable The requested shell is not installed
interrupted The daemon stopped mid-step — not a failure; the step re-runs and does not consume a retry
invalid_snapshot The task’s stored workflow snapshot is unusable

check_failed is the common and healthy one: it means a check caught something the agent claimed was done. Read the step’s transcript, then E to edit the prompt and retry with better instructions.

usage_limit — do nothing, unless you asked to be told

The agent CLI stopped because your account’s usage quota for the current window is spent. By default vincent treats this as a wait, not a failure:

  • the attempt is recorded interrupted and consumes no retry;
  • the task goes back to queued and gives up its concurrency slot, so other work carries on;
  • the row shows the time it will resume — queued → 14:20 on the board, and queued · usage limit → 14:20 in the detail header;
  • when that time comes the step re-runs by itself. There is nothing to press;
  • every other task on that agent waits too. When one reaches an agent step while the window is still shut, it goes back to queued with the same reason and resume time without starting the agent, so its timeline shows no attempt.

The resume time is the reset the CLI reported. When it reported none, vincent waits usage_limit_recheck_interval (default 15 minutes) and tries again, repeating until the window reopens. If you know your plan’s window, set that knob to match it.

If you would rather not wait, cancel the task. Pausing and resuming drops the wait too, but while vincent’s recorded window for that agent is still shut the task goes straight back to waiting on it, without starting the agent.

If it is blocked here instead of waiting, someone turned usage_limit_auto_continue down from its default. never blocks every quota stop; reported_only blocks the ones where the CLI named no reset time and vincent would have been guessing at the window. Nothing is lost: the attempt is still interrupted, no retry was consumed, and the task is still on the step it stopped at — press r once the window reopens and the step re-runs with a full budget. The board’s agent badge still names the window, so you can see what you are waiting for.

The reason to turn it down: vincent recognizes a spent quota from the CLI’s wording, and a stop it recognized wrongly would otherwise re-queue itself and spend money on the next attempt with nobody watching. If you have seen a task that keeps re-running against a quota that is plainly not spent, that is the switch.

The daemon remembers which adapter it was. A human action drops the task’s own wait, but the observation is per adapter and outlives it: the board header badges that agent (claude ⏳14:20), the daemon view names the reset beside its path and version, and the new-task form warns before you queue more work against the same window. → means the CLI stated that time; ≈ means vincent estimated it from the recheck interval. It clears itself the next time a step on that adapter succeeds — so if the badge is still up, nothing has proved otherwise yet. That is the observation path, and it is what an adapter with no reported reading is rendered from. Where the adapter reports one — codex on request, claude through its status line — the reading wins and the daemon view spells it out window by window instead. See Agents › How much quota is left.

Only the claude adapter recognizes usage-limit wording today. On codex and cursor a quota stop still surfaces as agent_error or nonzero_exit, because their wordings have not been captured from a real run and vincent will not guess at one: a wrong guess would park a genuinely failed task forever.

retry_backoff — also do nothing, but for a different reason

A task showing queued · retry backoff → 14:20 in the detail header (and queued → 14:20 on the board) is not walled by a quota. Its step failed, it has retries left, and the workflow asked for a pause between attempts with retry_backoff. Like a quota hold it gives up its concurrency slot and comes back on its own.

The difference is the cost, and it is the thing to read the row for:

  usage_limit retry_backoff
The attempt interrupted failed, with the reason it actually failed with
Retry budget untouched one spent
What ends the wait the quota window reopening the configured duration elapsing
If it keeps happening it waits again, indefinitely — unless usage_limit_auto_continue says to block the budget runs out and the task blocks with the step’s own reason

So a task that keeps reappearing on retry_backoff is a task on its way to being blocked — the transcripts of the attempts already made are what say why. A quota hold is not: it will sit there until the window returns.

If you would rather not wait out a paced retry, any human action drops it — cancel, or pause and resume.

agent_unauthenticated

The agent CLI is installed and runs, but is not logged in. This one does block — waiting cannot fix it. Log in with the CLI’s own command (claude auth login or claude interactively, codex login, cursor-agent login), then r to retry the task.

vincent doctor catches most of these before a task is ever created: it reports logged_in for claude, codex and cursor, re-probed on every run. What it cannot catch is credentials that are configured but no longer valid — an expired API key still reads as logged in — and a claude older than 2.1.41, which has no auth status command; there the first sign is still a failed step.

transcript_limit

One attempt produced more output than transcript_max_bytes (default 512MB). Vincent kills the process rather than filling your disk, and writes the tripping annotation into the file so it records why it ends. Usually this means an agent or command is in a loop; fix that rather than raising the cap.

cost_limit — raise the cap and retry

The task’s cost so far — every attempt of every step, retries included — has passed max_task_cost_usd. Vincent stopped it rather than letting it keep spending.

It is a policy stop, not a broken step. The step run that just finished keeps its own state and reason — if it succeeded, it still reads succeeded — it consumed no retry, and a retry that was already due did not run.

What to do:

  1. Look at what the money went on. The detail view shows cost per attempt, and a task that spent more than you expected has usually been looping — an agent redoing the same work every turn, or a step retrying into the same wall.
  2. If the work is worth it, raise max_task_cost_usd in config.yaml and press r to retry. The file is hot-reloaded, so no restart is needed. If the task was created with its own, lower max_task_cost_usd, that is the wall it hit: it is fixed at creation, so raising config’s does not move it.
  3. If it is not, cancel the task, or skip the step and let the rest run.

Retrying without raising the cap makes exactly one more attempt, then blocks here again. That is deliberate — the check runs after an attempt finishes, so a retry always buys one attempt of progress, and pressing retry is your decision to spend it. Nothing is lost either way, but nothing is free either.

Three things that surprise people:

  • You overshoot by up to one attempt. An agent reports what it spent when it finishes, so the attempt that crosses the line has already run. agent_timeout is what bounds a single run.
  • The cap counts one task. Every lane of a fan_out step is its own task with its own budget, so a tree can spend a multiple of it. To cap the tree as a whole, set max_tree_cost_usd; it blocks with tree_cost_limit, below.
  • The total is the task’s whole life and never resets. A repair run and a follow-up run are step runs of that task, so their cost counts too — a finished task already over the cap blocks here again on the first attempt of its next follow-up. skip abandons that follow-up and returns the task to done.

If a runaway task on codex or cursor sailed past the cap, that is expected: neither CLI reports cost, so the cap cannot see them. See Agents.

tree_cost_limit — raise the tree cap and retry the parent

The spend of a whole fan-out tree — the root task and every lane below it at any depth, every attempt of every step, archived lanes included — has passed max_tree_cost_usd. The task showing this reason is the one whose attempt crossed the line. That is usually a lane. It can also be the parent, when its own step before the fan-out or after the join, or an agent resolving a merge conflict, did the crossing.

Like cost_limit, it is a policy stop, not a broken step: the step run keeps its own state and reason, and it consumed no retry.

A parent waiting on its lanes never blocks this way, because it runs nothing while it waits. It stays awaiting_children, and its join stays open because a lane is blocked. The parent’s row reads awaiting_children (×N …), and GET /v1/tasks/{id} on it carries children.blocked with the lane ids and children.cost_usd with what the lanes have spent. The TUI’s Task Details adds the parent’s own spend to that and shows it as tree cost.

What to do:

  1. Look at what the money went on. Open the parent and walk its lanes (l), or read children.cost_usd and each lane’s attempts. A tree usually overspends because one lane looped, or because it has more lanes than the budget was sized for.
  2. If the work is worth it, raise max_tree_cost_usd in config.yaml (it is hot-reloaded), then press r on the parent, or run vincent task retry <parent-id>. One retry re-admits every blocked lane beneath it, at any depth. If the parent itself is the blocked task, the same retry resumes it and re-admits anything blocked below it. You can also retry a single lane.
  3. If it is not, cancel the parent. The cancel reaches every unfinished lane, and their branches and worktrees are kept.

Retrying without raising the cap costs one attempt per blocked lane, every time. The check runs after each attempt, so every re-admitted lane makes one attempt and blocks here again. A retry on the parent re-admits all of them, so on a tree with eight blocked lanes each press buys eight attempts.

Three more things that surprise people:

  • You overshoot by more than one attempt. Each task checks the total at its own boundary, so when the tree crosses the line every lane still running finishes its attempt, and a lane still queued makes one attempt when it starts. Each of them blocks after that attempt. The overshoot is up to one attempt per task still working in the tree.
  • cost_limit wins a tie. When one attempt takes a task past both its own max_task_cost_usd and the tree cap, the reason is cost_limit. Raising the tree cap would not have cleared that one.
  • The total never resets. A follow-up run on a finished parent, and any lanes it spawns, add to the same tree. A tree already over the cap blocks again on the first attempt of its next follow-up.

The cap sees only what agents reported. Codex and cursor report no cost, so their lanes add nothing to the total and a tree that mixes them with claude lanes has spent more than the figure says.

transcript_io_error — check the disk

Vincent could not write, encode or close the attempt’s transcript, so the record of the run is incomplete. The step fails rather than reporting a success it cannot evidence: the usual cause is a full disk or a data directory whose permissions changed under the daemon. df the data directory (vincent doctor prints it), fix the cause, and retry — a new attempt writes a new file, so this clears on its own once there is room.

It is not what a very long line of output produces. Those are captured in partial pieces and the step succeeds normally; only transcript_limit fails a step for size.

agent_protocol_error — vincent’s reader, not the CLI’s fault

Vincent could not read the agent’s event stream to the end, so its transcript is missing lines the CLI wrote. The reason is named separately from agent_error on purpose: the agent may have completed its work perfectly, and there is usually nothing to fix in the CLI. Retry the task; if it repeats on the same step, the transcript up to the cut is the thing to attach to a bug report.

A task is stuck in awaiting_input

It is waiting on you. It is pinned to the top of the board with a badge; press enter on the row to open the answer form. If nobody will ever be there, set on_input: deny in the workflow so vincent answers immediately and the run stays unattended.

Note that awaiting_input keeps its concurrency slot — the agent process is alive mid-step. A forgotten question does occupy a slot until input_timeout (default 24h) expires.

A task is stuck in queued

It is waiting for a scheduler slot. Check the caps: max_parallel_tasks globally (default 3) and the per-project cap. Raise the priority (vincent task add --priority, PATCH /v1/tasks/{id}, or + in the new-task form) to move it up the queue.

The TUI

I cannot select text with the mouse

The TUI owns mouse events while the mouse is on. Press M to toggle it off, or hold shift while dragging — most terminals treat that as “bypass the application”.

esc does not quit

By design: esc closes one layer (popup → screen → filter). q quits.

The screen looks wrong after a resize

Re-render with any key. If a pane is genuinely unusable, the terminal is likely too small for the TUI layout — the board alone needs a reasonable width to show state and step columns.

Reading the log

vincent daemon logs          # the last 500 lines
vincent daemon logs -n 50    # fewer
vincent daemon logs -f       # keep printing as it is written
{data_dir}/logs/daemon.log

Rotated and size-capped. vincent daemon logs and the TUI’s daemon view both read it from disk rather than through the API, so both still work when the daemon is the thing that died — which is when you want the log.

Turn up detail in config.yaml:

log_level: debug
debug: true      # record each step's resolved settings and full argv in its transcript

debug: true is the one to reach for when a step “ran with the wrong settings”: it writes the resolved agent, model, effort, permission mode, working directory and the exact argv into the step’s transcript. It is off by default because argv can carry a prompt and transcripts get pasted into issues.

Reporting a bug

Include:

  • vincent doctor --json — one paste covering paths, daemon, log tail, database, agents, storage and task counts. Read it first: it carries your directory paths and the tail of the daemon log
  • vincent version output, for the build details doctor does not carry
  • your OS and terminal
  • the relevant slice of the daemon log (vincent daemon logs -n 200, or {data_dir}/logs/daemon.log)
  • the workflow YAML, if a step misbehaved
  • the step’s transcript if you can share it (vincent task transcript <id> --step RUN, or vincent chat transcript <id> --turn N for a chat) — read it first, it contains the prompt and everything the agent did

Open an issue. Security issues go through private advisories instead — see SECURITY.md.