↓ Skip to main content

When a Closed Decision Reopens: Breaking the Scope Relitigation Cycle

A decision reached with your principal did not actually close the question. It closed the question in the room where it was made. When a new manager met the senior leader for the first time, the shared understanding that took weeks to build did not transfer, and the original ambiguity returned. The cycle you cannot escape is not a scope problem. It is a context-transfer problem that looks like a scope problem.

The Decision Didn’t Reopen. The Context Didn’t Transfer.
#

When leadership says “build this feature” without saying what “this” means, they have not given you a requirement. They have given you a vague goal, and asked you to find a concrete target inside it. You did the right thing. You built a demo to make the vague goal concrete enough to react to, you sat with the principal, and you negotiated a concrete, buildable target. That work was genuine, and it was correct.

Then a person who was not in the room walked into a different room with the senior leader, and the target changed.

The target moving feels like betrayal, or like a failure of the new manager, or like leadership changing its mind. It is usually none of those. It is the predictable consequence of a decision that was never written down in a form that could travel. A decision that exists only in the memory of the people who were present is a decision that cannot hold when it reaches anyone who was absent. Your organization keeps generating absent people: new hires, new managers, new conversations, new quarters. Every unwritten decision remains valid only until one new person enters the chain.

The senior leader was never actually aligned with you on a specific scope. They were aligned with a feeling, which is that the feature should be impressive and should exist. The specific, feasible compromise you reached with the principal was never transferred back to the person whose vague wish started the whole thing. So when the topic came up again, the leader returned to the only thing they still had, which was the original wish, unmodified by everything you had since learned was impossible.

You Have Two Problems, Not One
#

The conversation keeps switching between two positions because two genuinely different questions have been mixed together into one discussion, and the two questions keep conflicting.

The first question is the north star: what would the ideal version of this feature look like, if the tools caught up and the constraints disappeared? That vision is valuable. It tells you which direction to take, and it is the thing that excites leadership in the first place.

The second question is the milestone: what will we actually ship this quarter, with the tools we have today? That answer is necessarily smaller, uglier, and more compromised than the north star, and it is the only thing you can actually build.

The cycle happens when leadership talks about the north star and hears your milestone answer as a refusal, and when you talk about the milestone and hear their north star answer as scope creep. You are both correct, about different questions, and the conflict continues entirely because nobody has written the two answers on the same page and separated them.

The fix is to separate the questions, explicitly and in writing. Keep the north star. Document it, reference it, and let it do the work of inspiring people. Then, underneath it, write separately what you are committing to build now, why that subset and not another, and what specifically would have to change for the next increment to become feasible. The north star is a direction. The milestone is a contract. Confusing the two is what makes every meeting feel like the last meeting, again.

“That’s Impossible” Is Not an Answer a Leader Can Process
#

Here is the cruelest part of the situation, and the one most likely to block progress.

When you tell leadership “we can’t build the full vision, the tools aren’t there yet,” you are reporting a technical fact, and you expect it to be received as a boundary. It does not. It is received as an opinion, and a suspicious one, because every leader has heard engineers declare things impossible that later turned out to be merely hard. The word “impossible” carries no information a decision-maker can act on. It sounds like reluctance, and so it invites the very relitigation you are trying to end.

The move that actually ends the cycle is to convert impossibility into a set of specific, dated, falsifiable claims. Not “we can’t do X,” but “X requires capability Y, which does not exist in our stack; the closest vendor offering is at stage Z; building it ourselves is an estimated N months of work from the platform team, which is not currently funded.” Now the leader is not arguing with your willingness. They are looking at a menu, and the menu has prices on it. They can choose to accept the limit, fund the enabler, or descope, but whatever they choose, they are choosing with information instead of against a feeling.

Saying “impossible” stops the decision. An itemized list of what it would take lets the decision move forward. Leaders cannot act on a flat refusal, but they will act on an itemized list, and often they will fund the capability that enables the next step. This reframing also protects you. The day the tools do catch up, your document already names the capability you were waiting for, and reaching the north star becomes a matter of executing a plan you wrote months ago instead of restarting the conversation from scratch.

A Verbal Decision Is Just an Opinion That Hasn’t Been Overwritten Yet
#

The single highest-leverage thing you can do to stop the cycle is to insist that decisions get written down the moment they are made, in a place that outlives the meeting.

This is what Architecture Decision Records were invented for, as Michael Nygard argued when he proposed the format. The record is a short document that captures the context, the decision, the alternatives considered, and the status, so that a person who arrives later can understand not just what was decided but why. The same discipline applies to scope decisions, which are every bit as consequential as architectural ones. A one-page record that says “we will build this subset, not the full vision, because of these specific constraints, and we agreed this on this date with these people” is worth more than any number of follow-up meetings.

The reason writing matters more than talking is that writing behaves differently from speech. A conversation degrades the instant it ends. The memory of it degrades faster, and as it passes through more mouths it distorts, the way the children’s game of telephone turns a clean phrase into nonsense in a few retellings. A document does not degrade. It says the same thing to the new manager on their first day as it said to the principal on the day it was written. It says the same thing to the senior leader in their next one-on-one as it said in the room where the compromise was struck.

If a decision is worth making, it is worth making durable, and a decision is only as durable as the artifact it is recorded in. The cost of writing it down is ten minutes. The cost of not writing it down is the cycle you are currently in, repeated every week, forever.

The New Manager Is Not the Enemy. They Are a Missing Node.
#

It is easy, and emotionally satisfying, to blame the new manager for reopening the settled question. Resist that interpretation. Almost certainly, they walked into their first meeting with the senior leader underprepared, because nobody had handed them the history. They improvised from the leader’s enthusiasm, which pointed at the north star, not at your milestone compromise. That is not sabotage. That is a new person doing their best with the information they were given, which was not enough.

The deeper structural issue is that information in an organization flows along its communication paths, and an org that just gained a new link between the team and the senior leader has, in effect, rewired those paths. A decision that was stable under the old wiring may be unstable under the new wiring, because the new path bypasses the node where the context was stored. This is why onboarding a new manager is not a courtesy, it is a defensive necessity. Within their first week, they need to receive, in writing, the current decisions, the reasons behind them, and the constraints that produced them. A new manager who has read the decision records cannot accidentally contradict them, because they know they exist.

There is also a principal-agent dynamic here that is worth naming. The senior leader, the new manager, the principal, and you all have different information, different incentives, and different audiences they are trying to satisfy, the classic pattern of the principal-agent problem. The new manager may be, reasonably, trying to demonstrate value to their new boss, and an enthusiastic “we can do more” lands better in that first meeting than a cautious “actually, we already agreed to less.” They are not wrong to want to look good. The system is wrong to have left them no shared record to align to. Fix the system and the manager will follow it, because the record gives them something safer to bring to the leader than improvisation: a documented plan they can defend.

A Playbook for Breaking the Cycle
#

When you find yourself back at the start of a scope conversation you thought was over, do the following, in order.

First, stop re-arguing the scope in the meeting where it resurfaced. Relitigating under pressure produces worse decisions than the original ones, and it taxes the same finite decision-making energy that you need for the work itself. Gain time. Say that you want to make sure the conversation reflects everything the team has already learned, and that you will come back with a written summary. That sentence is free, and it stops the cycle.

Second, write the decision record, if it does not already exist. Context, the north star, the agreed milestone, the specific technical constraints that forced the compromise, the people who agreed, and the date. This is the artifact that should have existed all along, and creating it now is not empty procedure, it is what makes every later step work.

Third, convert every “we can’t” into a priced option. For each piece of the vision that is out of reach, write what it would take to reach it: which capability, which team, which rough timeline, which dependency. Give leadership the itemized options, not a flat refusal.

Fourth, brief the new manager before the next senior-leader conversation. Walk them through the record in person, make sure they understand the constraints, and explicitly invite them to bring their own objections now rather than in the room with the leader. Make them a co-author of the record, and they will defend it instead of overturning it.

Fifth, re-present to the senior leader, on your terms, with the record in hand. Acknowledge the north star openly, because dismissing the vision is what makes leaders resist. Then show the milestone, show the priced options, and ask for a single explicit decision: accept the current scope, fund an enabler, or descope. Whatever they choose, write the new decision down before anyone leaves the conversation. A meeting that ends without a written decision is a meeting that will have to be held again.

Finally, schedule the next scope review in advance, on a date you control, instead of waiting for the topic to come up on its own. Scope conversations are not avoidable, and they are not the enemy. The enemy is surprise relitigation, the version that catches you by surprise in someone else’s calendar. A standing quarterly review where the north star, the milestone, and the priced options are revisited together turns the cycle into a rhythm, and a rhythm is something a team can plan around.

The Lesson
#

You cannot build something that is impossible today, and you should not promise that you can. But you also cannot expect a vague wish, transmitted through a changing set of people, to ever stop generating the scope conversation you dread. The way out is not to win the argument one more time, harder. It is to change the medium in which the argument happens. Write the vision. Write the milestone. Write the prices of the gaps between them. Put all of it where the next person can find it. The conversation that reopens next month will reopen against a record instead of against your memory, and records settle those questions.

Decisions that are written, priced, and shared do not need to be relitigated, because they travel to the rooms you are not in and make your case for you.

See also
#

References
#


The Codebase Gardener: Keeping a Codebase Sane When Teammates and Their Agents Pull in Every Direction

You cannot review your way to a sane codebase when the code is being produced faster than you can read it. That mismatch is the new arithmetic of the LLM era, and it is brutal for the one engineer on the team who cares about consistency. The strategy that actually works is to stop fighting at the pull request and start fighting at the layer everyone, and every agent, is forced to pass through.

The Math Changed Under Us
#

For most of software history, one careful engineer could hold a codebase together by reviewing most of what landed in it. The volume was human. A teammate produced code at roughly the rate you could read it, and a determined reviewer could keep the drift in check. That equilibrium is gone, and two things killed it.

First, throughput. A teammate driving an LLM agent now opens in an afternoon the pull requests that used to take a week. The unit of work did not change; the rate at which units appear did.

Second, plurality. Each teammate may be driving a different agent, loaded with different skills, different conventions, a different mental picture of what “good” means for this codebase. The codebase does not converge on a single style through argument anymore. It fragments into as many dialects as there are agents producing it, and each dialect looks plausible, because plausibility is exactly what an LLM optimizes for. A function can be internally coherent, consistent with nothing around it, and wrong about the domain all at once.

You, alone, reading diffs, are now the slowest station in a pipeline that was designed to outrun you. Per-unit review scales linearly with your hours, and the entropy is being produced exponentially. No amount of discipline closes that gap.

Reviewing Harder Is the Trap
#

The instinct of the engineer who cares is to review more, longer, more strictly. Reviewing harder is exactly the wrong move, for reasons I laid out in Rethinking Code Review in the Age of LLMs. Every hour you spend line-editing someone else’s agent output is an hour the next three pull requests go unread. You become the bottleneck, and bottleneck reviewers do not stay bottleneck reviewers for long. Teammates start tagging each other for approvals, slicing changes to slip under your threshold, or simply waiting you out until you tire.

You end up exhausted, quietly resented, and still losing ground to the codebase. The worst part is that the work feels virtuous while you do it. Catching a misnamed variable in review feels like defending the standard. It is not. It is fixing one small problem while the codebase drifts as a whole.

Review is a per-unit activity, and per-unit activity cannot match entropy that is being produced faster than you can read it. If your entire strategy is “I will catch the problems at review,” the strategy has already lost. The question is not how to review better. The question is how to make most of the review unnecessary.

Move the Leverage From Review to Infrastructure
#

The way one person defends a codebase against many producers is to stop operating on the units and start operating on the system that produces them. This strategy is the theory of constraints applied to code quality: if you cannot keep up at the review station, do not add more reviewers. Change the station.

Concretely, it means every standard you care about has to live in one of the few places work is forced to pass through, rather than in your head or in your review comments. There are only a handful of such places, and together they are the entire leverage surface available to a single engineer.

The skill and rule files every agent loads at the start of a task. The CI gates every pull request must clear before it can merge. The templates and scaffolds every new module begins from. The default configuration the linters, formatters, and analyzers ship with. The hooks that run on commit, on push, and on merge.

If your standard is encoded in any of these, it executes on every change, forever, without you being present. If it lives only in your review comments, it executes only when you are awake, looking, and willing to argue about it. Running without you is the whole difference between a standard and an opinion. A standard runs whether or not anyone agrees with it. An opinion dies the moment you go on vacation.

The same point is made, from the team’s perspective, in Bringing Everyone to the Same Level: the senior engineer’s edge was always a process they ran in their head, and a process in a head does not scale. Here the stakes are higher, because you are not trying to lift the team to your level. You are trying to keep the standard in place while the team is actively, if innocently, drifting from it. The mechanism is the same. Encode the process, and let the encoding do the defending.

Make the Right Thing the Path of Least Resistance
#

Most code in a codebase is the way it is because that was the easiest thing to type at the time. This predictability is an advantage, not a problem, for someone trying to steer a codebase alone. If the easiest path also happens to be the correct path, conformity is free, and nobody has to be persuaded of anything.

This alignment is the paved road idea, and it is the single highest-leverage intervention available to a solo defender. You do not get compliance by arguing for it. You get it by making the compliant thing the thing that happens when nobody is thinking.

