// post

· 7 min read

vincent's next refactor: intake, workshop, and handoff as three separate pillars

// Why I want vincent to work like the developer of a small software company, the three pillars that job splits into, how GitHub issue handling is tangled into today's workflows, and why pulling it out is the first step.

// contents
  1. Three pillars around one job
  2. Why intake, workshop, and handoff
  3. How issue handling looks today
  4. What the seams should carry
  5. The first step: extracting intake

Before adding another feature to vincent, I spent several brainstorming sessions on a more basic question: what is it actually supposed to be? 0.9.0 gave it triggers that start work on their own and pull request actions that finish it, and once both ends existed the question was hard to put off.

The answer that kept coming back is easy to state. Vincent should be the developer of a fantasy software company. Someone files a ticket, the developer picks it up, does the work, and closes the item in whatever way fits: a pull request, a comment on the ticket and a resolved status, sometimes a deploy. That is a wider job than the one vincent does today, and it changes what the core of the codebase should look like.

Three pillars around one job

A developer in a real company does not own the whole flow. Work arrives from a system they do not control, and it leaves through another one. Thinking it through, the job splits into three pillars:

  • the backlog: the ticketing system, issue list, or whatever a given organization calls it; where work comes from,
  • the work: understanding the item, changing the code, and proving the change, which is vincent’s main scope today,
  • the closure: how the item departs, whether that is a pull request, a comment on the original ticket followed by resolving it, or something similar.

Read left to right, that is roughly requirements, then work, then CI/CD. The two ends are what change from one organization to the next. One team keeps its backlog in GitHub Issues, another in Jira or Linear, a solo project in a markdown file, and somebody in an email inbox. Closure varies just as much: a GitHub pull request, a GitLab merge request, a comment and a resolved ticket, a deploy. So both ends should be pluggable, and the middle should stay vincent’s core:

backlogGitHub IssuesJiraLinearmarkdown fileemailINTAKEtickets initemWORKSHOPvincent coreworkflowsworktreesagentschecksgatesresultHANDOFFwork outtargetsGitHub PRGitLab MRcomment+resolvedeployexists today, inside the workflowsadapter the design should allow, not built
Solid outlines are the only adapters vincent has today. Dashed ones are the targets the pillar boundaries have to leave room for.

Why intake, workshop, and handoff

The pillars need names, and the first ones that come to mind are too narrow. “Requirements” assumes every item is a specification, but a real backlog also holds bug reports, questions, chores, and whatever a scheduled trigger decides needs doing overnight. “CI/CD” assumes every item ends in a pipeline, while plenty of tickets end correctly with a comment explaining why nothing will change.

I settled on intake, workshop, and handoff. Intake is the word organizations already use for the place work enters, and it says nothing about the shape of that work. Workshop is where the developer builds, and it is the one pillar vincent already has in full. Handoff is the moment the result passes back to someone else, which fits a pull request waiting for review and a ticket comment waiting for its reporter equally well. I avoided “delivery” for the right side on purpose, because the DAG workflows already use the word for the middle: the fan-out step is named Deliver the units in parallel.

How issue handling looks today

Today the only backlog vincent reads is GitHub Issues, and the only handoff it makes is a GitHub pull request. Neither has its own place in the codebase. Both are steps inside the workflows that resolve issues.

github-resolve-issue.yaml is roughly 2,500 lines. Its first step, fetch, runs gh issue view on the declared issue field and refuses an issue that is closed or labeled neither bug nor enhancement. The next step, is-bug, is a probe whose exit code picks one of two brief paths. At the other end, publish pushes the branch and runs gh pr create, the PR body carries Closes #… so that merging closes the issue, and a merge tail rebases, waits for the required checks, and runs gh pr merge --merge --match-head-commit. Everything between those two ends is the actual work.

The DAG variant, github-resolve-issue-dag.yaml, says in its own header that everything before the implementation and everything after it is copied from the sequential file unchanged, so that the two fail the same way. For two files that was the right call. It does not survive a second backlog: Jira intake in this shape means either a Jira copy of both files or a Jira branch in every step that touches a ticket.

