# vincent 0.9.0: event triggers, pull request actions, and a firmer base for 1.0

> Source: <https://blog.lezli01.is-a.dev/blog/vincent-0-9-release/>
> Published: 2026-09-18 00:00:00+00:00

Fourteen days and 428 commits after 0.8.0, [vincent 0.9.0](https://github.com/lezli01/vincent/releases/tag/v0.9.0) is out. I’m excited to share this one. It is the biggest step so far toward a first stable release, and a good part of that step is fixes and reinforcement under features that already existed, which is exactly the kind of work a feature list undersells.

For anyone new here: [vincent](https://github.com/lezli01/vincent) is a local, vendor-independent control plane for coding agents. It runs Claude Code, Codex and Cursor as they ship, one task per branch and worktree, through workflows you can read, and it keeps the state, the gates and the verification on your machine. [0.7.0](https://blog.lezli01.is-a.dev/blog/vincent-0-7-release/) was the release where it started carrying its own development, and [0.8.0](https://blog.lezli01.is-a.dev/blog/vincent-dag-workflows/) taught it to split one issue into parallel lanes.

The [0.9.0 changelog](https://lezli01.is-a.dev/vincent/changelog.html#090--event-triggers-pull-request-actions-and-agents-inside-the-container) lists 23 additions, 6 changes and 11 fixes across 57 pull requests. The documentation was refreshed for the release too, including all 34 TUI screenshots, 25 of them new, so the images in this post come straight from it:

## Work that starts itself

Until 0.9.0 a task started because something called vincent: a keypress in the TUI, `vincent task add`, or a script hitting the API. Now vincent can watch for the event itself. An **event trigger** is one YAML file under `{config_dir}/triggers/` that watches a source (a command the daemon polls, a project’s GitHub issues or pull requests, or a signed HTTP push) and turns each new event into a task, or into a follow-up, retry or cancel of an existing one.

Here is the trigger the [triggers guide](https://lezli01.is-a.dev/vincent/docs/guides/triggers.html) uses to show the shape of one. Labeling an issue `agent-please` creates a task for it:

```
# {config_dir}/triggers/label-to-task.yaml
id: label-to-task
enabled: true
source:
  type: github_issues
  project: 1
match:
  action: labeled
  labels: agent-please
action:
  type: create_task
  title: '{{ .Event.Issue.Title }}'
  github_issue: '{{ .Event.Issue.Number }}'
dedupe_key: 'gh:issue:{{ .Event.Issue.Number }}:label:agent-please'
limits:
  max_per_hour: 5
```

A trigger can also be the clock. `cron:` or `every:`, with an optional time zone, covers a nightly dependency sweep or a weekday-morning report, and a schedule that came due while the machine slept fires once when the daemon is back, however many occurrences it missed. `overrun:` decides what happens when an event arrives while the previous run is still going: skip it, cancel the running task and fire, keep only the newest event, or queue them in turn, per ticket, pull request or branch.

The defaults are deliberately conservative, because a trigger has no keypress to stand in for your consent:

- **Off twice:** a file does nothing until both its own`enabled: true` and`triggers.enabled` in`config.yaml` are on.
- **Arming seeds:** the first poll after a trigger is armed records what the source already shows and fires nothing, so a backlog that existed before you switched it on never starts work.
- **Propose by default:** with`on_fire: propose` the task lands`paused` and nothing runs until you resume it, and`permission: restricted` runs every agent step of a created task in[restricted mode](https://lezli01.is-a.dev/vincent/docs/security-model.html#restricted-mode) .
- **Agents write, you arm:** the`create-trigger` and`update-triggers` built-ins and the`vincent-triggers` skill let an agent draft triggers, but`vincent trigger apply` refuses the whole proposal if any file would arm one. There is no override flag.

A new triggers view in the TUI creates, edits and enables them, shows each one’s poll health and delivery ledger, and dry-runs a trigger against a sample event or a live poll:

Work you start by hand gets the same controls. A new task, a retry or a follow-up can be created paused, can run every agent step restricted, and can carry a spend cap tighter than the global one, from the API, `vincent task add` or the TUI’s forms.

## From the task to the merge

0.8.0 let vincent open a task’s pull request and stopped there, because opening was the only thing it wrote to GitHub. 0.9.0 lets you finish the job from the same place: merge, close, reopen, comment on and re-run the checks of a task’s pull request from the Pull Request tab, `vincent github pr` or the API. These actions run only when a human asks, and none of them is exposed as an MCP tool, so vincent never hands an agent a merge button. A merge targets the exact head whose checks you are looking at, and it is refused up front when the branch is behind, a check is still running, or the head moved.

Agent steps also moved into the task’s container. With `container.image` set, a containerized task no longer starts its agents on your machine. The image’s own agent CLI runs next to the command steps and checks, with the same transcripts, costs and controls as a host run. In 0.8.0 only the command steps and checks ran in the container while the agent process stayed on the host, so a containerized task with agent steps was a mixed run. Credentials are mounted under a vincent home inside the container, and on macOS a containerized claude step needs its token passed through `environment`.

And a stopped task can now be talked to. When a task is blocked, waiting at a gate, done or aborted, you can open a chat on it: a conversation with an agent working on the task’s branch, starting from the task’s context and the failure or gate it stopped at. Nothing can retry or skip the task underneath while that chat is open.

## The reinforcement underneath

The rest of the release is what I mean by foundational work. Read the changelog looking for refusals and a pattern shows up.

Much of a stable release is a promise about what the tool will refuse to do.

`vincent trigger apply` refuses a proposal that would arm a trigger. A pull request merge is refused on any of the three conditions above. `parallel` and `manual` steps now refuse `max_retries` and `retry_backoff` instead of silently ignoring them, and `update-workflows` moves the value to where it applies. `tui.keys` lets you rebind the TUI, and refuses a key that would collide with anything before writing it. The new permanent delete on the archived boards never deletes a branch carrying commits past its base, and it is not something an agent can run. A new task fast-forwards your local base branch only when that is safe, and shows the base commit it actually started from.

The fan-out from 0.8.0 got its first round of real repairs. A fan-out whose lanes blocked was a dead end: one retry per lane, or a cancel that threw the run away. Now **one retry on the parent** re-admits every blocked lane beneath it. The board header could read `0/6 running` while lanes held every slot, and a parent whose lanes were working looked stalled. The header now counts every slot in use, the running fan-out step shows on the timeline, and `max_tree_cost_usd` puts one spending ceiling on a task and every lane below it, retries and follow-ups included.

Loops got the same kind of fix: retrying a task blocked inside a loop now evaluates its guards again, instead of keeping a stale answer and running a pass the loop should have ended.

One fix reads like a joke and was not one. A claude run that succeeded could be mistaken for a usage-limit stop when its output merely wrote about usage limits, and the task was re-queued, spending money, again and again. That is gone. Usage limits in general got stricter: once vincent watches an agent’s usage window close, other tasks on that agent wait for the reset instead of each starting a process into the same wall. `usage_limit_auto_continue` decides whether that wait happens unattended: always, only when the CLI named the reset time, or never, so nothing spends while nobody is watching.

Backups moved from a command you run to a schedule the daemon keeps. `backup.interval` makes the daemon take its backup archive on a schedule and keep the newest few, and `vincent task import` brings a single deleted task back out of an archive, from this installation or another one. The daemon view shows the config in effect against the built-in defaults, next to the database and the agent adapters:

Seeing what actually ran got better too. A new **Step Details** tab shows what each attempt was given: the rendered prompt or script, the agent, model and effort with the level each value came from, and the outcome, all recorded when the step ran. `vincent task show --step` prints the same thing.

Smaller items fill in the edges. `vincent agents` shows each agent CLI’s version, login state and quota, and `vincent doctor` warns when claude is not logged in before a task burns its retries. Vincent is built with Go 1.26.8, and on Windows the install docs lead with Scoop while the WinGet package waits for Microsoft’s review. After upgrading, `vincent doctor` also tells you whether your installed agent skills are behind this build, and `vincent skills install` brings them up to date, including the new `vincent-triggers` skill.

## Built with itself, again

Of the 88 pull requests merged between the two tags, 76 came from `vincent/` task branches, which is to say vincent opened them while working on itself. The release was prepared the same way: task 328 ran the `prepare-release` workflow through all 21 of its steps, and its branch became [#488](https://github.com/lezli01/vincent/pull/488), the refresh of the release-facing docs and Go module dependencies that went in ahead of the tag.

0.9.0 also carries the first contribution from someone other than me. [Zhengzhuo Wang](https://github.com/wangzhengzhuo05) fixed the MCP tool descriptions so they name the request bodies the daemon actually accepts ([#416](https://github.com/lezli01/vincent/pull/416)). Thank you.

Vincent is still 0.x, and this release shows why that label matters. A few TUI keys moved so that every shared operation has the same key on every screen, and two workflow fields that used to be ignored on some steps are now errors. That is the cost of this release. What it buys is a surface that fails loudly where it used to fail quietly, and that is the ground I want to stand on when I call something 1.0.

The [documentation](https://lezli01.is-a.dev/vincent) and the [GitHub project](https://github.com/lezli01/vincent) have the full detail. Feedback is welcome at [lezli01@gmail.com](mailto:lezli01@gmail.com), and contributions through the [contributing guide](https://lezli01.is-a.dev/vincent/contributing.html). Everything else I am building is at [lezli01.is-a.dev](https://lezli01.is-a.dev).