Ship a project template that already has the test harness, the lint config, the migration format, and the observability scaffolding wired in, and a teammate who spins up a new service produces conforming code by default, without ever reading your standards document. Wire the migration linter into CI so that a non-backward-compatible migration fails to merge, and the question of whether backward compatibility is “our standard” stops being a question at all. Friction for the wrong thing, zero friction for the right thing. That asymmetry does more work than any amount of documentation, because it operates on the path of least resistance rather than against it.

The reverse matters just as much. Remove friction from the behavior you want. If you want small, reversible changes, make small changes trivially easy to merge. If reviewing is expensive, people batch, and batching is exactly what produces the large, entangled, unreviewable pull requests that defeat you. Add friction to what you want less of, and remove friction from what you want more of, and do both in the tooling rather than in the standup.

Invariants, Not Preferences
#

Most review wars never end because most of what people fight over are preferences. Tabs versus spaces. Single exit versus early return. One assertion per test. Preferences are arguable, and in an LLM-saturated codebase, arguable means unwinnable, because the other engineer’s agent was told the exact opposite of what yours was told, with equal confidence.

The move is to convert as many preferences as possible into invariants before they can become arguments. An invariant is a property the system will enforce whether or not anyone agrees with it. Database migrations must be backward compatible. New dependencies require an audit record. Public API changes require a feature flag. Cyclomatic complexity above a threshold fails the build. These are not opinions. They are machine-checkable constraints, and a constraint that a machine checks is a constraint you never have to argue about again.

Every preference you promote to an invariant is one fewer conversation you have to win, and one fewer place the codebase can drift while you are not looking. Reserve your remaining human attention for the small set of things that genuinely cannot be encoded: taste, architectural direction, whether a feature should exist at all. Everything else should be a gate or a default, silently doing the work you used to do by hand.

Garden in Sweeps, Not in Diffs
#

Per-PR review is not the only way to fight decay, and in this era it is no longer even the best way. Some drift will always get through, because no gate is perfect and because some of what rots a codebase is not visible in any single diff. Naming conventions slide. Duplication accumulates. A module that was clean in isolation becomes tangled once three teammates have each extended it in a different direction.

The answer is to operate the Boy Scout rule at the codebase level rather than at the commit level: run regular maintenance sweeps instead of trying to catch everything one pull request at a time. Once a week, run the dead-code analysis. Run the duplication detector and look at the new clusters. Pull the complexity trend report and see which functions crossed a threshold this week. Skim a list of newly added dependencies.

A sweep lets you fix the eighty percent of drift that no individual pull request would have surfaced, and it lets you fix it in a way that does not require winning an argument on each one. You are not blocking anyone’s work. You are tidying, in batches, on your own time, against objective signals from the tools. The gardener does not follow every leaf as it falls. The gardener rakes.

Sweeping is also the frank acknowledgment that some entropy is the cost of speed, and that the goal is not zero drift. The goal is a drift rate low enough that your weekly sweep runs net positive, so that over months the codebase gets cleaner rather than dirtier even as it grows. A codebase that gets slowly cleaner under load is a codebase you are successfully defending. A codebase that gets slowly dirtier no matter how hard you review is one you are losing, and the review is not the fix.

Write the Decisions That Outlast You
#

One of the cheapest, highest-leverage things a solo defender can do is write things down in a form that survives without them. Architecture decision records, one page each, capturing what was decided, what alternatives were rejected, and why. A short conventions document that is explicitly the source of truth the lint config is derived from. A “why this exists” header on the modules most likely to be misunderstood.

The point of writing it down is not to win today’s argument. Today’s argument will be re-litigated regardless, because the engineer on the other side, or their agent, has not read it. The point is to ensure that the same argument does not have to be re-won from the beginning every time, by you, as it happens.

A decision that lives in a file is a decision the next agent can be pointed at. A decision that lives only in your head dies the moment you switch teams, or take a week off, or simply get tired of explaining it for the fiftieth time. When your defenses are encoded as files, the codebase keeps its structure without you maintaining it. When they are encoded only as your vigilance, the codebase is one two-week vacation away from drift you will spend a month undoing.

Pick the Battles That Are Actually One-Way Doors
#

You cannot defend every standard, and trying to is the fastest route to burnout. The discipline is to decide, in advance, which fights are worth your scarce attention, and the useful frame is the distinction between one-way and two-way doors, which I wrote about in When Engineers Disagree on Best Practices.

A one-way door is a decision that is hard or impossible to reverse. Choosing a primary database. Committing to a service boundary that will be expensive to move later. Adopting a framework that will permeate every file. Dropping a database column. These deserve your full attention, and as the defender of the codebase these are where you should spend it. Block them, slow them, write the decision record, make the team justify the trade-off.

A two-way door is a decision you can undo in an afternoon. A naming convention. A helper function in the wrong package. A test that could have been structured better. Let these go, or fix them in a sweep later. The solo defender who treats every diff as a one-way door exhausts themselves on reversible things and has nothing left for the decisions that actually compound.

This discipline is also the cure for the resentment that otherwise makes this role unsustainable. You will see things every day that are not how you would have done them. Most of them do not matter. Learn to recognize which decisions are irreversible, let the reversible ones pass, and you will last long enough to actually defend the things that count.

The Limit
#

Sometimes the team will not align, and no amount of infrastructure will fully save you. A teammate may insist on driving their own agent with their own skills, their own conventions, their own picture of the codebase, and treat your paved road as a suggestion rather than a default. A manager may value shipping velocity over every standard you have encoded, and quietly override the gates that matter.

At that point your job is no longer to win. It is to make the cost of the drift visible. Keep the trend reports. Keep the complexity numbers. Keep the record of which decisions were one-way doors that got walked through without the decision being made. Do this not to build a case against anyone, but because the most powerful thing a defender can produce, when defense fails, is a clear record of what was lost and when, so that the next attempt at sanity starts from evidence rather than from impressions.

And know when to stop defending a particular part of the codebase. A codebase is not worth your health, and a team that has decided, collectively, to let the codebase rot will rot it with or without you. Your leverage is highest at the start of a codebase’s life and lowest once the rot is structural. If you have encoded what you can, written down what you know, and the drift is still winning, the rational move is to spend less energy fighting and more energy deciding whether this is still the codebase you want to be responsible for.

What to Do Next
#

You do not need permission, and you do not need a migration. Pick the single standard that is being violated most often, the one that costs you the most review time, and convert it from a review comment into a gate. A lint rule. A CI check. A line in the skill file every agent loads. Run it on every pull request, including the ones you do not personally review.

Then do it again, next week, with the next most expensive standard. Each conversion is a piece of vigilance that stops being yours and starts being the system’s. Over a quarter, the surface you have to defend manually shrinks to the small set of genuinely judgment-laden calls, and the volume of entropy you face stops mattering quite so much, because most of it is being caught upstream of you, by machinery that does not tire.

The solo defender who wins is not the one who reviews the most. The solo defender who wins is the one who has made themselves, slowly and deliberately, the part of the pipeline that is no longer strictly necessary.

A sane codebase in this era is not one that a heroic reviewer holds together by force of attention. It is one whose standards have been pushed so far upstream, into the skills and gates and defaults and templates, that the code arrives mostly correct, and the reviewer is left doing the small amount of work that only a human can do. Build that, one encoded standard at a time, and the direction the rest of the team is pulling in starts to matter a great deal less.

See also
#

References
#

  • Wikipedia, “Theory of Constraints” – Goldratt’s framing for why you change the bottleneck station rather than adding effort at it, the basis for moving leverage from review to infrastructure
  • Wikipedia, “Paved road” – the principle of making the compliant path the easiest path, which lets a single engineer steer a codebase without persuading anyone
  • Wikipedia, “Technical debt” – the metaphor for accumulated drift that a weekly gardening sweep is meant to keep net-negative
  • Wikipedia, “Broken windows theory” – why visible decay accelerates further decay, and why keeping the drift rate below the sweep rate matters disproportionately
  • tomzx/agents – a working library of the skill files and review gates that implement the infrastructure-first defense described here

The Self-Evolving Repository: Automating a GitHub Project End to End with LLMs

The question is not whether an LLM can write code for your project. It already can. The question is whether a project can continue to evolve, fix its own bugs, add its own features, and improve its own quality without you ever opening a terminal, writing a prompt, or reviewing a pull request.

This is the self-evolving repository: a GitHub project where every function of the maintainer has been replaced by an automated loop driven by LLMs. No human control. No human-in-the-loop. The maintainer sets it up once, then does not intervene, and the project keeps making progress.

I want to be precise about what a self-evolving repository means, what it requires, and where it breaks.

What Full Automation Means
#

Most LLM-assisted development today is interactive. A developer describes a task, the LLM writes code, the developer reviews, requests changes, and merges. The developer controls what the LLM works on. The LLM does the work.

Full automation removes the developer’s control. The project must decide for itself what to work on, implement the work, verify it, and ship it. The maintainer who set it up does not intervene in the loop.

Full automation is a fundamentally different problem from “use an LLM to write faster code.” It is the problem of encoding every judgment a maintainer makes into a system that can execute that judgment autonomously, reliably, and indefinitely.

A maintainer of an active project does many things. They triage bug reports. They decide which feature requests align with the project’s direction. They prioritize competing work. They review contributions. They fix CI when it breaks. They update dependencies. They write documentation. They make architectural decisions. They decline changes that would make the codebase worse.

Each of these duties is a decision that currently requires human judgment. Full automation means building a system that makes each of these decisions without the human.

The Autonomous Loop
#

A self-evolving repository runs a continuous loop.

Observe → Decide → Implement → Verify → Ship → Learn

Each stage must be fully automated.

1. Observe
#

The system continuously monitors signals about the project’s state. Open issues and their content. Pull requests from external contributors. CI failures and test results. Dependency vulnerability alerts. User feedback in discussions. Stack Overflow questions mentioning the project. Download and usage statistics. Error reports from production telemetry if the project is a library or service.

The system uses these signals to understand its state. Without them, the project does not know what needs attention. The richer the signal sources, the better the system can prioritize.

A project that only monitors its own issue tracker misses much of what needs attention. A project that also monitors community discussions, ecosystem health, and downstream breakage has much more information about where to invest effort.

2. Decide
#

The decide stage is the hardest, and I will return to it.

Given the observed signals, the system must decide what to work on next. Deciding what to work on is not just “pick the highest priority issue.” It is a sequence of judgments: which problems are worth solving, which are urgent, which can wait, which should be declined, which require architectural changes versus incremental fixes.

The decision stage encodes the maintainer’s judgment about what matters. If the system encodes the maintainer’s judgment poorly, the project will be busy but not productive, shipping changes that do not move the project in a meaningful direction.

3. Implement
#

The system decomposes the chosen work into tasks and generates code. Implementation is the part that LLMs are already good at. Given a clear specification and sufficient context about the codebase, current models can produce working implementations for most routine tasks.

The implementation stage benefits from the same patterns that work in interactive LLM development: competitive generation (multiple attempts, best one selected), iterative refinement (generate, evaluate, regenerate), and specification-driven development (the spec is authored before the code).

The difference is that in the autonomous loop, the specification is also generated by the system, not by a human.

4. Verify
#

Before any change ships, it must pass automated verification. This is the quality gate that replaces human code review.

The verification pipeline should include: the existing test suite, static analysis and linting, security scanning, type checking, and scenario-based validation where an LLM evaluates whether the implementation satisfies the specification.

As I argued in Rethinking Code Review in the Age of LLMs, human code review of LLM-generated code is the lowest-value activity in the pipeline. Automated verification is not just faster, it is more consistent and more reliable for the kinds of checks that matter at this stage.

The critical requirement is that verification criteria are defined before implementation, not after. If the system writes tests to match what it already implemented, the tests prove nothing. The specification and its acceptance criteria must exist first.

5. Ship
#

Verified changes are merged and released. This stage is mostly mechanical: merge the pull request, run the release pipeline, publish the package, update the changelog.

The one non-obvious requirement is a rollback mechanism. If a shipped change introduces a regression that the verification pipeline missed, the system needs to detect the regression in production telemetry and automatically revert. Without an automatic rollback, errors compound.

6. Learn
#

The system records what worked and what did not. Every failed implementation, every reverted change, every missed bug is an input to future decisions.

This learning takes several forms. Failed prompts and their corrections become few-shot examples for future implementations. Bugs that escaped verification become new test cases. Decisions that led to regressions update the prioritization model. Architectural patterns that caused repeated problems become constraints the system avoids.

Over time, the system accumulates institutional knowledge the way a human maintainer does, but in a form that is explicit, versioned, and transferable.

The Direction Problem
#

The hardest problem in a self-evolving repository is not implementation. It is not verification. It is not even CI automation.

It is direction.

When a human maintainer decides what to work on, they are applying judgment that is difficult to articulate. They know the project’s trajectory, the community’s needs, the competitive landscape, and the technical debt that is becoming critical. They have taste. They have a vision for what the project should become.

Encoding the maintainer’s judgment into an autonomous system is the central challenge.

The roadmap as a steering mechanism
#

