Skip to main content

3 posts tagged with "developer-experience"

View All Tags

TDD Was Solving the Agent Problem Before Agents Existed

· 4 min read
Danish Javed
Software Engineer

The first time I set an agent loose on a real codebase, it ran out of context before it had done anything useful. That's a clarifying experience.

The repository wasn't exotic — a Python monorepo with shared libraries and some infrastructure code. I drew a diagram to understand what was happening. A rectangle for the full context window; blocks for what was already consumed just from loading the codebase: directory tree, CLAUDE.md, relevant modules, config, dependencies. The bar was more than half full before the agent had read a single line of task context or seen a single error message.

Stop Arguing With Your Terminal About Python Versions

· 4 min read
Danish Javed
Software Engineer

Project setup should be boring. Not in a "this is beneath me" way — in a "this takes thirty seconds and I never think about it" way.

Most of the time, it isn't.

You clone a repo you haven't touched in six months. The README says "install Python 3.11". You have 3.12. Something breaks. You remember there's a .python-version file somewhere, or maybe a requires-python in pyproject.toml. You spend twenty minutes figuring out which version manager you're even supposed to be using for this project before you've written a single line of code.

This isn't a Python problem. Terraform has tfenv. Node has nvm or volta or .nvmrc. Every language brings its own version manager, its own config file format, its own way of silently using the wrong version. And that's before you even get to figuring out how to run things — is it make test? ./scripts/test.sh? Some npm script buried in a package.json? Nobody knows. You ask Slack.

I got tired of this and started using mise. It's a single tool that handles both problems: pinned runtimes and discoverable tasks, for any language, in one file.

A Python service looks like this:

[tools]
python = "3.12.3"

[tasks.test]
description = "Run the test suite"
run = "pytest"

[tasks.verify]
description = "Run all checks before pushing"
depends = ["test", "build"]

Run mise install and you get exactly that Python version. Run mise tasks and you see everything the project knows how to do. Run mise run verify before pushing. That's it.

The part I find most satisfying is that mise run <task> becomes a stable interface that hides whatever's behind it. I had a project that needed a custom k6 binary with SSE support for load testing a streaming API. Building it requires Go and a tool called xk6, which most people have never heard of. With mise, that's just:

[tools]
go = "1.22.3"

[tasks.build]
description = "Build k6 with xk6-sse extension"
run = "xk6 build --with github.com/phymbert/xk6-sse"

Now mise run build works for everyone — the developer who knows what xk6 is, the one who doesn't, and the CI job. Nobody has to know what's behind it. When I added another extension later, I changed one line. The interface didn't move.

Speaking of CI — this is where the real payoff is. A GitHub Actions workflow for a mise project looks like:

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v4
- run: mise run verify

mise-action reads mise.toml, installs the pinned versions, and puts them on PATH. Then mise run verify runs the exact same thing you run locally. No separate version install steps. No drift between what CI checks and what you check. This is the thing that makes it worth the setup cost — CI and local are no longer two separate mental models.

The one thing mise can't do is install itself. You need it on the machine before any of this works. I solve that with Chezmoi, a dotfile manager that runs once on a fresh machine. A run_once_install-mise.sh script does the bootstrap:

#!/bin/sh
curl https://mise.run | sh

Then the shell hook in ~/.zshrc (also managed by Chezmoi) activates mise per directory:

eval "$(mise activate zsh)"

Chezmoi sets up the machine, mise sets up each project. Neither knows the other exists. You go from a blank laptop to a running project without reading a setup guide — which is the point.

It won't fix an undocumented deployment process or a service that can't run locally. It encodes what's already known. And if your team is already settled on nvm + make for a single-language, single-runtime project, the migration cost might not be worth it. The value really compounds when you're working across multiple projects or switching between them regularly — which, in my experience, is most of the time.


mise replaces pyenv, nvm, rbenv, tfenv, asdf, and most other per-language version managers. If you're on asdf already, migration is painless — mise reads .tool-versions files natively.

Journey To The Centre Of The Stack

· 5 min read

I first wrote this post in 2020 after spending several weeks containerising a legacy application I hadn't built and didn't fully understand. The experience was mostly archaeology — reading old config files, tracing hardcoded paths, figuring out what half a dozen processes actually did before touching anything. By the time I had a working Docker image, I'd earned it.

I'm updating it now because the journey has changed, and I think it's worth being honest about how.