I’ve written before about how my team at work has adopted a variant of the Mikado Method for software project planning. We started this in 2023 and have evolved the process a bit since then. Most notably, we’ve adopted a more strict process of prototyping during the planning phase. Where we originally only did a bit of spiking up front, we learned that our designs were better if the lead engineer actually built a “walking skeleton.” This code doesn’t have much error checking, there are no tests, and small bits may still be implemented as pseudocode in comments, but it makes it clear where parts of the system interface with one another and what each must provide.
The method works great (IMHO) when planning work for a team. It ensures the engineers working on implementation have a starting point (the prototype code), along with a work plan that allows incremental implementation (the Mikado chart, along with properly linked tickets in Jira).
But, there are a few drawbacks to this process. First, creating the prototype can be quite time-consuming. Additionally, the designer bounces between a diagramming tool to manage the Mikado chart and their IDE to write code. This disconnect between the plan and the prototype implementation makes it more difficult than it needs to be for engineers to map between the two.
In this article, I’m going to gloss over the first of those issues. Instead, I’ll focus on how we have started to use the Jujutsu (jj) version control system to address the latter.
If you’re curious about the former, we now have AI agents that happily and speedily produce not-quite-production-ready (prototype) code. We have started to lean on them for this process. However, we are still very careful to ensure that the scope of each node in the graph is small and targeted; after all, if a bug gets buried in a 2000-line PR, the agents aren’t the ones waking up when it causes a pager to go off at 3am. Less sarcastically, modern AI models also allow us to explore different design and implementation options as well, so we can quickly get a sense of how multiple approaches “feel” to work with, which is a real time saver and system design win.
The Key Insight Link to heading
Both a project plan and a version control history are ultimately directed acyclic graphs (DAGs). The key insight I had when thinking about the challenges listed above is this: if a node in a VCS graph could carry enough metadata to make its place in a project plan make sense, then the entire project plan could be represented within the graph itself. And, in our case, because the node would also contain just the prototype code for that node, it would become trivially easy to map between the code changes and the Mikado graph.
That only works, though, if editing the version control DAG is easy. This is where jj comes in.
Just Enough jj Link to heading
Jujutsu is a version control system that takes inspiration from earlier version control systems and abstracts the user interface from the underlying storage. In our case, Git is still the underlying storage for our repositories. However, jj gives us some capabilities that are nicer than working with Git natively.
Critically, jj makes rebasing a breeze. Imagine you’re working on a change (think
of this like a mutable commit), and you spot an issue from an earlier change that needs fixing.
You can switch to that one (jj edit <previous-change-id>), make the fix, and it will
auto-propagate to all descendants. In the event of conflicts, jj records them
in the affected descendants instead of halting the rebase to make you fix them immediately.
You can resolve them (jj resolve) whenever you’re ready, and the fix flows down to the
children the same way.
If you’re working on a change and decide you want to write some code that the change could
rely on, you can create a brand new parent and do the work there (jj new --insert-before @ to
insert before the “current” change). And, again, that work will auto-propagate to descendants.
This simplicity is a key benefit we’ve found.
Additionally, jj makes it simple to write great commit messages (jj calls these descriptions),
even on empty changes. A simple jj describe pops up your favorite text editor, where you can
write as much or as little as you want. And, you can evolve this. At any time, when you’re on a
change, you can run jj describe again to edit what you’ve previously written, to better reflect
the contents of the change. If you want to see the description along with the actual code
for each change, it’s as easy as jj show. Along with the ease of rebasing, this makes it
quite easy to ensure changes are atomic - gone are the days of commits like “wip” or “fix a typo.”
Admittedly, most of these are things that could be done using Git. But the ergonomics of jj feel more intuitive to me.
Bookmarks vs Branches Link to heading
The biggest change, for me, going from Git to jj is that jj doesn’t have branches in the sense
that Git does. Instead, you create a bookmark (jj bookmark create <name>), which gives a name
to a particular change. Switching to a bookmark is as easy as jj edit <name>. If you already
have updates in the change you switch to, you might alternatively consider jj new <name> to
create a new change off of the old one, and then jj squash once you’re ready to consolidate.
There are options here - I tend to jj edit, myself. Note that the trunk branch and any
remote branches you haven’t tracked are immutable by default, so they can’t be edited. (So, for
instance, if you try jj edit main, you’re likely to see an error - you need to create a new
change off of it instead, jj new main.)
When interacting with Git, jj treats these bookmarks roughly as branches. When you sync from
a Git repository, jj identifies branches as bookmarks. You tell jj which bookmarks you want to
track from the origin (e.g., jj bookmark track <name>@origin). When you push to a Git repo
(jj git push), jj will identify which tracked bookmarks in your current line of work have
differences with what’s on the origin and push them. If there isn’t already one, jj will
create a Git branch with the bookmark’s name on the remote. Note, though, that the bookmark
must be tracked, so if this is a new bookmark, be sure to use jj bookmark track as
described above to tell jj you want it kept in sync with the remote Git server.
Another common trip-up to be aware of: If you create a change based on a bookmarked
change, you need to remember to move it to the new change before pushing to Git
(jj bookmark move <name>). Unlike adding commits to a Git branch, descendant jj changes are
not automatically associated with a previous bookmark.
This takes a little getting used to, but it’s quite flexible. You don’t have to pre-declare a branch for a change. You can let the graph evolve naturally and name bookmarks accordingly.
Going Further Link to heading
If you’re interested in learning to use jj, I highly recommend this video by Matthew Sanabria. It’s an outstanding overview, and I share it with everyone I evangelize jj to! If you prefer text, Steve Klabnik’s Jujutsu Tutorial is also excellent!
Putting it Together: Planning a Project Link to heading
Let’s take a look at how we can leverage this newfound knowledge to do a Mikado-style plan, similar to the example from my earlier post. As a refresher, let’s say we have an application that counts the lines in files, and we want to add a CLI flag to count words instead.
Bounding the DAG Link to heading
I typically start by defining a start and an end point. Basing this off of the main bookmark
(which I have set up to track the main@origin Git branch), I create two new changes and give
them names.
jj new main
jj bookmark create start
jj new
jj bookmark create end
The bookmark names are arbitrary, but having a starting and stopping point is useful. The DAG
will be built fully between start and end. If you have design decisions that you want applied
throughout the DAG, you can put Markdown files on the start change, since they will propagate.
The dedicated starting change also makes it easy to rebase the whole DAG on the latest version of
main, assuming it is also changing while you’re planning this work.
I write the description on the end change as if it were a ticket. It defines what
success looks like, even before any code is written. This is what you’re going to be working
toward and how you know whether you accomplished it. Now, it can change! That usually means
that more detail is added to it throughout the planning effort. But you want a target to aim
for.
Here’s an example of where I might start in our hypothetical. Assuming I’m on the end change,
jj describe pops open Helix (for me), where I can write a reasonable description:
Add Word Count Mode
This change introduces a new "word count" mode to the application.
Providing "-w" as a flag to the CLI program will tell it to return
the number of words, rather than the default behavior of returning
the number of lines in the file.
Usage: my-awesome-program -w <file path>
Following this, I can see the current state of the DAG with jj log:
@ pwwzkooq my@email.com 2026-08-25 17:45:36 end 4b2d8706
│ (empty) Add Word Count Mode
○ otqzstsn my@email.com 2026-08-25 17:42:41 start e9e45f4a
│ (empty) (no description set)
◆ yqupzxny my@email.com 2026-08-25 17:42:07 main 2d4960f8
(Pro-tip: In big repos, you might see a lot of stuff you’re not interested in right now. Use
jj log -r 'start::end' to limit the output to only your planning changes.)
Building the DAG Link to heading
We’re basically following our Mikado approach here, so we start at the top (the end). Here,
I (or an agent) would begin writing some code that implements the functionality required
by the change description. The goal is for the code supporting the description to focus on
doing what the description says. Ideally, we want our changes to be implementable in roughly
a couple hundred lines of production code along with up to a few hundred lines of tests. This
isn’t the number of lines in our prototype, so there’s a judgment call here. The prototype
typically will give a pretty good idea of the changes required though.
Following the example from the previous article, let’s say I quickly realize that my first
hurdle is I have all line counting functionality in my main function (the left-hand node in
the graph in that previous post). I decide I need to fix this before I can do much more. So,
I’ll create a new change in the DAG: jj new -B @. This will create a new parent before
the current change and switch over to it. Before writing any code at all, I would first describe
what I think I’m going to need from this node using jj describe. This is important: The
descriptions describe what is required, not how to do it - that’s what the code is for. The
graph now looks like this:
○ pwwzkooq my@email.com 2026-08-25 18:20:00 end 15ae546a
│ (empty) Add Word Count Mode
@ utmmlmvt my@email.com 2026-08-25 18:20:00 d9d0c6e0
│ (empty) Replace Line Count Code in main with Function Call
○ otqzstsn my@email.com 2026-08-25 17:42:41 start e9e45f4a
Note that the change ID (“pwwzkooq”) for end stayed the same, but the underlying commit (“15ae546a”) has changed. On the other hand, there was no alteration to start, so its commit did not change. This ability to change contents as-needed is what I meant earlier when I said I think of changes a bit like mutable commits. Their identifiers are “locked,” but their contents can change.
From here, I would continue working on my prototype and building new parents as needed. From
the example, one other interesting case is the two parents for the “Replace Line Count…”
node. This node has two parents, not one. This is, again, easily achievable in jj. I can create
the first one just like before: jj new -B @ and then describe it. This inserts a new parent
between the change and its existing parent, so the graph looks like this:
○ pwwzkooq my@email.com 2026-08-25 18:28:05 end eef57e32
│ (empty) Add Word Count Mode
○ utmmlmvt my@email.com 2026-08-25 18:28:05 fc49cd48
│ (empty) Replace Line Count Code in main with Function Call
@ tsqwwvwx my@email.com 2026-08-25 18:28:05 aa32af0d
│ (empty) Write Integration Tests for Existing Behavior
○ otqzstsn my@email.com 2026-08-25 17:42:41 start e9e45f4a
But the next dependency is unrelated to this new one. Instead, we want to parent it on start.
Going back to change “utmmlmvt,” we can accomplish this with jj new -B utmmlmvt -A start. After
giving it a description, we have the following:
○ pwwzkooq my@email.com 2026-08-25 18:31:19 end fadc23b7
│ (empty) Add Word Count Mode
○ utmmlmvt my@email.com 2026-08-25 18:31:19 c9c24eac
├─╮ (empty) Replace Line Count Code in main with Function Call
│ @ oxwlrnos my@email.com 2026-08-25 18:31:19 ab937d4a
│ │ (empty) Make Helper Function for Line Counting
○ │ tsqwwvwx my@email.com 2026-08-25 18:28:05 aa32af0d
├─╯ (empty) Write Integration Tests for Existing Behavior
○ otqzstsn my@email.com 2026-08-25 17:42:41 start e9e45f4a
As you find nodes that need no more parents, you work your way back up the graph, fleshing out the prototype as you go.
And, really, this is about it. It’s an iterative process, just like the previous method we
used. But, you’re just looking for ever-smaller chunks of work that build up to meet the
goal you’ve defined in the change description. At the end, instead of a diagram - such as
from the previous post - you’d have something like this in jj log -r 'start::end':
@ pwwzkooq my@email.com 2026-08-25 18:38:16 end f1a23656
├─╮ (empty) Add Word Count Mode
│ ○ kllzlskq my@email.com 2026-08-25 18:38:16 ff604f87
│ ├─╮ (empty) Count Words When User Provides -w Flag
│ │ ○ wlstvytm my@email.com 2026-08-25 18:38:16 509418f3
│ │ │ (empty) Add CLI Flag Parsing to main
│ ○ │ lsvvrzox my@email.com 2026-08-25 18:37:53 f3253fc4
│ │ │ (empty) Create Word Count Helper Function
│ ○ │ uxyynurr my@email.com 2026-08-25 18:37:53 037e548f
│ ├─╯ (empty) Function to Split String into List From Spaces
○ │ utmmlmvt my@email.com 2026-08-25 18:31:19 c9c24eac
├───╮ (empty) Replace Line Count Code in main with Function Call
│ │ ○ oxwlrnos my@email.com 2026-08-25 18:31:19 ab937d4a
│ ├─╯ (empty) Make Helper Function for Line Counting
○ │ tsqwwvwx my@email.com 2026-08-25 18:28:05 aa32af0d
├─╯ (empty) Write Integration Tests for Existing Behavior
○ otqzstsn my@email.com 2026-08-25 17:42:41 start e9e45f4a
For this exercise, I didn’t write any code, which is why each change says it’s empty.
Normally, this wouldn’t be the case. There’s not much to see because I didn’t write any code,
but you can look at each change conveniently with, for example, jj show kllzlskq, which
will show the long-form description as well as a code diff for that change.
Handing Off to the Team Link to heading
So, what does the team get out of this? There are two main outputs.
First, the prototype branch (the end bookmark) is pushed to GitHub. Each jj change
corresponds to a Git commit, and the DAG is saved accordingly. Team members can pull down
the branch and use jj locally to review it. The entire graph can be viewed using jj log.
Second, the planner ensures there are Jira tickets created for each node in the DAG. This is easily automatable, and AI agents do this well. The Jira title is the change description summary line. The ticket body contents are the rest of the change description. And tickets are linked via blocked-by/blocks relationships based on the DAG (parents “block” children, which are “blocked by” their parents). We further augment this by including a comment with a link to the specific matching prototype commit in GitHub, which is a quality-of-life improvement for the engineers doing the work.
Rough Edges Link to heading
We’ve had initial success using this process, but there are some rough edges still.
The biggest gap is in visualizing the graph. Naturally, the diagramming tool we used before
allowed us to make very nice-looking graphs. Raw jj log output displays the
graph, but it can be more difficult to decipher than a purpose-built chart. The benefits of
not switching between systems outweigh the drawbacks here, in my opinion, but it’s a real
gap. A couple folks on my team built a nice VS Code plugin for visualizing these plans
(unfortunately, not open source). I do think in-editor visualization is probably a good approach
here, since we’re trying to keep people in their IDEs as much as possible.
The second issue is simply that it does rely so heavily on jj, and this is still a new tool for many folks. I use it for my day-to-day version control, so I’m quite comfortable with it. But, there is a small learning curve for folks to get up to speed using it.
Finally - and so far, for us, the most minor of all - our Jira tickets link to a commit, since there is no way to link a change ID. If the lead engineer were to make alterations to a change, these would leave the change ID intact, but the underlying Git commit (and all descendants, if any) is updated. When pushing to GitHub, then, the previous commit ID is replaced with the new one. In the event that the corresponding Jira ticket has already been created, the link to the commit will now be broken. This means going back through the appropriate tickets and updating links. Again, something an agent can do, but it’s a bit of an annoyance. In practice, we don’t usually create the Jira tickets until the prototype is considered “done,” but it’s worth watching out for.
Conclusion Link to heading
So, can a version control system help in project planning?
For me, yes. I was honestly skeptical of the idea when it first occurred to me, but it’s held up through planning a few projects this way. As an engineer, it helps me avoid bouncing between a diagram and my editor. Keeping everything in one place also makes it easy to share with others, who can quickly cross-reference the plan with the prototype implementation. It feels like a natural evolution of the Mikado process we’ve been using for years.
Even with a few rough edges, the process has been a net positive for planning, and I expect we’ll smooth those out over time. I’d encourage you to give this approach a shot.