The maintainer’s initial setup must include a machine-readable roadmap. Not a vague vision statement, but a structured document that defines: the project’s purpose and non-goals, the target user and their needs, the quality bar for changes, the architectural principles that must be respected, and the current priorities ranked by importance.

This roadmap is the project’s governing document. Every autonomous decision is evaluated against it. When the system decides what to work on, it checks the decision against the roadmap. When the system reviews a contribution, it checks whether the contribution moves the project toward a roadmap goal.

The roadmap does not need to be exhaustive. It needs to be sufficient to distinguish work that matters from work that does not. This is a lower bar than most people assume. Most of a maintainer’s triage decisions are not subtle architectural judgments. They are straightforward: this bug affects users, fix it; this feature request is out of scope, decline it; this dependency has a security vulnerability, update it.

Direction from external signals
#

The roadmap provides static direction. External signals provide dynamic direction.

If the system observes that a particular error message is appearing frequently in community discussions, that is a signal to improve the error message or fix the underlying issue. If the system observes that a downstream project broke after a recent release, that is a signal to fix the breaking change. If the system observes that a dependency released a major version, that is a signal to evaluate the migration.

These signals allow the project to evolve in response to its environment without a human pointing the way. The system reacts to what is actually happening rather than to what someone predicted would happen.

The drift problem
#

Signal-driven direction carries a fundamental tension. A self-evolving repository that only reacts to external signals will optimize for whatever those signals measure. If the signals are bug reports, the system will become excellent at fixing bugs and terrible at anything else. If the signals are feature requests, the system will accumulate features and lose coherence.

This drift is Goodhart’s law applied to software maintenance: when a measure becomes the target of an autonomous system, it ceases to be a good measure.

The roadmap exists to counteract this drift. It is the fixed point that keeps the project aligned with its original purpose even as the system optimizes for observable signals. But the roadmap itself can become stale. A project’s context changes over time. User needs shift. The ecosystem evolves. A roadmap written in January may be wrong by July.

A fully autonomous system needs a mechanism for updating its own roadmap. Updating the roadmap is where the direction problem becomes genuinely difficult. Updating the roadmap means making a judgment about what the project should become, and that judgment requires understanding the project’s users, ecosystem, and competitive position at a level that current LLMs can approximate but not fully replicate.

The pragmatic solution is not to solve the stale-roadmap problem perfectly but to bound it. Let the system make small roadmap adjustments based on observed signals, but require large directional changes to go through a human review. This keeps the system autonomous for 95% of decisions while preserving human oversight for the 5% that determine the project’s long-term trajectory.

Quality Without a Human
#

The quality question is the one most people get stuck on. “How do you trust code that no human reviewed?”

The answer is the same one I gave in The Future of Code Review: you do not trust the code, you trust the verification system.

A self-evolving repository needs a verification pipeline that is more rigorous than what most human-maintained projects have today. Not because the code is worse, but because there is no human fallback.

The pipeline should have multiple independent layers, each catching different classes of problems.

Unit and integration tests catch functional regressions. These should be comprehensive and fast. The system should be able to run the full suite in minutes, not hours.

Property-based testing catches edge cases that example-based tests miss. For pure functions and data transformations, property tests can generate thousands of inputs automatically, surfacing bugs that a human would never think to test.

Static analysis catches type errors, security vulnerabilities, and common anti-patterns. Tools like Semgrep, CodeQL, and language-specific analyzers should run on every change.

Mutation testing verifies that the test suite is actually meaningful. If you can mutate the code and the tests still pass, the tests are not testing what you think they are. Mutation testing is especially important when the tests themselves are LLM-generated.

Scenario-based validation uses an LLM to evaluate whether the implementation satisfies its specification. Scenario-based validation is the layer that catches semantic problems that automated tests miss: the code works, but it solves the wrong problem.

Adversarial verification has a separate agent actively try to break the implementation. This agent writes tests designed to fail, probes edge cases, and attempts to find inputs that produce incorrect behavior. It is the automated equivalent of a hostile code reviewer.

The key insight is that each layer should be independent. If the same LLM that wrote the code also wrote the tests and also ran the review, the correlated failure rate is high. The system will make the same mistake in implementation, tests, and review because they all share the same blind spots.

Diversity of verification methods reduces correlated failures. Static analysis does not share blind spots with property testing. Mutation testing does not share blind spots with scenario validation. The layers are strongest when they are structurally different, not just different invocations of the same model.

What Can Go Wrong
#

A self-evolving repository is a complex autonomous system. Complex autonomous systems have failure modes that are hard to predict.

Silent quality decay
#

Silent quality decay is the hardest failure mode to notice. The system ships changes that pass all verification layers but gradually degrade the codebase’s quality. Each individual change is defensible. The cumulative effect is a codebase that is harder to maintain, slower to evolve, and full of subtle interactions that no one understands.

Silent quality decay happens when the verification pipeline measures immediate quality (does this change pass tests?) but not systemic quality (does this change make the codebase healthier?). Metrics like cyclomatic complexity, coupling, and test coverage can help, but they are proxies, not ground truth.

The complexity spiral
#

Without a human saying “this is too complex, simplify it,” the system may accumulate complexity over time. Each new feature adds code. Each bug fix adds special cases. Each dependency update adds compatibility layers. The codebase grows without bound because nothing in the autonomous loop says “stop adding, start removing.”

A self-evolving repository needs an explicit simplification loop. The system should periodically identify the most complex parts of the codebase and attempt to simplify them, even if no bug is forcing the change. The simplification loop is technical debt reduction as an automated process.

Specification drift
#

When the system writes its own specifications, the specifications can drift from what users actually need. The system optimizes for specifications it can satisfy, not specifications that matter. Over time, the project accumulates well-implemented features that nobody uses.

Specification drift is the autonomous version of the problem I described in The Shifting Bottleneck: when implementation becomes free, deciding what to implement becomes the hard part. In a self-evolving repository, the decision problem is fully automated, which means the drift can go unnoticed for longer.

Cascading failures
#

A single bad change that passes verification can break downstream functionality in ways that the verification pipeline does not catch. The system then generates fixes for the symptoms rather than reverting the cause. Each fix introduces new changes, each of which can introduce new problems. The system enters a fix loop, shipping more patches that make the situation worse.

The defense against cascading failures is blast radius limitation. Changes should be small, merged incrementally, and monitored for downstream impact. The system should have a threshold: if more than N changes in a row require fixes, stop and flag for human attention rather than continuing to patch.

Cost runaway
#

Every iteration of the autonomous loop costs money: API calls for generation, verification, and decision-making. A system that is stuck in a fix loop, or that is attempting overly ambitious changes, can spend a large amount of compute before anyone notices.

The system needs explicit budgets. A maximum number of iterations per change. A maximum cost per change. A maximum number of changes per day. When a budget is exceeded, the system pauses and logs the situation rather than continuing to spend.

What the Maintainer Does Once
#

The maintainer’s role in a self-evolving repository is concentrated entirely in the setup phase. Everything that happens after setup is automated.

The setup has five components.

The roadmap. A structured document defining the project’s purpose, non-goals, priorities, quality standards, and architectural principles. The roadmap is the single most important artifact. It determines whether the autonomous system makes good decisions or busy decisions.

The verification pipeline. The combination of test suites, static analysis, property tests, mutation tests, scenario validation, and adversarial verification that gates every change. This pipeline must be in place and passing before the autonomous loop starts. If the verification pipeline is weak, the autonomous system will ship low-quality code.

The decision policy. The rules that govern how the system prioritizes work. What signals does it monitor? How does it rank competing tasks? When does it work on bugs versus features versus technical debt versus documentation? This policy encodes the maintainer’s judgment about how to allocate attention.

The guardrails. Budget limits, blast radius constraints, escalation triggers, and rollback mechanisms. These are the safety systems that prevent the autonomous loop from causing damage when something goes wrong.

The learning infrastructure. The mechanisms by which the system records outcomes and feeds them back into future decisions. This includes the few-shot example library, the bug-to-test pipeline, and the decision evaluation framework.

Once these five components are in place, the autonomous loop can run. The maintainer’s ongoing involvement is limited to reviewing escalation reports, the small percentage of decisions that the system flags as requiring human judgment.

When This Makes Sense
#

Not every project should be a self-evolving repository.

This approach makes sense for projects with: clear specifications and measurable quality criteria, active user bases generating feedback signals, maintenance-heavy codebases where most work is bug fixes and incremental improvements, and well-established architectures where most decisions are routine.

It makes less sense for projects that are: in early exploration where direction changes frequently, in domains where correctness is safety-critical, in codebases with poor test coverage or unclear specifications, or where the competitive advantage is design taste that is hard to encode.

The threshold question is simple: can you write down the rules a competent maintainer would follow, well enough that following them mechanically produces good outcomes? If yes, the system can work. If no, the system will be autonomous but not effective.

The Deeper Question
#

A self-evolving repository raises a question that I explored in Scaling the LLM Agent Company: when the system is its prompts, its pipelines, and its proprietary context, what is the project?

The project is no longer its code. The code is a byproduct of the system that generates it. The project is the roadmap, the verification pipeline, the decision policy, the guardrails, and the accumulated learning data. These are the artifacts that determine what the project becomes.

This reframes what it means to maintain a project. The maintainer’s most important work is not writing code, reviewing pull requests, or fixing bugs. It is designing the system that does all of those things autonomously. The maintainer becomes an architect of processes rather than an executor of tasks.

The project that outlives this setup is the one where the autonomous system makes decisions the maintainer would have made, ships quality the maintainer would have approved, and evolves in a direction the maintainer would have chosen.

The question is whether you can encode your judgment well enough to make yourself unnecessary.

If you can, the project evolves without you. If you cannot, no amount of automation will compensate for the missing taste.

See also
#


Bringing Everyone to the Same Level: How Skills and LLMs Collapse Code Quality Variance

Every team has the same structure. A few engineers ship clean, well-tested, well-scoped work, and the rest ship work that mostly works. The gap between them is not typing speed. It is everything that happens before and after the typing: the steps they remember to run, the checks they know to perform, the conventions they learned by fixing past failures. That knowledge has always been the most valuable asset, and it has never scaled, because it existed in the heads of a small number of people.

LLMs alone do not fix this. They make everyone faster, which is a different thing from making everyone produce the same quality. Done naively, they widen the variance, because an LLM is a multiplier on the quality of the instructions it receives. The fix is to give every engineer, and every model, the same instructions. A shared set of instructions is what a skill is, and it is why skills are the mechanism that finally brings everyone to the same level.

The Variance Problem Has Always Been a Knowledge Problem
#

Ask yourself what actually separates the output of your strongest engineer from your weakest. It is rarely the language syntax. Both of them can write a function.

The difference is that the strong engineer, before touching code, performs a long list of steps they never write down. They look for an existing solution before building a new one. They write down what “done” means before they implement. They check whether the change can be undone. They name things the way the rest of the codebase names things. They write the edge case the junior would have forgotten. They run the linter, and they run it before opening the pull request, not after a reviewer asks.

Each of these is a step, and a step can be written down. For most of the history of software, these steps were passed on informally, by watching and working alongside others. You learned them by pairing with someone better, by getting review comments on your seventh PR, by breaking production once and remembering forever. This is mentorship, and it works, but it is slow, expensive, and uneven. It cannot keep up with a team that is hiring, and it cannot keep up with a codebase that is changing. The senior engineer’s advantage was always a process they followed in their head, and a process in a head does not scale.

LLMs Raise the Floor but Not the Ceiling
#

Drop an LLM into this situation and the naive expectation is that it makes everyone equal. It does not, at least not on its own.

What an LLM actually does is amplify whatever it is given. Give it a vague one-line prompt and you get back plausible, generic code that does not match your conventions and forgets your edge cases. Give it a precise specification, the relevant files, the patterns to reuse, and the checks to satisfy, and you get back code that is hard to distinguish from your best engineer’s output. The model is the same in both cases. The difference is entirely in the context, which, as I argued in The Importance of Context When Interacting with LLMs, is the entire mechanism by which a frozen set of weights produces behavior relevant to your situation.

The amplification effect has an uncomfortable implication. Before skills, the LLM made the senior engineer better and the junior engineer faster, and the gap between them stayed roughly the same. The senior engineer instinctively provides the context the model needs, because they know what good work requires. The junior engineer does not know what they do not know, so they ask for less, and they get less back. The tool that was supposed to democratize quality quietly reproduced the existing hierarchy, because it rewarded the same hidden knowledge that had always separated them.

A Skill Is the Senior Engineer’s Process, Made Executable
#

A skill is a versioned file, usually markdown, that tells an LLM agent exactly how to perform a task. Not a vague hint. The steps, in order, the checks to run before declaring success, the gates the output must pass, and the format the output should take. When the agent loads the skill, it stops improvising and follows the encoded process instead.

The difference between a skill and advice is where the comparison to a human matters. A junior engineer told to “go implement the feature” forgets half the steps, because they never fully learned them. A junior engineer, or even an autonomous agent, told to implement the feature through a skill cannot forget the steps, because the steps are in the prompt the model reads on every run. The skill is not advice the engineer might ignore. It is part of the execution path.

Think of it as the checklist idea from medicine and aviation, with one critical upgrade. A surgical checklist still depends on a tired human choosing to read it and choosing to follow it. A loaded skill does not depend on anyone’s discipline. The model follows it because the skill is the instruction, and following instructions is what the model does.

