oh-my-opencode 4.6.0 → 4.7.1
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/bin/version-mismatch.js +47 -0
- package/bin/version-mismatch.test.ts +120 -0
- package/dist/cli/codex-ulw-loop.d.ts +12 -0
- package/dist/cli/doctor/checks/tui-plugin-config.d.ts +2 -0
- package/dist/cli/index.js +5999 -5542
- package/dist/cli/install-codex/codex-config-reasoning.d.ts +2 -1
- package/dist/cli/install-codex/codex-model-catalog.d.ts +13 -0
- package/dist/features/background-agent/concurrency.d.ts +1 -0
- package/dist/features/background-agent/process-cleanup.d.ts +6 -0
- package/dist/features/claude-code-session-state/state.d.ts +1 -0
- package/dist/features/opencode-skill-loader/index.d.ts +1 -0
- package/dist/features/opencode-skill-loader/opencode-config-skills-reader.d.ts +5 -0
- package/dist/features/tmux-subagent/attachable-session-status.d.ts +1 -1
- package/dist/features/tmux-subagent/session-status-parser.d.ts +1 -0
- package/dist/hooks/comment-checker/cli.d.ts +1 -0
- package/dist/hooks/tasks-todowrite-disabler/constants.d.ts +1 -1
- package/dist/index.js +4250 -3776
- package/dist/shared/command-executor/execute-hook-command.d.ts +2 -0
- package/dist/tools/skill/description-formatter.d.ts +5 -1
- package/dist/tools/skill/types.d.ts +1 -0
- package/package.json +13 -14
- package/packages/ast-grep-mcp/dist/cli.js +53 -9
- package/packages/lsp-tools-mcp/dist/lsp/process.js +1 -1
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +13 -0
- package/packages/omo-codex/plugin/components/lsp/src/cli.ts +6 -2
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +13 -2
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +30 -79
- package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +116 -0
- package/packages/omo-codex/plugin/components/lsp/src/mutated-file-paths.ts +88 -0
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +206 -0
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +5 -3
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +6 -4
- package/packages/omo-codex/plugin/components/rules/src/codex-hook-options.ts +1 -0
- package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
- package/packages/omo-codex/plugin/components/rules/src/rules/finder.ts +15 -2
- package/packages/omo-codex/plugin/components/rules/src/rules-engine-factory.ts +4 -1
- package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +28 -5
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/README.md +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +3 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +7 -7
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +4 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +12 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +19 -1
- package/packages/omo-codex/plugin/hooks/hooks.json +24 -2
- package/packages/omo-codex/plugin/model-catalog.json +49 -0
- package/packages/omo-codex/plugin/scripts/auto-update.mjs +159 -0
- package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +269 -0
- package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +4 -9
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -6
- package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +127 -0
- package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +9 -0
- package/packages/omo-codex/plugin/skills/refactor/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -7
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +6 -6
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +5 -4
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +4 -3
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +17 -17
- package/packages/omo-codex/plugin/test/aggregate.test.mjs +188 -19
- package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +7 -27
- package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -0
- package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +27 -1
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +22 -0
- package/packages/omo-codex/scripts/install/cli-args.mjs +1 -1
- package/packages/omo-codex/scripts/install/config.mjs +2 -15
- package/packages/omo-codex/scripts/install/delegated-command.mjs +1 -1
- package/packages/omo-codex/scripts/install/legacy-bins.mjs +1 -0
- package/packages/omo-codex/scripts/install/model-catalog.mjs +66 -0
- package/packages/omo-codex/scripts/install/permissions.mjs +11 -0
- package/packages/omo-codex/scripts/install/reasoning-config.mjs +65 -7
- package/packages/omo-codex/scripts/install-bin-links.test.mjs +23 -0
- package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +83 -0
- package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +82 -3
- package/packages/omo-codex/scripts/install-config.test.mjs +5 -6
- package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +30 -2
- package/packages/omo-codex/scripts/install-local.mjs +1 -1
- package/packages/omo-codex/scripts/install-local.test.mjs +3 -1
- package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +127 -0
- package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +9 -0
- package/packages/shared-skills/skills/review-work/SKILL.md +7 -7
- package/packages/shared-skills/skills/start-work/SKILL.md +6 -6
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -11
- package/postinstall.mjs +36 -3
- package/dist/cli/install-codex/codex-config-mcp.d.ts +0 -1
|
@@ -5,17 +5,30 @@ import test from "node:test";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
|
+
const mcpPackageManifestPaths = ["../../lsp-tools-mcp/package.json", "../../ast-grep-mcp/package.json", "../../git-bash-mcp/package.json"];
|
|
9
|
+
const mcpPackageManifestExists = await Promise.all(mcpPackageManifestPaths.map(exists));
|
|
8
10
|
|
|
9
11
|
async function readJson(relativePath) {
|
|
10
12
|
return JSON.parse(await readFile(join(root, relativePath), "utf8"));
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
async function exists(relativePath) {
|
|
16
|
+
try {
|
|
17
|
+
await stat(join(root, relativePath));
|
|
18
|
+
return true;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
async function readComponentHookManifests() {
|
|
14
26
|
const components = await readdir(join(root, "components"), { withFileTypes: true });
|
|
15
27
|
const manifests = [];
|
|
16
28
|
for (const entry of components) {
|
|
17
29
|
if (!entry.isDirectory()) continue;
|
|
18
30
|
const source = join("components", entry.name, "hooks", "hooks.json");
|
|
31
|
+
if (!(await exists(source))) continue;
|
|
19
32
|
manifests.push({ source, hooks: await readJson(source) });
|
|
20
33
|
}
|
|
21
34
|
return manifests.sort((left, right) => left.source.localeCompare(right.source));
|
|
@@ -57,6 +70,18 @@ function findSpawnAgentTypes(content) {
|
|
|
57
70
|
return [...agentTypes].sort();
|
|
58
71
|
}
|
|
59
72
|
|
|
73
|
+
function findRoleSpecificSpawnsWithoutForkTurnsNone(content) {
|
|
74
|
+
const missingForkTurns = [];
|
|
75
|
+
const regex = /spawn_agent\(agent_type="([^"]+)"[^)]*\)/g;
|
|
76
|
+
for (const match of content.matchAll(regex)) {
|
|
77
|
+
const call = match[0];
|
|
78
|
+
if (!call.includes('fork_turns="none"')) {
|
|
79
|
+
missingForkTurns.push(call);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return missingForkTurns;
|
|
83
|
+
}
|
|
84
|
+
|
|
60
85
|
test("#given aggregate plugin manifest #when inspected #then it owns the omo namespace", async () => {
|
|
61
86
|
// given
|
|
62
87
|
const manifest = await readJson(".codex-plugin/plugin.json");
|
|
@@ -99,6 +124,7 @@ test("#given isolated components #when hooks are inspected #then commands stay i
|
|
|
99
124
|
"components/telemetry/dist/cli.js",
|
|
100
125
|
"components/ulw-loop/dist/cli.js",
|
|
101
126
|
"components/ultrawork/dist/cli.js",
|
|
127
|
+
"scripts/auto-update.mjs",
|
|
102
128
|
];
|
|
103
129
|
|
|
104
130
|
// then
|
|
@@ -106,6 +132,23 @@ test("#given isolated components #when hooks are inspected #then commands stay i
|
|
|
106
132
|
assert.match(text, new RegExp(marker.replaceAll("/", "\\/")));
|
|
107
133
|
}
|
|
108
134
|
assert.doesNotMatch(text, /codex-(comment-checker|lsp|rules|telemetry|ulw-loop|ultrawork)@/);
|
|
135
|
+
assert.equal(await exists("scripts/migrate-codex-config.mjs"), true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("#given aggregate PostCompact hooks #when hooks are inspected #then LSP diagnostics cache reset is registered", async () => {
|
|
139
|
+
// given
|
|
140
|
+
const hooks = await readJson("hooks/hooks.json");
|
|
141
|
+
|
|
142
|
+
// when
|
|
143
|
+
const lspPostCompactHooks = collectCommandHooks(hooks, "hooks/hooks.json").filter(
|
|
144
|
+
(hook) =>
|
|
145
|
+
hook.eventName === "PostCompact" &&
|
|
146
|
+
hook.handler.command === 'node "${PLUGIN_ROOT}/components/lsp/dist/cli.js" hook post-compact',
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// then
|
|
150
|
+
assert.equal(lspPostCompactHooks.length, 1);
|
|
151
|
+
assert.equal(lspPostCompactHooks[0]?.handler.statusMessage, "LazyCodex(0.1.0): Resetting LSP Diagnostics Cache");
|
|
109
152
|
});
|
|
110
153
|
|
|
111
154
|
test("#given aggregate hook commands #when inspected #then every command exposes a Codex status message", async () => {
|
|
@@ -172,6 +215,24 @@ test("#given aggregate OMO plugin is enabled #when hooks are inspected #then she
|
|
|
172
215
|
assert.deepEqual(preToolUseGroups.map((group) => group.matcher), ["^Bash$", "^create_goal$"]);
|
|
173
216
|
});
|
|
174
217
|
|
|
218
|
+
test("#given aggregate SessionStart hooks #when inspected #then LazyCodex auto-update is registered", async () => {
|
|
219
|
+
// given
|
|
220
|
+
const hooks = await readJson("hooks/hooks.json");
|
|
221
|
+
const text = JSON.stringify(hooks);
|
|
222
|
+
|
|
223
|
+
// when
|
|
224
|
+
const sessionStartCommands = collectCommandHooks(hooks, "hooks/hooks.json")
|
|
225
|
+
.filter(({ eventName }) => eventName === "SessionStart")
|
|
226
|
+
.map(({ handler }) => handler.command);
|
|
227
|
+
const autoUpdateGroup = hooks.hooks.SessionStart.find((group) => JSON.stringify(group).includes("scripts/auto-update.mjs"));
|
|
228
|
+
|
|
229
|
+
// then
|
|
230
|
+
assert.equal(autoUpdateGroup?.matcher, "^startup$");
|
|
231
|
+
assert.match(text, /scripts\/auto-update\.mjs/);
|
|
232
|
+
assert.match(text, /Checking Auto Update/);
|
|
233
|
+
assert(sessionStartCommands.some((command) => command.includes("scripts/auto-update.mjs")));
|
|
234
|
+
});
|
|
235
|
+
|
|
175
236
|
test("#given aggregate MCP config #when inspected #then code MCPs reference package runtimes without package names", async () => {
|
|
176
237
|
// given
|
|
177
238
|
const packageJson = await readJson("package.json");
|
|
@@ -208,25 +269,29 @@ test("#given aggregate MCP config #when inspected #then code MCPs reference pack
|
|
|
208
269
|
assert.deepEqual(componentLocalMcpSources, []);
|
|
209
270
|
});
|
|
210
271
|
|
|
211
|
-
test(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
...Object.keys(lspPackageJson.bin ?? {}),
|
|
220
|
-
...Object.keys(astGrepPackageJson.bin ?? {}),
|
|
221
|
-
...Object.keys(gitBashPackageJson.bin ?? {}),
|
|
222
|
-
].sort();
|
|
272
|
+
test(
|
|
273
|
+
"#given package-level MCP CLIs #when package metadata is inspected #then bin names use the omo prefix",
|
|
274
|
+
{ skip: mcpPackageManifestExists.some((exists) => !exists) },
|
|
275
|
+
async () => {
|
|
276
|
+
// given
|
|
277
|
+
const [lspPackageJson, astGrepPackageJson, gitBashPackageJson] = await Promise.all(
|
|
278
|
+
mcpPackageManifestPaths.map((path) => readJson(path)),
|
|
279
|
+
);
|
|
223
280
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
281
|
+
// when
|
|
282
|
+
const binNames = [
|
|
283
|
+
...Object.keys(lspPackageJson.bin ?? {}),
|
|
284
|
+
...Object.keys(astGrepPackageJson.bin ?? {}),
|
|
285
|
+
...Object.keys(gitBashPackageJson.bin ?? {}),
|
|
286
|
+
].sort();
|
|
287
|
+
|
|
288
|
+
// then
|
|
289
|
+
assert.deepEqual(binNames, ["omo-ast-grep", "omo-git-bash", "omo-lsp"]);
|
|
290
|
+
for (const name of binNames) {
|
|
291
|
+
assert.match(name, /^omo-/);
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
);
|
|
230
295
|
|
|
231
296
|
test("#given aggregate plugin build script #when inspected #then hook status and telemetry sync run before workspace builds", async () => {
|
|
232
297
|
// given
|
|
@@ -261,7 +326,13 @@ test("#given component directories #when scanned #then only intentional resource
|
|
|
261
326
|
const expectedComponentManifests = new Map([["rules", { hooks: "./hooks/hooks.json" }]]);
|
|
262
327
|
|
|
263
328
|
// when
|
|
264
|
-
const componentNames =
|
|
329
|
+
const componentNames = [];
|
|
330
|
+
for (const entry of components) {
|
|
331
|
+
if (!entry.isDirectory()) continue;
|
|
332
|
+
if (!(await exists(join("components", entry.name, "package.json")))) continue;
|
|
333
|
+
componentNames.push(entry.name);
|
|
334
|
+
}
|
|
335
|
+
componentNames.sort();
|
|
265
336
|
|
|
266
337
|
// then
|
|
267
338
|
assert.deepEqual(componentNames, [
|
|
@@ -315,6 +386,65 @@ test("#given bundled Codex agents #when components/ultrawork/agents directory is
|
|
|
315
386
|
}
|
|
316
387
|
});
|
|
317
388
|
|
|
389
|
+
test("#given planner agent prompt #when inspected #then generated artifacts stay under .omo", async () => {
|
|
390
|
+
const prompt = await readFile(join(root, "components", "ultrawork", "agents", "plan.toml"), "utf8");
|
|
391
|
+
|
|
392
|
+
assert.match(prompt, /\.omo\/plans\/<slug>\.md/);
|
|
393
|
+
assert.match(prompt, /\.omo\/evidence\/task-<N>-<slug>\.<ext>/);
|
|
394
|
+
assert.doesNotMatch(prompt, /(?<!\.omo\/)plans\/<slug>\.md/);
|
|
395
|
+
assert.doesNotMatch(prompt, /(?<!\.omo\/)evidence\/task-/);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("#given reviewer agent prompt #when inspected #then default model is ChatGPT-account compatible", async () => {
|
|
399
|
+
const prompt = await readFile(
|
|
400
|
+
join(root, "components", "ultrawork", "agents", "codex-ultrawork-reviewer.toml"),
|
|
401
|
+
"utf8",
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
assert.match(prompt, /^model\s*=\s*"gpt-5\.5"$/m);
|
|
405
|
+
assert.match(prompt, /^model_reasoning_effort\s*=\s*"xhigh"$/m);
|
|
406
|
+
assert.doesNotMatch(prompt, /^model\s*=\s*"gpt-5\.2"$/m);
|
|
407
|
+
assert.match(prompt, /ChatGPT account/);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("#given bundled model catalog #when inspected #then default verifier and worker roles are pinned", async () => {
|
|
411
|
+
const catalog = JSON.parse(await readFile(join(root, "model-catalog.json"), "utf8"));
|
|
412
|
+
|
|
413
|
+
assert.equal(catalog.current.model, "gpt-5.5");
|
|
414
|
+
assert.equal(catalog.current.model_context_window, 400000);
|
|
415
|
+
assert.equal(catalog.current.model_reasoning_effort, "high");
|
|
416
|
+
assert.equal(catalog.current.plan_mode_reasoning_effort, "xhigh");
|
|
417
|
+
assert.deepEqual(catalog.roles.default, catalog.current);
|
|
418
|
+
assert.deepEqual(catalog.roles.verifier, {
|
|
419
|
+
model: "gpt-5.5",
|
|
420
|
+
model_reasoning_effort: "xhigh",
|
|
421
|
+
});
|
|
422
|
+
assert.deepEqual(catalog.roles.worker, {
|
|
423
|
+
model: "gpt-5.4",
|
|
424
|
+
model_reasoning_effort: "high",
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("#given Codex-facing orchestration surfaces #when inspected #then retired ChatGPT-account model names are not recommended", async () => {
|
|
429
|
+
const promptFiles = [
|
|
430
|
+
join(root, "skills", "ulw-loop", "references", "full-workflow.md"),
|
|
431
|
+
join(root, "components", "ulw-loop", "skills", "ulw-loop", "references", "full-workflow.md"),
|
|
432
|
+
join(root, "components", "ultrawork", "README.md"),
|
|
433
|
+
join(root, "components", "ultrawork", "CHANGELOG.md"),
|
|
434
|
+
join(root, "components", "rules", "src", "post-compact-budget.ts"),
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
const staleReferences = [];
|
|
438
|
+
for (const promptPath of promptFiles) {
|
|
439
|
+
const content = await readFile(promptPath, "utf8");
|
|
440
|
+
if (/gpt-5\.(?:2|3-codex)/i.test(content)) {
|
|
441
|
+
staleReferences.push(`${basename(dirname(promptPath))}/${basename(promptPath)}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
assert.deepEqual(staleReferences, []);
|
|
446
|
+
});
|
|
447
|
+
|
|
318
448
|
test("#given synced skills with Codex compatibility guidance #when a bundled agent_type is referenced #then a matching TOML is bundled", async () => {
|
|
319
449
|
const skillsDir = join(root, "skills");
|
|
320
450
|
const skillEntries = await readdir(skillsDir, { withFileTypes: true });
|
|
@@ -343,3 +473,42 @@ test("#given synced skills with Codex compatibility guidance #when a bundled age
|
|
|
343
473
|
assert.equal(basename(tomlPath), `${agentType}.toml`);
|
|
344
474
|
}
|
|
345
475
|
});
|
|
476
|
+
|
|
477
|
+
test('#given synced skills and bundled rules #when role-specific agents are spawned #then they set fork_turns="none"', async () => {
|
|
478
|
+
const skillsDir = join(root, "skills");
|
|
479
|
+
const skillEntries = await readdir(skillsDir, { withFileTypes: true });
|
|
480
|
+
const promptFiles = skillEntries
|
|
481
|
+
.filter((entry) => entry.isDirectory())
|
|
482
|
+
.map((entry) => join(skillsDir, entry.name, "SKILL.md"));
|
|
483
|
+
promptFiles.push(join(root, "components", "rules", "bundled-rules", "hephaestus.md"));
|
|
484
|
+
|
|
485
|
+
const missingForkTurns = [];
|
|
486
|
+
for (const promptPath of promptFiles) {
|
|
487
|
+
const content = await readFile(promptPath, "utf8");
|
|
488
|
+
for (const call of findRoleSpecificSpawnsWithoutForkTurnsNone(content)) {
|
|
489
|
+
missingForkTurns.push(`${basename(dirname(promptPath))}/${basename(promptPath)}: ${call}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
assert.deepEqual(missingForkTurns, []);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("#given long-running orchestration prompts #when waiting on child agents #then parent liveness is surfaced", async () => {
|
|
497
|
+
const promptFiles = [
|
|
498
|
+
join(root, "skills", "ulw-loop", "SKILL.md"),
|
|
499
|
+
join(root, "skills", "ulw-loop", "references", "full-workflow.md"),
|
|
500
|
+
join(root, "skills", "review-work", "SKILL.md"),
|
|
501
|
+
join(root, "skills", "start-work", "SKILL.md"),
|
|
502
|
+
join(root, "components", "rules", "bundled-rules", "hephaestus.md"),
|
|
503
|
+
];
|
|
504
|
+
|
|
505
|
+
const missingLivenessGuidance = [];
|
|
506
|
+
for (const promptPath of promptFiles) {
|
|
507
|
+
const content = await readFile(promptPath, "utf8");
|
|
508
|
+
if (!content.includes("active subagent count") || !content.includes("last heartbeat")) {
|
|
509
|
+
missingLivenessGuidance.push(`${basename(dirname(promptPath))}/${basename(promptPath)}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
assert.deepEqual(missingLivenessGuidance, []);
|
|
514
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { resolveAutoUpdatePlan, runAutoUpdateCheck } from "../scripts/auto-update.mjs";
|
|
8
|
+
|
|
9
|
+
test("#given auto update is disabled #when resolving plan #then no command is scheduled", () => {
|
|
10
|
+
const plan = resolveAutoUpdatePlan({
|
|
11
|
+
env: { LAZYCODEX_AUTO_UPDATE_DISABLED: "1" },
|
|
12
|
+
now: 1_000,
|
|
13
|
+
lastCheckedAt: 0,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
assert.equal(plan.shouldRun, false);
|
|
17
|
+
assert.equal(plan.reason, "disabled");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("#given stale state #when resolving plan #then installer update command is scheduled", () => {
|
|
21
|
+
const plan = resolveAutoUpdatePlan({
|
|
22
|
+
env: {},
|
|
23
|
+
now: 90_000_000,
|
|
24
|
+
lastCheckedAt: 0,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.equal(plan.shouldRun, true);
|
|
28
|
+
assert.deepEqual(plan.command, "npx");
|
|
29
|
+
assert.deepEqual(plan.args, ["--yes", "lazycodex-ai@latest", "install", "--no-tui", "--skip-auth"]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("#given recent state #when resolving plan #then update is throttled", () => {
|
|
33
|
+
const plan = resolveAutoUpdatePlan({
|
|
34
|
+
env: {},
|
|
35
|
+
now: 90_000_000,
|
|
36
|
+
lastCheckedAt: 89_999_000,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
assert.equal(plan.shouldRun, false);
|
|
40
|
+
assert.equal(plan.reason, "throttled");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("#given test command override #when running check #then records state and launches command", async () => {
|
|
44
|
+
const root = await mkdtemp(join(tmpdir(), "lazycodex-auto-update-"));
|
|
45
|
+
const logPath = join(root, "spawn.log");
|
|
46
|
+
const statePath = join(root, "state.json");
|
|
47
|
+
const codexHome = join(root, "codex-home");
|
|
48
|
+
|
|
49
|
+
const result = await runAutoUpdateCheck({
|
|
50
|
+
env: {
|
|
51
|
+
CODEX_HOME: codexHome,
|
|
52
|
+
LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json"),
|
|
53
|
+
LAZYCODEX_AUTO_UPDATE_STATE_PATH: statePath,
|
|
54
|
+
LAZYCODEX_AUTO_UPDATE_INTERVAL_MS: "0",
|
|
55
|
+
LAZYCODEX_AUTO_UPDATE_COMMAND: process.execPath,
|
|
56
|
+
LAZYCODEX_AUTO_UPDATE_ARGS_JSON: JSON.stringify(["-e", `require("node:fs").writeFileSync(${JSON.stringify(logPath)}, "ok")`]),
|
|
57
|
+
LAZYCODEX_AUTO_UPDATE_WAIT: "1",
|
|
58
|
+
},
|
|
59
|
+
now: 123_456,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
assert.equal(result.started, true);
|
|
63
|
+
assert.equal(JSON.parse(await readFile(statePath, "utf8")).lastCheckedAt, 123_456);
|
|
64
|
+
assert.equal(await readFile(logPath, "utf8"), "ok");
|
|
65
|
+
assert.match(await readFile(join(codexHome, "config.toml"), "utf8"), /model = "gpt-5\.5"/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("#given active lock #when running check #then skips concurrent update", async () => {
|
|
69
|
+
const root = await mkdtemp(join(tmpdir(), "lazycodex-auto-update-lock-"));
|
|
70
|
+
const statePath = join(root, "state.json");
|
|
71
|
+
const lockPath = join(root, "state.json.lock");
|
|
72
|
+
const codexHome = join(root, "codex-home");
|
|
73
|
+
await writeFile(lockPath, "locked\n");
|
|
74
|
+
|
|
75
|
+
const result = await runAutoUpdateCheck({
|
|
76
|
+
env: {
|
|
77
|
+
CODEX_HOME: codexHome,
|
|
78
|
+
LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json"),
|
|
79
|
+
LAZYCODEX_AUTO_UPDATE_STATE_PATH: statePath,
|
|
80
|
+
LAZYCODEX_AUTO_UPDATE_LOCK_PATH: lockPath,
|
|
81
|
+
LAZYCODEX_AUTO_UPDATE_INTERVAL_MS: "0",
|
|
82
|
+
LAZYCODEX_AUTO_UPDATE_LOCK_STALE_MS: "600000",
|
|
83
|
+
},
|
|
84
|
+
now: 123_456,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
assert.equal(result.started, false);
|
|
88
|
+
assert.equal(result.reason, "locked");
|
|
89
|
+
assert.match(await readFile(join(codexHome, "config.toml"), "utf8"), /model_context_window = 400000/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("#given throttled updater and stale Codex config #when running check #then config migration still runs", async () => {
|
|
93
|
+
const root = await mkdtemp(join(tmpdir(), "lazycodex-auto-update-migration-"));
|
|
94
|
+
const statePath = join(root, "state.json");
|
|
95
|
+
const codexHome = join(root, "codex-home");
|
|
96
|
+
await writeFile(statePath, JSON.stringify({ lastCheckedAt: 99_999 }, null, 2));
|
|
97
|
+
await mkdir(codexHome, { recursive: true });
|
|
98
|
+
await writeFile(
|
|
99
|
+
join(codexHome, "config.toml"),
|
|
100
|
+
[
|
|
101
|
+
'model = "gpt-5.2"',
|
|
102
|
+
"model_context_window = 272000",
|
|
103
|
+
'model_reasoning_effort = "low"',
|
|
104
|
+
'plan_mode_reasoning_effort = "medium"',
|
|
105
|
+
"",
|
|
106
|
+
"[features]",
|
|
107
|
+
"plugins = true",
|
|
108
|
+
"",
|
|
109
|
+
].join("\n"),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const result = await runAutoUpdateCheck({
|
|
113
|
+
env: {
|
|
114
|
+
CODEX_HOME: codexHome,
|
|
115
|
+
LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json"),
|
|
116
|
+
LAZYCODEX_AUTO_UPDATE_STATE_PATH: statePath,
|
|
117
|
+
},
|
|
118
|
+
now: 100_000,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const content = await readFile(join(codexHome, "config.toml"), "utf8");
|
|
122
|
+
assert.equal(result.started, false);
|
|
123
|
+
assert.equal(result.reason, "throttled");
|
|
124
|
+
assert.match(content, /model = "gpt-5\.5"/);
|
|
125
|
+
assert.match(content, /model_context_window = 400000/);
|
|
126
|
+
assert.match(content, /model_reasoning_effort = "high"/);
|
|
127
|
+
assert.match(content, /plan_mode_reasoning_effort = "xhigh"/);
|
|
128
|
+
assert.doesNotMatch(content, /gpt-5\.2/);
|
|
129
|
+
});
|
|
@@ -15,6 +15,7 @@ const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
|
15
15
|
const AGGREGATE_EXPECTED_LABELS = new Map([
|
|
16
16
|
["hooks/hooks.json:SessionStart:0:0", "Loading Project Rules"],
|
|
17
17
|
["hooks/hooks.json:SessionStart:1:0", "Recording Session Telemetry"],
|
|
18
|
+
["hooks/hooks.json:SessionStart:2:0", "Checking Auto Update"],
|
|
18
19
|
["hooks/hooks.json:UserPromptSubmit:0:0", "Loading Project Rules"],
|
|
19
20
|
["hooks/hooks.json:UserPromptSubmit:1:0", "Checking Ultrawork Trigger"],
|
|
20
21
|
["hooks/hooks.json:UserPromptSubmit:2:0", "Checking Ulw-Loop Steering"],
|
|
@@ -23,6 +24,7 @@ const AGGREGATE_EXPECTED_LABELS = new Map([
|
|
|
23
24
|
["hooks/hooks.json:PostToolUse:0:1", "Checking LSP Diagnostics"],
|
|
24
25
|
["hooks/hooks.json:PostToolUse:1:0", "Matching Project Rules"],
|
|
25
26
|
["hooks/hooks.json:PostCompact:0:0", "Resetting Project Rule Cache"],
|
|
27
|
+
["hooks/hooks.json:PostCompact:2:0", "Resetting LSP Diagnostics Cache"],
|
|
26
28
|
["hooks/hooks.json:Stop:0:0", "Checking Start-Work Continuation"],
|
|
27
29
|
["hooks/hooks.json:SubagentStop:0:0", "Checking Start-Work Continuation"],
|
|
28
30
|
]);
|
|
@@ -30,6 +32,7 @@ const AGGREGATE_EXPECTED_LABELS = new Map([
|
|
|
30
32
|
const COMPONENT_EXPECTED_LABELS = new Map([
|
|
31
33
|
["components/comment-checker/hooks/hooks.json:PostToolUse:0:0", "Checking Comments"],
|
|
32
34
|
["components/lsp/hooks/hooks.json:PostToolUse:0:0", "Checking LSP Diagnostics"],
|
|
35
|
+
["components/lsp/hooks/hooks.json:PostCompact:0:0", "Resetting LSP Diagnostics Cache"],
|
|
33
36
|
["components/rules/hooks/hooks.json:SessionStart:0:0", "Loading Project Rules"],
|
|
34
37
|
["components/rules/hooks/hooks.json:UserPromptSubmit:0:0", "Loading Project Rules"],
|
|
35
38
|
["components/rules/hooks/hooks.json:PostToolUse:0:0", "Matching Project Rules"],
|
|
@@ -69,25 +72,6 @@ async function readComponentHookManifests() {
|
|
|
69
72
|
return manifests.sort((left, right) => left.source.localeCompare(right.source));
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
async function readComponentVersions() {
|
|
73
|
-
const components = await readdir(join(root, "components"), { withFileTypes: true });
|
|
74
|
-
const versions = new Map();
|
|
75
|
-
for (const entry of components) {
|
|
76
|
-
if (!entry.isDirectory()) continue;
|
|
77
|
-
const packageJson = await readJson(join("components", entry.name, "package.json"));
|
|
78
|
-
versions.set(entry.name, packageJson.version);
|
|
79
|
-
}
|
|
80
|
-
return versions;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function hookOwnerVersion(hook, aggregateVersion, componentVersions) {
|
|
84
|
-
const command = hook.command;
|
|
85
|
-
for (const [componentName, version] of componentVersions.entries()) {
|
|
86
|
-
if (command.includes(`/components/${componentName}/dist/cli.js`)) return version;
|
|
87
|
-
}
|
|
88
|
-
return aggregateVersion;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
75
|
function collectCommandHooks(hooks, source, version) {
|
|
92
76
|
const commandHooks = [];
|
|
93
77
|
const normalizedSource = source.replaceAll("\\", "/");
|
|
@@ -147,15 +131,15 @@ test("#given loose legacy status label #when normalizing #then removes OMO wordi
|
|
|
147
131
|
|
|
148
132
|
test("#given aggregate comment-checker hook #when status is inspected #then it uses LazyCodex comments label", async () => {
|
|
149
133
|
// given
|
|
134
|
+
const aggregateVersion = (await readJson(".codex-plugin/plugin.json")).version;
|
|
150
135
|
const aggregateHooks = await readJson("hooks/hooks.json");
|
|
151
|
-
const componentVersions = await readComponentVersions();
|
|
152
136
|
|
|
153
137
|
// when
|
|
154
|
-
const hooks = collectCommandHooks(aggregateHooks, "hooks/hooks.json",
|
|
138
|
+
const hooks = collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion);
|
|
155
139
|
const commentCheckerHook = hooks.find((hook) => hook.id === "hooks/hooks.json:PostToolUse:0:0");
|
|
156
140
|
|
|
157
141
|
// then
|
|
158
|
-
assert.equal(commentCheckerHook?.statusMessage, formatLazyCodexHookStatusMessage(
|
|
142
|
+
assert.equal(commentCheckerHook?.statusMessage, formatLazyCodexHookStatusMessage(aggregateVersion, "Checking Comments"));
|
|
159
143
|
assert.doesNotMatch(JSON.stringify(aggregateHooks), /checking\s+OMO\s+comments/i);
|
|
160
144
|
});
|
|
161
145
|
|
|
@@ -164,14 +148,10 @@ test("#given aggregate and component hooks #when status messages are inspected #
|
|
|
164
148
|
const aggregateVersion = (await readJson(".codex-plugin/plugin.json")).version;
|
|
165
149
|
const aggregateHooks = await readJson("hooks/hooks.json");
|
|
166
150
|
const componentManifests = await readComponentHookManifests();
|
|
167
|
-
const componentVersions = await readComponentVersions();
|
|
168
151
|
|
|
169
152
|
// when
|
|
170
153
|
const commandHooks = [
|
|
171
|
-
...collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion)
|
|
172
|
-
...hook,
|
|
173
|
-
version: hookOwnerVersion(hook, aggregateVersion, componentVersions),
|
|
174
|
-
})),
|
|
154
|
+
...collectCommandHooks(aggregateHooks, "hooks/hooks.json", aggregateVersion),
|
|
175
155
|
...componentManifests.flatMap((manifest) => collectCommandHooks(manifest.hooks, manifest.source, manifest.version)),
|
|
176
156
|
];
|
|
177
157
|
const expectedLabels = new Map([...AGGREGATE_EXPECTED_LABELS, ...COMPONENT_EXPECTED_LABELS]);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { ensureCodexReasoningConfig, migrateCodexConfig } from "../scripts/migrate-codex-config.mjs";
|
|
8
|
+
|
|
9
|
+
test("#given stale root reasoning config #when ensuring config #then replaces stale values without duplicate keys", () => {
|
|
10
|
+
const result = ensureCodexReasoningConfig(
|
|
11
|
+
[
|
|
12
|
+
'model = "gpt-5.2"',
|
|
13
|
+
"model_context_window = 272000",
|
|
14
|
+
'model_reasoning_effort = "low"',
|
|
15
|
+
'plan_mode_reasoning_effort = "medium"',
|
|
16
|
+
"",
|
|
17
|
+
"[features]",
|
|
18
|
+
"plugins = true",
|
|
19
|
+
"",
|
|
20
|
+
].join("\n"),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
assert.equal(result.match(/^model\s*=/gm)?.length, 1);
|
|
24
|
+
assert.equal(result.match(/^model_context_window\s*=/gm)?.length, 1);
|
|
25
|
+
assert.equal(result.match(/^model_reasoning_effort\s*=/gm)?.length, 1);
|
|
26
|
+
assert.equal(result.match(/^plan_mode_reasoning_effort\s*=/gm)?.length, 1);
|
|
27
|
+
assert.match(result, /model = "gpt-5\.5"/);
|
|
28
|
+
assert.match(result, /model_context_window = 400000/);
|
|
29
|
+
assert.match(result, /model_reasoning_effort = "high"/);
|
|
30
|
+
assert.match(result, /plan_mode_reasoning_effort = "xhigh"/);
|
|
31
|
+
assert.doesNotMatch(result, /gpt-5\.2/);
|
|
32
|
+
assert.match(result, /\[features\]/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("#given global and project-local stale Codex configs #when migrating #then both configs are forced to current defaults", async () => {
|
|
36
|
+
const root = await mkdtemp(join(tmpdir(), "lazycodex-config-migration-"));
|
|
37
|
+
const codexHome = join(root, "codex-home");
|
|
38
|
+
const project = join(root, "project", "nested");
|
|
39
|
+
const projectConfig = join(root, "project", ".codex", "config.toml");
|
|
40
|
+
await mkdir(codexHome, { recursive: true });
|
|
41
|
+
await mkdir(dirname(projectConfig), { recursive: true });
|
|
42
|
+
await writeFile(join(codexHome, "config.toml"), 'model = "gpt-5.2"\n');
|
|
43
|
+
await writeFile(projectConfig, 'model = "gpt-5.2"\nmodel_context_window = 272000\n');
|
|
44
|
+
|
|
45
|
+
const result = await migrateCodexConfig({
|
|
46
|
+
env: { CODEX_HOME: codexHome, LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json") },
|
|
47
|
+
cwd: project,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
assert.deepEqual(result.changed.sort(), [join(codexHome, "config.toml"), projectConfig].sort());
|
|
51
|
+
assert.match(await readFile(join(codexHome, "config.toml"), "utf8"), /model = "gpt-5\.5"/);
|
|
52
|
+
assert.match(await readFile(projectConfig, "utf8"), /model_context_window = 400000/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("#given user-customized Codex model config #when migrating #then user values are preserved", async () => {
|
|
56
|
+
const root = await mkdtemp(join(tmpdir(), "lazycodex-config-custom-"));
|
|
57
|
+
const codexHome = join(root, "codex-home");
|
|
58
|
+
await mkdir(codexHome, { recursive: true });
|
|
59
|
+
await writeFile(
|
|
60
|
+
join(codexHome, "config.toml"),
|
|
61
|
+
[
|
|
62
|
+
'model = "gpt-5.4"',
|
|
63
|
+
"model_context_window = 123456",
|
|
64
|
+
'model_reasoning_effort = "medium"',
|
|
65
|
+
'plan_mode_reasoning_effort = "medium"',
|
|
66
|
+
"",
|
|
67
|
+
].join("\n"),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const result = await migrateCodexConfig({
|
|
71
|
+
env: { CODEX_HOME: codexHome, LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json") },
|
|
72
|
+
cwd: root,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const content = await readFile(join(codexHome, "config.toml"), "utf8");
|
|
76
|
+
assert.deepEqual(result.changed, []);
|
|
77
|
+
assert.match(content, /model = "gpt-5\.4"/);
|
|
78
|
+
assert.match(content, /model_context_window = 123456/);
|
|
79
|
+
assert.match(content, /model_reasoning_effort = "medium"/);
|
|
80
|
+
assert.match(content, /plan_mode_reasoning_effort = "medium"/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("#given managed catalog state #when catalog version advances #then only previously managed config is updated", async () => {
|
|
84
|
+
const root = await mkdtemp(join(tmpdir(), "lazycodex-config-catalog-state-"));
|
|
85
|
+
const codexHome = join(root, "codex-home");
|
|
86
|
+
const catalogPath = join(root, "catalog.json");
|
|
87
|
+
const statePath = join(root, "model-state.json");
|
|
88
|
+
await mkdir(codexHome, { recursive: true });
|
|
89
|
+
await writeFile(
|
|
90
|
+
catalogPath,
|
|
91
|
+
JSON.stringify(
|
|
92
|
+
{
|
|
93
|
+
version: "test.v1",
|
|
94
|
+
current: {
|
|
95
|
+
model: "gpt-5.4",
|
|
96
|
+
model_context_window: 1000000,
|
|
97
|
+
model_reasoning_effort: "high",
|
|
98
|
+
plan_mode_reasoning_effort: "xhigh",
|
|
99
|
+
},
|
|
100
|
+
managedProfiles: [],
|
|
101
|
+
},
|
|
102
|
+
null,
|
|
103
|
+
2,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const first = await migrateCodexConfig({
|
|
108
|
+
env: {
|
|
109
|
+
CODEX_HOME: codexHome,
|
|
110
|
+
LAZYCODEX_MODEL_CATALOG_PATH: catalogPath,
|
|
111
|
+
LAZYCODEX_MODEL_CATALOG_STATE_PATH: statePath,
|
|
112
|
+
},
|
|
113
|
+
cwd: root,
|
|
114
|
+
});
|
|
115
|
+
await writeFile(
|
|
116
|
+
catalogPath,
|
|
117
|
+
JSON.stringify(
|
|
118
|
+
{
|
|
119
|
+
version: "test.v2",
|
|
120
|
+
current: {
|
|
121
|
+
model: "gpt-5.5",
|
|
122
|
+
model_context_window: 400000,
|
|
123
|
+
model_reasoning_effort: "high",
|
|
124
|
+
plan_mode_reasoning_effort: "xhigh",
|
|
125
|
+
},
|
|
126
|
+
managedProfiles: [],
|
|
127
|
+
},
|
|
128
|
+
null,
|
|
129
|
+
2,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
const second = await migrateCodexConfig({
|
|
133
|
+
env: {
|
|
134
|
+
CODEX_HOME: codexHome,
|
|
135
|
+
LAZYCODEX_MODEL_CATALOG_PATH: catalogPath,
|
|
136
|
+
LAZYCODEX_MODEL_CATALOG_STATE_PATH: statePath,
|
|
137
|
+
},
|
|
138
|
+
cwd: root,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const content = await readFile(join(codexHome, "config.toml"), "utf8");
|
|
142
|
+
assert.deepEqual(first.changed, [join(codexHome, "config.toml")]);
|
|
143
|
+
assert.deepEqual(second.changed, [join(codexHome, "config.toml")]);
|
|
144
|
+
assert.match(content, /model = "gpt-5\.5"/);
|
|
145
|
+
assert.match(content, /model_context_window = 400000/);
|
|
146
|
+
});
|