You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This week’s aroma profile is burnt path math, copy-paste marmalade, and a crisp finish of “ship it, probably” 💩. The tests are plentiful, the intentions are noble, and the implementation still manages to trip over its own shoelaces in impressively creative ways.
I found enough real problems that I never had to whine about formatting. That is, somehow, the meanest compliment I can give this repo 🤮.
🤡 startsWith() is not path containment, and duplicating the bug in both apply and revert does not make it “battle tested.”
selectRootConfiguration() in both flows treats path strings as if filesystems were just long tweets. A sibling like /repo/app2/.ruler can be mistaken for living under /repo/app, which means hierarchical root selection can drift into the wrong config scope.
The extra insult is that the repo already contains a correct helper for this exact problem. So this is not an unavoidable edge case; it is a self-inflicted paper cut with a staple gun.
Replace both startsWith() checks with isPathInsideOrEqual().
Extract selectRootConfiguration() into one shared helper instead of copy-pasting the same mistake twice.
Add regression tests for sibling-prefix cases like /repo/app vs /repo/app2.
Nested Discovery Ignores Git Worktrees
☠️ Nested scanning only respects .git directories, which is adorable if you’ve never met a worktree, submodule, or modern Git setup.
findAllRulerDirs() stops traversal at nested repos only when .git is a directory. Linked worktrees commonly use a .git file with a gitdir: pointer, so nested discovery can wander across repository boundaries and start “helping” where it absolutely should not.
What makes this smell especially ripe is that GitignoreUtils already handles .git files correctly. The codebase knows the rule in one place and forgets it in another, like a goldfish with commit access.
Switch nested-boundary detection to lstat() and handle .git files with gitdir: pointers.
Extract a shared “resolve Git boundary” helper and reuse it in both discovery and ignore-file logic.
Add tests for linked worktrees and submodules, not just vanilla .git/ repos from 2009.
Configuration Loading Is Duplicated and Expensive
💸 The apply path loads config twice, re-reads rules twice, and maintains two overlapping config models because apparently I/O grows on trees.
loadSingleConfiguration() calls loadConfig(), reads markdown files, then imports and calls loadUnifiedConfig(), which goes right back to disk for TOML and markdown again. In nested mode, this little circus repeats per .ruler directory.
You also have LoadedConfig and RulerUnifiedConfig living side-by-side, which is a fantastic way to guarantee drift over time. Two sources of truth are not “flexibility”; they are an argument waiting to happen.
Make one loader canonical; derive legacy projections from it instead of parsing the world twice.
Pass pre-read rule files and config state through the apply pipeline instead of rediscovering them downstream.
Add a nested-mode perf test or trace assertion so this kind of duplicate I/O gets caught instead of quietly fermenting.
MCP Merge Is Not Actually a Merge
🧨 mergeMcp() does a shallow server replacement, so changing one field can silently nuke the rest of that server’s config.
Right now, merging is just { ...baseServers, ...incomingServers }. If a base server has env, headers, or timeout, and the incoming config only changes one piece, the whole original server object gets replaced. Congratulations: your “merge” is a polite overwrite wearing a fake moustache.
This is the kind of bug that looks fine in happy-path tests, then eats somebody’s auth headers in production and shrugs.
Deep-merge per-server objects instead of replacing them wholesale.
If full replacement is sometimes intended, make it explicit with a per-server overwrite flag.
Add tests for partial overrides of env, headers, args, and timeout.
Remote MCP Support Is Guesswork and Secret Spillage
🔐 Remote MCP handling currently combines protocol roulette, runtime @latest dependencies, and credential leakage into argv — a truly cursed little trifecta.
When an agent lacks remote support, the code rewrites remote servers to npx -y mcp-remote@latest ... --header .... That means secrets can land in process arguments, and behavior depends on whatever the npm registry thinks “latest” means today. Spectacularly bad idea.
Elsewhere, OpenHands decides whether a server is sse or shttp by regexing the URL for /sse/, rejects non-Bearer/custom headers outright, and Mistral claims streamable-http support while always emitting http. That is not protocol support; that is improv comedy with auth tokens.
returntrue;// Mistral Vibe supports http and streamable-http transports
Suggestions
Introduce one shared remote-server type with explicit transport metadata instead of guessing from URL shapes.
Stop putting secrets in argv; use env vars, temp config files, or native header fields.
Pin or vendor the bridge tool instead of invoking mcp-remote@latest.
Add end-to-end tests for sse, streamable-http, and non-Bearer/custom-header cases.
Agent Metadata Is Smeared Across Manual Registries
🫠 Adding an agent currently means editing a scavenger hunt of arrays, switches, maps, and constants, because one source of truth was apparently too mainstream.
The same metadata is repeated across allAgents, MCP path resolution, skill-target mapping, and path constants. That means every new adapter is a four-file memory test, and every missed edit becomes another weird “why does this one agent behave differently?” bug report.
This is classic drift bait. The code is manually synchronizing facts that should be declarative. It works right up until it very much does not, which is a bold engineering lifestyle choice.
Create a single AgentDescriptor registry with identifier, display name, factory, MCP-path resolver, skills target, and capability flags.
Derive allAgents, CLI help, path resolution, and skills propagation from that registry.
Add an exhaustiveness test so a new agent cannot be added half-wired.
CI and Release Automation Are Brittle and Wasteful
🚨 The workflows are a glorious mash-up of happy-path assumptions, hardcoded repo identity, and a completely unnecessary coverage tax.
release.yml only blocks prereleases when EVENT_NAME=release, so a manual workflow_dispatch recovery run can still publish a prerelease version to npm latest if somebody supplies the right tag. That safety rail is decorative. Meanwhile the smell-report workflow hardcodes intellectronica/ruler twice, because apparently forks and repo renames are for peasants.
On top of that, npm test always means jest --coverage, so every normal test run in CI drags instrumentation around like a dead refrigerator. You have separate jobs and matrices; you do not need to pay the coverage bill in every single lane.
if [ "${EVENT_NAME}" = "release" ] && [ "${RELEASE_PRERELEASE}" = "true" ]; thenecho "::error::GitHub prereleases must not be published to npm latest."exit 1fi
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
This Codebase Smells!
This week’s aroma profile is burnt path math, copy-paste marmalade, and a crisp finish of “ship it, probably” 💩. The tests are plentiful, the intentions are noble, and the implementation still manages to trip over its own shoelaces in impressively creative ways.
I found enough real problems that I never had to whine about formatting. That is, somehow, the meanest compliment I can give this repo 🤮.
Table of Contents
String Prefix Path Checks Are Fake Containment
🤡
startsWith()is not path containment, and duplicating the bug in both apply and revert does not make it “battle tested.”selectRootConfiguration()in both flows treats path strings as if filesystems were just long tweets. A sibling like/repo/app2/.rulercan be mistaken for living under/repo/app, which means hierarchical root selection can drift into the wrong config scope.The extra insult is that the repo already contains a correct helper for this exact problem. So this is not an unavoidable edge case; it is a self-inflicted paper cut with a staple gun.
Files
src/lib.tssrc/revert.tssrc/core/path-utils.tsCode
src/lib.tslines 445–454 (link)src/revert.tslines 325–334 (link)src/core/path-utils.tslines 3–15 (link)Suggestions
startsWith()checks withisPathInsideOrEqual().selectRootConfiguration()into one shared helper instead of copy-pasting the same mistake twice./repo/appvs/repo/app2.Nested Discovery Ignores Git Worktrees
☠️ Nested scanning only respects
.gitdirectories, which is adorable if you’ve never met a worktree, submodule, or modern Git setup.findAllRulerDirs()stops traversal at nested repos only when.gitis a directory. Linked worktrees commonly use a.gitfile with agitdir:pointer, so nested discovery can wander across repository boundaries and start “helping” where it absolutely should not.What makes this smell especially ripe is that
GitignoreUtilsalready handles.gitfiles correctly. The codebase knows the rule in one place and forgets it in another, like a goldfish with commit access.Files
src/core/FileSystemUtils.tssrc/core/GitignoreUtils.tsCode
src/core/FileSystemUtils.tslines 590–603 (link)src/core/GitignoreUtils.tslines 117–119 (link)src/core/GitignoreUtils.tslines 152–160 (link)Suggestions
lstat()and handle.gitfiles withgitdir:pointers..git/repos from 2009.Configuration Loading Is Duplicated and Expensive
💸 The apply path loads config twice, re-reads rules twice, and maintains two overlapping config models because apparently I/O grows on trees.
loadSingleConfiguration()callsloadConfig(), reads markdown files, then imports and callsloadUnifiedConfig(), which goes right back to disk for TOML and markdown again. In nested mode, this little circus repeats per.rulerdirectory.You also have
LoadedConfigandRulerUnifiedConfigliving side-by-side, which is a fantastic way to guarantee drift over time. Two sources of truth are not “flexibility”; they are an argument waiting to happen.Files
src/core/apply-engine.tssrc/core/ConfigLoader.tssrc/core/UnifiedConfigTypes.tssrc/core/UnifiedConfigLoader.tsCode
src/core/apply-engine.tslines 337–359 (link)src/core/ConfigLoader.tslines 298–306 (link)src/core/UnifiedConfigTypes.tslines 9–16 (link)src/core/UnifiedConfigLoader.tslines 243–279 (link)Suggestions
MCP Merge Is Not Actually a Merge
🧨
mergeMcp()does a shallow server replacement, so changing one field can silently nuke the rest of that server’s config.Right now, merging is just
{ ...baseServers, ...incomingServers }. If a base server hasenv,headers, ortimeout, and the incoming config only changes one piece, the whole original server object gets replaced. Congratulations: your “merge” is a polite overwrite wearing a fake moustache.This is the kind of bug that looks fine in happy-path tests, then eats somebody’s auth headers in production and shrugs.
Files
src/mcp/merge.tsCode
src/mcp/merge.tslines 68–76 (link)Suggestions
env,headers,args, andtimeout.Remote MCP Support Is Guesswork and Secret Spillage
🔐 Remote MCP handling currently combines protocol roulette, runtime
@latestdependencies, and credential leakage into argv — a truly cursed little trifecta.When an agent lacks remote support, the code rewrites remote servers to
npx -y mcp-remote@latest ... --header .... That means secrets can land in process arguments, and behavior depends on whatever the npm registry thinks “latest” means today. Spectacularly bad idea.Elsewhere, OpenHands decides whether a server is
sseorshttpby regexing the URL for/sse/, rejects non-Bearer/custom headers outright, and Mistral claimsstreamable-httpsupport while always emittinghttp. That is not protocol support; that is improv comedy with auth tokens.Files
src/mcp/capabilities.tssrc/mcp/propagateOpenHandsMcp.tssrc/agents/MistralVibeAgent.tsCode
src/mcp/capabilities.tslines 29–36 (link)src/mcp/capabilities.tslines 87–90 (link)src/mcp/propagateOpenHandsMcp.tslines 40–42 (link)src/mcp/propagateOpenHandsMcp.tslines 84–86 (link)src/agents/MistralVibeAgent.tslines 209–218 (link)src/agents/MistralVibeAgent.tslines 234–235 (link)Suggestions
mcp-remote@latest.sse,streamable-http, and non-Bearer/custom-header cases.Agent Metadata Is Smeared Across Manual Registries
🫠 Adding an agent currently means editing a scavenger hunt of arrays, switches, maps, and constants, because one source of truth was apparently too mainstream.
The same metadata is repeated across
allAgents, MCP path resolution, skill-target mapping, and path constants. That means every new adapter is a four-file memory test, and every missed edit becomes another weird “why does this one agent behave differently?” bug report.This is classic drift bait. The code is manually synchronizing facts that should be declarative. It works right up until it very much does not, which is a bold engineering lifestyle choice.
Files
src/agents/index.tssrc/paths/mcp.tssrc/core/SkillsProcessor.tssrc/constants.tsCode
src/agents/index.tslines 38–71 (link)src/paths/mcp.tslines 64–90 (link)src/core/SkillsProcessor.tslines 159–173 (link)src/constants.tslines 57–70 (link)Suggestions
AgentDescriptorregistry with identifier, display name, factory, MCP-path resolver, skills target, and capability flags.allAgents, CLI help, path resolution, and skills propagation from that registry.CI and Release Automation Are Brittle and Wasteful
🚨 The workflows are a glorious mash-up of happy-path assumptions, hardcoded repo identity, and a completely unnecessary coverage tax.
release.ymlonly blocks prereleases whenEVENT_NAME=release, so a manualworkflow_dispatchrecovery run can still publish a prerelease version to npmlatestif somebody supplies the right tag. That safety rail is decorative. Meanwhile the smell-report workflow hardcodesintellectronica/rulertwice, because apparently forks and repo renames are for peasants.On top of that,
npm testalways meansjest --coverage, so every normal test run in CI drags instrumentation around like a dead refrigerator. You have separate jobs and matrices; you do not need to pay the coverage bill in every single lane.Files
.github/workflows/release.yml.github/workflows/this-codebase-smells.ymlpackage.json.github/workflows/ci.ymlCode
.github/workflows/release.ymllines 4–15 (link).github/workflows/release.ymllines 48–56 (link).github/workflows/this-codebase-smells.ymllines 99–100 (link).github/workflows/this-codebase-smells.ymllines 118–120 (link)package.jsonlines 11–14 (link).github/workflows/ci.ymllines 28–33 (link)Suggestions
workflow_dispatchtoo, or require an explicit npm dist-tag and publish with--tag.owner/repovalues with${{ github.repository }}or derived shell variables from that context.testfromtest:coverage, then run coverage in exactly one CI lane instead of all of them.All reactions