Why This Collapses the Variance
#

Run the same task through two engineers with two different LLMs, but the same skill, and watch what happens to the output. Both implementations start from the same specification step. Both run the same “have you checked for an existing solution?” step. Both end at the same verification step, with the same gates. The code they produce is not identical, but it converges toward a common standard, because the process that produced it is identical.

The thing that used to vary, the unwritten checklist inside each engineer’s head, is now constant. What varies is only the judgment applied at each step, and even that is bounded by the gates the skill enforces. Skills do not make everyone equally brilliant. They make everyone equally unable to skip the steps that matter, and skipping the steps that matter is what produced most of the variance in the first place.

The convergence logic is the same one I described, from the other direction, in Developer Trust Profiles. There I argued the ideal end state is one where “every contributor, senior engineer or new hire, funnels their work through agents that enforce the same standards,” so that authorship stops carrying signal and “the output converges into something homogeneous.” The trust profile was a transitional step, and the point was to make itself obsolete. Skills are the mechanism that completes that transition. They are the shared, enforced pipeline that makes the variance shrink in the first place.

The Leverage Moves Up, Again
#

If the junior engineer can now produce work that follows the senior engineer’s process, what is left for the senior engineer to do?

The answer is the same one that keeps appearing everywhere AI touches the development pipeline. The bottleneck moves up the decision chain, as in The Shifting Bottleneck, and the leverage moves with it.

The new seniority is not in running the skills. Anyone, and any agent, can do that. The new seniority is in writing them. Deciding what the process should be in the first place. Deciding which gate matters and which only looks like a gate. Deciding what “done” means for this kind of task, precisely enough that a model can enforce it. The judgment that used to be applied privately, one pull request at a time, is now applied once, at the skill level, where it benefits every future execution.

This shift is why a good skill library is one of the most valuable assets a team can hold. It is the institutional memory of how the team does things well, written in a form that executes itself instead of sitting in a wiki nobody reads. When a senior engineer leaves, the skills stay, and the standard stays with them. When a new engineer joins, they do not spend a year absorbing the conventions through code review. They load the skills on day one, and their first pull request already follows the team’s process.

What a Skill Has to Get Right to Actually Level the Field
#

Not every file labeled “skill” collapses the variance. Most do not. Writing a skill that raises everyone to the same level requires a few specific disciplines.

Encode the steps that are actually forgotten, not the steps everyone already does. A skill that says “write clean code” is worthless, because nobody sets out to write dirty code and the phrase carries no executable instruction. A skill that says “before implementing, run a search for existing solutions and list what you found” changes behavior, because that is exactly the step people skip when they are in a hurry.

Make the gates concrete enough for a model to evaluate. “Make sure it is well tested” is an opinion. “Write tests for the empty input, the maximum value, and the concurrent case, then run the suite and confirm it passes” is a gate. The whole mechanism depends on the model being able to check its own work, and it can only do that against criteria it can test. The need for testable criteria is the same reason I argued, in Rethinking Code Review in the Age of LLMs, that quality is a property of your constraints, not of your reviewers. The constraints in a skill are where the quality actually lives.

Keep the skill shorter than the attention it will receive. As the “lost in the middle” work showed, a model’s reliability degrades when a context fills with noise. A skill that tries to encode every possible consideration becomes a skill the model half-follows. Prefer several focused skills over one giant document, and cut anything the model would do correctly without being told.

Version and maintain it like the code it produces. A skill that encodes a convention from two years ago is worse than no skill, because it enforces a stale standard on every run. Treat the skill library as part of the codebase, with owners, review, and the same “does this still justify its cost?” scrutiny you would give any dependency.

The Realistic Limits
#

Skills only work for the part of the job that is describable, and it is worth being clear about where that stops.

Skills cannot encode taste that the writer cannot articulate. If your best engineer’s advantage is a feel for when an abstraction is about to collapse, and they cannot explain the signals they are reading, that advantage does not survive the translation into a file. It stays in their head, and the skill without it will produce work that is competent but not inspired. For the routine majority of software, which is most of software, the describable process is enough. For the genuinely hard design calls, it is not.

Skills can also lock in mediocrity. A skill that encodes a mediocre process enforces that mediocrity on everyone, consistently, forever. The mechanism is morally neutral. It collapses the variance in whichever direction the skill points, so a careless skill library can quietly lower a strong team to a lower common standard instead of raising the rest. The remedy is that the skills themselves have to be written by the people whose process you actually want to reproduce, and revisited when the process improves.

Finally, skills raise the standard for producing code, but producing code was never the only bottleneck. As Software Engineering Teams in the Age of AI argues, deciding whether a feature should exist, and writing a precise specification for it, remain human and valuable. Skills make the execution layer homogeneous. They do not decide what to execute.

What to Do Next
#

You do not need a grand migration to start closing the gap.

Pick the one task where your team’s output varies the most, the one where the senior engineer’s pull request looks nothing like the junior’s. Write down the steps your strongest engineer actually takes when they do it well, the steps they would never admit to because they seem obvious to them. Turn that into a skill, and run the next instance of that task through it, regardless of who is doing it.

Watch the output converge toward the standard. Then do it again for the next task. The variance does not close all at once. It closes one encoded process at a time, and each skill you add moves a piece of seniority from a private habit to a shared standard.

The team that wins in this era is not the one with the most powerful model. It is the one whose best engineer’s process runs, unchanged and unskipped, in every other engineer’s session.

See also
#

References
#


When Engineers Disagree on Best Practices: Surviving the Forming Stage

A new team’s first disagreement about best practices is not a warning sign. It is the moment the team actually starts becoming a team. What separates teams that survive this phase from those that stall is not which practices they adopt, but how they resolve the disagreement.

Why the Forming Stage Produces Disagreement
#

Every engineer arrives with a set of practices that worked somewhere else. The testing strategy that saved your last team from weekly regressions. The code review checklist that caught the security bug before it shipped. The branching model that kept releases sane. These are not random preferences. They are lessons from actual failures, and the engineer who brings them believes, correctly, that they matter.

The problem is that every other engineer on the new team arrives with their own set of practices, learned from different failures. When these differing practices meet, you get the pattern Bruce Tuckman described in his 1965 study of small-group development: forming, storming, norming, performing. Teams do not skip from forming straight to performing. They pass through storming, the phase where members test each other, surface conflicting assumptions, and negotiate how work should be done.

The forming stage is uniquely prone to practice disagreements for three reasons.

First, there is no shared history. On an established team, conventions are settled and invisible. The team just does things a certain way because that is how it has always been done. A new team has no “always,” so everything is up for debate at once.

Second, there is no established trust. Disagreeing about code style feels low-stakes on a team where everyone has shipped together for a year, because everyone knows the argument is about the style, not about competence. On a new team, the same disagreement can feel like a judgment of someone’s expertise, because no one has yet proven anything to anyone else.

Third, there is no shared vocabulary for resolving conflict. An established team has accumulated meta-norms: how they make decisions, who decides what, when to escalate, when to let something go. A new team has none of this. So every disagreement is also, simultaneously, a negotiation about how disagreements get resolved.

Most “Best Practice” Debates Are Preference Debates
#

The first thing to recognize is that the phrase “best practice” misleads. A best practice is a practice that is demonstrably better than its alternatives across most contexts. Very few software engineering practices meet that standard.

Consider the debates that take up the most discussion time on a new team. Tabs versus spaces. Git Flow versus trunk-based development. Story points versus no estimates. Unit tests versus integration tests as the primary signal. Commit message conventions. These are not best practices. They are conventions, and a convention’s value comes almost entirely from being shared, not from being optimal. The team that uses the second-best testing framework consistently will outperform the team that uses the best one inconsistently.

This does not mean all practices are equal. Some debates involve genuine principles where the choice has lasting consequences: whether to build or buy a critical dependency, how much to invest in observability before shipping, whether a monolith or services is the right starting architecture. These deserve careful deliberation. The skill is in telling the convention debate from the principle debate, because they require completely different resolution mechanisms.

Reversible Versus Irreversible Decisions
#

A distinction that Jeff Bezos drew in his 2016 letter to shareholders maps directly onto the convention-versus-principle problem. Some decisions are one-way doors. Once you walk through, you cannot come back. Choosing a primary database, committing to a cloud provider, or picking a framework that will permeate your codebase are one-way door decisions. These deserve heavy deliberation, because the cost of being wrong is high and the cost of reversing is higher.

Most practice debates on a new team are two-way doors. You can adopt trunk-based development, try it for six weeks, and switch back if it is not working. You can pick a linter configuration, discover it fights your codebase, and change it. Two-way door decisions should be made quickly, because the fastest way to learn is often to try.

A new team that treats every practice debate as a one-way door will spend its first two months in meetings and produce nothing. A team that treats a genuine one-way door as reversible will make a choice it regrets for years. The discipline is in classifying the decision before debating it.

The Experiment Mindset
#

The single most effective frame for resolving practice disagreement during forming is to treat practices as hypotheses, not as identities.

When two engineers disagree about whether to require code review before merge, the unproductive version of that conversation is an argument about whether code review is good. Both people hold their position, cite their favorite blog posts, and the discussion becomes about winning.

The productive version converts the disagreement into an experiment. You require code review for the next month, track how many defects it catches in review versus in production, and look at the data together at the end. Now the two engineers are not opponents. They are co-investigators running the same experiment.

The experiment works because it removes the requirement that someone be wrong. An engineer who advocated for mandatory review does not lose standing if the experiment shows it adds little value, because they ran the test in good faith. An engineer who opposed it does not lose standing if the experiment shows it catches actual bugs, because they agreed to look at the evidence.

A few rules make experiments work.

Time-box them. An open-ended experiment is just a delayed argument. Four to six weeks is usually enough to see signal without exhausting the team’s patience.

Define success criteria before you start. If you wait until the experiment is over to decide what would count as success, you will re-litigate the original disagreement with data instead of opinions, which is only marginally better.

Keep the cost of reversal low. If switching back after the experiment is expensive, the experiment is an irreversible decision.

Write down the result. A one-paragraph note recording what you tried, what you observed, and what you decided turns a single resolution into institutional memory that saves the next new team member from repeating the same debate.

How You Decide Matters More Than What You Decide
#

Here is the point that is easy to miss when you are in the middle of a heated debate about, say, whether to use feature branches or trunk-based development.

The specific answer matters less than you think. What matters enormously, especially during the forming stage, is that the team builds a repeatable process for reaching decisions together.

A team that resolves its first disagreement well, with a clear process, shared evidence, and a decision everyone can live with, has just created something more valuable than the practice it chose. It has created the template for the next disagreement, and the one after that. Each resolved disagreement makes the next one easier, because the meta-process is now familiar.

A team that resolves its first disagreement badly, by letting the loudest person win, or by avoiding the conflict until it grows worse, or by escalating to a manager who picks a winner, has also created a template. That template will repeat. The first disagreement a new team faces sets the pattern for every disagreement that follows.

That template is why the forming stage deserves deliberate attention rather than the hope that things will sort themselves out. The norms a team establishes in its first weeks tend to persist. They become “how we do things here,” and once that identity forms, it is far harder to change later than to set well at the start.

A Playbook for the First Disagreement
#

Concretely, when the first genuine practice disagreement surfaces on a new team, the following sequence works.

Name it. Say out loud that this is a disagreement about practice, that disagreements are expected, and that the goal is to find a resolution the team can commit to, not to identify who is right.

Classify it. Ask whether this is a convention debate, where value comes from consistency, or a principle debate, where the choice has lasting consequences. Ask whether it is a one-way door or a two-way door.

For conventions and two-way doors, decide fast. Default to whoever has to live with the consequence most directly, or to the option that is easiest to change, or to the existing convention if one already exists. Time-box the decision to one conversation, not three.

For principles and one-way doors, slow down. Write a short decision record capturing the options, the trade-offs, and the reasoning. Get the right people in the room, which usually means the people who will own the consequences, not necessarily the people with the strongest opinions.

Either way, record the outcome. A one-page architecture decision record or a paragraph in the team handbook turns a fragile verbal agreement into something a new hire can read six months later.

And then commit. The principle of disagree and commit applies here. Once the team has decided, even those who argued otherwise support the decision fully. A practice executed with full commitment and an imperfect choice beats a better practice executed with resentment and inconsistency.

When Someone Refuses to Commit
#

There is a failure mode worth naming.

Occasionally an engineer treats a practice debate as something they will not concede under any circumstance, refuses to accept the team’s decision, and continues working the old way. This is no longer a disagreement about practice. It is a disagreement about whether the team’s decision-making process has legitimacy.

This must be addressed directly, and early. A team that tolerates a member who overrides collective decisions after they are made has no process at all. It has a veto by the most stubborn person. The conversation is no longer about the practice. It is about participation in a shared system.

In a healthy forming team, this conversation is rare, because the process itself was fair enough that losing a debate does not feel like losing status. When it does happen, resolving it cleanly, with empathy but without accommodation, is one of the most important trust-building acts the team will undertake. Underneath that trust is psychological safety, the shared belief that you can speak up, dissent, and even be wrong without being humiliated, which Google’s Project Aristotle identified as the single strongest predictor of team effectiveness. A team cannot storm productively without it.

