6 minute read

I wanted to learn Go, and I was tired of reading my own shell script.

Those two things led to GSD, the Go version of a small tool I had already built in Bash. GSD stands for Get Stuff Done. It takes a GitHub issue, prepares the work around it, and launches Claude, Codex, or Kiro with enough context to start moving.

The Bash version worked. I used it. That mattered because I was not searching for a practice project or inventing a fake problem to learn a language. I had a real workflow with real edge cases, and the shell implementation had grown enough to show me where Go would help.

GSD Go CLI turning a GitHub issue into parallel AI agent work and a completed pull request
GSD turns a GitHub issue into a prepared AI coding-agent run.

The command I wanted

I wanted GSD to accept the way I think about backlog work. Sometimes I know the exact issue. Sometimes I have a short batch. Other times I want everything carrying a label.

The explicit forms handle single issues, lists, ranges, and mixed input:

gsd "#123"
gsd "#123,#456"
gsd "12-15"
gsd "1 and 7"

GSD normalizes those inputs, removes duplicates, and builds one run around the selected issues. I can choose a different agent without changing the issue syntax:

gsd --codex "#123,#456"

Labels cover the days when the backlog is the instruction. This runs every open security issue:

gsd --label security

I can put a limit on the run as well. This works the next three open bugs one by one:

gsd 3 --label bug

That flexibility became one of the best reasons to move the parser out of a growing shell flow. Lists and ranges are explicit work. A label is an ongoing goal. That is how I already think about the work. The CLI can support both without making me translate the backlog into a different format first.

The shell script came first

The original project was called gsd.sh. Its job sounds simple: accept issue numbers and start an AI coding session.

The script has to do quite a bit before it reaches that last step. It parses inputs such as #123, #123,#456, and ranges. It figures out which GitHub repository it is running inside. It derives a branch and worktree name. It builds the prompt. It selects an agent. It handles different launch commands for Claude, Codex, and Kiro. It prints a briefing, gives me a countdown, syncs the checkout, and finally hands control to the agent.

That is a good shell use case at first. The whole workflow is mostly command-line tools talking to other command-line tools. Bash got the first version running quickly, and tools like ShellCheck plus set -euo pipefail kept it honest.

Then the script grew.

The main launcher passed 400 lines. The companion gsd-term launcher added more than 170. Parsing, validation, prompt construction, worktree handling, terminal output, and agent-specific behavior all lived in linear shell flows. The code still worked, but every change required me to hold more of the script in my head.

Bash got me to working software quickly. Go gave me room to turn the workflow into an application.

I needed a real reason to learn Go

I have never learned much from copying a tutorial project line for line. I retain a language when I have to make it solve one of my own problems.

GSD gave me the right kind of problem. It was small enough to finish, useful enough to keep my attention, and complicated enough to expose the parts of Go I wanted to understand: packages, structs, error handling, command execution, tests, and building a CLI that installs as one binary.

The target behavior already existed. That removed a lot of product ambiguity. I knew what each command should do because the Bash version had been doing it. When the Go version behaved differently, I had something concrete to compare.

This also stopped the project from turning into a language tour. I did not need to sample every Go feature. I needed to parse an issue string, inspect Git, create a worktree, build a prompt, and launch a process. The work decided which parts of the language I learned.

The rewrite forced boundaries into the code

The shell version is a pipeline. The Go version keeps that same flow but splits the responsibilities into packages:

cmd/             command and flag handling
internal/agent/  Claude, Codex, and Kiro launch behavior
internal/git/    repository detection and worktree management
internal/issue/  issue-number parsing
internal/prompt/ prompt construction
internal/setup/  bundled agent and skill setup
internal/ui/     terminal output and countdowns

That separation was the first payoff.

Issue parsing no longer shared a file with terminal colors and process launching. Tests cover Git remote detection without starting an agent. Worktree naming became a function with inputs and outputs. Prompt construction had a home. Each package made me decide what information it needed and what errors it should return.

The first port reached feature parity with the Bash workflow and included 14 unit tests. That number is modest, but those tests covered the parts most likely to become irritating in a shell script: flexible issue input, branch slugs, worktree names, and prompt behavior.

I can make a parsing change and prove that commas, ranges, the word and, and duplicate issue numbers still behave correctly. That feedback loop alone made the rewrite worthwhile.

Go did not replace the command line

The Go version still relies on the tools that make the workflow useful. It calls Git. It expects the GitHub CLI. It launches Claude, Codex, or Kiro. Go coordinates those programs instead of trying to absorb them.

That distinction kept the port manageable. Rewriting a shell tool does not require replacing every shell command with a library. A small exec.Command call is often the clearest boundary when the external tool already owns the behavior.

I also kept the user-facing interface familiar. The same environment variables carry over for agent selection, repository overrides, CI behavior, and worktree roots. gsd --auto 5 still lets the agent choose a priority-sorted batch, and gsd term feature/my-branch still opens an agent session in a prepared worktree without requiring an issue. I wanted a new implementation, not a new habit every time I opened a terminal.

What Go changed for me

The single binary is convenient. I can build it, put it on my path, and run it without wondering which Bash version or local script copy I am invoking. go install gives the project a clean distribution path.

The bigger change is how I reason about the tool.

In Bash, the happy path reads naturally from top to bottom. As branches and modes accumulate, state becomes harder to follow. In Go, the compiler makes me be explicit. Values have types. Functions have contracts. Errors have to go somewhere. Package boundaries expose shortcuts that a shell script can hide for a long time.

Not every line became shorter. The Go code is larger than the shell script. I expected that. The extra lines buy structure, tests, and room to change the tool without turning one file into a maze.

The rewrite also changed how I approach failures. A missing remote, unsupported agent, malformed issue range, or failed worktree creation now travels through a deliberate error path. The shell version can fail safely, but Go makes the failure model easier for me to inspect and test.

What the project taught me about rewrites

Starting from working software made the rewrite practical. The Bash version was the specification, including the awkward edge cases no greenfield design document would have captured.

I also learned to preserve the useful shape of the original. GSD is still a thin launcher. It still uses the tools already installed on my machine. It still turns a short command into a prepared agent session. Moving to Go did not need to turn it into a daemon, service, or framework.

That restraint matters. A rewrite can become an excuse to redesign everything. I had two goals: learn Go on a problem I cared about and move growing shell logic into a language that gave me stronger structure. The project met both goals without changing what made the original useful.

The Bash version proved the idea. The Go version gave the idea somewhere to grow.

The source is available at github.com/jon-the-dev/gsd.

Updated: