Problem Situation
The bundled OMO programming skill script check-no-excuse-rules.ts can fail when it is executed from the installed LazyCodex cache under ~/.codex, even when the caller project has typescript installed. This blocks the fallback "run the script directly or through an existing project runner" path for checking newly changed TypeScript files.
Reproduction Logs
/Users/bagjaeseog/winnation/node_modules/typescript exists and node -p "require.resolve('typescript')" resolves it, but the installed cached script still fails from the skill path:
$ bun run /Users/bagjaeseog/.codex/backups/bun-typescript-script-20260705-155429/check-no-excuse-rules.ts lib/donation/controller-game-triggers.ts lib/donation/controller-game-triggers.test.ts
error: Cannot find package 'typescript' from '/Users/bagjaeseog/.codex/backups/bun-typescript-script-20260705-155429/check-no-excuse-rules.ts'
Bun v1.3.14 (macOS arm64)
exit_code=1
Before the fix, the new regression test failed with the same error when it copied the script into a temporary ~/.codex cache path and ran it from a caller project that provided node_modules/typescript:
not ok 1 - #given TypeScript exists only in the caller project #when no-excuse runs from the bundled skill path #then it resolves that project dependency
error: Cannot find package 'typescript' from '/Users/bagjaeseog/.codex/lazycodex-no-excuse-typescript-zblfEN/check-no-excuse-rules.ts'
actual: 1
expected: 0
Root Cause
The script used a static ESM import:
import ts from "typescript"
When Bun executes the script from an installed ~/.codex/... skill cache path, that static import is resolved from the script/cache package context instead of reliably falling back to the caller project's dependency graph. The caller project can have a working typescript dependency, but the script still fails before it can inspect any target files.
Verified Fix
Load typescript through createRequire using two explicit resolution roots:
- the skill script location, for environments that bundle
typescript near the script;
process.cwd()/package.json, for the actual caller project being checked.
This keeps the change local to the TypeScript checker script and adds a regression test that exercises the installed-cache path.
diff --git a/plugins/omo/skills/programming/scripts/typescript/check-no-excuse-rules.ts b/plugins/omo/skills/programming/scripts/typescript/check-no-excuse-rules.ts
index 4ad7875..2476d2a 100644
--- a/plugins/omo/skills/programming/scripts/typescript/check-no-excuse-rules.ts
+++ b/plugins/omo/skills/programming/scripts/typescript/check-no-excuse-rules.ts
@@ -26,9 +26,36 @@
*/
import fs from "node:fs"
+import { createRequire } from "node:module"
import path from "node:path"
import process from "node:process"
-import ts from "typescript"
+
+const ts = loadTypeScript()
+
+function loadTypeScript(): typeof import("typescript") {
+ const requireCandidates = [
+ createRequire(import.meta.url),
+ createRequire(path.join(process.cwd(), "package.json")),
+ ]
+ let lastModuleResolutionError: unknown
+ for (const requireFrom of requireCandidates) {
+ try {
+ return requireFrom("typescript")
+ } catch (error) {
+ if (!isTypeScriptModuleResolutionError(error)) throw error
+ lastModuleResolutionError = error
+ }
+ }
+
+ console.error("Cannot find package 'typescript' from the skill path or current project.")
+ if (lastModuleResolutionError instanceof Error) console.error(lastModuleResolutionError.message)
+ process.exit(2)
+}
+
+function isTypeScriptModuleResolutionError(error: unknown): boolean {
+ const message = error instanceof Error ? error.message : String(error)
+ return message.includes("Cannot find module 'typescript'") || message.includes("Cannot find package 'typescript'")
+}
type RuleId =
| "no-any-assertion"
diff --git a/plugins/omo/test/no-excuse-typescript-resolution.test.mjs b/plugins/omo/test/no-excuse-typescript-resolution.test.mjs
new file mode 100644
index 0000000..abce615
--- /dev/null
+++ b/plugins/omo/test/no-excuse-typescript-resolution.test.mjs
@@ -0,0 +1,121 @@
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { homedir, tmpdir } from "node:os";
+import { join } from "node:path";
+import test from "node:test";
+
+import { root } from "./aggregate-plugin-fixture.mjs";
+
+const scriptPath = join(root, "skills", "programming", "scripts", "typescript", "check-no-excuse-rules.ts");
+
+function hasBun() {
+ return spawnSync("bun", ["--version"], { encoding: "utf8" }).status === 0;
+}
+
+function writeFakeTypeScriptPackage(projectRoot) {
+ const packageRoot = join(projectRoot, "node_modules", "typescript");
+ mkdirSync(packageRoot, { recursive: true });
+ writeFileSync(
+ join(packageRoot, "package.json"),
+ JSON.stringify({ name: "typescript", version: "0.0.0-test", main: "index.cjs" }),
+ );
+ writeFileSync(
+ join(packageRoot, "index.cjs"),
+ [
+ "function lineStarts(text) {",
+ " const starts = [0];",
+ " for (let index = 0; index < text.length; index += 1) {",
+ " if (text[index] === '\\n') starts.push(index + 1);",
+ " }",
+ " return starts;",
+ "}",
+ "function lineAndCharacter(text, position) {",
+ " const starts = lineStarts(text);",
+ " let line = 0;",
+ " for (let index = 0; index < starts.length; index += 1) {",
+ " if (starts[index] > position) break;",
+ " line = index;",
+ " }",
+ " return { line, character: position - starts[line] };",
+ "}",
+ "exports.ScriptTarget = { Latest: 'Latest' };",
+ "exports.SyntaxKind = { AnyKeyword: 1, ExportKeyword: 2 };",
+ "exports.NodeFlags = { Const: 1 };",
+ "exports.createSourceFile = function createSourceFile(fileName, text) {",
+ " return {",
+ " fileName,",
+ " text,",
+ " getEnd() { return text.length; },",
+ " getLineStarts() { return lineStarts(text); },",
+ " getLineAndCharacterOfPosition(position) { return lineAndCharacter(text, position); },",
+ " getStart() { return 0; },",
+ " };",
+ "};",
+ "exports.forEachChild = function forEachChild() {};",
+ "exports.getLeadingCommentRanges = function getLeadingCommentRanges() {};",
+ "exports.isAsExpression = function isAsExpression() { return false; };",
+ "exports.isEnumDeclaration = function isEnumDeclaration() { return false; };",
+ "exports.isNonNullExpression = function isNonNullExpression() { return false; };",
+ "exports.isThrowStatement = function isThrowStatement() { return false; };",
+ "exports.isStringLiteral = function isStringLiteral() { return false; };",
+ "exports.isNumericLiteral = function isNumericLiteral() { return false; };",
+ "exports.isNoSubstitutionTemplateLiteral = function isNoSubstitutionTemplateLiteral() { return false; };",
+ "exports.isTemplateExpression = function isTemplateExpression() { return false; };",
+ "exports.isVariableStatement = function isVariableStatement() { return false; };",
+ "exports.isTypeReferenceNode = function isTypeReferenceNode() { return false; };",
+ "exports.isParameter = function isParameter() { return false; };",
+ "exports.isVariableDeclaration = function isVariableDeclaration() { return false; };",
+ "exports.isPropertyDeclaration = function isPropertyDeclaration() { return false; };",
+ "exports.isPropertySignature = function isPropertySignature() { return false; };",
+ "exports.isFunctionDeclaration = function isFunctionDeclaration() { return false; };",
+ "exports.isMethodDeclaration = function isMethodDeclaration() { return false; };",
+ "exports.isArrowFunction = function isArrowFunction() { return false; };",
+ "exports.isFunctionExpression = function isFunctionExpression() { return false; };",
+ "exports.isCatchClause = function isCatchClause() { return false; };",
+ "",
+ ].join("\n"),
+ );
+}
+
+test("#given the no-excuse script source #when inspected #then TypeScript is resolved from the current project fallback", () => {
+ const script = readFileSync(scriptPath, "utf8");
+
+ assert.doesNotMatch(script, /import\s+ts\s+from\s+["']typescript["']/);
+ assert.match(script, /createRequire\(path\.join\(process\.cwd\(\), "package\.json"\)\)/);
+});
+
+test(
+ "#given TypeScript exists only in the caller project #when no-excuse runs from the bundled skill path #then it resolves that project dependency",
+ { skip: hasBun() ? false : "bun is required to execute the shipped TypeScript skill script" },
+ (t) => {
+ const codexRoot = join(homedir(), ".codex");
+ if (!existsSync(codexRoot)) {
+ t.skip("requires an existing ~/.codex cache root to reproduce the installed skill path");
+ return;
+ }
+ const tempRoot = mkdtempSync(join(tmpdir(), "lazycodex-no-excuse-typescript-"));
+ const installedRoot = mkdtempSync(join(codexRoot, "lazycodex-no-excuse-typescript-"));
+ try {
+ const projectRoot = join(tempRoot, "caller-project");
+ mkdirSync(projectRoot, { recursive: true });
+ writeFakeTypeScriptPackage(projectRoot);
+ const samplePath = join(projectRoot, "sample.ts");
+ const installedScriptPath = join(installedRoot, "check-no-excuse-rules.ts");
+ writeFileSync(samplePath, "export const value = 1;\n");
+ copyFileSync(scriptPath, installedScriptPath);
+
+ const result = spawnSync("bun", ["run", installedScriptPath, samplePath], {
+ cwd: projectRoot,
+ encoding: "utf8",
+ timeout: 30_000,
+ });
+
+ assert.equal(result.status, 0, `stderr: ${result.stderr}\nstdout: ${result.stdout}`);
+ assert.match(result.stdout, /No violations in 1 file\(s\)\./);
+ } finally {
+ rmSync(tempRoot, { recursive: true, force: true });
+ rmSync(installedRoot, { recursive: true, force: true });
+ }
+ },
+);
Verification
$ node --test plugins/omo/test/no-excuse-typescript-resolution.test.mjs
TAP version 13
# Subtest: \#given the no-excuse script source \#when inspected \#then TypeScript is resolved from the current project fallback
ok 1 - \#given the no-excuse script source \#when inspected \#then TypeScript is resolved from the current project fallback
---
duration_ms: 2.186292
type: 'test'
...
# Subtest: \#given TypeScript exists only in the caller project \#when no-excuse runs from the bundled skill path \#then it resolves that project dependency
ok 2 - \#given TypeScript exists only in the caller project \#when no-excuse runs from the bundled skill path \#then it resolves that project dependency
---
duration_ms: 232.933083
type: 'test'
...
1..2
# tests 2
# suites 0
# pass 2
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 334.079875
$ npm test
> lazycodex-ai@0.2.2 test
> node --test test/*.test.mjs
TAP version 13
# Subtest: GitHub workflows use the current Node and action major versions
ok 1 - GitHub workflows use the current Node and action major versions
---
duration_ms: 7.493542
type: 'test'
...
# Subtest: lazycodex-ai npm package
# Subtest: maps the package name and bin to lazycodex-ai
ok 1 - maps the package name and bin to lazycodex-ai
---
duration_ms: 0.344334
type: 'test'
...
# Subtest: keeps publish metadata aligned with the release version
ok 2 - keeps publish metadata aligned with the release version
---
duration_ms: 3.626375
type: 'test'
...
# Subtest: dry-runs install through oh-my-openagent with the Codex platform default
ok 3 - dry-runs install through oh-my-openagent with the Codex platform default
---
duration_ms: 48.518917
type: 'test'
...
# Subtest: dry-runs non-install commands through oh-my-openagent
ok 4 - dry-runs non-install commands through oh-my-openagent
---
duration_ms: 37.903209
type: 'test'
...
1..4
ok 2 - lazycodex-ai npm package
---
duration_ms: 90.971667
type: 'suite'
...
# Subtest: tracked text files use npx instead of the old package runner
ok 3 - tracked text files use npx instead of the old package runner
---
duration_ms: 1127.082209
type: 'test'
...
# Subtest: tracked text files do not contain Hangul
ok 4 - tracked text files do not contain Hangul
---
duration_ms: 1115.431
type: 'test'
...
# Subtest: public LazyCodex surfaces do not contain launch gating copy
ok 5 - public LazyCodex surfaces do not contain launch gating copy
---
duration_ms: 1113.977625
type: 'test'
...
# Subtest: README documents built-in LazyCodex workflows without Hangul
ok 6 - README documents built-in LazyCodex workflows without Hangul
---
duration_ms: 0.417458
type: 'test'
...
1..6
# tests 9
# suites 1
# pass 9
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 1195.218792
$ npm run pack:dry-run
> lazycodex-ai@0.2.2 pack:dry-run
> npm pack --dry-run
npm notice
npm notice 📦 lazycodex-ai@0.2.2
npm notice Tarball Contents
npm notice 1.1kB LICENSE
npm notice 12.1kB README.md
npm notice 767B bin/lazycodex-ai.js
npm notice 778B package.json
npm notice Tarball Details
npm notice name: lazycodex-ai
npm notice version: 0.2.2
npm notice filename: lazycodex-ai-0.2.2.tgz
npm notice package size: 6.6 kB
npm notice unpacked size: 14.7 kB
npm notice shasum: decef8d4f0f048e6c2b47bad55ca263c0acc945a
npm notice integrity: sha512-2XPp8haE/mwAC[...]NM9TFfdMSvwGw==
npm notice total files: 4
npm notice
lazycodex-ai-0.2.2.tgz
$ cd /Users/bagjaeseog/winnation && bun run installed-cache check-no-excuse-rules.ts ...
No violations in 2 file(s).
$ cd /Users/bagjaeseog/winnation && npm exec -- tsx installed-cache check-no-excuse-rules.ts ...
No violations in 2 file(s).
Additional local installed-cache verification after aligning the currently installed OMO cache script:
$ bun run /Users/bagjaeseog/.codex/plugins/cache/sisyphuslabs/omo/4.15.1/skills/programming/scripts/typescript/check-no-excuse-rules.ts /Users/bagjaeseog/.codex/plugins/cache/sisyphuslabs/omo/4.15.1/skills/programming/scripts/typescript/check-no-excuse-rules.ts
No violations in 1 file(s).
$ bun run /tmp/lazycodex-fix-no-excuse-typescript-SVM8j8/worktree/plugins/omo/skills/programming/scripts/typescript/check-no-excuse-rules.ts /tmp/lazycodex-fix-no-excuse-typescript-SVM8j8/worktree/plugins/omo/skills/programming/scripts/typescript/check-no-excuse-rules.ts
No violations in 1 file(s).
This fix was debugged, implemented, and verified with LazyCodex.
Tag: lazycodex-generated
Problem Situation
The bundled OMO programming skill script
check-no-excuse-rules.tscan fail when it is executed from the installed LazyCodex cache under~/.codex, even when the caller project hastypescriptinstalled. This blocks the fallback "run the script directly or through an existing project runner" path for checking newly changed TypeScript files.Reproduction Logs
/Users/bagjaeseog/winnation/node_modules/typescriptexists andnode -p "require.resolve('typescript')"resolves it, but the installed cached script still fails from the skill path:Before the fix, the new regression test failed with the same error when it copied the script into a temporary
~/.codexcache path and ran it from a caller project that providednode_modules/typescript:Root Cause
The script used a static ESM import:
When Bun executes the script from an installed
~/.codex/...skill cache path, that static import is resolved from the script/cache package context instead of reliably falling back to the caller project's dependency graph. The caller project can have a workingtypescriptdependency, but the script still fails before it can inspect any target files.Verified Fix
Load
typescriptthroughcreateRequireusing two explicit resolution roots:typescriptnear the script;process.cwd()/package.json, for the actual caller project being checked.This keeps the change local to the TypeScript checker script and adds a regression test that exercises the installed-cache path.
Verification
Additional local installed-cache verification after aligning the currently installed OMO cache script:
This fix was debugged, implemented, and verified with LazyCodex.
Tag: lazycodex-generated