From 2c62e7297b55588182c09dcd0fc472e2b242353e Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 13:01:14 +0900 Subject: [PATCH 1/2] test(rules-core): add red test for project rule discovery in markerless workspaces (BUG-F) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- packages/rules-core/src/index.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/rules-core/src/index.test.ts b/packages/rules-core/src/index.test.ts index 30bb05981..c7b9f770c 100644 --- a/packages/rules-core/src/index.test.ts +++ b/packages/rules-core/src/index.test.ts @@ -62,6 +62,29 @@ describe("rules-core", () => { expect(found.map((rule) => rule.relativePath)).not.toContain(".sisyphus/rules/sisyphus.md"); }); + it("#given a workspace directory has no project marker (no .git, no package.json, etc.) AND contains .omo/rules/ #when findRuleFiles is called #then the .omo/rules/ files are still discovered", () => { + // given + const root = createTestRoot("rules-core-markerless-workspace"); + const homeDir = join(root, "home"); + const sourceDir = join(root, "src"); + const ruleFile = join(root, ".omo", "rules", "test-rule.md"); + const currentFile = join(sourceDir, "index.ts"); + mkdirSync(join(root, ".omo", "rules"), { recursive: true }); + mkdirSync(homeDir, { recursive: true }); + mkdirSync(sourceDir, { recursive: true }); + writeFileSync(ruleFile, "markerless workspace rule"); + writeFileSync(currentFile, "export {};"); + const projectRoot = findProjectRoot(currentFile); + const options = { skipClaudeUserRules: false, workspaceDirectory: root }; + + // when + const found = findRuleFiles(projectRoot, homeDir, currentFile, options); + + // then + expect(projectRoot).toBeNull(); + expect(found.map((rule) => rule.path)).toContain(ruleFile); + }); + it("#given frontmatter aliases and negative glob #when matching #then honors applyTo paths and exclusions", () => { // given const { metadata } = parseRuleFrontmatter(`---\npaths: ["src/**/*.ts"]\napplyTo:\n - "!src/**/*.test.ts"\n---\nRule\n`); From 1f9a581e703d0416e4bf42846baeb12d58164efc Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 13:04:20 +0900 Subject: [PATCH 2/2] 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[] = [];