Preparing your repo for AI development: Lessons from Gram
Ryan Albert
November 20, 2025 - 5 min read
Engineering
When we started building Gram , our goal wasn’t specifically to make the codebase “AI-friendly.” Initially, we just wanted a codebase where any developer could jump in and be productive quickly, no tribal knowledge required.
Turns out, what makes a codebase easy for new developers also makes it easy for AI agents.
Think of AI as a developer with broad experience, but shallow context”
The best mental model we’ve found: AI coding agents are developers with a breadth of experience who lack knowledge depth, specifically in the context of your project. They excel at pattern matching but they don’t build on context overtime the same way a developer would.
This reframe changes how you think about structuring code. If a new developer would struggle to find where API schemas are defined or how to generate migrations, a coding agent will too.
Contract-first, generated code everywhere
The core principle behind Gram’s structure is contract-first design with extensive code generation. We use:
To add a new API endpoint, you define it in the design package :
var _ = Service("projects", func() { Description("Manages projects in Gram.") Method("listProjects", func() { Description("List all projects for an organization.") Payload(ListProjectsPayload) Result(ListProjectsResult) HTTP(func() { GET("/rpc/projects.list") Param("organization_id") }) Meta("openapi:operationId", "listProjects") Meta("openapi:extension:x-speakeasy-name-override", "list") Meta("openapi:extension:x-speakeasy-react-hook", `{"name": "ListProjects"}`) })})
Run mise gen:goa-server to generate the server stubs and OpenAPI spec.
For the database, it works similarly. When adding a new database table, you write a schema in server/database/schema.sql:
CREATE TABLE IF NOT EXISTS projects ( id uuid NOT NULL DEFAULT generate_uuidv7(), name TEXT NOT NULL CHECK (name <> '' AND CHAR_LENGTH(name) <= 40), slug TEXT NOT NULL CHECK (slug <> '' AND CHAR_LENGTH(slug) <= 40), organization_id TEXT NOT NULL, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), updated_at timestamptz NOT NULL DEFAULT clock_timestamp(), deleted_at timestamptz, deleted boolean NOT NULL GENERATED ALWAYS AS (deleted_at IS NOT NULL) stored, CONSTRAINT projects_pkey PRIMARY KEY (id));
Instead of writing migration files manually, run mise db:diff add_organization_metadata and Atlas generates the migration SQL for you.
Then write your queries using SQLC syntax in module-specific queries.sql files:
-- name: ListProjectsByOrganization :manySELECT *FROM projectsWHERE organization_id = @organization_id AND deleted IS FALSEORDER BY id ASC;
Run mise gen:sqlc-server and SQLC generates type-safe Go repository that you can connect to your server implementation. No room for deviation, no need to look at five different examples to figure out “the right way.”
Lastly, running mise gen:sdk updates the Speakeasy SDK, complete with React Query hooks for use in the client.
Mise: A recipe book for agents
Mise is a polyglot development environment manager and task runner. Beyond managing tool versions (Go, Node.js, pnpm), it provides a discoverable task system that has become central to how we work.
All our development tasks live in .mise-tasks as executable scripts organized hierarchically:
Agents can run mise tasks to see what’s available, then execute mise db:migrate or mise gen:sqlc-server. Progressive disclosure at its best, the agent discovers commands as needed rather than needing everything upfront.
Everything in one place: The monorepo advantage
Gram is a true monorepo: server, web app, CLI, and our NPM functions framework all live together in speakeasy-api/gram.
This matters because when an agent needs to make changes that touch multiple surfaces (say, adding a new CLI feature that calls a server endpoint), it can see both sides of the interaction. No context switching between repos, no hunting for the right version of a shared type.
Zero to productive in one command
We have a mise zero command that sets up everything: installs dependencies, pulls Docker images, starts local services. One command, and you’re ready to develop.
This isn’t revolutionary, it’s just thoughtful automation. But it eliminates a whole class of “getting started” friction that would otherwise require back-and-forth with an agent or digging through README files.
Keep agent instructions minimal
We maintain a CLAUDE.md (symlinked to AGENTS.md, GEMINI.md, etc.) with basic onboarding info. But we keep it lean. Rather than documenting every workflow, we point to general patterns and where a developer would find more information:
## Available commandsAll development commands are in Mise. Run `mise tasks` to see availablecommands, or execute them directly:- `mise db:*` - Database operations- `mise gen:*` - Code generation- `mise start:*` - Start services locally
The goal is day-one context, that a developer or a coding agent can explore from there.
What works well, what doesn’t
AI agents are great at:
Formulaic work (adding new API endpoints, database tables)
Bug investigation when you can describe the general area of the problem
Changes that generally are heavy on pattern matching
They struggle with:
Designing net new patterns or architectures
Understanding subtle business logic without extensive context
Making opinionated decisions about design
Our approach: let AI handle the boilerplate, while developers own the design and review. For simple things we describe the structure we want, and the agent writes it out faster than we could. This saves us time to work on the complex.
The real win
Building Gram this way didn’t just make it easier to use AI. It made the codebase easier to work with, period. New team members ramp up faster. Code reviews focus on logic, not style. Everyone benefits.
If you’re building a new project and want it to work well with AI tools, don’t overthink it. Focus on clear patterns, good discoverability, and minimizing cognitive load. The AI-friendliness will follow.
Want to see these principles in action? Check out the Gram repo or try building an MCP server with Gram Functions.