Shipping bosun with bosun
#The state at sunrise
I had a tool that'd been floating around in my head for a while. Over the last few weeks I'd been building it on nights and weekends, and I hadn't told anyone.
Bosun is a Go CLI for coordinating parallel Claude Code sessions on isolated git worktrees. The pitch: when you've got three or four agents going at once on the same repo, you need a place to see what each one is doing, a way to predict where they'll step on each other, and a clean merge story for when they finish. Without that, you end up with everyone on main (collisions) or each on its own branch with no coordination (and a painful integration step at the end).
I'd shipped it through eight tagged versions — v0.7 through v0.11.0 — but the repo was private the whole time. Every release had been "ship locally, prove it on my own work, file findings, fix them, retag." The plan from v0.6 onward was that a public flip would happen "after the work was actually done." That clause carried a lot of weight; it kept moving.
Today the work was actually done. The repo is public. Eight versions shipped, eighteen GitHub issues closed, one MCP-tool-driven Linux smoke test on a separate machine confirming go install works for any stranger with Go 1.25+ on PATH.
Most of the code was Claude-assisted, same as every previous post. I'm the architect and reviewer; Claude types fast and finds my mistakes when I ask it to. The pace claim is meaningless without that note. What was different about today wasn't the assistance — it was that bosun was the thing being shipped and the thing doing the shipping.
#The gap analysis that set the day
Sometime between yesterday's evening and this morning, a fresh review of the codebase ended up in docs/pre-launch-gap-analysis.md. Eight gaps, in roughly priority order:
- The v0.9 spawn lifecycle had two open bugs from trial #3c — sub-worktrees vanishing under unclear circumstances (Bug A) and the launcher silently failing on rapid-fire spawn (Bug D).
internal/launcherwas at 40.7% coverage — the lowest in the repo, and the package that silently failed during the very trial that made me write the gap analysis.- No
CONTRIBUTING.md,SECURITY.md,CODE_OF_CONDUCT.md, issue templates, or PR template. - No GoReleaser config — the only install path was
go install, which gates out anyone without a Go toolchain. - CI was bare-bones —
go vet+go build+go test -raceon three OSes, but nogolangci-lint, no fuzz/stress in CI, no dependency scanning. - README was missing badges, a TUI screenshot, a FAQ, and a "Used by" placeholder.
- No
bosun status --watch. Nobosun events --tail. Nobosun debugbundle. No session history archive. - Trial coverage was thin — every external-repo trial had been against
homelab-status-mcp(a Go MCP server), never a polyglot or a monorepo.
I read it at 6:58 am and split the eight items into three buckets: A (the spawn bugs — the v0.9 differentiator is broken without these), B (the launch infra — what GitHub's community-health checklist surfaces), C (the functional gaps — the things v1.0 needs but the user can live without).
#Bosun on bosun
The next part is recursive enough that it's worth slowing down for.
Bosun's whole point is coordinating parallel Claude Code sessions on isolated worktrees. bosun init 4 creates four worktrees off your main repo with branches bosun/session-1 through bosun/session-4. Each session gets a brief markdown file — BOSUN_BRIEF.md — describing its lane. You open a Claude Code agent in each worktree, the agents read their briefs, do their work, commit, run bosun done. Then you run bosun status to see where each one is, bosun predict to spot path overlaps, bosun merge to squash everything back to main, bosun cleanup to tear down the worktrees.
For today's three buckets, the work was bosun fixing bosun. Bucket A was a 2-lane round, Buckets B and C were 4-lane rounds each — ten total lanes, every one of them an agent doing bosun work on a bosun worktree, with bosun watching the whole thing.
The lanes were specified in plan markdowns under /tmp/bosun-work/bosun/ (the safe path, outside iCloud — more on that below). Each session's brief named the files it owned, the constraints it couldn't violate, and the done criteria it had to meet before calling bosun done. The discipline of writing the brief is the discipline that makes the parallelism work: if I can't say in advance which files each lane will touch, the lanes are going to step on each other.
#Bucket A — two bugs, two lanes
Lane 1 owned the spawn-tree reconciliation. The bug: .bosun/spawn-tree.json can hold entries for sub-sessions whose worktrees and branches have been removed externally — macOS's File Provider has a habit of doing this under iCloud-managed paths (more on that in a minute, too). The fix was a spawntree.SyncWithGit() method that prunes entries where BOTH worktree AND branch are missing, wired into bosun status, bosun cleanup --tree, and bosun merge --tree. Six files, +462 LOC including a regression test that builds the exact ghost-tree shape from trial #3c.
Lane 2 owned the launcher fix. The bug: when bosun_spawn creates three sub-sessions in ~8 seconds, only the parent's Claude process ends up alive — the three sub-agents silently never launched. Root cause turned out to be macOS AppleScript throttling on rapid-fire Ghostty window spawns. Fix: a small stagger delay between launches, plus surfacing any post-fork failures via the launcher's output stream so silent swallowing can't recur. Coverage went from 40.7% to 73.0% via two new tests — TestLauncher_RapidFire and TestLauncher_SurfacesPostForkFailure. Documented in docs/macos-setup.md.
Both lanes landed in about 16 minutes wall-time. Two real commits, +1,190 LOC. The merge was clean — no overlap.
#Bucket B — the launch infra
Four lanes. Each one a different kind of thing GitHub's "Community" health checklist measures.
Lane 1: the community files — CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md, three issue templates (bug, feature, safety-violation), one PR template. Seven files total. SECURITY.md names the safety contract as the load-bearing trust signal and commits to a 48-hour acknowledgment window for safety-contract violations. The CoC points at Contributor Covenant 2.1 by reference rather than embedding the canonical text — partly because it stays evergreen that way, partly because the agent writing it got content-filter-blocked mid-flight when it tried to embed the verbatim covenant. I drove its remaining five files manually after the block — that's the human-in-the-loop part nobody mentions in the "AI coded the whole thing" stories.
Lane 2: GoReleaser. .goreleaser.yaml builds darwin / linux / windows × amd64 / arm64 archives with version injection. A .github/workflows/release.yml fires on v* tag push. Two install scripts — scripts/install.sh and scripts/install.ps1 — that the README's curl-pipe-bash one-liner targets. A fuller install matrix at docs/installing.md.
Lane 3: CI improvements. .golangci.yml with errcheck, staticcheck, gosec, ineffassign, unused, govet. Dependabot config covering Go modules + GitHub Actions versions. Weekly cron workflows for make fuzz (Sundays at 03:00 UTC) and make stress (Saturdays at 03:00 UTC). A badges row at the top of the README.
Lane 4: README polish. Added a Comparison mini-section honestly contrasting bosun with raw git worktree, Claude Code's Agent(isolation: "worktree"), and other multi-agent tools. Added a FAQ with seven entries (the questions a stranger actually has). Reserved a "Used by" section for community usage — pre-launch it just says "Maintainer's personal workflow + the architect-mcp release prep."
Four lanes, ~1,500 LOC, one merge conflict on CONTRIBUTING.md because both Lane 1 and Lane 2 created it with different scopes. I hand-resolved by keeping Lane 1's broader operator-facing version and folding Lane 2's release-dry-run section into it.
#Bucket C — the functional gaps
Four more lanes. These are the things v1.0 needs but a user could live without on day one.
bosun status --watch — alt-screen ANSI refresh loop, 2-second default cadence, Ctrl-C cleanup. The gap analysis called this "the single most-natural-to-want feature we don't have." I'd been doing watch -n 2 bosun status for months.
bosun events --tail — terminal-side consumer of bosun serve's SSE event stream. Auto-detects the running server via a new .bosun/serve.pid file. The internal SSE client at internal/events/client.go is reusable for future surfaces (TUI overlays, status-bar widgets, etc.).
bosun debug — self-contained issue-report bundle. Gathers version, doctor output, git status, worktree list, redacted config, audit-log tails, spawn-tree, state + claims summaries, last 50 merges, OS + git version. Trailing operator-skim checklist nudges the user to look for personal paths and secrets the heuristic redaction missed before sharing publicly. Default redacts anything matching (?i)(api|secret|token|password|key)[_ -]*=.
Session history archive — .bosun/history/<utc-timestamp>-<label>/ with brief.md, claims.json, commits.log, merged.txt, metadata.json. Written by cleanup, remove, and merge before they wipe state. Best-effort: a history write failure never blocks the load-bearing git side. New bosun history list / show / grep / prune subcommands. The grep shells out to ripgrep when available with a Go regexp fallback.
Four lanes, ~1,800 LOC, two merge conflicts on cmd/bosun/root.go because three of the four lanes each added an AddCommand(...) line. Trivial — each lane appended one line, I resolved by stacking them.
#Where the friction actually showed up
Two specific moments worth surfacing.
The content-filter block. Lane 1 of Bucket B got blocked mid-flight after writing SECURITY.md. The Anthropic API's content filter — different from the in-conversation refusals you sometimes see — refused the next Write call. The hypothesis: the Contributor Covenant 2.1 verbatim text contains keywords (harassment / discrimination / violence) in a policy-document context that the filter sometimes flags without parsing the document type. I switched the lane's strategy: instead of embedding the canonical Covenant text, the CoC file just links to the canonical source. Cleaner anyway. The remaining four files (the three issue templates + PR template) I drove manually because by that point it was faster than restarting the lane.
The merge conflict that wasn't a code problem. Both Lane 1 (community files) and Lane 2 (GoReleaser) of Bucket B created CONTRIBUTING.md. Lane 2 KNEW it didn't own the file — it created a stub because the lane needed somewhere to document the GoReleaser release-dry-run command. The resolution wasn't really a code merge; it was a scope merge. Keep Lane 1's owner-version, fold Lane 2's section into it, drop the duplicate. The lesson is for the brief: when a deliverable spans two lanes, the brief has to name who owns it and how the other lane references it, not just what each lane produces.
#One command to flip
After the three buckets shipped, the repo was at v0.11.1 locally and on the private origin. The whole "is bosun ready for a stranger" question was answered by one command:
gh repo edit jasondillingham/bosun \
--visibility public \
--accept-visibility-change-consequences
The verification — also one command, run on a Linux box that had never seen bosun before:
GOBIN=/tmp/bosun-stranger/bin go install \
github.com/jasondillingham/bosun/cmd/bosun@latest
Then BOSUN_TOUR_AUTO=1 /tmp/bosun-stranger/bin/bosun tour, the new built-in walkthrough that builds its own sandbox repo and walks through the full bosun lifecycle — init → simulated edits → status → predict → merge → cleanup — in about 90 seconds. It ran end-to-end clean on Ubuntu 25.04 / kernel 6.14. The full happy path is reachable from a public go install with zero local setup.
#What's still open
Exactly one thing in the issue tracker: #15 — a macOS-specific corruption where iCloud File Provider strips git's worktree admin metadata files (HEAD, commondir, gitdir) on directories under ~/Documents/ or ~/Desktop/. The foundational fix is in place — bosun init refuses iCloud-managed paths by default, bosun doctor detects the corruption shape, and bosun doctor --fix recovers it without losing uncommitted work. But the issue stays open as a tracking signal until a real user hits the recovery flow and confirms it does what it says. Field validation is the kind of thing you can't simulate.
I'm not announcing this anywhere yet. The plan from earlier in the day: let the repo sit publicly-discoverable-but-unannounced for a couple of days, watch what surfaces from having other eyes on it, then write the blog-post-LinkedIn-HN-Reddit sequence we sketched. The first blog post in that sequence is this one. The HN Show post is days away.
Tomorrow I take v0.11.1 into a work project and see what new frictions surface from a real, non-dogfood context.
Bosun lives at github.com/jasondillingham/bosun. Today's three-bucket gap-analysis close-out and a longer trial-driven dogfood log on a separate repo are in docs/pre-launch-gap-analysis.md and docs/dogfood-architect-mcp.md respectively.
When was the last time you used your own tool to ship your own tool? What broke that you wouldn't have caught otherwise?