From 1f9a581e703d0416e4bf42846baeb12d58164efc Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 13:04:20 +0900 Subject: [PATCH] fix(rules-core): fall back to workspace directory when no project root marker is found Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- packages/rules-core/src/finder.ts | 26 ++++++++++++++++++----- packages/rules-core/src/types.ts | 1 + src/hooks/rules-injector/injector.test.ts | 1 + src/hooks/rules-injector/injector.ts | 5 ++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/rules-core/src/finder.ts b/packages/rules-core/src/finder.ts index ebf6e7ab5..8c7d58191 100644 --- a/packages/rules-core/src/finder.ts +++ b/packages/rules-core/src/finder.ts @@ -15,21 +15,37 @@ export function findRuleFiles( ): RuleFileCandidate[] { const startDir = dirname(resolve(currentFile)); const skipClaudeUserRules = options?.skipClaudeUserRules ?? false; - const cacheKey = [projectRoot ?? "", startDir, skipClaudeUserRules ? "1" : "0"].join("\0"); + const effectiveProjectRoot = resolveEffectiveProjectRoot( + projectRoot, + options?.workspaceDirectory, + startDir, + ); + const cacheKey = [projectRoot ?? "", effectiveProjectRoot, startDir, skipClaudeUserRules ? "1" : "0"].join( + "\0", + ); const cached = cache?.get(cacheKey); if (cached) return [...cached]; const candidates: RuleFileCandidate[] = []; const seenRealPaths = new Set(); - if (projectRoot) { - addProjectRuleCandidates(projectRoot, startDir, candidates, seenRealPaths, cache); - addProjectSingleFileCandidates(projectRoot, candidates, seenRealPaths); - } + addProjectRuleCandidates(effectiveProjectRoot, startDir, candidates, seenRealPaths, cache); + addProjectSingleFileCandidates(effectiveProjectRoot, candidates, seenRealPaths); addUserRuleCandidates(homeDir || homedir(), skipClaudeUserRules, candidates, seenRealPaths, cache); const sorted = sortCandidates(candidates); cache?.set(cacheKey, sorted); return sorted; } +function resolveEffectiveProjectRoot( + projectRoot: string | null, + workspaceDirectory: string | undefined, + startDir: string, +): string { + if (projectRoot) return projectRoot; + if (!workspaceDirectory) return startDir; + const workspaceRoot = resolve(workspaceDirectory); + return isSameOrChildPath(startDir, workspaceRoot) ? workspaceRoot : startDir; +} + function addProjectRuleCandidates( projectRoot: string, startDir: string, diff --git a/packages/rules-core/src/types.ts b/packages/rules-core/src/types.ts index a6d44c1da..3be7f2baa 100644 --- a/packages/rules-core/src/types.ts +++ b/packages/rules-core/src/types.ts @@ -58,6 +58,7 @@ export interface RuleScanCache { export interface FindRuleFilesOptions { readonly skipClaudeUserRules?: boolean; + readonly workspaceDirectory?: string; } export interface AgentsMdCache { diff --git a/src/hooks/rules-injector/injector.test.ts b/src/hooks/rules-injector/injector.test.ts index 67fa2b9b9..b7dd8acc2 100644 --- a/src/hooks/rules-injector/injector.test.ts +++ b/src/hooks/rules-injector/injector.test.ts @@ -339,6 +339,7 @@ describe("createRuleInjectionProcessor", () => { contentHashes: new Set(), realPaths: new Set([ruleRealPath]), }), + homedir: () => homeRoot, }); // when diff --git a/src/hooks/rules-injector/injector.ts b/src/hooks/rules-injector/injector.ts index cd23195d3..365da972c 100644 --- a/src/hooks/rules-injector/injector.ts +++ b/src/hooks/rules-injector/injector.ts @@ -137,6 +137,9 @@ export function createRuleInjectionProcessor(deps: { } = deps; const matchDecisionCache: MatchDecisionCache = new Map(); + const finderOptions: FindRuleFilesOptions = ruleFinderOptions + ? { ...ruleFinderOptions, workspaceDirectory } + : { workspaceDirectory }; function getParsedRule(filePath: string, realPath: string): ParsedRule { try { @@ -189,7 +192,7 @@ export function createRuleInjectionProcessor(deps: { projectRoot, home, resolved, - ruleFinderOptions, + finderOptions, ruleScanCache, ); const toInject: RuleToInject[] = [];