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 bouquet is copy-paste flambé with top notes of stale agent metadata, a heart of surprise JSON behavior, and a lingering finish of recursive filesystem tourism. 🤮 The repo is trying very hard — there are tests everywhere — but it keeps cashing that goodwill on architecture that looks like it was assembled during a caffeine-fueled hostage negotiation.
I picked the highest-payoff fixes: the stuff that buys back maintainability, robustness, performance, and developer sanity without requiring an exorcist. 💀
🪦 Most agent behavior is plain metadata, but the repo stores that metadata as classes, constants, registries, and docs like duplication is a lifestyle choice.
ClaudeAgent is basically a few strings wearing an inheritance trench coat, while allAgents manually instantiates the whole zoo and README.md keeps a second support matrix alive by hand. That is not elegant abstraction; that is distributed bookkeeping with extra ceremony.
Every new agent or path tweak now wants synchronized edits across runtime code, constants, and documentation. Drift is not a risk here — it is the default future.
Define one AgentDefinition registry with identifier, display name, rules path, MCP path/key, skills/subagent support, and optional custom transformers.
Generate allAgents, CLI help, and as much of the README support table as possible from that registry.
Keep real classes only for genuinely weird integrations; let boring agents be boring data.
Propagation Logic Is Copy Paste Exhaust
🤢 Skills and subagent propagation are built as clone armies even though both processors already contain target maps begging for a shared driver.
SkillsProcessor has a path map, then still hand-writes a parade of propagateSkillsFor* functions that all do the same access-check / dry-run / temp-dir / remove / replace dance. Incredible. A standing ovation for avoidable maintenance debt. 💩
SubagentsProcessor pulls the same stunt again. So every fix to atomic writes, cleanup, dry-run output, or logging now has to be copied into multiple near-identical branches and hopefully not forgotten in one of them.
exportasyncfunctionpropagateSkillsForClaude(projectRoot: string,options: {dryRun: boolean},): Promise<string[]>{constskillsDir=path.join(projectRoot,RULER_SKILLS_PATH);constclaudeSkillsPath=path.join(projectRoot,CLAUDE_SKILLS_PATH);constclaudeDir=path.dirname(claudeSkillsPath);try{awaitfs.access(skillsDir);}catch{return[];}if(options.dryRun){return[`Copy skills from ${RULER_SKILLS_PATH} to ${CLAUDE_SKILLS_PATH}`];}
And the second clone says the same thing with a fake moustache:
exportasyncfunctionpropagateSkillsForCodex(projectRoot: string,options: {dryRun: boolean},): Promise<string[]>{constskillsDir=path.join(projectRoot,RULER_SKILLS_PATH);constcodexSkillsPath=path.join(projectRoot,CODEX_SKILLS_PATH);constcodexDir=path.dirname(codexSkillsPath);try{awaitfs.access(skillsDir);}catch{return[];}if(options.dryRun){return[`Copy skills from ${RULER_SKILLS_PATH} to ${CODEX_SKILLS_PATH}`];}
Replace per-target functions with a shared propagateToTarget(definition, items, options) helper for skills and another for subagents.
Store path, extension, serializer, and dry-run formatter in the target definition map.
Convert tests to table-driven target matrices so adding an integration means adding data, not another 40-line ritual.
Config Parsing Is a JSON Roulette Wheel
🎰 Malformed config files get silently replaced in some agents, hard-failed in others, and barely validated anywhere, which is a splendid way to make bugs feel random.
OpenCodeAgent and CrushAgent swallow parse failures and rebuild config; QwenCodeAgent rethrows non-ENOENT errors. Same class of bad input, totally different outcome depending on which adapter touched the file first. Love that for support tickets.
The repo already has shared validation primitives, but agent adapters mostly freelance with Record<string, unknown> and vibes. That is not robustness; that is config fan fiction.
exportfunctionvalidateMcp(data: unknown): void{constmcpServers=data&&typeofdata==='object'
? (dataasRecord<string,unknown>).mcpServers
: undefined;if(!data||typeofdata!=='object'||!('mcpServers'indata)||!mcpServers||typeofmcpServers!=='object'||Array.isArray(mcpServers)){thrownewError('[ruler] Invalid MCP config: must contain an object property "mcpServers" (Ruler style)',);}}
Suggestions
Add one shared loadJsonConfig/loadTomlConfig utility that distinguishes missing-file from parse/shape errors and returns typed data.
Validate adapter inputs with Zod or the existing MCP validator before merge logic; do not treat malformed user config as a blank slate unless repair mode is explicit.
Centralize merge semantics so "merge" vs "overwrite" and error handling stop changing by adapter mood swing.
Nested Discovery Walks the Whole Damn Repo
🥴 Nested mode does multiple recursive filesystem tours and quietly swallows directory-read failures, because apparently the fastest code is the one that explores everything and explains nothing.
In nested mode, apply first crawls downward to find every .ruler, then recursively walks each discovered directory again to read Markdown. On a large repo, that turns a simple config command into a scenic monorepo backpacking trip.
The ignore list is hard-coded and short, and unreadable directories disappear into catch {} oblivion. So when performance or correctness gets interesting, diagnostics get worse. Beautifully cursed. 🤡
Files
src/core/apply-engine.ts
src/core/FileSystemUtils.ts
Code
Nested apply does discovery and then a second per-dir walk:
Replace the two-pass recursion with one traversal that discovers .ruler dirs and collects their markdown manifests together.
Make ignores configurable and/or .gitignore-aware, and add a depth/entry budget so huge repos do not trigger surprise filesystem safaris.
Surface skipped directories as warnings with path + errno instead of silent disappearance.
Warning State Leaks Into Production
🤡 “Warn once” is implemented with module-global booleans, and the tests import reset levers from production code to keep the whole circus running.
Logging behavior depends on process history instead of explicit call context. Same input, different output after the first call. That is cute for a shell script; it is a lousy contract for reusable core code.
The exported _reset...ForTests() hooks are the dead giveaway: tests need secret buttons because the design hides state in modules. That is not a test helper. That is an architectural cry for help. 🚨
lethasWarnedExperimental=false;functionwarnOnceExperimental(dryRun: boolean): void{if(hasWarnedExperimental)return;hasWarnedExperimental=true;logWarn('Subagents support is experimental and behavior may change in future releases.',dryRun,);}exportfunction_resetExperimentalWarningForTests(): void{hasWarnedExperimental=false;}
let_legacySubagentsWarned=false;functionwarnLegacySubagentsSection(): void{if(_legacySubagentsWarned)return;_legacySubagentsWarned=true;logWarn('`[subagents]` is deprecated; rename it to `[agents]` in your ruler.toml. '+'The legacy section is honored for now and will be removed in a future release.',);}exportfunction_resetLegacySubagentsWarningForTests(): void{_legacySubagentsWarned=false;}
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 bouquet is copy-paste flambé with top notes of stale agent metadata, a heart of surprise JSON behavior, and a lingering finish of recursive filesystem tourism. 🤮 The repo is trying very hard — there are tests everywhere — but it keeps cashing that goodwill on architecture that looks like it was assembled during a caffeine-fueled hostage negotiation.
I picked the highest-payoff fixes: the stuff that buys back maintainability, robustness, performance, and developer sanity without requiring an exorcist. 💀
Table of Contents
Agent Metadata Is Everywhere
🪦 Most agent behavior is plain metadata, but the repo stores that metadata as classes, constants, registries, and docs like duplication is a lifestyle choice.
ClaudeAgentis basically a few strings wearing an inheritance trench coat, whileallAgentsmanually instantiates the whole zoo andREADME.mdkeeps a second support matrix alive by hand. That is not elegant abstraction; that is distributed bookkeeping with extra ceremony.Files
README.mdsrc/agents/index.tssrc/agents/ClaudeAgent.tssrc/constants.tsCode
README.mdlines 57–62: https://github.com/intellectronica/ruler/blob/main/README.md#L57-L62src/agents/index.tslines 38–46: https://github.com/intellectronica/ruler/blob/main/src/agents/index.ts#L38-L46src/agents/ClaudeAgent.tslines 7–18: https://github.com/intellectronica/ruler/blob/main/src/agents/ClaudeAgent.ts#L7-L18src/constants.tslines 57–64: https://github.com/intellectronica/ruler/blob/main/src/constants.ts#L57-L64Suggestions
AgentDefinitionregistry with identifier, display name, rules path, MCP path/key, skills/subagent support, and optional custom transformers.allAgents, CLI help, and as much of the README support table as possible from that registry.Propagation Logic Is Copy Paste Exhaust
🤢 Skills and subagent propagation are built as clone armies even though both processors already contain target maps begging for a shared driver.
SkillsProcessorhas a path map, then still hand-writes a parade ofpropagateSkillsFor*functions that all do the same access-check / dry-run / temp-dir / remove / replace dance. Incredible. A standing ovation for avoidable maintenance debt. 💩SubagentsProcessorpulls the same stunt again. So every fix to atomic writes, cleanup, dry-run output, or logging now has to be copied into multiple near-identical branches and hopefully not forgotten in one of them.Files
src/core/SkillsProcessor.tssrc/core/SubagentsProcessor.tsCode
src/core/SkillsProcessor.tslines 81–85: https://github.com/intellectronica/ruler/blob/main/src/core/SkillsProcessor.ts#L81-L85src/core/SkillsProcessor.tslines 503–520: https://github.com/intellectronica/ruler/blob/main/src/core/SkillsProcessor.ts#L503-L520src/core/SkillsProcessor.tslines 561–579: https://github.com/intellectronica/ruler/blob/main/src/core/SkillsProcessor.ts#L561-L579src/core/SubagentsProcessor.tslines 82–86: https://github.com/intellectronica/ruler/blob/main/src/core/SubagentsProcessor.ts#L82-L86src/core/SubagentsProcessor.tslines 324–342: https://github.com/intellectronica/ruler/blob/main/src/core/SubagentsProcessor.ts#L324-L342Suggestions
propagateToTarget(definition, items, options)helper for skills and another for subagents.Config Parsing Is a JSON Roulette Wheel
🎰 Malformed config files get silently replaced in some agents, hard-failed in others, and barely validated anywhere, which is a splendid way to make bugs feel random.
OpenCodeAgentandCrushAgentswallow parse failures and rebuild config;QwenCodeAgentrethrows non-ENOENTerrors. Same class of bad input, totally different outcome depending on which adapter touched the file first. Love that for support tickets.Record<string, unknown>and vibes. That is not robustness; that is config fan fiction.Files
src/agents/OpenCodeAgent.tssrc/agents/CrushAgent.tssrc/agents/QwenCodeAgent.tssrc/mcp/validate.tsCode
src/agents/OpenCodeAgent.tslines 74–78: https://github.com/intellectronica/ruler/blob/main/src/agents/OpenCodeAgent.ts#L74-L78src/agents/CrushAgent.tslines 112–119: https://github.com/intellectronica/ruler/blob/main/src/agents/CrushAgent.ts#L112-L119src/agents/QwenCodeAgent.tslines 44–50: https://github.com/intellectronica/ruler/blob/main/src/agents/QwenCodeAgent.ts#L44-L50src/mcp/validate.tslines 7–23: https://github.com/intellectronica/ruler/blob/main/src/mcp/validate.ts#L7-L23Suggestions
loadJsonConfig/loadTomlConfigutility that distinguishes missing-file from parse/shape errors and returns typed data."merge"vs"overwrite"and error handling stop changing by adapter mood swing.Nested Discovery Walks the Whole Damn Repo
🥴 Nested mode does multiple recursive filesystem tours and quietly swallows directory-read failures, because apparently the fastest code is the one that explores everything and explains nothing.
applyfirst crawls downward to find every.ruler, then recursively walks each discovered directory again to read Markdown. On a large repo, that turns a simple config command into a scenic monorepo backpacking trip.catch {}oblivion. So when performance or correctness gets interesting, diagnostics get worse. Beautifully cursed. 🤡Files
src/core/apply-engine.tssrc/core/FileSystemUtils.tsCode
src/core/apply-engine.tslines 61–80: https://github.com/intellectronica/ruler/blob/main/src/core/apply-engine.ts#L61-L80src/core/FileSystemUtils.tslines 10–21: https://github.com/intellectronica/ruler/blob/main/src/core/FileSystemUtils.ts#L10-L21src/core/FileSystemUtils.tslines 417–445: https://github.com/intellectronica/ruler/blob/main/src/core/FileSystemUtils.ts#L417-L445src/core/FileSystemUtils.tslines 213–239: https://github.com/intellectronica/ruler/blob/main/src/core/FileSystemUtils.ts#L213-L239Suggestions
.rulerdirs and collects their markdown manifests together..gitignore-aware, and add a depth/entry budget so huge repos do not trigger surprise filesystem safaris.Warning State Leaks Into Production
🤡 “Warn once” is implemented with module-global booleans, and the tests import reset levers from production code to keep the whole circus running.
_reset...ForTests()hooks are the dead giveaway: tests need secret buttons because the design hides state in modules. That is not a test helper. That is an architectural cry for help. 🚨Files
src/core/SkillsProcessor.tssrc/core/SubagentsProcessor.tssrc/core/ConfigLoader.tstests/subagents-rules-concatenation.test.tsCode
src/core/SkillsProcessor.tslines 108–118: https://github.com/intellectronica/ruler/blob/main/src/core/SkillsProcessor.ts#L108-L118src/core/SubagentsProcessor.tslines 136–151: https://github.com/intellectronica/ruler/blob/main/src/core/SubagentsProcessor.ts#L136-L151src/core/ConfigLoader.tslines 20–33: https://github.com/intellectronica/ruler/blob/main/src/core/ConfigLoader.ts#L20-L33tests/subagents-rules-concatenation.test.tslines 74–77: https://github.com/intellectronica/ruler/blob/main/tests/subagents-rules-concatenation.test.ts#L74-L77Suggestions
apply, or into a logger instance with scoped state.All reactions