Staying in control of your codebase in the AI era A developer shares a workflow for using AI to write maintainable code, emphasizing constraints like linting rules and standardized testing to keep AI-generated code readable and team-friendly. I love coding, and I love my job. Like many others in the industry, I found it hard to cope with the changes of the last couple of years. At first, I had an existential crisis. Then I started experimenting and watching how people actually work with AI, which greatly reshaped my view. I really recommend this video from Jon Gjengset https://www.youtube.com/watch?v=vmKvw73V394 , where he shows how he works with AI, what it's good for, and how it's ultimately just another tool in your toolbox. Embracing AI and steering it to produce maintainable code at an acceptable cost is the name of the game. There are countless smart people out there working on different harnesses and techniques to crack the problem. The current craze is loops, but aside from being too expensive, the approach just doesn't click with me. There's a difference in effective AI use between working alone and being part of a team on the same project. In the latter case, I have obligations to my employer and my team. Some employment contracts even contain a clause like this: "the Employee shall perform their duties to the best of their ability and skill." I tend to take that line rather seriously. Ridiculous as it may sound, this is my creed. I take great pride in my work, and I plan to keep it that way. I invest heavily in keeping my code maintainable and reviewable, to make life easy for my peers and my future self. Let me show you the AI assisted development workflow I'm currently happy with. What works for me The approach that works today is constraint. Selectively adding guardrails to get more consistent, predictable output. I work with Go the most in my day-to-day, so my examples will use it, but I think these techniques apply to almost any other language. That said, Go has the advantage that different developers tend to produce fairly similar code, thanks to the language's rigid structure. Linter I find that linting is the alpha and omega of steering agents to produce acceptable code. As expected, I'm using the standard golangci-lint as my linter. These are the rules I mostly rely on to influence the shape the code takes: settings: cyclop: max-complexity: 10 funlen: lines: 90 statements: 50 revive: rules: - name: argument-limit arguments: - 8 - name: file-length-limit severity: error arguments: - max: 1000 skipComments: true skipBlankLines: true Your peers will thank you later for the effort you put into constraining the code's dimensions. Limiting the lines of code in a single file and capping cyclomatic complexity forces the agent to break code down into smaller logical units across multiple files. Function length and the number of allowed function arguments complement these rules, making the code far more readable to humans. Testing Agents are useful for generating unit tests to save you from the grunt work. Proper test coverage has always been important, but it matters more than ever in the era of full project rewrites. Take the agent by the hand and don't let go until it can produce sane test cases. I usually start by writing a few very good test cases manually. I almost always write table-driven tests, so that's a good starting point. Agents can easily generate mocks, but I usually give them a framework to do so. Invest early in coming up with sane interfaces, and force the agent to use those to generate mock structs with deterministic code generators such as moq. Agents like to solve problems in creative ways. Without guardrails, they usually produce working but hard-to-read assertions. GPT 5.4+ likes to write tests that basically "grep" a line of business logic, checking character by character for the existence of a function and calling it a unit test. Weird. I usually instruct the agent to use the testify package to keep test cases standardized and easy to read. Once the preparations are done, you can just ask the agent to look at your reference test implementation and write future tests based on it. Take a look at this simple go project: package main import "errors" type calculator struct{} func c calculator add a int, b int int, error { if a == 0 || b == 0 { return 0, errors.New "values cannot be 0" } return a + b, nil } func c calculator subtract a int, b int int, error { return a - b, nil } func addWrapper a int, b int, c calculator int, error { return c.add a, b } func subtractWrapper a int, b int, c calculator int, error { return c.subtract a, b } func main { addWrapper 10, 10, calculator{} } I prompted Claude 4.8 1M context High-thinking with the following text: Please take a look at blogpost/main.go. I would like you to write unit tests for addWrapper function. The test should have at least 3 different inputs. Write idiomatic go. The agent actually produced table-driver tests without being asked, and the tests themselves are fine, but we could make them more readable. I'm not happy with how the table-driven tests look, and those assertions could be much easier on the eyes. I'm also not happy with the direct use of calculator , which could change at any time, for example by introducing dependencies. See the agent's first attempt package main import "testing" func TestAddWrapper t testing.T { tests := struct { name string a int b int want int wantErr bool }{ { name: "two positive values", a: 10, b: 10, want: 20, }, { name: "positive and negative values", a: 5, b: -3, want: 2, }, { name: "zero operand returns error", a: 0, b: 7, wantErr: true, }, } for , tt := range tests { t.Run tt.name, func t testing.T { got, err := addWrapper tt.a, tt.b, calculator{} if tt.wantErr && err == nil { t.Fatalf "addWrapper %d, %d error = %v, want %v", tt.a, tt.b, err, tt.wantErr } if got = tt.want { t.Errorf "addWrapper %d, %d = %d, want %d", tt.a, tt.b, got, tt.want } } } } Lets make some changes to the main file, and create a reference unit test implementation: js package main import "errors" var errZeroValues = errors.New "values cannot be 0" //go:generate moq -stub -out calculator mocks.go . Calculator type Calculator interface { add a int, b int int, error subtract a int, b int int, error } type calculator struct{} func c calculator add a int, b int int, error { if a == 0 || b == 0 { return 0, errZeroValues } return a + b, nil } func c calculator subtract a int, b int int, error { return a - b, nil } func addWrapper a int, b int, c Calculator int, error { return c.add a, b } func subtractWrapper a int, b int, c Calculator int, error { return c.subtract a, b } I introduced a sentinel error called errZeroValues . I also added a Calculator interface, along with a generator that writes a mock to calculator mocks.go . Now let's write our reference test implementation: package main import "testing" "github.com/stretchr/testify/require" var mockCalculator = &CalculatorMock{ subtractFunc: func a, b int int, error { return a - b, nil }, } func TestSubtractWrapper t testing.T { tcs := map string struct { a int b int want int wantErr error }{ "positive and positive values": { a: 10, b: 10, want: 0, }, "positive and negative values": { a: 10, b: -10, want: 20, }, "negative and negative values": { a: -10, b: -10, want: 0, }, "zero operands": { a: 0, b: 0, want: 0, }, } for name, tc := range tcs { t.Run name, func t testing.T { got, err := subtractWrapper tc.a, tc.b, mockCalculator require.NoError t, err require.Equal t, tc.want, got } } } Lets see what the next prompt produces: Please re-read blogpost/main.go, and the reference test implementation in blogpost/main test.go. I would like you to follow the practices seen in the TestSubtractWrapper reference unit test, and write TestAddtWrapper. The agent extended the mock and used require, just as asked. Please ignore the fact that the mock and the real implementation are the same; this is just a small example. This shows that agents are capable of producing sane unit tests, given some investment in crafting good examples. See the agent's revised attempt package main import "testing" "github.com/stretchr/testify/require" var mockCalculator = &CalculatorMock{ addFunc: func a, b int int, error { if a == 0 || b == 0 { return 0, errZeroValues } return a + b, nil }, subtractFunc: func a, b int int, error { return a - b, nil }, } func TestSubtractWrapper t testing.T { tcs := map string struct { a int b int want int wantErr error }{ "positive and positive values": { a: 10, b: 10, want: 0, }, "positive and negative values": { a: 10, b: -10, want: 20, }, "negative and negative values": { a: -10, b: -10, want: 0, }, "zero operands": { a: 0, b: 0, want: 0, }, } for name, tc := range tcs { t.Run name, func t testing.T { got, err := subtractWrapper tc.a, tc.b, mockCalculator require.NoError t, err require.Equal t, tc.want, got } } } func TestAddWrapper t testing.T { tcs := map string struct { a int b int want int wantErr error }{ "positive and positive values": { a: 10, b: 10, want: 20, }, "positive and negative values": { a: 10, b: -10, want: 0, }, "negative and negative values": { a: -10, b: -10, want: -20, }, "zero operands": { a: 0, b: 0, want: 0, wantErr: errZeroValues, }, } for name, tc := range tcs { t.Run name, func t testing.T { got, err := addWrapper tc.a, tc.b, mockCalculator if tc.wantErr = nil { require.ErrorIs t, err, tc.wantErr return } require.NoError t, err require.Equal t, tc.want, got } } } Agents.md and Claude.md I often think my Markdown files might look naive to agentic power users, but I like to keep things simple, and it seems to work for me. This is the basic structure I usually start with: AGENTS