The most valuable developer skill right now isn't writing more code faster. It's learning unfamiliar codebases, building context that guides decisions, planning strategic approaches to problems, and shipping production code with confidence.
I recently added .env file support to xc, a Markdown-based task runner written in Go. The codebase was completely unfamiliar. I'm not a Go expert. But in 2.5 hours, I went from zero knowledge to a production-ready pull request with 84% test coverage and zero bugs in manual testing.
Here's what's different: I didn't write a single line of code. Not one. AI wrote everything—tests, implementation, integration, documentation. My role was entirely different: I questioned, I planned, I directed, I reviewed. I read the code, but I didn't write it.
This isn't another "I asked ChatGPT to build an app" story. This is about the skills that separate developers who use AI as a force multiplier from those who just ask it to generate code. It's about onboarding fast, documenting strategically, planning thoroughly, directing execution, and reviewing confidently. The code writing? That's handled.
Complete .ai/ folder in the working fork:
github.com/sudoish/xc/tree/ai-context/.ai
Production-ready PR:
github.com/joerdav/xc/pull/167
The .ai/ folder lives in a separate ai-context branch so it doesn't clutter the main codebase but remains available for reference and iteration.
Why This Matters
Most AI coding demos show you the magic: "I asked ChatGPT to build X and it worked!" They skip the parts that actually matter for professional development: How do you onboard to a codebase you've never seen? How do you make architectural decisions when you don't understand the patterns yet? How do you ensure your code is production-ready when AI helped write it?
These are the skills that matter now. Code generation is table stakes. What matters is context building, strategic planning, and confident execution.
Here's the project: xc, a task runner that reads tasks from Markdown files. About 5,000 lines of Go. Completely unfamiliar to me. The feature request was straightforward: add .env file support (Issue #162). In 2.5 hours, using free AI models and a structured approach, I went from knowing nothing about the codebase to a merged pull request.
The difference wasn't better prompts. It was better process.
The Actual Workflow: What I Did vs What AI Did
Here's the honest breakdown of who did what. I didn't write a single line of code myself. That's not the valuable work anymore.
What I did:
- Explored the codebase with AI — Asked questions, challenged its understanding, verified explanations against the actual code
- Built the
.ai/ structure — Wrote context docs, ADRs, rules, and implementation specs based on my growing understanding
- Questioned the strategy — Evaluated alternatives, captured trade-offs, made architectural decisions
- Directed the implementation — "Follow the spec. Implement test 1. Now test 2." Each step validated before moving forward
- Reviewed iteratively — Asked AI to review the code, digested its findings, confirmed issues, asked it to fix them. Repeated multiple times
- Final deep review — Read through the entire PR on GitHub, verified everything made sense, marked ready for review
What AI did:
- Answered my questions — Explained architecture, pointed me to relevant files, clarified patterns
- Wrote all the code — Tests, implementation, integration, everything
- Found its own bugs — Self-review caught 5 issues before I even looked at the code
- Fixed the issues — Applied fixes based on its own review findings
- Followed the plan — Implemented exactly what the spec described, in the order specified
What we did together:
- Built understanding through conversation
- Validated each step before proceeding
- Caught subtle bugs through TDD
- Created production-ready code with high confidence
The key insight: I never typed code. I read it, reviewed it, directed changes to it. But I didn't write it. My value was in understanding, planning, and judgment. AI's value was in execution and self-checking. This is the new division of labor.
The Four Skills
This walkthrough demonstrates four skills that matter more than code generation:
Skill 1: Rapid Onboarding. Learning an unfamiliar codebase fast by building structured context instead of reading every file. The .ai/ folder captures architecture, patterns, and limitations in a way both humans and AI can reference.
Skill 2: Strategic Documentation. Building documentation that guides development, not just records it. Architecture Decision Records (ADRs) capture the "why" behind choices, evaluate alternatives, and create a shared understanding before code is written.
Skill 3: Systematic Planning. Breaking down problems into testable steps. Each test defines expected behavior. Each implementation proves the behavior works. Each commit tells part of the story. No guessing, no hoping.
Skill 4: Confident Execution. Shipping code you trust because you've tested it thoroughly, reviewed it critically, and validated it works in real scenarios. AI can help write code, but you own the quality.
These skills work regardless of the AI tool you use. They work with free models. They work on unfamiliar codebases.
The Feature Request
First, a quick primer on how xc works: it's a task runner that reads tasks directly from your README.md (or any markdown file). Tasks are defined as markdown headings with code blocks. When you run xc test, it finds the ## test heading in your README and executes the code block beneath it. The genius is that your documentation is your task runner, so they never get out of sync.
A user opened Issue #162 asking for .env file support. They wanted to use the same set of tasks for different environments without cluttering the Markdown with environment variables.
Before the feature, you'd have to write this in your README.md:
## deploy
Deploy to production.
Env: DATABASE_URL=postgres://prod/db, API_KEY=secret123, ENV=production
kubectl apply -f deployment.yaml
Then run with xc deploy.
After the feature, your README stays clean:
## deploy
Deploy to production.
kubectl apply -f deployment.yaml
The environment variables live in a separate .env file:
DATABASE_URL=postgres://prod/db
API_KEY=secret123
ENV=production
You still run the same command, but now the credentials are managed in .env instead of cluttering your documentation.
Simple ask, but the implementation requires real decisions. When do you load the files? What about overrides? How do you handle security? What about backward compatibility?
The .ai/ Structure: Context as Code
Before writing any code, I created a structured context folder. This turned out to be the key to working with AI effectively. It's not about better prompts, it's about better structure.
The folder looks like this:
.ai/
├── agents.md # Who's working on what
├── context.md # Project overview, architecture
├── architecture/
│ ├── decisions.md # Current design patterns
│ └── adrs/
│ └── 001-dotenv-support.md # Design decisions for this feature
├── rules/
│ ├── code-style.md # Go conventions
│ ├── testing.md # TDD workflow
│ └── commits.md # Commit message format
└── tasks/
└── 001-dotenv-implementation.md # Step-by-step plan
Important: This structure is an investment, not overhead you repeat for every feature. You build it once during your first feature, then leverage it for every feature after. The context.md, architecture/decisions.md, and rules/ files rarely change. Each new feature just adds a new ADR (like 002-api-caching.md) and a new task spec (like 002-api-caching-implementation.md).
Think of it like setting up your development environment. The initial setup takes time, but every feature after that is faster because the foundation exists.
Each file serves a specific purpose. The context.md file becomes AI's memory. It explains what xc does, how it's architected with its cmd/, models/, run/, and parser/ packages, what key behaviors exist like dependencies and environment handling, and what current limitations we're working around. Every time I ask AI a question, this context gets included automatically.
The rules/testing.md file defines the TDD workflow we follow: write a failing test first (red), write minimal code to make it pass (green), clean up without changing behavior (refactor), then commit. This keeps both me and AI honest. No skipping tests. No shortcuts.
The real gem is adrs/001-dotenv-support.md, the Architecture Decision Record. This is where design happens. It's not "build me a feature," it's "here's why we chose this approach." We decided to load .env files at application startup rather than per-task, to support .env.local overrides, to skip world-readable files for security, and to add CLI flags like --env-file and --no-env. We considered alternatives like per-task loading (rejected as too complex) and requiring an explicit flag (rejected as too much friction). This ADR becomes the source of truth. When AI suggests something different, I can just say "check the ADR."
The living documentation principle: As the codebase evolves, so does the .ai/ folder. When you add a new feature, you write a new ADR (002, 003, etc.). When architecture changes, you update architecture/decisions.md or add a new ADR explaining the change. When patterns emerge, you document them. The folder grows with the project, but the structure stays the same. Each feature builds on the understanding captured before it.
This means the second feature is faster than the first. The third is faster than the second. The documentation compounds.
The Task Spec: Planning Before Coding
Before writing any code, I created tasks/001-dotenv-implementation.md, a step-by-step plan for implementing the feature. This isn't a project management document. It's a development spec that breaks the feature into TDD cycles.
The spec listed each test I needed to write, what behavior it should verify, and the expected implementation. Test for file not found. Test for loading valid env. Test for .env.local overrides. Test for security checks. Each one became a TDD cycle.
This is what makes AI effective. Without the spec, I'd be asking AI "what should I do next?" every five minutes. With the spec, I'm asking "implement the next test according to the plan." The spec keeps development focused and systematic. It's the difference between wandering and following a map.
For your second feature, you write a new spec. For your third, another one. The format is consistent, but each spec is tailored to its feature. This is the work that makes development fast and confident.
The TDD Flow: Red → Green → Refactor → Commit
Here's where the real work happens. Each test defines acceptance criteria for exactly what needs to be built.
Cycle 1: Valid .env should load variables
First behavior: if a .env file exists and contains KEY=value pairs, those should be loaded into the environment. Test written, test failed (red)—no loader existed yet. Implementation added using the godotenv library (green). Test passed. Committed with "load env vars from dotenv file".
Cycle 2: .env.local should override .env
Expected behavior: if both .env and .env.local exist, and both define the same variable, the .env.local value wins. This is crucial for local development where you want to override defaults without modifying the base file. Test written, test failed initially because I was using the wrong function, fixed the implementation, test passed. Committed.
Cycle 3: World-readable files should be skipped
Security requirement: if a .env file has permissions that allow other users to read it (like chmod 644), skip loading it and warn the user. This prevents accidentally exposing secrets. Test created, test failed (secrets were being loaded), added permission check, test passed. Committed.
This rhythm of define → test → implement → verify → commit creates a clean history. When I looked at the final commit log, I could see exactly how the feature evolved: add godotenv dependency, load env vars from dotenv file, support dotenv local overrides, add security check for world readable files, integrate dotenv loading into main, add env file cli flags. Thirteen commits total, each one atomic and meaningful. Each commit is a story about one specific behavior being added.
The Review Process
After the implementation was done, I did a deep review of my own code. I found five issues that needed fixing.
Issue 1: Test Isolation (Critical)
Tests were modifying the global environment without properly restoring it. If a test set TEST_KEY=value, the cleanup would delete it, but what if that key already existed before the test ran? The cleanup wasn't restoring the original value, just removing the key. This breaks parallel test execution because tests can interfere with each other.
The fix: create a helper function that saves the current state of environment variables before the test runs, then restores that exact state (including whether the variable existed at all) when the test completes. Now tests are safe to run in parallel. Committed with "add test environment isolation helper".
Issue 2: Windows Test Bug (Critical)
One test needed to skip execution on Windows because file permission models are different. I had written the check incorrectly, reading from an environment variable instead of the language's built-in constant. This would break Windows CI. Small mistake, but important. Fixed and committed with "fix windows test skip to use runtime goos".
Issue 3: Early Exit Timing
The .env loading was happening even for commands like --help and --version, which meant users could see security warnings when just checking the version. Moved the loading to happen after those early exits. Performance optimization and better user experience. Committed.
Issue 4: Error Context
When file operations failed, errors didn't indicate which file caused the problem. Added context wrapping so errors show the specific file path. Makes debugging much easier. Committed.
Issue 5: Test Coverage
One helper function didn't have its own test. Added coverage to bring the total to 84%. Committed.
Each issue got its own fix, its own verification, its own commit. The same disciplined process for fixes that I used for features.
The Manual Testing
Code works in tests, but does it work for real users? I installed my version and created a test project to verify everything worked end-to-end.
I created a .env file with some variables, created a .env.local file that overrode some of them, and made sure the permissions were correct with chmod 600. Then I added a task to my README.md to verify the variables were loaded:
In README.md:
## check-env
Check loaded environment variables.
echo "Environment: $ENV"
echo "Database: $DATABASE_URL"
echo "API Key: ${API_KEY:0:8}..."
When I ran xc check-env, I saw exactly what I expected. The xc command read the task from the README...
Read the full post at sudoish