oh-my-opencode 4.7.3 → 4.7.5
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.
- package/dist/cli/index.js +80 -77
- package/package.json +13 -12
- package/packages/lsp-tools-mcp/package.json +52 -0
- package/packages/omo-codex/plugin/README.md +23 -0
- package/packages/omo-codex/plugin/components/rules/README.md +2 -2
- package/packages/omo-codex/plugin/components/rules/skills/rules/SKILL.md +2 -4
- package/packages/omo-codex/plugin/components/rules/src/config.ts +1 -5
- package/packages/omo-codex/plugin/components/rules/src/rules/constants.ts +1 -7
- package/packages/omo-codex/plugin/components/rules/src/rules/finder-sources.ts +0 -5
- package/packages/omo-codex/plugin/components/rules/src/rules/sources.ts +2 -6
- package/packages/omo-codex/plugin/components/rules/src/rules/types.ts +2 -6
- package/packages/omo-codex/plugin/components/rules/test/agent-doc-sources.test.ts +119 -0
- package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-budget.test.ts +4 -2
- package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-dedup.test.ts +4 -2
- package/packages/omo-codex/plugin/components/rules/test/codex-hook.test.ts +41 -2
- package/packages/omo-codex/plugin/components/rules/test/config.test.ts +22 -0
- package/packages/omo-codex/plugin/components/rules/test/engine.test.ts +1 -1
- package/packages/omo-codex/plugin/components/rules/test/finder.test.ts +2 -7
- package/packages/omo-codex/plugin/components/rules/test/formatter.test.ts +14 -14
- package/packages/omo-codex/plugin/components/rules/test/post-compact-test-fixture.ts +4 -2
- package/packages/omo-codex/plugin/components/rules/test/sources.test.ts +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +67 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +51 -0
- package/packages/omo-codex/plugin/scripts/auto-update.mjs +24 -2
- package/packages/omo-codex/plugin/skills/rules/SKILL.md +2 -4
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +16 -0
- package/packages/omo-codex/scripts/install/config.mjs +2 -0
- package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +11 -6
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@code-yeongyu/lsp-tools-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Standalone Language Server Protocol tools exposed as a stdio MCP server.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"packageManager": "npm@11.12.1",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/code-yeongyu/lsp-tools-mcp",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/code-yeongyu/lsp-tools-mcp.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/code-yeongyu/lsp-tools-mcp/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"lsp",
|
|
19
|
+
"language-server-protocol",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"typescript",
|
|
22
|
+
"nodejs"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"omo-lsp": "./dist/cli.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"NOTICE",
|
|
31
|
+
"README.md",
|
|
32
|
+
"CHANGELOG.md"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"test": "vitest --run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"lint": "biome check .",
|
|
40
|
+
"lint:fix": "biome check --write .",
|
|
41
|
+
"check": "tsc --noEmit && biome check . && npm run build"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@biomejs/biome": "2.4.15",
|
|
45
|
+
"@types/node": "^25.7.0",
|
|
46
|
+
"typescript": "^6.0.3",
|
|
47
|
+
"vitest": "^4.1.5"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -9,5 +9,28 @@ Internally each component remains isolated under `components/`:
|
|
|
9
9
|
- `components/lsp`
|
|
10
10
|
- `components/ultrawork`
|
|
11
11
|
- `components/ulw-loop`
|
|
12
|
+
- `components/telemetry`
|
|
12
13
|
|
|
13
14
|
The root plugin manifest exports one Codex plugin named `omo`, with aggregate hooks, skills, and the LSP MCP server.
|
|
15
|
+
|
|
16
|
+
## Telemetry
|
|
17
|
+
|
|
18
|
+
The bundled telemetry component emits the anonymous `omo_codex_daily_active` event at most once per UTC day per machine when the Codex `SessionStart` hook runs. It uses `sha256("omo-codex:" + hostname)` as the distinct ID, disables PostHog person profiles, and stores daily deduplication state in `$XDG_DATA_HOME/omo-codex/posthog-activity.json` or `~/.local/share/omo-codex/posthog-activity.json`.
|
|
19
|
+
|
|
20
|
+
Captured properties are limited to product/runtime metadata, operating-system metadata, coarse machine shape, locale/timezone, shell/terminal hints, `source`, `reason`, and `day_utc`. It does not send prompt contents, chat transcripts, source files, repository contents, file paths, access tokens, API keys, raw hostnames, Git remotes, usernames, email addresses, or runtime error diagnostics.
|
|
21
|
+
|
|
22
|
+
Opt out before launching Codex:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
export OMO_CODEX_DISABLE_POSTHOG=1
|
|
26
|
+
export OMO_CODEX_SEND_ANONYMOUS_TELEMETRY=0
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Global opt-out flags also disable this telemetry:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export OMO_DISABLE_POSTHOG=1
|
|
33
|
+
export OMO_SEND_ANONYMOUS_TELEMETRY=0
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Detailed implementation notes live in `components/telemetry/README.md`; the root product disclosure lives in `docs/reference/codex-telemetry.md` in the source repository.
|
|
@@ -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`
|
|
@@ -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
|
|
@@ -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
|
-
- `.
|
|
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=
|
|
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
|
|
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;
|
|
@@ -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[] = [
|
|
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
|
|
|
@@ -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
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { SOURCE_PRIORITY } from "./constants.js";
|
|
2
|
-
import type { PiRulesConfig
|
|
2
|
+
import type { PiRulesConfig } from "./types.js";
|
|
3
3
|
|
|
4
|
-
export const DEFAULT_AUTO_DISABLED_SOURCES: readonly
|
|
5
|
-
"AGENTS.md",
|
|
6
|
-
"~/.claude/rules",
|
|
7
|
-
"~/.claude/CLAUDE.md",
|
|
8
|
-
];
|
|
4
|
+
export const DEFAULT_AUTO_DISABLED_SOURCES: readonly string[] = ["AGENTS.md", "~/.claude/rules", "~/.claude/CLAUDE.md"];
|
|
9
5
|
|
|
10
6
|
export function disabledSourcesFromConfig(config: PiRulesConfig): ReadonlySet<string> | undefined {
|
|
11
7
|
if (config.enabledSources === "auto") {
|
|
@@ -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
|
|
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
|
+
});
|
package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-budget.test.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
const tempDirectories: string[] = [];
|
|
15
15
|
const PROJECT_RULES_ENV = {
|
|
16
|
-
CODEX_RULES_ENABLED_SOURCES: "
|
|
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"),
|
|
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"),
|
package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-dedup.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
|
|
16
16
|
const tempDirectories: string[] = [];
|
|
17
17
|
const PROJECT_ONLY_ENV = {
|
|
18
|
-
CODEX_RULES_ENABLED_SOURCES: "
|
|
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"), "
|
|
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"), "
|
|
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"),
|
|
@@ -223,7 +233,36 @@ describe("codex rules hooks", () => {
|
|
|
223
233
|
// then
|
|
224
234
|
const parsed = parseHookOutput(output);
|
|
225
235
|
expect(parsed.hookSpecificOutput?.additionalContext).toContain("## Project Instructions");
|
|
226
|
-
expect(parsed.hookSpecificOutput?.additionalContext).not.toContain("
|
|
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("");
|
|
227
266
|
});
|
|
228
267
|
|
|
229
268
|
it("#given static context already injected #when UserPromptSubmit runs #then it emits no duplicate context", async () => {
|
|
@@ -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
|
+
});
|
|
@@ -216,7 +216,7 @@ describe("rule engine default source selection", () => {
|
|
|
216
216
|
expect(capturedDisabledSources?.has("CLAUDE.md")).toBe(false);
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
-
it("#given
|
|
219
|
+
it("#given removed agent-doc sources and a real source are requested #when loading static rules #then only real sources are enabled", () => {
|
|
220
220
|
// given
|
|
221
221
|
let capturedDisabledSources: ReadonlySet<string> | undefined;
|
|
222
222
|
const deps = {
|
|
@@ -24,6 +24,7 @@ function makeProject(): { projectRoot: string; homeRoot: string; targetPath: str
|
|
|
24
24
|
mkdirSync(join(homeRoot, ".config", "opencode"), { recursive: true });
|
|
25
25
|
writeFileSync(join(projectRoot, "package.json"), JSON.stringify({ name: "fixture" }));
|
|
26
26
|
writeFileSync(join(projectRoot, "AGENTS.md"), "Project rule\n");
|
|
27
|
+
writeFileSync(join(projectRoot, "CLAUDE.md"), "Claude rule\n");
|
|
27
28
|
writeFileSync(join(projectRoot, "src", ".omo", "rules", "local.md"), "Local rule\n");
|
|
28
29
|
writeFileSync(join(projectRoot, ".omo", "rules", "root.md"), "Root rule\n");
|
|
29
30
|
writeFileSync(join(homeRoot, ".opencode", "rules", "global.md"), "Global rule\n");
|
|
@@ -54,9 +55,7 @@ describe("findRuleCandidates", () => {
|
|
|
54
55
|
expect(candidates.map(candidateSummary)).toEqual([
|
|
55
56
|
".omo/rules:0:src/.omo/rules/local.md",
|
|
56
57
|
".omo/rules:1:.omo/rules/root.md",
|
|
57
|
-
"AGENTS.md:1:AGENTS.md",
|
|
58
58
|
"~/.opencode/rules:9999:.opencode/rules/global.md",
|
|
59
|
-
"~/.config/opencode/AGENTS.md:9999:.config/opencode/AGENTS.md",
|
|
60
59
|
]);
|
|
61
60
|
});
|
|
62
61
|
|
|
@@ -73,10 +72,7 @@ describe("findRuleCandidates", () => {
|
|
|
73
72
|
});
|
|
74
73
|
|
|
75
74
|
// then
|
|
76
|
-
expect(candidates.map(candidateSummary)).toEqual([
|
|
77
|
-
"AGENTS.md:1:AGENTS.md",
|
|
78
|
-
"~/.config/opencode/AGENTS.md:9999:.config/opencode/AGENTS.md",
|
|
79
|
-
]);
|
|
75
|
+
expect(candidates.map(candidateSummary)).toEqual([]);
|
|
80
76
|
});
|
|
81
77
|
|
|
82
78
|
it("#given skip user home #when finding candidates #then only project rules are returned", () => {
|
|
@@ -96,7 +92,6 @@ describe("findRuleCandidates", () => {
|
|
|
96
92
|
expect(candidates.map(candidateSummary)).toEqual([
|
|
97
93
|
".omo/rules:0:src/.omo/rules/local.md",
|
|
98
94
|
".omo/rules:1:.omo/rules/root.md",
|
|
99
|
-
"AGENTS.md:1:AGENTS.md",
|
|
100
95
|
]);
|
|
101
96
|
});
|
|
102
97
|
});
|
|
@@ -12,8 +12,8 @@ describe("rules formatter hook context", () => {
|
|
|
12
12
|
it("#given multiline dynamic rules #when formatting PostToolUse context #then labels and bodies render on separate lines", () => {
|
|
13
13
|
// given
|
|
14
14
|
const rule = loadedRule({
|
|
15
|
-
path: "/repo/packages/
|
|
16
|
-
relativePath: "packages/
|
|
15
|
+
path: "/repo/packages/CONTEXT.md",
|
|
16
|
+
relativePath: "packages/CONTEXT.md",
|
|
17
17
|
body: ["# packages", "", "## OVERVIEW", "23 sibling packages.", "", "## CONVENTIONS", "Use npm."].join("\n"),
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -29,7 +29,7 @@ describe("rules formatter hook context", () => {
|
|
|
29
29
|
[
|
|
30
30
|
"Additional project instructions matched for packages/omo-codex/plugin/components/ulw-loop/src/paths.ts:",
|
|
31
31
|
"",
|
|
32
|
-
"Instructions from: /repo/packages/
|
|
32
|
+
"Instructions from: /repo/packages/CONTEXT.md",
|
|
33
33
|
"",
|
|
34
34
|
"# packages",
|
|
35
35
|
"",
|
|
@@ -45,8 +45,8 @@ describe("rules formatter hook context", () => {
|
|
|
45
45
|
it("#given static rules #when formatting SessionStart context #then it avoids leading blank lines", () => {
|
|
46
46
|
// given
|
|
47
47
|
const rule = loadedRule({
|
|
48
|
-
path: "/repo/
|
|
49
|
-
relativePath: "
|
|
48
|
+
path: "/repo/CONTEXT.md",
|
|
49
|
+
relativePath: "CONTEXT.md",
|
|
50
50
|
body: "Keep generated hook context readable.",
|
|
51
51
|
});
|
|
52
52
|
|
|
@@ -58,7 +58,7 @@ describe("rules formatter hook context", () => {
|
|
|
58
58
|
[
|
|
59
59
|
"## Project Instructions",
|
|
60
60
|
"",
|
|
61
|
-
"Instructions from: /repo/
|
|
61
|
+
"Instructions from: /repo/CONTEXT.md",
|
|
62
62
|
"",
|
|
63
63
|
"Keep generated hook context readable.",
|
|
64
64
|
].join("\n"),
|
|
@@ -82,13 +82,13 @@ describe("rules formatter hook context", () => {
|
|
|
82
82
|
it("#given duplicate static rules with different line endings #when formatting context #then it renders one copy", () => {
|
|
83
83
|
// given
|
|
84
84
|
const lfRule = loadedRule({
|
|
85
|
-
path: "/repo/
|
|
86
|
-
relativePath: "
|
|
85
|
+
path: "/repo/CONTEXT.md",
|
|
86
|
+
relativePath: "CONTEXT.md",
|
|
87
87
|
body: "Shared rule\nKeep one copy.",
|
|
88
88
|
});
|
|
89
89
|
const crlfRule = loadedRule({
|
|
90
|
-
path: "/repo/packages/
|
|
91
|
-
relativePath: "packages/
|
|
90
|
+
path: "/repo/packages/CONTEXT.md",
|
|
91
|
+
relativePath: "packages/CONTEXT.md",
|
|
92
92
|
body: "Shared rule\r\nKeep one copy.",
|
|
93
93
|
});
|
|
94
94
|
|
|
@@ -97,7 +97,7 @@ describe("rules formatter hook context", () => {
|
|
|
97
97
|
|
|
98
98
|
// then
|
|
99
99
|
expect(occurrenceCount(block, "Shared rule\nKeep one copy.")).toBe(1);
|
|
100
|
-
expect(block).not.toContain("/repo/packages/
|
|
100
|
+
expect(block).not.toContain("/repo/packages/CONTEXT.md");
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
it("#given multiple oversized rules #when formatting under a tight result budget #then every rule receives a fair truncated share with a read-full guide", () => {
|
|
@@ -145,9 +145,9 @@ function loadedRule(input: {
|
|
|
145
145
|
readonly source?: RuleSource;
|
|
146
146
|
readonly matchReason?: MatchReason;
|
|
147
147
|
}): LoadedRule {
|
|
148
|
-
const path = input.path ?? "/repo/
|
|
149
|
-
const relativePath = input.relativePath ?? "
|
|
150
|
-
const source = input.source ?? "
|
|
148
|
+
const path = input.path ?? "/repo/CONTEXT.md";
|
|
149
|
+
const relativePath = input.relativePath ?? "CONTEXT.md";
|
|
150
|
+
const source = input.source ?? "CONTEXT.md";
|
|
151
151
|
return {
|
|
152
152
|
path,
|
|
153
153
|
realPath: path,
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import type { CodexPostCompactInput, CodexSessionStartInput, CodexUserPromptSubmitInput } from "../src/codex-hook.js";
|
|
6
6
|
|
|
7
7
|
export const PROJECT_RULES_ENV = {
|
|
8
|
-
CODEX_RULES_ENABLED_SOURCES: "
|
|
8
|
+
CODEX_RULES_ENABLED_SOURCES: "CONTEXT.md,.omo/rules",
|
|
9
9
|
CODEX_RULES_MAX_RESULT_CHARS: "50000",
|
|
10
10
|
CODEX_RULES_MAX_RULE_CHARS: "30000",
|
|
11
11
|
};
|
|
@@ -30,7 +30,9 @@ export function makeOversizedProject(prefix = "budget"): { root: string; pluginD
|
|
|
30
30
|
const pluginData = mkdtempSync(path.join(tmpdir(), `codex-rules-post-compact-${prefix}-data-`));
|
|
31
31
|
tempDirectories.push(root, pluginData);
|
|
32
32
|
writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "fixture" }));
|
|
33
|
-
writeFileSync(path.join(root, "AGENTS.md"),
|
|
33
|
+
writeFileSync(path.join(root, "AGENTS.md"), "Project AGENTS.md should stay Codex-native.");
|
|
34
|
+
writeFileSync(path.join(root, "CLAUDE.md"), "Project CLAUDE.md should stay outside rules hook context.");
|
|
35
|
+
writeFileSync(path.join(root, "CONTEXT.md"), `Project rule\n${"A".repeat(30_000)}`);
|
|
34
36
|
mkdirSync(path.join(root, ".omo", "rules"), { recursive: true });
|
|
35
37
|
writeFileSync(
|
|
36
38
|
path.join(root, ".omo", "rules", "typescript.md"),
|