oh-my-openagent 4.7.2 → 4.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/cli/index.js +14 -12
  2. package/package.json +12 -12
  3. package/packages/omo-codex/plugin/components/rules/README.md +4 -4
  4. package/packages/omo-codex/plugin/components/rules/skills/rules/SKILL.md +2 -4
  5. package/packages/omo-codex/plugin/components/rules/src/config.ts +1 -5
  6. package/packages/omo-codex/plugin/components/rules/src/dynamic-target-fingerprints.ts +2 -11
  7. package/packages/omo-codex/plugin/components/rules/src/rules/constants.ts +1 -7
  8. package/packages/omo-codex/plugin/components/rules/src/rules/engine.ts +3 -12
  9. package/packages/omo-codex/plugin/components/rules/src/rules/finder-sources.ts +0 -5
  10. package/packages/omo-codex/plugin/components/rules/src/rules/sources.ts +13 -0
  11. package/packages/omo-codex/plugin/components/rules/src/rules/types.ts +2 -6
  12. package/packages/omo-codex/plugin/components/rules/test/agent-doc-sources.test.ts +119 -0
  13. package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-budget.test.ts +4 -2
  14. package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-dedup.test.ts +4 -2
  15. package/packages/omo-codex/plugin/components/rules/test/codex-hook.test.ts +72 -1
  16. package/packages/omo-codex/plugin/components/rules/test/config.test.ts +22 -0
  17. package/packages/omo-codex/plugin/components/rules/test/dynamic-target-fingerprints.test.ts +71 -0
  18. package/packages/omo-codex/plugin/components/rules/test/engine.test.ts +52 -0
  19. package/packages/omo-codex/plugin/components/rules/test/finder.test.ts +2 -7
  20. package/packages/omo-codex/plugin/components/rules/test/formatter.test.ts +14 -14
  21. package/packages/omo-codex/plugin/components/rules/test/post-compact-test-fixture.ts +4 -2
  22. package/packages/omo-codex/plugin/components/rules/test/sources.test.ts +46 -0
  23. package/packages/omo-codex/plugin/components/ultrawork/README.md +2 -0
  24. package/packages/omo-codex/plugin/components/ultrawork/src/codex-hook.ts +62 -0
  25. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +51 -0
  26. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +1 -1
  27. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
  28. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +67 -3
  29. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +51 -0
  30. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +15 -5
  31. package/packages/omo-codex/plugin/skills/rules/SKILL.md +2 -4
  32. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -1
  33. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +87 -1
  34. package/packages/omo-codex/scripts/install/config.mjs +2 -0
  35. package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +11 -6
