thoth-agents 0.3.10 → 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 (52) hide show
  1. package/README.md +119 -33
  2. package/dist/{chunk-TKIDBRM3.js → chunk-5UBJB5QO.js} +4 -1
  3. package/dist/{chunk-M2WF46BP.js → chunk-DX4U3AD3.js} +387 -231
  4. package/dist/{chunk-JBG7XMIG.js → chunk-VPVBYBPU.js} +602 -180
  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/config/constants.d.ts +3 -3
  9. package/dist/harness/adapters/claude-code.d.ts +8 -0
  10. package/dist/harness/adapters/codex-surfaces.d.ts +2 -2
  11. package/dist/harness/core/agent-pack.d.ts +60 -5
  12. package/dist/harness/core/sdd.d.ts +32 -19
  13. package/dist/harness/writers/codex-plugin-package.d.ts +1 -0
  14. package/dist/index.js +2 -2
  15. package/package.json +4 -4
  16. package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +7 -7
  17. package/plugin/.claude-plugin/plugin.json +1 -1
  18. package/plugin/.codex-plugin/plugin.json +1 -1
  19. package/plugin/agents/deep.md +10 -31
  20. package/plugin/agents/designer.md +10 -31
  21. package/plugin/agents/explorer.md +10 -30
  22. package/plugin/agents/librarian.md +10 -30
  23. package/plugin/agents/oracle.md +11 -31
  24. package/plugin/agents/orchestrator.md +48 -35
  25. package/plugin/agents/quick.md +10 -31
  26. package/plugin/skills/plan-reviewer/SKILL.md +54 -15
  27. package/plugin/skills/thoth-archive/SKILL.md +5 -0
  28. package/plugin/skills/thoth-archive/scripts/archive.mjs +30 -10
  29. package/plugin/skills/thoth-constitution/SKILL.md +5 -0
  30. package/plugin/skills/thoth-constitution/templates/constitution.md +19 -2
  31. package/plugin/skills/thoth-init/SKILL.md +5 -0
  32. package/plugin/skills/thoth-sdd/SKILL.md +48 -11
  33. package/plugin/skills/thoth-sdd/references/phases/implement.md +44 -6
  34. package/plugin/skills/thoth-sdd/references/phases/tasks.md +23 -2
  35. package/plugin/skills/thoth-sdd/references/phases/verify.md +7 -5
  36. package/plugin/skills/thoth-sdd/scripts/validate.mjs +263 -30
  37. package/plugin/skills/thoth-sdd/templates/tasks.md +18 -2
  38. package/skills/README.md +2 -1
  39. package/skills/plan-reviewer/SKILL.md +54 -15
  40. package/skills/thoth-archive/SKILL.md +5 -0
  41. package/skills/thoth-archive/scripts/archive.mjs +30 -10
  42. package/skills/thoth-constitution/SKILL.md +5 -0
  43. package/skills/thoth-constitution/templates/constitution.md +19 -2
  44. package/skills/thoth-init/SKILL.md +5 -0
  45. package/skills/thoth-sdd/SKILL.md +48 -11
  46. package/skills/thoth-sdd/references/phases/implement.md +44 -6
  47. package/skills/thoth-sdd/references/phases/tasks.md +23 -2
  48. package/skills/thoth-sdd/references/phases/verify.md +7 -5
  49. package/skills/thoth-sdd/scripts/validate.mjs +263 -30
  50. package/skills/thoth-sdd/templates/tasks.md +18 -2
  51. package/.agents/plugins/marketplace.json +0 -20
  52. package/.claude-plugin/marketplace.json +0 -21
@@ -28,13 +28,14 @@ import {
28
28
  isValidPackageVersion,
29
29
  loadAgentPrompt,
30
30
  parseConfig,
31
+ renderAgentRoutingDescription,
31
32
  renderPromptSection,
32
33
  renderRolePrompt,
33
34
  resolveExecutingPackageVersion,
34
35
  updateOpenCodeMainConfig,
35
36
  writeConfig,
36
37
  writeLiteConfig
37
- } from "./chunk-M2WF46BP.js";
38
+ } from "./chunk-DX4U3AD3.js";
38
39
 
39
40
  // src/cli/codex-paths.ts
40
41
  import { homedir } from "os";
