thoth-agents 0.3.11 → 0.3.12

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 (42) hide show
  1. package/README.md +119 -33
  2. package/dist/{chunk-CB3EBA7H.js → chunk-5UBJB5QO.js} +1 -1
  3. package/dist/{chunk-GIUU6FNE.js → chunk-DX4U3AD3.js} +95 -50
  4. package/dist/{chunk-ZEAAWBF2.js → chunk-VPVBYBPU.js} +575 -98
  5. package/dist/cli/codex-plugin-install.d.ts +16 -2
  6. package/dist/cli/index.js +7 -5
  7. package/dist/cli/tui/index.js +2 -2
  8. package/dist/harness/core/agent-pack.d.ts +26 -6
  9. package/dist/harness/core/sdd.d.ts +27 -15
  10. package/dist/harness/writers/codex-plugin-package.d.ts +1 -0
  11. package/dist/index.js +2 -2
  12. package/package.json +4 -4
  13. package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +2 -2
  14. package/plugin/.claude-plugin/plugin.json +1 -1
  15. package/plugin/.codex-plugin/plugin.json +1 -1
  16. package/plugin/agents/oracle.md +3 -3
  17. package/plugin/agents/orchestrator.md +35 -30
  18. package/plugin/skills/plan-reviewer/SKILL.md +54 -15
  19. package/plugin/skills/thoth-archive/SKILL.md +5 -0
  20. package/plugin/skills/thoth-constitution/SKILL.md +5 -0
  21. package/plugin/skills/thoth-constitution/templates/constitution.md +19 -2
  22. package/plugin/skills/thoth-init/SKILL.md +5 -0
  23. package/plugin/skills/thoth-sdd/SKILL.md +35 -10
  24. package/plugin/skills/thoth-sdd/references/phases/implement.md +13 -2
  25. package/plugin/skills/thoth-sdd/references/phases/tasks.md +23 -2
  26. package/plugin/skills/thoth-sdd/references/phases/verify.md +7 -5
  27. package/plugin/skills/thoth-sdd/scripts/validate.mjs +263 -30
  28. package/plugin/skills/thoth-sdd/templates/tasks.md +18 -2
  29. package/skills/README.md +2 -1
  30. package/skills/plan-reviewer/SKILL.md +54 -15
  31. package/skills/thoth-archive/SKILL.md +5 -0
  32. package/skills/thoth-constitution/SKILL.md +5 -0
  33. package/skills/thoth-constitution/templates/constitution.md +19 -2
  34. package/skills/thoth-init/SKILL.md +5 -0
  35. package/skills/thoth-sdd/SKILL.md +35 -10
  36. package/skills/thoth-sdd/references/phases/implement.md +13 -2
  37. package/skills/thoth-sdd/references/phases/tasks.md +23 -2
  38. package/skills/thoth-sdd/references/phases/verify.md +7 -5
  39. package/skills/thoth-sdd/scripts/validate.mjs +263 -30
  40. package/skills/thoth-sdd/templates/tasks.md +18 -2
  41. package/.agents/plugins/marketplace.json +0 -20
  42. package/.claude-plugin/marketplace.json +0 -21
@@ -35,7 +35,7 @@ import {
35
35
  updateOpenCodeMainConfig,
36
36
  writeConfig,
37
37
  writeLiteConfig
38
- } from "./chunk-GIUU6FNE.js";
38
+ } from "./chunk-DX4U3AD3.js";
39
39
 
40
40
  // src/cli/codex-paths.ts
41
41
  import { homedir } from "os";
@@ -638,8 +638,8 @@ function stripProviderMcp(content) {
638
638
  }
639
639
  function pluginRootReference(pathValue) {
640
640
  const normalized = normalizePath(pathValue);
641
- const relative = normalized.slice(".codex-plugin/".length);
642
- return `./${relative}`;
641
+ const relative2 = normalized.slice(".codex-plugin/".length);
642
+ return `./${relative2}`;
643
643
  }
