From 7ffe823fa981fa04244fb9385bf6d49f63b964b9 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 13:03:20 +0900 Subject: [PATCH 1/5] test(rules-core): add red tests for restored .sisyphus/rules discovery + deprecation warning (BUG-G) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- packages/rules-core/src/index.test.ts | 60 +++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/rules-core/src/index.test.ts b/packages/rules-core/src/index.test.ts index c7b9f770c..4448893d5 100644 --- a/packages/rules-core/src/index.test.ts +++ b/packages/rules-core/src/index.test.ts @@ -1,7 +1,7 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, describe, expect, it } from "bun:test"; +import { afterEach, describe, expect, it, mock, spyOn } from "bun:test"; import { clearProjectRootCache, @@ -13,9 +13,12 @@ import { parseRuleFrontmatter, shouldApplyRule, } from "./index"; +import * as logger from "../../../src/shared/logger"; 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(() => { + mock.restore(); if (testRoot) { rmSync(testRoot, { recursive: true, force: true }); testRoot = null; @@ -58,8 +62,52 @@ 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 logSpy = spyOn(logger, "log").mockImplementation(() => {}); + + // when + findRuleFiles(root, root, join(root, "src", "index.ts")); + findRuleFiles(root, root, join(root, "src", "index.ts")); + const warnings = logSpy.mock.calls.filter( + ([message, data]) => message === SISYPHUS_DEPRECATION_MESSAGE && isSisyphusDeprecationData(data, legacyRulePath), + ); + + // then + expect(warnings).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 +184,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 +202,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; +} From 1bab6ec4126c75b817985ea414956191cfba637b Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 13:05:09 +0900 Subject: [PATCH 2/5] fix(rules-core): restore .sisyphus/rules discovery with deprecation warning (planned removal v4.3.0) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- packages/rules-core/src/constants.ts | 5 ++++- packages/rules-core/src/finder.ts | 18 ++++++++++++++++++ packages/rules-core/src/types.ts | 4 +++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/rules-core/src/constants.ts b/packages/rules-core/src/constants.ts index e254b1768..cbe6659e7 100644 --- a/packages/rules-core/src/constants.ts +++ b/packages/rules-core/src/constants.ts @@ -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 = 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], ]); diff --git a/packages/rules-core/src/finder.ts b/packages/rules-core/src/finder.ts index 8c7d58191..1dcd46a92 100644 --- a/packages/rules-core/src/finder.ts +++ b/packages/rules-core/src/finder.ts @@ -5,6 +5,11 @@ 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 = new Set([".sisyphus/rules", "~/.sisyphus/rules"]); +const warnedSisyphusRuleDirectories = new Set(); export function findRuleFiles( projectRoot: string | null, @@ -62,6 +67,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 +121,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 +143,17 @@ 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); + log(SISYPHUS_DEPRECATION_MESSAGE, { + event: "rules-sisyphus-deprecated", + path, + }); +} + function validFileRealPath(filePath: string): string | null { if (!existsSync(filePath)) return null; try { diff --git a/packages/rules-core/src/types.ts b/packages/rules-core/src/types.ts index 3be7f2baa..07e67972f 100644 --- a/packages/rules-core/src/types.ts +++ b/packages/rules-core/src/types.ts @@ -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; From d92894a34b4207298264bcbcec9f66cf732b51c8 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 13:06:07 +0900 Subject: [PATCH 3/5] docs(changelog): document .sisyphus/rules restoration and planned removal in v4.3.0 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cc0a82b9..399d62ca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 53ddc470e31805c0e0ac30cfa3dce3546e2f445b Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 13:14:17 +0900 Subject: [PATCH 4/5] test(rules-injector): align duplicate-cache mock types Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/hooks/rules-injector/injector.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/rules-injector/injector.test.ts b/src/hooks/rules-injector/injector.test.ts index b7dd8acc2..b34c94df3 100644 --- a/src/hooks/rules-injector/injector.test.ts +++ b/src/hooks/rules-injector/injector.test.ts @@ -87,10 +87,10 @@ async function createProcessor(projectRoot: string): Promise<{ trackedShouldApplyRuleCount += 1; return { applies: true, reason: "matched" }; }, - isDuplicateByRealPath: (realPath: string, cache: Set) => + isDuplicateByRealPath: (realPath: string, cache: ReadonlySet) => cache.has(realPath), createContentHash: (content: string) => `hash:${content}`, - isDuplicateByContentHash: (hash: string, cache: Set) => + isDuplicateByContentHash: (hash: string, cache: ReadonlySet) => cache.has(hash), }); } From 12aaff28b58b3a18a21ff2c47cc23982f53ebb92 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 20 May 2026 13:19:33 +0900 Subject: [PATCH 5/5] test(rules-core): isolate sisyphus deprecation warning assertion Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- packages/rules-core/src/finder.ts | 12 +++++++++++- packages/rules-core/src/index.test.ts | 17 ++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/rules-core/src/finder.ts b/packages/rules-core/src/finder.ts index 1dcd46a92..71bd12205 100644 --- a/packages/rules-core/src/finder.ts +++ b/packages/rules-core/src/finder.ts @@ -10,6 +10,7 @@ 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 = new Set([".sisyphus/rules", "~/.sisyphus/rules"]); const warnedSisyphusRuleDirectories = new Set(); +let logSisyphusRuleDeprecation: typeof log = log; export function findRuleFiles( projectRoot: string | null, @@ -148,12 +149,21 @@ function warnSisyphusRuleDeprecation(source: RuleSource, path: string): void { const warningKey = dirname(path); if (warnedSisyphusRuleDirectories.has(warningKey)) return; warnedSisyphusRuleDirectories.add(warningKey); - log(SISYPHUS_DEPRECATION_MESSAGE, { + 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 { diff --git a/packages/rules-core/src/index.test.ts b/packages/rules-core/src/index.test.ts index 4448893d5..bec5438e2 100644 --- a/packages/rules-core/src/index.test.ts +++ b/packages/rules-core/src/index.test.ts @@ -1,7 +1,7 @@ import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { afterEach, describe, expect, it, mock, spyOn } from "bun:test"; +import { afterEach, describe, expect, it } from "bun:test"; import { clearProjectRootCache, @@ -13,7 +13,7 @@ import { parseRuleFrontmatter, shouldApplyRule, } from "./index"; -import * as logger from "../../../src/shared/logger"; +import { _resetSisyphusRuleDeprecationWarningStateForTesting, _setSisyphusRuleDeprecationLoggerForTesting } from "./finder"; let testRoot: string | null = null; @@ -26,7 +26,7 @@ function createTestRoot(name: string): string { } afterEach(() => { - mock.restore(); + _resetSisyphusRuleDeprecationWarningStateForTesting(); if (testRoot) { rmSync(testRoot, { recursive: true, force: true }); testRoot = null; @@ -97,17 +97,20 @@ describe("rules-core", () => { mkdirSync(join(root, ".sisyphus", "rules"), { recursive: true }); mkdirSync(join(root, "src"), { recursive: true }); writeFileSync(legacyRulePath, "legacy"); - const logSpy = spyOn(logger, "log").mockImplementation(() => {}); + 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 warnings = logSpy.mock.calls.filter( - ([message, data]) => message === SISYPHUS_DEPRECATION_MESSAGE && isSisyphusDeprecationData(data, legacyRulePath), + const deprecationWarnings = warnings.filter( + ({ message, data }) => message === SISYPHUS_DEPRECATION_MESSAGE && isSisyphusDeprecationData(data, legacyRulePath), ); // then - expect(warnings).toHaveLength(1); + 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", () => {