Operate a local autonomous GitHub with jj workspaces

Forget pull requests if you want to unlock maximum productivity with parallel coding agents. In fact, forget pushing changes to the remote. This post describes a fully local workflow that lets you build gigantic features in a short period of time with parallel coding agents.

Here’s the scenario: I got nerd sniped by an idea recently and worked on a proof-of-concept over the last five days in between other tasks. Over 700 million tokens later, 150 commits, and 40k lines of TypeScript, I have a fully functional app that I’m itching to start using daily. I never pushed the code to GitHub. I didn’t even open the project in my IDE! My setup:

I posted some thoughts about the jj workspace workflow on X and Thorsten Ball asked me to share more details, so this post is my attempt to elaborate.

Thorsten Ball
Thorsten Ball
@thorstenball
Can you write up how you do it? I'm really really interested
10:18 AM · Jan 24, 2026

If you’re not familiar with Thorsten, he writes an excellent newletter Register Spill.

#Separate worktrees are good, actually

Once you fully embrace agentic coding, you quickly start running several agents at the same time. Parallel agents is the killer feature of agentic coding! When all the code is written by hand, you need multiple humans to properly multi-task. When agents are writing all the code, your mind is freed up to more easily context switch between unrelated tasks.

I’ve seen claims that it’s fine to run parallel agents in the same git checkout. I tried this and it didn’t work well for my setup. The problem I hit on is that the dev server kept hot-module reloading changes from concurrent agents so the website was constantly in a broken state making it impossible for me to manually test changes.

My problems with parallel agents were solved by letting each agent operate on a separate copy of the codebase. For each copy, I opened an iTerm tab with three horizontal panes: top pane for Cursor CLI, middle pane for ad-hoc commands, and bottom pane for the dev server running on a separate port number.

┌────────────────────────────────────────────────────────────────────┐
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐        │
│ │Pet Inventory (●)│ │ Shopping Cart   │ │ Checkout Flow   │  ...   │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘        │
├────────────────────────────────────────────────────────────────────┤
│ .../src/components/PetCard.tsx +15                                 │
│ + import { useCart } from '../hooks/useCart';                      │
│ + const { addToCart } = useCart();                                 │
│ ...                                                                │
│ $ npm run typecheck 2>&1 3.2s                                      │
│ Fixed. The issue was that `useInventory` was called after          │
│ the early return. I moved it before the early returns.             │
│                                                                    │
│ Claude 4.5 Opus (Thinking) - 70.5k · 5 files edited                │
│ / commands · 0 files · ! shell · ctrl+r to review edits            │
│ ▶ Auto-run all commands (shift+tab to turn off)                    │
├────────────────────────────────────────────────────────────────────┤
│ $ jj log                                                           │
│ $                                                                  │
├────────────────────────────────────────────────────────────────────┤
│ VITE v6.1.0  ready in 1279 ms                                      │
│ → Local:   http://localhost:3000/                                  │
│ [vite] connected.                                                  │
└────────────────────────────────────────────────────────────────────┘
 ↑ Tab title = automatically generated chat session name
 (●) = Notification bubble (agent needs attention)

Next, for each iTerm tab, I had a matching browser tab to manually test the changes for that agent. This setup immediately clicked with me, and felt more productive than my usual setup with the Cursor agent in the sidebar and the text editor in the middle.

#Git worktrees are … unintuitive

I’ll admit, I didn’t actually use git worktrees for this project since I fully switched to jj several months ago. However, git worktrees never clicked for me while the jj equivalent, workspaces, felt intuitive to work with from day one.

I think the main reason git worktrees never clicked for me is because git is too low-level. Manipulating raw git commits feels like writing C with goto statements, while working on jj changes feels like working with a high-level programming language such as TypeScript. With jj, you declare the relationships between changes and jj automatically keeps it all rebased and up-to-date. Git, on the other hand, requires more discipline to scale up and you’re one bad command from getting into a bad state that’s difficult to recover from.

