← Back to articles

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: 🔄

  1. Create our code repository
  2. Write our specifications in a README (instead of sending them directly to our coding agent)
  3. 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
  4. Test the code — if it works well, commit and version it
  5. 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:

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