@@ -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 }));
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
@@ -169,7 +169,9 @@ async function configPaths({ env, cwd }) {
169
169
  const codexHome = resolve(env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
170
170
  const paths = new Set([join(codexHome, "config.toml")]);
171
171
  for (const projectConfig of projectConfigPaths({ cwd, stopAt: homedir() })) {
172
- if (await pathExists(projectConfig)) paths.add(projectConfig);
172
+ if (!(await isRegularFile(projectConfig))) continue;
173
+ if (!(await isRegularDirectory(dirname(projectConfig)))) continue;
174
+ paths.add(projectConfig);
173
175
  }
174
176
  return [...paths];
175
177
  }
@@ -216,10 +218,18 @@ async function writeState(statePath, state) {
216
218
  await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`);
217
219
  }
218
220
 
219
- async function pathExists(path) {
221
+ async function isRegularFile(path) {
220
222
  try {
221
- await stat(path);
222
- return true;
223
+ return (await lstat(path)).isFile();
224
+ } catch (error) {
225
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
226
+ throw error;
227
+ }
228
+ }
229
+
230
+ async function isRegularDirectory(path) {
231
+ try {
232
+ return (await lstat(path)).isDirectory();
223
233
  } catch (error) {
224
234
  if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
225
235
  throw error;
@@ -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:
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
2
+ import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import test from "node:test";
@@ -32,6 +32,66 @@ test("#given stale root reasoning config #when ensuring config #then replaces st
32
32
  assert.match(result, /\[features\]/);
33
33
  });
34
34
 
35
+ test("#given project .codex is a symlink #when migrating #then project config is skipped", async (t) => {
36
+ if (!(await canCreateSymlink("dir"))) t.skip("symbolic links are unavailable in this environment");
37
+
38
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-config-symlink-dir-"));
39
+ const codexHome = join(root, "codex-home");
40
+ const project = join(root, "project");
41
+ const projectNested = join(project, "nested");
42
+ const projectCodexDirectory = join(root, "project-codex-real");
43
+ const projectConfigTarget = join(projectCodexDirectory, "config.toml");
44
+ const projectConfig = join(project, ".codex", "config.toml");
45
+
46
+ await mkdir(codexHome, { recursive: true });
47
+ await mkdir(projectCodexDirectory, { recursive: true });
48
+ await mkdir(dirname(projectConfigTarget), { recursive: true });
49
+ await mkdir(projectNested, { recursive: true });
50
+ await writeFile(join(codexHome, "config.toml"), 'model = "gpt-5.2"\n');
51
+ await writeFile(projectConfigTarget, 'model = "gpt-5.2"\nmodel_context_window = 272000\n');
52
+ await rm(join(project, ".codex"), { recursive: true, force: true });
53
+ await symlink(projectCodexDirectory, join(project, ".codex"), "dir");
54
+
55
+ const result = await migrateCodexConfig({
56
+ env: {
57
+ CODEX_HOME: codexHome,
58
+ LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json"),
59
+ },
60
+ cwd: projectNested,
61
+ });
62
+
63
+ assert.deepEqual(result.changed, [join(codexHome, "config.toml")]);
64
+ assert.match(await readFile(projectConfig, "utf8"), /model = "gpt-5\.2"/);
65
+ });
66
+
67
+ test("#given project config.toml is a symlink #when migrating #then project config is skipped", async (t) => {
68
+ if (!(await canCreateSymlink("file"))) t.skip("symbolic links are unavailable in this environment");
69
+
70
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-config-symlink-file-"));
71
+ const codexHome = join(root, "codex-home");
72
+ const project = join(root, "project");
73
+ const projectConfigDirectory = join(project, ".codex");
74
+ const projectConfig = join(projectConfigDirectory, "config.toml");
75
+ const realConfigSource = join(root, "shared-config.toml");
76
+
77
+ await mkdir(codexHome, { recursive: true });
78
+ await mkdir(projectConfigDirectory, { recursive: true });
79
+ await writeFile(join(codexHome, "config.toml"), 'model = "gpt-5.2"\n');
80
+ await writeFile(realConfigSource, 'model = "gpt-5.2"\nmodel_context_window = 272000\n');
81
+ await symlink(realConfigSource, projectConfig, "file");
82
+
83
+ const result = await migrateCodexConfig({
84
+ env: {
85
+ CODEX_HOME: codexHome,
86
+ LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json"),
87
+ },
88
+ cwd: project,
89
+ });
90
+
91
+ assert.deepEqual(result.changed, [join(codexHome, "config.toml")]);
92
+ assert.match(await readFile(realConfigSource, "utf8"), /model = "gpt-5\.2"/);
93
+ });
94
+
35
95
  test("#given global and project-local stale Codex configs #when migrating #then both configs are forced to current defaults", async () => {
36
96
  const root = await mkdtemp(join(tmpdir(), "lazycodex-config-migration-"));
37
97
  const codexHome = join(root, "codex-home");
@@ -144,3 +204,29 @@ test("#given managed catalog state #when catalog version advances #then only pre
144
204
  assert.match(content, /model = "gpt-5\.5"/);
145
205
  assert.match(content, /model_context_window = 400000/);
146
206
  });
207
+
208
+ async function canCreateSymlink(type) {
209
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-symlink-capability-"));
210
+ const target = join(root, "target");
211
+ const link = join(root, "link");
212
+
213
+ try {
214
+ if (type === "dir") {
215
+ await mkdir(target, { recursive: true });
216
+ await symlink(target, link, "dir");
217
+ } else {
218
+ await writeFile(target, "");
219
+ await symlink(target, link, "file");
220
+ }
221
+
222
+ await rm(link);
223
+ await rm(target, { recursive: true, force: true });
224
+ await rm(root, { recursive: true, force: true });
225
+ return true;
226
+ } catch (error) {
227
+ await rm(root, { recursive: true, force: true });
228
+ if (!(error instanceof Error)) throw error;
229
+ if (error.code === "EPERM" || error.code === "EEXIST") return false;
230
+ return false;
231
+ }
232
+ }
@@ -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
  });