On July 17 I opened the most serious issue my project has ever had, wrote the fix the same morning, and pushed it as a pull request. CI went green on Node 20, 22 and 24 within minutes. Zero review comments. mergeStateStatus: CLEAN.
Then it sat there.
I merged it today — July 21. Four days.
For those four days the fix existed, was correct, was tested, and was visible to nobody. Every npx safari-mcp in that window installed the version with the bug. The npm counter says roughly 1,400 people a week pull this thing. The fix was done and the users still had the bug, and those two facts sat side by side without touching each other.
I want to be precise about why, because the interesting part isn't "I was busy."
The bug, briefly
safari-mcp lets an AI agent drive the Safari you're already logged into. That's the value and that's the danger: you're using the browser at the same time as the agent. The one promise the project makes is the agent never touches a tab it didn't open.
That promise had a hole. When the tracked tab index ran past the end of the window — you closed a tab, or tore one into its own window — the resolver clamped the index to the last tab:
if (_st().activeTabIndex && _st().activeTabIndex > tabCount) {
console.error(`[Safari MCP] Tab ghost proactive fix: clamping to ${tabCount}`);
_st().activeTabIndex = tabCount;
}
The last tab in the window is not our tab. It's whatever you happen to have open there. So at the exact moment the code discovers it no longer knows where its tab is, it points the session at one of yours — and logs the words "proactive fix" while doing it.
The fix is four lines: fail closed. Drop ownership, return null, let the next call open a fresh tab.
Small diff. Green CI. Four days.
What actually held it
Not review capacity. I'm the only reviewer; there was no queue.
Not risk. The change makes an unsafe path safe; the worst case of shipping it is that the agent opens a redundant tab.
What held it was that merging was the fourth step of a five-step ritual and only the first step was automated. Merge, bump version, write the changelog entry, cut a GitHub release, and only then does the Publish workflow fire and npm gets the fix. Steps 2–4 are me, at a keyboard, in a mood to do release chores.
CI told me the code was good. Nothing told me the code was stuck. Those are different signals and I only had one of them.
The tell I ignored
Here's the part that made me write this instead of quietly shipping.
While cutting today's release I opened CHANGELOG.md and found the previous release, v2.15.3, had no entry at all. Shipped July 15. The release commit touched exactly two files: package.json and package-lock.json.
Three real fixes went out that day — clicks landing on the wrong tab, ARIA combobox typeahead never loading, a focus-helper queue that poisoned itself after a timeout, the last one contributed by an outside developer. Anyone reading the changelog to decide whether to upgrade saw nothing between 2.15.2 and 2.15.4. The contributor's fix was invisible.
I didn't skip that entry on purpose. I skipped it because it's step 3 of the same manual ritual, and step 3 has no test.
So the pattern isn't "a PR was slow." The pattern is: the parts of shipping that a machine watches are reliable, and the parts that only a human watches decay — silently, and in the same direction every time. My CI has 57 tests. My release process has zero.
The uncomfortable comparison
There's a well-worn instinct in solo open source that goes: don't automate the release, you'll ship something bad by accident. Manual = careful.
I believed that. Look at what manual actually bought me:
| Automated | Manual | |
|---|---|---|
| Code correctness | 57 tests, 3 Node versions | — |
| Packaging |
packaging.test.mjs fails CI if an imported file isn't published |
— |
| Registry sync | server.json version synced + published on release | — |
| Merge decision | — | 4-day gap |
| Changelog entry | — | silently missing |
| Cutting the release | — | mood-dependent |
Every column with a machine in it held. Every column with only me in it drifted. The "careful" half is the half that failed, and it failed quietly, which is worse than failing loudly.
The honest version of the instinct isn't manual is careful. It's manual feels careful because you were paying attention at the moment you did it, and you have no record of the moments you weren't.
What I'm changing
Not full auto-release-on-merge. I still want a human deciding when a release exists, because "these three fixes belong together" is a judgment call and I don't have a robot with taste.
What I want is for the gaps to be loud:
-
A changelog check in CI. If the diff touches
safari.jsorindex.jsand doesn't touchCHANGELOG.md, CI complains. That's the test step 3 never had. It's ten lines and it would have caught the 2.15.3 hole on the day it happened. -
A stale-green-PR alarm. Any PR that is
CLEAN+ passing + zero-review-comments for more than 48 hours gets surfaced, loudly, wherever I'll actually see it. Not to force a merge — to make "still holding this" a decision I re-make rather than a default I drift into. - Release notes generated from the changelog, so the GitHub release and the changelog can't disagree — one source, two renders.
None of that is clever. That's the point. The failure wasn't clever either.
The thing I'd actually generalize
If you maintain something alone, look at your last five releases and ask a narrow question: which steps between "merged" and "a user can install it" have no automated observer?
Not "which steps are manual" — plenty of manual steps are fine. Which manual steps would fail without producing a symptom you'd notice.
A missing changelog entry produces no error. A green PR sitting for four days produces no error. A release you didn't cut produces no error. They produce the absence of something, and absence doesn't page anybody.
My whole test suite is built to catch code that does the wrong thing. Not one of those 57 tests can catch code that does the right thing where nobody can reach it.
v2.15.4 is on npm now — the tab-index path fails closed, and the 2.15.3 changelog entry has been backfilled. Source: github.com/achiya-automation/safari-mcp
What's the gap in your pipeline that has no observer? I'm genuinely collecting these — the ones I find myself are always the boring ones, and the interesting ones seem to come from other people's setups.
Top comments (2)
The stale-green-PR alarm is a strong takeaway because it treats release latency as an operational bug, not a calendar problem. A passing CI signal says the fix is valid, but users are still exposed until publish actually happens.
That gap matters even more in browser or agent-control tooling because the blast radius is active sessions, not a passive edge case that only shows up under unusual input.
Have you thought about making publish readiness an explicit required check so CLEAN plus passing cannot look finished while users are still installing the broken version?
This really underscores the review bottleneck for critical fixes. Did you have any "fast-track" process for