oh-my-codex 0.21.22 → 0.21.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.js +24 -0
  4. package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.js.map +1 -1
  5. package/dist/cli/__tests__/setup-install-mode.test.js +210 -2
  6. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  7. package/dist/cli/__tests__/team-decompose.test.js +207 -0
  8. package/dist/cli/__tests__/team-decompose.test.js.map +1 -1
  9. package/dist/cli/plugin-marketplace.d.ts +3 -0
  10. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  11. package/dist/cli/plugin-marketplace.js +103 -26
  12. package/dist/cli/plugin-marketplace.js.map +1 -1
  13. package/dist/cli/setup.d.ts.map +1 -1
  14. package/dist/cli/setup.js +2 -4
  15. package/dist/cli/setup.js.map +1 -1
  16. package/dist/cli/team.d.ts +2 -6
  17. package/dist/cli/team.d.ts.map +1 -1
  18. package/dist/cli/team.js +44 -24
  19. package/dist/cli/team.js.map +1 -1
  20. package/dist/team/__tests__/completion-evidence.test.js +207 -0
  21. package/dist/team/__tests__/completion-evidence.test.js.map +1 -1
  22. package/dist/team/__tests__/repo-aware-decomposition.test.js +61 -0
  23. package/dist/team/__tests__/repo-aware-decomposition.test.js.map +1 -1
  24. package/dist/team/__tests__/task-evidence-intent.test.d.ts +2 -0
  25. package/dist/team/__tests__/task-evidence-intent.test.d.ts.map +1 -0
  26. package/dist/team/__tests__/task-evidence-intent.test.js +184 -0
  27. package/dist/team/__tests__/task-evidence-intent.test.js.map +1 -0
  28. package/dist/team/repo-aware-decomposition.d.ts.map +1 -1
  29. package/dist/team/repo-aware-decomposition.js +21 -12
  30. package/dist/team/repo-aware-decomposition.js.map +1 -1
  31. package/dist/team/task-evidence-intent.d.ts +6 -0
  32. package/dist/team/task-evidence-intent.d.ts.map +1 -0
  33. package/dist/team/task-evidence-intent.js +111 -0
  34. package/dist/team/task-evidence-intent.js.map +1 -0
  35. package/package.json +1 -1
  36. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
@@ -10,12 +10,13 @@ import { decideWindowsNativeHookShimReference, setNativeHookClaimJournalDurabili
10
10
  import { resolveSetupRefreshArgs } from "../update.js";
11
11
  import { uninstall } from "../uninstall.js";
12
12
  import { OMX_DEVELOPER_INSTRUCTIONS, OMX_PLUGIN_DEVELOPER_INSTRUCTIONS, } from "../../config/generator.js";
13
- import { materializePackagedOmxPluginCache, registerOmxPluginCacheLivePin, resolvePackagedOmxMarketplace, computeOmxPluginCacheClaimDigest, } from "../plugin-marketplace.js";
13
+ import { materializePackagedOmxPluginCache, registerOmxPluginCacheLivePin, resolvePackagedOmxMarketplace, computeOmxPluginCacheClaimDigest, setPluginCacheMutationHooksForTest, } from "../plugin-marketplace.js";
14
14
  import { buildManagedCodexHookTrustState, buildManagedCodexHooksConfig, buildManagedCodexNativeHookWindowsShimContent, buildManagedCodexNativeHookWindowsShimPath, buildManagedCodexNativeHookWindowsShimPathForFilesystem, } from "../../config/codex-hooks.js";
15
15
  const packageRoot = process.cwd();
16
16
  async function writeBoundCompletionMarker(cacheDir) {
17
17
  const claimDigest = await computeOmxPluginCacheClaimDigest(cacheDir);
18
- await writeFile(join(cacheDir, ".omx-complete"), `${JSON.stringify({ claimDigest })}\n`);
18
+ const version = basename(cacheDir);
19
+ await writeFile(join(cacheDir, ".omx-complete"), `${JSON.stringify({ version, claimDigest })}\n`);
19
20
  }
20
21
  let previousPathForFakeCodex;
21
22
  let fakeCodexBinDir = null;
@@ -481,6 +482,15 @@ async function seedSameVersionPluginCacheWithStaleHooks(codexHomeDir) {
481
482
  assert.ok(preToolUse, "expected packaged plugin PreToolUse hook fixture");
482
483
  preToolUse.matcher = "Bash";
483
484
  await writeFile(hooksPath, JSON.stringify(hooks, null, 2) + "\n");
485
+ const manifest = JSON.parse(await readFile(join(packageRoot, "plugins", "oh-my-codex", ".codex-plugin", "plugin.json"), "utf-8"));
486
+ const ownershipRecord = {
487
+ version: 1,
488
+ cacheVersion: manifest.version,
489
+ pid: 99999999,
490
+ processStartHash: null,
491
+ livePinSupportVersion: 1,
492
+ };
493
+ await writeFile(join(cacheDir, ".omx-managed"), `${JSON.stringify(ownershipRecord)}\n`);
484
494
  await writeBoundCompletionMarker(cacheDir);
485
495
  return cacheDir;
486
496
  }
@@ -491,6 +501,15 @@ async function seedSameVersionPluginCacheWithStaleLauncher(codexHomeDir) {
491
501
  recursive: true,
492
502
  });
