oh-my-openagent 4.7.2 → 4.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/cli/index.js +14 -12
  2. package/package.json +12 -12
  3. package/packages/omo-codex/plugin/components/rules/README.md +4 -4
  4. package/packages/omo-codex/plugin/components/rules/skills/rules/SKILL.md +2 -4
  5. package/packages/omo-codex/plugin/components/rules/src/config.ts +1 -5
  6. package/packages/omo-codex/plugin/components/rules/src/dynamic-target-fingerprints.ts +2 -11
  7. package/packages/omo-codex/plugin/components/rules/src/rules/constants.ts +1 -7
  8. package/packages/omo-codex/plugin/components/rules/src/rules/engine.ts +3 -12
  9. package/packages/omo-codex/plugin/components/rules/src/rules/finder-sources.ts +0 -5
  10. package/packages/omo-codex/plugin/components/rules/src/rules/sources.ts +13 -0
  11. package/packages/omo-codex/plugin/components/rules/src/rules/types.ts +2 -6
  12. package/packages/omo-codex/plugin/components/rules/test/agent-doc-sources.test.ts +119 -0
  13. package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-budget.test.ts +4 -2
  14. package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-dedup.test.ts +4 -2
  15. package/packages/omo-codex/plugin/components/rules/test/codex-hook.test.ts +72 -1
  16. package/packages/omo-codex/plugin/components/rules/test/config.test.ts +22 -0
  17. package/packages/omo-codex/plugin/components/rules/test/dynamic-target-fingerprints.test.ts +71 -0
  18. package/packages/omo-codex/plugin/components/rules/test/engine.test.ts +52 -0
  19. package/packages/omo-codex/plugin/components/rules/test/finder.test.ts +2 -7
  20. package/packages/omo-codex/plugin/components/rules/test/formatter.test.ts +14 -14
  21. package/packages/omo-codex/plugin/components/rules/test/post-compact-test-fixture.ts +4 -2
  22. package/packages/omo-codex/plugin/components/rules/test/sources.test.ts +46 -0
  23. package/packages/omo-codex/plugin/components/ultrawork/README.md +2 -0
  24. package/packages/omo-codex/plugin/components/ultrawork/src/codex-hook.ts +62 -0
  25. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +51 -0
  26. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +1 -1
  27. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
  28. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +67 -3
  29. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +51 -0
  30. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +15 -5
  31. package/packages/omo-codex/plugin/skills/rules/SKILL.md +2 -4
  32. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -1
  33. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +87 -1
  34. package/packages/omo-codex/scripts/install/config.mjs +2 -0
  35. package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +11 -6