The coupling reaches into the lanes too. Each lane of a DAG run starts with a step that re-runs gh issue view, because the fetched issue sits in an uncommitted scratch directory that a lane’s worktree cannot see. It is visible in a run I showed in the 0.8.0 post:

vincent Workflow tab for task #194: an approved graph, an eager fan_out, and two lanes each starting with an Assemble this unit's context command step
Task #194 on a v0.7.0-211 development build. Each lane's first step, Assemble this unit's context, reads the GitHub issue again because the ticket has no home inside vincent.

The closure side is only half built. When the brief step decides an issue is reject or needs-info, the triage step fails the task with a status on vincent’s board, and the issue stays open with nothing written back to it. Clarifying questions go to whoever is at the TUI, through claude’s AskUserQuestion tool, not to the issue thread where the reporter would see them. Only the pull request path closes anything, and it does so through GitHub’s closing keyword. A developer who resolved tickets that way at a real company would hear about it from the reporters.

What the seams should carry

The direction I’m exploring is two contracts. Intake turns whatever the backlog holds into a work item vincent owns: an identity and a link back to the source, the title, the body, the discussion thread, and a kind that a workflow can branch on instead of probing a label. Handoff takes the workshop’s result and does what the target needs: open a pull request, merge it, or post a comment and resolve the ticket. The exact shape of both interfaces is still open, and I’d rather let the extraction settle it than design it on paper.

The same ticket, walked through the company:

ticketintakework itembriefverdictproceedimplementverify, gatehandoffpull requestmergedticket closedreject / needs-infohandoffcomment backresolvedor waitingtoday: the dashed path ends as a failed task on the board, with nothing written to the ticket
The solid path is what the GitHub workflows already do end to end. The dashed one is the handoff they are missing.

The strongest objection is that this is abstraction ahead of need. Vincent has one backlog and one delivery target, and an interface designed around a single implementation usually encodes that implementation’s accidents and calls them a contract. The objection is fair, and it shapes the order of the work rather than cancelling it. The duplication already exists with one adapter: two workflow files carry the same intake and closure steps, and every DAG lane re-reads the issue on its own. So the first step does not design for Jira. It moves the GitHub behavior that already works into its own pillar, keeps it behaving the same, and lets the second adapter prove or correct the boundary.

The first step: extracting intake

The first part of this refactor is separating issue handling from its current shape. Today fetch, the label check behind is-bug, and the gh issue view inside every lane are workflow steps. After the extraction they should be one intake pillar that reads the ticket once, holds it as a work item, and hands the same item to the sequential workflow, the DAG workflow, and each of its lanes:

todaygithub-resolve-issue.yamlfetch · is-bug · triagegh issue viewbrief · implement · docs · verifypublish · merge-attemptgh pr create / mergegithub-resolve-issue-dag.yamlfetch · is-bug · triagecopied unchangedplan-dag · build · integrateeach lane: gh issue viewpublish · merge-attemptcopied unchangeddirectionINTAKEreads the ticket oncework itemresolve-issue: the workresolve-issue-dag: the workresultHANDOFFPR, or comment back
Filled segments are GitHub-specific steps. In the direction below the rule, only the pillars know which system the ticket came from.

Intake goes first because it is upstream of everything else. The 0.9.0 triggers already watch a project’s GitHub issues and create tasks from them, and the new-task form and vincent task add --github-issue already know what an issue is, yet the workflow still fetches the issue again itself. Pulling that into one place gives the triggers, the task form, the workflows, and the lanes a single item to agree on. Handoff comes after, and it is where the missing comment back on a rejected ticket gets its home.

The tradeoff, named plainly: a refactor that ships no new capability, done on workflows that already work, in a project that is not yet 1.0, in exchange for a core that does not care where a ticket came from or where the result goes. That second vincent is the one I want to be adding features to. The rest of what I’m building lives at lezli01.is-a.dev.

// thoughts? reply by email

// comments

// comments load as you scroll here. They live in GitHub Discussions; sign in with GitHub to join in.

← all posts