// post

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

// What vincent 0.9.0 adds and hardens: triggers that propose work on their own, pull request actions from the task, agents inside the task container, and the fixes and guardrails that make it the biggest step yet toward a first stable release.

Fourteen days and 428 commits after 0.8.0, vincent 0.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 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 was the release where it started carrying its own development, and 0.8.0 taught it to split one issue into parallel lanes.

The 0.9.0 changelog 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:

Three vincent TUI views stacked on top of each other: the triggers screen at the back, the Step Details tab in the middle and the Pull Request tab in front
The docs capture the real TUI on seeded demo data with a build from before the tag, so the header still reads 0.8.0.

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 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.
  • 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:

The triggers screen: an armed command trigger selected above a disabled GitHub issues trigger, with its delivery ledger below
The armed ci-red trigger's ledger holds two events it seeded on arming. Neither created a task.

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.

The Pull Request tab on a finished task: the pull request's details, its checks with the failed build selected, and the keys for merging, closing, commenting and re-running failed jobs
Checks for head d3adb33fd3ad with one still in progress, so a merge from this tab would be refused until it settles.

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.

The board filtered to a fan-out parent in awaiting_children and expanded: its storage and client lanes done, and the handlers lane waiting at its gate
The handlers lane was spawned in the second round, once storage and client had merged.

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:

The daemon view: the config in effect against the built-in defaults, the database block, and the agent adapters
Claude sits behind an observed usage limit until 19:53, and the backups row points at backup.interval because it is off.

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.

The Step Details tab on an agent step: the rendered prompt, the rendered check, the result summary, and where each setting came from
An attempt older than the feature says so: model and effort read not recorded rather than a guess.

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, 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 fixed the MCP tool descriptions so they name the request bodies the daemon actually accepts (#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 and the GitHub project have the full detail. Feedback is welcome at lezli01@gmail.com, and contributions through the contributing guide. Everything else I am building is at lezli01.is-a.dev.

← all posts