493
503
  await writeFile(join(cacheDir, "hooks", "omx-command.json"), JSON.stringify({ command: "/stale/node", argsPrefix: ["/stale/omx.js"] }, null, 2) + "\n");
504
+ const manifest = JSON.parse(await readFile(join(packageRoot, "plugins", "oh-my-codex", ".codex-plugin", "plugin.json"), "utf-8"));
505
+ const ownershipRecord = {
506
+ version: 1,
507
+ cacheVersion: manifest.version,
508
+ pid: 99999999,
509
+ processStartHash: null,
510
+ livePinSupportVersion: 1,
511
+ };
512
+ await writeFile(join(cacheDir, ".omx-managed"), `${JSON.stringify(ownershipRecord)}\n`);
494
513
  await writeBoundCompletionMarker(cacheDir);
495
514
  return cacheDir;
496
515
  }
@@ -1191,6 +1210,158 @@ describe("omx setup install mode behavior", () => {
1191
1210
  await rm(wd, { recursive: true, force: true });
1192
1211
  }
1193
1212
  });
1213
+ it("asserts removalSafe field values for cache repair outcomes", async () => {
1214
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-removalSafe-"));
1215
+ try {
1216
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
1217
+ await withTempCwd(wd, async () => {
1218
+ const packagedMarketplace = await resolvePackagedOmxMarketplace(packageRoot);
1219
+ assert.ok(packagedMarketplace);
1220
+ // Test 1: untrusted/symlink cache - removalSafe should be false
1221
+ const cacheBaseDir = await packagedPluginCacheDir(codexHomeDir);
1222
+ await mkdir(dirname(cacheBaseDir), { recursive: true });
1223
+ const externalCacheDir = join(wd, "external-cache");
1224
+ await mkdir(externalCacheDir, { recursive: true });
1225
+ await symlink(externalCacheDir, cacheBaseDir);
1226
+ const untrustedResult = await materializePackagedOmxPluginCache(codexHomeDir, packagedMarketplace);
1227
+ assert.equal(untrustedResult.status, "stale-launcher");
1228
+ assert.match(untrustedResult.reason ?? "", /symlink or non-directory entry/);
1229
+ assert.equal(untrustedResult.removalSafe, false, "untrusted symlink should have removalSafe=false");
1230
+ // Test 2a: late-pin - pin created after inspection but before removal
1231
+ const wd2a = await mkdtemp(join(tmpdir(), "omx-setup-late-pin-"));
1232
+ try {
1233
+ await withIsolatedUserHome(wd2a, async (codexHomeDir2a) => {
1234
+ await withTempCwd(wd2a, async () => {
1235
+ // Setup: create a valid managed cache
1236
+ const cacheDir = await seedSameVersionPluginCacheWithStaleHooks(codexHomeDir2a);
1237
+ await writeBoundCompletionMarker(cacheDir);
1238
+ // Setup: inject a hook that creates a live pin AFTER inspection but BEFORE actual removal
1239
+ let hookCalled = false;
1240
+ const originalRestore = setPluginCacheMutationHooksForTest({
1241
+ beforeRemoveSyscall: async (path) => {
1242
+ // Simulate late-pin: create a live pin after the initial checks
1243
+ // Run the hook only once, the first time it's called
1244
+ if (!hookCalled) {
1245
+ hookCalled = true;
1246
+ const { createHash } = await import("node:crypto");
1247
+ const sessionHash = createHash("sha256").update("test-late-pin-session").digest("hex");
1248
+ const pinDir = join(path, ".omx-live-pin");
1249
+ await mkdir(pinDir, { recursive: true });
1250
+ const pinRecord = {
1251
+ version: 1,
1252
+ pid: process.pid,
1253
+ processStartHash: null,
1254
+ sessionHash,
1255
+ };
1256
+ const pinFilePath = join(pinDir, `session-${sessionHash}.json`);
1257
+ await writeFile(pinFilePath, `${JSON.stringify(pinRecord)}\n`);
1258
+ }
1259
+ },
1260
+ });
1261
+ try {
1262
+ const latePinResult = await materializePackagedOmxPluginCache(codexHomeDir2a, packagedMarketplace, { repairCache: true });
1263
+ assert.equal(latePinResult.status, "stale-launcher", "(a) late-pin should result in stale-launcher status");
1264
+ assert.equal(hookCalled, true, "(a) late-pin: pin must be created after inspection, inside the removal path");
1265
+ assert.match(latePinResult.reason ?? "", /pinned by a live Codex session|became pinned or changed during repair/, "(a) late-pin: should cite pin/change reason");
1266
+ assert.equal(latePinResult.removalSafe, false, "(a) late-pin should have removalSafe=false");
1267
+ // Verify snapshot remains untouched
1268
+ assert.equal(existsSync(cacheDir), true, "(a) late-pin: snapshot should remain untouched");
1269
+ }
1270
+ finally {
1271
+ originalRestore();
1272
+ }
1273
+ });
1274
+ });
1275
+ }
1276
+ finally {
1277
+ await rm(wd2a, { recursive: true, force: true });
1278
+ }
1279
+ // Test 2b: live-pin present before repair
1280
+ const wd2b = await mkdtemp(join(tmpdir(), "omx-setup-live-pin-"));
1281
+ try {
1282
+ await withIsolatedUserHome(wd2b, async (codexHomeDir2b) => {
1283
+ await withTempCwd(wd2b, async () => {
1284
+ // Setup: create a managed cache with a stale launcher and a live pin
1285
+ // This ensures the test exercises the repair eligibility path (not the unchanged fast path)
1286
+ const cacheDir = await seedSameVersionPluginCacheWithStaleLauncher(codexHomeDir2b);
1287
+ await registerOmxPluginCacheLivePin(cacheDir, "test-live-pin-session", process.pid);
1288
+ const livePinResult = await materializePackagedOmxPluginCache(codexHomeDir2b, packagedMarketplace, { repairCache: true });
1289
+ assert.equal(livePinResult.status, "stale-launcher", "(b) live-pin should result in stale-launcher status");
1290
+ assert.match(livePinResult.reason ?? "", /pinned by a live Codex session/, "(b) live-pin: should cite live session reason");
1291
+ assert.equal(livePinResult.removalSafe, false, "(b) live-pin should have removalSafe=false");
1292
+ });
1293
+ });
1294
+ }
1295
+ finally {
1296
+ await rm(wd2b, { recursive: true, force: true });
1297
+ }
1298
+ // Test 2c: safe repair - no pins, successful removal
1299
+ const wd2c = await mkdtemp(join(tmpdir(), "omx-setup-safe-repair-"));
1300
+ try {
1301
+ await withIsolatedUserHome(wd2c, async (codexHomeDir2c) => {
1302
+ await withTempCwd(wd2c, async () => {
1303
+ // Setup: create a stale cache that can be safely removed
1304
+ const cacheDir = await seedSameVersionPluginCacheWithStaleHooks(codexHomeDir2c);
1305
+ await writeBoundCompletionMarker(cacheDir);
1306
+ const safeRepairResult = await materializePackagedOmxPluginCache(codexHomeDir2c, packagedMarketplace, { repairCache: true });
1307
+ assert.equal(safeRepairResult.status, "materialized", "(c) safe repair should succeed and materialize");
1308
+ // removalSafe should be undefined or true (treated as safe)
1309
+ assert.notEqual(safeRepairResult.removalSafe, false, "(c) safe repair should NOT have removalSafe=false");
1310
+ });
1311
+ });
1312
+ }
1313
+ finally {
1314
+ await rm(wd2c, { recursive: true, force: true });
1315
+ }
1316
+ });
1317
+ });
1318
+ }
1319
+ finally {
1320
+ await rm(wd, { recursive: true, force: true });
1321
+ }
1322
+ });
1323
+ it("does not suggest codex plugin remove for dry-run repair with live pin", async () => {
1324
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-dry-run-live-pin-"));
1325
+ try {
1326
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
1327
+ await withTempCwd(wd, async () => {
1328
+ await mkdir(join(wd, ".omx"), { recursive: true });
1329
+ const hudConfigPath = join(wd, ".omx", "hud-config.json");
1330
+ const userHudConfig = '{"preset":"custom"}\n';
1331
+ await writeFile(hudConfigPath, userHudConfig);
1332
+ const packagedMarketplace = await resolvePackagedOmxMarketplace(packageRoot);
1333
+ assert.ok(packagedMarketplace);
1334
+ const initial = await materializePackagedOmxPluginCache(codexHomeDir, packagedMarketplace);
1335
+ assert.equal(initial.status, "materialized");
1336
+ const cacheDir = initial.cacheDir;
1337
+ // Register a live pin
1338
+ await registerOmxPluginCacheLivePin(cacheDir, "dry-run-live-session", process.pid);
1339
+ // Run setup with dry-run and repair on byte-identical cache with live pin
1340
+ // The cache is unchanged (byte-identical), so dry-run should return unchanged, not report removal
1341
+ const dryRunOutput = await captureConsoleOutput(async () => {
1342
+ await setup({
1343
+ scope: "user",
1344
+ installMode: "plugin",
1345
+ repairPluginCache: true,
1346
+ dryRun: true,
1347
+ pluginAgentsMdPrompt: async () => false,
1348
+ codexFeaturesProbe: () => "hooks stable true\nplugin_hooks experimental true\ngoals experimental true\n",
1349
+ codexVersionProbe: () => "codex-cli 0.999.0",
1350
+ });
1351
+ });
1352
+ // Verify that removal was NOT suggested (because cache is unchanged)
1353
+ assert.doesNotMatch(dryRunOutput, /codex plugin remove/);
1354
+ // Verify that "close that session" advice is NOT printed (because cache is unchanged, no repair needed)
1355
+ assert.doesNotMatch(dryRunOutput, /close that session before repairing it/);
1356
+ // Cache should report as already correct
1357
+ assert.match(dryRunOutput, /Local Codex plugin cache already exposes packaged OMX skills/);
1358
+ });
1359
+ });
1360
+ }
1361
+ finally {
1362
+ await rm(wd, { recursive: true, force: true });
1363
+ }
1364
+ });
1194
1365
  it("rejects scoped plugin-cache repair outside plugin setup mode", async () => {
1195
1366
  const wd = await mkdtemp(join(tmpdir(), "omx-setup-plugin-cache-mode-"));
1196
1367
  try {
@@ -1211,6 +1382,43 @@ describe("omx setup install mode behavior", () => {
1211
1382
  await rm(wd, { recursive: true, force: true });
1212
1383
  }
1213
1384
  });
1385
+ it("verifies setup output uses removalSafe field to gate 'codex plugin remove' advice", async () => {
1386
+ // Test that setup.ts checks removalSafe !== false before advising 'codex plugin remove'
1387
+ const wd = await mkdtemp(join(tmpdir(), "omx-setup-removal-gate-"));
1388
+ try {
1389
+ await withIsolatedUserHome(wd, async (codexHomeDir) => {
1390
+ await withTempCwd(wd, async () => {
1391
+ const packagedMarketplace = await resolvePackagedOmxMarketplace(packageRoot);
1392
+ assert.ok(packagedMarketplace);
1393
+ // (d-i) Verify that removalSafe=false (untrusted symlink cache) does NOT output 'codex plugin remove' advice
1394
+ const cacheBaseDir = await packagedPluginCacheDir(codexHomeDir);
1395
+ await mkdir(dirname(cacheBaseDir), { recursive: true });
1396
+ const externalCacheDir = join(wd, "external-cache");
1397
+ await mkdir(externalCacheDir, { recursive: true });
1398
+ await symlink(externalCacheDir, cacheBaseDir);
1399
+ const unsafeOutput = await captureConsoleOutput(async () => {
1400
+ await setup({ scope: "user", installMode: "plugin" });
1401
+ });
1402
+ // When removalSafe=false, the setup command SHOULD NOT output 'codex plugin remove' advice
1403
+ assert.match(unsafeOutput, /symlink or non-directory entry/);
1404
+ assert.doesNotMatch(unsafeOutput, /codex plugin remove/, "(d-i) untrusted symlink cache should not suggest 'codex plugin remove'");
1405
+ // (d-ii) Verify that safe repair (dead launcher) DOES output 'codex plugin remove' advice
1406
+ await rm(codexHomeDir, { recursive: true, force: true });
1407
+ await mkdir(dirname(codexHomeDir), { recursive: true });
1408
+ const cacheDir = await seedSameVersionPluginCacheWithStaleLauncher(codexHomeDir);
1409
+ const safeOutput = await captureConsoleOutput(async () => {
1410
+ await setup({ scope: "user", installMode: "plugin" });
1411
+ });
1412
+ // When removalSafe !== false (safe repair for dead launcher), the setup command SHOULD output 'codex plugin remove' advice
1413
+ assert.match(safeOutput, /pinned launcher command does not exist/);
1414
+ assert.match(safeOutput, /codex plugin remove oh-my-codex@oh-my-codex-local --json/, "(d-ii) dead launcher safe repair should suggest 'codex plugin remove'");
1415
+ });
1416
+ });
1417
+ }
1418
+ finally {
1419
+ await rm(wd, { recursive: true, force: true });
1420
+ }
1421
+ });
1214
1422
  it("refuses to replace an already published plugin cache root", async () => {
1215
1423
  const wd = await mkdtemp(join(tmpdir(), "omx-setup-install-mode-"));
1216
1424
  try {