@@ -120,62 +121,6 @@ function renderConfiguredRolePrompt({
120
121
  }
121
122
 
122
123
  // src/harness/core/memory-governance.ts
123
- function roleRules(role) {
124
- const sharedRules = [
125
- "thoth-mem remains an external provider; its installed provider guidance is authoritative for memory operations and thoth-agents does not prescribe the mechanism.",
126
- "Provider-dependent use requires parent-scoped authorization with the stable root session identity or explicit unavailable state and project supplied by dispatch.",
127
- "The MEMORY authorization is none, recall, or observe: none forbids provider work, recall permits bounded reads, and observe additionally permits one bounded durable observation under the delegated scope.",
128
- "Only authorized context may be used, and the delegate must report missing, stale, contradictory, or insufficient context instead of guessing.",
129
- "Memory authorization never changes workspace permissions or grants control of root lifecycle and real-user-intent ownership.",
130
- "A handoff must keep accepted scope, decisions, permissions, and artifacts plus bounded memory context available to the authorized delegate.",
131
- "Completion continuity is a provider-confirmed semantic summary outcome owned by the root.",
132
- "Missing capability evidence is reported as degraded or unsupported and never as successful persistence or recovery.",
133
- "openspec/ is the canonical SDD store; do not mirror spec.md, plan.md, tasks.md, verification reports, or archive reports into provider memory.",
134
- "Do not invent a consumer fallback or silently change the selected persistence mode."
135
- ];
136
- if (role.name === "orchestrator") {
137
- return [
138
- ...sharedRules,
139
- "The root coordinator owns authorization, root lifecycle, real-user-intent handling, and semantic completion outcomes.",
140
- "Dispatch task and authorization context without embedding secrets, raw transcripts, or irrelevant context."
141
- ];
142
- }
143
- if (role.mode === "read-only") {
144
- return [
145
- ...sharedRules,
146
- "For a read-only workspace role, observe may authorize a durable provider observation but does not authorize workspace mutation.",
147
- "Root lifecycle ownership never transfers to this delegate."
148
- ];
149
- }
150
- return [
151
- ...sharedRules,
152
- "For a write-capable workspace role, provider observations still require observe authorization and the delegated parent scope.",
153
- "Root lifecycle ownership never transfers to this delegate."
154
- ];
155
- }
156
- function getRoleMemoryGovernance(role) {
157
- return {
158
- role: role.name,
159
- workspaceMode: role.mode,
160
- requiresParentContext: role.name !== "orchestrator",
161
- availableAuthorizations: ["none", "recall", "observe"],
162
- ownsRootLifecycle: role.name === "orchestrator",
163
- rules: roleRules(role)
164
- };
165
- }
166
- function renderMemoryGovernanceInstructions(role, dialect) {
167
- const governance = getRoleMemoryGovernance(role);
168
- const harnessRules = dialect ? [
169
- `- Harness wording: use ${dialect.renderRoleInvocation("orchestrator")} as the memory owner and \`${dialect.tools.userQuestionTool}\` for blocking memory-context questions.`,
170
- `- Progress ownership remains with the coordinator; report memory-governance verification for tracking in ${dialect.tools.progressTool}.`
171
- ] : [];
172
- return [
173
- "External thoth-mem provider memory governance:",
174
- ...governance.rules.map((rule) => `- ${rule}`),
175
- ...harnessRules,
176
- `- Runtime enforcement: ${role.name === "orchestrator" ? "root-owned outcomes" : "instruction-level unless the target harness validates per-agent memory controls"}.`
177
- ].join("\n");
178
- }
179
124
  function memoryGovernanceDiagnostics(input) {
180
125
  const diagnostics = [];
181
126
  if (input.permissionControls !== "supported") {
@@ -448,12 +393,13 @@ var CODEX_SURFACES = [
448
393
  target: "permission-control",
449
394
  status: "unsupported",
450
395
  fields: [
451
- "named installed-role selection",
396
+ "conditional agent_type selection",
397
+ "role-prefixed fallback",
452
398
  "per-role permission enforcement",
453
399
  "per-agent tool allow/deny equivalent to OpenCode permissions"
454
400
  ],
455
401
  diagnosticCode: "codex.permission.memory.enforcement_gap",
456
- summary: "Named installed-role selection and per-role permission enforcement are instruction-only because collaboration.spawn_agent has no role selector; OpenCode-style per-agent MCP/tool deny maps are not validated.",
402
+ summary: "Static packages cannot guarantee active-runtime named-role selection: use collaboration.spawn_agent agent_type when exposed, otherwise the bounded role-prefixed fallback is instruction-only; per-role permission enforcement and OpenCode-style MCP/tool deny maps are not validated.",
457
403
  evidence: "Codex approvals/security docs cover sandbox and approval policies, not exact OpenCode per-agent permission maps.",
458
404
  fallback: "instruction-only"
459
405
  },
@@ -692,8 +638,8 @@ function stripProviderMcp(content) {
692
638
  }
693
639
  function pluginRootReference(pathValue) {
694
640
  const normalized = normalizePath(pathValue);
695
- const relative = normalized.slice(".codex-plugin/".length);
696
- return `./${relative}`;
641
+ const relative2 = normalized.slice(".codex-plugin/".length);
642
+ return `./${relative2}`;
697
643
  }
698
644
  function sha256(content) {
699
645
  return `sha256:${createHash("sha256").update(content).digest("hex")}`;
@@ -746,7 +692,7 @@ function codexHookPackageDiagnostic(code, message, severity = "warning") {
746
692
  function normalizeHookDefinition(hook) {
747
693
  return {
748
694
  command: hook.handler.command,
749
- ...hook.outputFields?.length ? { output: [...hook.outputFields].sort() } : {}
695
+ type: "command"
750
696
  };
751
697
  }
752
698
  function renderHookDefinitions(hookDefinitions) {
@@ -759,7 +705,10 @@ function renderHookDefinitions(hookDefinitions) {
759
705
  continue;
760
706
  }
761
707
  const eventHooks = hooksByEvent.get(validation.event) ?? [];
762
- eventHooks.push(normalizeHookDefinition(hook));
708
+ eventHooks.push({
709
+ ...hook.matcher !== void 0 ? { matcher: hook.matcher } : {},
710
+ hooks: [normalizeHookDefinition(hook)]
711
+ });
763
712
  hooksByEvent.set(validation.event, eventHooks);
764
713
  }
765
714
  if (hooksByEvent.size === 0) {
@@ -772,11 +721,11 @@ function renderHookDefinitions(hookDefinitions) {
772
721
  );
773
722
  return { diagnostics };
774
723
  }
775
- const content = stableJson(
776
- Object.fromEntries(
724
+ const content = stableJson({
725
+ hooks: Object.fromEntries(
777
726
  [...hooksByEvent.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([event, hooks]) => [event, hooks.map(stableValue)])
778
727
  )
779
- );
728
+ });
780
729
  return { content, diagnostics };
781
730
  }
782
731
  function parseRawHookContent(content) {
@@ -807,37 +756,66 @@ function parseRawHookContent(content) {
807
756
  ]
808
757
  };
809
758
  }
810
- 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)) {
811
771
  if (!Array.isArray(definitions)) {
812
772
  diagnostics.push(
813
773
  codexHookPackageDiagnostic(
814
774
  "codex.plugin.hooks.invalid_shape",
815
- `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.`
816
776
  )
817
777
  );
818
778
  continue;
819
779
  }
820
- for (const definition of definitions) {
821
- if (!isRecord(definition)) {
780
+ for (const group of definitions) {
781
+ if (!isRecord(group) || !Array.isArray(group.hooks)) {
822
782
  diagnostics.push(
823
783
  codexHookPackageDiagnostic(
824
784
  "codex.plugin.hooks.invalid_shape",
825
- `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.`
826
786
  )
827
787
  );
828
788
  continue;
829
789
  }
830
- const output = definition.output;
831
- hookDefinitions.push({
832
- event,
833
- handler: {
834
- type: typeof definition.type === "string" ? definition.type : "command",
835
- command: definition.command,
836
- async: definition.async
837
- },
838
- outputFields: Array.isArray(output) ? output.filter((field) => typeof field === "string") : void 0,
839
- interceptsToolExecution: definition.interceptsToolExecution === true
840
- });
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
+ }
841
819
  }
842
820
  }
843
821
  return { hookDefinitions, diagnostics };
@@ -1109,7 +1087,7 @@ function codexRuntimeGuidance() {
1109
1087
  "- The ambient Codex session is the adaptive root; no orchestrator child TOML is generated.",
1110
1088
  "- Delegate with `collaboration.spawn_agent` only when the root determines that specialization, context isolation, review, or independent parallel work creates a net gain.",
1111
1089
  "- Collaboration tools are direct tools and must not be called from inside `functions.exec`.",
1112
- "- Use a role-prefixed `task_name` and a self-contained English `message` with scope, anchors, constraints, verification, and the compact return contract. The current collaboration surface has no hard custom-role selector, so role selection remains instruction-level.",
1090
+ "- When the active `collaboration.spawn_agent` schema exposes `agent_type`, set it to the selected canonical role. When it does not, use a role-prefixed `task_name` plus a self-contained bounded envelope; that fallback is instruction-only.",
1113
1091
  "- Keep maximum depth 1: children do not delegate. Use one writer per mutable surface and parallelize only independent work.",
1114
1092
  "- A `collaboration.wait_agent` timeout is nonterminal; inspect `collaboration.list_agents` for the same task before rerouting or interrupting it.",
1115
1093
  "- Use `request_user_input` only for blocking material choices and always omit `autoResolutionMs` entirely.",
@@ -1121,15 +1099,8 @@ function codexRuntimeGuidance() {
1121
1099
  function codexRoleInstructions(role) {
1122
1100
  return [
1123
1101
  "<role-operational-contract>",
1124
- `- Role: ${role.name}`,
1125
- `- Mode: ${role.mode}`,
1126
- `- Scope: ${role.scope}`,
1127
- `- Responsibility: ${role.responsibility}`,
1128
- "- Use request_user_input for local blocking decisions.",
1129
1102
  "- Permissions, memory governance, runtime hooks, and provider-per-agent controls are instruction-level unless the active Codex runtime documents stronger enforcement.",
1130
1103
  `- ${role.name} runs as a Codex custom-agent TOML entry; the orchestrator remains the ambient Codex root session, not a generated role TOML.`,
1131
- ...role.toolGovernance.map((rule) => `- ${rule}`),
1132
- ...role.verification.map((rule) => `- ${rule}`),
1133
1104
  "</role-operational-contract>"
1134
1105
  ].join("\n");
1135
1106
  }
@@ -1142,8 +1113,7 @@ function roleInstructions(role, config) {
1142
1113
  config,
1143
1114
  model
1144
1115
  }),
1145
- codexRoleInstructions(role),
1146
- renderMemoryGovernanceInstructions(role, CODEX_PROMPT_DIALECT)
1116
+ codexRoleInstructions(role)
1147
1117
  ].join("\n\n");
1148
1118
  }
1149
1119
  function renderCodexRootInstructions(config) {
@@ -1189,7 +1159,7 @@ function renderAgentArtifacts({ config }) {
1189
1159
  surfaceId: "project-agent-toml",
1190
1160
  values: {
1191
1161
  name: role.name,
1192
- description: role.responsibility,
1162
+ description: renderAgentRoutingDescription(role),
1193
1163
  developer_instructions: roleInstructions(role, config),
1194
1164
  ...model ? { model } : {},
1195
1165
  ...effort ? { model_reasoning_effort: effort } : {},
@@ -1825,7 +1795,7 @@ function buildCodexSetupPlan(config) {
1825
1795
  configPath: targets.configPath,
1826
1796
  pluginId: config.pluginId,
1827
1797
  diagnostics: [
1828
- "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.",
1829
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.",
1830
1800
  "The /hooks surface may show hooks from independently installed plugins; this thoth-agents setup plan does not install provider hooks.",
1831
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.",
@@ -2005,6 +1975,15 @@ var CLAUDE_CODE_SUBAGENT_DEFAULT_MODELS = {
2005
1975
  quick: "haiku",
2006
1976
  deep: "sonnet"
2007
1977
  };
1978
+ var CLAUDE_CODE_SUBAGENT_DEFAULT_EFFORTS = {
1979
+ explorer: "low",
1980
+ quick: "low",
1981
+ designer: "medium",
1982
+ deep: "medium",
1983
+ librarian: "high",
1984
+ oracle: "high"
1985
+ };
1986
+ var CLAUDE_CODE_EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "max"]);
2008
1987
  function isClaudeCodeSubagentName(name) {
2009
1988
  return name in CLAUDE_CODE_SUBAGENT_DEFAULT_MODELS;
2010
1989
  }
@@ -2014,20 +1993,18 @@ function getClaudeCodeAgentModel(role, config) {
2014
1993
  if (override && isClaudeCodeModel(override)) return override;
2015
1994
  return CLAUDE_CODE_SUBAGENT_DEFAULT_MODELS[role.name];
2016
1995
  }
1996
+ function getClaudeCodeAgentEffort(role, config) {
1997
+ if (!isClaudeCodeSubagentName(role.name)) return void 0;
1998
+ const configured = config?.agents?.[role.name]?.variant;
1999
+ return configured && CLAUDE_CODE_EFFORTS.has(configured) ? configured : CLAUDE_CODE_SUBAGENT_DEFAULT_EFFORTS[role.name];
2000
+ }
2017
2001
  function claudeCodeRoleInstructions(role) {
2018
2002
  return [
2019
2003
  "<role-operational-contract>",
2020
- `- Role: ${role.name}`,
2021
- `- Mode: ${role.mode}`,
2022
- `- Scope: ${role.scope}`,
2023
- `- Responsibility: ${role.responsibility}`,
2024
- "- Use AskUserQuestion for local blocking decisions.",
2025
2004
  `- ${role.name} runs as an auto-discovered Claude Code plugin subagent invoked via Agent(subagent_type: ${claudeCodeSubagentType(role.name)}); plugin subagents are namespaced with the plugin name. The orchestrator is the main Claude Code session.`,
2026
2005
  ...role.mode === "read-only" ? [
2027
2006
  "- Write and Edit are denied in frontmatter while all other inherited tools, including MCP tools, remain available."
2028
2007
  ] : [],
2029
- ...role.toolGovernance.map((rule) => `- ${rule}`),
2030
- ...role.verification.map((rule) => `- ${rule}`),
2031
2008
  "</role-operational-contract>"
2032
2009
  ].join("\n");
2033
2010
  }
@@ -2040,8 +2017,7 @@ function roleInstructions2(role, config) {
2040
2017
  config,
2041
2018
  model
2042
2019
  }),
2043
- claudeCodeRoleInstructions(role),
2044
- renderMemoryGovernanceInstructions(role, CLAUDE_CODE_PROMPT_DIALECT)
2020
+ claudeCodeRoleInstructions(role)
2045
2021
  ].join("\n\n");
2046
2022
  }
2047
2023
  function renderClaudeCodeRootInstructions(config) {
@@ -2111,8 +2087,9 @@ function renderSubagentArtifacts(config) {
2111
2087
  )) {
2112
2088
  const content = renderClaudeCodeSubagent({
2113
2089
  name: role.name,
2114
- description: role.responsibility,
2090
+ description: renderAgentRoutingDescription(role),
2115
2091
  model: getClaudeCodeAgentModel(role, config),
2092
+ effort: getClaudeCodeAgentEffort(role, config),
2116
2093
  ...role.mode === "read-only" ? { disallowedTools: "Write, Edit" } : {},
2117
2094
  instructions: roleInstructions2(role, config)
2118
2095
  });
@@ -2194,11 +2171,18 @@ var CLAUDE_CODE_ROLE_NAMES = [
2194
2171
  ];
2195
2172
 
2196
2173
  // src/cli/claude-code-install.ts
2197
- var MARKETPLACE_NAME = "thoth-agents";
2198
- var MARKETPLACE_SOURCE = "EremesNG/thoth-agents";
2199
- var PLUGIN_ID = "thoth-agents@thoth-agents";
2200
- var MARKETPLACE_TARGET = "claude://marketplaces/thoth-agents";
2201
- 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}`;
2202
2186
  function defaultCommandExecutor(command, args, options) {
2203
2187
  const result = spawnSync(command, [...args], {
2204
2188
  cwd: options.cwd,
@@ -2226,12 +2210,26 @@ function parseJsonRecords(content) {
2226
2210
  }
2227
2211
  }
2228
2212
  function isCanonicalMarketplace(entry) {
2229
- if (entry.name !== MARKETPLACE_NAME) return false;
2230
- if (entry.repo === MARKETPLACE_SOURCE || entry.source === MARKETPLACE_SOURCE) {
2231
- 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);
2232
2222
  }
2233
- const source = entry.source;
2234
- 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
+ );
2235
2233
  }
2236
2234
  function inspectClaudeManager(executor, cwd, scope) {
2237
2235
  const marketplaceResult = executor(
@@ -2267,6 +2265,19 @@ function inspectClaudeManager(executor, cwd, scope) {
2267
2265
  diagnostics
2268
2266
  };
2269
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
+ }
2270
2281
  const namedMarketplaces = marketplaces.filter(
2271
2282
  (entry) => entry.name === MARKETPLACE_NAME
2272
2283
  );
@@ -2276,7 +2287,7 @@ function inspectClaudeManager(executor, cwd, scope) {
2276
2287
  }
2277
2288
  if (marketplace === "conflict") {
2278
2289
  diagnostics.push(
2279
- "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."
2280
2291
  );
2281
2292
  }
2282
2293
  const matchingPlugins = plugins.filter(
@@ -2301,7 +2312,7 @@ function commandItem(action, scope, projectRoot) {
2301
2312
  kind: "native-marketplace",
2302
2313
  action,
2303
2314
  targetPath: MARKETPLACE_TARGET,
2304
- description: "Register the package-owned thoth-agents Claude marketplace.",
2315
+ description: "Register the central Thoth Claude marketplace.",
2305
2316
  requiresBackup: false,
2306
2317
  command: {
2307
2318
  executable: "claude",
@@ -2309,7 +2320,7 @@ function commandItem(action, scope, projectRoot) {
2309
2320
  "plugin",
2310
2321
  "marketplace",
2311
2322
  "add",
2312
- MARKETPLACE_SOURCE,
2323
+ MARKETPLACE_INSTALL_SOURCE,
2313
2324
  "--scope",
2314
2325
  scope
2315
2326
  ],
@@ -2383,7 +2394,7 @@ function buildClaudeCodeSetupPlan(config) {
2383
2394
  commandExecutor,
2384
2395
  diagnostics: [
2385
2396
  ...inspection.diagnostics,
2386
- "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.",
2387
2398
  "Restart Claude Code or run /reload-plugins after installation; use /plugin to inspect marketplace and plugin state.",
2388
2399
  "Provider capability is owned by the external provider and is not established by this thoth-agents setup plan."
2389
2400
  ],
@@ -3604,16 +3615,52 @@ function defaultClaudeCodeModelRoles() {
3604
3615
  }
3605
3616
 
3606
3617
  // src/cli/operations/codex.ts
3607
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3618
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
3608
3619
  import { basename as basename2 } from "path";
3609
3620
 
3610
3621
  // src/cli/codex-plugin-install.ts
3611
3622
  import { spawnSync as spawnSync4 } from "child_process";
3612
- var MARKETPLACE_NAME2 = "thoth-agents";
3613
- var MARKETPLACE_SOURCE2 = "EremesNG/thoth-agents";
3614
- var PLUGIN_ID2 = "thoth-agents@thoth-agents";
3615
- var MARKETPLACE_TARGET2 = "codex://marketplaces/thoth-agents";
3616
- 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}`;
3617
3664
  function getCodexCommand(args, options = {}) {
3618
3665
  if ((options.platform ?? process.platform) !== "win32") {
3619
3666
  return { command: "codex", args: [...args] };
@@ -3651,10 +3698,153 @@ function parseManagerRecord(content) {
3651
3698
  return null;
3652
3699
  }
3653
3700
  }
3654
- function normalizeMarketplaceSource(value) {
3655
- 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();
3656
3703
  }
3657
- 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) {
3658
3848
  const sources = [entry.repo, entry.source];
3659
3849
  if (isRecord5(entry.marketplaceSource)) {
3660
3850
  sources.push(entry.marketplaceSource.repo, entry.marketplaceSource.source);
@@ -3664,13 +3854,16 @@ function marketplaceSources(entry) {
3664
3854
  );
3665
3855
  }
3666
3856
  function isCanonicalMarketplace2(entry) {
3667
- if (entry.name !== MARKETPLACE_NAME2) return false;
3668
- const canonical = MARKETPLACE_SOURCE2.toLowerCase();
3669
- return marketplaceSources(entry).some(
3670
- (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
3671
3864
  );
3672
3865
  }
3673
- function inspectCodexManager(executor, cwd) {
3866
+ function inspectCodexManager(executor, cwd, expectedVersion) {
3674
3867
  const marketplaceResult = executor(
3675
3868
  "codex",
3676
3869
  ["plugin", "marketplace", "list", "--json"],
@@ -3690,6 +3883,9 @@ function inspectCodexManager(executor, cwd) {
3690
3883
  ready: false,
3691
3884
  marketplace: "unknown",
3692
3885
  plugin: "unknown",
3886
+ legacyPluginIds: [],
3887
+ legacyMarketplaceNames: [],
3888
+ legacyMarketplaceConflicts: [],
3693
3889
  diagnostics
3694
3890
  };
3695
3891
  }
@@ -3705,9 +3901,35 @@ function inspectCodexManager(executor, cwd) {
3705
3901
  ready: false,
3706
3902
  marketplace: "unknown",
3707
3903
  plugin: "unknown",
3904
+ legacyPluginIds: [],
3905
+ legacyMarketplaceNames: [],
3906
+ legacyMarketplaceConflicts: [],
3708
3907
  diagnostics
3709
3908
  };
3710
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
+ }
3711
3933
  const namedMarketplaces = marketplaces.filter(
3712
3934
  (entry) => entry.name === MARKETPLACE_NAME2
3713
3935
  );
@@ -3717,31 +3939,81 @@ function inspectCodexManager(executor, cwd) {
3717
3939
  }
3718
3940
  if (marketplace === "conflict") {
3719
3941
  diagnostics.push(
3720
- "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."
3721
3943
  );
3722
3944
  }
3723
3945
  const matchingPlugins = installedPlugins.filter(
3724
3946
  (entry) => entry.pluginId === PLUGIN_ID2
3725
3947
  );
3726
3948
  let plugin = "absent";
3727
- if (matchingPlugins.some((entry) => entry.enabled === true)) {
3949
+ if (matchingPlugins.some(
3950
+ (entry) => entry.enabled === true && entry.version === expectedVersion
3951
+ )) {
3728
3952
  plugin = "installed";
3953
+ } else if (matchingPlugins.some((entry) => entry.enabled === true)) {
3954
+ plugin = "outdated";
3729
3955
  } else if (matchingPlugins.length > 0) {
3730
3956
  plugin = "disabled";
3731
3957
  }
3732
3958
  return {
3733
- ready: marketplace !== "conflict",
3959
+ ready: marketplace !== "conflict" && legacyMarketplaceConflicts.length === 0,
3734
3960
  marketplace,
3735
3961
  plugin,
3962
+ legacyPluginIds,
3963
+ legacyMarketplaceNames,
3964
+ legacyMarketplaceConflicts,
3736
3965
  diagnostics
3737
3966
  };
3738
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
+ }
3739
4011
  function commandItem2(action, projectRoot) {
3740
4012
  if (action === "register-marketplace") {
3741
4013
  return {
3742
4014
  action,
3743
4015
  targetPath: MARKETPLACE_TARGET2,
3744
- description: "Register the package-owned thoth-agents Codex marketplace.",
4016
+ description: "Register the central Thoth Codex marketplace.",
3745
4017
  command: {
3746
4018
  executable: "codex",
3747
4019
  args: ["plugin", "marketplace", "add", MARKETPLACE_SOURCE2, "--json"],
@@ -3762,25 +4034,54 @@ function commandItem2(action, projectRoot) {
3762
4034
  }
3763
4035
  function buildCodexPluginSetupPlan(config) {
3764
4036
  const commandExecutor = config.commandExecutor ?? defaultCommandExecutor3;
3765
- const inspection = inspectCodexManager(commandExecutor, config.projectRoot);
3766
- const items = [];
4037
+ const inspection = inspectCodexManager(
4038
+ commandExecutor,
4039
+ config.projectRoot,
4040
+ config.expectedVersion
4041
+ );
4042
+ let cleanupPreflight;
4043
+ let cleanupPreflightError;
3767
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) {
3768
4056
  if (inspection.marketplace === "absent") {
3769
4057
  items.push(commandItem2("register-marketplace", config.projectRoot));
3770
4058
  }
3771
- if (inspection.plugin === "absent" || inspection.plugin === "disabled") {
4059
+ if (inspection.plugin === "absent" || inspection.plugin === "disabled" || inspection.plugin === "outdated") {
3772
4060
  items.push(commandItem2("install-plugin", config.projectRoot));
3773
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
+ );
3774
4071
  }
3775
4072
  return {
4073
+ codexHome: cleanupPreflight?.codexHome ?? getCodexHome({ codexHome: config.codexHome, homeDir: config.homeDir }),
3776
4074
  dryRun: config.dryRun === true,
4075
+ expectedVersion: config.expectedVersion,
3777
4076
  items,
3778
- ready: inspection.ready,
4077
+ ready,
3779
4078
  projectRoot: config.projectRoot,
3780
4079
  commandExecutor,
3781
4080
  diagnostics: [
3782
4081
  ...inspection.diagnostics,
3783
- "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.",
3784
4085
  "Restart Codex after installation; use /plugins to inspect marketplace and plugin state."
3785
4086
  ]
3786
4087
  };
@@ -3804,12 +4105,28 @@ function applyCodexPluginSetup(plan) {
3804
4105
  };
3805
4106
  }
3806
4107
  if (plan.dryRun) return { success: true, changed, diagnostics };
3807
- for (const item of plan.items) {
3808
- const result = plan.commandExecutor(
3809
- item.command.executable,
3810
- item.command.args,
3811
- { cwd: item.command.cwd }
3812
- );
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
+ });
3813
4130
  if (result.exitCode !== 0) {
3814
4131
  return {
3815
4132
  success: false,
@@ -3822,10 +4139,11 @@ function applyCodexPluginSetup(plan) {
3822
4139
  }
3823
4140
  const inspection = inspectCodexManager(
3824
4141
  plan.commandExecutor,
3825
- plan.projectRoot
4142
+ plan.projectRoot,
4143
+ plan.expectedVersion
3826
4144
  );
3827
4145
  diagnostics.push(...inspection.diagnostics);
3828
- if (inspection.marketplace !== "installed" || inspection.plugin !== "installed") {
4146
+ if (!centralPluginVerified(inspection)) {
3829
4147
  return {
3830
4148
  success: false,
3831
4149
  changed,
@@ -3833,11 +4151,112 @@ function applyCodexPluginSetup(plan) {
3833
4151
  error: "Codex did not verify the expected marketplace and enabled plugin after mutation."
3834
4152
  };
3835
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
+ }
3836
4255
  return { success: true, changed, diagnostics: uniqueMessages3(diagnostics) };
3837
4256
  }
3838
4257
  function formatCodexPluginSetupPlan(plan) {
3839
4258
  const lines = plan.items.map(
3840
- (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}`
3841
4260
  );
3842
4261
  return ["Codex plugin setup plan:", ...lines].join("\n");
3843
4262
  }
@@ -3965,7 +4384,7 @@ function backupForItem(item) {
3965
4384
  function rootBlockState(item) {
3966
4385
  if (!existsSync8(item.targetPath))
3967
4386
  return { state: "missing", observed: "absent" };
3968
- const content = readFileSync6(item.targetPath, "utf8");
4387
+ const content = readFileSync7(item.targetPath, "utf8");
3969
4388
  if (item.content && content.includes(item.content)) {
3970
4389
  return { state: "installed", observed: "managed root block present" };
3971
4390
  }
@@ -3978,11 +4397,11 @@ function managedModelState(item) {
3978
4397
  if (!existsSync8(item.targetPath))
3979
4398
  return { state: "missing", observed: "absent" };
3980
4399
  try {
3981
- const parsed = JSON.parse(readFileSync6(item.targetPath, "utf8"));
4400
+ const parsed = JSON.parse(readFileSync7(item.targetPath, "utf8"));
3982
4401
  if (parsed.version !== MANAGED_MODEL_STATE_VERSION || !parsed.models || typeof parsed.models !== "object" || Array.isArray(parsed.models)) {
3983
4402
  return { state: "unknown", observed: "invalid managed model state" };
3984
4403
  }
3985
- 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" };
3986
4405
  } catch {
3987
4406
  return { state: "unknown", observed: "unparseable managed model state" };
3988
4407
  }
@@ -3990,7 +4409,7 @@ function managedModelState(item) {
3990
4409
  function userConfigState(item) {
3991
4410
  if (!existsSync8(item.targetPath))
3992
4411
  return { state: "missing", observed: "absent" };
3993
- const content = readFileSync6(item.targetPath, "utf8");
4412
+ const content = readFileSync7(item.targetPath, "utf8");
3994
4413
  if (/^\s*default_mode_request_user_input\s*=\s*true\b/m.test(content)) {
3995
4414
  return {
3996
4415
  state: "installed",
@@ -4005,7 +4424,7 @@ function userConfigState(item) {
4005
4424
  function contentState(item) {
4006
4425
  if (!existsSync8(item.targetPath))
4007
4426
  return { state: "missing", observed: "absent" };
4008
- const observed = readFileSync6(item.targetPath, "utf8");
4427
+ const observed = readFileSync7(item.targetPath, "utf8");
4009
4428
  if (item.content === observed)
4010
4429
  return { state: "installed", observed: "current" };
4011
4430
  if (item.role) {
@@ -4172,7 +4591,7 @@ function planItemFromPluginSetup(item) {
4172
4591
  path: item.targetPath,
4173
4592
  label: "Codex native plugin manager target"
4174
4593
  },
4175
- 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}`,
4176
4595
  backup: { required: false, strategy: "external" }
4177
4596
  };
4178
4597
  }
@@ -4270,7 +4689,10 @@ function buildCompleteCodexPlan(action, context2) {
4270
4689
  const packageVersion = resolveVersion();
4271
4690
  const buildPlugin = context2.buildCodexPluginSetupPlan ?? buildCodexPluginSetupPlan;
4272
4691
  const pluginPlan = buildPlugin({
4692
+ codexHome: context2.codexHome,
4273
4693
  dryRun: true,
4694
+ expectedVersion: packageVersion.ok ? packageVersion.version : "0.0.0",
4695
+ homeDir: context2.homeDir,
4274
4696
  projectRoot: context2.cwd,
4275
4697
  commandExecutor: context2.codexPluginCommandExecutor
4276
4698
  });
@@ -4666,7 +5088,7 @@ function applyCodexPlan(plan) {
4666
5088
  // src/cli/operations/opencode.ts
4667
5089
  import { createHash as createHash3 } from "crypto";
4668
5090
  import { existsSync as existsSync10 } from "fs";
4669
- import { join as join5 } from "path";
5091
+ import { join as join6 } from "path";
4670
5092
 
4671
5093
  // src/cli/opencode-effort.ts
4672
5094
  var OPENAI_GPT_RUNTIME_VARIANTS = /* @__PURE__ */ new Set([
@@ -4705,11 +5127,11 @@ import {
4705
5127
  mkdirSync as mkdirSync4,
4706
5128
  mkdtempSync,
4707
5129
  renameSync as renameSync3,
4708
- rmSync as rmSync2,
5130
+ rmSync as rmSync3,
4709
5131
  statSync
4710
5132
  } from "fs";
4711
5133
  import { homedir as homedir4 } from "os";
4712
- import { dirname as dirname5, join as join4 } from "path";
5134
+ import { dirname as dirname5, join as join5 } from "path";
4713
5135
  import { fileURLToPath as fileURLToPath4 } from "url";
4714
5136
 
4715
5137
  // src/harness/core/owned-skills.ts
@@ -4739,8 +5161,8 @@ function getOpenCodeOwnedSkillEntries(options = {}) {
4739
5161
  const homeDir = options.homeDir ?? homedir4();
4740
5162
  return THOTH_OWNED_SKILL_NAMES.map((name) => ({
4741
5163
  name,
4742
- sourcePath: join4(packageRoot, "skills", name),
4743
- destinationPath: join4(homeDir, ".config", "opencode", "skills", name)
5164
+ sourcePath: join5(packageRoot, "skills", name),
5165
+ destinationPath: join5(homeDir, ".config", "opencode", "skills", name)
4744
5166
  }));
4745
5167
  }
4746
5168
  function validateCanonicalSkill(entry) {
@@ -4749,7 +5171,7 @@ function validateCanonicalSkill(entry) {
4749
5171
  `Canonical owned skill directory is missing: ${entry.name}`
4750
5172
  );
4751
5173
  }
4752
- const contractPath = join4(entry.sourcePath, "SKILL.md");
5174
+ const contractPath = join5(entry.sourcePath, "SKILL.md");
4753
5175
  if (!existsSync9(contractPath) || !statSync(contractPath).isFile()) {
4754
5176
  throw new Error(`Canonical owned skill contract is missing: ${entry.name}`);
4755
5177
  }
@@ -4759,21 +5181,21 @@ function installOwnedSkills(skills) {
4759
5181
  if (!firstSkill) return;
4760
5182
  const globalRoot = dirname5(firstSkill.destinationPath);
4761
5183
  mkdirSync4(globalRoot, { recursive: true });
4762
- const transactionRoot = mkdtempSync(join4(globalRoot, ".thoth-agents-owned-"));
4763
- const stagedRoot = join4(transactionRoot, "staged");
4764
- 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");
4765
5187
  mkdirSync4(stagedRoot, { recursive: true });
4766
5188
  mkdirSync4(backupRoot, { recursive: true });
4767
5189
  try {
4768
5190
  for (const skill of skills) {
4769
- cpSync(skill.sourcePath, join4(stagedRoot, skill.name), {
5191
+ cpSync(skill.sourcePath, join5(stagedRoot, skill.name), {
4770
5192
  recursive: true,
4771
5193
  force: true
4772
5194
  });
4773
5195
  }
4774
5196
  for (const skill of skills) {
4775
- const stagedPath = join4(stagedRoot, skill.name);
4776
- const backupPath = join4(backupRoot, skill.name);
5197
+ const stagedPath = join5(stagedRoot, skill.name);
5198
+ const backupPath = join5(backupRoot, skill.name);
4777
5199
  const hadExistingDestination = existsSync9(skill.destinationPath);
4778
5200
  if (hadExistingDestination) {
4779
5201
  renameSync3(skill.destinationPath, backupPath);
@@ -4782,7 +5204,7 @@ function installOwnedSkills(skills) {
4782
5204
  renameSync3(stagedPath, skill.destinationPath);
4783
5205
  } catch (error) {
4784
5206
  if (existsSync9(skill.destinationPath)) {
4785
- rmSync2(skill.destinationPath, { recursive: true, force: true });
5207
+ rmSync3(skill.destinationPath, { recursive: true, force: true });
4786
5208
  }
4787
5209
  if (hadExistingDestination && existsSync9(backupPath)) {
4788
5210
  renameSync3(backupPath, skill.destinationPath);
@@ -4790,11 +5212,11 @@ function installOwnedSkills(skills) {
4790
5212
  throw error;
4791
5213
  }
4792
5214
  if (existsSync9(backupPath)) {
4793
- rmSync2(backupPath, { recursive: true, force: true });
5215
+ rmSync3(backupPath, { recursive: true, force: true });
4794
5216
  }
4795
5217
  }
4796
5218
  } finally {
4797
- rmSync2(transactionRoot, { recursive: true, force: true });
5219
+ rmSync3(transactionRoot, { recursive: true, force: true });
4798
5220
  }
4799
5221
  }
4800
5222
  function syncOpenCodeOwnedSkills(options = {}) {
@@ -5055,7 +5477,7 @@ function titleCaseSkillName(value) {
5055
5477
  }).join("-");
5056
5478
  }
5057
5479
  function homeDirFromContext(context2) {
5058
- 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);
5059
5481
  }
5060
5482
  function openCodeSkillTargets(context2) {
5061
5483
  const homeDir = homeDirFromContext(context2);
@@ -5079,7 +5501,7 @@ function openCodeSkillTargets(context2) {
5079
5501
  const ownedTargets = getOpenCodeOwnedSkillEntries({
5080
5502
  homeDir
5081
5503
  }).map((skill) => {
5082
- const path = join5(skill.destinationPath, "SKILL.md");
5504
+ const path = join6(skill.destinationPath, "SKILL.md");
5083
5505
  const installed = existsSync10(path);
5084
5506
  return {
5085
5507
  kind: "skill",
@@ -6462,7 +6884,7 @@ import { execFileSync } from "child_process";
6462
6884
  import {
6463
6885
  existsSync as existsSync11,
6464
6886
  mkdirSync as mkdirSync5,
6465
- readFileSync as readFileSync7,
6887
+ readFileSync as readFileSync8,
6466
6888
  renameSync as renameSync4,
6467
6889
  unlinkSync,
6468
6890
  writeFileSync as writeFileSync4
@@ -6500,7 +6922,7 @@ function resolveModelsDevCachePath(options = {}) {
6500
6922
  function readModelsDevCache(cachePath) {
6501
6923
  if (!existsSync11(cachePath)) return void 0;
6502
6924
  try {
6503
- const input = JSON.parse(readFileSync7(cachePath, "utf8"));
6925
+ const input = JSON.parse(readFileSync8(cachePath, "utf8"));
6504
6926
  const parsed = modelsDevCacheSchema.safeParse(input);
6505
6927
  return parsed.success ? parsed.data : void 0;
6506
6928
  } catch {
@@ -6714,7 +7136,7 @@ var CODEX_DOCUMENTED_EFFORTS2 = /* @__PURE__ */ new Set([
6714
7136
  "max",
6715
7137
  "ultra"
6716
7138
  ]);
6717
- var CLAUDE_CODE_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
7139
+ var CLAUDE_CODE_EFFORTS2 = ["low", "medium", "high", "xhigh", "max"];
6718
7140
  function parseOpenCodeModels(output) {
6719
7141
  const seen = /* @__PURE__ */ new Set();
6720
7142
  const options = [];
@@ -6747,7 +7169,7 @@ function writableEfforts(harness, option) {
6747
7169
  }
6748
7170
  if (harness === "claude") {
6749
7171
  return option.efforts.filter(
6750
- (effort) => CLAUDE_CODE_EFFORTS.includes(effort)
7172
+ (effort) => CLAUDE_CODE_EFFORTS2.includes(effort)
6751
7173
  );
6752
7174
  }
6753
7175
  return option.efforts.filter(
@@ -6798,7 +7220,7 @@ var CLAUDE_CODE_MODEL_OPTIONS = [
6798
7220
  }
6799
7221
  ].map((option) => ({
6800
7222
  ...option,
6801
- efforts: option.id === "inherit" ? [] : [...CLAUDE_CODE_EFFORTS],
7223
+ efforts: option.id === "inherit" ? [] : [...CLAUDE_CODE_EFFORTS2],
6802
7224
  source: "manual"
6803
7225
  }));
6804
7226
  async function getModelOptions(harness) {
@@ -6835,7 +7257,7 @@ async function getModelOptions(harness) {
6835
7257
  }
6836
7258
 
6837
7259
  // src/cli/tui/operations.ts
6838
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
7260
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
6839
7261
  var context = { cwd: process.cwd() };
6840
7262
  var codexContext = { cwd: process.cwd() };
6841
7263
  var claudeCodeContext = {
@@ -6877,7 +7299,7 @@ function getCodexModelRoles(source = codexContext) {
6877
7299
  const item = plan.items.find(
6878
7300
  (candidate) => candidate.action === "write-role-toml" && candidate.role === role
6879
7301
  );
6880
- 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;
6881
7303
  const effort = content ? parseRoleTomlEffort(content) : void 0;
6882
7304
  return {
6883
7305
  role,