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 perfume is a rich, artisanal blend of fake abstractions, accidental config vandalism, and “surely this won’t scale” optimism 🤮. The repo clearly wants to be a tidy adapter-driven CLI, but parts of it behave like a raccoon with fs.unlink permissions. I stopped at seven high-ROI fixes, mostly because my blood pressure filed a complaint.
🤡 --agents pi can legitimately select GitHub Copilot, which is some world-class clown engineering.
Agent selection accepts a filter when it matches an identifier or merely appears anywhere inside an agent display name. That means pi matches the real pi agent and also "GitHub Copilot" because, congratulations, "copilot" contains "pi".
The same logic is reused for default_agents, so this is not just a CLI paper cut. It can silently generate extra agent files from perfectly valid config. Very user-friendly, if the user is your worst enemy.
If fuzzy matching stays, only allow it when the result is unambiguous.
Add collision tests for short filters like pi, code, q, and other chaos goblins.
Open Hands Treats Root config.toml Like Disposable Junk
☠️ A generic repo-root config.toml is being treated as Ruler-owned, which is how tools become urban legends.
Open Hands MCP targets plain config.toml in the project root. That filename is absurdly generic. Then revert hard-codes the same file into its cleanup list and will unlink it if no backup exists. Incredible stuff: the most collision-prone path in the repo, now with delete privileges.
Apply is no saint either. If parsing the existing TOML fails, Ruler shrugs, assumes empty config, and writes a fresh file anyway. That turns “user has an unrelated root TOML” into “user had an unrelated root TOML.”
case'Open Hands':
// For Open Hands, we target the main config file, not a separate mcp.jsoncandidates.push(path.join(projectRoot,'config.toml'));break;
Stop using bare root config.toml unless ownership is provable.
Only overwrite or delete files with a Ruler marker, manifest entry, or known backup.
On invalid existing TOML, fail loudly with path + parse error instead of rewriting from {}.
Disabling Skills Leaves Stale Generated Directories Behind
🧟 There is cleanup logic for disabled skills, and the main flow politely never calls it. Absolute genius.
propagateSkills() explicitly handles !skillsEnabled by cleaning generated skill directories. Great. Unfortunately, lib.ts only calls propagateSkills() when the resolved flag is truthy, so the cleanup branch is dead code in normal runs.
Result: once skills were generated, turning them off doesn’t reliably remove .claude/skills, .codex/skills, and the rest of the undead swarm. The flag says “disabled”; the filesystem says “lol no.”
if(!skillsEnabled){logVerboseInfo('Skills support disabled, cleaning up skills directories',verbose,dryRun,);awaitcleanupSkillsDirectories(projectRoot,dryRun,verbose);}
Better yet, rename it to something honest like syncSkillsState.
Add regression tests for enabled -> disabled transitions in both single and nested modes.
Nested Discovery Crawls the Whole Repo Like a Goblin
🐀 Nested mode recursively walks every non-hidden directory, which is adorable until it meets a monorepo and starts eating drywall.
The README sells nested mode for complex hierarchies and monorepos. The implementation responds by recursively traversing basically everything except hidden directories. So node_modules, dist, coverage, vendored code, build output, and other landfill all get a warm personal visit.
That is a needless performance tax and a reliability hazard. The bigger the repo, the more this feature turns into a filesystem punishment ritual.
- Discover all `.ruler/` directories in the project hierarchy
- Load and concatenate rules from each directory in order
- Decide whether nested mode is enabled using the following precedence:
1.`ruler apply --nested` (or `--no-nested`) takes top priority
2.`nested = true` in `ruler.toml`
Respect .gitignore or a Ruler-specific exclude list.
Replace manual recursion with a fast glob/search that supports ignore patterns.
The Adapter Pattern Is Mostly Decorative
🎭 You built agent adapters and then stuffed the real behavior back into central special-case code like a magician hiding pigeons in his sleeves.
AbstractAgent advertises a nice adapter interface. Then apply-engine.ts immediately branches on hard-coded agent identifiers for routing, skipping, and transforming MCP behavior. So the core still knows everybody’s weird little secrets. Splendid abstraction. No notes. Actually, many notes.
The same smell shows up in SkillsProcessor.ts, where per-agent skills propagation is mostly the same access -> mkdir -> temp dir -> copy -> rm -> rename ritual duplicated over and over. Adding one agent now means editing core orchestration and cloning more boilerplate. Maintenance by photocopier 💩.
exportasyncfunctionpropagateSkillsForClaude(...){constskillsDir=path.join(projectRoot,RULER_SKILLS_PATH);constclaudeSkillsPath=path.join(projectRoot,CLAUDE_SKILLS_PATH);
...
if(options.dryRun){return[`Copy skills from ${RULER_SKILLS_PATH} to ${CLAUDE_SKILLS_PATH}`];}}
exportasyncfunctionpropagateSkillsForCodex(...){constskillsDir=path.join(projectRoot,RULER_SKILLS_PATH);constcodexSkillsPath=path.join(projectRoot,CODEX_SKILLS_PATH);
...
if(options.dryRun){return[`Copy skills from ${RULER_SKILLS_PATH} to ${CODEX_SKILLS_PATH}`];}}
Suggestions
Move MCP application/transform hooks onto the agent interface.
Replace per-agent skills functions with a registry like { agentId, targetPath, sharedTargetGroup? }.
Keep the core engine dumb; make adapters smart. That is literally the whole point of the pattern.
Invalid MCP Files Get “Recovered” by Being Overwritten
🤕 Several code paths treat parse failures as {} and then happily write new config, which is less recovery and more assisted amnesia.
readNativeMcp() swallows every error and returns an empty object. So malformed JSON, unsupported vendor changes, bad edits, or corrupted files all become “looks empty to me, boss.” That is a spectacular way to lose diagnostics and context.
The same pattern shows up in agent-specific writers like OpenCode and RooCode. If parsing fails, the code assumes a blank config and keeps going. Users don’t get a clear error; they get their file quietly normalized into whatever Ruler currently feels like writing. Wonderful.
letexisting=awaitreadNativeMcp(dest);if(isCodexToml){try{consttomlContent=awaitfs.readFile(dest,'utf8');existing=parseTOML(tomlContent)asRecord<string,unknown>;}catch(error){// ignore missing or invalid TOML, fall back to previously read value}}
Treat ENOENT as normal, but treat parse errors as real errors.
Abort on invalid existing config unless the user explicitly asks for overwrite.
Back up the raw file before parsing and include path + parse error in the failure message.
The Test Suite Pays a Full Build Tax and Commits Dead Artifacts
🧱 Every npm test starts with a full project build, while compiled .js test files sit in the repo like stale roadkill.
globalSetup unconditionally runs npm run build, so even plain unit tests pay the cost of compiling the whole project before doing anything useful. That might make sense for a narrow e2e lane, but for the default test path it’s just a big slow tax receipt.
Meanwhile Jest only matches .ts tests, but compiled .js copies of tests are committed under tests/unit/core/. So the repo stores dead generated artifacts that do not even run. This is clutter, confusion, and future drift bundled into one convenient little trash bag.
describe('RuleProcessor',()=>{it('concatenates and formats rule content with source markers',()=>{constfiles=[{path: '/project/.ruler/a.md',content: 'A rule'},{path: '/project/.ruler/b.md',content: 'B rule'},];
describe('RuleProcessor',()=>{it('concatenates and formats rule content with source markers',()=>{constfiles=[{path: '/project/.ruler/a.md',content: 'A rule'},{path: '/project/.ruler/b.md',content: 'B rule'},];jest.spyOn(process,'cwd').mockReturnValue('/project');
Suggestions
Build only for e2e/integration suites that require emitted dist artifacts.
Remove committed generated .js tests and ignore them so they stop fossilizing in git.
Split fast unit tests from heavyweight CLI/e2e runs so local feedback stops taking the scenic route.
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 perfume is a rich, artisanal blend of fake abstractions, accidental config vandalism, and “surely this won’t scale” optimism 🤮. The repo clearly wants to be a tidy adapter-driven CLI, but parts of it behave like a raccoon with
fs.unlinkpermissions. I stopped at seven high-ROI fixes, mostly because my blood pressure filed a complaint.Table of Contents
config.tomlLike Disposable JunkSubstring Agent Filters Pick the Wrong Agents
🤡
--agents pican legitimately selectGitHub Copilot, which is some world-class clown engineering.pimatches the realpiagent and also"GitHub Copilot"because, congratulations,"copilot"contains"pi".default_agents, so this is not just a CLI paper cut. It can silently generate extra agent files from perfectly valid config. Very user-friendly, if the user is your worst enemy.Files
src/core/agent-selection.tssrc/agents/CopilotAgent.tssrc/agents/PiAgent.tsCode
default_agentsrepeats the same trick: src/core/agent-selection.ts#L81-L83Suggestions
pi,code,q, and other chaos goblins.Open Hands Treats Root
config.tomlLike Disposable Junk☠️ A generic repo-root
config.tomlis being treated as Ruler-owned, which is how tools become urban legends.config.tomlin the project root. That filename is absurdly generic. Then revert hard-codes the same file into its cleanup list and will unlink it if no backup exists. Incredible stuff: the most collision-prone path in the repo, now with delete privileges.Files
src/paths/mcp.tssrc/core/revert-engine.tssrc/mcp/propagateOpenHandsMcp.tsCode
config.toml: src/paths/mcp.ts#L33-L36config.tomlinto extra cleanup: src/core/revert-engine.ts#L311-L319Suggestions
config.tomlunless ownership is provable.{}.Disabling Skills Leaves Stale Generated Directories Behind
🧟 There is cleanup logic for disabled skills, and the main flow politely never calls it. Absolute genius.
propagateSkills()explicitly handles!skillsEnabledby cleaning generated skill directories. Great. Unfortunately,lib.tsonly callspropagateSkills()when the resolved flag is truthy, so the cleanup branch is dead code in normal runs..claude/skills,.codex/skills, and the rest of the undead swarm. The flag says “disabled”; the filesystem says “lol no.”Files
src/lib.tssrc/core/SkillsProcessor.tsCode
Suggestions
propagateSkills(projectRoot, agents, resolvedFlag, ...).syncSkillsState.enabled -> disabledtransitions in both single and nested modes.Nested Discovery Crawls the Whole Repo Like a Goblin
🐀 Nested mode recursively walks every non-hidden directory, which is adorable until it meets a monorepo and starts eating drywall.
node_modules,dist,coverage, vendored code, build output, and other landfill all get a warm personal visit.Files
README.mdsrc/core/FileSystemUtils.tsCode
Suggestions
node_modules,dist,build,coverage,vendor,.next, etc..gitignoreor a Ruler-specific exclude list.The Adapter Pattern Is Mostly Decorative
🎭 You built agent adapters and then stuffed the real behavior back into central special-case code like a magician hiding pigeons in his sleeves.
AbstractAgentadvertises a nice adapter interface. Thenapply-engine.tsimmediately branches on hard-coded agent identifiers for routing, skipping, and transforming MCP behavior. So the core still knows everybody’s weird little secrets. Splendid abstraction. No notes. Actually, many notes.SkillsProcessor.ts, where per-agent skills propagation is mostly the sameaccess -> mkdir -> temp dir -> copy -> rm -> renameritual duplicated over and over. Adding one agent now means editing core orchestration and cloning more boilerplate. Maintenance by photocopier 💩.Files
src/agents/AbstractAgent.tssrc/core/apply-engine.tssrc/core/SkillsProcessor.tsCode
Suggestions
{ agentId, targetPath, sharedTargetGroup? }.Invalid MCP Files Get “Recovered” by Being Overwritten
🤕 Several code paths treat parse failures as
{}and then happily write new config, which is less recovery and more assisted amnesia.readNativeMcp()swallows every error and returns an empty object. So malformed JSON, unsupported vendor changes, bad edits, or corrupted files all become “looks empty to me, boss.” That is a spectacular way to lose diagnostics and context.Files
src/paths/mcp.tssrc/core/apply-engine.tssrc/mcp/propagateOpenCodeMcp.tssrc/agents/RooCodeAgent.tsCode
Suggestions
ENOENTas normal, but treat parse errors as real errors.The Test Suite Pays a Full Build Tax and Commits Dead Artifacts
🧱 Every
npm teststarts with a full project build, while compiled.jstest files sit in the repo like stale roadkill.globalSetupunconditionally runsnpm run build, so even plain unit tests pay the cost of compiling the whole project before doing anything useful. That might make sense for a narrow e2e lane, but for the default test path it’s just a big slow tax receipt..tstests, but compiled.jscopies of tests are committed undertests/unit/core/. So the repo stores dead generated artifacts that do not even run. This is clutter, confusion, and future drift bundled into one convenient little trash bag.Files
jest.setup.jsjest.config.jstests/unit/core/RuleProcessor.test.tstests/unit/core/RuleProcessor.test.jstests/unit/core/FileSystemUtils.test.jsCode
Suggestions
distartifacts..jstests and ignore them so they stop fossilizing in git.All reactions