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.
- package/dist/cli/index.js +14 -12
- package/package.json +12 -12
- package/packages/omo-codex/plugin/components/rules/README.md +4 -4
- 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/dynamic-target-fingerprints.ts +2 -11
- package/packages/omo-codex/plugin/components/rules/src/rules/constants.ts +1 -7
- package/packages/omo-codex/plugin/components/rules/src/rules/engine.ts +3 -12
- 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 +13 -0
- 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 +72 -1
- package/packages/omo-codex/plugin/components/rules/test/config.test.ts +22 -0
- package/packages/omo-codex/plugin/components/rules/test/dynamic-target-fingerprints.test.ts +71 -0
- package/packages/omo-codex/plugin/components/rules/test/engine.test.ts +52 -0
- 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 +46 -0
- package/packages/omo-codex/plugin/components/ultrawork/README.md +2 -0
- package/packages/omo-codex/plugin/components/ultrawork/src/codex-hook.ts +62 -0
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +51 -0
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +67 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +51 -0
- package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +15 -5
- package/packages/omo-codex/plugin/skills/rules/SKILL.md +2 -4
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +87 -1
- package/packages/omo-codex/scripts/install/config.mjs +2 -0
- 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,
|
|
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
|
|
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
|
|
221
|
+
async function isRegularFile(path) {
|
|
220
222
|
try {
|
|
221
|
-
await
|
|
222
|
-
|
|
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
|
-
- `.
|
|
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:
|
|
@@ -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
|
|
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
|
});
|