The Payoff
#

A team that navigates its forming-stage disagreements well emerges with two assets.

The first is a set of shared conventions, probably imperfect, that let the team move fast without relitigating every choice. The second, and more important, is a shared process for handling the next thing they do not yet agree on.

New disagreements never stop. The codebase grows, the team grows, technology changes, and yesterday’s settled practice becomes tomorrow’s debate. The teams that handle this well are not the ones that picked the best practices on day one. They are the ones that learned, during forming, how to disagree productively, decide efficiently, and commit fully.

That skill compounds for the life of the team.

See also
#

References
#


Managing Many Concurrent LLM Agent Sessions

When one person can spawn a dozen LLM agent sessions in parallel, the bottleneck is no longer the agents. It is the human trying to keep track of them all. Your working memory holds a handful of items at best, your context switching cost is measurable, and every interruption leaves residue that degrades the next task you pick up. The question is not whether you can run 20 sessions simultaneously. The question is whether you can remain effective while doing so.

The Cognitive Bottleneck
#

Before discussing strategies, it helps to understand what actually limits a human supervisor.

Working memory is tiny. George Miller’s foundational work suggested that humans can hold approximately seven plus or minus two items in short-term memory. Later research by Nelson Cowan revised this estimate down to about four chunks for young adults. Either way, the number is small. If each agent session requires you to remember its goal, its current state, its blockers, and the decision you were about to make, you hit the ceiling at three to five sessions, not twenty.

Context switching has a measurable cost. Research on task switching in cognitive psychology shows that switching between tasks incurs a time penalty, sometimes called “switch cost.” This cost ranges from fractions of a second to several minutes depending on task complexity. When you switch from agent session A to agent session B, you must offload A’s context from your working memory, load B’s context, and reconstruct where you left off. The more complex each session’s context, the more expensive the switch.

Attention residue accumulates. When you switch from one task to another without fully completing the first, part of your attention remains stuck on the previous task. The Zeigarnik effect describes how unfinished tasks persist in memory, consuming cognitive resources even when you are trying to focus on something else. With multiple agent sessions running concurrently, many are perpetually unfinished, creating constant attention residue.

Decision fatigue compounds. Each agent session requires you to make decisions: approve this output, redirect this approach, answer this clarifying question. Research on decision fatigue shows that the quality of decisions degrades after a long session of choice-making. Supervising many sessions means making many decisions, and the later decisions in the day will be worse than the earlier ones unless you manage the load.

Working memory, switch cost, attention residue, and decision fatigue together define the ceiling on how many sessions you can manage. Every strategy below works by attacking one or more of them.

Strategy 1: Externalize All Session State
#

The single most powerful principle is to stop relying on your brain to hold session state. If it is not written down, it is consuming working memory.

Maintain a state file per session. Every agent session should produce a machine-readable and human-readable state file that captures its current status, goal, progress, blockers, and next steps. When you return to a session after an hour or a day, you read the file, not your memory. The file is the single source of truth. This state file is the agent equivalent of the workstack concept applied at scale.

Use structured status summaries. The state file should follow a consistent template across all sessions. A format like the following works well:

Session: fix-auth-bug
Goal: Fix OAuth callback failing on production
Status: blocked
Progress: Identified root cause (clock skew on load balancer)
Blocker: Need production access to verify fix
Next: Deploy fix to staging, verify callback works

When every session uses the same structure, you can scan a dashboard of twenty sessions in under a minute. The consistency eliminates the cognitive parsing overhead that comes from varied formats.

Write decisions to a log. Every decision you make about a session should be written to the session’s log file with a timestamp and rationale. Writing to the log serves two purposes. First, when you return to the session, you can see not just where it is but why it is there, without reconstructing the reasoning. Second, the log becomes an audit trail that other humans (or agents) can consume.

Checkpoint before switching. Before switching away from any session, write a one-line “resume hint” that tells your future self exactly where to pick up. This takes five seconds and saves minutes of reconstruction time. The hint should be specific enough that you can act on it without re-reading the entire session history.

Strategy 2: Standardize All Interactions
#

Variability prevents scaling. Every difference between how sessions operate is a new thing your brain must parse, increasing cognitive load and switch cost.

Use a uniform session protocol. Every session should follow the same lifecycle: initialization, execution, checkpointing, escalation, and completion. The protocol defines what the agent does at each stage, what it reports, and how it signals that it needs human input. When the protocol is uniform, switching between sessions costs less because you always know what to expect.

Standardize output formats. If session A reports progress in a bulleted list and session B writes a paragraph and session C uses a table, your brain must context-switch not just on content but on format. Pick one format and enforce it everywhere. Markdown headers, numbered lists, and consistent section ordering let your eyes scan quickly without parsing structure.

Establish a shared vocabulary. Define the terms agents use to communicate status: “blocked,” “waiting-for-human,” “in-progress,” “complete,” “failed.” Every agent uses the same words to mean the same things. A shared vocabulary reduces ambiguity and eliminates the need to translate between sessions.

Create session naming conventions. A session named “auth-fix-3” tells you nothing when you have twenty sessions. A session named “fix-oauth-callback-prod-bug” tells you exactly what it is doing. Descriptive names reduce the time to identify and triage sessions, which reduces the overhead of switching between them.

Strategy 3: Make Interactions Asynchronous and Batched
#

Synchronous interaction with agents prevents parallelism. If you must respond to each agent within seconds, you can only effectively manage one at a time.

Let agents run to a natural stopping point. Instead of watching each agent in real-time and responding to each question as it arises, configure agents to work until they hit a genuine blocker. At that point, they checkpoint their state and wait. You then process the batch of blocked agents at your convenience.

Batch your decisions. When multiple sessions need human input, collect all the requests and process them in a single sitting. Batching is more efficient than context-switching for each one individually, because you stay in the same cognitive “mode” for the entire batch. Five decisions made in one focused session will be faster and higher quality than the same five decisions made across five interruptions.

Time-box your session reviews. Instead of monitoring sessions continuously, check them at fixed intervals: every 30 minutes, every hour, twice a day. This creates predictable rhythm and eliminates the reactive, interrupt-driven pattern that fragments attention. Between check-ins, agents work autonomously and you focus on other things.

Coalesce interruptions. If you have three sessions that will each need input within the next 15 minutes, do not handle them one at a time. Wait until all three are ready, then handle them together. Interruption coalescing is a well-known technique in operating systems and it applies equally to human attention management.

Strategy 4: Make Agents Resolve Their Own Ambiguities
#

Every question an agent asks you is a context switch. The fewer questions it asks, the more sessions you can manage.

Provide rich default resolution rules. Instead of letting agents escalate every ambiguity, give them standing instructions for common situations. “If the test suite fails, attempt to fix it before escalating.” “If a dependency is missing, install it from the lockfile.” “If a naming convention is unclear, follow the pattern used in the nearest file.” These defaults eliminate a large fraction of questions that would otherwise interrupt you.

Define escalation thresholds explicitly. Agents should escalate to you only when they cross a defined threshold: they have tried multiple approaches and failed, they need access they do not have, or they are about to make an irreversible decision. Everything else should be handled autonomously. The clearer the threshold, the fewer unnecessary escalations.

Allow agents to ask other agents. If you have multiple sessions running, agents can sometimes resolve each other’s questions. An agent that needs to understand how another part of the system works can query the agent working on that part. This peer-to-peer resolution keeps you out of the loop for routine coordination.

Use confidence-based escalation. Agents that can express confidence in their outputs can be configured to escalate only when confidence drops below a threshold. High-confidence work proceeds without interruption. Low-confidence work pauses for review. Confidence-based escalation naturally filters your attention toward the sessions that need it most.

Strategy 5: Use Hierarchical Orchestration
#

The most effective way to manage many agents is to not manage them all directly.

Introduce orchestrator agents. Instead of you supervising 20 worker agents, have 4 orchestrator agents each supervise 5 workers. You interact only with the orchestrators, who aggregate and summarize their workers’ status. This reduces your direct interaction count by a factor of 5 and lets you operate within your working memory limit. This mirrors the pattern described in Scaling the LLM Agent Company, where the orchestration layer replaces the human management layer.

Fan-out and fan-in. Give an orchestrator a single high-level task. It decomposes the task, assigns subtasks to workers, collects results, and presents you with a synthesized output. You never interact with the individual workers. Your cognitive load is the same whether the orchestrator manages 3 workers or 30.

Assign team leads per domain. If your agents work across multiple domains (frontend, backend, infrastructure, testing), assign one orchestrator per domain. Each domain lead reports to you with a one-paragraph summary. You manage four domain leads instead of twenty individual contributors.

Layer the hierarchy as needed. For very large numbers of agents, add another layer. A chief orchestrator manages domain orchestrators, who manage team leads, who manage workers. Each layer compresses information, so you always deal with a manageable number of direct reports.

Strategy 6: Practice Progressive Disclosure
#

You should never need to understand everything about every session to make a decision. Information should arrive in layers, from summary to detail, on demand.

Lead with the one-line summary. Every session status should begin with a single sentence that tells you whether action is needed: “Session blocked, needs your decision on X” or “Session running normally, 60% complete.” If the summary says no action needed, you move on. You never read the details unless the summary demands it.

Provide drill-down on request. Behind the one-line summary is a paragraph. Behind the paragraph is the full session log. Behind the log are the raw outputs. Each layer is available but not shown by default. Progressive disclosure keeps your default view clean and your cognitive load low.

Use severity indicators. Color-code or tag sessions by urgency: red for “needs immediate human input,” yellow for “proceeding but with a risk you should know about,” green for “all clear.” Your eye scans for red first, yellow second, and ignores green. This triage happens in seconds, not minutes.

Summarize at the right granularity. An orchestrator reporting “all five workers are making progress” is useless if one of them is about to make a critical mistake. An orchestrator reporting every token from every worker is noise. The right granularity is: what is done, what is in progress, what is blocked, and what decision you need to make next.

Strategy 7: Isolate Sessions from Each Other
#

Interference between sessions is a major source of cognitive load. When sessions share state, a change in one can invalidate your understanding of another.

Enforce session independence. Each session should operate in its own workspace, with its own file tree, its own dependencies, and its own state. Sessions should not mutate shared resources that other sessions depend on. Session independence prevents the “I changed something in session A that broke session B” problem, which is one of the most expensive debugging scenarios when managing many sessions.

Use separate branches or worktrees. If sessions work on the same repository, each should use its own git worktree or branch. Separate worktrees ensure that the output of one session does not corrupt the working state of another. When you are ready to integrate, you merge branches deliberately, not accidentally.

Scope tools and permissions per session. Each session should have access only to the tools and resources it needs. A session fixing a frontend bug does not need database write access. Scoping permissions reduces the impact of errors and eliminates cross-session interference.

Make session boundaries explicit. When you switch from session A to session B, the boundary should be clear. Close session A’s workspace, open session B’s. The physical or virtual separation reinforces the cognitive separation.

Strategy 8: Build a Session Dashboard
#

When managing more than three or four sessions, you need a single view that shows all of them at once. Switching between terminal tabs or IDE windows to check on each session is itself a form of context switching.

Create a single pane of glass. A dashboard that lists all active sessions with their status, progress, and severity indicators lets you assess every session in one glance. This dashboard can be as simple as a generated text file or as sophisticated as a web UI. The key is that it presents all sessions in a uniform, scannable format.

Surface only actionable information. The dashboard should show you what needs your attention, not everything that is happening. Sessions running normally should be collapsed or summarized. Sessions that need input should be highlighted. The dashboard is a triage tool, not a log viewer.

Automate status collection. The dashboard should be generated automatically from the state files that each session produces. No manual updates. If a session’s state file changes, the dashboard reflects it. Automatic generation eliminates the overhead of manually polling each session for status.

Add alerting for critical events. Instead of watching the dashboard continuously, configure alerts for events that genuinely require immediate attention: a session failed, a session is about to make an irreversible change, a session has been blocked for more than N minutes. Everything else can wait for your next scheduled review.

Strategy 9: Use AI to Triage AI
#

One of the most effective ways to manage many agent sessions is to use an LLM to help you manage them.

AI-assisted summarization. Before you look at a blocked session, have an LLM read the session log and produce a one-paragraph summary of what happened, what the blocker is, and what decision you need to make. The summary saves you from reading potentially hundreds of lines of conversation history. The summary gets you to the decision point faster.

AI-assisted triage. When you return from a break and find 12 sessions waiting for input, have an LLM rank them by urgency and group similar requests. “Sessions 3, 7, and 12 all need the same decision about database schema, you can answer them together.” “Session 9 is urgent, it is blocked on a production deploy.” This pre-processing reduces your cognitive load and helps you batch effectively.

AI-assisted decision drafting. For each blocked session, have an LLM propose a decision with rationale. You review the proposal, accept it or modify it, and move on. Reviewing a well-formed proposal is faster and less cognitively demanding than constructing a response from scratch, especially for the tenth session of the day.

AI-assisted state reconstruction. When you return to a session after days away, have an LLM produce a “previously on” summary that reconstructs the relevant context. A reconstructed summary is faster and more reliable than reading the full log yourself, and it handles the long-gap resumption problem that pure state files struggle with.

Strategy 10: Manage Your Own Cognitive Resources
#

