Readme Driven AI Development: Bringing Structure to Vibe Coding
Imagine we could harness the power of “Vibe Coding” without the nagging sensation that we’re building on shifting sands. What if we could blend AI’s creative potential with the solid engineering practices we’ve trusted for decades?
The term “Vibe coding” is gaining tremendous popularity these days. It’s now confirmed that Large Language Models (LLMs) possess genuine programming capabilities, despite their foundational mechanism of prediction rather than true understanding.
Many of us are increasingly using LLMs to solve problems with small context — getting documentation subsets tailored to specific needs, generating code snippets we can adapt, reviewing code, or writing tests. But as traditional software engineers, we might struggle to envision building an entire project through “vibes,” sending countless prompts to rebuild our software repeatedly until bugs are fixed or features work as intended.
While we can certainly imagine an LLM generating a complete project, we still need the essential tools that have served us throughout our careers: versioning and structure.
How Can We Structure a Full AI-Assisted Project? 📝
A few days ago, I recalled an insightful article written 15 years ago by Tom Preston-Werner: Readme Driven Development.
The concept is simple: Write the Readme first.
This approach perfectly aligns with how we can effectively build a complete project with AI assistance.
The Process: 🔄
- Create our code repository
- Write our specifications in a README (instead of sending them directly to our coding agent)
- Once the initial set of specs is written, prompt the LLM to generate the first version of our project in an empty directory of our repository
- Test the code — if it works well, commit and version it
- Treat this AI-generated code like an artifact — similar to a lock file in package management that we commit to our repository (think uv.lock or Cargo.lock)
Then the iterative process begins:
- Add new specifications to our README
- Run the LLM by providing the current code version and the README diff since the first version
- The LLM makes modifications based on our updated requirements
- Test the changes — if satisfactory, version them
- Repeat this cycle ♻️
This structured approach combines the power of AI with the reliability of traditional software engineering practices, giving us the best of both worlds.
Example of such a README file
# Merge Request Lister
A command-line tool written in Rust to list merge requests (MRs) from a GitLab repository.
## 📋 Specifications
This tool must:
- Fetch merge requests from a specified GitLab project using the GitLab REST API
- Support filtering by MR state: `open`, `closed`, or `merged`
- Display relevant MR details: title, author, state, and URL
- Support both text and JSON output formats
## 🛠️ Tech Requirements
- **Language**: Rust
- **Dependencies**:
- reqwest – for making HTTP requests
- serde and serde_json – for JSON serialization/deserialization
- clap – for CLI argument parsing
## 🧪 Test Specification
- Unit tests for:
- Parsing GitLab API responses
- Building correct API URLs based on input
- Integration tests using:
- A public GitLab test project or mocked API endpoints
- Error case testing:
- Invalid token
- Network failure
- Empty or malformed responses
- Coverage goal: ≥80% of core logic
## 🏗️ Build
To build the project locally:
git clone https://your.repo.url/merge-request-lister.git
cd merge-request-lister
cargo build --release
This will generate the binary at `./target/release/merge-request-lister`.
## ▶️ Usage
You can run the tool by passing the required arguments via the command line:
./target/release/merge-request-lister --project-id <PROJECT_ID> --token <GITLAB_API_TOKEN>
### Command-Line Arguments
| Flag | Description | Required |
|----------------|-------------------------------------------------|----------|
| `--project-id` | GitLab project ID | ✅ |
| `--token` | GitLab personal access token | ✅ |
| `--state` | Filter by MR state: `open`, `closed`, `merged` | ❌ (default: `open`) |
| `--output` | Output format: `text` or `json` | ❌ (default: `text`) |
### Example
./merge-request-lister \
--project-id 123456 \
--token glpat-xxxxxxxxxxxxxxxxxxxx \
--state merged \
--output json
### Example Output (text)
#234 Fix login bug by @alice (open)
#235 Add CI config by @bob (merged)
### Example Output (json)
[
{
"id": 234,
"title": "Fix login bug",
"author": "alice",
"state": "open",
"url": "https://gitlab.com/yourproject/merge_requests/234"
}
]
## ⚙️ Optional Configuration via Environment Variables
Instead of passing values via flags, you can use environment variables:
export GITLAB_TOKEN="your_token_here"
export GITLAB_API_URL="https://gitlab.com/api/v4"
> CLI arguments always override environment variables if both are provided.
## 📁 Project Structure
merge-request-lister/
├── src/
│ ├── main.rs # CLI and entry point
│ └── gitlab.rs # GitLab API logic
├── tests/
│ └── integration.rs # Integration tests
├── Cargo.toml
└── README.md
## 📌 Notes for LLM
- Follow idiomatic Rust practices (modular code, `Result`, proper error handling)
- Include command-line help with `clap`'s derive macro
- Display errors clearly to the user (e.g., "Invalid token", "No MRs found")
- Write clear, maintainable, and tested codemark
By Thomas Martin
Follow me or comment