Vibe Coding with Guardrails: Tickets as a Contract with the Coding Agent
How versioned Markdown tickets turn spontaneous prompts into a controllable development process, where the approach helps and where it reaches its limits.
Vibe Coding is remarkably good at reducing the distance between an idea and running code to a few minutes. It is much worse at remembering two weeks later why a particular decision was made, which constraints applied and what the implementation was deliberately not supposed to touch.
For a small prototype, that is not a problem yet. You describe what you want in the chat, let the agent create a few files and correct whatever does not fit. The conversation serves as requirement, planning document and documentation at the same time.
As the project grows, that works increasingly poorly. Conversations get longer, new sessions start without the old context, and different models interpret the same requirement differently. At some point the agent adds a button and, while it is there, also changes the API structure, replaces a library and cleans up three modules even though none of that was requested.
The problem is not necessarily model quality. What is missing is a durable, reviewable work order.
My solution is to keep tickets directly in the repository. Not merely a task list in Jira and not a prompt that disappears when the session ends, but versioned Markdown files that move through the development process together with the code.
That sounds like the opposite of Vibe Coding. The small amount of friction is exactly the point.
Chat is not project memory
A coding agent can process a lot of context. That does not make a chat history a good specification.
In a typical conversation, requirements emerge incrementally:
Add a CSV export.
Later:
It obviously has to respect the current filters.
Then:
But only for data the user is allowed to see in the UI.
And eventually:
Please do not introduce another CSV library; we already have a utility for that.
A human can roughly reconstruct the intended result from that conversation. To an agent, these are four messages with different priorities. It may already have implemented a solution based on the first message. The relevant information may have fallen outside the loaded context window by now. Or the work may continue in a new session entirely.
A ticket consolidates that discussion into one unambiguous work order. It is not a copy of the prompt, but its cleaned-up and reviewed form.
Or more briefly: the ticket is the prompt that survives the session.
A Kanban board made from directories
The basic model is intentionally simple:
tickets/
├── open/
├── in-progress/
└── done/
A new ticket starts under tickets/open/. When implementation begins, the file moves to tickets/in-progress/. After review and merge it ends up under tickets/done/.
Status therefore exists not only as a field inside the document but also in the file path. Git records when a ticket was created, started, changed and completed.
Every ticket gets its own feature branch:
feature/APP-42-csv-export
That branch contains both the code and the corresponding version of the ticket. If the requirement is refined during implementation, that change remains visible in Git history as well.
This is an important difference from an external ticketing system. A coding agent needs no additional API, credentials or specialised integration. It reads the ticket with the same tools it uses for the rest of the source tree.
What belongs in a ticket
A useful ticket has to give the agent enough context without pre-writing every block of code. Above all, it should answer five questions:
- Which problem has to be solved?
- How can we tell that it has been solved?
- What is explicitly out of scope?
- Which technical guardrails are binding?
- How will the implementation be verified?
A simple ticket can look like this:
# APP-42: Gefilterte Bestellungen als CSV exportieren
**Status:** open
**Type:** feature
**Created:** 2026-07-28
**Branch:** —
**Estimated effort:** medium
## Motivation
Mitarbeiter im Support müssen die aktuell angezeigten Bestellungen
regelmässig für eine externe Auswertung exportieren. Derzeit kopieren
sie die Daten manuell aus der Oberfläche.
Der Export soll exakt den Datenbestand enthalten, den der Benutzer
mit den aktuellen Filtern, der Sortierung und seinen Berechtigungen
in der Oberfläche sieht.
## Akzeptanzkriterien
- [ ] In der Ergebnisansicht gibt es eine Aktion „Als CSV exportieren“.
- [ ] Der Export berücksichtigt die aktiven Filter.
- [ ] Der Export verwendet die aktuelle Sortierung.
- [ ] Es werden nur Datensätze exportiert, auf die der Benutzer Zugriff hat.
- [ ] Die CSV-Datei ist UTF-8-kodiert und enthält eine stabile Kopfzeile.
- [ ] Bei einem fehlgeschlagenen Export erscheint eine verständliche Fehlermeldung.
- [ ] Backend- und Frontend-Tests werden ergänzt.
- [ ] Der Changelog wird aktualisiert.
## Out of scope
- Geplante oder wiederkehrende Exporte
- Versand der Datei per E-Mail
- XLSX- oder PDF-Export
- Änderungen am Berechtigungskonzept
## Technische Leitplanken
- Die bestehende Berechtigungsprüfung im OrderQueryService muss verwendet werden.
- Berechtigungen dürfen nicht im Frontend nachgebaut werden.
- Für die CSV-Erzeugung ist die vorhandene CsvWriter-Utility zu verwenden.
- Der Export muss auch bei grossen Ergebnismengen ohne vollständiges Laden
aller Datensätze in den Arbeitsspeicher funktionieren.
## Designfragen
- Soll der Dateiname das aktuelle Datum enthalten?
- Benötigt Excel in der Zielumgebung ein UTF-8-BOM?
## Relevante Dateien
- `src/orders/OrderQueryService.ts`
- `src/orders/OrderController.ts`
- `src/ui/orders/OrderList.tsx`
- `tests/orders/`
The motivation describes the problem from the user’s perspective. The acceptance criteria define observable behaviour. The Out of scope section prevents the agent from turning a CSV export into a general-purpose reporting system.
The technical guardrails contain only decisions that genuinely matter to the project. They do not prescribe the name of every function. The agent should still be able to find an implementation that fits the codebase.
Acceptance criteria instead of vague goals
The most important part of a ticket is not the technical guidance but its acceptance criteria.
A sentence such as “The export should work reliably” sounds reasonable but cannot be verified. An agent can describe almost any implementation as reliable.
Concrete statements are better:
- [ ] Bei HTTP 429 wird der Wert aus dem Retry-After-Header berücksichtigt.
- [ ] Ohne Retry-After-Header wird ein exponentielles Backoff verwendet.
- [ ] Nach drei erfolglosen Versuchen wird kein weiterer Request gesendet.
- [ ] Der Fehler enthält den Namen des aufgerufenen Dienstes.
Now it is clear which cases have to be implemented and tested.
Acceptance criteria should describe behaviour rather than the desired implementation wherever possible. “Create method retryRequest() in file X” is usually not an acceptance criterion. “Stop the call after three failed attempts” is.
That separation matters because otherwise a ticket quickly turns into source code written as prose. This not only removes useful freedom from the agent, it may also preserve a solution that merely sounded plausible when the ticket was written.
A bad ticket can produce a very precisely implemented misunderstanding.
Deliberately separate planning from implementation
The ticket process does not begin with writing code.
In the first phase, the agent may inspect the repository, read existing architectural decisions and create a ticket draft. It should identify open questions, name affected components and record potential risks. Product-code changes are not part of this phase.
I review the ticket afterwards.
Is the motivation correct? Are the acceptance criteria complete? Has the agent presented an assumption as fact? Is the scope still small enough for a manageable pull request?
Only after these questions are settled does implementation begin. That can even happen in a new session. The new agent does not need the complete previous conversation. It reads the project rules, the ticket and the relevant code.
This separation prevents an agent from creating facts in code while the requirement is still being discussed imprecisely. At the same time, it forces me to formulate the requirement completely once before several hundred lines of code appear.
The ticket lifecycle
In practice, the flow looks roughly like this.
1. Create the ticket
The ticket is created under tickets/open/. At this point it contains neither a branch nor implementation details that can only emerge during the work.
2. Review the ticket
Before implementation starts, the ticket is read manually. Open design questions either have to be resolved or explicitly marked as a decision the implementer may make.
The Out of scope section matters particularly here. Coding agents are often extremely helpful. That becomes a problem when they add refactorings, dependency upgrades or architectural changes alongside the requested work.
3. Create a feature branch
Work always starts from the current main branch. The agent creates a branch containing the ticket number and a short name.
It then moves the ticket to tickets/in-progress/ and writes the branch into the metadata.
4. Implement only the ticket scope
The agent may document adjacent issues but must not fix them automatically. A newly discovered follow-up problem either becomes a separate ticket or is discussed with me first.
That does more than prevent unnecessary changes. It also makes the later review considerably easier because the diff actually corresponds to the task being reviewed.
5. Update tests and documentation
Tests are part of the ticket, not optional cleanup at the end.
If a ticket changes public behaviour, the corresponding documentation has to change as well. If it introduces a durable architectural rule, that rule does not belong only in the ticket; it belongs in architecture documentation or an ADR.
A ticket describes one change. It must not become the only place where permanently relevant knowledge exists.
6. Stop and report
After implementation, the agent should not update the main branch by itself. It stops on the feature branch and reports:
- the branch,
- the latest commit,
- the tests that were run,
- known limitations,
- remaining risks.
The merge remains a deliberate human decision.
For me, this is one of the most important guardrails in the entire process. The agent may work largely autonomously inside the branch, but it does not get to decide that its own work has been reviewed sufficiently.
7. Review, merge and close
After review, the branch is merged. Only then does the ticket move to tickets/done/.
That keeps “code written” and “change complete” as two distinct states.
Three layers of context
Repository-local tickets work particularly well when information is distributed cleanly across several layers.
A file such as AGENTS.md contains permanent project rules: architectural principles, test commands, prohibited shortcuts, naming conventions and the policy for external libraries.
The ticket contains only the current work order. It describes motivation, scope, acceptance criteria and the guardrails relevant to this particular change.
Tests and CI provide the executable control layer. They verify at least part of what the ticket promises.
A changelog can document the visible outcome, while ADRs preserve larger architectural decisions.
This separation avoids two extremes: one enormous global rulebook that has to be loaded for every task, and tickets that explain the complete project architecture again for every change.
What works well about the approach
The biggest advantage is durable context. A new session or a different model can take over without requiring me to reconstruct the previous conversation. The ticket exists on the branch and describes the state to which the code relates.
Scope control matters just as much. A clear Out of scope section is surprisingly effective against well-intentioned side effects. The agent may still point out adjacent problems, but it cannot simply fold them into the same change.
Reviews become easier as well. Instead of judging a diff only by general impression, I can compare it against concrete acceptance criteria. If an error case is missing or a constraint was ignored, that can be identified directly.
Traceability is another benefit. Ticket, branch, commits, tests and documentation changes visibly belong together. If a criterion changes during implementation, that change remains in Git history too.
The system is largely tool-independent. Whether the code is changed with Claude Code, Codex, an IDE agent or a local model makes no difference to the ticket format. Almost every tool can read Markdown.
For solo projects and small teams, the simplicity is useful in its own right. There is no additional board, no automation and no synchronisation between systems.
Finally, keeping merge manual leaves responsibility with the human. The agent receives a large amount of freedom inside its branch but no automatic approval for production code.
Where the system reaches its limits
The model is simple, and that also makes inconsistency simple.
A file can already live under tickets/done/ while still containing Status: in-progress. Acceptance criteria may remain unchecked. Git does not prevent any of that. Without additional validation, directory, metadata and checkboxes are merely conventions.
A small CI script can improve the situation. It can verify, for example, that tickets under done/ have the correct status, contain no open criteria and have a corresponding changelog entry. Even that does not solve the problem completely. A checked box is not proof that the criterion has actually been satisfied.
The second drawback is overhead. A two-line change does not justify a multi-page ticket. Forcing every trivial correction through the same process quickly creates private bureaucracy.
There needs to be a sensible lower threshold. An obvious typo or small configuration correction can be handled directly. Once a change touches several files, introduces new behaviour or contains a design decision, a ticket usually pays for itself.
Ticket inflation is another risk. Coding agents can write impressively detailed implementation plans. A large ticket can therefore grow to several hundred lines, including proposed classes, method signatures and example code.
More text does not automatically mean more clarity. Long tickets consume context, become difficult to review and freeze solutions before the existing code has been understood fully.
I therefore treat only three kinds of statements as binding:
- the desired behaviour,
- explicit architectural rules,
- clearly named technical constraints.
An implementation plan is initially a proposal. If the work reveals that another solution fits the codebase better, the agent may deviate from it. The deviation has to be justified and made visible, though.
It is also problematic when the same agent writes the ticket, implements it and then evaluates its own work as complete. That creates a risk of a closed confirmation loop. A misunderstanding from the planning phase is translated into code and then declared correct against criteria written from the same misunderstanding.
The human review of the ticket before implementation is therefore not a formality. It is the most important review in the entire flow.
For larger teams, file-based Kanban also lacks many features of conventional ticket systems: assignments, notifications, dependencies, prioritisation, search filters, dashboards and reliable reporting. Once several developers plan and prioritise in parallel, the repository alone becomes difficult to manage.
Possible alternatives
The ticket model is not the best solution for every situation.
Work only in chat
For a spike, throwaway script or very small prototype, a formal workflow is often unnecessary. A direct prompt is faster and keeps the entry barrier low.
The limit is reached once work spans several sessions or decisions need to remain understandable later.
One shared PLAN.md
During the early phase of a project, a central PLAN.md can be more useful than many individual tickets. It works well for a linear MVP build with sequential milestones.
Over time, however, such a file becomes longer and longer. Completed, current and future work mix together. Parallel changes create conflicts more easily and individual tasks have no independent history.
A good moment to switch to tickets is when the initial build is complete and changes increasingly become independent from one another.
GitHub Issues, Linear or Jira
For teams, conventional ticket systems are usually superior. They provide states, ownership, comments, links and automation.
The drawback for coding agents is the separation from the repository. The tool needs access to the external system and has to load the correct ticket explicitly. The description in the issue can also drift away from the actual implementation on the branch.
A useful combination is an external issue for discussion, prioritisation and collaboration plus a compact repository-local specification for the concrete implementation. The two link to one another.
RFCs and ADRs
A normal feature ticket is not enough for larger architectural changes. When several systems, data models or public interfaces are affected, an RFC or ADR should come first.
The architectural decision can then be reviewed independently from implementation. Only afterwards is it split into smaller tickets.
That is slower, but it prevents a coding agent from making a far-reaching system decision inside what appears to be a routine feature.
Tests as the starting point
For clearly reproducible defects, a failing test can be the best specification.
First write a test that exposes the problem. Then let the agent implement the smallest change that makes the test pass.
That is highly effective but cannot express every requirement. User experience, documentation, performance, security and deliberate non-goals are not always captured adequately by one test.
A hybrid is usually the most useful approach
In practice, I combine several layers:
- A ticketing system or simple list handles prioritisation.
- The Markdown ticket in the repository is the concrete work order.
AGENTS.mdcontains permanent project rules.- ADRs document larger decisions.
- Tests and CI verify the implementation.
- The pull request remains the review and approval boundary.
No single tool has to do everything.
The repository ticket is primarily the interface between human and coding agent. It contains exactly the context the agent needs for that change and remains versioned together with the code.
Vibe Coding does not automatically become engineering
A ticket process does not turn a bad requirement into a good one. It does not prevent faulty implementation and it does not replace review.
What it changes is the point at which ambiguity becomes visible.
Without a ticket, ambiguity usually surfaces during or after implementation. With a ticket, at least part of it has to be resolved before the agent starts working.
That costs a few minutes at the beginning. On larger changes, it saves several rounds of corrections, side effects and discussions about what was originally meant.
The agent may still be creative inside the ticket. It can identify existing patterns, choose appropriate abstractions and propose a better solution than the one I had in mind. But it may not silently change the goal, expand the scope or approve its own work.
For me, that is the difference between pure Vibe Coding and AI Engineering: not less speed, but a controlled framework for where speed is useful.