The human supervisor is a finite resource. Strategies that optimize agent throughput while ignoring human cognitive limits will fail.

Chunk sessions into clusters. Instead of treating 20 sessions as 20 independent items, group them into 4 clusters of 5. Each cluster shares a domain, a goal, or an orchestrator. You manage 4 clusters, not 20 sessions. This grouping leverages chunking, the same memory technique that lets experts recall complex board positions in chess.

Dedicate focus blocks. Reserve uninterrupted blocks of time for session supervision. During a focus block, you process the queue of blocked sessions, make decisions, and unblock agents. Outside of focus blocks, you do other work. Focus blocks prevent the constant low-level attention drain that comes from monitoring sessions while trying to do other things.

Reserve decision budget for high-stakes sessions. Not all decisions are equal. A decision about whether to deploy to production matters more than a decision about variable naming. Allocate your decision budget accordingly. Make the important decisions when you are fresh. Batch the trivial decisions and make them in a low-energy slot.

Take recovery breaks. Context switching and decision-making are cognitively expensive. After a heavy session of supervising multiple agents, take a deliberate break to let attention residue dissipate. This is not laziness, it is maintenance of the resource that matters most: your judgment.

Strategy 11: Reduce Session Count Through Better Task Definition
#

The easiest session to manage is the one you never needed to start.

Write precise specifications. A well-specified task runs autonomously and produces fewer questions. A vaguely specified task generates constant clarifying questions, each of which is a context switch. Investing in specification quality before launching a session benefits the session for its entire lifetime. This is the same principle described in The Importance of Context When Interacting with LLMs: the quality of the context determines the quality of the output.

Merge related tasks. If two tasks touch the same code or the same domain, combine them into one session instead of running two. One session managing a coherent area is easier to track than two sessions with overlapping scope.

Eliminate unnecessary sessions. Before launching a new session, ask whether the task is worth the supervision overhead. Some tasks are faster to do yourself than to delegate to an agent and then supervise. The shifting bottleneck principle applies: as agent supervision becomes the bottleneck, the optimal strategy shifts from spawning more agents to being more selective about which tasks to delegate.

Pre-compute common answers. If you find yourself answering the same type of question across multiple sessions, encode the answer once in a shared resource that all sessions can access. A shared FAQ, a coding standards document, or a decision tree eliminates a category of future questions.

Strategy 12: Version and Review Session Patterns
#

Managing many sessions is a skill that improves with deliberate practice.

Record what worked. When a session runs smoothly from start to finish, note what made it smooth. Was the specification clear? Were the escalation thresholds well-calibrated? Did the status format make triage easy? These observations compound into better patterns over time.

Post-mortem failed sessions. When a session drifts from its instructions, review why. Did the agent lack critical context? Did the escalation threshold let it run too long without checking in? Did the state file fail to capture the information you needed to intervene effectively? Each failure is a data point for improving the protocol.

Iterate on the protocol. The session protocol, the status format, the escalation rules, and the dashboard design should all evolve as you learn what works. Treat your agent management system as a product that you are continuously improving. The goal is not to find the perfect system on day one but to get a little better every week.

Share patterns across teams. If multiple people in your organization supervise agent sessions, share what works. The patterns that reduce cognitive load for one person will likely help others. This is the same knowledge-sharing principle that makes software engineering teams effective: institutional knowledge compounds when it is explicit and shared.

The Ceiling: How Many Can You Actually Manage?
#

There is no single number, but the constraints are concrete.

With no system, no tooling, and synchronous interaction, a human can effectively manage 1 to 2 concurrent agent sessions. The cognitive overhead of tracking each session’s state in working memory and responding to questions in real time is too high to scale further.

With externalized state files, standardized protocols, and asynchronous batching, 4 to 6 concurrent sessions become feasible. You are no longer holding state in your head, and you batch your interactions to reduce switch cost.

With hierarchical orchestration, progressive disclosure, and a dashboard, 10 to 15 concurrent sessions are manageable. The orchestrators compress information, the dashboard provides a single view, and you interact with a manageable number of direct reports.

With AI-assisted triage, tight escalation thresholds, and well-specified tasks, 20+ concurrent sessions are achievable for a human who has invested in the workflow. At this scale, you are no longer managing individual sessions. You are managing a system that manages sessions.

The progression mirrors the task-stack philosophy: you push interruptions onto a stack, handle them in batches, and pop them off when done. The difference is that the stack now contains not just your own tasks but the states of dozens of autonomous workers, and the stack is externalized rather than in your head.

The Meta-Principle
#

Every strategy in this article reduces to one of four operations on the cognitive constraints:

Reduce working memory load by externalizing state, standardizing formats, and chunking sessions into clusters.

Reduce switch cost by standardizing protocols, batching interactions, and isolating sessions.

Reduce attention residue by letting sessions reach natural stopping points, taking recovery breaks, and using focus blocks.

Reduce decision fatigue by having agents resolve their own ambiguities, using decision templates, and reserving decision budget for high-stakes choices.

The agents are not the bottleneck. Your brain is. Every hour you spend engineering the workflow around your own cognitive limits is worth ten hours of trying to work through them.

References
#


slack-cached - Cache Slack threads, channels, and users to a local SQLite database

Note (2026-07-02): slack-cached has been renamed to slackx.

Slack is where your team’s decisions live, but the data doesn’t belong to you. Search is slow, threads become hard to find, and the moment you leave a workspace the history is gone. There’s no official CLI, and the web client is the only first-class way to read anything.

I built slack-cached to fix this. It’s a small Python CLI that caches Slack threads, channel messages, users, and channels to a local SQLite database. Once cached, the data is yours: query it with SQL, grep it, feed it to an LLM, or just read it offline.

The problem
#

Slack makes exporting and archival surprisingly hard. Export tools exist for admins, but most members aren’t admins. The search box returns messages, but not in a form you can slice, join, or version. Important decisions get lost in threads that nobody scrolls back to.

The problem gets worse when you want to do anything programmatic. Building a knowledge base, summarizing a channel, or tracking decisions all require raw access to the messages. Hitting the Slack API on demand works, but you incur the latency and rate-limit cost every time, and edits disappear if you only ever fetch live.

How slack-cached works
#

$ uv sync
$ uv run slack-cached --help
usage: slack-cached [-h] {fetch,show,fetch-users,fetch-channels,show-users,show-channels,poll} ...

Cache Slack threads to a local SQLite database.

Cache a thread from a URL (no stdout, just a one-line summary on stderr):

$ slack-cached fetch https://acme.slack.com/archives/C0123ABCDEF/p1700000000123456
cached 3 messages (3 new/updated, full) for C0123ABCDEF/1700000000.123456

Or by explicit channel and timestamp; run it again and it only asks Slack for what changed:

$ slack-cached fetch --channel C0123ABCDEF --ts 1700000000.123456
cached 3 messages (3 new/updated, full) for C0123ABCDEF/1700000000.123456

$ slack-cached fetch --channel C0123ABCDEF --ts 1700000000.123456   # run again
cached 4 messages (1 new/updated, incremental) for C0123ABCDEF/1700000000.123456

Read it back, human-readable by default:

$ slack-cached show https://acme.slack.com/archives/C0123ABCDEF/p1700000000123456
Thread C0123ABCDEF/1700000000.123456
3 message(s)

[2023-11-14T22:13:20+00:00] Alice Smith (alice)
    Has anyone tried the new deploy script?

[2023-11-14T22:15:12+00:00] Bob Lee (bob)
    Yes, but you need to bump the token first.

[2023-11-14T22:16:45+00:00] Alice Smith (alice)
    Thanks!

show auto-fetches if the thread isn’t cached yet, and renders display names like Alice Smith (alice) instead of raw user ids once you’ve cached users. Pass --json to get the raw records for piping into other tools:

$ slack-cached show --json https://acme.slack.com/archives/C0123ABCDEF/p1700000000123456
{
  "channel": "C0123ABCDEF",
  "channel_name": "deploys",
  "thread_ts": "1700000000.123456",
  "message_count": 3,
  "messages": [
    {
      "ts": "1700000000.123456",
      "user": "U1",
      "text": "Has anyone tried the new deploy script?",
      "payload": { "ts": "1700000000.123456", "user": "U1", "type": "message" },
      "user_name": "Alice Smith (alice)"
    },
    {
      "ts": "1700000112.000200",
      "user": "U2",
      "text": "Yes, but you need to bump the token first.",
      "payload": { "ts": "1700000112.000200", "user": "U2", "type": "message" },
      "user_name": "Bob Lee (bob)"
    },
    {
      "ts": "1700000205.500300",
      "user": "U1",
      "text": "Thanks!",
      "payload": { "ts": "1700000205.500300", "user": "U1", "type": "message" },
      "user_name": "Alice Smith (alice)"
    }
  ]
}

Channels, users, and polling
#

Cache every top-level message in a channel:

$ slack-cached fetch --channel C0123ABCDEF
cached 142 messages for C0123ABCDEF (38 fetched)

Add --full-threads to also pull every reply thread, so you get the full conversation tree:

$ slack-cached fetch --channel C0123ABCDEF --full-threads
cached 318 messages for C0123ABCDEF (214 fetched, 47 threads with replies fetched)

For continuous capture, poll watches multiple channels and fetches new messages on a schedule. Each entry in --channels can be a channel id, a bare name (general), or a #-prefixed name (#general); names are resolved against the cached channels, which is why the per-cycle summary below reports ids. Progress goes to stderr, and (because of --json) one compact JSON object per cycle goes to stdout. Ctrl+C stops it gracefully:

$ slack-cached poll --channels #general,#engineering,#deploys --interval 5m --last 5m --full-threads --json
polling 3 channel(s) every 5m (lookback: 5m, full_threads: True, concurrency: 3)
{"cycle": 1, "elapsed_seconds": 1.234, "channels": [{"channel": "C001", "fetched": 3, "total": 312}, {"channel": "C002", "fetched": 4, "total": 188}, {"channel": "C003", "fetched": 0, "total": 27}]}
cycle 1: 7 new message(s) across 3 channel(s) in 1.2s
^C
poll stopped after 1 cycle(s)

That stdout stream is easy to wire into a downstream pipeline or a knowledge-base builder.

Finally, cache the workspace’s users and channels so threads can be rendered with display names:

$ slack-cached fetch-users
processed 184 users (184 added, 184 total in db)
$ slack-cached fetch-channels
processed 37 channels (37 added, 37 total in db)
$ slack-cached show-users
184 user(s)

U1  alice - Alice Smith
U2  bob - Bob Lee
...
$ slack-cached show-channels --json
{
  "channel_count": 37,
  "channels": [
    {
      "id": "C1",
      "name": "general",
      "is_private": false,
      "fetched_at": 1700000260.0,
      "payload": { "id": "C1", "name": "general", "is_channel": true }
    },
    ...
  ]
}

The refresh strategy
#

fetch always contacts Slack, but it’s incremental. On a re-fetch, it calls conversations.replies with oldest=<latest_cached_ts>, so the API returns only new replies and any edits at the boundary. Messages are upserted by ts, which means edits replace the old text in place instead of creating duplicates.

Rate limits are handled for you. HTTP 429 / ratelimited responses are retried with exponential backoff, up to five attempts, respecting the Retry-After header. That matters for --full-threads and poll, where you can easily make hundreds of calls against a busy channel.

Where the cache lives
#

The database defaults to $XDG_CACHE_HOME/slack-cached/threads.db (typically ~/.cache/slack-cached/threads.db). Override it per command with --db /path/to/file.db. Because it’s plain SQLite, you can open it directly and ask it anything Slack’s search box can’t:

$ sqlite3 ~/.cache/slack-cached/threads.db \
    "SELECT (u.real_name || ' (' || u.name || ')') AS who, COUNT(*) AS msgs
     FROM messages m JOIN users u ON m.user = u.id
     WHERE m.channel = 'C0123ABCDEF'
     GROUP BY who ORDER BY msgs DESC LIMIT 5;"
Alice Smith (alice)|142
Bob Lee (bob)|87
Carol Ng (carol)|53

That query is the main point of the tool. The cache is not an opaque blob; it’s a table of messages you can SELECT from, join against users and channels, and export however you like.

Authentication
#

Credentials load from environment variables first, then a config file at $XDG_CONFIG_HOME/slack-cached/config:

SLACK_TOKEN=xoxb-...
SLACK_COOKIE=...
SLACK_API_BASE_URL=https://slack.com/api

SLACK_COOKIE is there for xoxc- web-client tokens, which need the matching cookie to authenticate. Every command also accepts --api-base-url, which is how the built-in fake Slack server plugs in.

A fake Slack server, for free
#

The repo includes slack-fake-server, a deterministic fake Slack API for testing:

$ uv run slack-fake-server --port 8199 --num-threads 50 --rate-limits
2026-06-15T12:00:00Z [info     ] fake_slack_server_starting   host=127.0.0.1 port=8199 seed=42 users=8 channels=5 threads=50 rate_limits=True

It serves conversations.list, conversations.replies, conversations.history, and users.list, and can simulate Slack-tier rate limiting. Point slack-cached at the fake server and you can develop and test against a realistic API without touching your own workspace:

