Thursday, 2:13 PM. A pull request landed in the repository with the title rename last_seen to last_activity in auth helper. Twenty-seven lines changed, most of them the same variable moving through a single file. The PR was opened by someone we had never met.
The review came back with ten comments. Every single one was technically defensible. Add a unit test for the rename. Update the doc comment on the function. Run the linter locally and paste the output. Move a constant to the top of the file. Update the two call sites in handlers/auth.go. Add a test for the empty-session case. Update the changelog. Rename the test variable to match. Re-run the benchmark suite. Rebase because another PR merged in the meantime.
None of those comments was wrong. All of them together were a wall.
The contributor spent two weeks going back and forth. Four revisions. On day eleven, the reviewer added two more requests. On day fourteen, the PR was closed. Not by the maintainer. By the contributor. No explanation, no follow-up. Just a closed pull request and a vanished username.
It took a quarterly contributor-retention tally to notice. That quarter had one new contributor who never came back. We had to dig through the git history to find out why.
We wrote the first postmortem as "over-reviewing." Then we wrote a second one as "the contributor wasn't resilient enough." The third postmortem was the one that stuck: he was fine. The gate was too high. But we didn't understand what kind of gate until the same week a file sync job ran at 09:47 as it had run every six hours for a year, and deleted forty-three private modules from the archive. The rule list it consulted had last been touched forty-six days earlier. Nobody had updated it when the module directory grew. The job looked at paths that weren't in its rules and treated them as garbage. Recovery took two hours. Rebuilding confidence in the archive took two weeks.
Both incidents had the same shape. Power was granted to something that executes without asking about context. The context lived in one person's head. The human who owned the variable name and the human who owned the rule list both assumed that someone else would see the gap. Neither assumption was ever written anywhere.
We stopped maintaining a governance document after that. We started running a governance layer.
The first thing we built was decision memory. Every maintainer decision that changes behavior gets written to a decision-log server: who, what, when, and which alternatives were rejected. The first version of that log didn't record alternatives. It was useless. Three weeks in, someone reopened a settled trade-off about the plugin API. The bot attached the issue to the thread and said, "This was settled in #742." The response came back: "Fine, but why was option B rejected?" The log had nothing. We had to replay the entire issue history, extract the comparison, and append it retroactively.
Now every new proposal triggers a search against that log, and the entry bot attaches the relevant history to the issue. If someone re-opens a settled trade-off, the bot says so explicitly and attaches the rejected alternative. It doesn't block disagreement. It removes the "I didn't know" defense. The lesson we keep re-learning: writing the decision is useful, but writing the rejected path is what makes the memory work.
The second thing we built was power expiry. The most dangerous open source asset is a dormant maintainer who still holds commit rights. Identity gets attached to that bit, and nobody wants to send the email that takes it away. We tried a soft version first: a quarterly audit that flagged inactive maintainers and asked them to confirm their status. Half confirmed. The other half were silently gone, and their commit tokens were still valid.
We switched to capability tokens that expire on a cycle. Re-issuance requires at least one actual review or commit in the previous 120 days. A bot tags dormant maintainers, disables their commit access, and sends a message with the path back in. The first time this ran, a core maintainer lost access mid-sabbatical. He was annoyed for a day, came back, did two reviews, and got re-issued. No social drama. The token expired, which is a much easier message to swallow than "you have been removed." This is what an agent scheduler would call bounded trust.
The third thing came from the issue queue. We had a primary reviewer who was also the architect, and he was the only person who could approve changes to the core registry. When he went quiet, everything stalled. We built a failover ladder into the repository config: if the primary reviewer does not respond within 5 days, a backup reviewer is pinged automatically. After 10 days, the backup's approval counts.
Our first attempt just added requested_reviewers to the PR template. The backup had no signal to act, so nothing changed. The fix was a workflow rule that reassigns after the deadline. The system owns the delay. Nobody has to be the villain. A stuck PR in this setup means the routing timer has not expired. It does not mean the maintainer is ignoring you.
The fourth thing was conflict dampening. A fork usually starts with a legitimacy gap, around a decision that felt unjust. The technical direction is a detail; the emotion is the engine. We added a cool-off window for reversals and license changes. During the window, an arbitration bot runs the contested commit against the baseline and publishes observable behavioral differences. A test report.
Our first bot diffed the README. Useless. We replaced it with a script that instruments the baseline and the proposal through the same test suite and posts pass/fail deltas. That turns "you can't change it because reasons" into "the proposal fails 3 of 12 baseline tests." Same decision, better hygiene. The trade-off is urgent security fixes. We added an exception path that bypasses the cool-off window, but it requires three maintainers to sign the exception. The cutoff lives in runtime config, and yes, someone set it to four hours once. The audit log caught it. The audit log only helps if somebody reads it, which is a second-order problem we haven't solved.
One more guardrail: exit packets. Anyone with commit rights who leaves the project must export a document covering open threads, pending decisions, and tool access they control. We already require a pre-disconnect routine for any agent with write access to community records. The rule for humans is the same. No packet, no exit.
The first time we tried to enforce this, we heard the argument that you can't make volunteers do paperwork. Then a maintainer quit over a weekend, and we found out that three pending decisions lived only in their inbox. The community spent a month guessing. Ever since, a missing exit packet is a corrupted memory event.
Then the trade-offs. We tried a 40-page governance document. Nobody read it. We tried full automation, a bot that decided everything, but the cool-off window turned into a bottleneck and the bot's reports read like lab notebooks. The middle ground is one page of intent and the rest as runtime code. The long expiry cycle still scares me for the occasional deep reviewer who only shows up twice a year. The failover ladder softens it, but that is a real loss. And as I said, the arbitration bot's exception path is only as strong as the humans watching the audit log. Governance layers are agents too, and they have their own failure modes.
Going back to the PR that cost us a contributor: the real fix wasn't fewer review comments. It was a question we now ask ourselves before sending a review. Ten comments each take a maintainer thirty seconds to type. For the person on the other end, each one is an evening. The maintainer in that thread wasn't malicious. He was doing exactly what the sync job did: applying a list of rules without checking what the list looked like from the other side.
So now the rule is this. Before you send ten review comments, ask yourself one question: which of these is worth two weeks of a newcomer's evenings?
If you can't answer in five seconds, delete the comment and try again. The next agent that reads the thread will judge you by what you chose to say, not by how fast you said it.
Top comments (0)