package/dist/cli/index.js CHANGED
@@ -61669,7 +61669,7 @@ var {
61669
61669
  // package.json
61670
61670
  var package_default = {
61671
61671
  name: "oh-my-openagent",
61672
- version: "4.7.2",
61672
+ version: "4.7.4",
61673
61673
  description: "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
61674
61674
  main: "./dist/index.js",
61675
61675
  types: "dist/index.d.ts",
@@ -61804,17 +61804,17 @@ var package_default = {
61804
61804
  zod: "^4.4.3"
61805
61805
  },
61806
61806
  optionalDependencies: {
61807
- "oh-my-openagent-darwin-arm64": "4.7.2",
61808
- "oh-my-openagent-darwin-x64": "4.7.2",
61809
- "oh-my-openagent-darwin-x64-baseline": "4.7.2",
61810
- "oh-my-openagent-linux-arm64": "4.7.2",
61811
- "oh-my-openagent-linux-arm64-musl": "4.7.2",
61812
- "oh-my-openagent-linux-x64": "4.7.2",
61813
- "oh-my-openagent-linux-x64-baseline": "4.7.2",
61814
- "oh-my-openagent-linux-x64-musl": "4.7.2",
61815
- "oh-my-openagent-linux-x64-musl-baseline": "4.7.2",
61816
- "oh-my-openagent-windows-x64": "4.7.2",
61817
- "oh-my-openagent-windows-x64-baseline": "4.7.2"
61807
+ "oh-my-openagent-darwin-arm64": "4.7.4",
61808
+ "oh-my-openagent-darwin-x64": "4.7.4",
61809
+ "oh-my-openagent-darwin-x64-baseline": "4.7.4",
61810
+ "oh-my-openagent-linux-arm64": "4.7.4",
61811
+ "oh-my-openagent-linux-arm64-musl": "4.7.4",
61812
+ "oh-my-openagent-linux-x64": "4.7.4",
61813
+ "oh-my-openagent-linux-x64-baseline": "4.7.4",
61814
+ "oh-my-openagent-linux-x64-musl": "4.7.4",
61815
+ "oh-my-openagent-linux-x64-musl-baseline": "4.7.4",
61816
+ "oh-my-openagent-windows-x64": "4.7.4",
61817
+ "oh-my-openagent-windows-x64-baseline": "4.7.4"
61818
61818
  },
61819
61819
  overrides: {
61820
61820
  hono: "^4.12.18",
@@ -62940,6 +62940,8 @@ async function updateCodexConfig(input) {
62940
62940
  config = removeStaleManagedAgentBlocks(config, new Set((input.agentConfigs ?? []).map((agentConfig) => agentConfig.name)));
62941
62941
  config = ensureFeatureEnabled2(config, "plugins");
62942
62942
  config = ensureFeatureEnabled2(config, "plugin_hooks");
62943
+ config = ensureFeatureEnabled2(config, "multi_agent");
62944
+ config = ensureFeatureEnabled2(config, "child_agents_md");
62943
62945
  config = ensureCodexReasoningConfig(config, await readCodexModelCatalog(input.repoRoot));
62944
62946
  config = ensureCodexMultiAgentV2Config(config);
62945
62947
  if (input.autonomousPermissions === true)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-openagent",
3
- "version": "4.7.2",
3
+ "version": "4.7.4",
4
4
  "description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
5
5
  "main": "./dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -135,17 +135,17 @@
135
135
  "zod": "^4.4.3"
136
136
  },
137
137
  "optionalDependencies": {
138
- "oh-my-openagent-darwin-arm64": "4.7.2",
139
- "oh-my-openagent-darwin-x64": "4.7.2",
140
- "oh-my-openagent-darwin-x64-baseline": "4.7.2",
141
- "oh-my-openagent-linux-arm64": "4.7.2",
142
- "oh-my-openagent-linux-arm64-musl": "4.7.2",
143
- "oh-my-openagent-linux-x64": "4.7.2",
144
- "oh-my-openagent-linux-x64-baseline": "4.7.2",
145
- "oh-my-openagent-linux-x64-musl": "4.7.2",
146
- "oh-my-openagent-linux-x64-musl-baseline": "4.7.2",
147
- "oh-my-openagent-windows-x64": "4.7.2",
148
- "oh-my-openagent-windows-x64-baseline": "4.7.2"
138
+ "oh-my-openagent-darwin-arm64": "4.7.4",
139
+ "oh-my-openagent-darwin-x64": "4.7.4",
140
+ "oh-my-openagent-darwin-x64-baseline": "4.7.4",
141
+ "oh-my-openagent-linux-arm64": "4.7.4",
142
+ "oh-my-openagent-linux-arm64-musl": "4.7.4",
143
+ "oh-my-openagent-linux-x64": "4.7.4",
144
+ "oh-my-openagent-linux-x64-baseline": "4.7.4",
145
+ "oh-my-openagent-linux-x64-musl": "4.7.4",
146
+ "oh-my-openagent-linux-x64-musl-baseline": "4.7.4",
147
+ "oh-my-openagent-windows-x64": "4.7.4",
148
+ "oh-my-openagent-windows-x64-baseline": "4.7.4"
149
149
  },
150
150
  "overrides": {
151
151
  "hono": "^4.12.18",
@@ -17,8 +17,6 @@ The runtime has no npm production dependencies, so a clean Codex marketplace cop
17
17
 
18
18
  Project-level sources:
19
19
 
20
- - `AGENTS.md`
21
- - `CLAUDE.md`
22
20
  - `CONTEXT.md`
23
21
  - `.omo/rules/**/*.md`
24
22
  - `.claude/rules/**/*.md`
@@ -26,7 +24,7 @@ Project-level sources:
26
24
  - `.github/instructions/**/*.md`
27
25
  - `.github/copilot-instructions.md`
28
26
 
29
- User-home sources are also supported by the ported engine when available.
27
+ User-home sources are also supported by the ported engine when available. `AGENTS.md` is not part of `auto` source selection because Codex already loads it as native project instructions, so re-injecting it through hooks duplicates context; opt into it explicitly with `CODEX_RULES_ENABLED_SOURCES` if you need hook-level migration behavior. Claude user-home sources (`~/.claude/rules`, `~/.claude/CLAUDE.md`) are also excluded from `auto` because they usually contain Claude Code runtime instructions rather than Codex rules; opt into them explicitly when you want that migration behavior.
30
28
 
31
29
  Markdown rule files may use frontmatter such as:
32
30
 
@@ -58,6 +56,8 @@ It also enables:
58
56
  [features]
59
57
  plugins = true
60
58
  plugin_hooks = true
59
+ multi_agent = true
60
+ child_agents_md = true
61
61
 
62
62
  [plugins."omo@sisyphuslabs"]
63
63
  enabled = true
@@ -73,7 +73,7 @@ Use `CODEX_RULES_*` environment variables:
73
73
  | `CODEX_RULES_MODE` | `both`, `static`, `dynamic`, `off` | `both` |
74
74
  | `CODEX_RULES_MAX_RULE_CHARS` | positive integer | `12000` |
75
75
  | `CODEX_RULES_MAX_RESULT_CHARS` | positive integer | `40000` |
76
- | `CODEX_RULES_ENABLED_SOURCES` | comma-separated source names | `auto` |
76
+ | `CODEX_RULES_ENABLED_SOURCES` | comma-separated source names or `auto` | `auto` (excludes `AGENTS.md`, `~/.claude/rules`, `~/.claude/CLAUDE.md`) |
77
77
 
78
78
  For migration from `pi-rules`, equivalent `PI_RULES_*` variables are accepted as fallbacks.
79
79
 
@@ -14,10 +14,8 @@ Dynamic `PostToolUse` output is injected as additional context and is deduplicat
14
14
 
15
15
  Supported project sources:
16
16
 
17
- - `AGENTS.md`
18
- - `CLAUDE.md`
19
17
  - `CONTEXT.md`
20
- - `.sisyphus/rules/**/*.md`
18
+ - `.omo/rules/**/*.md`
21
19
  - `.claude/rules/**/*.md`
22
20
  - `.cursor/rules/**/*.md`
23
21
  - `.github/instructions/**/*.md`
@@ -29,6 +27,6 @@ Supported environment knobs:
29
27
  - `CODEX_RULES_MODE=both|static|dynamic|off`
30
28
  - `CODEX_RULES_MAX_RULE_CHARS=<number>`
31
29
  - `CODEX_RULES_MAX_RESULT_CHARS=<number>`
32
- - `CODEX_RULES_ENABLED_SOURCES=AGENTS.md,.sisyphus/rules`
30
+ - `CODEX_RULES_ENABLED_SOURCES=CONTEXT.md,.omo/rules`
33
31
 
34
32
  The legacy `PI_RULES_*` variables are accepted as fallbacks for users migrating from `pi-rules`.
@@ -77,7 +77,7 @@ function parseEnabledSources(value: string | undefined, disableBundledRules: boo
77
77
  sources.push(source);
78
78
  }
79
79
  const enabledSources = disableBundledRules ? sources.filter((source) => source !== "plugin-bundled") : sources;
80
- return enabledSources.length > 0 || sources.length > 0 ? enabledSources : "auto";
80
+ return enabledSources;
81
81
  }
82
82
 
83
83
  function sourcesWithoutBundledRules(): RuleSource[] {
@@ -91,15 +91,11 @@ function toRuleSource(value: string): RuleSource | null {
91
91
  case ".cursor/rules":
92
92
  case ".github/instructions":
93
93
  case ".github/copilot-instructions.md":
94
- case "AGENTS.md":
95
- case "CLAUDE.md":
96
94
  case "CONTEXT.md":
97
95
  case "plugin-bundled":
98
96
  case "~/.omo/rules":
99
97
  case "~/.opencode/rules":
100
98
  case "~/.claude/rules":
101
- case "~/.config/opencode/AGENTS.md":
102
- case "~/.claude/CLAUDE.md":
103
99
  return value;
104
100
  default:
105
101
  return null;
@@ -1,11 +1,11 @@
1
1
  import { statSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { isSameOrChildPath, toPosixPath, uniqueStrings } from "./path-utils.js";
4
- import { SOURCE_PRIORITY } from "./rules/constants.js";
5
4
  import { createRuleDiscoveryCache, findRuleCandidates } from "./rules/finder.js";
6
5
  import { hashContent } from "./rules/matcher.js";
7
6
  import { sortCandidates } from "./rules/ordering.js";
8
7
  import { findProjectRoot } from "./rules/project-root.js";
8
+ import { disabledSourcesFromConfig } from "./rules/sources.js";
9
9
  import type { PiRulesConfig, RuleCandidate } from "./rules/types.js";
10
10
 
11
11
  export interface DynamicTargetFingerprint {
@@ -19,7 +19,7 @@ export function fingerprintDynamicTargets(
19
19
  targetPaths: ReadonlyArray<string>,
20
20
  config: PiRulesConfig,
21
21
  ): DynamicTargetFingerprint[] {
22
- const disabledSources = disabledSourcesFor(config);
22
+ const disabledSources = disabledSourcesFromConfig(config);
23
23
  const discoveryCache = createRuleDiscoveryCache();
24
24
  const cwdProjectRoot = findProjectRoot(cwd);
25
25
  const fingerprints: DynamicTargetFingerprint[] = [];
@@ -84,15 +84,6 @@ function fileFingerprint(filePath: string): string {
84
84
  }
85
85
  }
86
86
 
87
- function disabledSourcesFor(config: PiRulesConfig): ReadonlySet<string> | undefined {
88
- if (config.enabledSources === "auto") {
89
- return undefined;
90
- }
91
-
92
- const enabledSources = new Set(config.enabledSources);
93
- return new Set([...SOURCE_PRIORITY.keys()].filter((source) => !enabledSources.has(source)));
94
- }
95
-
96
87
  function dynamicTargetCacheKey(targetPath: string): string {
97
88
  return toPosixPath(resolve(targetPath));
98
89
  }
@@ -30,8 +30,6 @@ export const PROJECT_RULE_SUBDIRS: ReadonlyArray<readonly [string, string]> = [
30
30
  */
31
31
  export const PROJECT_SINGLE_FILES: readonly string[] = [
32
32
  ".github/copilot-instructions.md",
33
- "AGENTS.md",
34
- "CLAUDE.md",
35
33
  "CONTEXT.md",
36
34
  ];
37
35
 
@@ -43,7 +41,7 @@ export const USER_HOME_RULE_SUBDIRS: readonly string[] = [".omo/rules", ".openco
43
41
  /**
44
42
  * User-home single-file rules. The first one to exist wins per "first-match" semantics.
45
43
  */
46
- export const USER_HOME_SINGLE_FILES: readonly string[] = [".config/opencode/AGENTS.md", ".claude/CLAUDE.md"];
44
+ export const USER_HOME_SINGLE_FILES: readonly string[] = [];
47
45
 
48
46
  /**
49
47
  * Bundled plugin rule directory relative to the rules component root.
@@ -64,14 +62,10 @@ export const SOURCE_PRIORITY: ReadonlyMap<RuleSource, number> = new Map([
64
62
  [".cursor/rules", 2],
65
63
  [".github/instructions", 3],
66
64
  [".github/copilot-instructions.md", 4],
67
- ["AGENTS.md", 5],
68
- ["CLAUDE.md", 6],
69
65
  ["CONTEXT.md", 7],
70
66
  ["~/.omo/rules", 100],
71
67
  ["~/.opencode/rules", 101],
72
68
  ["~/.claude/rules", 102],
73
- ["~/.config/opencode/AGENTS.md", 103],
74
- ["~/.claude/CLAUDE.md", 104],
75
69
  ["plugin-bundled", 200],
76
70
  ]);
77
71
 
@@ -15,13 +15,13 @@ import {
15
15
  DEFAULT_POST_COMPACT_MAX_RESULT_CHARS,
16
16
  DEFAULT_POST_COMPACT_MAX_RULE_CHARS,
17
17
  PROJECT_SINGLE_FILES,
18
- SOURCE_PRIORITY,
19
18
  } from "./constants.js";
20
19
  import { createRuleDiscoveryCache, type RuleDiscoveryCache } from "./finder.js";
21
20
  import { formatDynamicBlock, formatStaticBlock } from "./formatter.js";
22
21
  import { hashContent, matchRule } from "./matcher.js";
23
22
  import { sortCandidates } from "./ordering.js";
24
23
  import { parseRule } from "./parser.js";
24
+ import { disabledSourcesFromConfig } from "./sources.js";
25
25
  import type { LoadedRule, MatchReason, PiRulesConfig, RuleCandidate, RuleDiagnostic, SessionState } from "./types.js";
26
26
 
27
27
  interface LoadedRuleContent {
@@ -97,7 +97,7 @@ export function createEngine(config: PiRulesConfig, deps: EngineDeps): Engine {
97
97
  projectRoot,
98
98
  targetFile: null,
99
99
  };
100
- const disabledSources = disabledSourcesFor(config);
100
+ const disabledSources = disabledSourcesFromConfig(config);
101
101
  if (disabledSources !== undefined) {
102
102
  findOptions.disabledSources = disabledSources;
103
103
  }
@@ -121,7 +121,7 @@ export function createEngine(config: PiRulesConfig, deps: EngineDeps): Engine {
121
121
  const seenRules = new Set<string>();
122
122
  const loadedRuleContent = new Map<string, LoadedRuleContent | null>();
123
123
  const projectMembership = new Map<string, boolean>();
124
- const disabledSources = disabledSourcesFor(config);
124
+ const disabledSources = disabledSourcesFromConfig(config);
125
125
  const discoveryCache = createRuleDiscoveryCache();
126
126
  const candidateDiscoveryCache: CandidateDiscoveryCache = new Map();
127
127
  const cwdProjectRoot = deps.findProjectRoot(cwd);
@@ -442,15 +442,6 @@ function staticMatchReason(rule: LoadedRule): MatchReason | null {
442
442
  return null;
443
443
  }
444
444
 
445
- function disabledSourcesFor(config: PiRulesConfig): ReadonlySet<string> | undefined {
446
- if (config.enabledSources === "auto") {
447
- return undefined;
448
- }
449
-
450
- const enabledSources = new Set(config.enabledSources);
451
- return new Set([...SOURCE_PRIORITY.keys()].filter((source) => !enabledSources.has(source)));
452
- }
453
-
454
445
  function isDedupedRootSingleFile(candidate: RuleCandidate, rootSingleFileSelected: boolean): boolean {
455
446
  return rootSingleFileSelected && isRootSingleFile(candidate);
456
447
  }
@@ -17,8 +17,6 @@ export function toProjectRuleSource(parentDirectory: string, subDirectory: strin
17
17
  export function toProjectSingleFileSource(ruleFile: string): RuleSource {
18
18
  switch (ruleFile) {
19
19
  case ".github/copilot-instructions.md":
20
- case "AGENTS.md":
21
- case "CLAUDE.md":
22
20
  case "CONTEXT.md":
23
21
  return ruleFile;
24
22
  default:
@@ -41,9 +39,6 @@ export function toUserHomeRuleSource(ruleSubdir: string): RuleSource {
41
39
  export function toUserHomeSingleFileSource(ruleFile: string): RuleSource {
42
40
  const source = `~/${ruleFile}`;
43
41
  switch (source) {
44
- case "~/.config/opencode/AGENTS.md":
45
- case "~/.claude/CLAUDE.md":
46
- return source;
47
42
  default:
48
43
  throw new UnsupportedRuleSourceError(`Unsupported user-home single-file source: ${source}`);
49
44
  }
@@ -0,0 +1,13 @@
1
+ import { SOURCE_PRIORITY } from "./constants.js";
2
+ import type { PiRulesConfig } from "./types.js";
3
+
4
+ export const DEFAULT_AUTO_DISABLED_SOURCES: readonly string[] = ["AGENTS.md", "~/.claude/rules", "~/.claude/CLAUDE.md"];
5
+
6
+ export function disabledSourcesFromConfig(config: PiRulesConfig): ReadonlySet<string> | undefined {
7
+ if (config.enabledSources === "auto") {
8
+ return new Set(DEFAULT_AUTO_DISABLED_SOURCES);
9
+ }
10
+
11
+ const enabledSources = new Set(config.enabledSources);
12
+ return new Set([...SOURCE_PRIORITY.keys()].filter((source) => !enabledSources.has(source)));
13
+ }
@@ -51,7 +51,7 @@ export interface RuleCandidate {
51
51
  distance: number;
52
52
  isGlobal: boolean;
53
53
  /**
54
- * True when this candidate is a SINGLE-FILE rule like AGENTS.md or
54
+ * True when this candidate is a SINGLE-FILE rule like
55
55
  * `.github/copilot-instructions.md` (frontmatter optional, applies always).
56
56
  */
57
57
  isSingleFile: boolean;
@@ -81,15 +81,11 @@ export type RuleSource =
81
81
  | ".cursor/rules"
82
82
  | ".github/instructions"
83
83
  | ".github/copilot-instructions.md"
84
- | "AGENTS.md"
85
- | "CLAUDE.md"
86
84
  | "CONTEXT.md"
87
85
  | "plugin-bundled"
88
86
  | "~/.omo/rules"
89
87
  | "~/.opencode/rules"
90
- | "~/.claude/rules"
91
- | "~/.config/opencode/AGENTS.md"
92
- | "~/.claude/CLAUDE.md";
88
+ | "~/.claude/rules";
93
89
 
94
90
  /**
95
91
  * Why a candidate matched the target file. Surfaced in the injection block so
@@ -0,0 +1,119 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+
6
+ import {
7
+ type CodexPostToolUseInput,
8
+ type CodexSessionStartInput,
9
+ runPostToolUseHook,
10
+ runSessionStartHook,
11
+ } from "../src/codex-hook.js";
12
+
13
+ const REMOVED_AGENT_DOC_SOURCE_LISTS = ["AGENTS.md", "CLAUDE.md", "AGENTS.md,CLAUDE.md"] as const;
14
+
15
+ const tempDirectories: string[] = [];
16
+
17
+ type AgentDocProject = {
18
+ readonly pluginData: string;
19
+ readonly root: string;
20
+ readonly nestedSourceFile: string;
21
+ };
22
+
23
+ afterEach(() => {
24
+ for (const directory of tempDirectories.splice(0)) {
25
+ rmSync(directory, { recursive: true, force: true });
26
+ }
27
+ });
28
+
29
+ function makeAgentDocProject(): AgentDocProject {
30
+ const root = mkdtempSync(path.join(tmpdir(), "codex-rules-agent-docs-"));
31
+ const pluginData = mkdtempSync(path.join(tmpdir(), "codex-rules-agent-docs-data-"));
32
+ tempDirectories.push(root, pluginData);
33
+
34
+ writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "fixture" }));
35
+ writeFileSync(path.join(root, "AGENTS.md"), "Root AGENTS.md must remain Codex-native.");
36
+ writeFileSync(path.join(root, "CLAUDE.md"), "Root CLAUDE.md must stay outside rules hook context.");
37
+ writeFileSync(path.join(root, "CONTEXT.md"), "Context source must not leak through removed-only allowlists.");
38
+
39
+ mkdirSync(path.join(root, ".omo", "rules"), { recursive: true });
40
+ writeFileSync(
41
+ path.join(root, ".omo", "rules", "typescript.md"),
42
+ [
43
+ "---",
44
+ "description: TypeScript",
45
+ 'globs: ["**/*.ts"]',
46
+ "---",
47
+ "",
48
+ "Dynamic .omo/rules context must not leak through removed-only allowlists.",
49
+ ].join("\n"),
50
+ );
51
+
52
+ const nestedDirectory = path.join(root, "child", "src");
53
+ mkdirSync(nestedDirectory, { recursive: true });
54
+ writeFileSync(path.join(root, "child", "AGENTS.md"), "Child AGENTS.md must remain Codex-native.");
55
+ const nestedSourceFile = path.join(nestedDirectory, "app.ts");
56
+ writeFileSync(nestedSourceFile, "export const app = true;\n");
57
+
58
+ return { root, pluginData, nestedSourceFile };
59
+ }
60
+
61
+ function sessionStartInput(root: string): CodexSessionStartInput {
62
+ return {
63
+ session_id: "session-1",
64
+ transcript_path: null,
65
+ cwd: root,
66
+ hook_event_name: "SessionStart",
67
+ model: "gpt-5.5",
68
+ permission_mode: "default",
69
+ source: "startup",
70
+ };
71
+ }
72
+
73
+ function postToolUseInput(root: string, filePath: string): CodexPostToolUseInput {
74
+ return {
75
+ session_id: "session-1",
76
+ turn_id: "turn-1",
77
+ transcript_path: null,
78
+ cwd: root,
79
+ hook_event_name: "PostToolUse",
80
+ model: "gpt-5.5",
81
+ permission_mode: "default",
82
+ tool_name: "mcp__filesystem__read_file",
83
+ tool_input: { path: filePath },
84
+ tool_response: { text: "file contents" },
85
+ tool_use_id: "call-1",
86
+ };
87
+ }
88
+
89
+ describe("agent doc sources", () => {
90
+ for (const sourceList of REMOVED_AGENT_DOC_SOURCE_LISTS) {
91
+ it(`#given ${sourceList} removed-only source allowlist #when SessionStart runs #then it emits no OMO rules context`, async () => {
92
+ // given
93
+ const { root, pluginData } = makeAgentDocProject();
94
+
95
+ // when
96
+ const output = await runSessionStartHook(sessionStartInput(root), {
97
+ pluginDataRoot: pluginData,
98
+ env: { CODEX_RULES_ENABLED_SOURCES: sourceList },
99
+ });
100
+
101
+ // then
102
+ expect(output).toBe("");
103
+ });
104
+ }
105
+
106
+ it("#given nested AGENTS.md and removed-only source allowlist #when PostToolUse targets a child file #then it emits no dynamic OMO rules context", async () => {
107
+ // given
108
+ const { root, pluginData, nestedSourceFile } = makeAgentDocProject();
109
+
110
+ // when
111
+ const output = await runPostToolUseHook(postToolUseInput(root, nestedSourceFile), {
112
+ pluginDataRoot: pluginData,
113
+ env: { CODEX_RULES_ENABLED_SOURCES: "AGENTS.md,CLAUDE.md" },
114
+ });
115
+
116
+ // then
117
+ expect(output).toBe("");
118
+ });
119
+ });
@@ -13,7 +13,7 @@ import {
13
13
 
14
14
  const tempDirectories: string[] = [];
15
15
  const PROJECT_RULES_ENV = {
16
- CODEX_RULES_ENABLED_SOURCES: "AGENTS.md,.omo/rules",
16
+ CODEX_RULES_ENABLED_SOURCES: "CONTEXT.md,.omo/rules",
17
17
  CODEX_RULES_MAX_RESULT_CHARS: "50000",
18
18
  CODEX_RULES_MAX_RULE_CHARS: "30000",
19
19
  };
@@ -56,7 +56,9 @@ function makeOversizedProject(): { root: string; pluginData: string } {
56
56
  const pluginData = mkdtempSync(path.join(tmpdir(), "codex-rules-post-compact-budget-data-"));
57
57
  tempDirectories.push(root, pluginData);
58
58
  writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "fixture" }));
59
- writeFileSync(path.join(root, "AGENTS.md"), `Project rule\n${"A".repeat(30_000)}`);
59
+ writeFileSync(path.join(root, "AGENTS.md"), "Project AGENTS.md should stay Codex-native.");
60
+ writeFileSync(path.join(root, "CLAUDE.md"), "Project CLAUDE.md should stay outside rules hook context.");
61
+ writeFileSync(path.join(root, "CONTEXT.md"), `Project rule\n${"A".repeat(30_000)}`);
60
62
  mkdirSync(path.join(root, ".omo", "rules"), { recursive: true });
61
63
  writeFileSync(
62
64
  path.join(root, ".omo", "rules", "typescript.md"),
@@ -15,7 +15,7 @@ import {
15
15
 
16
16
  const tempDirectories: string[] = [];
17
17
  const PROJECT_ONLY_ENV = {
18
- CODEX_RULES_ENABLED_SOURCES: "AGENTS.md,.omo/rules",
18
+ CODEX_RULES_ENABLED_SOURCES: "CONTEXT.md,.omo/rules",
19
19
  };
20
20
 
21
21
  afterEach(() => {
@@ -145,7 +145,9 @@ function makeTempProject(): { root: string; pluginData: string } {
145
145
  const pluginData = mkdtempSync(path.join(tmpdir(), "codex-rules-compact-dedup-data-"));
146
146
  tempDirectories.push(root, pluginData);
147
147
  writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "fixture" }));
148
- writeFileSync(path.join(root, "AGENTS.md"), "Always wear safety goggles when refactoring.");
148
+ writeFileSync(path.join(root, "AGENTS.md"), "Project AGENTS.md should stay Codex-native.");
149
+ writeFileSync(path.join(root, "CLAUDE.md"), "Project CLAUDE.md should stay outside rules hook context.");
150
+ writeFileSync(path.join(root, "CONTEXT.md"), "Always wear safety goggles when refactoring.");
149
151
  mkdirSync(path.join(root, ".omo", "rules"), { recursive: true });
150
152
  writeFileSync(
151
153
  path.join(root, ".omo", "rules", "typescript.md"),
@@ -55,9 +55,17 @@ function runHookCli(input: string, subcommand = "post-tool-use", env: NodeJS.Pro
55
55
 
56
56
  const tempDirectories: string[] = [];
57
57
  const PROJECT_ONLY_ENV = {
58
+ CODEX_RULES_ENABLED_SOURCES: "CONTEXT.md,.omo/rules",
59
+ };
60
+
61
+ const AGENTS_AND_RULES_ENV = {
58
62
  CODEX_RULES_ENABLED_SOURCES: "AGENTS.md,.omo/rules",
59
63
  };
60
64
 
65
+ const CLAUDE_AND_RULES_ENV = {
66
+ CODEX_RULES_ENABLED_SOURCES: "CLAUDE.md,.omo/rules",
67
+ };
68
+
61
69
  const RULES_ONLY_ENV = {
62
70
  CODEX_RULES_ENABLED_SOURCES: ".omo/rules",
63
71
  };
@@ -73,7 +81,9 @@ function makeTempProject(): { root: string; pluginData: string } {
73
81
  const pluginData = mkdtempSync(path.join(tmpdir(), "codex-rules-data-"));
74
82
  tempDirectories.push(root, pluginData);
75
83
  writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "fixture" }));
76
- writeFileSync(path.join(root, "AGENTS.md"), "Always wear safety goggles when refactoring.");
84
+ writeFileSync(path.join(root, "AGENTS.md"), "Project AGENTS.md should stay Codex-native.");
85
+ writeFileSync(path.join(root, "CLAUDE.md"), "Project CLAUDE.md should stay outside rules hook context.");
86
+ writeFileSync(path.join(root, "CONTEXT.md"), "Always wear safety goggles when refactoring.");
77
87
  mkdirSync(path.join(root, ".omo", "rules"), { recursive: true });
78
88
  writeFileSync(
79
89
  path.join(root, ".omo", "rules", "typescript.md"),
@@ -211,6 +221,50 @@ describe("codex rules hooks", () => {
211
221
  expect(parsed.hookSpecificOutput?.additionalContext).toContain("Always wear safety goggles");
212
222
  });
213
223
 
224
+ it("#given default auto sources #when SessionStart runs #then native Codex AGENTS.md is not duplicated", async () => {
225
+ // given
226
+ const { root, pluginData } = makeTempProject();
227
+
228
+ // when
229
+ const output = await runSessionStartHook(sessionStartInput(root), {
230
+ pluginDataRoot: pluginData,
231
+ });
232
+
233
+ // then
234
+ const parsed = parseHookOutput(output);
235
+ expect(parsed.hookSpecificOutput?.additionalContext).toContain("## Project Instructions");
236
+ expect(parsed.hookSpecificOutput?.additionalContext).not.toContain("Project AGENTS.md should stay Codex-native.");
237
+ expect(parsed.hookSpecificOutput?.additionalContext).not.toContain("Project CLAUDE.md should stay outside rules hook context.");
238
+ });
239
+
240
+ it("#given project AGENTS.md #when SessionStart runs #then rules hook leaves AGENTS.md to Codex native handling", async () => {
241
+ // given
242
+ const { root, pluginData } = makeTempProject();
243
+
244
+ // when
245
+ const output = await runSessionStartHook(sessionStartInput(root), {
246
+ pluginDataRoot: pluginData,
247
+ env: AGENTS_AND_RULES_ENV,
248
+ });
249
+
250
+ // then
251
+ expect(output).toBe("");
252
+ });
253
+
254
+ it("#given project CLAUDE.md #when SessionStart runs #then rules hook leaves CLAUDE.md out of context", async () => {
255
+ // given
256
+ const { root, pluginData } = makeTempProject();
257
+
258
+ // when
259
+ const output = await runSessionStartHook(sessionStartInput(root), {
260
+ pluginDataRoot: pluginData,
261
+ env: CLAUDE_AND_RULES_ENV,
262
+ });
263
+
264
+ // then
265
+ expect(output).toBe("");
266
+ });
267
+
214
268
  it("#given static context already injected #when UserPromptSubmit runs #then it emits no duplicate context", async () => {
215
269
  // given
216
270
  const { root, pluginData } = makeTempProject();
@@ -351,6 +405,23 @@ describe("codex rules hooks", () => {
351
405
  expect(readSessionCache(pluginData).dynamicTargetFingerprints).toEqual(cachedState.dynamicTargetFingerprints);
352
406
  });
353
407
 
408
+ it("#given default auto sources #when excluded AGENTS.md changes #then PostToolUse fingerprint stays stable", async () => {
409
+ // given
410
+ const { root, pluginData } = makeTempProject();
411
+ const filePath = path.join(root, "src", "app.ts");
412
+ const input = postToolUseInput(root, filePath);
413
+ await runPostToolUseHook(input, { pluginDataRoot: pluginData });
414
+ const cachedState = readSessionCache(pluginData);
415
+ writeFileSync(path.join(root, "AGENTS.md"), "Native Codex instructions changed outside codex-rules auto.");
416
+
417
+ // when
418
+ const output = await runPostToolUseHook(input, { pluginDataRoot: pluginData });
419
+
420
+ // then
421
+ expect(output).toBe("");
422
+ expect(readSessionCache(pluginData).dynamicTargetFingerprints).toEqual(cachedState.dynamicTargetFingerprints);
423
+ });
424
+
354
425
  it("#given dynamic context remains in transcript but cache is missing #when PostToolUse repeats #then it emits no duplicate context", async () => {
355
426
  // given
356
427
  const { root, pluginData } = makeTempProject();
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { configFromEnvironment } from "../src/config.js";
4
+
5
+ const REMOVED_AGENT_DOC_SOURCE_LISTS = ["AGENTS.md", "CLAUDE.md", "AGENTS.md,CLAUDE.md"] as const;
6
+
7
+ describe("rules config", () => {
8
+ for (const sourceList of REMOVED_AGENT_DOC_SOURCE_LISTS) {
9
+ it(`#given removed agent-doc source ${sourceList} #when parsing enabled sources #then preserves the explicit empty allowlist`, () => {
10
+ // given
11
+ const env = {
12
+ CODEX_RULES_ENABLED_SOURCES: sourceList,
13
+ } satisfies NodeJS.ProcessEnv;
14
+
15
+ // when
16
+ const config = configFromEnvironment(env);
17
+
18
+ // then
19
+ expect(config.enabledSources).toEqual([]);
20
+ });
21
+ }
22
+ });