644
644
  function sha256(content) {
645
645
  return `sha256:${createHash("sha256").update(content).digest("hex")}`;
@@ -692,7 +692,7 @@ function codexHookPackageDiagnostic(code, message, severity = "warning") {
692
692
  function normalizeHookDefinition(hook) {
693
693
  return {
694
694
  command: hook.handler.command,
695
- ...hook.outputFields?.length ? { output: [...hook.outputFields].sort() } : {}
695
+ type: "command"
696
696
  };
697
697
  }
698
698
  function renderHookDefinitions(hookDefinitions) {
@@ -705,7 +705,10 @@ function renderHookDefinitions(hookDefinitions) {
705
705
  continue;
706
706
  }
707
707
  const eventHooks = hooksByEvent.get(validation.event) ?? [];
708
- eventHooks.push(normalizeHookDefinition(hook));
708
+ eventHooks.push({
709
+ ...hook.matcher !== void 0 ? { matcher: hook.matcher } : {},
710
+ hooks: [normalizeHookDefinition(hook)]
711
+ });
709
712
  hooksByEvent.set(validation.event, eventHooks);
710
713
  }
711
714
  if (hooksByEvent.size === 0) {
@@ -718,11 +721,11 @@ function renderHookDefinitions(hookDefinitions) {
718
721
  );
719
722
  return { diagnostics };
720
723
  }
721
- const content = stableJson(
722
- Object.fromEntries(
724
+ const content = stableJson({
725
+ hooks: Object.fromEntries(
723
726
  [...hooksByEvent.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([event, hooks]) => [event, hooks.map(stableValue)])
724
727
  )
725
- );
728
+ });
726
729
  return { content, diagnostics };
727
730
  }
728
731
  function parseRawHookContent(content) {
@@ -753,37 +756,66 @@ function parseRawHookContent(content) {
753
756
  ]
754
757
  };
755
758
  }
756
- for (const [event, definitions] of Object.entries(parsed)) {
759
+ if (!isRecord(parsed.hooks)) {
760
+ return {
761
+ hookDefinitions,
762
+ diagnostics: [
763
+ codexHookPackageDiagnostic(
764
+ "codex.plugin.hooks.invalid_shape",
765
+ 'Skipping Codex plugin hooks asset because hooks.json must contain a nested "hooks" object.'
766
+ )
767
+ ]
768
+ };
769
+ }
770
+ for (const [event, definitions] of Object.entries(parsed.hooks)) {
757
771
  if (!Array.isArray(definitions)) {
758
772
  diagnostics.push(
759
773
  codexHookPackageDiagnostic(
760
774
  "codex.plugin.hooks.invalid_shape",
761
- `Skipping Codex plugin hook event "${event}" because its value is not an array of command handlers.`
775
+ `Skipping Codex plugin hook event "${event}" because its value is not an array of matcher groups.`
762
776
  )
763
777
  );
764
778
  continue;
765
779
  }
766
- for (const definition of definitions) {
767
- if (!isRecord(definition)) {
780
+ for (const group of definitions) {
781
+ if (!isRecord(group) || !Array.isArray(group.hooks)) {
768
782
  diagnostics.push(
769
783
  codexHookPackageDiagnostic(
770
784
  "codex.plugin.hooks.invalid_shape",
771
- `Skipping Codex plugin hook event "${event}" entry because it is not an object.`
785
+ `Skipping Codex plugin hook event "${event}" entry because it is not a matcher group with a hooks array.`
772
786
  )
773
787
  );
774
788
  continue;
775
789
  }
776
- const output = definition.output;
777
- hookDefinitions.push({
778
- event,
779
- handler: {
780
- type: typeof definition.type === "string" ? definition.type : "command",
781
- command: definition.command,
782
- async: definition.async
783
- },
784
- outputFields: Array.isArray(output) ? output.filter((field) => typeof field === "string") : void 0,
785
- interceptsToolExecution: definition.interceptsToolExecution === true
786
- });
790
+ if (group.matcher !== void 0 && typeof group.matcher !== "string") {
791
+ diagnostics.push(
792
+ codexHookPackageDiagnostic(
793
+ "codex.plugin.hooks.invalid_shape",
794
+ `Skipping Codex plugin hook event "${event}" matcher group because matcher is not a string.`
795
+ )
796
+ );
797
+ continue;
798
+ }
799
+ for (const definition of group.hooks) {
800
+ if (!isRecord(definition)) {
801
+ diagnostics.push(
802
+ codexHookPackageDiagnostic(
803
+ "codex.plugin.hooks.invalid_shape",
804
+ `Skipping Codex plugin hook event "${event}" handler because it is not an object.`
805
+ )
806
+ );
807
+ continue;
808
+ }
809
+ hookDefinitions.push({
810
+ event,
811
+ ...typeof group.matcher === "string" ? { matcher: group.matcher } : {},
812
+ handler: {
813
+ type: typeof definition.type === "string" ? definition.type : "command",
814
+ command: definition.command,
815
+ async: definition.async
816
+ }
817
+ });
818
+ }
787
819
  }
788
820
  }
789
821
  return { hookDefinitions, diagnostics };
@@ -1763,7 +1795,7 @@ function buildCodexSetupPlan(config) {
1763
1795
  configPath: targets.configPath,
1764
1796
  pluginId: config.pluginId,
1765
1797
  diagnostics: [
1766
- "The combined install flow runs `codex plugin marketplace add EremesNG/thoth-agents` and `codex plugin add thoth-agents@thoth-agents` through the native manager; restart Codex, then use /plugins to review its state.",
1798
+ "The combined install flow runs `codex plugin marketplace add https://github.com/EremesNG/thoth-plugins.git` and `codex plugin add thoth-agents@thoth-plugins` through the native manager; restart Codex, then use /plugins to review its state.",
1767
1799
  "The CLI does not copy a plugin into ~/.codex/plugins or merge ~/.agents/plugins/marketplace.json directly; Codex owns marketplace snapshots and installed plugin cache state.",
1768
1800
  "The /hooks surface may show hooks from independently installed plugins; this thoth-agents setup plan does not install provider hooks.",
1769
1801
  "Codex Default mode user-input requests require features.default_mode_request_user_input = true and use the request_user_input tool; other modes may not expose it.",
@@ -2139,11 +2171,18 @@ var CLAUDE_CODE_ROLE_NAMES = [
2139
2171
  ];
2140
2172
 
2141
2173
  // src/cli/claude-code-install.ts
2142
- var MARKETPLACE_NAME = "thoth-agents";
2143
- var MARKETPLACE_SOURCE = "EremesNG/thoth-agents";
2144
- var PLUGIN_ID = "thoth-agents@thoth-agents";
2145
- var MARKETPLACE_TARGET = "claude://marketplaces/thoth-agents";
2146
- var PLUGIN_TARGET = "claude://plugins/thoth-agents@thoth-agents";
2174
+ var PLUGIN_NAME = "thoth-agents";
2175
+ var MARKETPLACE_NAME = "thoth-plugins";
2176
+ var MARKETPLACE_SOURCE = "https://github.com/EremesNG/thoth-plugins.git";
2177
+ var MARKETPLACE_INSTALL_SOURCE = MARKETPLACE_SOURCE;
2178
+ var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
2179
+ var LEGACY_MARKETPLACE_SOURCE = "https://github.com/EremesNG/thoth-agents.git";
2180
+ var LEGACY_MARKETPLACE_NAMES = [PLUGIN_NAME, `${PLUGIN_NAME}-claude`];
2181
+ var LEGACY_PLUGIN_IDS = LEGACY_MARKETPLACE_NAMES.map(
2182
+ (name) => `${PLUGIN_NAME}@${name}`
2183
+ );
2184
+ var MARKETPLACE_TARGET = `claude://marketplaces/${MARKETPLACE_NAME}`;
2185
+ var PLUGIN_TARGET = `claude://plugins/${PLUGIN_ID}`;
2147
2186
  function defaultCommandExecutor(command, args, options) {
2148
2187
  const result = spawnSync(command, [...args], {
2149
2188
  cwd: options.cwd,
@@ -2171,12 +2210,26 @@ function parseJsonRecords(content) {
2171
2210
  }
2172
2211
  }
2173
2212
  function isCanonicalMarketplace(entry) {
2174
- if (entry.name !== MARKETPLACE_NAME) return false;
2175
- if (entry.repo === MARKETPLACE_SOURCE || entry.source === MARKETPLACE_SOURCE) {
2176
- return true;
2213
+ return isMarketplaceIdentity(entry, MARKETPLACE_NAME);
2214
+ }
2215
+ function normalizeMarketplaceSource(value) {
2216
+ return value.trim().replace(/#.*$/u, "").replace(/^git@github\.com:/i, "").replace(/^(?:https?|ssh):\/\/(?:git@)?github\.com\//i, "").replace(/^github\.com\//i, "").replace(/\/?\.git\/?$/i, "").replace(/\/$/u, "").toLowerCase();
2217
+ }
2218
+ function marketplaceSources(entry) {
2219
+ const sources = [entry.repo, entry.source];
2220
+ if (isRecord3(entry.marketplaceSource)) {
2221
+ sources.push(entry.marketplaceSource.repo, entry.marketplaceSource.source);
2177
2222
  }
2178
- const source = entry.source;
2179
- return isRecord3(source) && (source.repo === MARKETPLACE_SOURCE || source.source === MARKETPLACE_SOURCE);
2223
+ return sources.filter(
2224
+ (source) => typeof source === "string"
2225
+ );
2226
+ }
2227
+ function isMarketplaceIdentity(entry, name, source = MARKETPLACE_SOURCE) {
2228
+ if (entry.name !== name) return false;
2229
+ const canonical = normalizeMarketplaceSource(source);
2230
+ return marketplaceSources(entry).some(
2231
+ (source2) => normalizeMarketplaceSource(source2) === canonical
2232
+ );
2180
2233
  }
2181
2234
  function inspectClaudeManager(executor, cwd, scope) {
2182
2235
  const marketplaceResult = executor(
@@ -2212,6 +2265,19 @@ function inspectClaudeManager(executor, cwd, scope) {
2212
2265
  diagnostics
2213
2266
  };
2214
2267
  }
2268
+ const legacyMarketplace = LEGACY_MARKETPLACE_NAMES.some(
2269
+ (name) => marketplaces.some(
2270
+ (entry) => isMarketplaceIdentity(entry, name, LEGACY_MARKETPLACE_SOURCE)
2271
+ )
2272
+ );
2273
+ const hasLegacyState = legacyMarketplace || plugins.some(
2274
+ (entry) => typeof entry.id === "string" && LEGACY_PLUGIN_IDS.includes(entry.id)
2275
+ );
2276
+ if (hasLegacyState) {
2277
+ diagnostics.push(
2278
+ "Legacy Claude thoth-agents marketplace or plugin state was detected and preserved; the host-specific identity will be managed independently."
2279
+ );
2280
+ }
2215
2281
  const namedMarketplaces = marketplaces.filter(
2216
2282
  (entry) => entry.name === MARKETPLACE_NAME
2217
2283
  );
@@ -2221,7 +2287,7 @@ function inspectClaudeManager(executor, cwd, scope) {
2221
2287
  }
2222
2288
  if (marketplace === "conflict") {
2223
2289
  diagnostics.push(
2224
- "A Claude marketplace named thoth-agents is registered from a different source; resolve it through Claude Code before retrying."
2290
+ "A Claude marketplace named thoth-plugins is registered from a different source; resolve it through Claude Code before retrying."
2225
2291
  );
2226
2292
  }
2227
2293
  const matchingPlugins = plugins.filter(
@@ -2246,7 +2312,7 @@ function commandItem(action, scope, projectRoot) {
2246
2312
  kind: "native-marketplace",
2247
2313
  action,
2248
2314
  targetPath: MARKETPLACE_TARGET,
2249
- description: "Register the package-owned thoth-agents Claude marketplace.",
2315
+ description: "Register the central Thoth Claude marketplace.",
2250
2316
  requiresBackup: false,
2251
2317
  command: {
2252
2318
  executable: "claude",
@@ -2254,7 +2320,7 @@ function commandItem(action, scope, projectRoot) {
2254
2320
  "plugin",
2255
2321
  "marketplace",
2256
2322
  "add",
2257
- MARKETPLACE_SOURCE,
2323
+ MARKETPLACE_INSTALL_SOURCE,
2258
2324
  "--scope",
2259
2325
  scope
2260
2326
  ],
@@ -2328,7 +2394,7 @@ function buildClaudeCodeSetupPlan(config) {
2328
2394
  commandExecutor,
2329
2395
  diagnostics: [
2330
2396
  ...inspection.diagnostics,
2331
- "Claude Code installs thoth-agents from EremesNG/thoth-agents through its native marketplace and owns the cached plugin files.",
2397
+ "Claude Code installs thoth-agents from the central thoth-plugins marketplace, while the thoth-agents repository remains the pinned plugin source.",
2332
2398
  "Restart Claude Code or run /reload-plugins after installation; use /plugin to inspect marketplace and plugin state.",
2333
2399
  "Provider capability is owned by the external provider and is not established by this thoth-agents setup plan."
2334
2400
  ],
@@ -3549,16 +3615,52 @@ function defaultClaudeCodeModelRoles() {
3549
3615
  }
3550
3616
 
3551
3617
  // src/cli/operations/codex.ts
3552
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3618
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
3553
3619
  import { basename as basename2 } from "path";
3554
3620
 
3555
3621
  // src/cli/codex-plugin-install.ts
3556
3622
  import { spawnSync as spawnSync4 } from "child_process";
3557
- var MARKETPLACE_NAME2 = "thoth-agents";
3558
- var MARKETPLACE_SOURCE2 = "EremesNG/thoth-agents";
3559
- var PLUGIN_ID2 = "thoth-agents@thoth-agents";
3560
- var MARKETPLACE_TARGET2 = "codex://marketplaces/thoth-agents";
3561
- var PLUGIN_TARGET2 = "codex://plugins/thoth-agents@thoth-agents";
3623
+ import {
3624
+ lstatSync,
3625
+ readdirSync,
3626
+ readFileSync as readFileSync6,
3627
+ realpathSync,
3628
+ rmSync as rmSync2
3629
+ } from "fs";
3630
+ import { isAbsolute, join as join4, relative, resolve as resolve2, sep } from "path";
3631
+ var PLUGIN_NAME2 = "thoth-agents";
3632
+ var MARKETPLACE_NAME2 = "thoth-plugins";
3633
+ var MARKETPLACE_SOURCE2 = "https://github.com/EremesNG/thoth-plugins.git";
3634
+ var PLUGIN_ID2 = `${PLUGIN_NAME2}@${MARKETPLACE_NAME2}`;
3635
+ var LEGACY_MARKETPLACE_SOURCE2 = "https://github.com/EremesNG/thoth-agents.git";
3636
+ var LEGACY_MARKETPLACE_NAMES2 = [PLUGIN_NAME2, `${PLUGIN_NAME2}-codex`];
3637
+ var LEGACY_PLUGIN_IDS2 = LEGACY_MARKETPLACE_NAMES2.map(
3638
+ (name) => `${PLUGIN_NAME2}@${name}`
3639
+ );
3640
+ var LEGACY_ROOTS = [
3641
+ {
3642
+ relativePath: "plugins/cache/thoth-agents",
3643
+ kind: "cache",
3644
+ marketplaceName: "thoth-agents"
3645
+ },
3646
+ {
3647
+ relativePath: "plugins/cache/thoth-agents-codex",
3648
+ kind: "cache",
3649
+ marketplaceName: "thoth-agents-codex"
3650
+ },
3651
+ {
3652
+ relativePath: ".tmp/marketplaces/thoth-agents",
3653
+ kind: "snapshot",
3654
+ marketplaceName: "thoth-agents"
3655
+ },
3656
+ {
3657
+ relativePath: ".tmp/marketplaces/thoth-agents-codex",
3658
+ kind: "snapshot",
3659
+ marketplaceName: "thoth-agents-codex"
3660
+ }
3661
+ ];
3662
+ var MARKETPLACE_TARGET2 = `codex://marketplaces/${MARKETPLACE_NAME2}`;
3663
+ var PLUGIN_TARGET2 = `codex://plugins/${PLUGIN_ID2}`;
3562
3664
  function getCodexCommand(args, options = {}) {
3563
3665
  if ((options.platform ?? process.platform) !== "win32") {
3564
3666
  return { command: "codex", args: [...args] };
@@ -3596,10 +3698,153 @@ function parseManagerRecord(content) {
3596
3698
  return null;
3597
3699
  }
3598
3700
  }
3599
- function normalizeMarketplaceSource(value) {
3600
- return value.trim().replace(/^git@github\.com:/i, "").replace(/^(?:https?|ssh):\/\/(?:git@)?github\.com\//i, "").replace(/^github\.com\//i, "").replace(/\/?\.git\/?$/i, "").replace(/\/$/, "").toLowerCase();
3701
+ function normalizeMarketplaceSource2(value) {
3702
+ return value.trim().replace(/#.*$/u, "").replace(/^git@github\.com:/i, "").replace(/^(?:https?|ssh):\/\/(?:git@)?github\.com\//i, "").replace(/^github\.com\//i, "").replace(/\/?\.git\/?$/i, "").replace(/\/$/, "").toLowerCase();
3601
3703
  }
3602
- function marketplaceSources(entry) {
3704
+ function lstatIfPresent(path) {
3705
+ try {
3706
+ return lstatSync(path);
3707
+ } catch (error) {
3708
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
3709
+ return null;
3710
+ }
3711
+ throw error;
3712
+ }
3713
+ }
3714
+ function strictDescendant(base, candidate) {
3715
+ const suffix = relative(base, candidate);
3716
+ return suffix !== "" && suffix !== ".." && !suffix.startsWith(`..${sep}`) && !isAbsolute(suffix);
3717
+ }
3718
+ function parseObjectFile(path, label) {
3719
+ const file = lstatIfPresent(path);
3720
+ if (!file || file.isSymbolicLink() || !file.isFile()) {
3721
+ throw new Error(`${label} must be a regular non-link file`);
3722
+ }
3723
+ try {
3724
+ const value = JSON.parse(readFileSync6(path, "utf8"));
3725
+ if (!isRecord5(value)) throw new Error("not an object");
3726
+ return value;
3727
+ } catch (error) {
3728
+ throw new Error(
3729
+ `${label} is invalid JSON: ${error instanceof Error ? error.message : String(error)}`
3730
+ );
3731
+ }
3732
+ }
3733
+ function validateCacheManifest(root) {
3734
+ const productEntries = readdirSync(root, { withFileTypes: true });
3735
+ if (productEntries.length === 0) return;
3736
+ if (productEntries.length !== 1 || productEntries[0]?.name !== PLUGIN_NAME2) {
3737
+ throw new Error(
3738
+ `Legacy cache ${root} contains state outside ${PLUGIN_NAME2}`
3739
+ );
3740
+ }
3741
+ const productRoot = join4(root, PLUGIN_NAME2);
3742
+ const product = lstatIfPresent(productRoot);
3743
+ if (!product || product.isSymbolicLink() || !product.isDirectory()) {
3744
+ throw new Error(`Legacy cache product root ${productRoot} is unsafe`);
3745
+ }
3746
+ const versions = readdirSync(productRoot, { withFileTypes: true });
3747
+ if (versions.length === 0) {
3748
+ throw new Error(`Legacy cache ${root} has no plugin manifest`);
3749
+ }
3750
+ for (const entry of versions) {
3751
+ const versionRoot = join4(productRoot, entry.name);
3752
+ const version = lstatIfPresent(versionRoot);
3753
+ if (!entry.isDirectory() || !version || version.isSymbolicLink() || !version.isDirectory()) {
3754
+ throw new Error(`Legacy cache version root ${versionRoot} is unsafe`);
3755
+ }
3756
+ const manifest = parseObjectFile(
3757
+ join4(versionRoot, ".codex-plugin", "plugin.json"),
3758
+ `Legacy cache manifest for ${versionRoot}`
3759
+ );
3760
+ if (manifest.name !== PLUGIN_NAME2) {
3761
+ throw new Error(
3762
+ `Legacy cache manifest for ${versionRoot} does not identify ${PLUGIN_NAME2}`
3763
+ );
3764
+ }
3765
+ }
3766
+ }
3767
+ function validateSnapshotManifest(root, marketplaceName) {
3768
+ if (readdirSync(root).length === 0) return;
3769
+ const installation = parseObjectFile(
3770
+ join4(root, ".codex-marketplace-install.json"),
3771
+ `Legacy marketplace receipt for ${marketplaceName}`
3772
+ );
3773
+ if (typeof installation.source !== "string" || normalizeMarketplaceSource2(installation.source) !== normalizeMarketplaceSource2(LEGACY_MARKETPLACE_SOURCE2)) {
3774
+ throw new Error(
3775
+ `Legacy marketplace snapshot ${marketplaceName} has conflicting provenance`
3776
+ );
3777
+ }
3778
+ const marketplace = parseObjectFile(
3779
+ join4(root, ".agents", "plugins", "marketplace.json"),
3780
+ `Legacy marketplace manifest for ${marketplaceName}`
3781
+ );
3782
+ const plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
3783
+ const pluginNames = plugins.map(
3784
+ (plugin) => isRecord5(plugin) ? plugin.name : null
3785
+ );
3786
+ if (marketplace.name !== marketplaceName || pluginNames.length === 0 || pluginNames.some((name) => name !== PLUGIN_NAME2)) {
3787
+ throw new Error(
3788
+ `Legacy marketplace snapshot ${marketplaceName} does not identify only ${PLUGIN_NAME2}`
3789
+ );
3790
+ }
3791
+ }
3792
+ function validateLegacyRoot(codexHome, definition, expectedRealPath) {
3793
+ const path = resolve2(codexHome, ...definition.relativePath.split("/"));
3794
+ if (!strictDescendant(codexHome, path)) {
3795
+ throw new Error(
3796
+ `Legacy cleanup target escapes CODEX_HOME: ${definition.relativePath}`
3797
+ );
3798
+ }
3799
+ const target = lstatIfPresent(path);
3800
+ if (!target) return null;
3801
+ const home = lstatIfPresent(codexHome);
3802
+ if (!home || !home.isDirectory()) {
3803
+ throw new Error(`Resolved CODEX_HOME is not a directory: ${codexHome}`);
3804
+ }
3805
+ let boundary = codexHome;
3806
+ for (const segment of definition.relativePath.split("/")) {
3807
+ boundary = join4(boundary, segment);
3808
+ const value = lstatIfPresent(boundary);
3809
+ if (!value || value.isSymbolicLink() || !value.isDirectory()) {
3810
+ throw new Error(
3811
+ `Legacy cleanup boundary is not a real directory: ${boundary}`
3812
+ );
3813
+ }
3814
+ }
3815
+ const realHome = realpathSync(codexHome);
3816
+ const realPath = realpathSync(path);
3817
+ if (!strictDescendant(realHome, realPath)) {
3818
+ throw new Error(
3819
+ `Legacy cleanup target resolves outside CODEX_HOME: ${definition.relativePath}`
3820
+ );
3821
+ }
3822
+ if (expectedRealPath && realPath !== expectedRealPath) {
3823
+ throw new Error(
3824
+ `Legacy cleanup target changed after preflight: ${definition.relativePath}`
3825
+ );
3826
+ }
3827
+ if (definition.kind === "cache") validateCacheManifest(path);
3828
+ else validateSnapshotManifest(path, definition.marketplaceName);
3829
+ return {
3830
+ kind: definition.kind,
3831
+ marketplaceName: definition.marketplaceName,
3832
+ path,
3833
+ realPath,
3834
+ relativePath: definition.relativePath
3835
+ };
3836
+ }
3837
+ function preflightLegacyRoots(codexHomeInput) {
3838
+ if (typeof codexHomeInput !== "string" || codexHomeInput.trim() === "") {
3839
+ throw new Error("CODEX_HOME could not be resolved");
3840
+ }
3841
+ const codexHome = resolve2(codexHomeInput);
3842
+ const roots = LEGACY_ROOTS.map(
3843
+ (definition) => validateLegacyRoot(codexHome, definition)
3844
+ ).filter((root) => root !== null);
3845
+ return { codexHome, roots };
3846
+ }
3847
+ function marketplaceSources2(entry) {
3603
3848
  const sources = [entry.repo, entry.source];
3604
3849
  if (isRecord5(entry.marketplaceSource)) {
3605
3850
  sources.push(entry.marketplaceSource.repo, entry.marketplaceSource.source);
@@ -3609,13 +3854,16 @@ function marketplaceSources(entry) {
3609
3854
  );
3610
3855
  }
3611
3856
  function isCanonicalMarketplace2(entry) {
3612
- if (entry.name !== MARKETPLACE_NAME2) return false;
3613
- const canonical = MARKETPLACE_SOURCE2.toLowerCase();
3614
- return marketplaceSources(entry).some(
3615
- (source) => normalizeMarketplaceSource(source) === canonical
3857
+ return isMarketplaceIdentity2(entry, MARKETPLACE_NAME2);
3858
+ }
3859
+ function isMarketplaceIdentity2(entry, name, source = MARKETPLACE_SOURCE2) {
3860
+ if (entry.name !== name) return false;
3861
+ const canonical = normalizeMarketplaceSource2(source);
3862
+ return marketplaceSources2(entry).some(
3863
+ (source2) => normalizeMarketplaceSource2(source2) === canonical
3616
3864
  );
3617
3865
  }
3618
- function inspectCodexManager(executor, cwd) {
3866
+ function inspectCodexManager(executor, cwd, expectedVersion) {
3619
3867
  const marketplaceResult = executor(
3620
3868
  "codex",
3621
3869
  ["plugin", "marketplace", "list", "--json"],
@@ -3635,6 +3883,9 @@ function inspectCodexManager(executor, cwd) {
3635
3883
  ready: false,
3636
3884
  marketplace: "unknown",
3637
3885
  plugin: "unknown",
3886
+ legacyPluginIds: [],
3887
+ legacyMarketplaceNames: [],
3888
+ legacyMarketplaceConflicts: [],
3638
3889
  diagnostics
3639
3890
  };
3640
3891
  }
@@ -3650,9 +3901,35 @@ function inspectCodexManager(executor, cwd) {
3650
3901
  ready: false,
3651
3902
  marketplace: "unknown",
3652
3903
  plugin: "unknown",
3904
+ legacyPluginIds: [],
3905
+ legacyMarketplaceNames: [],
3906
+ legacyMarketplaceConflicts: [],
3653
3907
  diagnostics
3654
3908
  };
3655
3909
  }
3910
+ const legacyMarketplaceNames = LEGACY_MARKETPLACE_NAMES2.filter(
3911
+ (name) => marketplaces.some(
3912
+ (entry) => isMarketplaceIdentity2(entry, name, LEGACY_MARKETPLACE_SOURCE2)
3913
+ )
3914
+ );
3915
+ const legacyMarketplaceConflicts = LEGACY_MARKETPLACE_NAMES2.filter(
3916
+ (name) => marketplaces.some(
3917
+ (entry) => entry.name === name && !isMarketplaceIdentity2(entry, name, LEGACY_MARKETPLACE_SOURCE2)
3918
+ )
3919
+ );
3920
+ const legacyPluginIds = LEGACY_PLUGIN_IDS2.filter(
3921
+ (pluginId) => installedPlugins.some((entry) => entry.pluginId === pluginId)
3922
+ );
3923
+ if (legacyPluginIds.length > 0 || legacyMarketplaceNames.length > 0) {
3924
+ diagnostics.push(
3925
+ "Legacy Codex thoth-agents state was detected; cleanup is planned only after the central plugin is verified."
3926
+ );
3927
+ }
3928
+ if (legacyMarketplaceConflicts.length > 0) {
3929
+ diagnostics.push(
3930
+ `Legacy Codex marketplace provenance conflicts were found for ${legacyMarketplaceConflicts.join(", ")}; no manager mutation will be attempted.`
3931
+ );
3932
+ }
3656
3933
  const namedMarketplaces = marketplaces.filter(
3657
3934
  (entry) => entry.name === MARKETPLACE_NAME2
3658
3935
  );
@@ -3662,31 +3939,81 @@ function inspectCodexManager(executor, cwd) {
3662
3939
  }
3663
3940
  if (marketplace === "conflict") {
3664
3941
  diagnostics.push(
3665
- "A Codex marketplace named thoth-agents is registered from a different source; resolve it through Codex before retrying."
3942
+ "A Codex marketplace named thoth-plugins is registered from a different source; resolve it through Codex before retrying."
3666
3943
  );
3667
3944
  }
3668
3945
  const matchingPlugins = installedPlugins.filter(
3669
3946
  (entry) => entry.pluginId === PLUGIN_ID2
3670
3947
  );
3671
3948
  let plugin = "absent";
3672
- if (matchingPlugins.some((entry) => entry.enabled === true)) {
3949
+ if (matchingPlugins.some(
3950
+ (entry) => entry.enabled === true && entry.version === expectedVersion
3951
+ )) {
3673
3952
  plugin = "installed";
3953
+ } else if (matchingPlugins.some((entry) => entry.enabled === true)) {
3954
+ plugin = "outdated";
3674
3955
  } else if (matchingPlugins.length > 0) {
3675
3956
  plugin = "disabled";
3676
3957
  }
3677
3958
  return {
3678
- ready: marketplace !== "conflict",
3959
+ ready: marketplace !== "conflict" && legacyMarketplaceConflicts.length === 0,
3679
3960
  marketplace,
3680
3961
  plugin,
3962
+ legacyPluginIds,
3963
+ legacyMarketplaceNames,
3964
+ legacyMarketplaceConflicts,
3681
3965
  diagnostics
3682
3966
  };
3683
3967
  }
3968
+ function centralPluginVerified(inspection) {
3969
+ return inspection.ready && inspection.marketplace === "installed" && inspection.plugin === "installed";
3970
+ }
3971
+ function legacyManagerClean(inspection) {
3972
+ return inspection.legacyPluginIds.length === 0 && inspection.legacyMarketplaceNames.length === 0;
3973
+ }
3974
+ function legacyRootsAbsent(codexHome) {
3975
+ return LEGACY_ROOTS.every(
3976
+ ({ relativePath }) => !lstatIfPresent(resolve2(codexHome, ...relativePath.split("/")))
3977
+ );
3978
+ }
3979
+ function legacyPluginCommandItem(pluginId, projectRoot) {
3980
+ return {
3981
+ action: "remove-legacy-plugin",
3982
+ targetPath: `codex://plugins/${pluginId}`,
3983
+ description: `Remove the registered legacy Codex plugin ${pluginId}.`,
3984
+ command: {
3985
+ executable: "codex",
3986
+ args: ["plugin", "remove", pluginId, "--json"],
3987
+ cwd: projectRoot
3988
+ }
3989
+ };
3990
+ }
3991
+ function legacyMarketplaceCommandItem(marketplaceName, projectRoot) {
3992
+ return {
3993
+ action: "remove-legacy-marketplace",
3994
+ targetPath: `codex://marketplaces/${marketplaceName}`,
3995
+ description: `Remove the registered legacy Codex marketplace ${marketplaceName}.`,
3996
+ command: {
3997
+ executable: "codex",
3998
+ args: ["plugin", "marketplace", "remove", marketplaceName, "--json"],
3999
+ cwd: projectRoot
4000
+ }
4001
+ };
4002
+ }
4003
+ function legacyRootPlanItem(root) {
4004
+ return {
4005
+ action: "remove-legacy-root",
4006
+ targetPath: root.path,
4007
+ description: `Remove the preflight-approved legacy Codex root ${root.relativePath}.`,
4008
+ cleanupRoot: root
4009
+ };
4010
+ }
3684
4011
  function commandItem2(action, projectRoot) {
3685
4012
  if (action === "register-marketplace") {
3686
4013
  return {
3687
4014
  action,
3688
4015
  targetPath: MARKETPLACE_TARGET2,
3689
- description: "Register the package-owned thoth-agents Codex marketplace.",
4016
+ description: "Register the central Thoth Codex marketplace.",
3690
4017
  command: {
3691
4018
  executable: "codex",
3692
4019
  args: ["plugin", "marketplace", "add", MARKETPLACE_SOURCE2, "--json"],
@@ -3707,25 +4034,54 @@ function commandItem2(action, projectRoot) {
3707
4034
  }
3708
4035
  function buildCodexPluginSetupPlan(config) {
3709
4036
  const commandExecutor = config.commandExecutor ?? defaultCommandExecutor3;
3710
- const inspection = inspectCodexManager(commandExecutor, config.projectRoot);
3711
- const items = [];
4037
+ const inspection = inspectCodexManager(
4038
+ commandExecutor,
4039
+ config.projectRoot,
4040
+ config.expectedVersion
4041
+ );
4042
+ let cleanupPreflight;
4043
+ let cleanupPreflightError;
3712
4044
  if (inspection.ready) {
4045
+ try {
4046
+ cleanupPreflight = preflightLegacyRoots(
4047
+ getCodexHome({ codexHome: config.codexHome, homeDir: config.homeDir })
4048
+ );
4049
+ } catch (error) {
4050
+ cleanupPreflightError = `Legacy cleanup preflight failed: ${error instanceof Error ? error.message : String(error)}. No manager mutation will be attempted.`;
4051
+ }
4052
+ }
4053
+ const ready = inspection.ready && cleanupPreflightError === void 0;
4054
+ const items = [];
4055
+ if (ready) {
3713
4056
  if (inspection.marketplace === "absent") {
3714
4057
  items.push(commandItem2("register-marketplace", config.projectRoot));
3715
4058
  }
3716
- if (inspection.plugin === "absent" || inspection.plugin === "disabled") {
4059
+ if (inspection.plugin === "absent" || inspection.plugin === "disabled" || inspection.plugin === "outdated") {
3717
4060
  items.push(commandItem2("install-plugin", config.projectRoot));
3718
4061
  }
4062
+ items.push(
4063
+ ...inspection.legacyPluginIds.map(
4064
+ (pluginId) => legacyPluginCommandItem(pluginId, config.projectRoot)
4065
+ ),
4066
+ ...inspection.legacyMarketplaceNames.map(
4067
+ (marketplaceName) => legacyMarketplaceCommandItem(marketplaceName, config.projectRoot)
4068
+ ),
4069
+ ...cleanupPreflight?.roots.map(legacyRootPlanItem) ?? []
4070
+ );
3719
4071
  }
3720
4072
  return {
4073
+ codexHome: cleanupPreflight?.codexHome ?? getCodexHome({ codexHome: config.codexHome, homeDir: config.homeDir }),
3721
4074
  dryRun: config.dryRun === true,
4075
+ expectedVersion: config.expectedVersion,
3722
4076
  items,
3723
- ready: inspection.ready,
4077
+ ready,
3724
4078
  projectRoot: config.projectRoot,
3725
4079
  commandExecutor,
3726
4080
  diagnostics: [
3727
4081
  ...inspection.diagnostics,
3728
- "Codex installs thoth-agents from EremesNG/thoth-agents through its native plugin manager and owns the cached plugin files.",
4082
+ ...cleanupPreflightError ? [cleanupPreflightError] : [],
4083
+ "Codex installs thoth-agents from the central thoth-plugins marketplace through its native plugin manager, while the thoth-agents repository remains the pinned plugin source.",
4084
+ "Close every Codex process before applying legacy cleanup; process-name detection is not a portable or race-free safety boundary.",
3729
4085
  "Restart Codex after installation; use /plugins to inspect marketplace and plugin state."
3730
4086
  ]
3731
4087
  };
@@ -3749,12 +4105,28 @@ function applyCodexPluginSetup(plan) {
3749
4105
  };
3750
4106
  }
3751
4107
  if (plan.dryRun) return { success: true, changed, diagnostics };
3752
- for (const item of plan.items) {
3753
- const result = plan.commandExecutor(
3754
- item.command.executable,
3755
- item.command.args,
3756
- { cwd: item.command.cwd }
3757
- );
4108
+ const installationItems = plan.items.filter(
4109
+ (item) => item.action === "register-marketplace" || item.action === "install-plugin"
4110
+ );
4111
+ const cleanupItems = plan.items.filter(
4112
+ (item) => item.action === "remove-legacy-plugin" || item.action === "remove-legacy-marketplace"
4113
+ );
4114
+ const rootItems = plan.items.filter(
4115
+ (item) => item.action === "remove-legacy-root"
4116
+ );
4117
+ for (const item of installationItems) {
4118
+ const command = item.command;
4119
+ if (!command) {
4120
+ return {
4121
+ success: false,
4122
+ changed,
4123
+ diagnostics,
4124
+ error: `Codex plugin setup plan item ${item.action} has no command.`
4125
+ };
4126
+ }
4127
+ const result = plan.commandExecutor(command.executable, command.args, {
4128
+ cwd: command.cwd
4129
+ });
3758
4130
  if (result.exitCode !== 0) {
3759
4131
  return {
3760
4132
  success: false,
@@ -3767,10 +4139,11 @@ function applyCodexPluginSetup(plan) {
3767
4139
  }
3768
4140
  const inspection = inspectCodexManager(
3769
4141
  plan.commandExecutor,
3770
- plan.projectRoot
4142
+ plan.projectRoot,
4143
+ plan.expectedVersion
3771
4144
  );
3772
4145
  diagnostics.push(...inspection.diagnostics);
3773
- if (inspection.marketplace !== "installed" || inspection.plugin !== "installed") {
4146
+ if (!centralPluginVerified(inspection)) {
3774
4147
  return {
3775
4148
  success: false,
3776
4149
  changed,
@@ -3778,11 +4151,112 @@ function applyCodexPluginSetup(plan) {
3778
4151
  error: "Codex did not verify the expected marketplace and enabled plugin after mutation."
3779
4152
  };
3780
4153
  }
4154
+ for (const item of cleanupItems) {
4155
+ const command = item.command;
4156
+ if (!command) {
4157
+ return {
4158
+ success: false,
4159
+ changed,
4160
+ diagnostics: uniqueMessages3(diagnostics),
4161
+ error: `Codex plugin cleanup plan item ${item.action} has no command.`
4162
+ };
4163
+ }
4164
+ const result = plan.commandExecutor(command.executable, command.args, {
4165
+ cwd: command.cwd
4166
+ });
4167
+ if (result.exitCode !== 0) {
4168
+ return {
4169
+ success: false,
4170
+ changed,
4171
+ diagnostics: uniqueMessages3(diagnostics),
4172
+ error: `${boundedCommandFailure2(item, result)} Central ${PLUGIN_ID2} remains installed; close Codex and retry.`
4173
+ };
4174
+ }
4175
+ changed.push(item.targetPath);
4176
+ }
4177
+ const managerCleanupInspection = inspectCodexManager(
4178
+ plan.commandExecutor,
4179
+ plan.projectRoot,
4180
+ plan.expectedVersion
4181
+ );
4182
+ diagnostics.push(...managerCleanupInspection.diagnostics);
4183
+ if (!centralPluginVerified(managerCleanupInspection) || !legacyManagerClean(managerCleanupInspection)) {
4184
+ return {
4185
+ success: false,
4186
+ changed,
4187
+ diagnostics: uniqueMessages3(diagnostics),
4188
+ error: `Central ${PLUGIN_ID2} remains installed, but exact legacy manager cleanup did not converge; close Codex and retry.`
4189
+ };
4190
+ }
4191
+ for (const item of rootItems) {
4192
+ const root = item.cleanupRoot;
4193
+ const definition = root ? LEGACY_ROOTS.find(
4194
+ (candidate) => candidate.relativePath === root.relativePath
4195
+ ) : void 0;
4196
+ if (!root || !definition) {
4197
+ return {
4198
+ success: false,
4199
+ changed,
4200
+ diagnostics: uniqueMessages3(diagnostics),
4201
+ error: `Central ${PLUGIN_ID2} remains installed, but the fixed legacy cleanup plan is invalid; close Codex and retry.`
4202
+ };
4203
+ }
4204
+ const exactPath = resolve2(
4205
+ plan.codexHome,
4206
+ ...definition.relativePath.split("/")
4207
+ );
4208
+ if (resolve2(root.path) !== exactPath || resolve2(item.targetPath) !== exactPath || root.kind !== definition.kind || root.marketplaceName !== definition.marketplaceName) {
4209
+ return {
4210
+ success: false,
4211
+ changed,
4212
+ diagnostics: uniqueMessages3(diagnostics),
4213
+ error: `Central ${PLUGIN_ID2} remains installed, but the fixed legacy cleanup target changed; close Codex and retry.`
4214
+ };
4215
+ }
4216
+ if (!lstatIfPresent(exactPath)) continue;
4217
+ try {
4218
+ const validated = validateLegacyRoot(
4219
+ plan.codexHome,
4220
+ definition,
4221
+ root.realPath
4222
+ );
4223
+ if (!validated) continue;
4224
+ rmSync2(validated.path, { recursive: true, force: false });
4225
+ if (lstatIfPresent(validated.path)) {
4226
+ throw new Error(
4227
+ `Legacy cleanup target still exists: ${root.relativePath}`
4228
+ );
4229
+ }
4230
+ changed.push(item.targetPath);
4231
+ } catch (error) {
4232
+ const detail = (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").slice(0, 240);
4233
+ return {
4234
+ success: false,
4235
+ changed,
4236
+ diagnostics: uniqueMessages3(diagnostics),
4237
+ error: `Central ${PLUGIN_ID2} remains installed; legacy root cleanup failed${detail ? `: ${detail}` : ""}. Close Codex and retry.`
4238
+ };
4239
+ }
4240
+ }
4241
+ const finalInspection = inspectCodexManager(
4242
+ plan.commandExecutor,
4243
+ plan.projectRoot,
4244
+ plan.expectedVersion
4245
+ );
4246
+ diagnostics.push(...finalInspection.diagnostics);
4247
+ if (!centralPluginVerified(finalInspection) || !legacyManagerClean(finalInspection) || !legacyRootsAbsent(plan.codexHome)) {
4248
+ return {
4249
+ success: false,
4250
+ changed,
4251
+ diagnostics: uniqueMessages3(diagnostics),
4252
+ error: `Central ${PLUGIN_ID2} remains installed, but exact legacy cleanup did not converge; close Codex and retry.`
4253
+ };
4254
+ }
3781
4255
  return { success: true, changed, diagnostics: uniqueMessages3(diagnostics) };
3782
4256
  }
3783
4257
  function formatCodexPluginSetupPlan(plan) {
3784
4258
  const lines = plan.items.map(
3785
- (item) => `- ${item.action}: ${item.command.executable} ${item.command.args.join(" ")}`
4259
+ (item) => item.command ? `- ${item.action}: ${item.command.executable} ${item.command.args.join(" ")}` : `- ${item.action}: ${item.cleanupRoot?.relativePath ?? item.targetPath}`
3786
4260
  );
3787
4261
  return ["Codex plugin setup plan:", ...lines].join("\n");
3788
4262
  }
@@ -3910,7 +4384,7 @@ function backupForItem(item) {
3910
4384
  function rootBlockState(item) {
3911
4385
  if (!existsSync8(item.targetPath))
3912
4386
  return { state: "missing", observed: "absent" };
3913
- const content = readFileSync6(item.targetPath, "utf8");
4387
+ const content = readFileSync7(item.targetPath, "utf8");
3914
4388
  if (item.content && content.includes(item.content)) {
3915
4389
  return { state: "installed", observed: "managed root block present" };
3916
4390
  }
@@ -3923,11 +4397,11 @@ function managedModelState(item) {
3923
4397
  if (!existsSync8(item.targetPath))
3924
4398
  return { state: "missing", observed: "absent" };
3925
4399
  try {
3926
- const parsed = JSON.parse(readFileSync6(item.targetPath, "utf8"));
4400
+ const parsed = JSON.parse(readFileSync7(item.targetPath, "utf8"));
3927
4401
  if (parsed.version !== MANAGED_MODEL_STATE_VERSION || !parsed.models || typeof parsed.models !== "object" || Array.isArray(parsed.models)) {
3928
4402
  return { state: "unknown", observed: "invalid managed model state" };
3929
4403
  }
3930
- return item.content === readFileSync6(item.targetPath, "utf8") ? { state: "installed", observed: "managed model state current" } : { state: "drift", observed: "managed model state differs" };
4404
+ return item.content === readFileSync7(item.targetPath, "utf8") ? { state: "installed", observed: "managed model state current" } : { state: "drift", observed: "managed model state differs" };
3931
4405
  } catch {
3932
4406
  return { state: "unknown", observed: "unparseable managed model state" };
3933
4407
  }
@@ -3935,7 +4409,7 @@ function managedModelState(item) {
3935
4409
  function userConfigState(item) {
3936
4410
  if (!existsSync8(item.targetPath))
3937
4411
  return { state: "missing", observed: "absent" };
3938
- const content = readFileSync6(item.targetPath, "utf8");
4412
+ const content = readFileSync7(item.targetPath, "utf8");
3939
4413
  if (/^\s*default_mode_request_user_input\s*=\s*true\b/m.test(content)) {
3940
4414
  return {
3941
4415
  state: "installed",
@@ -3950,7 +4424,7 @@ function userConfigState(item) {
3950
4424
  function contentState(item) {
3951
4425
  if (!existsSync8(item.targetPath))
3952
4426
  return { state: "missing", observed: "absent" };
3953
- const observed = readFileSync6(item.targetPath, "utf8");
4427
+ const observed = readFileSync7(item.targetPath, "utf8");
3954
4428
  if (item.content === observed)
3955
4429
  return { state: "installed", observed: "current" };
3956
4430
  if (item.role) {
@@ -4117,7 +4591,7 @@ function planItemFromPluginSetup(item) {
4117
4591
  path: item.targetPath,
4118
4592
  label: "Codex native plugin manager target"
4119
4593
  },
4120
- preview: `${item.command.executable} ${item.command.args.join(" ")}`,
4594
+ preview: item.command ? `${item.command.executable} ${item.command.args.join(" ")}` : `Remove exact preflight-approved Codex root ${item.cleanupRoot?.relativePath ?? item.targetPath}`,
4121
4595
  backup: { required: false, strategy: "external" }
4122
4596
  };
4123
4597
  }
@@ -4215,7 +4689,10 @@ function buildCompleteCodexPlan(action, context2) {
4215
4689
  const packageVersion = resolveVersion();
4216
4690
  const buildPlugin = context2.buildCodexPluginSetupPlan ?? buildCodexPluginSetupPlan;
4217
4691
  const pluginPlan = buildPlugin({
4692
+ codexHome: context2.codexHome,
4218
4693
  dryRun: true,
4694
+ expectedVersion: packageVersion.ok ? packageVersion.version : "0.0.0",
4695
+ homeDir: context2.homeDir,
4219
4696
  projectRoot: context2.cwd,
4220
4697
  commandExecutor: context2.codexPluginCommandExecutor
4221
4698
  });
@@ -4611,7 +5088,7 @@ function applyCodexPlan(plan) {
4611
5088
  // src/cli/operations/opencode.ts
4612
5089
  import { createHash as createHash3 } from "crypto";
4613
5090
  import { existsSync as existsSync10 } from "fs";
4614
- import { join as join5 } from "path";
5091
+ import { join as join6 } from "path";
4615
5092
 
4616
5093
  // src/cli/opencode-effort.ts
4617
5094
  var OPENAI_GPT_RUNTIME_VARIANTS = /* @__PURE__ */ new Set([
@@ -4650,11 +5127,11 @@ import {
4650
5127
  mkdirSync as mkdirSync4,
4651
5128
  mkdtempSync,
4652
5129
  renameSync as renameSync3,
4653
- rmSync as rmSync2,
5130
+ rmSync as rmSync3,
4654
5131
  statSync
4655
5132
  } from "fs";
4656
5133
  import { homedir as homedir4 } from "os";
4657
- import { dirname as dirname5, join as join4 } from "path";
5134
+ import { dirname as dirname5, join as join5 } from "path";
4658
5135
  import { fileURLToPath as fileURLToPath4 } from "url";
4659
5136
 
4660
5137
  // src/harness/core/owned-skills.ts
@@ -4684,8 +5161,8 @@ function getOpenCodeOwnedSkillEntries(options = {}) {
4684
5161
  const homeDir = options.homeDir ?? homedir4();
4685
5162
  return THOTH_OWNED_SKILL_NAMES.map((name) => ({
4686
5163
  name,
4687
- sourcePath: join4(packageRoot, "skills", name),
4688
- destinationPath: join4(homeDir, ".config", "opencode", "skills", name)
5164
+ sourcePath: join5(packageRoot, "skills", name),
5165
+ destinationPath: join5(homeDir, ".config", "opencode", "skills", name)
4689
5166
  }));
4690
5167
  }
4691
5168
  function validateCanonicalSkill(entry) {
@@ -4694,7 +5171,7 @@ function validateCanonicalSkill(entry) {
4694
5171
  `Canonical owned skill directory is missing: ${entry.name}`
4695
5172
  );
4696
5173
  }
4697
- const contractPath = join4(entry.sourcePath, "SKILL.md");
5174
+ const contractPath = join5(entry.sourcePath, "SKILL.md");
4698
5175
  if (!existsSync9(contractPath) || !statSync(contractPath).isFile()) {
4699
5176
  throw new Error(`Canonical owned skill contract is missing: ${entry.name}`);
4700
5177
  }
@@ -4704,21 +5181,21 @@ function installOwnedSkills(skills) {
4704
5181
  if (!firstSkill) return;
4705
5182
  const globalRoot = dirname5(firstSkill.destinationPath);
4706
5183
  mkdirSync4(globalRoot, { recursive: true });
4707
- const transactionRoot = mkdtempSync(join4(globalRoot, ".thoth-agents-owned-"));
4708
- const stagedRoot = join4(transactionRoot, "staged");
4709
- const backupRoot = join4(transactionRoot, "backup");
5184
+ const transactionRoot = mkdtempSync(join5(globalRoot, ".thoth-agents-owned-"));
5185
+ const stagedRoot = join5(transactionRoot, "staged");
5186
+ const backupRoot = join5(transactionRoot, "backup");
4710
5187
  mkdirSync4(stagedRoot, { recursive: true });
4711
5188
  mkdirSync4(backupRoot, { recursive: true });
4712
5189
  try {
4713
5190
  for (const skill of skills) {
4714
- cpSync(skill.sourcePath, join4(stagedRoot, skill.name), {
5191
+ cpSync(skill.sourcePath, join5(stagedRoot, skill.name), {
4715
5192
  recursive: true,
4716
5193
  force: true
4717
5194
  });
4718
5195
  }
4719
5196
  for (const skill of skills) {
4720
- const stagedPath = join4(stagedRoot, skill.name);
4721
- const backupPath = join4(backupRoot, skill.name);
5197
+ const stagedPath = join5(stagedRoot, skill.name);
5198
+ const backupPath = join5(backupRoot, skill.name);
4722
5199
  const hadExistingDestination = existsSync9(skill.destinationPath);
4723
5200
  if (hadExistingDestination) {
4724
5201
  renameSync3(skill.destinationPath, backupPath);
@@ -4727,7 +5204,7 @@ function installOwnedSkills(skills) {
4727
5204
  renameSync3(stagedPath, skill.destinationPath);
4728
5205
  } catch (error) {
4729
5206
  if (existsSync9(skill.destinationPath)) {
4730
- rmSync2(skill.destinationPath, { recursive: true, force: true });
5207
+ rmSync3(skill.destinationPath, { recursive: true, force: true });
4731
5208
  }
4732
5209
  if (hadExistingDestination && existsSync9(backupPath)) {
4733
5210
  renameSync3(backupPath, skill.destinationPath);
@@ -4735,11 +5212,11 @@ function installOwnedSkills(skills) {
4735
5212
  throw error;
4736
5213
  }
4737
5214
  if (existsSync9(backupPath)) {
4738
- rmSync2(backupPath, { recursive: true, force: true });
5215
+ rmSync3(backupPath, { recursive: true, force: true });
4739
5216
  }
4740
5217
  }
4741
5218
  } finally {
4742
- rmSync2(transactionRoot, { recursive: true, force: true });
5219
+ rmSync3(transactionRoot, { recursive: true, force: true });
4743
5220
  }
4744
5221
  }
4745
5222
  function syncOpenCodeOwnedSkills(options = {}) {
@@ -5000,7 +5477,7 @@ function titleCaseSkillName(value) {
5000
5477
  }).join("-");
5001
5478
  }
5002
5479
  function homeDirFromContext(context2) {
5003
- return context2.env?.HOME ?? context2.env?.USERPROFILE ?? (context2.cwd !== process.cwd() ? join5(context2.cwd, "home") : void 0);
5480
+ return context2.env?.HOME ?? context2.env?.USERPROFILE ?? (context2.cwd !== process.cwd() ? join6(context2.cwd, "home") : void 0);
5004
5481
  }
5005
5482
  function openCodeSkillTargets(context2) {
5006
5483
  const homeDir = homeDirFromContext(context2);
@@ -5024,7 +5501,7 @@ function openCodeSkillTargets(context2) {
5024
5501
  const ownedTargets = getOpenCodeOwnedSkillEntries({
5025
5502
  homeDir
5026
5503
  }).map((skill) => {
5027
- const path = join5(skill.destinationPath, "SKILL.md");
5504
+ const path = join6(skill.destinationPath, "SKILL.md");
5028
5505
  const installed = existsSync10(path);
5029
5506
  return {
5030
5507
  kind: "skill",
@@ -6407,7 +6884,7 @@ import { execFileSync } from "child_process";
6407
6884
  import {
6408
6885
  existsSync as existsSync11,
6409
6886
  mkdirSync as mkdirSync5,
6410
- readFileSync as readFileSync7,
6887
+ readFileSync as readFileSync8,
6411
6888
  renameSync as renameSync4,
6412
6889
  unlinkSync,
6413
6890
  writeFileSync as writeFileSync4
@@ -6445,7 +6922,7 @@ function resolveModelsDevCachePath(options = {}) {
6445
6922
  function readModelsDevCache(cachePath) {
6446
6923
  if (!existsSync11(cachePath)) return void 0;
6447
6924
  try {
6448
- const input = JSON.parse(readFileSync7(cachePath, "utf8"));
6925
+ const input = JSON.parse(readFileSync8(cachePath, "utf8"));
6449
6926
  const parsed = modelsDevCacheSchema.safeParse(input);
6450
6927
  return parsed.success ? parsed.data : void 0;
6451
6928
  } catch {
@@ -6780,7 +7257,7 @@ async function getModelOptions(harness) {
6780
7257
  }
6781
7258
 
6782
7259
  // src/cli/tui/operations.ts
6783
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
7260
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
6784
7261
  var context = { cwd: process.cwd() };
6785
7262
  var codexContext = { cwd: process.cwd() };
6786
7263
  var claudeCodeContext = {
@@ -6822,7 +7299,7 @@ function getCodexModelRoles(source = codexContext) {
6822
7299
  const item = plan.items.find(
6823
7300
  (candidate) => candidate.action === "write-role-toml" && candidate.role === role
6824
7301
  );
6825
- const content = item && existsSync12(item.targetPath) ? readFileSync8(item.targetPath, "utf8") : item?.content;
7302
+ const content = item && existsSync12(item.targetPath) ? readFileSync9(item.targetPath, "utf8") : item?.content;
6826
7303
  const effort = content ? parseRoleTomlEffort(content) : void 0;
6827
7304
  return {
6828
7305
  role,