How We Build Features
For Kristine, and for Alex, Diana, and Nicholas. Written 2026-07-27, ahead of the 2026-07-28 kickoff. Nothing here requires you to write code.
Why this document exists
You already know how to run product development. You do it every day at Reffer. What is different here is not the management practice, it is who does the building: most of the code on this project is written by AI agents working from written specifications, with Jonny reviewing and merging.
That changes exactly one thing, and it is a big one. The quality of what gets built is set by the quality of what you write. Not by how detailed it is. By how clearly it states what should be true when the work is done, and how someone would check.
Everything below is in service of that. The process stages exist so ideas do not get lost. The consideration pool exists so good ideas get stress-tested instead of quietly dying. The GitHub mapping exists so you never have to touch GitHub if you do not want to. But the part that actually determines whether this works is section 2, on how to write a feature. If you read one section, read that one.
1. The path an idea takes
Seven stages. An idea can die at any of them, and dying early is a good outcome, not a failed one.
Stage 1: Design concept
What it is. A drawing, a wireframe, a screenshot with arrows on it, a paragraph in Slack, a panel in your Command Center export. Anything that says "the product should do this."
Who does it. Anyone. You, Alex, Nicholas, Jonny, Limore, a client on a call.
What done looks like. The artifact exists somewhere findable. That is the whole bar. Do not polish at this stage.
What happens to it. It gets deposited into intake. In practice that means a folder with a short manifest: what it is, who made it, what problem it was trying to solve. Your 2026-07-22 Command Center export is the largest deposit we have.
Stage 2: Feature spec
What it is. The concept broken into individual features, each written up so someone else could build it without asking you questions.
Who does it. Whoever made the concept, working with an analyst or an agent that drafts the first pass. You review and correct. The correcting is where your value is; the drafting is cheap.
What done looks like. Each feature answers four questions:
- Where does the information come from? (which pipeline, which run, which file)
- How does it show up? (a table, a card, a queue, a chart)
- Who is reading it, and in what moment? (Thomas at 07:00 on a Monday, not "the user")
- What can they do about it? (one of five actions, listed in section 5)
Those four questions are the whole system. We call a feature that answers all four a route. A feature missing any one of them is a picture, not a spec, and it does not get built yet. Section 2 shows you how to write the rest of the spec around those four.
Why four and not more. Because those are the four things nobody can infer. Everything else about a feature can be reasoned out from them, and reasoning it out is what the agent is good at.
Stage 3: Consideration pool
What it is. A holding area where every candidate feature from every source sits together and gets compared before anything is built.
Who does it. Jonny adjudicates. You, Alex, and Nicholas argue for your candidates.
What done looks like. Each candidate has a status: approved for a sprint, held for a later sprint, merged into another candidate, parked pending a decision, or killed with a reason recorded.
Section 3 explains why this stage exists and why it is not a gate on your work.
Stage 4: Jira issue
What it is. An approved candidate becomes a real ticket in your sprint board, with a user story, acceptance criteria, an owner, and an estimate if you use them.
Who does it. You. This is your surface and your call on how it is structured.
What done looks like. The ticket carries the user story and acceptance criteria in the description field, is linked to its GitHub issue (by hand for now, see section 4), and has an owner who has accepted it.
One convention worth keeping. A route declaration and a user story are the same thing said two ways. Given the four fields from stage 2, the user story writes itself:
As Thomas, at 07:00 on Monday with ten minutes before my first call, I need the competitor movements that crossed their own baseline, shown as a stack rank with my row highlighted and the movers beside it, so that I can hand the one that matters to the right person before the call starts.
Reader plus moment gives you the as and the at. The source pipeline plus its filter gives you the I need. The form gives you the shown as. The action gives you the so that. You are not writing two documents.
Stage 5: Prototype
What it is. A cheap version that proves the idea works before anyone builds the real one. Sometimes an Obsidian view over real notes, sometimes a single HTML file, sometimes a wireframe with live data pasted in.
Who does it. Whoever owns the ticket, often with an agent doing the mechanical part.
What done looks like. A screenshot in the ticket, and an honest note about what surprised you. Prototypes exist to surface the thing nobody thought of. If a prototype teaches you nothing, it was probably too faithful to the spec.
Why this stage is not optional. It is where you find out that the data does not have the shape you assumed. That discovery costs an afternoon here and costs a sprint later.
Stage 6: Review
What it is. The built work gets read by a human before it becomes part of the product.
Who does it. Jonny reviews and merges all code. You review whether the feature does what you asked. Those are two different reviews and both happen.
What done looks like. Automated checks pass (section 4 lists them), Jonny approves the code, and you confirm the acceptance criteria are actually met by looking at the running thing, not by reading a summary of it.
The one review habit worth building. When you check acceptance criteria, check them against the real surface, not against a description. "The queue sorts by severity" is checkable in ten seconds by looking. Take the ten seconds.
Stage 7: Production
What it is. Merged, deployed, live.
Who does it. Jonny merges and deploys. Deployment is gated: nothing ships to a client-facing URL without a verified deploy check.
What done looks like. The ticket closes with an outcome note comparing what shipped to the intended outcome you wrote in stage 2. If they differ, the difference is written down. That note is what makes the next spec better.
2. How to write a feature so an agent can build it
This is the heart of the guide.
The old habit and why it fails here
The instinct with a new team, especially one you cannot look over the shoulder of, is to be exhaustive. Enumerate every step. Specify every click. Leave nothing to interpretation.
That instinct is correct when you are directing people who lack context and will do exactly what you say. It backfires with an agent, for two reasons.
First, you get your steps, including the wrong ones. An agent will follow an enumerated sequence faithfully, including the step you wrote at 11pm that does not quite work. It has no reason to question step 6 because you did not tell it what step 6 was for.
Second, you leave it nothing to check its own work against. An agent given a list of steps knows it is done when it has performed the steps. An agent given an outcome knows it is done when the outcome is true, which is a much higher bar and one it can test itself against repeatedly.
The four things a spec needs
Write these four. Skip almost everything else.
1. The outcome
What is true when this ships, stated as an observable fact about the world. Not "build a snooze button." Something like "an item Thomas has dealt with does not appear again on Tuesday."
The test for a good outcome statement: could a person who has never seen the product tell whether it is true by using the product? If yes, it is an outcome. If they would have to read the code, it is an implementation detail.
2. The reason
Why this matters, to whom, in what moment. One or two sentences.
This is the field people skip and it is the one that does the most work. When the agent hits a decision you did not anticipate, and it will hit several, the reason is the only thing it has to decide with. A spec with a reason produces sensible choices in the gaps. A spec without one produces coin flips.
3. The local exceptions
The specific places where the obvious answer is wrong. This is where your expertise actually lives and it is the part nobody else can supply.
Examples of real local exceptions from our own work:
- A brand new company should not show a score of 50. It should show "not analyzed yet." Fifty looks like a measurement and it is not one.
- A rank change column with only one run of data should say "first run," not "0." Zero is a claim that nothing moved.
- A tile that cannot be clicked into is worse than no tile.
None of those are guessable. All of them are things you know from having looked at a lot of dashboards. Write them down and the agent will honor them; leave them out and you will get the obvious wrong answer, competently built.
4. How you will know it is right
Three to six checks a person could perform without reading code. These become your acceptance criteria.
Good ones are specific and falsifiable: "snoozing an item until tomorrow removes it from the queue and from the ticker in the same render, and it comes back the next morning." Bad ones are moods: "the queue feels responsive."
At least one check should be a negative check, something the feature must refuse to do. "An item with no supporting evidence does not render at all." Negative checks catch a whole class of quiet failures that positive checks miss.
What to leave out
- How to build it. Which library, which pattern, which file. Not your call and not useful.
- Click-by-click paths. Say what the user needs to accomplish and how few steps it should take. Do not choreograph it.
- Pixel specifics, unless they are load-bearing. "The reader's own row is visually distinguished" is a requirement. "12px left margin" is not, unless it is.
- Anything you are guessing at. If you do not know whether snooze should be per-person or shared, write the open question instead of a guess. A named open question gets answered. A guess gets built.
Worked example: the attention queue snooze
This is KG-CC-ATTENTION from the Command Center export, story S-01 in the feature register.
Before
Attention Panel: add snooze
Add a snooze button to each row of the attention panel. When clicked it should open a dropdown with the options: 1 hour, 5 hours, 1 day, end of week, next week. When the user picks one, hide the row from the list. Store the snooze in local storage with the item id and the wake time. On page load, check local storage and filter out any items whose wake time has not passed. Add a small clock icon to the button. Also update the ticker count at the top so it matches. The snooze button should be next to the complete button and the hide button on the right side of the row.
That is a competent ticket and it is what most of us would write. Here is what goes wrong with it.
It specifies local storage, which means Thomas's snoozes vanish when he opens the terminal on his laptop instead of his desktop. Nobody decided that; it arrived as a side effect of a sentence written in passing. It says "update the ticker count so it matches," which is a repair instruction rather than a statement that the two should never disagree, so the next thing added to the ticker will break it again. It never says who snooze is for, so when the agent hits the question of whether Thomas snoozing an item hides it from the analyst too, it has nothing to go on. And there is no way to tell whether the finished work is right, other than clicking around and forming an impression.
After
Attention queue: an item Thomas has handled stops asking for attention
Outcome. When Thomas deals with something in the attention queue, it stops competing for his attention everywhere in the product, immediately, and stays gone until he asked for it back. Three ways to deal with something: it is done, it is not for me, or it is not for me right now.
Reason. Thomas opens this at 07:00 on a Monday with ten minutes before his first call. The queue is only useful if it shrinks as he works it. A queue that shows him the same six items on Tuesday that it showed him on Monday is a report, and he already has reports.
Local exceptions. - Snooze state belongs to the person, not to the item. Thomas snoozing something must not hide it from the analyst, and it must follow Thomas to a different machine. - "End of week" and "next week" mean Thomas's calendar week, not a fixed number of hours. - If an item's underlying score moves again while it is snoozed, it comes back early. He snoozed the old situation, not the entity. - The count in the ticker and the length of the queue are the same number by construction. They are not two numbers we keep in agreement.
How we will know it is right. - Handling an item removes it from the queue and from the ticker in the same render, with no refresh. - Snoozing until tomorrow, closing the browser, and opening the terminal on a different machine: the item is still hidden. - The analyst's view of the same queue is unaffected by anything Thomas did. - A snoozed item whose score changes by more than the noise floor reappears before its wake time, and says why it came back. - Nothing in the interface can put the ticker count and the queue length out of agreement.
Open question for Jonny. Where per-person state is stored. There is no user-preference store yet, and this is the first feature that needs one. It may be worth building that first rather than inside this ticket.
What changed
The after version is not longer by much. It is different in kind.
It never says local storage, so the storage question surfaces as an open decision instead of arriving as an accident. It states the ticker and the queue as one number rather than two that must be reconciled, which removes a whole category of future bug. It adds the early-wake rule, which is a real product insight that only comes from thinking about what snooze is for, and which no agent would invent. And every one of the five checks can be performed by a person in under a minute.
It also does something the before version could not: it tells the agent what to do when it hits something neither of you thought about. Confronted with an unanticipated case, an agent holding "this item should stop competing for Thomas's attention until he asked for it back" makes a decent decision. An agent holding "hide the row from the list" makes a coin flip.
One more habit: write the open question
Every spec you write will have at least one thing you are not sure about. Write it down under its own heading and address it to a person. That single habit prevents more rework than anything else in this document, because an unmarked guess is indistinguishable from a decision once it is in the ticket.
3. The consideration pool
What it is
Every candidate feature, from every source, sitting in one list before anything gets built. Right now the pool holds four sources:
- Your Command Center export, converted into 12 epics and 20 stories (
kg-wireframe-feature-register-2026-07-27.md) - Six presentation forms from the 2026-07-23 creative session
- Eleven forms already built and shipped inside report HTML, which need extracting into reusable pieces
- The 2026-07-23 research sweep on terminal design
Your export is the largest and most complete input by a wide margin. That is exactly why it goes into the pool rather than around it.
Why it exists, honestly
Not as a gate on design work. As the only place where four things happen that cannot happen anywhere else.
Duplicates get merged before they get built twice. Your rank table and the stack rank already shipping in the weekly report are close to the same thing. Caught in the pool, that is one feature. Caught after the sprint, that is two implementations that disagree by January.
Conflicts with prior decisions surface with dates attached. Your export uses a dark theme. On 2026-07-23 we settled on a cream field with no dark mode. That is not a criticism of your design and it is not automatically final. It is a decision with a date that someone now has to either uphold or reverse deliberately. Both are fine. Silently building against both is not.
Missing pipelines get named before they are promised. Roughly ten panels in the export cannot say where their data comes from, because that data does not exist yet. Better to know that in a room on 2026-07-28 than in a demo.
Ideas that lose still get recorded. This is the part that matters most and the part pools usually get wrong. When a candidate is killed, the reason is written down and the candidate stays visible. Your evidence drawer sat in our registry for weeks as a line reading "should exist, does not." Your export gave it a concrete shape and it is now sprint-1 work. That only happened because the idea had somewhere to sit while it waited.
What the pool asks of you
One thing: when you propose a feature, say where its data comes from. If you do not know, say you do not know. Both answers are useful. A guess is not.
What you get back
A written verdict on every candidate, with a reason. Not silence, and not "we will get to it."
4. How Jira connects to the code
First, what is actually built
None of it, yet. That is on purpose.
The monorepo and the GitHub organization have not been created. Jonny is holding off until this round of work is finished, so the repository gets laid out correctly the first time instead of being restructured three weeks in. The feature register, the design primitive spec and this guide are the inputs that make it possible to set it up properly.
So read this section as the thing we are building, not the thing you are walking into. It is closer to a specification than a manual. Where something below does not exist yet, it says so.
We will also roll this out slowly. Nobody needs to have their bearings on day one.
| Piece | State |
|---|---|
Report Studio repository, production branch main |
Live today |
| The monorepo | Sprint 1 |
| The Jira project | Sprint 1 |
| Jira and GitHub linked to each other | Sprint 1 |
| Grammar gate running on pull requests | Decided 2026-07-17, moves to CI when CI exists |
Route gate (route-gate.py) |
Not written yet |
| Tickets changing state on their own | Later, once the rest is steady |
Who decides what
Jonny decides everything that moves through the system. Which pull requests get accepted, which features get edited, what ships and when. That is not a formality. He is the one who can see what a given change does to the rest of the product, and a change that looks small in one place routinely is not.
Decisions get made in conversation. Disagreeing in a ticket comment is useful and welcome. The decision still lands in one place.
Everyone else proposes. You write the feature so the intent is unambiguous, argue for it, and then it goes through the gate like everything else.
What you do in Jira is the part you already know how to do from previous feature work: write the story, keep the acceptance criteria honest, answer questions on the ticket, and say what depends on what. That is the whole job here, and it is the part that determines whether the built thing matches what you meant.
You do not need a GitHub account to do that. You will have one, and it is worth ten minutes of walkthrough, but nothing in the daily work requires it.
The chain we are building, one link at a time
Jira issue to GitHub issue. The plan is a link, so a story you write shows up where the code work happens carrying your user story and acceptance criteria verbatim. That text is what the developer and the agent actually read, which is why section 2 matters as much as it does. Until the link exists, we copy it across by hand, which works fine at the volume we are starting with.
GitHub issue to branch. A branch is a private copy of the whole codebase where one feature gets built without touching what is live. One branch per feature, named after the issue. Created by whoever picks up the work, or by an agent.
Branch to pull request. A pull request is the built work presented for review: exactly what changed, line by line, with the issue it belongs to.
Pull request to checks. Once CI is standing, three checks run before a person looks:
- Grammar gate. Fails the pull request if generated client-facing prose breaks the voice rules. This one already exists as a script and was decided on 2026-07-17 to move into CI.
- Route gate. Would fail the pull request if a feature references a data source, a form or a reader that is not registered. This is the enforcement point behind "a form with no channel is a sketch." It has not been written. Until it is, that check is a person reading the register.
- Tests. Fails the pull request if something that used to work no longer does.
A red check is not a problem. It means the system caught something before a person had to.
Pull request to review to merge. Jonny reads the change and merges. Your separate review, whether the feature does what you asked, happens on the running product and is a different event from the code being correct.
Merge to deploy. Deployment is manual and gated, and stays that way. Merged is not live, deliberately.
What you do, and what we intend to automate later
Right now the right-hand column is mostly people. That is fine at this size, and it is worth feeling the manual version before automating it, because that is how you find out which steps actually matter.
| You do | Eventually automatic |
|---|---|
| Write the story with outcome, reason, exceptions, checks | GitHub issue created and linked |
| Prioritise and sequence the sprint | Branch named from the issue |
| Set the owner | Ticket moves to In Progress on first commit |
| Answer questions on the ticket | Ticket moves to In Review when the pull request opens |
| Confirm acceptance on the running product | Checks run, ticket moves to Done on merge |
| Say when something should ship | Nothing. Deploys stay a human decision permanently. |
Two things worth knowing
Comments on a ticket are part of the record. When you answer a question in a Jira comment, that answer reaches whoever is doing the work and stays attached to the feature. Answering in Slack does not. This is the single most valuable habit to keep, and it costs nothing.
"Blocked by" links are load-bearing. When you mark a story blocked by another, people respect it now and the tooling will respect it later. Several stories in the register were blocked by a single decision about score dimensions, settled on 2026-07-27. Marking that explicitly is what stops three people building against two different vocabularies.
5. Backed, partial, unbacked
The idea in one paragraph
A feature is backed when there is a real pipeline producing the data it needs, running today, on a schedule, writing a file someone can point at. It is unbacked when the data would have to come from somewhere that does not exist. Unbacked features look exactly like backed features in a wireframe, which is the entire problem.
How to tell
Ask one question: which pipeline produces this, how often does it run, and where does it write?
We have twelve registered pipelines, CH-01 through CH-12. Some run nightly, some weekly, some when someone triggers them. If a feature's data comes from one of the twelve, it is backed. If the honest answer is "from a CRM we do not have" or "from a forecasting engine nobody has built," it is unbacked.
The three labels used in the feature register:
| Label | Meaning | Example from your export |
|---|---|---|
| BACKED | Live pipeline, running today | Stack rank. It ships every Monday already. |
| PARTIAL | Some inputs live, a specific piece missing | Competitor change feed. Most signal types are produced nightly; two collectors are missing and both are small. |
| UNBACKED | Named capability does not exist | Action dispatch. There is nowhere to send an assignment, because the task system was retired on 2026-07-25 with no replacement. |
Why this matters more than it looks
An unbacked feature built anyway becomes a screen full of confident-looking numbers with nothing behind them. It demos beautifully. It is also the fastest way to lose a client's trust, because the moment they ask where a number came from and the answer is "it is placeholder," every other number on the screen is in question too.
Roughly 60% of the Command Center export is unbacked. That is not a flaw in the design work. It is a design that ran ahead of the data, which is a normal and useful thing for a design to do. It becomes a problem only if we build it as though the data were there.
What to do when something is unbacked
Four options, in order of how often they are right.
1. Park it and name the missing pipeline. The feature stays in the pool with a note: "needs a CRM connection." When that connection exists, the feature is already specified and ready. Most of the revenue operations epic belongs here.
2. Cut it down to the backed part. Often a feature is 80% backed and 20% aspiration. The portfolio map is real; the movement trails need historical snapshots we store but cannot yet query as a series. Ship the map, spec the trails separately, and be honest about which is which.
3. Build the pipeline first, as its own ticket. If the missing piece is small and clearly worth having, it becomes work in its own right. The two missing collectors behind the change feed are a day of work each and would move that feature from partial to backed.
4. Build it as an explicit placeholder. Rarely correct, and only under one condition: the interface says plainly that no data feeds it. "No pipeline feeds this yet" is an acceptable thing for a panel to say. A fabricated number is not, ever, including in a demo.
What we do not do is build the panel with realistic-looking sample data and remember to fix it later.
6. Quick reference
The seven stages
| Stage | Who | Done when |
|---|---|---|
| 1. Design concept | Anyone | The artifact exists somewhere findable |
| 2. Feature spec | Author with an analyst or agent | Source, form, reader, and action are all named |
| 3. Consideration pool | Jonny adjudicates | Written verdict with a reason |
| 4. Jira issue | KG | User story plus checks, owner accepted |
| 5. Prototype | Ticket owner | Screenshot in the ticket plus what surprised you |
| 6. Review | Jonny for code, KG for the feature | Checks green, acceptance confirmed on the running thing |
| 7. Production | Jonny deploys | Ticket closed with an outcome note |
Every feature answers four questions
- Where does the information come from? (which pipeline)
- How does it show up? (which form)
- Who reads it, in what moment? (which person, what time of day, what are they about to do)
- What can they do about it? (one of the five actions)
Missing any one of the four means it is a picture, not a spec.
The five actions
| Action | What happens |
|---|---|
review-and-assign |
Creates a named handoff to a person |
arm-rule |
The system watches a number and flags it when it crosses a line |
open-evidence |
Shows what the claim rests on. Ends there. |
commission-research |
Opens a research assignment |
run-simulation |
Tests a message against a modeled audience |
If a feature does not resolve to one of these five, say so out loud. It might mean we need a sixth, and that is a real conversation. It usually means the feature is a display and not a decision.
Writing a spec: the four fields
- Outcome. What is true when it ships, checkable by a person using the product.
- Reason. Who needs it, in what moment, and why. This is what the agent decides with when it hits something you did not anticipate.
- Local exceptions. Where the obvious answer is wrong. Your expertise lives here.
- How we will know it is right. Three to six checks a person can perform, at least one of them negative.
Plus: the open question, addressed to a named person.
Leave out
Implementation choices. Click-by-click paths. Pixel values that are not load-bearing. Anything you are guessing at, which becomes an open question instead.
Backed or unbacked
Ask: which pipeline produces this, how often does it run, where does it write?
- Backed: a live pipeline. Build it.
- Partial: name the missing piece. Often it is small.
- Unbacked: park it and name the missing pipeline, cut it to the backed part, build the pipeline as its own ticket, or say plainly in the interface that nothing feeds it yet.
Never fabricated sample data, including in demos.
Jira to code
Jira issue → GitHub issue → branch → pull request → checks → Jonny reviews and merges → ticket closes → deploy is a separate human decision.
None of the arrows are automatic yet and the repository does not exist yet, both on purpose. We are defining the process before building it, and rolling it out gradually. Section 4 says what is live and what is not.
Jonny decides every change that moves through. Everyone else proposes, argues in the ticket, and the decision lands in one place.
Your part: write the story, sequence the sprint, answer questions on the ticket, confirm acceptance on the running product.
Three habits worth more than the rest
- Answer questions on the ticket, not in Slack. The ticket is the record.
- Write the open question rather than guessing. A named question gets answered; a guess gets built.
- Check acceptance by looking at the real thing, not by reading a summary of it.
Related: Dev Sprint 1 index, sprint-plan-2026-07-27.