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 is “security check used as a coaster” with top notes of copy-paste architecture and a lingering finish of CI sadness. 🤮 There’s good intent buried in here, but too much of the implementation still behaves like a developer muttered “eh, probably fine” and sprinted away from the keyboard. Let’s disinfect the worst bits.
🧨 The helper that is supposed to keep writes inside the project can fail open when it reaches /, which is a truly inspired interpretation of “security boundary.”
The containment check walks up the directory tree until it finds something real. If it walks all the way to filesystem root without finding an existing ancestor, it just returns success. Amazing. The guard dog saw the fence disappear and decided that was a management problem.
Because both assertManagedPathInsideRoot() and writeGeneratedFile() rely on this logic, a non-existent out-of-tree target can be blessed without ever proving it lives under the project root. It’s not the easiest bug to hit, but fail-open path validation is exactly the kind of “rare edge case” that becomes a postmortem later.
Fail closed when the upward walk hits filesystem root without proving containment.
Validate the resolved lexical path against rootPathbefore any mkdir/write, then re-check the real path after creation.
Add tests for non-existent paths outside the repo, not just symlink/hardlink cases.
Apply and Unified Config Run Two Rule Pipelines
🤹 The repo reads and concatenates rules in two separate subsystems, because apparently one source of truth was too emotionally stable.
apply-engine.ts reads markdown files and builds the actual rule payload itself, then separately loads UnifiedConfigLoader and uses it only for MCP data. Meanwhile UnifiedConfigLoader has its own rule discovery, metadata, diagnostics, and hashing path. So yes, this codebase has two different machines doing the same job and politely pretending that won’t drift.
That split is already begging for inconsistency: rule ordering fixes, diagnostics, inclusion rules, and future metadata changes can land in one path and never reach the other. The main apply flow is effectively saying, “Thanks for the unified loader, I’ll just use the side dish.” 🍽️
Make apply-engine consume unifiedConfig.rules.concatenated instead of rebuilding rules itself.
Extract one shared rule-loading pipeline for single and nested modes.
Surface unified-loader diagnostics in the apply path so warnings actually protect the code that gets written.
Broken Config Files Get Repaired With a Flamethrower
🔥 Several agents treat “config missing” and “config invalid” as the same thing, then overwrite the user’s file like a raccoon with admin access.
OpenCode, RooCode, and CodexCli all swallow parse failures and continue with a fresh config. That means malformed user config can get silently bulldozed into a brand-new managed file instead of surfacing a useful error. Congrats: the tool is now “self-healing” in the same way deleting production is a kind of cleanup.
The really spicy part is that MistralVibeAgent already shows the sane pattern: tolerate ENOENT, throw on invalid config, preserve the original file. So the repo already knows how not to be reckless; it just hasn’t bothered to be consistent about it.
letexistingConfig: CodexCliConfig={};try{constexistingContent=awaitfs.readFile(configPath,'utf8');existingConfig=parseTOML(existingContent);}catch{// File doesn't exist or can't be parsed, use empty config}
try{constexistingContent=awaitfs.readFile(configPath,'utf8');configExists=true;existingConfig=parseTOML(existingContent)asVibeConfig;}catch(error){if((errorasNodeJS.ErrnoException).code==='ENOENT'){existingConfig={};}else{thrownewError(`Invalid Mistral config at ${configPath}: ...`);}}
Concrete suggestions
Only swallow ENOENT; treat parse errors as real errors and stop.
Preserve malformed files untouched and avoid creating backups/writes when parsing fails.
Extract a shared “read existing managed config” helper with strict error semantics.
Replace the Roo invalid-JSON behavior with tests that assert failure and preservation.
Agent Output Path Rules Are Copy Pasted Everywhere
🧬 The repo already has a shared output-path resolver, and then a bunch of agents re-implement the same precedence logic anyway because duplication is apparently a spiritual practice.
getAgentOutputPaths() already centralizes outputPath / outputPathInstructions / outputPathConfig precedence, and core apply/revert logic uses it. But agents like Aider, CodexCli, and MistralVibe still hand-roll the same fallback chains inline. This is how tiny logic differences metastasize into “why does revert disagree with apply?” bugs six months from now.
Worse, the shared helper drives .gitignore and revert accounting while agent implementations do the actual writes. If those two worlds drift, the repo gets the delightful experience of tracking one path and mutating another. Chef’s kiss. 💩
Move all output-path resolution into one shared helper that returns typed, absolute paths.
Make agent adapters consume that helper instead of re-encoding precedence by hand.
Add table-driven tests that assert apply, revert, and agent write paths all agree for the same config inputs.
The Test Tooling Is Slow Blind and Manual
🐢 The repo’s tooling manages to be simultaneously expensive, under-scoped, and hand-maintained — a nasty little hat trick.
Every Jest invocation runs a full build first, even though plenty of unit tests import src directly while only CLI-style helpers actually depend on dist. So fast feedback gets dragged through a production build because the suite is one giant bucket of unrelated concerns.
Then lint ignores tests/**, and formatting is managed through a lovingly hand-assembled pile of path globs in package.json. So contributors get slower test runs, weaker static checks, and more script-maintenance chores. Truly premium developer experience. ✨🤡
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 is “security check used as a coaster” with top notes of copy-paste architecture and a lingering finish of CI sadness. 🤮 There’s good intent buried in here, but too much of the implementation still behaves like a developer muttered “eh, probably fine” and sprinted away from the keyboard. Let’s disinfect the worst bits.
Table of Contents
Root Path Guard Gives Up at Filesystem Root
🧨 The helper that is supposed to keep writes inside the project can fail open when it reaches
/, which is a truly inspired interpretation of “security boundary.”The containment check walks up the directory tree until it finds something real. If it walks all the way to filesystem root without finding an existing ancestor, it just returns success. Amazing. The guard dog saw the fence disappear and decided that was a management problem.
Because both
assertManagedPathInsideRoot()andwriteGeneratedFile()rely on this logic, a non-existent out-of-tree target can be blessed without ever proving it lives under the project root. It’s not the easiest bug to hit, but fail-open path validation is exactly the kind of “rare edge case” that becomes a postmortem later.Files
src/core/FileSystemUtils.tsCode
src/core/FileSystemUtils.tslines 93-95 — https://github.com/intellectronica/ruler/blob/main/src/core/FileSystemUtils.ts#L93-L95src/core/FileSystemUtils.tslines 107-118 — https://github.com/intellectronica/ruler/blob/main/src/core/FileSystemUtils.ts#L107-L118Concrete suggestions
rootPathbefore any mkdir/write, then re-check the real path after creation.Apply and Unified Config Run Two Rule Pipelines
🤹 The repo reads and concatenates rules in two separate subsystems, because apparently one source of truth was too emotionally stable.
apply-engine.tsreads markdown files and builds the actual rule payload itself, then separately loadsUnifiedConfigLoaderand uses it only for MCP data. MeanwhileUnifiedConfigLoaderhas its own rule discovery, metadata, diagnostics, and hashing path. So yes, this codebase has two different machines doing the same job and politely pretending that won’t drift.That split is already begging for inconsistency: rule ordering fixes, diagnostics, inclusion rules, and future metadata changes can land in one path and never reach the other. The main apply flow is effectively saying, “Thanks for the unified loader, I’ll just use the side dish.” 🍽️
Files
src/core/apply-engine.tssrc/core/UnifiedConfigLoader.tsCode
src/core/apply-engine.tslines 302-315 — https://github.com/intellectronica/ruler/blob/main/src/core/apply-engine.ts#L302-L315src/core/apply-engine.tslines 73-82 — https://github.com/intellectronica/ruler/blob/main/src/core/apply-engine.ts#L73-L82src/core/UnifiedConfigLoader.tslines 199-239 — https://github.com/intellectronica/ruler/blob/main/src/core/UnifiedConfigLoader.ts#L199-L239Concrete suggestions
apply-engineconsumeunifiedConfig.rules.concatenatedinstead of rebuilding rules itself.Broken Config Files Get Repaired With a Flamethrower
🔥 Several agents treat “config missing” and “config invalid” as the same thing, then overwrite the user’s file like a raccoon with admin access.
OpenCode,RooCode, andCodexCliall swallow parse failures and continue with a fresh config. That means malformed user config can get silently bulldozed into a brand-new managed file instead of surfacing a useful error. Congrats: the tool is now “self-healing” in the same way deleting production is a kind of cleanup.The really spicy part is that
MistralVibeAgentalready shows the sane pattern: tolerateENOENT, throw on invalid config, preserve the original file. So the repo already knows how not to be reckless; it just hasn’t bothered to be consistent about it.Files
src/agents/OpenCodeAgent.tssrc/agents/RooCodeAgent.tssrc/agents/CodexCliAgent.tssrc/agents/MistralVibeAgent.tstests/unit/agents/RooCodeAgent.test.tsCode
src/agents/OpenCodeAgent.tslines 65-86 — https://github.com/intellectronica/ruler/blob/main/src/agents/OpenCodeAgent.ts#L65-L86src/agents/RooCodeAgent.tslines 78-86 — https://github.com/intellectronica/ruler/blob/main/src/agents/RooCodeAgent.ts#L78-L86src/agents/CodexCliAgent.tslines 102-109 — https://github.com/intellectronica/ruler/blob/main/src/agents/CodexCliAgent.ts#L102-L109src/agents/MistralVibeAgent.tslines 156-168 — https://github.com/intellectronica/ruler/blob/main/src/agents/MistralVibeAgent.ts#L156-L168Concrete suggestions
ENOENT; treat parse errors as real errors and stop.Agent Output Path Rules Are Copy Pasted Everywhere
🧬 The repo already has a shared output-path resolver, and then a bunch of agents re-implement the same precedence logic anyway because duplication is apparently a spiritual practice.
getAgentOutputPaths()already centralizesoutputPath/outputPathInstructions/outputPathConfigprecedence, and core apply/revert logic uses it. But agents likeAider,CodexCli, andMistralVibestill hand-roll the same fallback chains inline. This is how tiny logic differences metastasize into “why does revert disagree with apply?” bugs six months from now.Worse, the shared helper drives
.gitignoreand revert accounting while agent implementations do the actual writes. If those two worlds drift, the repo gets the delightful experience of tracking one path and mutating another. Chef’s kiss. 💩Files
src/agents/agent-utils.tssrc/core/apply-engine.tssrc/agents/AiderAgent.tssrc/agents/CodexCliAgent.tssrc/agents/MistralVibeAgent.tsCode
src/agents/agent-utils.tslines 22-36 — https://github.com/intellectronica/ruler/blob/main/src/agents/agent-utils.ts#L22-L36src/core/apply-engine.tslines 452-457 — https://github.com/intellectronica/ruler/blob/main/src/core/apply-engine.ts#L452-L457src/agents/AiderAgent.tslines 33-39 — https://github.com/intellectronica/ruler/blob/main/src/agents/AiderAgent.ts#L33-L39src/agents/CodexCliAgent.tslines 60-65 — https://github.com/intellectronica/ruler/blob/main/src/agents/CodexCliAgent.ts#L60-L65src/agents/MistralVibeAgent.tslines 76-80 — https://github.com/intellectronica/ruler/blob/main/src/agents/MistralVibeAgent.ts#L76-L80Concrete suggestions
The Test Tooling Is Slow Blind and Manual
🐢 The repo’s tooling manages to be simultaneously expensive, under-scoped, and hand-maintained — a nasty little hat trick.
Every Jest invocation runs a full build first, even though plenty of unit tests import
srcdirectly while only CLI-style helpers actually depend ondist. So fast feedback gets dragged through a production build because the suite is one giant bucket of unrelated concerns.Then
lintignorestests/**, and formatting is managed through a lovingly hand-assembled pile of path globs inpackage.json. So contributors get slower test runs, weaker static checks, and more script-maintenance chores. Truly premium developer experience. ✨🤡Files
jest.config.jsjest.setup.jstests/unit/cli/commands.test.tstests/harness.tspackage.jsonCode
jest.config.jslines 27-28 — https://github.com/intellectronica/ruler/blob/main/jest.config.js#L27-L28jest.setup.jslines 3-8 — https://github.com/intellectronica/ruler/blob/main/jest.setup.js#L3-L8tests/unit/cli/commands.test.tslines 1-3 — https://github.com/intellectronica/ruler/blob/main/tests/unit/cli/commands.test.ts#L1-L3tests/harness.tslines 52-57 — https://github.com/intellectronica/ruler/blob/main/tests/harness.ts#L52-L57package.jsonlines 8-12 — https://github.com/intellectronica/ruler/blob/main/package.json#L8-L12Concrete suggestions
src, build-dependent CLI/integration tests separately.tests/**/*.{ts,tsx}with test-specific ESLint overrides instead of pretending test code is decorative.prettier --write ./--check .plus.prettierignore.All reactions