$ slack-cached fetch --api-base-url http://localhost:8199/api --channel C0123ABCDEF --full-threads
cached 318 messages for C0123ABCDEF (214 fetched, 47 threads with replies fetched)

When this is useful
#

  • Knowledge bases and channel summaries. Poll a set of channels, then build monthly digests or feed the SQLite cache to an LLM.
  • Decision tracking. Important calls often happen in threads. Cache them so they survive workspace churn and account turnover.
  • Offline access and archival. Keep a readable copy of the conversations you actually care about, independent of Slack’s retention window.
  • Bulk analysis. Once the data is in SQLite, you can answer questions with a query that Slack’s search box can’t express.

What to Do Next
#

$ git clone https://github.com/TomzxCode/slack-cached
$ cd slack-cached
$ uv sync
$ export SLACK_TOKEN=xoxb-...
$ slack-cached fetch-channels
processed 37 channels (37 added, 37 total in db)
$ slack-cached fetch --channel C0123ABCDEF --full-threads
cached 318 messages for C0123ABCDEF (214 fetched, 47 threads with replies fetched)
$ slack-cached show https://acme.slack.com/archives/C0123ABCDEF/p1700000000123456
Thread C0123ABCDEF/1700000000.123456
3 message(s)

[2023-11-14T22:13:20+00:00] Alice Smith (alice)
    Has anyone tried the new deploy script?

[2023-11-14T22:15:12+00:00] Bob Lee (bob)
    Yes, but you need to bump the token first.

[2023-11-14T22:16:45+00:00] Alice Smith (alice)
    Thanks!

Repository and source code, documentation.

References
#


Developer Trust Profiles: Earned Scrutiny for Automated Code Review

When you automate code review, the obvious design is to treat every pull request identically. Same checks, same threshold, same outcome, regardless of author. This obvious design is wrong.

A reviewer that applies the same scrutiny to a developer who has shipped two hundred clean PRs and to a stranger on their first contribution is either too strict for the first or too lax for the second. It is a blunt instrument, and bluntness prevents autonomy. The more you rely on automated approval to unblock work, the more you need a mechanism that varies scrutiny by evidence.

I built that mechanism as a pair of skills in my agent library, developer-trust-profile and initialize-developer-trust-profile. This is the idea behind them, and why I believe it is the missing primitive for autonomous code review.

The problem with a reviewer that has no memory
#

Most automated review tools are stateless. They look at a diff, run their checks, and emit a verdict. Next PR, they start over, as if they had never seen the author before.

Stateless review is not how any experienced human reviewer works. When a trusted colleague opens a PR, you skim it, confirm the tests pass, and approve. When someone with a history of breaking the build opens one, you read every line. You already know who tends to forget tests, who mixes unrelated refactors into a single commit, who writes the clearest descriptions. That knowledge is not bias to be eliminated; it is signal, accumulated over hundreds of reviews, that tells you where to spend your attention.

A stateless automated reviewer throws all of that signal away. It re-derives, badly and from scratch, what a human reviewer simply remembers. The result is a system that either approves indiscriminately (unsafe) or applies maximum suspicion to everyone (slow, and damaging to trust).

The trust profile is the answer to a direct question: how does an automated reviewer remember?

What a trust profile stores
#

Each developer gets a single file, ~/.developer-trust/{github_username}.md, and the whole directory is a git repository. The file accumulates observations across every review.

Four trust levels exist, and each one maps to a different behavior, not just a different label:

Level Meaning Effect on the automated reviewer
trusted Consistently clean, high-quality PRs Standard checks, lean toward approval on borderline cases
neutral Unknown or mixed track record Standard checks, default behavior
cautious History of issues, missed edge cases, unclear PRs Stricter interpretation, flag marginal cases as failures
always_reject Persistent quality or policy issues Skip entirely, never auto-approve, require manual review

Beyond the level, the profile keeps a running overview, lists of strengths and weaknesses observed across PRs, recurring PR patterns, and a full review history table with dates, repos, outcomes, and a one-line note per review.

Crucially, it is plain markdown in a git repo. That makes the system’s memory auditable, diffable, and portable. You can see the exact review that tipped an author from neutral to cautious, and the reasoning behind it. For a mechanism that gates code into production, that written record is not a nice-to-have; it is a requirement.

How earned trust changes review behavior
#

The levels are not decorative. They modify how the automated reviewer interprets its own checks.

The reviewer I run, quick-pr-review, evaluates a fixed set of gates: significant public interface changes, security-sensitive code, new dependencies, reversibility, passing CI, alignment with the linked issue’s acceptance criteria. For a neutral author, each gate is binary. For a trusted author, a borderline call (is this small new export a “significant” public interface change?) leans toward passing. For a cautious author, that same borderline call is treated as a failure.

This is differential scrutiny, and it is the whole point. The checks are identical; the threshold moves with evidence. A trusted author gets unblocked faster because the reviewer stops re-examining cases it has effectively already resolved. A cautious author gets caught earlier, before a recurring weakness reaches production again.

And then there is always_reject, the hard stop. When an author sits at that level, the reviewer does not fetch the diff, does not post a comment, does not approve. It reports that the PR was skipped and that a human needs to look. This is the system saying: I have enough evidence to know I should not be making this decision.

The loop closes
#

The profile is not a static config file that a human maintains by hand. It is both consumed and produced by the review pipeline.

