I'm building Higherlevel because I believe that, as coding agents produce vastly more code, faster and at a lower cost, humans will increasingly focus their attention on product behaviour.
We still need to understand what our software does, decide what we want it to do, and evaluate the changes being made to it. Having several agents work on several features at once makes keeping that understanding up to date harder.
Higherlevel is the platform allowing builders, software engineers, and product people to specify, discuss, and review product behaviour using concrete examples and evidence.
Why I'm building Higherlevel
More and more code is generated by LLMs. You can end up with huge pull requests. Splitting them into smaller PRs helps: you can build an understanding of the changes step by step, which can make review easier and faster.
But that understanding still takes time. With plenty of stacked PRs, you also have more moving pieces to keep in sync while remembering what goes where and why. When you find a bug, which PR introduced it? When a review raises an issue, which PR should address it, and what does that fix mean for the others?
If you manually review everything an agent produces, you can still become the bottleneck. Even with smaller PRs, adding more agents can increase the amount of work waiting for your attention.
I see three ways to respond to this.
Accept the bottleneck: You can accept being the bottleneck and keep reviewing every change yourself. That can be a reasonable choice. But you either limit how much work your agents produce or accumulate a growing backlog of unmerged work that still needs to be kept up to date.
Review selected parts: You can choose to review less of the implementation: the public APIs, the documentation, or the tests, for example. You still exercise judgement over the code, but you deliberately leave more of its details to the agents.
Automate code review: Or you can work towards fully automating code review and focus your energy on product behaviour: what the application should do, and whether it actually does it. This is the approach I'm advocating.
I believe code review will eventually become fully automated through a combination of better LLM reviews and deterministic guardrails that enforce invariants. There will also be processes for triaging the resulting comments: which findings block the change, which belong in a separate pull request, and which do not need action.
I'm well aware that implementation details matter. An application built by coding agents can be difficult to maintain or contain security holes. My conviction is that better models, combined with guardrails that are cheap and fast to run frequently, will make more of this implementation work reliable and automatable.
I see this as an opportunity to focus more of our effort on product behaviour.
In some ways, this is already how people without a software engineering background use app builders and coding agents. They describe what they want, try the resulting application, and ask for changes. Much of their attention goes directly to what the product does.
But how does this approach scale as the application becomes more complex and modular? And what happens when you are working on several products at once?
Trying the application after each change can take considerable time. Different roles, permissions, states, and interactions introduce more cases to consider. A change can work in the situation you tried while breaking another workflow you did not think to revisit.
Across several projects, you also have to remember how each product behaves, what recently changed, and what you have already checked. When multiple agents are making changes simultaneously, keeping that understanding up to date becomes a substantial task of its own.
The risk is that review becomes increasingly dependent on what you happen to remember and try. You inspect the most visible changes, follow a few familiar paths, and move on. Other behaviours receive less attention, and it becomes difficult to tell where the gaps are.
I want a way to make this review explicit: describe the behaviours we care about, connect them to evidence, and keep track of what has been checked and what still needs attention. That would also give humans and agents a shared basis for dividing the review work.
Software engineers have long had tools for managing changes to implementation: inspecting changes, commenting on them, requesting revisions, and approving them. I'd like to make that kind of collaboration available around product behaviour.
There are things I value in the tools I already use: GitHub's review process, Notion's flexibility for writing specifications, and Loom's discussions around recordings. But I still have to connect the pieces myself: what we want to change, the evidence of what the product actually does, the feedback, and what we have agreed on. As I delegate more work to agents, I want a shared place where we can follow a product change through that whole process.
With Higherlevel, I want people to be able to describe how their application currently behaves, specify how it should change, examine evidence of the result, and review it with humans and LLMs alike.
What exactly are we changing?
When I write about specifications here, I mean specifications of product behaviour: what happens under particular conditions when someone takes an action.
An implementation specification might describe modules, database tables, or the abstractions to introduce. Those decisions matter, but I expect us to need less detailed human direction over them as coding agents improve. My expectation is that agents will become better at reading an existing system and choosing appropriate abstractions, while operating at a speed that makes following every implementation decision increasingly costly.
We can also enforce certain implementation requirements mechanically. I've built a series of packages called Bylaw for enforcing invariants in a codebase: never hard-delete a soft-deletable record, always index foreign keys, always deterministically order lists of records, and so on.
These checks give us a way to encode requirements once and enforce them repeatedly. That lets us spend more attention on the behaviour we are asking the implementation to produce.
After all, implementation is downstream of product behaviour. A correct implementation of the wrong behaviour is still costly.
For a product change, I want to establish three things:
- How does the product behave today?
- How should it behave after the change?
- What should remain unchanged?
Understanding the current behaviour matters in its own right. Sometimes I want to investigate a bug or understand an unfamiliar part of the application before deciding whether anything should change. I need a way for an agent to communicate what it found, with screenshots, recordings, or other evidence that I can inspect.
Once we decide to make a change, that understanding gives us a starting point. We can describe the intended difference and make its boundaries clear.
I increasingly think of my job less as writing software and more as engineering change: understanding what the product does, deciding how it should evolve, and working with agents to get there.
That still requires getting into the details. If an invitation is valid “through Friday,” whose time zone determines when Friday ends? An agent can choose a reasonable interpretation and implement it correctly, but that choice determines whether someone can still join. I want to understand what was decided and why. If I keep delegating those decisions without examining them, how much of the product do I still understand?
Why specify behaviour through examples?
Specification by example is the process of using concrete examples to discover, communicate, and agree on expected behaviour before implementing it.
I like this approach because examples are often much easier to produce and judge than comprehensive abstract rules.
There is an idea I associate with the French education system: you do not fundamentally understand something if you can only explain it through examples. Being able to express a general definition or rule may demonstrate a deeper understanding, but arriving at that definition can be hard work.
If I asked you to explain what a cat is without using any examples, you would probably have a harder time than if I asked you to show me some cats.
Over the course of my career as a software engineer, I've noticed something similar when working with domain experts. Even people with ten or more years of experience can struggle to articulate all the rules a software system should follow.
Ask a payroll expert to explain how an entire payroll system should behave, and they may have to think very hard. Give them a concrete case, and they can often tell you quickly and confidently how to handle it. The knowledge is there; a specific situation makes it easier to express.
There is a related observation in this Hacker News comment: accounting experts who struggled to state the general rules could still evaluate individual transactions. Working through examples helped uncover the requirements.
Even deciding what to eat can work this way. Ask someone what they want for dinner, and they might not know. Suggest a particular restaurant, and they can suddenly tell you very quickly whether they want to eat there.
The suggestion gives them something concrete to react to. That reaction helps reveal a preference they had difficulty expressing in the abstract.
I also find a useful analogy in machine learning. To train an image classifier to recognise cats, we can provide labelled examples of cats and other things. We do not have to manually write every visual rule that distinguishes a cat.
Why shouldn't examples also help us discover what we want from our products?
I think there is value in generating many examples quickly and putting them in front of people who can evaluate them. Contrasting cases, unexpected situations, and meaningful boundaries can reveal requirements we would otherwise miss. The examples can help us discover general rules and make those rules easier to discuss.
We still need to choose which examples deserve attention. Repeating the same case with slightly different names adds little. A case that exposes a disagreement or changes someone's understanding can be extremely valuable.
Why Given / When / Then?
For expressing these examples, I like Gherkin's Given / When / Then structure:
Given <preconditions>
When <action>
Then <expected result>
For example:
Given a workspace invitation has expired
When the invited person opens the invitation link
Then they cannot join the workspace using that invitation
Then they are told that the invitation has expired
It's almost a shared protocol for communicating about software behaviour. Each example follows the same structure, and it's surprising how much you can express with it.
I like using Gherkin with LLMs for two reasons.
Product behaviour over implementation: .feature and .feature.md files are most likely part of LLMs' training data. As such, LLMs are biased to use them to encode high-level product behaviour instead of implementation details.
Concise and structured: Gherkin enforces concise, focused, structured communication instead of verbose, unstructured English, which becomes tiring to read multiple times per day, multiple days per week. With Given / When / Then, I know where to look:
- Under which conditions does this happen?
- Which action is taken?
- What is the result?
For example, here is an agent describing an expired invitation, and the same thing in Given / When / Then:
what the agent wrote147 words
Great question! I dug into how invitation expiry currently works, and the short answer is that expired invitations are handled, though there are a couple of nuances worth flagging.
When InvitationController.show/2 loads the token it calls Accounts.get_valid_invitation/1, which filters on expires_at > now(). If nothing matches we fall through to the generic :not_found branch, so the person effectively can't accept it (the row still exists, but accept/2 would reject it separately via changeset validation anyway).
On the UX side, they currently see the generic “Invitation not found” flash rather than an expiry-specific message, which is arguably fine but could be a little confusing. I've left that as is for now, but happy to add a dedicated :expired state if you'd like. It'd be a small change plus a test update.
Overall this should be robust going forward. Let me know if you want me to dig deeper!
the same, as an example26 words
Given a workspace invitation has expiredWhen the invited person opens the invitation linkThen they cannot join the workspaceThen they see “Invitation not found”
The syntax alone cannot guarantee a useful specification, but it gives us a consistent shape for the conversation.
We can use it to describe how a product currently behaves, a bug we have observed, or how we want the product to behave after a change. That makes it useful throughout the process, including when we are still trying to understand the problem.
From specified behaviour to observed behaviour
This is broadly how I've been using specification by example for new product behaviour:
- Draft the expected behaviour.
- Review the specification.
- Implement the behaviour.
- Review the actual result.
I treat this as a cycle. Reviewing an example can reveal something we misunderstood about the current product. Implementing it can uncover a missing case. Seeing the result can change our opinion about what we wanted.
Draft the expected behaviour
I start with concrete examples of what should happen. Alongside the happy path, I include contrasting cases, failures, retries, and meaningful boundaries.
Sometimes I write the first few examples by hand. That helps me become familiar with the problem and wrestle with the details before asking an agent to expand on them.
A .feature.md file can already be useful as a thinking tool. You do not need to set up the full Cucumber suite to benefit from writing down behaviour in this form.
Review the specification
Before implementing anything, I compare the examples with the current product. A specification can accidentally describe changes that have nothing to do with the intended goal.
For example, imagine an HR system that labels employees as <fullname> - <position>. An agent drafts a specification that uses <fullname> / <position> instead.
If the separator is irrelevant to the change, we have introduced an unnecessary discrepancy. An implementing agent may change the label to match the specification. A reviewing agent may flag the implementation if it preserves the existing label. Either way, we have created work around something we never intended to change.
This is a small example, but the same problem becomes more difficult to spot when the behaviour is complex. Comparing the specification with the current product helps keep the intended change focused.
I also review the specification itself:
- Does it contradict itself?
- Are the conditions, actions, and expected results precise?
- Does it cover the relevant happy paths, failures, and retries?
- Are there duplicate scenarios?
- Does each proposed change help achieve the goal?
Implement the behaviour
From here, the implementation can be handled by you or the coding agent, using whatever process fits the task.
With less capable models, I tend to break the work into more explicit steps: draft tests from the specification, review whether those tests cover the scenarios and express the expected behaviour, implement the application code, and then review the implementation against the specification, the tests, and the surrounding code.
I've noticed that the amount of detailed direction I need to provide varies with the model. As models improve, I expect some of these intermediate steps to happen without my involvement.
This approach lets me spend time at the beginning clarifying the behaviour and focus my attention where it has the most value.
Review the actual result
Once the implementation exists, I want to inspect what the product actually does.
A scenario describes an expectation. Evidence gives us something to examine when deciding whether the implementation meets it. For a visible interaction, that might be a screenshot or a recording. For another kind of behaviour, a test result, API response, or exported file may be more useful.
A screenshot can show a resulting state; a recording can show the interaction that led to it. Test results can report whether particular assertions passed. Each helps answer a different question, and the evidence should be relevant to the behaviour under review.
Over time, I want to support reviewing other kinds of evidence too. For a data migration, that might mean comparing CSV exports before and after the change: which records changed, and were the values that should stay the same preserved? For a browser interaction, it might mean inspecting a trace of the actions and requests. The behaviour we are investigating should determine what evidence we bring into the discussion.
I want to be able to examine that evidence alongside the scenario it relates to, ask questions, leave comments, request changes, and discuss the result with both humans and agents.
There are two questions I might discover here: does the result match the specification, and, now that I can see it, is this actually the behaviour I want?
Sometimes the implementation needs to change. Sometimes the specification does. Either way, we have something concrete to discuss.
This is a central part of what I want Higherlevel to support: a place where the current behaviour, the intended change, the evidence, and the conversation can be understood together.
What remains valuable as agents improve?
I want to build Higherlevel with a future in mind where inference is fast, cheap, and reliable.
Suppose an agent could take an agreed specification and implement it correctly on the first attempt. Some of the steps I use today would become unnecessary. I would have less reason to supervise the implementation or repeatedly check whether the agent had followed the instructions.
We would still need to decide what the product should do.
People may have different expectations. A domain expert may recognise the right behaviour when shown an example while struggling to express the general rule. A team may need to understand an existing workflow before deciding how to change it. Seeing a proposed result may reveal that we want something different.
And when many changes happen at once, people still need a way to understand what has changed and communicate their decisions to each other.
Better agents can help with all of this. They can investigate current behaviour, propose examples, surface contradictions, and produce evidence. I want Higherlevel to give people a shared place to work with those contributions, discuss the product, and decide how it should evolve.