{% raw %} # Troubleshooting The failures people actually hit, what each one means, and what to do. - [Start here: `vincent doctor`](#start-here-vincent-doctor) - [The daemon](#the-daemon) - [Agent CLIs](#agent-clis) - [Projects and worktrees](#projects-and-worktrees) - [Workflows](#workflows) - [Tasks that block](#tasks-that-block) - [The TUI](#the-tui) - [Reading the log](#reading-the-log) - [Reporting a bug](#reporting-a-bug) --- ## 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: ```sh 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`](agents.md#found-is-not-usable); whether the [GitHub integration](../reference/configuration.md#github) 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](../reference/cli.md#vincent-skills) are installed and current for your agents; whether a newer vincent has been [released](../reference/cli.md#vincent-update) and whether the running daemon is older than the binary you just ran; whether [scheduled backups](../reference/configuration.md#backup) 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. ```sh 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`](../reference/cli.md#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 worktree prune` clears it. ## The daemon ### Every subcommand exits 2 Exit `2` means nothing answered. Start one: ```sh 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](running-at-login.md#upgrading-from-a-pre-scheduled-task-version). - 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`](../reference/cli.md#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: ```yaml 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](../reference/configuration.md#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 /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](agents.md#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](mcp.md) — 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`](../reference/configuration.md#mcp) — 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](../reference/workflow-schema.md#platforms)). `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`](../reference/workflow-schema.md#type-agent): 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`](../reference/cli.md#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](../reference/configuration.md#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: ```yaml 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: ```sh 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](../reference/cli.md#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: ```sh 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: ```sh 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: ```sh 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`](../reference/cli.md#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 --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`](../reference/cli.md#creating-a-task-from-a-pull-request)), 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 --hard`s 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 ` 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 ` 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: ```yaml {{with index .Task.Fields "ticket"}}Ticket: {{.}}{{end}} ``` The other cause is `.Steps.` 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 ``** — 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: ```yaml 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 ` catches a guard that does not *parse*. `vincent workflow render ` 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: ```sh 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](tui.md#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`](../reference/configuration.md#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`](../reference/workflow-schema.md#step-fields) 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`](../reference/configuration.md#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`](../reference/configuration.md#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](agents.md#how-much-quota-is-left-and-who-will-say). 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`](../reference/workflow-schema.md#step-fields). 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`](../reference/configuration.md#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`](../reference/api.md#paused-restricted-and-capped-tasks), 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`](../reference/configuration.md#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](agents.md). ### `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`](../reference/configuration.md#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 `. 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 ```sh 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`](../reference/configuration.md): ```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 --step RUN`, or `vincent chat transcript --turn N` for a chat) — **read it first**, it contains the prompt and everything the agent did [Open an issue](https://github.com/lezli01/vincent/issues/new/choose). Security issues go through [private advisories](https://github.com/lezli01/vincent/security/advisories/new) instead — see [SECURITY.md](../../SECURITY.md). {% endraw %}