I made three failed attempts last year to switch over to jj so I won’t lie that there’s a learning curve involved. However, with Opus 4.5, you don’t have to run jj commands by yourself anymore so the learning curve might be smaller now. With Sonnet 4.5, I had to reference jj docs in every chat session while it feels like Opus 4.5 has enough jj knowledge pre-baked into its weights.

#Jujutsu workspaces are (mostly) great

What sounds like a complicated setup is actually easy to get started with because you aren’t running commands yourself anymore. The agent is better at using jj than you are! To start with, initialize jj and a few workspaces. I did this manually, but the agent will comfortably do it for you as well.

❯ jj git init
❯ jj workspace add ../pet-store-1
❯ jj workspace add ../pet-store-2
❯ jj workspace add ../pet-store-3

Then, run agents in each of those directories and use the following skill to ask an agent to merge the work from the different workspaces.

skill/merge-workspaces.md

If I’m being honest, I didn’t even read the document in full. It’s just agents all the way down.

Part of the magic is that Opus is proficient at using the jj revset language to select what changes to duplicate (aka. cherry-pick), rebase, or squash. Git has commands with similar names but they don’t operate at the same high level of abstractions as their equivalent commands do in jj.

I still regularly run jj to manually get an overview of what fixes are available in the different workspaces.

❯ jj
○  wprutuwr [email protected] 2026-01-25 12:01:14 pet-store-agent-4@ cb21bfbf
│  feat: add OrderConfirmation component with order summary
○  wrpruvwm [email protected] 2026-01-25 12:01:14 a8848d47
│  feat(checkout): implement multi-step checkout flow
@  nxqyznsr [email protected] 2026-01-25 12:01:14 main default@ 49b349b7
│  Add order types and API client for order management
│ ○  wmporypo [email protected] 2026-01-25 12:01:14 pet-store-agent-3@ 69af9189
│ │  WIP: style: add CSS animations and responsive styles for modal
│ ○  zzynkuzl [email protected] 2026-01-25 12:01:14 7e984206
├─╯  feat(modal): add pet detail modal with accessibility support
○  slwmplqp [email protected] 2026-01-25 12:01:14 2f9de97a
│  Add utility functions for price formatting and validation
│ ○  xwxyzskp [email protected] 2026-01-25 12:01:14 pet-store-agent-2@ b7ae4706
│ │  fix: add debouncing to search and fix edge cases
│ ○  rnznwpqu [email protected] 2026-01-25 12:01:14 0f539a3a
├─╯  feat(search): implement search and filter functionality
│ ○  vmzsktuk [email protected] 2026-01-25 12:01:14 pet-store-agent-1@ 01ecf059
│ │  WIP: test: add unit tests for AddToCartButton component
│ ○  wzwnqztk [email protected] 2026-01-25 12:01:14 a1a83f07
│ │  refactor: extract AddToCartButton into reusable component
│ ○  upwnnmkz [email protected] 2026-01-25 12:01:14 bcb118b9
├─╯  feat(PetCard): add 'Add to Cart' button with inventory refresh
○  nkpxnqpo [email protected] 2026-01-25 12:01:14 68dabab6
│  Initial pet store setup with React, TypeScript, and basic components
◆  zzzzzzzz root() 00000000

That’s about it. For this project, I didn’t merge any changes by hand, I didn’t even review the merged code. I just tested the final product to make all the changes were working as intended, dedicating my full attention to finding issues in the user-facing product and breaking them down into actionable tasks for the agents.

#The bad parts of jj workspaces

Before you rush to setup jj workspaces, be warned that there rough edges:

The takeaway for me is that you want your jj state to be small, almost ephemeral so it’s fine even if the .jj/ gets wiped away. Aggressively jj abandon dead experiments to keep your log pristine. In an ideal setup, your default jj log should fit within the viewport of your terminal and all the changes have single-character aliases.

#Conclusion

This style of coding is an entirely new world for me, and it’s far from the ideal state. What I describe above is already going to be outdated next week.

My past week felt productive since it was the first time I unlocked true multi-tasking with parallel agents thanks to jj workspaces. It’s still to be determined if my week actually was productive, or if I just burnt a metric ton of tokens to produce slop. The real test will be if the app will actually get used or not…