oh-my-opencode 4.7.3 → 4.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +80 -77
- package/package.json +13 -12
- package/packages/lsp-tools-mcp/package.json +52 -0
- package/packages/omo-codex/plugin/README.md +23 -0
- package/packages/omo-codex/plugin/components/rules/README.md +2 -2
- package/packages/omo-codex/plugin/components/rules/skills/rules/SKILL.md +2 -4
- package/packages/omo-codex/plugin/components/rules/src/config.ts +1 -5
- package/packages/omo-codex/plugin/components/rules/src/rules/constants.ts +1 -7
- package/packages/omo-codex/plugin/components/rules/src/rules/finder-sources.ts +0 -5
- package/packages/omo-codex/plugin/components/rules/src/rules/sources.ts +2 -6
- package/packages/omo-codex/plugin/components/rules/src/rules/types.ts +2 -6
- package/packages/omo-codex/plugin/components/rules/test/agent-doc-sources.test.ts +119 -0
- package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-budget.test.ts +4 -2
- package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-dedup.test.ts +4 -2
- package/packages/omo-codex/plugin/components/rules/test/codex-hook.test.ts +41 -2
- package/packages/omo-codex/plugin/components/rules/test/config.test.ts +22 -0
- package/packages/omo-codex/plugin/components/rules/test/engine.test.ts +1 -1
- package/packages/omo-codex/plugin/components/rules/test/finder.test.ts +2 -7
- package/packages/omo-codex/plugin/components/rules/test/formatter.test.ts +14 -14
- package/packages/omo-codex/plugin/components/rules/test/post-compact-test-fixture.ts +4 -2
- package/packages/omo-codex/plugin/components/rules/test/sources.test.ts +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +67 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +51 -0
- package/packages/omo-codex/plugin/scripts/auto-update.mjs +24 -2
- package/packages/omo-codex/plugin/skills/rules/SKILL.md +2 -4
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -1
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +16 -0
- package/packages/omo-codex/scripts/install/config.mjs +2 -0
- package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +11 -6
|
@@ -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 }));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
-
import { mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { appendFile, mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
@@ -40,14 +40,19 @@ export async function runAutoUpdateCheck({ env = process.env, now = Date.now() }
|
|
|
40
40
|
if (!plan.shouldRun) return { started: false, reason: plan.reason };
|
|
41
41
|
|
|
42
42
|
const lock = await acquireLock(resolveLockPath(env, statePath), now, env);
|
|
43
|
-
if (lock === null)
|
|
43
|
+
if (lock === null) {
|
|
44
|
+
await appendUpdateLog(env, now, "locked");
|
|
45
|
+
return { started: false, reason: "locked" };
|
|
46
|
+
}
|
|
44
47
|
try {
|
|
45
48
|
await writeState(statePath, { lastCheckedAt: now });
|
|
49
|
+
await appendUpdateLog(env, now, "started", { command: plan.command, args: plan.args });
|
|
46
50
|
if (env.LAZYCODEX_AUTO_UPDATE_WAIT === "1") {
|
|
47
51
|
const result = spawnSync(plan.command, plan.args, {
|
|
48
52
|
env: plan.env,
|
|
49
53
|
stdio: "ignore",
|
|
50
54
|
});
|
|
55
|
+
await appendUpdateLog(env, now, "finished", { status: result.status ?? 0 });
|
|
51
56
|
return { started: true, status: result.status ?? 0 };
|
|
52
57
|
}
|
|
53
58
|
|
|
@@ -94,6 +99,12 @@ function resolveStatePath(env) {
|
|
|
94
99
|
return join(dataRoot, "auto-update.json");
|
|
95
100
|
}
|
|
96
101
|
|
|
102
|
+
function resolveLogPath(env) {
|
|
103
|
+
if (env.LAZYCODEX_AUTO_UPDATE_LOG_PATH?.trim()) return env.LAZYCODEX_AUTO_UPDATE_LOG_PATH;
|
|
104
|
+
const dataRoot = env.PLUGIN_DATA?.trim() || join(homedir(), ".local", "share", "lazycodex");
|
|
105
|
+
return join(dataRoot, "auto-update.log");
|
|
106
|
+
}
|
|
107
|
+
|
|
97
108
|
function resolveLockPath(env, statePath) {
|
|
98
109
|
if (env.LAZYCODEX_AUTO_UPDATE_LOCK_PATH?.trim()) return env.LAZYCODEX_AUTO_UPDATE_LOCK_PATH;
|
|
99
110
|
return `${statePath}.lock`;
|
|
@@ -145,6 +156,17 @@ async function writeState(statePath, state) {
|
|
|
145
156
|
await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
146
157
|
}
|
|
147
158
|
|
|
159
|
+
async function appendUpdateLog(env, now, event, details = {}) {
|
|
160
|
+
const logPath = resolveLogPath(env);
|
|
161
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
162
|
+
const entry = {
|
|
163
|
+
timestamp: new Date(now).toISOString(),
|
|
164
|
+
event,
|
|
165
|
+
...details,
|
|
166
|
+
};
|
|
167
|
+
await appendFile(logPath, `${JSON.stringify(entry)}\n`);
|
|
168
|
+
}
|
|
169
|
+
|
|
148
170
|
function parsePositiveInteger(value, fallback) {
|
|
149
171
|
if (value === undefined || value === "") return fallback;
|
|
150
172
|
const parsed = Number.parseInt(value, 10);
|
|
@@ -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:
|
|
@@ -43,6 +43,7 @@ test("#given recent state #when resolving plan #then update is throttled", () =>
|
|
|
43
43
|
test("#given test command override #when running check #then records state and launches command", async () => {
|
|
44
44
|
const root = await mkdtemp(join(tmpdir(), "lazycodex-auto-update-"));
|
|
45
45
|
const logPath = join(root, "spawn.log");
|
|
46
|
+
const updateLogPath = join(root, "auto-update.log");
|
|
46
47
|
const statePath = join(root, "state.json");
|
|
47
48
|
const codexHome = join(root, "codex-home");
|
|
48
49
|
|
|
@@ -51,6 +52,7 @@ test("#given test command override #when running check #then records state and l
|
|
|
51
52
|
CODEX_HOME: codexHome,
|
|
52
53
|
LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json"),
|
|
53
54
|
LAZYCODEX_AUTO_UPDATE_STATE_PATH: statePath,
|
|
55
|
+
LAZYCODEX_AUTO_UPDATE_LOG_PATH: updateLogPath,
|
|
54
56
|
LAZYCODEX_AUTO_UPDATE_INTERVAL_MS: "0",
|
|
55
57
|
LAZYCODEX_AUTO_UPDATE_COMMAND: process.execPath,
|
|
56
58
|
LAZYCODEX_AUTO_UPDATE_ARGS_JSON: JSON.stringify(["-e", `require("node:fs").writeFileSync(${JSON.stringify(logPath)}, "ok")`]),
|
|
@@ -62,6 +64,20 @@ test("#given test command override #when running check #then records state and l
|
|
|
62
64
|
assert.equal(result.started, true);
|
|
63
65
|
assert.equal(JSON.parse(await readFile(statePath, "utf8")).lastCheckedAt, 123_456);
|
|
64
66
|
assert.equal(await readFile(logPath, "utf8"), "ok");
|
|
67
|
+
const updateLog = (await readFile(updateLogPath, "utf8")).trim().split("\n").map((line) => JSON.parse(line));
|
|
68
|
+
assert.deepEqual(updateLog, [
|
|
69
|
+
{
|
|
70
|
+
timestamp: "1970-01-01T00:02:03.456Z",
|
|
71
|
+
event: "started",
|
|
72
|
+
command: process.execPath,
|
|
73
|
+
args: ["-e", `require("node:fs").writeFileSync(${JSON.stringify(logPath)}, "ok")`],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
timestamp: "1970-01-01T00:02:03.456Z",
|
|
77
|
+
event: "finished",
|
|
78
|
+
status: 0,
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
65
81
|
assert.match(await readFile(join(codexHome, "config.toml"), "utf8"), /model = "gpt-5\.5"/);
|
|
66
82
|
});
|
|
67
83
|
|
|
@@ -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
|
});
|