Merge pull request #4200 from code-yeongyu/fix/rules-core-restore-sisyphus-with-deprecation-warning

fix(rules-core): restore .sisyphus/rules discovery with deprecation warning (planned removal v4.3.0)
This commit is contained in:
YeonGyu-Kim
2026-05-20 13:24:31 +09:00
committed by GitHub
6 changed files with 103 additions and 6 deletions
+7
View File
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [4.2.3] - Unreleased
### Reverted Breaking Changes
- Restored `.sisyphus/rules` and `~/.sisyphus/rules` rule-source discovery that was silently removed in v4.2.2..HEAD. They now load with LOWEST priority among project rule sources and emit a deprecation warning. **Planned removal in v4.3.0**: migrate to `.omo/rules` and `~/.omo/rules`.
## [4.2.1] - Unreleased
### Fixed
@@ -49,5 +55,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Delegated child-session early-failure fallback (BLOCKER-4)**: PR #3825's `fac90d69f` was reverted by PR #4044 because its own regression test failed on clean root `bun test`. The delegate-task fallback bug for empty session history remains unaddressed in v4.2.0. Reland targets v4.2.1 once the regression test is stabilized against post-#4032 schema and the new gate semantics. See `docs/reference/known-issues.md` for details and workaround.
- **First-prompt watchdog supersession history (L16)**: PR #3952 was superseded by PR #4051 (rebased over #4007/factory refactor with `internallyAbortedSessions` threading). The supersession represents conflict resolution, not a feature pivot. The final watchdog logic shipped via #4051 + `a130fa70d` covers subagent first-prompt silence past 90 seconds with cleanup via session.deleted.
[4.2.3]: https://github.com/code-yeongyu/oh-my-openagent/compare/v4.2.2...HEAD
[4.2.1]: https://github.com/code-yeongyu/oh-my-openagent/compare/v4.2.0...HEAD
[4.2.0]: https://github.com/code-yeongyu/oh-my-openagent/compare/v4.1.2...v4.2.0
+4 -1
View File
@@ -7,10 +7,11 @@ export const PROJECT_RULE_SUBDIRS = [
[".claude", "rules"],
[".cursor", "rules"],
[".github", "instructions"],
[".sisyphus", "rules"],
] as const;
export const PROJECT_RULE_FILES = [".github/copilot-instructions.md"] as const;
export const OPENCODE_USER_RULE_DIRS = [".omo/rules", ".opencode/rules"] as const;
export const OPENCODE_USER_RULE_DIRS = [".omo/rules", ".opencode/rules", ".sisyphus/rules"] as const;
export const USER_RULE_DIR = ".claude/rules";
export const RULE_EXTENSIONS = [".md", ".mdc"] as const;
export const GITHUB_INSTRUCTIONS_PATTERN = /\.instructions\.md$/;
@@ -24,7 +25,9 @@ export const SOURCE_PRIORITY: ReadonlyMap<RuleSource, number> = new Map([
[".cursor/rules", 2],
[".github/instructions", 3],
[".github/copilot-instructions.md", 4],
[".sisyphus/rules", 5],
["~/.omo/rules", 100],
["~/.opencode/rules", 101],
["~/.claude/rules", 102],
["~/.sisyphus/rules", 103],
]);
+28
View File
@@ -5,6 +5,12 @@ import { GLOBAL_DISTANCE, OPENCODE_USER_RULE_DIRS, PROJECT_RULE_FILES, PROJECT_R
import { sortCandidates } from "./ordering";
import { findRuleFilesRecursive, safeRealpathSync } from "./scanner";
import type { DirectoryScanEntry, FindRuleFilesOptions, RuleFileCandidate, RuleScanCache, RuleSource } from "./types";
import { log } from "../../../src/shared/logger";
const SISYPHUS_DEPRECATION_MESSAGE = "[rules] .sisyphus/rules is deprecated and will be removed in v4.3.0; migrate to .omo/rules";
const SISYPHUS_LEGACY_RULE_SOURCES: ReadonlySet<RuleSource> = new Set([".sisyphus/rules", "~/.sisyphus/rules"]);
const warnedSisyphusRuleDirectories = new Set<string>();
let logSisyphusRuleDeprecation: typeof log = log;
export function findRuleFiles(
projectRoot: string | null,
@@ -62,6 +68,7 @@ function addProjectRuleCandidates(
for (const entry of scanDirectoryWithCache(ruleDir, cache)) {
if (seenRealPaths.has(entry.realPath)) continue;
seenRealPaths.add(entry.realPath);
warnSisyphusRuleDeprecation(source, entry.path);
candidates.push({
path: entry.path,
realPath: entry.realPath,
@@ -115,6 +122,7 @@ function addUserRuleCandidates(
for (const entry of scanDirectoryWithCache(userRuleDir, cache)) {
if (seenRealPaths.has(entry.realPath)) continue;
seenRealPaths.add(entry.realPath);
warnSisyphusRuleDeprecation(source, entry.path);
candidates.push({
path: entry.path,
realPath: entry.realPath,
@@ -136,6 +144,26 @@ function scanDirectoryWithCache(dir: string, cache: RuleScanCache | undefined):
return entries;
}
function warnSisyphusRuleDeprecation(source: RuleSource, path: string): void {
if (!SISYPHUS_LEGACY_RULE_SOURCES.has(source)) return;
const warningKey = dirname(path);
if (warnedSisyphusRuleDirectories.has(warningKey)) return;
warnedSisyphusRuleDirectories.add(warningKey);
logSisyphusRuleDeprecation(SISYPHUS_DEPRECATION_MESSAGE, {
event: "rules-sisyphus-deprecated",
path,
});
}
export function _setSisyphusRuleDeprecationLoggerForTesting(logger: typeof log): void {
logSisyphusRuleDeprecation = logger;
}
export function _resetSisyphusRuleDeprecationWarningStateForTesting(): void {
warnedSisyphusRuleDirectories.clear();
logSisyphusRuleDeprecation = log;
}
function validFileRealPath(filePath: string): string | null {
if (!existsSync(filePath)) return null;
try {
+59 -2
View File
@@ -13,9 +13,12 @@ import {
parseRuleFrontmatter,
shouldApplyRule,
} from "./index";
import { _resetSisyphusRuleDeprecationWarningStateForTesting, _setSisyphusRuleDeprecationLoggerForTesting } from "./finder";
let testRoot: string | null = null;
const SISYPHUS_DEPRECATION_MESSAGE = "[rules] .sisyphus/rules is deprecated and will be removed in v4.3.0; migrate to .omo/rules";
function createTestRoot(name: string): string {
testRoot = join(tmpdir(), `${name}-${Date.now()}-${Math.random()}`);
mkdirSync(testRoot, { recursive: true });
@@ -23,6 +26,7 @@ function createTestRoot(name: string): string {
}
afterEach(() => {
_resetSisyphusRuleDeprecationWarningStateForTesting();
if (testRoot) {
rmSync(testRoot, { recursive: true, force: true });
testRoot = null;
@@ -58,8 +62,55 @@ describe("rules-core", () => {
".claude/rules/claude.md",
".cursor/rules/cursor.md",
".github/instructions/github.instructions.md",
".sisyphus/rules/sisyphus.md",
]);
expect(found.map((rule) => rule.relativePath)).not.toContain(".sisyphus/rules/sisyphus.md");
});
it("#given a workspace with .sisyphus/rules/*.md #when findRuleFiles is called #then those files are discovered with lowest priority among project sources", () => {
// given
const root = createTestRoot("rules-core-sisyphus-restored");
mkdirSync(join(root, ".git"));
mkdirSync(join(root, ".omo", "rules"), { recursive: true });
mkdirSync(join(root, ".sisyphus", "rules"), { recursive: true });
mkdirSync(join(root, "src"), { recursive: true });
writeFileSync(join(root, ".omo", "rules", "shared.md"), "omo");
writeFileSync(join(root, ".sisyphus", "rules", "shared.md"), "legacy");
writeFileSync(join(root, ".sisyphus", "rules", "legacy.md"), "legacy");
// when
const found = findRuleFiles(root, root, join(root, "src", "index.ts"));
const relativePaths = found.map((rule) => rule.relativePath);
const omoSharedIndex = relativePaths.indexOf(".omo/rules/shared.md");
const sisyphusSharedIndex = relativePaths.indexOf(".sisyphus/rules/shared.md");
// then
expect(relativePaths).toContain(".sisyphus/rules/legacy.md");
expect(omoSharedIndex).toBeGreaterThanOrEqual(0);
expect(sisyphusSharedIndex).toBeGreaterThan(omoSharedIndex);
});
it("#given .sisyphus/rules is discovered #when the finder runs #then a deprecation warning is logged exactly once", () => {
// given
const root = createTestRoot("rules-core-sisyphus-warning");
const legacyRulePath = join(root, ".sisyphus", "rules", "legacy.md");
mkdirSync(join(root, ".git"));
mkdirSync(join(root, ".sisyphus", "rules"), { recursive: true });
mkdirSync(join(root, "src"), { recursive: true });
writeFileSync(legacyRulePath, "legacy");
const warnings: Array<{ readonly message: string; readonly data: unknown }> = [];
_setSisyphusRuleDeprecationLoggerForTesting((message, data) => {
warnings.push({ message, data });
});
// when
findRuleFiles(root, root, join(root, "src", "index.ts"));
findRuleFiles(root, root, join(root, "src", "index.ts"));
const deprecationWarnings = warnings.filter(
({ message, data }) => message === SISYPHUS_DEPRECATION_MESSAGE && isSisyphusDeprecationData(data, legacyRulePath),
);
// then
expect(deprecationWarnings).toHaveLength(1);
});
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", () => {
@@ -136,7 +187,7 @@ describe("rules-core", () => {
// then
expect(first).toEqual(second);
expect(cache.stats()).toEqual({ candidateEntries: 1, directoryEntries: 9 });
expect(cache.stats()).toEqual({ candidateEntries: 1, directoryEntries: 11 });
});
it("#given nested project markers #when finding project root #then memoizes ancestor lookups", () => {
@@ -154,3 +205,9 @@ describe("rules-core", () => {
expect(second).toBe(root);
});
});
function isSisyphusDeprecationData(data: unknown, path: string): boolean {
if (typeof data !== "object" || data === null) return false;
if (!("event" in data) || !("path" in data)) return false;
return data.event === "rules-sisyphus-deprecated" && data.path === path;
}
+3 -1
View File
@@ -27,9 +27,11 @@ export type RuleSource =
| ".cursor/rules"
| ".github/instructions"
| ".github/copilot-instructions.md"
| ".sisyphus/rules"
| "~/.omo/rules"
| "~/.opencode/rules"
| "~/.claude/rules";
| "~/.claude/rules"
| "~/.sisyphus/rules";
export interface MatchResult {
readonly applies: boolean;
+2 -2
View File
@@ -87,10 +87,10 @@ async function createProcessor(projectRoot: string): Promise<{
trackedShouldApplyRuleCount += 1;
return { applies: true, reason: "matched" };
},
isDuplicateByRealPath: (realPath: string, cache: Set<string>) =>
isDuplicateByRealPath: (realPath: string, cache: ReadonlySet<string>) =>
cache.has(realPath),
createContentHash: (content: string) => `hash:${content}`,
isDuplicateByContentHash: (hash: string, cache: Set<string>) =>
isDuplicateByContentHash: (hash: string, cache: ReadonlySet<string>) =>
cache.has(hash),
});
}