oh-my-openagent 4.7.3 → 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.
- package/dist/cli/index.js +14 -12
- package/package.json +12 -12
- 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/skills/rules/SKILL.md +2 -4
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -1
- package/packages/omo-codex/scripts/install/config.mjs +2 -0
- 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.
|
|
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.
|
|
61808
|
-
"oh-my-openagent-darwin-x64": "4.7.
|
|
61809
|
-
"oh-my-openagent-darwin-x64-baseline": "4.7.
|
|
61810
|
-
"oh-my-openagent-linux-arm64": "4.7.
|
|
61811
|
-
"oh-my-openagent-linux-arm64-musl": "4.7.
|
|
61812
|
-
"oh-my-openagent-linux-x64": "4.7.
|
|
61813
|
-
"oh-my-openagent-linux-x64-baseline": "4.7.
|
|
61814
|
-
"oh-my-openagent-linux-x64-musl": "4.7.
|
|
61815
|
-
"oh-my-openagent-linux-x64-musl-baseline": "4.7.
|
|
61816
|
-
"oh-my-openagent-windows-x64": "4.7.
|
|
61817
|
-
"oh-my-openagent-windows-x64-baseline": "4.7.
|
|
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.
|
|
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.
|
|
139
|
-
"oh-my-openagent-darwin-x64": "4.7.
|
|
140
|
-
"oh-my-openagent-darwin-x64-baseline": "4.7.
|
|
141
|
-
"oh-my-openagent-linux-arm64": "4.7.
|
|
142
|
-
"oh-my-openagent-linux-arm64-musl": "4.7.
|
|
143
|
-
"oh-my-openagent-linux-x64": "4.7.
|
|
144
|
-
"oh-my-openagent-linux-x64-baseline": "4.7.
|
|
145
|
-
"oh-my-openagent-linux-x64-musl": "4.7.
|
|
146
|
-
"oh-my-openagent-linux-x64-musl-baseline": "4.7.
|
|
147
|
-
"oh-my-openagent-windows-x64": "4.7.
|
|
148
|
-
"oh-my-openagent-windows-x64-baseline": "4.7.
|
|
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`
|
|
@@ -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"),
|
|
@@ -24,7 +24,7 @@ describe("rules source selection", () => {
|
|
|
24
24
|
// given
|
|
25
25
|
const config: PiRulesConfig = {
|
|
26
26
|
...defaultConfig(),
|
|
27
|
-
enabledSources: [".omo/rules", "
|
|
27
|
+
enabledSources: [".omo/rules", "plugin-bundled"],
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
// when
|
|
@@ -33,13 +33,14 @@ describe("rules source selection", () => {
|
|
|
33
33
|
if (disabledSources === undefined) return;
|
|
34
34
|
|
|
35
35
|
// then
|
|
36
|
-
for (const source of [".omo/rules", "
|
|
36
|
+
for (const source of [".omo/rules", "plugin-bundled"]) {
|
|
37
37
|
expect(disabledSources.has(source)).toBe(false);
|
|
38
38
|
}
|
|
39
|
-
expect(disabledSources.has("plugin-bundled")).toBe(true);
|
|
40
39
|
expect(disabledSources.has("~/.claude/rules")).toBe(true);
|
|
41
40
|
expect(disabledSources).toEqual(
|
|
42
|
-
new Set(
|
|
41
|
+
new Set(
|
|
42
|
+
[...SOURCE_PRIORITY.keys()].filter((source) => source !== ".omo/rules" && source !== "plugin-bundled"),
|
|
43
|
+
),
|
|
43
44
|
);
|
|
44
45
|
});
|
|
45
46
|
});
|
package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md
CHANGED
|
@@ -172,7 +172,7 @@ Trigger only when one goal remains and all its criteria are passing.
|
|
|
172
172
|
1. Run targeted verification for changed behavior.
|
|
173
173
|
2. Run `ai-slop-cleaner` on changed files. If no relevant edits exist, record a passed no-op cleaner report.
|
|
174
174
|
3. Rerun verification after cleanup.
|
|
175
|
-
4.
|
|
175
|
+
4. Judge the change size. Spawn the `codex-ultrawork-reviewer` agent (`spawn_agent(agent_type="codex-ultrawork-reviewer", fork_turns="none", ...)`; fall back to `agent_type="worker"` with a scoped reviewer assignment if unavailable) only when the work is large or risky (multi-file, cross-cutting, new architecture, security/data surfaces, or you are unsure it is sound); for a small, local, low-risk change, do the review yourself and record `codeReview` with `evidence` starting `UNCONDITIONAL APPROVAL` plus a one-line justification of why the change was small enough to self-review.
|
|
176
176
|
5. Clean review means `codeReview.recommendation == "APPROVE"` and `codeReview.architectStatus == "CLEAR"`.
|
|
177
177
|
6. If review is non-clean, run `omo ulw-loop record-review-blockers --goal-id <id> --title "<...>" --objective "<...>" --evidence "<review findings>" --codex-goal-json <snapshot> --json`.
|
|
178
178
|
7. If clean, checkpoint final completion:
|
|
@@ -100,18 +100,18 @@ function formatCriterionLine(criterion: UlwLoopSuccessCriterion): string {
|
|
|
100
100
|
|
|
101
101
|
function finalSection(plan: UlwLoopPlan, goal: UlwLoopItem, isFinal: boolean, aggregate: boolean): string {
|
|
102
102
|
if (!isFinal)
|
|
103
|
-
return "- This is not the final ulw-loop story; do not run the final ai-slop-cleaner
|
|
103
|
+
return "- This is not the final ulw-loop story; do not run the final ai-slop-cleaner/code-review gate yet.";
|
|
104
104
|
const option = sessionOption(plan);
|
|
105
105
|
const blockerCommand = `omo ulw-loop record-review-blockers${option} --goal-id ${goal.id} --title "Resolve final code-review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --codex-goal-json "<active get_goal JSON or path>"`;
|
|
106
106
|
const checkpointCommand = `omo ulw-loop checkpoint${option} --goal-id ${goal.id} --status complete --evidence "<tests/files/PR evidence>" --codex-goal-json "<fresh complete get_goal JSON or path>" --quality-gate-json "<quality gate JSON or path>"`;
|
|
107
107
|
return joinLines([
|
|
108
108
|
"Final story — run mandatory quality gate before update_goal:",
|
|
109
|
-
"- Run ai-slop-cleaner on changed files even when it is a no-op, rerun verification, then run
|
|
110
|
-
"- If final
|
|
109
|
+
"- Run ai-slop-cleaner on changed files even when it is a no-op, rerun verification, then run the code review (spawn_agent(agent_type=\"codex-ultrawork-reviewer\", fork_turns=\"none\", ...); fall back to agent_type=\"worker\" with a scoped reviewer assignment if unavailable).",
|
|
110
|
+
"- If the final review is not APPROVE with architect status CLEAR, do not call update_goal. Record blocker work first:",
|
|
111
111
|
` ${blockerCommand}`,
|
|
112
112
|
aggregate
|
|
113
|
-
? '- If final
|
|
114
|
-
: '- If final
|
|
113
|
+
? '- If the final review is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint the aggregate story:'
|
|
114
|
+
: '- If the final review is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint:',
|
|
115
115
|
` ${checkpointCommand}`,
|
|
116
116
|
]);
|
|
117
117
|
}
|
|
@@ -12,6 +12,7 @@ const GHCR_PATTERN =
|
|
|
12
12
|
/\b(ghcr|github container registry|read packages|imagepullsecret|package api|anonymous|container image)\b/;
|
|
13
13
|
const GHCR_401_PATTERN = /\b(401|unauthorized|anonymous pull|authentication required)\b/;
|
|
14
14
|
const GHCR_403_PATTERN = /\b(403|forbidden|read packages|package api)\b/;
|
|
15
|
+
const UNCONDITIONAL_APPROVAL_PATTERN = /\bUNCONDITIONAL\s+APPROVAL\b/i;
|
|
15
16
|
|
|
16
17
|
function invalid(message: string, field: string): never {
|
|
17
18
|
throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", { details: { field } });
|
|
@@ -42,6 +43,58 @@ function stringArray(value: unknown, field: string): string[] {
|
|
|
42
43
|
return value.map((item) => nonEmptyString(item, field));
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function normalizeReviewerField({
|
|
47
|
+
value,
|
|
48
|
+
field,
|
|
49
|
+
expectedValue,
|
|
50
|
+
evidenceApproved,
|
|
51
|
+
}: {
|
|
52
|
+
value: unknown;
|
|
53
|
+
field: string;
|
|
54
|
+
expectedValue: "APPROVE";
|
|
55
|
+
evidenceApproved: boolean;
|
|
56
|
+
}): "APPROVE";
|
|
57
|
+
function normalizeReviewerField({
|
|
58
|
+
value,
|
|
59
|
+
field,
|
|
60
|
+
expectedValue,
|
|
61
|
+
evidenceApproved,
|
|
62
|
+
}: {
|
|
63
|
+
value: unknown;
|
|
64
|
+
field: string;
|
|
65
|
+
expectedValue: "CLEAR";
|
|
66
|
+
evidenceApproved: boolean;
|
|
67
|
+
}): "CLEAR";
|
|
68
|
+
function normalizeReviewerField({
|
|
69
|
+
value,
|
|
70
|
+
field,
|
|
71
|
+
expectedValue,
|
|
72
|
+
evidenceApproved,
|
|
73
|
+
}: {
|
|
74
|
+
value: unknown;
|
|
75
|
+
field: string;
|
|
76
|
+
expectedValue: "APPROVE" | "CLEAR";
|
|
77
|
+
evidenceApproved: boolean;
|
|
78
|
+
}): "APPROVE" | "CLEAR" {
|
|
79
|
+
if (typeof value === "string") {
|
|
80
|
+
const trimmed = value.trim();
|
|
81
|
+
if (trimmed === "") {
|
|
82
|
+
if (evidenceApproved) return expectedValue;
|
|
83
|
+
invalid(
|
|
84
|
+
`${field} must be ${expectedValue} or codeReview.evidence should include UNCONDITIONAL APPROVAL.`,
|
|
85
|
+
field,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (trimmed === expectedValue) return expectedValue;
|
|
89
|
+
invalid(`${field} must be ${expectedValue}.`, field);
|
|
90
|
+
}
|
|
91
|
+
if (value === undefined) {
|
|
92
|
+
if (evidenceApproved) return expectedValue;
|
|
93
|
+
invalid(`${field} must be ${expectedValue} or codeReview.evidence should include UNCONDITIONAL APPROVAL.`, field);
|
|
94
|
+
}
|
|
95
|
+
invalid(`${field} must be ${expectedValue}.`, field);
|
|
96
|
+
}
|
|
97
|
+
|
|
45
98
|
export function validateQualityGate(input: unknown): UlwLoopQualityGate {
|
|
46
99
|
const gate = section(input, "qualityGate");
|
|
47
100
|
const cleaner = section(gate["aiSlopCleaner"], "aiSlopCleaner");
|
|
@@ -50,8 +103,6 @@ export function validateQualityGate(input: unknown): UlwLoopQualityGate {
|
|
|
50
103
|
const coverage = section(gate["criteriaCoverage"], "criteriaCoverage");
|
|
51
104
|
if (cleaner["status"] !== "passed") invalid("aiSlopCleaner.status must be passed.", "aiSlopCleaner.status");
|
|
52
105
|
if (verification["status"] !== "passed") invalid("verification.status must be passed.", "verification.status");
|
|
53
|
-
if (review["recommendation"] !== "APPROVE") invalid("recommendation must be APPROVE.", "codeReview.recommendation");
|
|
54
|
-
if (review["architectStatus"] !== "CLEAR") invalid("architectStatus must be CLEAR.", "codeReview.architectStatus");
|
|
55
106
|
const totalCriteria = numberField(coverage["totalCriteria"], "criteriaCoverage.totalCriteria");
|
|
56
107
|
const passCount = numberField(coverage["passCount"], "criteriaCoverage.passCount");
|
|
57
108
|
if (passCount < totalCriteria)
|
|
@@ -61,10 +112,23 @@ export function validateQualityGate(input: unknown): UlwLoopQualityGate {
|
|
|
61
112
|
const cleanerEvidence = nonEmptyString(cleaner["evidence"], "aiSlopCleaner.evidence");
|
|
62
113
|
const verificationEvidence = nonEmptyString(verification["evidence"], "verification.evidence");
|
|
63
114
|
const reviewEvidence = nonEmptyString(review["evidence"], "codeReview.evidence");
|
|
115
|
+
const approvalEvidence = UNCONDITIONAL_APPROVAL_PATTERN.test(reviewEvidence);
|
|
116
|
+
const recommendation = normalizeReviewerField({
|
|
117
|
+
value: review["recommendation"],
|
|
118
|
+
field: "codeReview.recommendation",
|
|
119
|
+
expectedValue: "APPROVE",
|
|
120
|
+
evidenceApproved: approvalEvidence,
|
|
121
|
+
});
|
|
122
|
+
const architectStatus = normalizeReviewerField({
|
|
123
|
+
value: review["architectStatus"],
|
|
124
|
+
field: "codeReview.architectStatus",
|
|
125
|
+
expectedValue: "CLEAR",
|
|
126
|
+
evidenceApproved: approvalEvidence,
|
|
127
|
+
});
|
|
64
128
|
const result: UlwLoopQualityGate = {
|
|
65
129
|
aiSlopCleaner: { status: "passed", evidence: cleanerEvidence },
|
|
66
130
|
verification: { status: "passed", commands, evidence: verificationEvidence },
|
|
67
|
-
codeReview: { recommendation
|
|
131
|
+
codeReview: { recommendation, architectStatus, evidence: reviewEvidence },
|
|
68
132
|
};
|
|
69
133
|
Object.assign(result, { criteriaCoverage: { totalCriteria, passCount, adversarialClassesCovered: covered } });
|
|
70
134
|
return result;
|
|
@@ -81,6 +81,57 @@ describe("validateQualityGate", () => {
|
|
|
81
81
|
expect(gate).toMatchObject({ criteriaCoverage: { totalCriteria: 9, passCount: 9 } });
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
it("infers APPROVE/CLEAR when clean reviewer evidence omits structured fields", () => {
|
|
85
|
+
// given
|
|
86
|
+
const input = makeGate({
|
|
87
|
+
codeReview: {
|
|
88
|
+
evidence: "UNCONDITIONAL APPROVAL\nAll criteria and QA evidence are complete.",
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// when
|
|
93
|
+
const gate = validateQualityGate(input);
|
|
94
|
+
|
|
95
|
+
// then
|
|
96
|
+
expect(gate.codeReview.recommendation).toBe("APPROVE");
|
|
97
|
+
expect(gate.codeReview.architectStatus).toBe("CLEAR");
|
|
98
|
+
expect(gate.codeReview.evidence).toBe("UNCONDITIONAL APPROVAL\nAll criteria and QA evidence are complete.");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("infers APPROVE/CLEAR when clean reviewer evidence has blank structured fields", () => {
|
|
102
|
+
// given
|
|
103
|
+
const input = makeGate({
|
|
104
|
+
codeReview: {
|
|
105
|
+
recommendation: "",
|
|
106
|
+
architectStatus: " ",
|
|
107
|
+
evidence: "UNCONDITIONAL APPROVAL\nAll criteria and QA evidence are complete.",
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// when
|
|
112
|
+
const gate = validateQualityGate(input);
|
|
113
|
+
|
|
114
|
+
// then
|
|
115
|
+
expect(gate.codeReview.recommendation).toBe("APPROVE");
|
|
116
|
+
expect(gate.codeReview.architectStatus).toBe("CLEAR");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("throws when reviewer fields are omitted and evidence has no approval verdict", () => {
|
|
120
|
+
// given
|
|
121
|
+
const input = makeGate({
|
|
122
|
+
codeReview: {
|
|
123
|
+
evidence: "review completed without an explicit verdict",
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// when
|
|
128
|
+
const error = getQualityGateError(input);
|
|
129
|
+
|
|
130
|
+
// then
|
|
131
|
+
expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
|
|
132
|
+
expect(error.message).toContain("UNCONDITIONAL APPROVAL");
|
|
133
|
+
});
|
|
134
|
+
|
|
84
135
|
it("throws UlwLoopError when aiSlopCleaner missing", () => {
|
|
85
136
|
// when
|
|
86
137
|
const error = getQualityGateError(makeGate({ aiSlopCleaner: undefined }));
|
|
@@ -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`.
|
|
@@ -172,7 +172,7 @@ Trigger only when one goal remains and all its criteria are passing.
|
|
|
172
172
|
1. Run targeted verification for changed behavior.
|
|
173
173
|
2. Run `ai-slop-cleaner` on changed files. If no relevant edits exist, record a passed no-op cleaner report.
|
|
174
174
|
3. Rerun verification after cleanup.
|
|
175
|
-
4.
|
|
175
|
+
4. Judge the change size. Spawn the `codex-ultrawork-reviewer` agent (`spawn_agent(agent_type="codex-ultrawork-reviewer", fork_turns="none", ...)`; fall back to `agent_type="worker"` with a scoped reviewer assignment if unavailable) only when the work is large or risky (multi-file, cross-cutting, new architecture, security/data surfaces, or you are unsure it is sound); for a small, local, low-risk change, do the review yourself and record `codeReview` with `evidence` starting `UNCONDITIONAL APPROVAL` plus a one-line justification of why the change was small enough to self-review.
|
|
176
176
|
5. Clean review means `codeReview.recommendation == "APPROVE"` and `codeReview.architectStatus == "CLEAR"`.
|
|
177
177
|
6. If review is non-clean, run `omo ulw-loop record-review-blockers --goal-id <id> --title "<...>" --objective "<...>" --evidence "<review findings>" --codex-goal-json <snapshot> --json`.
|
|
178
178
|
7. If clean, checkpoint final completion:
|
|
@@ -44,6 +44,8 @@ export async function updateCodexConfig({
|
|
|
44
44
|
config = removeStaleManagedAgentBlocks(config, new Set(agentConfigs.map((agentConfig) => agentConfig.name)));
|
|
45
45
|
config = ensureFeatureEnabled(config, "plugins");
|
|
46
46
|
config = ensureFeatureEnabled(config, "plugin_hooks");
|
|
47
|
+
config = ensureFeatureEnabled(config, "multi_agent");
|
|
48
|
+
config = ensureFeatureEnabled(config, "child_agents_md");
|
|
47
49
|
config = ensureCodexReasoningConfig(config, await readCodexModelCatalog(repoRoot));
|
|
48
50
|
config = ensureCodexMultiAgentV2Config(config);
|
|
49
51
|
if (autonomousPermissions === true) config = ensureAutonomousPermissions(config);
|
|
@@ -6,7 +6,8 @@ import test from "node:test";
|
|
|
6
6
|
|
|
7
7
|
import { updateCodexConfig } from "./install/config.mjs";
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const ALWAYS_ON_FEATURES = ["plugins", "plugin_hooks", "multi_agent", "child_agents_md"];
|
|
10
|
+
const AUTONOMOUS_PERMISSION_FEATURES = ["unified_exec", "goals"];
|
|
10
11
|
|
|
11
12
|
test("#given autonomous permissions requested #when script installer updates config #then enables Codex autonomy feature flags", async () => {
|
|
12
13
|
// given
|
|
@@ -39,12 +40,15 @@ test("#given autonomous permissions requested #when script installer updates con
|
|
|
39
40
|
// then
|
|
40
41
|
const content = await readFile(configPath, "utf8");
|
|
41
42
|
assert.match(content, /network_access = "enabled"/);
|
|
42
|
-
for (const featureName of
|
|
43
|
+
for (const featureName of ALWAYS_ON_FEATURES) {
|
|
44
|
+
assert.match(content, new RegExp(`${featureName} = true`));
|
|
45
|
+
}
|
|
46
|
+
for (const featureName of AUTONOMOUS_PERMISSION_FEATURES) {
|
|
43
47
|
assert.match(content, new RegExp(`${featureName} = true`));
|
|
44
48
|
}
|
|
45
49
|
});
|
|
46
50
|
|
|
47
|
-
test("#given autonomous permissions disabled #when script installer updates config #then
|
|
51
|
+
test("#given autonomous permissions disabled #when script installer updates config #then keeps native Codex feature flags enabled", async () => {
|
|
48
52
|
// given
|
|
49
53
|
const root = await mkdtemp(join(tmpdir(), "omo-codex-script-config-autonomous-features-disabled-"));
|
|
50
54
|
const configPath = join(root, "config.toml");
|
|
@@ -75,9 +79,10 @@ test("#given autonomous permissions disabled #when script installer updates conf
|
|
|
75
79
|
// then
|
|
76
80
|
const content = await readFile(configPath, "utf8");
|
|
77
81
|
assert.match(content, /network_access = "disabled"/);
|
|
78
|
-
for (const featureName of
|
|
82
|
+
for (const featureName of ALWAYS_ON_FEATURES) {
|
|
83
|
+
assert.match(content, new RegExp(`${featureName} = true`));
|
|
84
|
+
}
|
|
85
|
+
for (const featureName of AUTONOMOUS_PERMISSION_FEATURES) {
|
|
79
86
|
assert.match(content, new RegExp(`${featureName} = false`));
|
|
80
87
|
}
|
|
81
|
-
assert.match(content, /plugins = true/);
|
|
82
|
-
assert.match(content, /plugin_hooks = true/);
|
|
83
88
|
});
|