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.
Files changed (27) 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 +2 -2
  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/rules/constants.ts +1 -7
  7. package/packages/omo-codex/plugin/components/rules/src/rules/finder-sources.ts +0 -5
  8. package/packages/omo-codex/plugin/components/rules/src/rules/sources.ts +2 -6
  9. package/packages/omo-codex/plugin/components/rules/src/rules/types.ts +2 -6
  10. package/packages/omo-codex/plugin/components/rules/test/agent-doc-sources.test.ts +119 -0
  11. package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-budget.test.ts +4 -2
  12. package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-dedup.test.ts +4 -2
  13. package/packages/omo-codex/plugin/components/rules/test/codex-hook.test.ts +41 -2
  14. package/packages/omo-codex/plugin/components/rules/test/config.test.ts +22 -0
  15. package/packages/omo-codex/plugin/components/rules/test/engine.test.ts +1 -1
  16. package/packages/omo-codex/plugin/components/rules/test/finder.test.ts +2 -7
  17. package/packages/omo-codex/plugin/components/rules/test/formatter.test.ts +14 -14
  18. package/packages/omo-codex/plugin/components/rules/test/post-compact-test-fixture.ts +4 -2
  19. package/packages/omo-codex/plugin/components/rules/test/sources.test.ts +5 -4
  20. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +1 -1
  21. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
  22. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +67 -3
  23. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +51 -0
  24. package/packages/omo-codex/plugin/skills/rules/SKILL.md +2 -4
  25. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -1
  26. package/packages/omo-codex/scripts/install/config.mjs +2 -0
  27. 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.3",
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.3",
61808
- "oh-my-openagent-darwin-x64": "4.7.3",
61809
- "oh-my-openagent-darwin-x64-baseline": "4.7.3",
61810
- "oh-my-openagent-linux-arm64": "4.7.3",
61811
- "oh-my-openagent-linux-arm64-musl": "4.7.3",
61812
- "oh-my-openagent-linux-x64": "4.7.3",
61813
- "oh-my-openagent-linux-x64-baseline": "4.7.3",
61814
- "oh-my-openagent-linux-x64-musl": "4.7.3",
61815
- "oh-my-openagent-linux-x64-musl-baseline": "4.7.3",
61816
- "oh-my-openagent-windows-x64": "4.7.3",
61817
- "oh-my-openagent-windows-x64-baseline": "4.7.3"
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",
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.3",
139
- "oh-my-openagent-darwin-x64": "4.7.3",
140
- "oh-my-openagent-darwin-x64-baseline": "4.7.3",
141
- "oh-my-openagent-linux-arm64": "4.7.3",
142
- "oh-my-openagent-linux-arm64-musl": "4.7.3",
143
- "oh-my-openagent-linux-x64": "4.7.3",
144
- "oh-my-openagent-linux-x64-baseline": "4.7.3",
145
- "oh-my-openagent-linux-x64-musl": "4.7.3",
146
- "oh-my-openagent-linux-x64-musl-baseline": "4.7.3",
147
- "oh-my-openagent-windows-x64": "4.7.3",
148
- "oh-my-openagent-windows-x64-baseline": "4.7.3"
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
- - `.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;
@@ -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
 
@@ -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, RuleSource } from "./types.js";
2
+ import type { PiRulesConfig } from "./types.js";
3
3
 
4
- export const DEFAULT_AUTO_DISABLED_SOURCES: readonly RuleSource[] = [
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 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"),
@@ -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("Always wear safety goggles");
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 Codex-native and Claude-home sources are explicitly enabled #when loading static rules #then they are not disabled", () => {
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/AGENTS.md",
16
- relativePath: "packages/AGENTS.md",
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/AGENTS.md",
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/AGENTS.md",
49
- relativePath: "AGENTS.md",
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/AGENTS.md",
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/AGENTS.md",
86
- relativePath: "AGENTS.md",
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/AGENTS.md",
91
- relativePath: "packages/AGENTS.md",
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/AGENTS.md");
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/AGENTS.md";
149
- const relativePath = input.relativePath ?? "AGENTS.md";
150
- const source = input.source ?? "AGENTS.md";
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: "AGENTS.md,.omo/rules",
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"), `Project rule\n${"A".repeat(30_000)}`);
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", "AGENTS.md"],
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", "AGENTS.md"]) {
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([...SOURCE_PRIORITY.keys()].filter((source) => source !== ".omo/rules" && source !== "AGENTS.md")),
41
+ new Set(
42
+ [...SOURCE_PRIORITY.keys()].filter((source) => source !== ".omo/rules" && source !== "plugin-bundled"),
43
+ ),
43
44
  );
44
45
  });
45
46
  });
@@ -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. Run `$code-review`.
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/$code-review gate yet.";
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 $code-review.",
110
- "- If final $code-review is not APPROVE with architect status CLEAR, do not call update_goal. Record blocker work first:",
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 $code-review is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint the aggregate story:'
114
- : '- If final $code-review is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint:',
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: "APPROVE", architectStatus: "CLEAR", evidence: reviewEvidence },
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
- - `.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`.
@@ -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. Run `$code-review`.
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 AUTONOMOUS_FEATURES = ["multi_agent", "child_agents_md", "unified_exec", "goals"];
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 AUTONOMOUS_FEATURES) {
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 preserves autonomy feature opt-outs", async () => {
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 AUTONOMOUS_FEATURES) {
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
  });