RFD 100: In-Repo Ticket Tracking
- Status: Implemented
- Category: Process
- Authors: Jean Mertz git@jeanmertz.com
- Date: 2026-08-05
- Extends: RFD 001, RFD 041
Summary
This document establishes tickets: lightweight work items tracked as markdown files in the repository, alongside RFDs. Tickets carry comments, sit on a kanban board, and can be imported from GitHub issues. They take over the work that RFDs have been absorbing but were never meant to hold.
Motivation
The RFD process works for what it was designed for. It has also become the only place to write anything down, and the corpus shows it: 98 published RFDs and 58 drafts, with one-pagers sitting next to real designs. RFD 068 is 585 words. RFD 065 is 4638. Two drafts share a title.
When "significant architectural change" is the bar and there is no lower rung, everything climbs to the top rung.
GitHub Issues are the obvious lower rung, and they stay — outside contributors should not have to clone a repository to file a bug. But assistants grep the repository constantly and rarely search GitHub, so issues sit outside the context that actually gets read. They are also absent from git grep and detached from the commits that resolve them.
What a Ticket Is
A ticket is a unit of work: a bug, a feature, a chore. It records what needs doing and the discussion around it.
| RFD | Ticket | |
|---|---|---|
| Question | What should we build, and why? | What needs doing? |
| Contains | A design and its rationale | A description and a discussion |
| Lifetime | Permanent record | Closed when the work is done |
| Deletable | No | Yes |
Write a ticket when the work is clear enough to start, an RFD when it needs a design first. The signal is whether there is a decision to argue about: "the tool call header misaligns below 80 columns" is a ticket even if the fix is subtle; "how should tool output be bounded?" is an RFD even if the implementation is trivial.
A ticket whose discussion turns into a disagreement about approach is promoted to an RFD draft — the pressure valve that keeps design work out of tickets without pushing task work back into RFDs.
Unlike RFDs, tickets can be deleted — a ticket carrying false claims or imported spam is removed outright, so that nothing reads it as true. Ticket numbers are never reused: the next id comes from a monotonic counter, not from the highest file on disk. References to a deleted ticket render as dangling.
Ticket Format
A ticket is a single markdown file at docs/ticket/NNNN-slug.md, where NNNN is a zero-padded sequential number. The canonical reference form is T0042; tooling accepts 42, 042, and T42.
Description and full discussion live in that one file, so reading a ticket is one fs_read_file, one cat, or one page on the website.
# T0042: Tool call header misaligned
- **Status**: In Progress
- **Kind**: Bug
- **Authors**: Jean Mertz <git@jeanmertz.com>
- **Date**: 2026-08-05
- **Implements**: 095
The header renders one column left of the body when `style.parameters` is
`function_call` and the terminal is narrower than 80 columns.
## Comments
-----
- **From**: jean
- **Date**: 2026-08-05T14:03:11Z
Reproduced at 72 columns. Not at 80.
-----
- **From**: jp
- **Date**: 2026-08-05T14:31:02Z
- **Re**: T0042#1
The wrap calculation in `jp_printer` uses the pre-indent width:
```rust
let available = width - indent;
```
-----Metadata
Tickets use the same - **Key**: Value idiom as RFDs, for the same reason: it parses with a three-line regex and renders as visible content.
| Field | Required | Values |
|---|---|---|
Status | yes | Todo, In Progress, Done |
Kind | yes | Bug, Feature, Chore |
Authors | yes | |
Date | yes | YYYY-MM-DD |
Blocked by | no | T0041, or free text |
Implements | no | The RFD this ticket implements |
Promoted to | no | The RFD this ticket became |
GitHub | no | #123, set by import |
There is no close date. The ticket_* tooling reads it from git history, so the file never carries a timestamp that can go stale.
Comments
Comments are separated by a line of five or more dashes and open with their own metadata block. From is a short handle (jean, jp); imported GitHub comments use gh:username.
The structure of the file is append-only: comment blocks are never removed or reordered, and a new comment is a pure append at EOF. Block contents are freely editable, as on GitHub — descriptions grow as information arrives, and authors fix their own comments. Deleting a comment replaces its body with a marker naming the reason (deleted, off-topic, spam) and keeps the block.
Replies are recorded as - **Re**: T0042#1, referencing a comment by its 1-based position. Storage stays flat; the website and terminal render the thread. Positions are not identifiers: two branches appending concurrently can shift one, and a reply then points at the wrong comment. Accepted rather than solved; stable ids are the fix if it ever bites.
Parsing
A comment boundary is a line of five or more dashes at column zero, followed by a blank line, followed by a metadata block containing both From and Date. Content inside fenced code blocks is skipped. Everything before the first boundary is the description.
The ## Comments heading is decorative. Tooling inserts it before the first comment and never consults it when parsing, so a heading of that name anywhere in a description or comment body is harmless.
The Board
Tickets sit on a kanban board with three columns:
Todo → In Progress → DoneColumns are stages of work; priority is the vertical order within a column. Todo is therefore the prioritized work queue, read top-down, and it holds triaged and untriaged work alike — an imported bug report that may not be a bug still belongs there.
Blocked is a metadata field rendered as a badge, not a column: a blocked ticket is still at whatever stage it reached, and a column would lose that.
Done is ordered like any other column, newest first, and the board view shows only the head of it. Full history lives in the ticket index.
Two pieces of state, kept apart:
- Status lives in the ticket file. It is semantic and belongs to the ticket.
- Order within a column lives in a single board file. It is relational and belongs to no individual ticket.
Encoding rank as a ticket field would mean every drag rewrites a dozen files. Same split the RFD priority board already uses.
Relationship to RFDs
Tickets and RFDs are separate document kinds with separate numbering and separate lifecycles. They meet in three places.
RFD phases become tickets, optionally
When an RFD is accepted, rfd-promote offers to turn each phase of its Implementation Plan into a ticket carrying Implements: 045. A prompt, not an automatic step — the same shape as the existing tracking-issue prompt — because acceptance records an agreed direction, not a commitment to start building. Whoever accepts the prompt reviews the resulting tickets before they land.
This replaces the GitHub tracking issue from RFD 041 §2, and one ticket now serves as both the tracking item and the prioritized work item.
Tickets promote to RFDs
A ticket whose discussion turns into a design question is promoted: the tooling seeds an RFD draft from the ticket, and the ticket closes as Done with Promoted to pointing at the draft. The work item is finished; the work moved.
The RFD board keeps ordering; it loses inDevelopment
The inDevelopment flag leaves docs/rfd/priority.json. "Someone is currently writing code for this" is a property of a work item, not of a design document — it was on the RFD board only because tickets did not exist. It becomes derived: an RFD is in development when any ticket carrying Implements: NNN sits in the In Progress column. Derived, not synced, so there is nothing to keep in step.
RFD priority ordering stays. The two boards rank different things:
| RFD priority board | Ticket board | |
|---|---|---|
| Question | What should we design next? | What are we building now? |
| Holds | Ranked design pipeline | Work items that exist |
Most of the RFD backlog is not a work item — nobody has said "this needs doing" about it beyond "we should think about it" — so a ticket per RFD would flood the board.
GitHub Issues
GitHub Issues remain the front door for outside contributors. They are imported as tickets, one way only.
Each import replaces the ticket's content — title, description, comments — wholesale, and never touches the metadata block. GitHub owns what was written on GitHub; the repository owns Status, Kind, Blocked by, and Implements, so an imported ticket can be triaged and moved across the board without the next import undoing it.
Imported tickets are read-only for discussion: replies go on GitHub and arrive on the next import, so there is no divergence to reconcile and nothing is ever written back.
Imported text is untrusted input. The site compiles markdown as Vue, so imported bodies are escaped before they reach the working tree and are rendered as data, never as page source.
Alternatives
Git objects instead of files. PR #872 proposes issues as per-writer, append-only operation logs under a dedicated ref namespace, folded deterministically so concurrent edits never conflict. It is the stronger data model and the wrong fit here: refs are unreachable by fs_read_file and git grep, invisible to the site build, and absent from pull-request diffs — the three properties this design exists to provide. Its conflict-freedom solves offline editing across replicas that cannot see each other, which is not this project. The cost accepted instead is merge conflicts on the board file and on concurrent appends.
It also keeps priority in the store, where a re-rank rewrites one object per issue rather than one board file, and tombstones rather than deletes.
Non-Goals
- Labels, assignees, milestones. Add them when the absence hurts.
- A search DSL.
just ticket-grepover markdown files. - Write-back to GitHub.
- Notifications. The board is the notification.
- Stable comment ids. Positions are good enough until they are not.
- Replacing RFDs. This lowers the bar for entry; it does not remove the top rung.
Risks and Open Questions
Two boards may be one too many. If the RFD board goes stale once tickets exist, fold RFDs into the ticket board as cards and retire
priority.json.Ticket sprawl replaces RFD sprawl. Mitigated only by tickets being deletable and closed tickets leaving the board — neither true of RFDs.
Agent comment volume. A ticket with forty comments is a large file to read. The answer is closing tickets sooner, not splitting the file.
Implementation Plan
This document fixes the file format, the vocabulary, the board semantics, and the relationship to RFDs. How the tooling gets there is not fixed.
- Format and lifecycle.
docs/ticket/, the comment parser, and recipes to create, comment, close, and list. - Assistant tools.
ticket_*tools through.jp/mcp/tools/. The point of the exercise. - Website and board. A
/ticket/index and the kanban board. RetireinDevelopmentand derive it from ticket state. - GitHub import. One-way, issues and comments.
- RFD seams. The phase-ticket prompt and ticket-to-RFD promotion, which needs an idempotent retry story. Amend RFD 001 and RFD 041, including 041's stale claim that tracking issues are created at Discussion, and add a TIP to 041 once this RFD holds a permanent number.
- Shared substrate. Extract the duplicated metadata parsing, id allocation, and board state — if the duplication has proven itself.