Before a review, quick-pr-review reads the author’s profile to set its thresholds. After the review, it writes back: it appends a row to the history, merges new observations into the strengths and weaknesses, and reconsiders the trust level if the accumulated evidence warrants it. Each profile update is committed to the local git repo with a message like Update alice trust profile (approved: acme/api#42).

This makes the system learn gradually rather than judge. A single review does not move the trust level, unless it is egregious. Trust degrades through recurring patterns observed across many PRs, and the level only shifts when the weight of evidence demands it. That hysteresis is deliberate. It prevents one poor review from permanently labeling a developer, and it prevents one PR that passed by chance from granting unwarranted autonomy.

Bootstrapping without survivorship bias
#

The hardest part of any reputation system is the cold start. A new contributor, or a contributor new to the system, starts at neutral with an empty file. That is safe but unhelpful; you want a profile based on actual history, not an empty file that takes months to fill.

The initialize skill bootstraps a profile from history by scanning the author’s last N pull requests across every repository the token can see. The detail that separates this from a naive implementation is that it samples both merged and rejected PRs.

Sampling only merged PRs is survivorship bias in its purest form. It would make every developer look good, because the failures were quietly closed and forgotten. By pulling closed-without-merge PRs as well, and by deriving the outcome from actual review data (was there a changes-requested that was never superseded?) rather than assuming merge equals approval, the bootstrap produces a profile that reflects how a developer actually works, not just their wins.

Processed oldest-first, each historical PR feeds into the same update pipeline as a live review, so a freshly initialized profile looks exactly like one that was built up review by review over time.

Limitations
#

The design has genuine trade-offs, and they are worth naming.

Profiles live on the reviewer’s machine and are not shared. This is a feature (no global reputation database, no public scoring of humans) and a cost (each reviewer builds a different picture, and the memory does not transfer). For a single operator running their own agents, that is the right trade. For an organization, you would want a shared, access-controlled store, and the design does not claim otherwise.

There is a risk that a reputation becomes fixed and a developer cannot escape it. The skills mitigate this by removing observations that recent evidence contradicts and by reconsidering the level on every update. But any system that summarizes a human into a label can trap them in that label. The fix is transparency (everything is in a diffable file) and a human who can edit the file when the summary is wrong.

There is also a gaming risk. Once authors know a trust system exists, they can optimize for it. On balance this is fine: optimizing for clean, well-tested, well-scoped PRs that reference their issues is exactly the behavior you wanted anyway. The system fails safe, because the worst case is that people produce better PRs.

Why this matters more in the age of LLM-authored code
#

I have written elsewhere about why human review of LLM-generated code is a poor use of attention (see The Future of Code Review). The short version is that the human’s leverage has moved upstream, to specifying the problem, while machines verify compliance.

But that argument is incomplete. Even after you accept automated review, you are left with a follow-up question: should the automated reviewer treat every author identically? The answer is no, and for a reason that is sharper now than it was five years ago.

In an LLM-heavy workflow, the “author” of a PR is increasingly a human paired with a model, or an agent operating on a human’s behalf. The trust profile stops measuring just a developer’s coding skill and starts measuring something more valuable: the quality of a human’s oversight of their tools. Two developers can submit PRs that an LLM wrote. One vets the output carefully, keeps PRs atomic, links the spec, and ships reversible changes. The other rubber-stamps whatever the model produced, mixes concerns, and breaks public interfaces. A stateless reviewer cannot tell them apart. A reviewer with a trust profile can, and it calibrates its scrutiny accordingly.

Seen this way, the trust profile supports the automated reviewer: the checks define what “good” looks like, and the profile decides how much to trust that a given author is delivering that standard.

But that framing also points at the profile’s eventual obsolescence, which is the goal.

A bridge, not a destination
#

The trust profile is a mechanism I want to make obsolete.

The ideal end state is not a finely calibrated reputation system that perfectly sorts developers into tiers. The ideal end state is that there is nothing to sort. Every contributor, senior engineer or new hire, funnels their work through agents that enforce the same standards: atomic PRs, tests that pass, interfaces that do not break, changes that are reversible and tied to a spec. The output converges into something homogeneous, and by the time it reaches the code stage it is, for all practical purposes, perfect. Authorship stops carrying signal, because every author is producing the same uniform quality through the same disciplined pipeline.

In that world the reviewer’s checks still run, but they never flag anything, because the problems were engineered out upstream rather than caught downstream. And the trust profile, with nothing left to differentiate, collapses: everyone sits at trusted, the level never moves, and the file stops being worth reading.

That is the accurate framing for the trust profile. It is not the destination; it is the bridge. Today’s reality is heterogeneous, a mix of careful and careless, hand-written and agent-generated, vetted and rubber-stamped, and the profile does useful work precisely because that variance exists. As the variance shrinks, so does the profile’s job, until the most successful outcome is that no one, me included, needs it anymore.


The Importance of Context When Interacting with LLMs

Most interactions with LLMs fail not because the model lacks capability, but because the user fails to provide enough context for the model to succeed. Context is not a prompt engineering trick. It is the entire mechanism by which a frozen set of weights produces behavior relevant to your specific situation.

What Context Means for an LLM
#

A large language model is a fixed function at inference time. Its weights were determined during training and do not change. Everything you want the model to know about your current task, your constraints, your codebase, your domain, and your preferences must be communicated through the context window.

This window has three conceptual regions.

  1. The system prompt establishes persistent instructions and persona.
  2. The conversation history provides the back-and-forth of the interaction.
  3. The retrieved or injected context supplies external knowledge that the model was not trained on.

When people say “prompt engineering,” they are usually talking about the system prompt and a handful of few-shot examples. When people say “RAG,” they are talking about dynamically injecting retrieved documents into the third region. When people say “context engineering,” they are talking about the deliberate design of all three regions together.

In-Context Learning: Why This Works at All
#

The GPT-3 paper demonstrated something surprising: a sufficiently large language model can learn new tasks from examples provided in the prompt, without any gradient updates. This is in-context learning, and it is the reason context matters so much.

A model that has never seen your internal API conventions can follow them perfectly if you show it three examples. A model that does not know your company’s style guide can adopt it verbatim if you paste it in. The model is not truly “learning” in the statistical sense. The model is recognizing patterns in the provided context and extending them.

In-context learning means the quality of the context is the quality of the output. Vague context produces vague output. Contradictory context produces contradictory output. Missing context produces plausible-sounding output that is wrong in ways specific to your situation.

The Spectrum of Context Strategies
#

Context can be provided along a spectrum, from minimal to extensive.

Zero-shot prompting relies entirely on the model’s pre-trained knowledge. Zero-shot prompting works for generic tasks (summarize this text, translate this sentence) because the training data likely contained millions of similar examples. Zero-shot prompting fails for domain-specific tasks (generate a query against our proprietary schema, review code against our internal standards) because the model has never seen your specific conventions.

Few-shot prompting injects examples into the context window. The original GPT-3 evaluation showed that performance scales with the number of examples, but with diminishing returns. Three to five high-quality examples often capture 80% of the benefit. Example quality matters more than example quantity. One example that perfectly demonstrates the desired behavior outperforms ten mediocre ones.

Retrieval-augmented generation (RAG) dynamically fetches relevant documents and injects the documents into the context. The original RAG paper showed that combining a parametric model with non-parametric retrieval produces better grounded answers than either alone. RAG addresses the fundamental limitation that a model’s training data is a snapshot in time and cannot contain proprietary or recent information.

Long-context ingestion bypasses retrieval by stuffing everything into a large context window. Models such as Gemini with million-token context windows make the approach technically feasible. The question is whether dumping everything into the window actually works. Liu et al. demonstrated that models suffer from a “lost in the middle” effect: information at the beginning and end of the context is retrieved more reliably than information in the middle. Simply increasing the window does not linearly increase the model’s ability to use the information.

Context engineering is the emerging discipline of orchestrating all of these strategies deliberately. Context engineering recognizes that context is not just “what you put in the prompt,” but a system that includes retrieval logic, example selection, conversation management, and information ordering.

Where Context Breaks Down
#

Understanding the failure modes of context is as important as understanding how to provide context.

Attention dilution. Every token in the context window competes for the model’s finite attention. Adding irrelevant context does not just waste tokens. Adding irrelevant context actively degrades performance on the relevant portions. Liu et al.’s “Lost in the Middle” showed that even when the relevant information is present in the context, retrieval accuracy drops when the context is cluttered with noise. The lesson: more context is not always better. The right context, curated and ordered, outperforms a dump of everything that might be relevant.

Instruction following degrades at scale. System prompts and instructions are more reliably followed when the instructions are prominent in the context. As the context window fills with retrieved documents, conversation history, and examples, the model’s adherence to its original instructions weakens. Weakening adherence is why many production systems re-inject key instructions at the end of long contexts, not just the beginning.

Stale context. In a long agent session, early conversation turns become increasingly irrelevant. The model continues to weigh the early turns in its attention computation. The result is drift, where the model brings up constraints or preferences from turn 3 that are no longer applicable at turn 30. Effective agent systems summarize or prune earlier context to maintain relevance.

Implicit context gaps. The most dangerous context failures are the ones you do not notice. You assume the model knows that your API returns snake_case JSON, that your timestamps are in UTC, that your user IDs are integers not strings. The model does not know any of these things unless you tell it. Each implicit assumption is a potential bug in generated code or a hallucinated fact in generated text.

Practical Context Design
#

For engineers building with LLMs, context design is the highest-impact activity. Here are concrete patterns that work.

Layer your context deliberately. Start with a system prompt that defines the task, constraints, and output format. Follow with retrieved documents that are directly relevant to the current query. Then provide conversation history, pruned to the most recent and relevant turns. End by restating the specific question or instruction. This ordering respects the model’s attention patterns, which weight the beginning and end of context most heavily.

Curate your examples. A few-shot example should demonstrate the hardest case, not the easiest. If your task involves edge cases (empty inputs, special characters, ambiguous queries), show examples of those edge cases. Showing five variations of the happy path teaches the model less than one example of each difficult variant.

Use structured context. When injecting retrieved information, format the information consistently. Markdown headers, delimited sections, and numbered lists give the model explicit boundaries between pieces of information. A blob of unstructured text forces the model to spend attention on parsing structure rather than reasoning about content.

Separate context from instruction. Mixing “here is some reference information” with “now do this task” in the same unstructured block reduces reliability. Explicitly mark where context ends and instructions begin. Many production systems use XML tags or special tokens for this purpose.

Test your context independently. Before deploying an LLM pipeline, test whether the model can answer simple factual questions based solely on the provided context. If the model cannot reliably retrieve a fact that is clearly present in the context, the context is too long, too noisy, or poorly structured. The test is a fast diagnostic that catches many context problems before the context problems become production incidents.

Context Engineering as a Discipline
#

The shift from “prompt engineering” to “context engineering” reflects a maturation in how we think about LLM interactions. Prompt engineering suggests that the right wording produces better performance. Context engineering recognizes that the entire information environment determines the output.

The distinction matters because it changes where you invest effort. If you believe that the right words are what matters, you spend your time iterating on wording. If you believe in context engineering, you invest in retrieval systems, example libraries, context ordering, and information architecture.

The results from Anthropic’s work on contextual retrieval illustrate the payoff well. By adding a small amount of context to each retrieved chunk (explaining how the chunk relates to the broader document), Anthropic reduced retrieval failure rates by 67%. Not by changing the model. Not by changing the prompt. By changing how context was prepared and presented.

Chain-of-thought prompting is another example. The model’s reasoning improves not because you asked the model to “think step by step”, but because you expanded the context window with intermediate reasoning steps. The model uses its own generated context as additional input for subsequent tokens. Context is not just what you provide. Context is also what the model generates and then consumes.

The Uncomfortable Implication
#

If context determines output quality, the ceiling on LLM performance in production is not the model’s capability but the quality of the context pipeline feeding the model.

The conclusion is uncomfortable because the hardest engineering problem in LLM applications turns out not to be model selection or fine-tuning. The hard problem is information retrieval, information architecture, and information presentation. These are old problems from search engineering and information science, now applied to a new interface.

The engineers who build the best LLM products will not necessarily be the ones who understand transformer architectures most deeply. The best LLM products will be built by the engineers who can design systems that surface the right information, at the right time, in the right format, and place the information where the model will actually attend to it.

Context is not a feature you add to an LLM application; it is the application.

References
#


The Merge Gate: Do You Need a Human to Approve Your Pull Requests?

My previous piece was about code review: a human reading code before it ships. This one is narrower, and more direct. It is about the merge gate, the specific act of requiring a human to click “approve” before code can land in a codebase.

People treat review and approval as the same thing. They are not. You can review code without being able to block it. You can approve code without reading it. The valuable act (reading) and the gating act (merging) have been combined by our tooling, and that combination is worth separating.

Most arguments for keeping a human in the merge loop collapse once you ask a single question. What does the approval click certify that the automated gates did not?

What the Approval Click Actually Certifies
#

Watch what happens when a pull request gets approved.

The approver sees a green checkmark from CI. They see a passing test suite. They scan the diff for a few seconds. They click approve.

Ask them, afterward, what they certified. They will struggle to answer.

They did not re-run the tests; CI already did that. They did not verify the implementation against the spec; they assumed someone else did. They did not reason about every edge case; the diff was too long to reason about in three minutes. They did not assess whether the change could be undone; that is not what diffs show.

The approval click, in most teams, certifies exactly one thing: that a specific human was awake and present at the moment the PR was merged. That is a low-information event, and it is doing almost no safety work.

The actual safety is being produced by the systems around the gate. The test suite. The linter. The static analyzer. The deployment pipeline. The rollback path. The human approval is layered on top, credited with safety it did not generate.

The Cost of the Gate
#

A gate that does little safety work still imposes a cost. Several costs, in fact, and they add up.

Latency. Every pull request waits for a human. That human is in a meeting, asleep, on another team, or simply not looking at their inbox. Cycle time stretches from minutes to hours to days. Work that could ship this morning ships next Tuesday.

Batch amplification. Batch amplification is the subtlest and most damaging cost. When approval is expensive to obtain, people batch. They hold three small changes until they have a fourth, because each approval is a fixed-cost interruption. The gate incentivizes the exact thing it should discourage: larger, riskier changes. A mechanism designed to keep changes safe ends up making them less safe, because the unit of review grows to fill the cost of getting reviewed.

Context switching. Every approval is an interruption for the approver. Their work is paused, their context is swapped, their focus is interrupted. You are taxing your most experienced engineers to perform a low-information ritual.

Single point of failure. If only two people can approve a given area and both are out, the pull request stalls. Approval authority concentrates, and concentration creates bottlenecks and bus factors.

Soft target. The approver is the cheapest attack surface in the entire pipeline. You can harden CI, pin dependencies, and scan for secrets, but a tired approver who clicks approve on a social-engineered change defeats all of it. A human gate is a human vulnerability.

What Actually Makes a Merge Safe
#

Remove the ritual and ask what keeps a merge safe. It is a short list, and the human approver is not on it.

A comprehensive test suite that runs on every change. Static analysis that catches the classes of bugs tests miss. A deployment pipeline that canaries before it fully rolls out. Feature flags so new behavior can be disabled without a redeploy. A rollback path that has actually been tested, not assumed. Blast-radius limits that cap how much any single change can touch.

Each of these operates on reality, not on a human’s prediction of reality. A canary either surfaces the problem or it does not. An approver might notice the problem, or might be thinking about lunch.

The gate is credited with safety the system produces. Once you see this, you cannot ignore it.

The Accountability Objection
#

The first objection is accountability. Without a human approval, who is responsible for the code that ships?

I made this argument in the previous piece, and I will not repeat all of it here. The short version is that a human glancing at a diff was never truly responsible for what shipped. Responsibility starts upstream, with who decided the problem was worth solving and who wrote the specification.

The approval signature adds nothing to that account. It is a name on a line, demanded because it feels like accountability. When a change breaks production, the approver is not the one who gets blamed, disciplined, or even consulted. The signature exists to allocate blame after the fact, not to prevent harm before it.

What remains, once you accept this, is the audit and compliance framing. That is the stronger case for keeping a human signature, and it deserves its own answer.

The Compliance Objection
#

“But regulation requires human approval.”

Sometimes it does. More often, regulation requires traceability, a named owner, a documented decision, an auditable path. Those are not the same thing as a tired human clicking approve at four in the afternoon on a Friday.

When a rule says a change must be “reviewed and approved”, it is trying to ensure that someone with authority consciously decided the change was acceptable. That intent can be satisfied several ways. A named owner who signed off on a specification. An automated gate whose rules were themselves approved by a human. A risk classification that a human defined and a machine enforces consistently.

The function is accountability and traceability. The form is “a human clicks a button”. Teams satisfy the form and skip the function all the time. The sound path is the reverse: satisfy the function rigorously, and let the form follow.

If your auditor insists that safety depends on a specific human keystroke, you have an education problem, not an engineering one.

The Binary Mistake
#

The deeper error is treating approval as binary. Either every pull request needs a human, or no pull request does.

The binary view is wrong, and it is not how anyone actually behaves. A README typo and a production schema migration are both pull requests. They do not need the same gate. Treating them the same is not caution; it is a failure to think about risk.

The right unit of gating is not “is this a pull request”. It is “what is the blast radius of this change, and is it reversible”.

A one-line documentation fix is low blast radius and trivially reversible. Let it merge on green. No human needs to see it.

A change that drops a database column is high blast radius and may be irreversible. That deserves a human looking at it, carefully, with time.

A change that alters an authentication boundary is medium blast radius but high trust impact. That deserves a human, and probably more than one.

The properties that should trigger a human gate are properties of the change: irreversibility, blast radius, trust-boundary crossing, external commitment. They are not properties of the artifact. When you gate on the change instead of the artifact, the fraction of changes that need a human collapses to a small minority.

The Small Set Where a Human Gate Genuinely Adds Value
#

Frankly, there is a set of changes where a human in the merge loop actually adds value. It is small, but it exists.

Irreversible changes. Data destruction, schema drops, deletions of public content, sending actual money, publishing to external systems. Once these execute, you cannot call them back. A human who understands the irreversibility should look at them, because the automated gates can only verify forward correctness, not undo impossibility.

Trust-boundary changes. Authentication, authorization, permission models, security-sensitive code paths. These are exactly where a subtle mistake is both likely and catastrophic. A human reviewer adds value here, not because they will catch every bug, but because the cost of a miss is high enough to justify the latency.

Changes to the gating system itself. You do not want the merge gate to auto-approve changes to the merge gate. That is the one place where circular logic causes harm. A human reviews the rules that the machine enforces.

External commitments. Public API changes, contractually obligated behaviors, compliance-relevant logs. These have consequences outside the codebase, and a human should confirm the external surface is intentional.

Note what is not on this list. Styling. Refactors within a single module. New tests. Documentation. Internal-only features. Dependency bumps that pass audit. These are the overwhelming majority of pull requests. They do not need a human gate. They need the automated gates, and then they need to merge.

The Transition
#

You do not get to “no human gate for most changes” by decree. You get there by making the default path safe.

Start by making the low-blast-radius path auto-merge on green. Documentation, tests, internal-only changes within a single module. CI passes, the merge happens, nobody clicks anything.

Then compute blast radius automatically. Which files changed. Did the change touch the public API. Did it touch the database schema. Did it add a dependency. Did it change infrastructure or deployment configuration. Did it cross a security boundary.

Each of these is a machine-checkable property. Route the change to the human gate only when it crosses a threshold.

When a change does reach the human gate, change what the human is actually doing. They are no longer reviewing code line by line. They are reviewing risk. Is this change as irreversible as the system thinks it is? Is the rollback plan genuine? Is the blast radius acceptable? Does the external commitment match what was approved upstream?

Reviewing risk is high-leverage work. Reviewing diffs for style is not. The transition moves the human from the low-leverage activity to the high-leverage one, and it frees the rest of the pipeline to move at the speed the machines can sustain.

This is not a proposal. It is a description of the workflow from the previous piece, where a pull request is checked against concrete gates: does it introduce security-sensitive changes, add dependencies, change public interfaces, is it reversible, do the tests pass, does it satisfy the acceptance criteria. What this article adds is the principle behind that workflow. The human gate is the exception, selected by the properties of the change, not the default triggered by the existence of a pull request. The skills that implement it are publicly available.

The Merge Button Is a Ritual
#

The merge button exists because our tools gave us a button. We built a workflow around it, assigned it meaning, and then treated the meaning as structural.

It is not. The safety of a codebase is produced by the systems around the merge, not by the merge approval itself. The click certifies almost nothing those systems did not already certify. And it imposes an actual cost: latency, batched risk, interrupted focus, concentrated bus factors, and a soft target for anyone who wants to push a change through unnoticed.

The defensible position is not “no humans in the loop”. It is “humans in the loop where they add value, out of the loop where they do not”.

Most pull requests do not need a human to approve them. A small, identifiable minority do. The mistake of the current default is that it treats every change as if it belonged to that minority.

Free the majority to merge on green. Reserve the human gate for the changes where it actually certifies something the machine cannot. And stop pretending the button is what keeps you safe.

The gate was never the safety. The system was.