thoth-agents 0.3.11 → 0.4.0

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 (74) hide show
  1. package/README.md +182 -389
  2. package/dist/agents/prompt-dialects.d.ts +4 -1
  3. package/dist/agents/prompt-sections.d.ts +2 -1
  4. package/dist/{chunk-CB3EBA7H.js → chunk-5EBXGMOK.js} +1 -1
  5. package/dist/chunk-62L3ORXN.js +30 -0
  6. package/dist/{chunk-GIUU6FNE.js → chunk-Q6SDM4KP.js} +208 -106
  7. package/dist/chunk-SECDI3TL.js +312 -0
  8. package/dist/{chunk-ZEAAWBF2.js → chunk-WJ2YMM2E.js} +2877 -290
  9. package/dist/cli/codex-plugin-install.d.ts +16 -2
  10. package/dist/cli/index.js +188 -15
  11. package/dist/cli/install-completion.d.ts +13 -0
  12. package/dist/cli/install-ledger.d.ts +1 -1
  13. package/dist/cli/install.d.ts +11 -1
  14. package/dist/cli/operations/pi.d.ts +43 -0
  15. package/dist/cli/owned-skills.d.ts +17 -0
  16. package/dist/cli/pi-effort.d.ts +10 -0
  17. package/dist/cli/pi-install.d.ts +131 -0
  18. package/dist/cli/pi-migration.d.ts +12 -0
  19. package/dist/cli/pi-native-probe.d.ts +34 -0
  20. package/dist/cli/pi-package-receipt.d.ts +59 -0
  21. package/dist/cli/pi-paths.d.ts +20 -0
  22. package/dist/cli/pi-resources.d.ts +15 -0
  23. package/dist/cli/skills.d.ts +1 -1
  24. package/dist/cli/tui/index.js +12 -4
  25. package/dist/cli/tui/operations.d.ts +1 -0
  26. package/dist/cli/types.d.ts +4 -2
  27. package/dist/harness/adapters/pi.d.ts +8 -0
  28. package/dist/harness/core/agent-pack.d.ts +26 -6
  29. package/dist/harness/core/sdd.d.ts +27 -15
  30. package/dist/harness/pi-specialists.d.ts +6 -0
  31. package/dist/harness/types.d.ts +2 -2
  32. package/dist/harness/writers/codex-plugin-package.d.ts +1 -0
  33. package/dist/harness/writers/pi-agent.d.ts +16 -0
  34. package/dist/index.js +6 -4
  35. package/dist/pi.d.ts +11 -0
  36. package/dist/pi.js +46 -0
  37. package/package.json +17 -6
  38. package/pi/.thoth-agents-assets.json +12 -0
  39. package/pi/agents/thoth-deep.md +81 -0
  40. package/pi/agents/thoth-designer.md +81 -0
  41. package/pi/agents/thoth-explorer.md +81 -0
  42. package/pi/agents/thoth-librarian.md +83 -0
  43. package/pi/agents/thoth-oracle.md +83 -0
  44. package/pi/agents/thoth-quick.md +81 -0
  45. package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +2 -2
  46. package/plugin/.claude-plugin/plugin.json +1 -1
  47. package/plugin/.codex-plugin/plugin.json +1 -1
  48. package/plugin/agents/oracle.md +3 -3
  49. package/plugin/agents/orchestrator.md +35 -30
  50. package/plugin/skills/plan-reviewer/SKILL.md +54 -15
  51. package/plugin/skills/thoth-archive/SKILL.md +5 -0
  52. package/plugin/skills/thoth-constitution/SKILL.md +5 -0
  53. package/plugin/skills/thoth-constitution/templates/constitution.md +19 -2
  54. package/plugin/skills/thoth-init/SKILL.md +5 -0
  55. package/plugin/skills/thoth-sdd/SKILL.md +35 -10
  56. package/plugin/skills/thoth-sdd/references/phases/implement.md +13 -2
  57. package/plugin/skills/thoth-sdd/references/phases/tasks.md +23 -2
  58. package/plugin/skills/thoth-sdd/references/phases/verify.md +7 -5
  59. package/plugin/skills/thoth-sdd/scripts/validate.mjs +263 -30
  60. package/plugin/skills/thoth-sdd/templates/tasks.md +18 -2
  61. package/skills/README.md +2 -1
  62. package/skills/plan-reviewer/SKILL.md +54 -15
  63. package/skills/thoth-archive/SKILL.md +5 -0
  64. package/skills/thoth-constitution/SKILL.md +5 -0
  65. package/skills/thoth-constitution/templates/constitution.md +19 -2
  66. package/skills/thoth-init/SKILL.md +5 -0
  67. package/skills/thoth-sdd/SKILL.md +35 -10
  68. package/skills/thoth-sdd/references/phases/implement.md +13 -2
  69. package/skills/thoth-sdd/references/phases/tasks.md +23 -2
  70. package/skills/thoth-sdd/references/phases/verify.md +7 -5
  71. package/skills/thoth-sdd/scripts/validate.mjs +263 -30
  72. package/skills/thoth-sdd/templates/tasks.md +18 -2
  73. package/.agents/plugins/marketplace.json +0 -20
  74. package/.claude-plugin/marketplace.json +0 -21
@@ -1,20 +1,26 @@
1
+ import {
2
+ CONTEXT7_MCP_URL,
3
+ GREP_APP_MCP_URL,
4
+ exa
5
+ } from "./chunk-62L3ORXN.js";
6
+ import {
7
+ PI_MANAGED_OWNER,
8
+ PI_ROOT_END,
9
+ PI_ROOT_START,
10
+ piAdapter,
11
+ renderConfiguredRolePrompt,
12
+ syncPiSpecialists
13
+ } from "./chunk-SECDI3TL.js";
1
14
  import {
2
15
  ALL_AGENT_NAMES,
3
16
  CLAUDE_CODE_PROMPT_DIALECT,
4
17
  CLAUDE_CODE_SUBAGENT_NAMESPACE,
5
18
  CODEX_PROMPT_DIALECT,
6
19
  CONFIRMED_OPENAI_SUBAGENT_PRESET,
7
- CONTEXT7_MCP_URL,
8
20
  DEFAULT_MODELS,
9
- GREP_APP_MCP_URL,
10
- appendPromptSections,
21
+ PI_SPECIALIST_ROLES,
11
22
  claudeCodeSubagentType,
12
- composeAgentPrompt,
13
- createModelFamilySection,
14
- createRolePromptSections,
15
- createStepBudgetSection,
16
23
  ensureConfigDir,
17
- exa,
18
24
  findPackageRoot,
19
25
  generateLiteConfig,
20
26
  getAgentOverride,
@@ -25,17 +31,16 @@ import {
25
31
  getExistingLiteConfigPath,
26
32
  getOpenCodeManagedModelStatePath,
27
33
  getPrimaryModelId,
34
+ isPiSpecialistRole,
28
35
  isValidPackageVersion,
29
- loadAgentPrompt,
30
36
  parseConfig,
37
+ piSpecialistName,
31
38
  renderAgentRoutingDescription,
32
- renderPromptSection,
33
- renderRolePrompt,
34
39
  resolveExecutingPackageVersion,
35
40
  updateOpenCodeMainConfig,
36
41
  writeConfig,
37
42
  writeLiteConfig
38
- } from "./chunk-GIUU6FNE.js";
43
+ } from "./chunk-Q6SDM4KP.js";
39
44
 
40
45
  // src/cli/codex-paths.ts
41
46
  import { homedir } from "os";
@@ -94,32 +99,6 @@ import { fileURLToPath as fileURLToPath2 } from "url";
94
99
  // src/harness/adapters/codex.ts
95
100
  import { fileURLToPath } from "url";
96
101
 
97
- // src/agents/configured-role-prompt.ts
98
- function renderConfiguredRolePrompt({
99
- role,
100
- dialect,
101
- config,
102
- model
103
- }) {
104
- const promptOverrides = loadAgentPrompt(role, config?.preset);
105
- const override = getAgentOverride(config, role);
106
- const modelSection = createModelFamilySection(role, model);
107
- const stepSection = createStepBudgetSection(override?.steps);
108
- const basePrompt = renderRolePrompt(createRolePromptSections(role), dialect);
109
- const prompt = composeAgentPrompt({
110
- basePrompt,
111
- customPrompt: promptOverrides.prompt,
112
- customAppendPrompt: appendPromptSections(
113
- modelSection ? renderPromptSection(modelSection, dialect) : void 0,
114
- promptOverrides.appendPrompt
115
- )
116
- });
117
- return appendPromptSections(
118
- prompt,
119
- stepSection ? renderPromptSection(stepSection, dialect) : void 0
120
- );
121
- }
122
-
123
102
  // src/harness/core/memory-governance.ts
124
103
  function memoryGovernanceDiagnostics(input) {
125
104
  const diagnostics = [];
@@ -638,8 +617,8 @@ function stripProviderMcp(content) {
638
617
  }
639
618
  function pluginRootReference(pathValue) {
640
619
  const normalized = normalizePath(pathValue);
641
- const relative = normalized.slice(".codex-plugin/".length);
642
- return `./${relative}`;
620
+ const relative2 = normalized.slice(".codex-plugin/".length);
621
+ return `./${relative2}`;
643
622
  }
644
623
  function sha256(content) {
645
624
  return `sha256:${createHash("sha256").update(content).digest("hex")}`;
@@ -692,7 +671,7 @@ function codexHookPackageDiagnostic(code, message, severity = "warning") {
692
671
  function normalizeHookDefinition(hook) {
693
672
  return {
694
673
  command: hook.handler.command,
695
- ...hook.outputFields?.length ? { output: [...hook.outputFields].sort() } : {}
674
+ type: "command"
696
675
  };
697
676
  }
698
677
  function renderHookDefinitions(hookDefinitions) {
@@ -705,7 +684,10 @@ function renderHookDefinitions(hookDefinitions) {
705
684
  continue;
706
685
  }
707
686
  const eventHooks = hooksByEvent.get(validation.event) ?? [];
708
- eventHooks.push(normalizeHookDefinition(hook));
687
+ eventHooks.push({
688
+ ...hook.matcher !== void 0 ? { matcher: hook.matcher } : {},
689
+ hooks: [normalizeHookDefinition(hook)]
690
+ });
709
691
  hooksByEvent.set(validation.event, eventHooks);
710
692
  }
711
693
  if (hooksByEvent.size === 0) {
@@ -718,11 +700,11 @@ function renderHookDefinitions(hookDefinitions) {
718
700
  );
719
701
  return { diagnostics };
720
702
  }
721
- const content = stableJson(
722
- Object.fromEntries(
703
+ const content = stableJson({
704
+ hooks: Object.fromEntries(
723
705
  [...hooksByEvent.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([event, hooks]) => [event, hooks.map(stableValue)])
724
706
  )
725
- );
707
+ });
726
708
  return { content, diagnostics };
727
709
  }
728
710
  function parseRawHookContent(content) {
@@ -753,37 +735,66 @@ function parseRawHookContent(content) {
753
735
  ]
754
736
  };
755
737
  }
756
- for (const [event, definitions] of Object.entries(parsed)) {
738
+ if (!isRecord(parsed.hooks)) {
739
+ return {
740
+ hookDefinitions,
741
+ diagnostics: [
742
+ codexHookPackageDiagnostic(
743
+ "codex.plugin.hooks.invalid_shape",
744
+ 'Skipping Codex plugin hooks asset because hooks.json must contain a nested "hooks" object.'
745
+ )
746
+ ]
747
+ };
748
+ }
749
+ for (const [event, definitions] of Object.entries(parsed.hooks)) {
757
750
  if (!Array.isArray(definitions)) {
758
751
  diagnostics.push(
759
752
  codexHookPackageDiagnostic(
760
753
  "codex.plugin.hooks.invalid_shape",
761
- `Skipping Codex plugin hook event "${event}" because its value is not an array of command handlers.`
754
+ `Skipping Codex plugin hook event "${event}" because its value is not an array of matcher groups.`
762
755
  )
763
756
  );
764
757
  continue;
765
758
  }
766
- for (const definition of definitions) {
767
- if (!isRecord(definition)) {
759
+ for (const group of definitions) {
760
+ if (!isRecord(group) || !Array.isArray(group.hooks)) {
768
761
  diagnostics.push(
769
762
  codexHookPackageDiagnostic(
770
763
  "codex.plugin.hooks.invalid_shape",
771
- `Skipping Codex plugin hook event "${event}" entry because it is not an object.`
764
+ `Skipping Codex plugin hook event "${event}" entry because it is not a matcher group with a hooks array.`
772
765
  )
773
766
  );
774
767
  continue;
775
768
  }
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
- });
769
+ if (group.matcher !== void 0 && typeof group.matcher !== "string") {
770
+ diagnostics.push(
771
+ codexHookPackageDiagnostic(
772
+ "codex.plugin.hooks.invalid_shape",
773
+ `Skipping Codex plugin hook event "${event}" matcher group because matcher is not a string.`
774
+ )
775
+ );
776
+ continue;
777
+ }
778
+ for (const definition of group.hooks) {
779
+ if (!isRecord(definition)) {
780
+ diagnostics.push(
781
+ codexHookPackageDiagnostic(
782
+ "codex.plugin.hooks.invalid_shape",
783
+ `Skipping Codex plugin hook event "${event}" handler because it is not an object.`
784
+ )
785
+ );
786
+ continue;
787
+ }
788
+ hookDefinitions.push({
789
+ event,
790
+ ...typeof group.matcher === "string" ? { matcher: group.matcher } : {},
791
+ handler: {
792
+ type: typeof definition.type === "string" ? definition.type : "command",
793
+ command: definition.command,
794
+ async: definition.async
795
+ }
796
+ });
797
+ }
787
798
  }
788
799
  }
789
800
  return { hookDefinitions, diagnostics };
@@ -921,9 +932,9 @@ function stripProviderMcpValues(surfaceId, values) {
921
932
  const { thoth_mem: _provider, ...mcpServers } = servers;
922
933
  return { ...values, mcp_servers: mcpServers };
923
934
  }
924
- function fieldOrder(fields, keys) {
935
+ function fieldOrder(fields, keys2) {
925
936
  const orderedRoots = fields.map((field) => field.split(".")[0]);
926
- return [...keys].sort((left, right) => {
937
+ return [...keys2].sort((left, right) => {
927
938
  const leftIndex = orderedRoots.indexOf(left);
928
939
  const rightIndex = orderedRoots.indexOf(right);
929
940
  if (leftIndex !== -1 || rightIndex !== -1) {
@@ -1763,7 +1774,7 @@ function buildCodexSetupPlan(config) {
1763
1774
  configPath: targets.configPath,
1764
1775
  pluginId: config.pluginId,
1765
1776
  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.",
1777
+ "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
1778
  "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
1779
  "The /hooks surface may show hooks from independently installed plugins; this thoth-agents setup plan does not install provider hooks.",
1769
1780
  "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 +2150,18 @@ var CLAUDE_CODE_ROLE_NAMES = [
2139
2150
  ];
2140
2151
 
2141
2152
  // 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";
2153
+ var PLUGIN_NAME = "thoth-agents";
2154
+ var MARKETPLACE_NAME = "thoth-plugins";
2155
+ var MARKETPLACE_SOURCE = "https://github.com/EremesNG/thoth-plugins.git";
2156
+ var MARKETPLACE_INSTALL_SOURCE = MARKETPLACE_SOURCE;
2157
+ var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
2158
+ var LEGACY_MARKETPLACE_SOURCE = "https://github.com/EremesNG/thoth-agents.git";
2159
+ var LEGACY_MARKETPLACE_NAMES = [PLUGIN_NAME, `${PLUGIN_NAME}-claude`];
2160
+ var LEGACY_PLUGIN_IDS = LEGACY_MARKETPLACE_NAMES.map(
2161
+ (name) => `${PLUGIN_NAME}@${name}`
2162
+ );
2163
+ var MARKETPLACE_TARGET = `claude://marketplaces/${MARKETPLACE_NAME}`;
2164
+ var PLUGIN_TARGET = `claude://plugins/${PLUGIN_ID}`;
2147
2165
  function defaultCommandExecutor(command, args, options) {
2148
2166
  const result = spawnSync(command, [...args], {
2149
2167
  cwd: options.cwd,
@@ -2171,12 +2189,26 @@ function parseJsonRecords(content) {
2171
2189
  }
2172
2190
  }
2173
2191
  function isCanonicalMarketplace(entry) {
2174
- if (entry.name !== MARKETPLACE_NAME) return false;
2175
- if (entry.repo === MARKETPLACE_SOURCE || entry.source === MARKETPLACE_SOURCE) {
2176
- return true;
2192
+ return isMarketplaceIdentity(entry, MARKETPLACE_NAME);
2193
+ }
2194
+ function normalizeMarketplaceSource(value) {
2195
+ 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();
2196
+ }
2197
+ function marketplaceSources(entry) {
2198
+ const sources = [entry.repo, entry.source];
2199
+ if (isRecord3(entry.marketplaceSource)) {
2200
+ sources.push(entry.marketplaceSource.repo, entry.marketplaceSource.source);
2177
2201
  }
2178
- const source = entry.source;
2179
- return isRecord3(source) && (source.repo === MARKETPLACE_SOURCE || source.source === MARKETPLACE_SOURCE);
2202
+ return sources.filter(
2203
+ (source) => typeof source === "string"
2204
+ );
2205
+ }
2206
+ function isMarketplaceIdentity(entry, name, source = MARKETPLACE_SOURCE) {
2207
+ if (entry.name !== name) return false;
2208
+ const canonical = normalizeMarketplaceSource(source);
2209
+ return marketplaceSources(entry).some(
2210
+ (source2) => normalizeMarketplaceSource(source2) === canonical
2211
+ );
2180
2212
  }
2181
2213
  function inspectClaudeManager(executor, cwd, scope) {
2182
2214
  const marketplaceResult = executor(
@@ -2212,6 +2244,19 @@ function inspectClaudeManager(executor, cwd, scope) {
2212
2244
  diagnostics
2213
2245
  };
2214
2246
  }
2247
+ const legacyMarketplace = LEGACY_MARKETPLACE_NAMES.some(
2248
+ (name) => marketplaces.some(
2249
+ (entry) => isMarketplaceIdentity(entry, name, LEGACY_MARKETPLACE_SOURCE)
2250
+ )
2251
+ );
2252
+ const hasLegacyState = legacyMarketplace || plugins.some(
2253
+ (entry) => typeof entry.id === "string" && LEGACY_PLUGIN_IDS.includes(entry.id)
2254
+ );
2255
+ if (hasLegacyState) {
2256
+ diagnostics.push(
2257
+ "Legacy Claude thoth-agents marketplace or plugin state was detected and preserved; the host-specific identity will be managed independently."
2258
+ );
2259
+ }
2215
2260
  const namedMarketplaces = marketplaces.filter(
2216
2261
  (entry) => entry.name === MARKETPLACE_NAME
2217
2262
  );
@@ -2221,7 +2266,7 @@ function inspectClaudeManager(executor, cwd, scope) {
2221
2266
  }
2222
2267
  if (marketplace === "conflict") {
2223
2268
  diagnostics.push(
2224
- "A Claude marketplace named thoth-agents is registered from a different source; resolve it through Claude Code before retrying."
2269
+ "A Claude marketplace named thoth-plugins is registered from a different source; resolve it through Claude Code before retrying."
2225
2270
  );
2226
2271
  }
2227
2272
  const matchingPlugins = plugins.filter(
@@ -2246,7 +2291,7 @@ function commandItem(action, scope, projectRoot) {
2246
2291
  kind: "native-marketplace",
2247
2292
  action,
2248
2293
  targetPath: MARKETPLACE_TARGET,
2249
- description: "Register the package-owned thoth-agents Claude marketplace.",
2294
+ description: "Register the central Thoth Claude marketplace.",
2250
2295
  requiresBackup: false,
2251
2296
  command: {
2252
2297
  executable: "claude",
@@ -2254,7 +2299,7 @@ function commandItem(action, scope, projectRoot) {
2254
2299
  "plugin",
2255
2300
  "marketplace",
2256
2301
  "add",
2257
- MARKETPLACE_SOURCE,
2302
+ MARKETPLACE_INSTALL_SOURCE,
2258
2303
  "--scope",
2259
2304
  scope
2260
2305
  ],
@@ -2328,7 +2373,7 @@ function buildClaudeCodeSetupPlan(config) {
2328
2373
  commandExecutor,
2329
2374
  diagnostics: [
2330
2375
  ...inspection.diagnostics,
2331
- "Claude Code installs thoth-agents from EremesNG/thoth-agents through its native marketplace and owns the cached plugin files.",
2376
+ "Claude Code installs thoth-agents from the central thoth-plugins marketplace, while the thoth-agents repository remains the pinned plugin source.",
2332
2377
  "Restart Claude Code or run /reload-plugins after installation; use /plugin to inspect marketplace and plugin state.",
2333
2378
  "Provider capability is owned by the external provider and is not established by this thoth-agents setup plan."
2334
2379
  ],
@@ -2416,11 +2461,12 @@ var INSTALL_LEDGER_SCHEMA_VERSION = 1;
2416
2461
  var INSTALL_HARNESSES = /* @__PURE__ */ new Set([
2417
2462
  "opencode",
2418
2463
  "codex",
2419
- "claude"
2464
+ "claude",
2465
+ "pi"
2420
2466
  ]);
2421
- function hasOnlyKeys(value, keys) {
2467
+ function hasOnlyKeys(value, keys2) {
2422
2468
  const actual = Object.keys(value);
2423
- return actual.length === keys.length && actual.every((key) => keys.includes(key));
2469
+ return actual.length === keys2.length && actual.every((key) => keys2.includes(key));
2424
2470
  }
2425
2471
  function parseInstallLedger(serialized) {
2426
2472
  let parsed;
@@ -2612,8 +2658,6 @@ function getThothMemSetupCommand(harness, dryRun, options = {}) {
2612
2658
  "thoth-mem@latest",
2613
2659
  "setup",
2614
2660
  harness,
2615
- "--scope",
2616
- "global",
2617
2661
  ...dryRun ? ["--plan"] : [],
2618
2662
  "--json"
2619
2663
  ],
@@ -2691,26 +2735,11 @@ function runThothMemSetup(options) {
2691
2735
  function isConsistentProviderSuccess(result, harness) {
2692
2736
  return result.success && result.evidenceValid && result.status === "complete" && result.exitCode === 0 && result.harness === harness;
2693
2737
  }
2694
- function finalizeHarnessInstall(options) {
2738
+ function recordHarnessInstallCompletion(options) {
2695
2739
  const ledgerPath = getInstallLedgerPath(options.ledgerOptions);
2696
- const setup = options.runThothMemSetup ?? runThothMemSetup;
2697
- const provider = setup({
2698
- harness: options.harness,
2699
- dryRun: options.dryRun,
2700
- cwd: options.cwd
2701
- });
2702
- if (!isConsistentProviderSuccess(provider, options.harness)) {
2703
- return {
2704
- success: false,
2705
- provider,
2706
- ledger: { status: "not-attempted", path: ledgerPath },
2707
- error: provider.error ?? "thoth-mem setup did not return consistent complete evidence."
2708
- };
2709
- }
2710
2740
  if (options.dryRun) {
2711
2741
  return {
2712
2742
  success: true,
2713
- provider,
2714
2743
  ledger: { status: "planned", path: ledgerPath }
2715
2744
  };
2716
2745
  }
@@ -2723,7 +2752,6 @@ function finalizeHarnessInstall(options) {
2723
2752
  if (!ledger.success) {
2724
2753
  return {
2725
2754
  success: false,
2726
- provider,
2727
2755
  ledger: {
2728
2756
  status: "failed",
2729
2757
  path: ledger.path,
@@ -2734,7 +2762,6 @@ function finalizeHarnessInstall(options) {
2734
2762
  }
2735
2763
  return {
2736
2764
  success: true,
2737
- provider,
2738
2765
  ledger: {
2739
2766
  status: "recorded",
2740
2767
  path: ledger.path,
@@ -2743,6 +2770,34 @@ function finalizeHarnessInstall(options) {
2743
2770
  }
2744
2771
  };
2745
2772
  }
2773
+ function finalizeHarnessInstall(options) {
2774
+ const ledgerPath = getInstallLedgerPath(options.ledgerOptions);
2775
+ const setup = options.runThothMemSetup ?? runThothMemSetup;
2776
+ const provider = setup({
2777
+ harness: options.harness,
2778
+ dryRun: options.dryRun,
2779
+ cwd: options.cwd
2780
+ });
2781
+ if (!isConsistentProviderSuccess(provider, options.harness)) {
2782
+ return {
2783
+ success: false,
2784
+ provider,
2785
+ ledger: { status: "not-attempted", path: ledgerPath },
2786
+ error: provider.error ?? "thoth-mem setup did not return consistent complete evidence."
2787
+ };
2788
+ }
2789
+ const completion = recordHarnessInstallCompletion({
2790
+ harness: options.harness,
2791
+ version: options.version,
2792
+ dryRun: options.dryRun,
2793
+ recordCompletedInstall: options.recordCompletedInstall,
2794
+ ledgerOptions: options.ledgerOptions
2795
+ });
2796
+ return {
2797
+ provider,
2798
+ ...completion
2799
+ };
2800
+ }
2746
2801
 
2747
2802
  // src/cli/skills.ts
2748
2803
  import { spawnSync as spawnSync3 } from "child_process";
@@ -2752,7 +2807,8 @@ import { join as join3 } from "path";
2752
2807
  var SKILLS_CLI_AGENT = {
2753
2808
  opencode: "opencode",
2754
2809
  codex: "codex",
2755
- claude: "claude-code"
2810
+ claude: "claude-code",
2811
+ pi: "pi"
2756
2812
  };
2757
2813
  var GLOBAL_SKILL_ROOTS = {
2758
2814
  opencode: [
@@ -2760,7 +2816,8 @@ var GLOBAL_SKILL_ROOTS = {
2760
2816
  [".agents", "skills"]
2761
2817
  ],
2762
2818
  codex: [[".agents", "skills"]],
2763
- claude: [[".claude", "skills"]]
2819
+ claude: [[".claude", "skills"]],
2820
+ pi: [[".pi", "agent", "skills"]]
2764
2821
  };
2765
2822
  var REQUIRED_SKILLS = [
2766
2823
  {
@@ -2817,7 +2874,8 @@ function getRequiredSkillInstallCommand(skill, harness, options = {}) {
2817
2874
  "--global",
2818
2875
  "--agent",
2819
2876
  SKILLS_CLI_AGENT[harness],
2820
- "--yes"
2877
+ "--yes",
2878
+ ...harness === "pi" ? ["--copy"] : []
2821
2879
  ],
2822
2880
  options
2823
2881
  );
@@ -3549,16 +3607,52 @@ function defaultClaudeCodeModelRoles() {
3549
3607
  }
3550
3608
 
3551
3609
  // src/cli/operations/codex.ts
3552
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3610
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
3553
3611
  import { basename as basename2 } from "path";
3554
3612
 
3555
3613
  // src/cli/codex-plugin-install.ts
3556
3614
  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";
3615
+ import {
3616
+ lstatSync,
3617
+ readdirSync,
3618
+ readFileSync as readFileSync6,
3619
+ realpathSync,
3620
+ rmSync as rmSync2
3621
+ } from "fs";
3622
+ import { isAbsolute, join as join4, relative, resolve as resolve2, sep } from "path";
3623
+ var PLUGIN_NAME2 = "thoth-agents";
3624
+ var MARKETPLACE_NAME2 = "thoth-plugins";
3625
+ var MARKETPLACE_SOURCE2 = "https://github.com/EremesNG/thoth-plugins.git";
3626
+ var PLUGIN_ID2 = `${PLUGIN_NAME2}@${MARKETPLACE_NAME2}`;
3627
+ var LEGACY_MARKETPLACE_SOURCE2 = "https://github.com/EremesNG/thoth-agents.git";
3628
+ var LEGACY_MARKETPLACE_NAMES2 = [PLUGIN_NAME2, `${PLUGIN_NAME2}-codex`];
3629
+ var LEGACY_PLUGIN_IDS2 = LEGACY_MARKETPLACE_NAMES2.map(
3630
+ (name) => `${PLUGIN_NAME2}@${name}`
3631
+ );
3632
+ var LEGACY_ROOTS = [
3633
+ {
3634
+ relativePath: "plugins/cache/thoth-agents",
3635
+ kind: "cache",
3636
+ marketplaceName: "thoth-agents"
3637
+ },
3638
+ {
3639
+ relativePath: "plugins/cache/thoth-agents-codex",
3640
+ kind: "cache",
3641
+ marketplaceName: "thoth-agents-codex"
3642
+ },
3643
+ {
3644
+ relativePath: ".tmp/marketplaces/thoth-agents",
3645
+ kind: "snapshot",
3646
+ marketplaceName: "thoth-agents"
3647
+ },
3648
+ {
3649
+ relativePath: ".tmp/marketplaces/thoth-agents-codex",
3650
+ kind: "snapshot",
3651
+ marketplaceName: "thoth-agents-codex"
3652
+ }
3653
+ ];
3654
+ var MARKETPLACE_TARGET2 = `codex://marketplaces/${MARKETPLACE_NAME2}`;
3655
+ var PLUGIN_TARGET2 = `codex://plugins/${PLUGIN_ID2}`;
3562
3656
  function getCodexCommand(args, options = {}) {
3563
3657
  if ((options.platform ?? process.platform) !== "win32") {
3564
3658
  return { command: "codex", args: [...args] };
@@ -3596,10 +3690,153 @@ function parseManagerRecord(content) {
3596
3690
  return null;
3597
3691
  }
3598
3692
  }
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();
3693
+ function normalizeMarketplaceSource2(value) {
3694
+ 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
3695
  }
3602
- function marketplaceSources(entry) {
3696
+ function lstatIfPresent(path) {
3697
+ try {
3698
+ return lstatSync(path);
3699
+ } catch (error) {
3700
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
3701
+ return null;
3702
+ }
3703
+ throw error;
3704
+ }
3705
+ }
3706
+ function strictDescendant(base, candidate) {
3707
+ const suffix = relative(base, candidate);
3708
+ return suffix !== "" && suffix !== ".." && !suffix.startsWith(`..${sep}`) && !isAbsolute(suffix);
3709
+ }
3710
+ function parseObjectFile(path, label) {
3711
+ const file = lstatIfPresent(path);
3712
+ if (!file || file.isSymbolicLink() || !file.isFile()) {
3713
+ throw new Error(`${label} must be a regular non-link file`);
3714
+ }
3715
+ try {
3716
+ const value = JSON.parse(readFileSync6(path, "utf8"));
3717
+ if (!isRecord5(value)) throw new Error("not an object");
3718
+ return value;
3719
+ } catch (error) {
3720
+ throw new Error(
3721
+ `${label} is invalid JSON: ${error instanceof Error ? error.message : String(error)}`
3722
+ );
3723
+ }
3724
+ }
3725
+ function validateCacheManifest(root) {
3726
+ const productEntries = readdirSync(root, { withFileTypes: true });
3727
+ if (productEntries.length === 0) return;
3728
+ if (productEntries.length !== 1 || productEntries[0]?.name !== PLUGIN_NAME2) {
3729
+ throw new Error(
3730
+ `Legacy cache ${root} contains state outside ${PLUGIN_NAME2}`
3731
+ );
3732
+ }
3733
+ const productRoot = join4(root, PLUGIN_NAME2);
3734
+ const product = lstatIfPresent(productRoot);
3735
+ if (!product || product.isSymbolicLink() || !product.isDirectory()) {
3736
+ throw new Error(`Legacy cache product root ${productRoot} is unsafe`);
3737
+ }
3738
+ const versions = readdirSync(productRoot, { withFileTypes: true });
3739
+ if (versions.length === 0) {
3740
+ throw new Error(`Legacy cache ${root} has no plugin manifest`);
3741
+ }
3742
+ for (const entry of versions) {
3743
+ const versionRoot = join4(productRoot, entry.name);
3744
+ const version = lstatIfPresent(versionRoot);
3745
+ if (!entry.isDirectory() || !version || version.isSymbolicLink() || !version.isDirectory()) {
3746
+ throw new Error(`Legacy cache version root ${versionRoot} is unsafe`);
3747
+ }
3748
+ const manifest = parseObjectFile(
3749
+ join4(versionRoot, ".codex-plugin", "plugin.json"),
3750
+ `Legacy cache manifest for ${versionRoot}`
3751
+ );
3752
+ if (manifest.name !== PLUGIN_NAME2) {
3753
+ throw new Error(
3754
+ `Legacy cache manifest for ${versionRoot} does not identify ${PLUGIN_NAME2}`
3755
+ );
3756
+ }
3757
+ }
3758
+ }
3759
+ function validateSnapshotManifest(root, marketplaceName) {
3760
+ if (readdirSync(root).length === 0) return;
3761
+ const installation = parseObjectFile(
3762
+ join4(root, ".codex-marketplace-install.json"),
3763
+ `Legacy marketplace receipt for ${marketplaceName}`
3764
+ );
3765
+ if (typeof installation.source !== "string" || normalizeMarketplaceSource2(installation.source) !== normalizeMarketplaceSource2(LEGACY_MARKETPLACE_SOURCE2)) {
3766
+ throw new Error(
3767
+ `Legacy marketplace snapshot ${marketplaceName} has conflicting provenance`
3768
+ );
3769
+ }
3770
+ const marketplace = parseObjectFile(
3771
+ join4(root, ".agents", "plugins", "marketplace.json"),
3772
+ `Legacy marketplace manifest for ${marketplaceName}`
3773
+ );
3774
+ const plugins = Array.isArray(marketplace.plugins) ? marketplace.plugins : [];
3775
+ const pluginNames = plugins.map(
3776
+ (plugin) => isRecord5(plugin) ? plugin.name : null
3777
+ );
3778
+ if (marketplace.name !== marketplaceName || pluginNames.length === 0 || pluginNames.some((name) => name !== PLUGIN_NAME2)) {
3779
+ throw new Error(
3780
+ `Legacy marketplace snapshot ${marketplaceName} does not identify only ${PLUGIN_NAME2}`
3781
+ );
3782
+ }
3783
+ }
3784
+ function validateLegacyRoot(codexHome, definition, expectedRealPath) {
3785
+ const path = resolve2(codexHome, ...definition.relativePath.split("/"));
3786
+ if (!strictDescendant(codexHome, path)) {
3787
+ throw new Error(
3788
+ `Legacy cleanup target escapes CODEX_HOME: ${definition.relativePath}`
3789
+ );
3790
+ }
3791
+ const target = lstatIfPresent(path);
3792
+ if (!target) return null;
3793
+ const home = lstatIfPresent(codexHome);
3794
+ if (!home || !home.isDirectory()) {
3795
+ throw new Error(`Resolved CODEX_HOME is not a directory: ${codexHome}`);
3796
+ }
3797
+ let boundary = codexHome;
3798
+ for (const segment of definition.relativePath.split("/")) {
3799
+ boundary = join4(boundary, segment);
3800
+ const value = lstatIfPresent(boundary);
3801
+ if (!value || value.isSymbolicLink() || !value.isDirectory()) {
3802
+ throw new Error(
3803
+ `Legacy cleanup boundary is not a real directory: ${boundary}`
3804
+ );
3805
+ }
3806
+ }
3807
+ const realHome = realpathSync(codexHome);
3808
+ const realPath = realpathSync(path);
3809
+ if (!strictDescendant(realHome, realPath)) {
3810
+ throw new Error(
3811
+ `Legacy cleanup target resolves outside CODEX_HOME: ${definition.relativePath}`
3812
+ );
3813
+ }
3814
+ if (expectedRealPath && realPath !== expectedRealPath) {
3815
+ throw new Error(
3816
+ `Legacy cleanup target changed after preflight: ${definition.relativePath}`
3817
+ );
3818
+ }
3819
+ if (definition.kind === "cache") validateCacheManifest(path);
3820
+ else validateSnapshotManifest(path, definition.marketplaceName);
3821
+ return {
3822
+ kind: definition.kind,
3823
+ marketplaceName: definition.marketplaceName,
3824
+ path,
3825
+ realPath,
3826
+ relativePath: definition.relativePath
3827
+ };
3828
+ }
3829
+ function preflightLegacyRoots(codexHomeInput) {
3830
+ if (typeof codexHomeInput !== "string" || codexHomeInput.trim() === "") {
3831
+ throw new Error("CODEX_HOME could not be resolved");
3832
+ }
3833
+ const codexHome = resolve2(codexHomeInput);
3834
+ const roots = LEGACY_ROOTS.map(
3835
+ (definition) => validateLegacyRoot(codexHome, definition)
3836
+ ).filter((root) => root !== null);
3837
+ return { codexHome, roots };
3838
+ }
3839
+ function marketplaceSources2(entry) {
3603
3840
  const sources = [entry.repo, entry.source];
3604
3841
  if (isRecord5(entry.marketplaceSource)) {
3605
3842
  sources.push(entry.marketplaceSource.repo, entry.marketplaceSource.source);
@@ -3609,13 +3846,16 @@ function marketplaceSources(entry) {
3609
3846
  );
3610
3847
  }
3611
3848
  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
3849
+ return isMarketplaceIdentity2(entry, MARKETPLACE_NAME2);
3850
+ }
3851
+ function isMarketplaceIdentity2(entry, name, source = MARKETPLACE_SOURCE2) {
3852
+ if (entry.name !== name) return false;
3853
+ const canonical = normalizeMarketplaceSource2(source);
3854
+ return marketplaceSources2(entry).some(
3855
+ (source2) => normalizeMarketplaceSource2(source2) === canonical
3616
3856
  );
3617
3857
  }
3618
- function inspectCodexManager(executor, cwd) {
3858
+ function inspectCodexManager(executor, cwd, expectedVersion) {
3619
3859
  const marketplaceResult = executor(
3620
3860
  "codex",
3621
3861
  ["plugin", "marketplace", "list", "--json"],
@@ -3635,6 +3875,9 @@ function inspectCodexManager(executor, cwd) {
3635
3875
  ready: false,
3636
3876
  marketplace: "unknown",
3637
3877
  plugin: "unknown",
3878
+ legacyPluginIds: [],
3879
+ legacyMarketplaceNames: [],
3880
+ legacyMarketplaceConflicts: [],
3638
3881
  diagnostics
3639
3882
  };
3640
3883
  }
@@ -3650,9 +3893,35 @@ function inspectCodexManager(executor, cwd) {
3650
3893
  ready: false,
3651
3894
  marketplace: "unknown",
3652
3895
  plugin: "unknown",
3896
+ legacyPluginIds: [],
3897
+ legacyMarketplaceNames: [],
3898
+ legacyMarketplaceConflicts: [],
3653
3899
  diagnostics
3654
3900
  };
3655
3901
  }
3902
+ const legacyMarketplaceNames = LEGACY_MARKETPLACE_NAMES2.filter(
3903
+ (name) => marketplaces.some(
3904
+ (entry) => isMarketplaceIdentity2(entry, name, LEGACY_MARKETPLACE_SOURCE2)
3905
+ )
3906
+ );
3907
+ const legacyMarketplaceConflicts = LEGACY_MARKETPLACE_NAMES2.filter(
3908
+ (name) => marketplaces.some(
3909
+ (entry) => entry.name === name && !isMarketplaceIdentity2(entry, name, LEGACY_MARKETPLACE_SOURCE2)
3910
+ )
3911
+ );
3912
+ const legacyPluginIds = LEGACY_PLUGIN_IDS2.filter(
3913
+ (pluginId) => installedPlugins.some((entry) => entry.pluginId === pluginId)
3914
+ );
3915
+ if (legacyPluginIds.length > 0 || legacyMarketplaceNames.length > 0) {
3916
+ diagnostics.push(
3917
+ "Legacy Codex thoth-agents state was detected; cleanup is planned only after the central plugin is verified."
3918
+ );
3919
+ }
3920
+ if (legacyMarketplaceConflicts.length > 0) {
3921
+ diagnostics.push(
3922
+ `Legacy Codex marketplace provenance conflicts were found for ${legacyMarketplaceConflicts.join(", ")}; no manager mutation will be attempted.`
3923
+ );
3924
+ }
3656
3925
  const namedMarketplaces = marketplaces.filter(
3657
3926
  (entry) => entry.name === MARKETPLACE_NAME2
3658
3927
  );
@@ -3662,31 +3931,81 @@ function inspectCodexManager(executor, cwd) {
3662
3931
  }
3663
3932
  if (marketplace === "conflict") {
3664
3933
  diagnostics.push(
3665
- "A Codex marketplace named thoth-agents is registered from a different source; resolve it through Codex before retrying."
3934
+ "A Codex marketplace named thoth-plugins is registered from a different source; resolve it through Codex before retrying."
3666
3935
  );
3667
3936
  }
3668
3937
  const matchingPlugins = installedPlugins.filter(
3669
3938
  (entry) => entry.pluginId === PLUGIN_ID2
3670
3939
  );
3671
3940
  let plugin = "absent";
3672
- if (matchingPlugins.some((entry) => entry.enabled === true)) {
3941
+ if (matchingPlugins.some(
3942
+ (entry) => entry.enabled === true && entry.version === expectedVersion
3943
+ )) {
3673
3944
  plugin = "installed";
3945
+ } else if (matchingPlugins.some((entry) => entry.enabled === true)) {
3946
+ plugin = "outdated";
3674
3947
  } else if (matchingPlugins.length > 0) {
3675
3948
  plugin = "disabled";
3676
3949
  }
3677
3950
  return {
3678
- ready: marketplace !== "conflict",
3951
+ ready: marketplace !== "conflict" && legacyMarketplaceConflicts.length === 0,
3679
3952
  marketplace,
3680
3953
  plugin,
3954
+ legacyPluginIds,
3955
+ legacyMarketplaceNames,
3956
+ legacyMarketplaceConflicts,
3681
3957
  diagnostics
3682
3958
  };
3683
3959
  }
3960
+ function centralPluginVerified(inspection) {
3961
+ return inspection.ready && inspection.marketplace === "installed" && inspection.plugin === "installed";
3962
+ }
3963
+ function legacyManagerClean(inspection) {
3964
+ return inspection.legacyPluginIds.length === 0 && inspection.legacyMarketplaceNames.length === 0;
3965
+ }
3966
+ function legacyRootsAbsent(codexHome) {
3967
+ return LEGACY_ROOTS.every(
3968
+ ({ relativePath }) => !lstatIfPresent(resolve2(codexHome, ...relativePath.split("/")))
3969
+ );
3970
+ }
3971
+ function legacyPluginCommandItem(pluginId, projectRoot) {
3972
+ return {
3973
+ action: "remove-legacy-plugin",
3974
+ targetPath: `codex://plugins/${pluginId}`,
3975
+ description: `Remove the registered legacy Codex plugin ${pluginId}.`,
3976
+ command: {
3977
+ executable: "codex",
3978
+ args: ["plugin", "remove", pluginId, "--json"],
3979
+ cwd: projectRoot
3980
+ }
3981
+ };
3982
+ }
3983
+ function legacyMarketplaceCommandItem(marketplaceName, projectRoot) {
3984
+ return {
3985
+ action: "remove-legacy-marketplace",
3986
+ targetPath: `codex://marketplaces/${marketplaceName}`,
3987
+ description: `Remove the registered legacy Codex marketplace ${marketplaceName}.`,
3988
+ command: {
3989
+ executable: "codex",
3990
+ args: ["plugin", "marketplace", "remove", marketplaceName, "--json"],
3991
+ cwd: projectRoot
3992
+ }
3993
+ };
3994
+ }
3995
+ function legacyRootPlanItem(root) {
3996
+ return {
3997
+ action: "remove-legacy-root",
3998
+ targetPath: root.path,
3999
+ description: `Remove the preflight-approved legacy Codex root ${root.relativePath}.`,
4000
+ cleanupRoot: root
4001
+ };
4002
+ }
3684
4003
  function commandItem2(action, projectRoot) {
3685
4004
  if (action === "register-marketplace") {
3686
4005
  return {
3687
4006
  action,
3688
4007
  targetPath: MARKETPLACE_TARGET2,
3689
- description: "Register the package-owned thoth-agents Codex marketplace.",
4008
+ description: "Register the central Thoth Codex marketplace.",
3690
4009
  command: {
3691
4010
  executable: "codex",
3692
4011
  args: ["plugin", "marketplace", "add", MARKETPLACE_SOURCE2, "--json"],
@@ -3707,25 +4026,54 @@ function commandItem2(action, projectRoot) {
3707
4026
  }
3708
4027
  function buildCodexPluginSetupPlan(config) {
3709
4028
  const commandExecutor = config.commandExecutor ?? defaultCommandExecutor3;
3710
- const inspection = inspectCodexManager(commandExecutor, config.projectRoot);
3711
- const items = [];
4029
+ const inspection = inspectCodexManager(
4030
+ commandExecutor,
4031
+ config.projectRoot,
4032
+ config.expectedVersion
4033
+ );
4034
+ let cleanupPreflight;
4035
+ let cleanupPreflightError;
3712
4036
  if (inspection.ready) {
4037
+ try {
4038
+ cleanupPreflight = preflightLegacyRoots(
4039
+ getCodexHome({ codexHome: config.codexHome, homeDir: config.homeDir })
4040
+ );
4041
+ } catch (error) {
4042
+ cleanupPreflightError = `Legacy cleanup preflight failed: ${error instanceof Error ? error.message : String(error)}. No manager mutation will be attempted.`;
4043
+ }
4044
+ }
4045
+ const ready = inspection.ready && cleanupPreflightError === void 0;
4046
+ const items = [];
4047
+ if (ready) {
3713
4048
  if (inspection.marketplace === "absent") {
3714
4049
  items.push(commandItem2("register-marketplace", config.projectRoot));
3715
4050
  }
3716
- if (inspection.plugin === "absent" || inspection.plugin === "disabled") {
4051
+ if (inspection.plugin === "absent" || inspection.plugin === "disabled" || inspection.plugin === "outdated") {
3717
4052
  items.push(commandItem2("install-plugin", config.projectRoot));
3718
4053
  }
4054
+ items.push(
4055
+ ...inspection.legacyPluginIds.map(
4056
+ (pluginId) => legacyPluginCommandItem(pluginId, config.projectRoot)
4057
+ ),
4058
+ ...inspection.legacyMarketplaceNames.map(
4059
+ (marketplaceName) => legacyMarketplaceCommandItem(marketplaceName, config.projectRoot)
4060
+ ),
4061
+ ...cleanupPreflight?.roots.map(legacyRootPlanItem) ?? []
4062
+ );
3719
4063
  }
3720
4064
  return {
4065
+ codexHome: cleanupPreflight?.codexHome ?? getCodexHome({ codexHome: config.codexHome, homeDir: config.homeDir }),
3721
4066
  dryRun: config.dryRun === true,
4067
+ expectedVersion: config.expectedVersion,
3722
4068
  items,
3723
- ready: inspection.ready,
4069
+ ready,
3724
4070
  projectRoot: config.projectRoot,
3725
4071
  commandExecutor,
3726
4072
  diagnostics: [
3727
4073
  ...inspection.diagnostics,
3728
- "Codex installs thoth-agents from EremesNG/thoth-agents through its native plugin manager and owns the cached plugin files.",
4074
+ ...cleanupPreflightError ? [cleanupPreflightError] : [],
4075
+ "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.",
4076
+ "Close every Codex process before applying legacy cleanup; process-name detection is not a portable or race-free safety boundary.",
3729
4077
  "Restart Codex after installation; use /plugins to inspect marketplace and plugin state."
3730
4078
  ]
3731
4079
  };
@@ -3749,12 +4097,28 @@ function applyCodexPluginSetup(plan) {
3749
4097
  };
3750
4098
  }
3751
4099
  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
- );
4100
+ const installationItems = plan.items.filter(
4101
+ (item) => item.action === "register-marketplace" || item.action === "install-plugin"
4102
+ );
4103
+ const cleanupItems = plan.items.filter(
4104
+ (item) => item.action === "remove-legacy-plugin" || item.action === "remove-legacy-marketplace"
4105
+ );
4106
+ const rootItems = plan.items.filter(
4107
+ (item) => item.action === "remove-legacy-root"
4108
+ );
4109
+ for (const item of installationItems) {
4110
+ const command = item.command;
4111
+ if (!command) {
4112
+ return {
4113
+ success: false,
4114
+ changed,
4115
+ diagnostics,
4116
+ error: `Codex plugin setup plan item ${item.action} has no command.`
4117
+ };
4118
+ }
4119
+ const result = plan.commandExecutor(command.executable, command.args, {
4120
+ cwd: command.cwd
4121
+ });
3758
4122
  if (result.exitCode !== 0) {
3759
4123
  return {
3760
4124
  success: false,
@@ -3767,10 +4131,11 @@ function applyCodexPluginSetup(plan) {
3767
4131
  }
3768
4132
  const inspection = inspectCodexManager(
3769
4133
  plan.commandExecutor,
3770
- plan.projectRoot
4134
+ plan.projectRoot,
4135
+ plan.expectedVersion
3771
4136
  );
3772
4137
  diagnostics.push(...inspection.diagnostics);
3773
- if (inspection.marketplace !== "installed" || inspection.plugin !== "installed") {
4138
+ if (!centralPluginVerified(inspection)) {
3774
4139
  return {
3775
4140
  success: false,
3776
4141
  changed,
@@ -3778,25 +4143,126 @@ function applyCodexPluginSetup(plan) {
3778
4143
  error: "Codex did not verify the expected marketplace and enabled plugin after mutation."
3779
4144
  };
3780
4145
  }
3781
- return { success: true, changed, diagnostics: uniqueMessages3(diagnostics) };
3782
- }
3783
- function formatCodexPluginSetupPlan(plan) {
3784
- const lines = plan.items.map(
3785
- (item) => `- ${item.action}: ${item.command.executable} ${item.command.args.join(" ")}`
3786
- );
3787
- return ["Codex plugin setup plan:", ...lines].join("\n");
3788
- }
3789
-
3790
- // src/cli/operations/codex.ts
3791
- var CODEX_DISPLAY_NAME = "Codex";
3792
- var codexPlanSources = /* @__PURE__ */ new WeakMap();
3793
- var codexModelSources = /* @__PURE__ */ new WeakMap();
3794
- var codexActions = [
3795
- {
3796
- id: "codex-status",
3797
- kind: "status",
3798
- label: "Status",
3799
- description: "Inspect managed Codex setup state",
4146
+ for (const item of cleanupItems) {
4147
+ const command = item.command;
4148
+ if (!command) {
4149
+ return {
4150
+ success: false,
4151
+ changed,
4152
+ diagnostics: uniqueMessages3(diagnostics),
4153
+ error: `Codex plugin cleanup plan item ${item.action} has no command.`
4154
+ };
4155
+ }
4156
+ const result = plan.commandExecutor(command.executable, command.args, {
4157
+ cwd: command.cwd
4158
+ });
4159
+ if (result.exitCode !== 0) {
4160
+ return {
4161
+ success: false,
4162
+ changed,
4163
+ diagnostics: uniqueMessages3(diagnostics),
4164
+ error: `${boundedCommandFailure2(item, result)} Central ${PLUGIN_ID2} remains installed; close Codex and retry.`
4165
+ };
4166
+ }
4167
+ changed.push(item.targetPath);
4168
+ }
4169
+ const managerCleanupInspection = inspectCodexManager(
4170
+ plan.commandExecutor,
4171
+ plan.projectRoot,
4172
+ plan.expectedVersion
4173
+ );
4174
+ diagnostics.push(...managerCleanupInspection.diagnostics);
4175
+ if (!centralPluginVerified(managerCleanupInspection) || !legacyManagerClean(managerCleanupInspection)) {
4176
+ return {
4177
+ success: false,
4178
+ changed,
4179
+ diagnostics: uniqueMessages3(diagnostics),
4180
+ error: `Central ${PLUGIN_ID2} remains installed, but exact legacy manager cleanup did not converge; close Codex and retry.`
4181
+ };
4182
+ }
4183
+ for (const item of rootItems) {
4184
+ const root = item.cleanupRoot;
4185
+ const definition = root ? LEGACY_ROOTS.find(
4186
+ (candidate) => candidate.relativePath === root.relativePath
4187
+ ) : void 0;
4188
+ if (!root || !definition) {
4189
+ return {
4190
+ success: false,
4191
+ changed,
4192
+ diagnostics: uniqueMessages3(diagnostics),
4193
+ error: `Central ${PLUGIN_ID2} remains installed, but the fixed legacy cleanup plan is invalid; close Codex and retry.`
4194
+ };
4195
+ }
4196
+ const exactPath = resolve2(
4197
+ plan.codexHome,
4198
+ ...definition.relativePath.split("/")
4199
+ );
4200
+ if (resolve2(root.path) !== exactPath || resolve2(item.targetPath) !== exactPath || root.kind !== definition.kind || root.marketplaceName !== definition.marketplaceName) {
4201
+ return {
4202
+ success: false,
4203
+ changed,
4204
+ diagnostics: uniqueMessages3(diagnostics),
4205
+ error: `Central ${PLUGIN_ID2} remains installed, but the fixed legacy cleanup target changed; close Codex and retry.`
4206
+ };
4207
+ }
4208
+ if (!lstatIfPresent(exactPath)) continue;
4209
+ try {
4210
+ const validated = validateLegacyRoot(
4211
+ plan.codexHome,
4212
+ definition,
4213
+ root.realPath
4214
+ );
4215
+ if (!validated) continue;
4216
+ rmSync2(validated.path, { recursive: true, force: false });
4217
+ if (lstatIfPresent(validated.path)) {
4218
+ throw new Error(
4219
+ `Legacy cleanup target still exists: ${root.relativePath}`
4220
+ );
4221
+ }
4222
+ changed.push(item.targetPath);
4223
+ } catch (error) {
4224
+ const detail = (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").slice(0, 240);
4225
+ return {
4226
+ success: false,
4227
+ changed,
4228
+ diagnostics: uniqueMessages3(diagnostics),
4229
+ error: `Central ${PLUGIN_ID2} remains installed; legacy root cleanup failed${detail ? `: ${detail}` : ""}. Close Codex and retry.`
4230
+ };
4231
+ }
4232
+ }
4233
+ const finalInspection = inspectCodexManager(
4234
+ plan.commandExecutor,
4235
+ plan.projectRoot,
4236
+ plan.expectedVersion
4237
+ );
4238
+ diagnostics.push(...finalInspection.diagnostics);
4239
+ if (!centralPluginVerified(finalInspection) || !legacyManagerClean(finalInspection) || !legacyRootsAbsent(plan.codexHome)) {
4240
+ return {
4241
+ success: false,
4242
+ changed,
4243
+ diagnostics: uniqueMessages3(diagnostics),
4244
+ error: `Central ${PLUGIN_ID2} remains installed, but exact legacy cleanup did not converge; close Codex and retry.`
4245
+ };
4246
+ }
4247
+ return { success: true, changed, diagnostics: uniqueMessages3(diagnostics) };
4248
+ }
4249
+ function formatCodexPluginSetupPlan(plan) {
4250
+ const lines = plan.items.map(
4251
+ (item) => item.command ? `- ${item.action}: ${item.command.executable} ${item.command.args.join(" ")}` : `- ${item.action}: ${item.cleanupRoot?.relativePath ?? item.targetPath}`
4252
+ );
4253
+ return ["Codex plugin setup plan:", ...lines].join("\n");
4254
+ }
4255
+
4256
+ // src/cli/operations/codex.ts
4257
+ var CODEX_DISPLAY_NAME = "Codex";
4258
+ var codexPlanSources = /* @__PURE__ */ new WeakMap();
4259
+ var codexModelSources = /* @__PURE__ */ new WeakMap();
4260
+ var codexActions = [
4261
+ {
4262
+ id: "codex-status",
4263
+ kind: "status",
4264
+ label: "Status",
4265
+ description: "Inspect managed Codex setup state",
3800
4266
  dryRun: false,
3801
4267
  requiresConfirmation: false,
3802
4268
  supported: true
@@ -3910,7 +4376,7 @@ function backupForItem(item) {
3910
4376
  function rootBlockState(item) {
3911
4377
  if (!existsSync8(item.targetPath))
3912
4378
  return { state: "missing", observed: "absent" };
3913
- const content = readFileSync6(item.targetPath, "utf8");
4379
+ const content = readFileSync7(item.targetPath, "utf8");
3914
4380
  if (item.content && content.includes(item.content)) {
3915
4381
  return { state: "installed", observed: "managed root block present" };
3916
4382
  }
@@ -3923,11 +4389,11 @@ function managedModelState(item) {
3923
4389
  if (!existsSync8(item.targetPath))
3924
4390
  return { state: "missing", observed: "absent" };
3925
4391
  try {
3926
- const parsed = JSON.parse(readFileSync6(item.targetPath, "utf8"));
4392
+ const parsed = JSON.parse(readFileSync7(item.targetPath, "utf8"));
3927
4393
  if (parsed.version !== MANAGED_MODEL_STATE_VERSION || !parsed.models || typeof parsed.models !== "object" || Array.isArray(parsed.models)) {
3928
4394
  return { state: "unknown", observed: "invalid managed model state" };
3929
4395
  }
3930
- return item.content === readFileSync6(item.targetPath, "utf8") ? { state: "installed", observed: "managed model state current" } : { state: "drift", observed: "managed model state differs" };
4396
+ return item.content === readFileSync7(item.targetPath, "utf8") ? { state: "installed", observed: "managed model state current" } : { state: "drift", observed: "managed model state differs" };
3931
4397
  } catch {
3932
4398
  return { state: "unknown", observed: "unparseable managed model state" };
3933
4399
  }
@@ -3935,7 +4401,7 @@ function managedModelState(item) {
3935
4401
  function userConfigState(item) {
3936
4402
  if (!existsSync8(item.targetPath))
3937
4403
  return { state: "missing", observed: "absent" };
3938
- const content = readFileSync6(item.targetPath, "utf8");
4404
+ const content = readFileSync7(item.targetPath, "utf8");
3939
4405
  if (/^\s*default_mode_request_user_input\s*=\s*true\b/m.test(content)) {
3940
4406
  return {
3941
4407
  state: "installed",
@@ -3950,7 +4416,7 @@ function userConfigState(item) {
3950
4416
  function contentState(item) {
3951
4417
  if (!existsSync8(item.targetPath))
3952
4418
  return { state: "missing", observed: "absent" };
3953
- const observed = readFileSync6(item.targetPath, "utf8");
4419
+ const observed = readFileSync7(item.targetPath, "utf8");
3954
4420
  if (item.content === observed)
3955
4421
  return { state: "installed", observed: "current" };
3956
4422
  if (item.role) {
@@ -4117,7 +4583,7 @@ function planItemFromPluginSetup(item) {
4117
4583
  path: item.targetPath,
4118
4584
  label: "Codex native plugin manager target"
4119
4585
  },
4120
- preview: `${item.command.executable} ${item.command.args.join(" ")}`,
4586
+ preview: item.command ? `${item.command.executable} ${item.command.args.join(" ")}` : `Remove exact preflight-approved Codex root ${item.cleanupRoot?.relativePath ?? item.targetPath}`,
4121
4587
  backup: { required: false, strategy: "external" }
4122
4588
  };
4123
4589
  }
@@ -4215,7 +4681,10 @@ function buildCompleteCodexPlan(action, context2) {
4215
4681
  const packageVersion = resolveVersion();
4216
4682
  const buildPlugin = context2.buildCodexPluginSetupPlan ?? buildCodexPluginSetupPlan;
4217
4683
  const pluginPlan = buildPlugin({
4684
+ codexHome: context2.codexHome,
4218
4685
  dryRun: true,
4686
+ expectedVersion: packageVersion.ok ? packageVersion.version : "0.0.0",
4687
+ homeDir: context2.homeDir,
4219
4688
  projectRoot: context2.cwd,
4220
4689
  commandExecutor: context2.codexPluginCommandExecutor
4221
4690
  });
@@ -4611,7 +5080,7 @@ function applyCodexPlan(plan) {
4611
5080
  // src/cli/operations/opencode.ts
4612
5081
  import { createHash as createHash3 } from "crypto";
4613
5082
  import { existsSync as existsSync10 } from "fs";
4614
- import { join as join5 } from "path";
5083
+ import { join as join6 } from "path";
4615
5084
 
4616
5085
  // src/cli/opencode-effort.ts
4617
5086
  var OPENAI_GPT_RUNTIME_VARIANTS = /* @__PURE__ */ new Set([
@@ -4647,14 +5116,16 @@ function resolveOpenCodeEffort(input) {
4647
5116
  import {
4648
5117
  cpSync,
4649
5118
  existsSync as existsSync9,
5119
+ lstatSync as lstatSync2,
4650
5120
  mkdirSync as mkdirSync4,
4651
5121
  mkdtempSync,
5122
+ readFileSync as readFileSync8,
4652
5123
  renameSync as renameSync3,
4653
- rmSync as rmSync2,
5124
+ rmSync as rmSync3,
4654
5125
  statSync
4655
5126
  } from "fs";
4656
5127
  import { homedir as homedir4 } from "os";
4657
- import { dirname as dirname5, join as join4 } from "path";
5128
+ import { dirname as dirname5, join as join5 } from "path";
4658
5129
  import { fileURLToPath as fileURLToPath4 } from "url";
4659
5130
 
4660
5131
  // src/harness/core/owned-skills.ts
@@ -4667,6 +5138,68 @@ var THOTH_OWNED_SKILL_NAMES = [
4667
5138
  ];
4668
5139
 
4669
5140
  // src/cli/owned-skills.ts
5141
+ function parseSkillFrontmatter(content) {
5142
+ const lines = content.split(/\r?\n/);
5143
+ if (lines[0] !== "---") return void 0;
5144
+ const closingDelimiter = lines.findIndex(
5145
+ (line, index) => index > 0 && line === "---"
5146
+ );
5147
+ if (closingDelimiter < 0) return void 0;
5148
+ const values = /* @__PURE__ */ new Map();
5149
+ for (const line of lines.slice(1, closingDelimiter)) {
5150
+ const field = /^(name|description):\s*(.*)$/.exec(line);
5151
+ if (!field) continue;
5152
+ const [, key, rawValue] = field;
5153
+ if (!key || rawValue === void 0 || values.has(key)) return void 0;
5154
+ const value = rawValue.trim();
5155
+ values.set(
5156
+ key,
5157
+ value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'") ? value.slice(1, -1) : value
5158
+ );
5159
+ }
5160
+ const name = values.get("name");
5161
+ const description = values.get("description");
5162
+ if (!name || !description || description.startsWith("#")) return void 0;
5163
+ return { name, description };
5164
+ }
5165
+ function inspectPiPackageSkill(entry) {
5166
+ if (!existsSync9(entry.sourcePath))
5167
+ return {
5168
+ name: entry.name,
5169
+ state: "missing",
5170
+ message: `Package-declared Pi skill directory is missing: ${entry.name}`
5171
+ };
5172
+ const root = lstatSync2(entry.sourcePath);
5173
+ if (!root.isDirectory() || root.isSymbolicLink())
5174
+ return {
5175
+ name: entry.name,
5176
+ state: "drift",
5177
+ message: `Package-declared Pi skill directory is not a regular directory: ${entry.name}`
5178
+ };
5179
+ const contractPath = join5(entry.sourcePath, "SKILL.md");
5180
+ if (!existsSync9(contractPath))
5181
+ return {
5182
+ name: entry.name,
5183
+ state: "missing",
5184
+ message: `Package-declared Pi skill contract is missing: ${entry.name}`
5185
+ };
5186
+ const contract = lstatSync2(contractPath);
5187
+ if (!contract.isFile() || contract.isSymbolicLink())
5188
+ return {
5189
+ name: entry.name,
5190
+ state: "drift",
5191
+ message: `Package-declared Pi skill contract is not a regular file: ${entry.name}`
5192
+ };
5193
+ const content = readFileSync8(contractPath, "utf8");
5194
+ const frontmatter = parseSkillFrontmatter(content);
5195
+ if (!frontmatter || frontmatter.name !== entry.name)
5196
+ return {
5197
+ name: entry.name,
5198
+ state: "drift",
5199
+ message: `Package-declared Pi skill contract is malformed: ${entry.name}`
5200
+ };
5201
+ return void 0;
5202
+ }
4670
5203
  function resolveOwnedSkillPackageRoot(moduleUrl) {
4671
5204
  const packageRoot = findPackageRoot(dirname5(fileURLToPath4(moduleUrl)));
4672
5205
  if (!packageRoot) {
@@ -4684,8 +5217,16 @@ function getOpenCodeOwnedSkillEntries(options = {}) {
4684
5217
  const homeDir = options.homeDir ?? homedir4();
4685
5218
  return THOTH_OWNED_SKILL_NAMES.map((name) => ({
4686
5219
  name,
4687
- sourcePath: join4(packageRoot, "skills", name),
4688
- destinationPath: join4(homeDir, ".config", "opencode", "skills", name)
5220
+ sourcePath: join5(packageRoot, "skills", name),
5221
+ destinationPath: join5(homeDir, ".config", "opencode", "skills", name)
5222
+ }));
5223
+ }
5224
+ function getPiOwnedSkillEntries(options) {
5225
+ const packageRoot = options.packageRoot;
5226
+ return THOTH_OWNED_SKILL_NAMES.map((name) => ({
5227
+ name,
5228
+ sourcePath: join5(packageRoot, "skills", name),
5229
+ destinationPath: join5(packageRoot, "skills", name)
4689
5230
  }));
4690
5231
  }
4691
5232
  function validateCanonicalSkill(entry) {
@@ -4694,7 +5235,7 @@ function validateCanonicalSkill(entry) {
4694
5235
  `Canonical owned skill directory is missing: ${entry.name}`
4695
5236
  );
4696
5237
  }
4697
- const contractPath = join4(entry.sourcePath, "SKILL.md");
5238
+ const contractPath = join5(entry.sourcePath, "SKILL.md");
4698
5239
  if (!existsSync9(contractPath) || !statSync(contractPath).isFile()) {
4699
5240
  throw new Error(`Canonical owned skill contract is missing: ${entry.name}`);
4700
5241
  }
@@ -4704,21 +5245,21 @@ function installOwnedSkills(skills) {
4704
5245
  if (!firstSkill) return;
4705
5246
  const globalRoot = dirname5(firstSkill.destinationPath);
4706
5247
  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");
5248
+ const transactionRoot = mkdtempSync(join5(globalRoot, ".thoth-agents-owned-"));
5249
+ const stagedRoot = join5(transactionRoot, "staged");
5250
+ const backupRoot = join5(transactionRoot, "backup");
4710
5251
  mkdirSync4(stagedRoot, { recursive: true });
4711
5252
  mkdirSync4(backupRoot, { recursive: true });
4712
5253
  try {
4713
5254
  for (const skill of skills) {
4714
- cpSync(skill.sourcePath, join4(stagedRoot, skill.name), {
5255
+ cpSync(skill.sourcePath, join5(stagedRoot, skill.name), {
4715
5256
  recursive: true,
4716
5257
  force: true
4717
5258
  });
4718
5259
  }
4719
5260
  for (const skill of skills) {
4720
- const stagedPath = join4(stagedRoot, skill.name);
4721
- const backupPath = join4(backupRoot, skill.name);
5261
+ const stagedPath = join5(stagedRoot, skill.name);
5262
+ const backupPath = join5(backupRoot, skill.name);
4722
5263
  const hadExistingDestination = existsSync9(skill.destinationPath);
4723
5264
  if (hadExistingDestination) {
4724
5265
  renameSync3(skill.destinationPath, backupPath);
@@ -4727,7 +5268,7 @@ function installOwnedSkills(skills) {
4727
5268
  renameSync3(stagedPath, skill.destinationPath);
4728
5269
  } catch (error) {
4729
5270
  if (existsSync9(skill.destinationPath)) {
4730
- rmSync2(skill.destinationPath, { recursive: true, force: true });
5271
+ rmSync3(skill.destinationPath, { recursive: true, force: true });
4731
5272
  }
4732
5273
  if (hadExistingDestination && existsSync9(backupPath)) {
4733
5274
  renameSync3(backupPath, skill.destinationPath);
@@ -4735,11 +5276,11 @@ function installOwnedSkills(skills) {
4735
5276
  throw error;
4736
5277
  }
4737
5278
  if (existsSync9(backupPath)) {
4738
- rmSync2(backupPath, { recursive: true, force: true });
5279
+ rmSync3(backupPath, { recursive: true, force: true });
4739
5280
  }
4740
5281
  }
4741
5282
  } finally {
4742
- rmSync2(transactionRoot, { recursive: true, force: true });
5283
+ rmSync3(transactionRoot, { recursive: true, force: true });
4743
5284
  }
4744
5285
  }
4745
5286
  function syncOpenCodeOwnedSkills(options = {}) {
@@ -4758,6 +5299,37 @@ function syncOpenCodeOwnedSkills(options = {}) {
4758
5299
  };
4759
5300
  }
4760
5301
  }
5302
+ function inspectPiPackageSkills(options) {
5303
+ const skills = getPiOwnedSkillEntries(options);
5304
+ try {
5305
+ const issues = skills.flatMap((skill) => {
5306
+ const issue = inspectPiPackageSkill(skill);
5307
+ return issue ? [issue] : [];
5308
+ });
5309
+ if (issues.length > 0)
5310
+ return {
5311
+ success: false,
5312
+ state: "unavailable",
5313
+ skills,
5314
+ issues,
5315
+ error: issues.map(({ message }) => message).join("; ")
5316
+ };
5317
+ return {
5318
+ success: true,
5319
+ state: "available",
5320
+ skills,
5321
+ issues: []
5322
+ };
5323
+ } catch (error) {
5324
+ return {
5325
+ success: false,
5326
+ state: "unavailable",
5327
+ skills,
5328
+ issues: [],
5329
+ error: error instanceof Error ? error.message : String(error)
5330
+ };
5331
+ }
5332
+ }
4761
5333
 
4762
5334
  // src/cli/operations/opencode.ts
4763
5335
  var PACKAGE_NAME = "thoth-agents";
@@ -4809,7 +5381,7 @@ function canonicalData(value, active) {
4809
5381
  if (ownKeys.some((key) => typeof key === "symbol")) {
4810
5382
  throw new NonCanonicalDataError("Symbol keys are not canonical.");
4811
5383
  }
4812
- const keys = ownKeys;
5384
+ const keys2 = ownKeys;
4813
5385
  active.add(value);
4814
5386
  try {
4815
5387
  if (array) {
@@ -4821,7 +5393,7 @@ function canonicalData(value, active) {
4821
5393
  if (!Number.isSafeInteger(length) || length < 0) {
4822
5394
  throw new NonCanonicalDataError("Array length is invalid.");
4823
5395
  }
4824
- const indexKeys = keys.filter((key) => key !== "length");
5396
+ const indexKeys = keys2.filter((key) => key !== "length");
4825
5397
  if (indexKeys.length !== length || indexKeys.some(
4826
5398
  (key) => !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= length || String(Number(key)) !== key
4827
5399
  )) {
@@ -4843,7 +5415,7 @@ function canonicalData(value, active) {
4843
5415
  return `[${parts2.join(",")}]`;
4844
5416
  }
4845
5417
  const parts = [];
4846
- for (const key of keys.sort()) {
5418
+ for (const key of keys2.sort()) {
4847
5419
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
4848
5420
  if (!descriptor || !("value" in descriptor)) {
4849
5421
  throw new NonCanonicalDataError("Accessors are not canonical.");
@@ -5000,7 +5572,7 @@ function titleCaseSkillName(value) {
5000
5572
  }).join("-");
5001
5573
  }
5002
5574
  function homeDirFromContext(context2) {
5003
- return context2.env?.HOME ?? context2.env?.USERPROFILE ?? (context2.cwd !== process.cwd() ? join5(context2.cwd, "home") : void 0);
5575
+ return context2.env?.HOME ?? context2.env?.USERPROFILE ?? (context2.cwd !== process.cwd() ? join6(context2.cwd, "home") : void 0);
5004
5576
  }
5005
5577
  function openCodeSkillTargets(context2) {
5006
5578
  const homeDir = homeDirFromContext(context2);
@@ -5024,7 +5596,7 @@ function openCodeSkillTargets(context2) {
5024
5596
  const ownedTargets = getOpenCodeOwnedSkillEntries({
5025
5597
  homeDir
5026
5598
  }).map((skill) => {
5027
- const path = join5(skill.destinationPath, "SKILL.md");
5599
+ const path = join6(skill.destinationPath, "SKILL.md");
5028
5600
  const installed = existsSync10(path);
5029
5601
  return {
5030
5602
  kind: "skill",
@@ -6382,131 +6954,2109 @@ function applyOpenCodePlan(plan) {
6382
6954
  };
6383
6955
  }
6384
6956
 
6385
- // src/cli/operations/index.ts
6386
- var OPERATION_HARNESSES = {
6387
- opencode: opencodeOperationAdapter,
6388
- codex: codexOperationAdapter,
6389
- claude: claudeCodeOperationAdapter
6390
- };
6391
- var SUPPORTED_OPERATION_HARNESSES = Object.keys(
6392
- OPERATION_HARNESSES
6393
- );
6394
- function listOperationHarnesses() {
6395
- return SUPPORTED_OPERATION_HARNESSES.map(
6396
- (harness) => OPERATION_HARNESSES[harness]
6397
- );
6398
- }
6399
- function getOperationHarness(harness) {
6400
- return OPERATION_HARNESSES[harness];
6957
+ // src/cli/operations/pi.ts
6958
+ import { spawnSync as spawnSync7 } from "child_process";
6959
+ import { createHash as createHash5 } from "crypto";
6960
+ import { existsSync as existsSync15, readFileSync as readFileSync12 } from "fs";
6961
+ import { basename as basename3, isAbsolute as isAbsolute4, join as join12 } from "path";
6962
+
6963
+ // src/cli/pi-effort.ts
6964
+ var PI_EFFORT_VALUES = [
6965
+ "off",
6966
+ "minimal",
6967
+ "low",
6968
+ "medium",
6969
+ "high",
6970
+ "xhigh"
6971
+ ];
6972
+ var PI_EFFORTS = new Set(PI_EFFORT_VALUES);
6973
+ function resolvePiEffort(input) {
6974
+ if (!input.effort || input.effort.kind === "inherit") {
6975
+ return { ok: true, effort: void 0 };
6976
+ }
6977
+ const effort = input.effort.value;
6978
+ if (!PI_EFFORTS.has(effort)) {
6979
+ return {
6980
+ ok: false,
6981
+ message: `Pi does not support effort ${effort}; expected ${PI_EFFORT_VALUES.join(", ")}, or inherit.`
6982
+ };
6983
+ }
6984
+ if (input.availableEfforts !== void 0 && !input.availableEfforts.includes(effort)) {
6985
+ return {
6986
+ ok: false,
6987
+ message: `Effort ${effort} is not available for ${input.model} in the selected model catalog entry.`
6988
+ };
6989
+ }
6990
+ return { ok: true, effort };
6401
6991
  }
6402
6992
 
6403
- // src/cli/tui/model-catalog.ts
6404
- import { execFileSync } from "child_process";
6993
+ // src/cli/pi-install.ts
6994
+ import { spawnSync as spawnSync6 } from "child_process";
6995
+ import { createHash as createHash4 } from "crypto";
6996
+ import {
6997
+ copyFileSync as copyFileSync5,
6998
+ existsSync as existsSync14,
6999
+ lstatSync as lstatSync3,
7000
+ mkdirSync as mkdirSync7,
7001
+ readdirSync as readdirSync3,
7002
+ readFileSync as readFileSync11,
7003
+ renameSync as renameSync6,
7004
+ rmSync as rmSync6,
7005
+ writeFileSync as writeFileSync7
7006
+ } from "fs";
7007
+ import { dirname as dirname9, isAbsolute as isAbsolute3, join as join11 } from "path";
7008
+ import { fileURLToPath as fileURLToPath5, pathToFileURL } from "url";
6405
7009
 
6406
- // src/cli/model-catalog/cache.ts
7010
+ // src/cli/pi-migration.ts
6407
7011
  import {
7012
+ copyFileSync as copyFileSync4,
6408
7013
  existsSync as existsSync11,
6409
7014
  mkdirSync as mkdirSync5,
6410
- readFileSync as readFileSync7,
7015
+ readdirSync as readdirSync2,
7016
+ readFileSync as readFileSync9,
6411
7017
  renameSync as renameSync4,
6412
- unlinkSync,
7018
+ statSync as statSync2,
6413
7019
  writeFileSync as writeFileSync4
6414
7020
  } from "fs";
6415
- import { homedir as homedir5 } from "os";
6416
- import { dirname as dirname6, posix, win32 } from "path";
6417
- import { z as z2 } from "zod";
6418
- var modelCatalogSourceSchema = z2.enum(["remote", "lkg", "manual"]);
6419
- var cachedModelOptionSchema = z2.object({
6420
- id: z2.string().min(1),
6421
- catalogId: z2.string().min(1).optional(),
6422
- label: z2.string().min(1),
6423
- provider: z2.string().min(1),
6424
- efforts: z2.array(z2.string().min(1)),
6425
- source: modelCatalogSourceSchema
6426
- }).passthrough();
6427
- var modelsDevCacheSchema = z2.object({
6428
- version: z2.literal(1),
6429
- etag: z2.string().optional(),
6430
- fetchedAt: z2.string().datetime(),
6431
- catalog: z2.object({ models: z2.array(cachedModelOptionSchema) }).passthrough()
6432
- }).passthrough();
6433
- function resolveModelsDevCachePath(options = {}) {
6434
- const platform = options.platform ?? process.platform;
6435
- const env = options.env ?? process.env;
6436
- const home = options.homeDir ?? homedir5();
6437
- if (platform === "win32") {
6438
- const path = win32;
6439
- const base2 = env.LOCALAPPDATA ?? env.APPDATA ?? path.join(home, "AppData", "Local");
6440
- return path.join(base2, "thoth-agents", "models-dev-v1.json");
6441
- }
6442
- const base = env.XDG_CACHE_HOME ?? posix.join(home, ".cache");
6443
- return posix.join(base, "thoth-agents", "models-dev-v1.json");
7021
+ import { dirname as dirname6, join as join7 } from "path";
7022
+ function files(root, current = root) {
7023
+ if (!existsSync11(root)) return [];
7024
+ return readdirSync2(current, { withFileTypes: true }).flatMap((entry) => {
7025
+ const path = join7(current, entry.name);
7026
+ return entry.isDirectory() ? files(root, path) : [path.slice(root.length + 1).replaceAll("\\", "/")];
7027
+ }).sort();
7028
+ }
7029
+ function identicalTrees(left, right) {
7030
+ const leftFiles = files(left);
7031
+ const rightFiles = files(right);
7032
+ return leftFiles.length === rightFiles.length && leftFiles.every(
7033
+ (name, index) => name === rightFiles[index] && readFileSync9(join7(left, name)).equals(readFileSync9(join7(right, name)))
7034
+ );
6444
7035
  }
6445
- function readModelsDevCache(cachePath) {
6446
- if (!existsSync11(cachePath)) return void 0;
6447
- try {
6448
- const input = JSON.parse(readFileSync7(cachePath, "utf8"));
6449
- const parsed = modelsDevCacheSchema.safeParse(input);
6450
- return parsed.success ? parsed.data : void 0;
6451
- } catch {
6452
- return void 0;
6453
- }
7036
+ function removeRootBlock(content) {
7037
+ const start = content.indexOf(PI_ROOT_START);
7038
+ const end = content.indexOf(PI_ROOT_END, start + PI_ROOT_START.length);
7039
+ if (start < 0 && end < 0) return void 0;
7040
+ if (start < 0 || end < 0 || content.indexOf(PI_ROOT_START, start + 1) >= 0 || content.indexOf(PI_ROOT_END, end + 1) >= 0)
7041
+ throw new Error("Legacy Pi root block ownership is ambiguous.");
7042
+ const after = end + PI_ROOT_END.length;
7043
+ return `${content.slice(0, start)}${content.slice(after).replace(/^\r?\n/, "")}`;
6454
7044
  }
6455
- function writeModelsDevCacheAtomic(cachePath, cache) {
6456
- const validated = modelsDevCacheSchema.safeParse(cache);
6457
- if (!validated.success) {
6458
- throw new Error(`Invalid models.dev cache: ${validated.error.message}`);
7045
+ function migrateLegacyPiResources(options) {
7046
+ const changed = [];
7047
+ const manualActions = [];
7048
+ const appendPath = join7(options.piRoot, "APPEND_SYSTEM.md");
7049
+ let nextRoot;
7050
+ try {
7051
+ if (existsSync11(appendPath))
7052
+ nextRoot = removeRootBlock(readFileSync9(appendPath, "utf8"));
7053
+ } catch (error) {
7054
+ manualActions.push(error instanceof Error ? error.message : String(error));
7055
+ }
7056
+ const retire = [];
7057
+ for (const name of THOTH_OWNED_SKILL_NAMES) {
7058
+ const source = join7(options.piRoot, "skills", name);
7059
+ if (!existsSync11(source)) continue;
7060
+ const canonical = join7(options.packageRoot, "skills", name);
7061
+ const backup = `${source}.thoth-agents-legacy.bak`;
7062
+ if (!statSync2(source).isDirectory() || !identicalTrees(source, canonical) || existsSync11(backup)) {
7063
+ manualActions.push(
7064
+ `Legacy Pi skill ${name} is modified or has ambiguous backup ownership; remove it manually after review.`
7065
+ );
7066
+ continue;
7067
+ }
7068
+ retire.push({ source, backup });
6459
7069
  }
6460
- mkdirSync5(dirname6(cachePath), { recursive: true });
6461
- const temporaryPath = `${cachePath}.${process.pid}.${Date.now()}.tmp`;
7070
+ if (options.dryRun)
7071
+ return {
7072
+ success: true,
7073
+ changed: [
7074
+ ...nextRoot === void 0 ? [] : [appendPath],
7075
+ ...retire.map(({ source }) => source)
7076
+ ],
7077
+ manualActions
7078
+ };
6462
7079
  try {
6463
- writeFileSync4(
6464
- temporaryPath,
6465
- `${JSON.stringify(validated.data, null, 2)}
6466
- `
6467
- );
6468
- renameSync4(temporaryPath, cachePath);
7080
+ if (nextRoot !== void 0) {
7081
+ mkdirSync5(dirname6(appendPath), { recursive: true });
7082
+ copyFileSync4(appendPath, `${appendPath}.thoth-agents-legacy.bak`);
7083
+ const temporary = `${appendPath}.tmp-${process.pid}`;
7084
+ writeFileSync4(temporary, nextRoot);
7085
+ renameSync4(temporary, appendPath);
7086
+ changed.push(appendPath);
7087
+ }
7088
+ for (const item of retire) {
7089
+ renameSync4(item.source, item.backup);
7090
+ changed.push(item.source);
7091
+ }
7092
+ return { success: true, changed, manualActions };
6469
7093
  } catch (error) {
6470
- if (existsSync11(temporaryPath)) unlinkSync(temporaryPath);
6471
- throw error;
7094
+ try {
7095
+ for (const item of retire)
7096
+ if (existsSync11(item.backup) && !existsSync11(item.source))
7097
+ renameSync4(item.backup, item.source);
7098
+ if (existsSync11(`${appendPath}.thoth-agents-legacy.bak`))
7099
+ copyFileSync4(`${appendPath}.thoth-agents-legacy.bak`, appendPath);
7100
+ } catch {
7101
+ }
7102
+ return {
7103
+ success: false,
7104
+ changed,
7105
+ manualActions,
7106
+ error: error instanceof Error ? error.message : String(error)
7107
+ };
6472
7108
  }
6473
7109
  }
6474
7110
 
6475
- // src/cli/model-catalog/models-dev.ts
6476
- import { z as z3 } from "zod";
6477
- var modelsDevModelSchema = z3.object({
6478
- id: z3.string().min(1).optional(),
6479
- name: z3.string().min(1).optional(),
6480
- reasoning: z3.boolean().optional(),
6481
- reasoning_options: z3.array(z3.unknown()).optional()
6482
- }).passthrough();
6483
- var modelsDevProviderSchema = z3.object({
6484
- models: z3.record(z3.string(), modelsDevModelSchema)
6485
- }).passthrough();
6486
- var modelsDevCatalogSchema = z3.record(
6487
- z3.string(),
6488
- modelsDevProviderSchema
6489
- );
6490
- var effortOptionSchema = z3.object({
6491
- type: z3.literal("effort"),
6492
- values: z3.array(z3.string().min(1))
6493
- }).passthrough();
6494
- function normalizedCatalogId(provider, modelKey, declaredId) {
6495
- const candidate = (declaredId ?? modelKey).trim().toLowerCase();
6496
- return candidate.includes("/") ? candidate : `${provider}/${candidate}`;
6497
- }
6498
- function runtimeModelId(provider, catalogId) {
6499
- const prefix = `${provider}/`;
6500
- return catalogId.startsWith(prefix) ? catalogId.slice(prefix.length) : catalogId;
6501
- }
6502
- function effortValues(options) {
6503
- const values = /* @__PURE__ */ new Set();
6504
- for (const option of options ?? []) {
6505
- const parsed = effortOptionSchema.safeParse(option);
6506
- if (!parsed.success) continue;
6507
- for (const value of parsed.data.values) {
6508
- const normalized = value.trim();
6509
- if (normalized) values.add(normalized);
7111
+ // src/cli/pi-native-probe.ts
7112
+ import { spawnSync as spawnSync5 } from "child_process";
7113
+ import { existsSync as existsSync12, mkdtempSync as mkdtempSync2, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
7114
+ import { tmpdir } from "os";
7115
+ import { dirname as dirname7, join as join8, resolve as resolve3 } from "path";
7116
+ function defaultExecutor(command, args, env) {
7117
+ let executable = command;
7118
+ let executableArgs = [...args];
7119
+ if (process.platform === "win32" && command === "pi") {
7120
+ const located = spawnSync5("where.exe", ["pi.cmd"], { encoding: "utf8" }).stdout?.split(/\r?\n/).find(Boolean);
7121
+ const cli = located ? join8(
7122
+ dirname7(located),
7123
+ "node_modules",
7124
+ "@earendil-works",
7125
+ "pi-coding-agent",
7126
+ "dist",
7127
+ "bundle",
7128
+ "cli.js"
7129
+ ) : void 0;
7130
+ if (cli && existsSync12(cli)) {
7131
+ executable = process.execPath;
7132
+ executableArgs = [cli, ...args];
7133
+ }
7134
+ }
7135
+ const result = spawnSync5(executable, executableArgs, {
7136
+ encoding: "utf8",
7137
+ timeout: 3e4,
7138
+ env: { ...process.env, ...env }
7139
+ });
7140
+ return {
7141
+ exitCode: result.status,
7142
+ stdout: result.stdout ?? "",
7143
+ stderr: result.stderr ?? "",
7144
+ error: result.error
7145
+ };
7146
+ }
7147
+ function occurrences(text, marker) {
7148
+ return text.split(marker).length - 1;
7149
+ }
7150
+ function observePiNativeRoot(options) {
7151
+ const ownsIsolatedRoot = options.piHome === void 0;
7152
+ const isolatedRoot = options.piHome ?? mkdtempSync2(join8(tmpdir(), "thoth-pi-observer-"));
7153
+ const observerPath = join8(
7154
+ isolatedRoot,
7155
+ `observer-${process.pid}-${Date.now()}.mjs`
7156
+ );
7157
+ try {
7158
+ writeFileSync5(
7159
+ observerPath,
7160
+ `import { existsSync, readdirSync, readFileSync } from 'node:fs';
7161
+ import { isAbsolute, join, relative, resolve } from 'node:path';
7162
+ import { createAssistantMessageEventStream } from '@earendil-works/pi-ai';
7163
+ const manifestSha256=${JSON.stringify(options.manifestSha256)};
7164
+ const extensionSha256=${JSON.stringify(options.extensionSha256)};
7165
+ const packageRoot=${JSON.stringify(options.packageRoot)};
7166
+ function discoveredSkills(systemPrompt) {
7167
+ const block=systemPrompt.match(/<available_skills>([\\s\\S]*?)<\\/available_skills>/)?.[1] ?? '';
7168
+ const entries=[...block.matchAll(/<skill>[\\s\\S]*?<name>([^<]+)<\\/name>[\\s\\S]*?<location>([^<]+)<\\/location>[\\s\\S]*?<\\/skill>/g)]
7169
+ .map((match)=>({name:match[1],location:match[2]}));
7170
+ if (!packageRoot) return entries;
7171
+ const skillRoot=resolve(packageRoot,'skills');
7172
+ return entries.filter(({location}) => {
7173
+ const child=relative(skillRoot,resolve(location));
7174
+ return child !== '' && !child.startsWith('..') && !isAbsolute(child);
7175
+ });
7176
+ }
7177
+ function specialistEvidence() {
7178
+ const root=join(process.env.PI_CODING_AGENT_DIR ?? '', 'agents');
7179
+ if (!existsSync(root)) return {materializedSpecialists:[],orchestratorChild:false};
7180
+ const managed=readdirSync(root)
7181
+ .filter((name)=>name.endsWith('.md'))
7182
+ .filter((name)=>/^managed-by:\\s*thoth-agents\\s*$/m.test(readFileSync(join(root,name),'utf8')))
7183
+ .map((name)=>name.slice(0,-3))
7184
+ .sort();
7185
+ return {materializedSpecialists:managed.filter((name)=>name!=='orchestrator'),orchestratorChild:managed.includes('orchestrator')};
7186
+ }
7187
+ export default function observer(pi) {
7188
+ let hookCount=0;
7189
+ let sessionStartCount=0;
7190
+ let sessionEvidence={materializedSpecialists:[],orchestratorChild:false};
7191
+ let skills=[];
7192
+ pi.on('session_start', () => {
7193
+ sessionStartCount += 1;
7194
+ sessionEvidence=specialistEvidence();
7195
+ });
7196
+ pi.on('before_provider_request', (event) => {
7197
+ hookCount += 1;
7198
+ process.stdout.write(JSON.stringify({systemPrompt:JSON.stringify(event.payload),manifestSha256,extensionSha256,hookCount,sessionStartCount,discoveredSkills:skills,...sessionEvidence})+'\\n');
7199
+ });
7200
+ pi.registerProvider('thoth-observer', {
7201
+ name:'Thoth Observer', baseUrl:'local://thoth-observer', apiKey:'credential-free', api:'thoth-observer',
7202
+ models:[{id:'probe',name:'Probe',api:'thoth-observer',reasoning:false,input:['text'],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:8192,maxTokens:16}],
7203
+ streamSimple(model, context, streamOptions) {
7204
+ const stream=createAssistantMessageEventStream();
7205
+ queueMicrotask(async () => {
7206
+ try {
7207
+ skills=discoveredSkills(context.systemPrompt);
7208
+ const raw={systemPrompt:context.systemPrompt,messages:context.messages};
7209
+ if (streamOptions?.onPayload) await streamOptions.onPayload(raw);
7210
+ const message={role:'assistant',content:[{type:'text',text:'probe'}],api:model.api,provider:model.provider,model:model.id,usage:{input:0,output:1,cacheRead:0,cacheWrite:0,totalTokens:1,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}},stopReason:'stop',timestamp:Date.now()};
7211
+ stream.push({type:'done',message});
7212
+ } catch (error) { stream.push({type:'error',error}); }
7213
+ });
7214
+ return stream;
7215
+ }
7216
+ });
7217
+ }
7218
+ `
7219
+ );
7220
+ const args = [
7221
+ "--mode",
7222
+ "json",
7223
+ "--no-session",
7224
+ "--no-approve",
7225
+ "--offline",
7226
+ "--no-extensions",
7227
+ "--extension",
7228
+ resolve3(options.extensionPath),
7229
+ "--extension",
7230
+ observerPath,
7231
+ "--provider",
7232
+ "thoth-observer",
7233
+ "--model",
7234
+ "thoth-observer/probe",
7235
+ "--print",
7236
+ options.probePrompt ?? "Return the single word probe."
7237
+ ];
7238
+ const command = { command: options.piCommand ?? "pi", args };
7239
+ const result = (options.commandExecutor ?? defaultExecutor)(
7240
+ command.command,
7241
+ args,
7242
+ { PI_CODING_AGENT_DIR: isolatedRoot }
7243
+ );
7244
+ if (result.exitCode === null || result.error)
7245
+ return {
7246
+ state: "unavailable",
7247
+ basis: [
7248
+ result.stderr || String(result.error ?? "Pi executable unavailable")
7249
+ ],
7250
+ command
7251
+ };
7252
+ if (result.exitCode !== 0)
7253
+ return {
7254
+ state: "unobserved",
7255
+ basis: [`Pi probe exited ${result.exitCode}: ${result.stderr.trim()}`],
7256
+ command
7257
+ };
7258
+ const records = `${result.stdout}
7259
+ ${result.stderr}`.split(/\r?\n/).flatMap((line) => {
7260
+ try {
7261
+ const value = JSON.parse(line);
7262
+ return typeof value.systemPrompt === "string" ? [value] : [];
7263
+ } catch {
7264
+ return [];
7265
+ }
7266
+ });
7267
+ const record = records.at(-1);
7268
+ const prompt = typeof record?.systemPrompt === "string" ? record.systemPrompt : "";
7269
+ const discoveredSkills = Array.isArray(record?.discoveredSkills) ? record.discoveredSkills.flatMap((entry) => {
7270
+ if (!entry || typeof entry !== "object" || typeof entry.name !== "string" || typeof entry.location !== "string")
7271
+ return [];
7272
+ return [{ name: entry.name, location: entry.location }];
7273
+ }) : void 0;
7274
+ const materializedSpecialists = Array.isArray(
7275
+ record?.materializedSpecialists
7276
+ ) ? record.materializedSpecialists.filter(
7277
+ (name) => typeof name === "string"
7278
+ ) : void 0;
7279
+ const orchestratorChild = typeof record?.orchestratorChild === "boolean" ? record.orchestratorChild : void 0;
7280
+ const sessionStartCount = typeof record?.sessionStartCount === "number" ? record.sessionStartCount : void 0;
7281
+ const valid = record?.manifestSha256 === options.manifestSha256 && record?.extensionSha256 === options.extensionSha256 && record?.hookCount === 1 && occurrences(prompt, PI_ROOT_START) === 1 && occurrences(prompt, PI_ROOT_END) === 1 && prompt.indexOf(PI_ROOT_START) < prompt.indexOf(PI_ROOT_END);
7282
+ return valid ? {
7283
+ state: "observed-at-install",
7284
+ basis: [
7285
+ "Real Pi final provider request contained exactly one receipt-bound root marker."
7286
+ ],
7287
+ command,
7288
+ ...discoveredSkills ? { discoveredSkills } : {},
7289
+ ...materializedSpecialists ? { materializedSpecialists } : {},
7290
+ ...orchestratorChild !== void 0 ? { orchestratorChild } : {},
7291
+ ...sessionStartCount !== void 0 ? { sessionStartCount } : {}
7292
+ } : {
7293
+ state: "unobserved",
7294
+ basis: [
7295
+ "Pi probe did not return one complete digest-bound native root marker."
7296
+ ],
7297
+ command
7298
+ };
7299
+ } finally {
7300
+ if (ownsIsolatedRoot)
7301
+ rmSync4(isolatedRoot, { recursive: true, force: true });
7302
+ else rmSync4(observerPath, { force: true });
7303
+ }
7304
+ }
7305
+
7306
+ // src/cli/pi-package-receipt.ts
7307
+ import {
7308
+ existsSync as existsSync13,
7309
+ mkdirSync as mkdirSync6,
7310
+ readFileSync as readFileSync10,
7311
+ renameSync as renameSync5,
7312
+ rmSync as rmSync5,
7313
+ writeFileSync as writeFileSync6
7314
+ } from "fs";
7315
+ import { homedir as homedir5 } from "os";
7316
+ import { dirname as dirname8, isAbsolute as isAbsolute2, join as join9, normalize, resolve as resolve4 } from "path";
7317
+ var keys = [
7318
+ "schemaVersion",
7319
+ "owner",
7320
+ "scope",
7321
+ "packageName",
7322
+ "source",
7323
+ "installSource",
7324
+ "version",
7325
+ "manifestSha256",
7326
+ "extensionSha256"
7327
+ ];
7328
+ var semver = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
7329
+ var digest = /^[0-9a-f]{64}$/;
7330
+ function piPackagePathsEqual(left, right) {
7331
+ const normalizedLeft = normalize(resolve4(left));
7332
+ const normalizedRight = normalize(resolve4(right));
7333
+ return process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight;
7334
+ }
7335
+ function getPiPackageReceiptPath(options = {}) {
7336
+ const env = options.env ?? process.env;
7337
+ const root = options.configRoot ?? env.XDG_CONFIG_HOME?.trim() ?? join9(options.homeDir ?? homedir5(), ".config");
7338
+ return join9(root, "thoth-agents", "pi-package.json");
7339
+ }
7340
+ function parse(value) {
7341
+ if (!value || typeof value !== "object" || Array.isArray(value))
7342
+ throw new Error("Receipt must be a JSON object.");
7343
+ const record = value;
7344
+ if (Object.keys(record).sort().join("|") !== [...keys].sort().join("|"))
7345
+ throw new Error("Receipt contains missing or additional keys.");
7346
+ if (record.schemaVersion !== 1 || record.owner !== "thoth-agents" || record.scope !== "user" || record.packageName !== "thoth-agents")
7347
+ throw new Error("Receipt identity is invalid.");
7348
+ if (typeof record.version !== "string" || !semver.test(record.version))
7349
+ throw new Error("Receipt version is invalid.");
7350
+ const npmSource = `npm:thoth-agents@${record.version}`;
7351
+ const npmIdentity = record.source === npmSource && record.installSource === npmSource;
7352
+ const localIdentity = typeof record.source === "string" && record.source.length > 0 && !isAbsolute2(record.source) && !/^[a-z][a-z+.-]*:/i.test(record.source) && typeof record.installSource === "string" && isAbsolute2(record.installSource);
7353
+ if (!npmIdentity && !localIdentity)
7354
+ throw new Error("Receipt source is not canonical.");
7355
+ if (typeof record.manifestSha256 !== "string" || !digest.test(record.manifestSha256) || typeof record.extensionSha256 !== "string" || !digest.test(record.extensionSha256))
7356
+ throw new Error("Receipt digests are invalid.");
7357
+ return record;
7358
+ }
7359
+ function readPiPackageReceipt(options = {}) {
7360
+ const path = getPiPackageReceiptPath(options);
7361
+ if (!existsSync13(path)) return { status: "missing", path };
7362
+ try {
7363
+ return {
7364
+ status: "valid",
7365
+ path,
7366
+ receipt: parse(JSON.parse(readFileSync10(path, "utf8")))
7367
+ };
7368
+ } catch (error) {
7369
+ return {
7370
+ status: "invalid",
7371
+ path,
7372
+ error: error instanceof Error ? error.message : String(error)
7373
+ };
7374
+ }
7375
+ }
7376
+ function writePiPackageReceipt(receipt, options = {}) {
7377
+ const path = getPiPackageReceiptPath(options);
7378
+ try {
7379
+ const validated = parse(receipt);
7380
+ mkdirSync6(dirname8(path), { recursive: true });
7381
+ const temporary = `${path}.tmp-${process.pid}`;
7382
+ writeFileSync6(temporary, `${JSON.stringify(validated, null, 2)}
7383
+ `, {
7384
+ flag: "wx"
7385
+ });
7386
+ try {
7387
+ renameSync5(temporary, path);
7388
+ } catch (error) {
7389
+ rmSync5(temporary, { force: true });
7390
+ throw error;
7391
+ }
7392
+ return { success: true, path };
7393
+ } catch (error) {
7394
+ return {
7395
+ success: false,
7396
+ path,
7397
+ error: error instanceof Error ? error.message : String(error)
7398
+ };
7399
+ }
7400
+ }
7401
+ function classifyPiPackageOwnership(input) {
7402
+ if (input.receipt.status === "invalid" || input.projectPackages.length > 0 || input.globalPackages.length > 1)
7403
+ return {
7404
+ state: "conflicting",
7405
+ reason: input.receipt.status === "invalid" ? input.receipt.error : "Ambiguous or project-local first-party source."
7406
+ };
7407
+ const configured = input.globalPackages[0];
7408
+ if (input.receipt.status === "missing")
7409
+ return { state: configured ? "configured-unowned" : "missing" };
7410
+ if (!configured) return { state: "owned-missing" };
7411
+ const configuredPath = configured.installedPath;
7412
+ const receipt = input.receipt.receipt;
7413
+ const identityMatches = (configured.packageName === void 0 || configured.packageName === receipt.packageName) && (configured.packageVersion === void 0 || configured.packageVersion === receipt.version);
7414
+ const isNpm = receipt.source === receipt.installSource;
7415
+ const pathMatches = isNpm || configuredPath !== void 0 && piPackagePathsEqual(configuredPath, receipt.installSource);
7416
+ return configured.source === receipt.source && pathMatches && identityMatches ? { state: "owned-current" } : {
7417
+ state: "conflicting",
7418
+ reason: "Configured source, resolved package identity, or path differs from the ownership receipt."
7419
+ };
7420
+ }
7421
+
7422
+ // src/cli/pi-paths.ts
7423
+ import { homedir as homedir6 } from "os";
7424
+ import { join as join10, resolve as resolve5 } from "path";
7425
+ function resolvePiPaths(options = {}) {
7426
+ const env = options.env ?? process.env;
7427
+ const homeDir = resolve5(
7428
+ options.homeDir ?? env.HOME ?? env.USERPROFILE ?? homedir6()
7429
+ );
7430
+ const defaultPiRoot = join10(homeDir, ".pi", "agent");
7431
+ const configured = env.PI_CODING_AGENT_DIR?.trim();
7432
+ const piRoot = configured ? resolve5(configured) : defaultPiRoot;
7433
+ const cwd = resolve5(options.cwd ?? process.cwd());
7434
+ const configRoot = env.XDG_CONFIG_HOME?.trim() ? resolve5(env.XDG_CONFIG_HOME) : join10(homeDir, ".config");
7435
+ return {
7436
+ homeDir,
7437
+ piRoot,
7438
+ defaultPiRoot,
7439
+ customPiRoot: piRoot !== defaultPiRoot,
7440
+ skillsDestinationCompatible: piRoot === defaultPiRoot,
7441
+ appendSystemPath: join10(piRoot, "APPEND_SYSTEM.md"),
7442
+ agentsRoot: join10(piRoot, "agents"),
7443
+ alternateAgentsRoot: join10(piRoot, "subagents"),
7444
+ ownedSkillsRoot: join10(piRoot, "skills"),
7445
+ mcpConfigPath: join10(configRoot, "mcp", "mcp.json"),
7446
+ projectAgentRoots: [
7447
+ join10(cwd, ".pi", "agents"),
7448
+ join10(cwd, ".pi", "subagents")
7449
+ ],
7450
+ projectMcpPaths: [join10(cwd, ".mcp.json"), join10(cwd, ".pi", "mcp.json")]
7451
+ };
7452
+ }
7453
+
7454
+ // src/cli/pi-install.ts
7455
+ var PI_MINIMUM_VERSION = "0.84.4";
7456
+ var PI_NODE_MINIMUM = "22.19.0";
7457
+ var PI_COMMAND_TIMEOUT_MS = 12e4;
7458
+ var PI_PACKAGE_SPECS = [
7459
+ {
7460
+ id: "delegation",
7461
+ source: "npm:pi-subagents-j0k3r@1.5.9",
7462
+ packageName: "pi-subagents-j0k3r",
7463
+ version: "1.5.9"
7464
+ },
7465
+ {
7466
+ id: "context7",
7467
+ source: "npm:@upstash/context7-pi@0.1.2",
7468
+ packageName: "@upstash/context7-pi",
7469
+ version: "0.1.2"
7470
+ },
7471
+ {
7472
+ id: "web-access",
7473
+ source: "npm:pi-web-access@0.27.0",
7474
+ packageName: "pi-web-access",
7475
+ version: "0.27.0"
7476
+ },
7477
+ {
7478
+ id: "grep-adapter",
7479
+ source: "npm:pi-mcp-adapter@2.32.1",
7480
+ packageName: "pi-mcp-adapter",
7481
+ version: "2.32.1"
7482
+ },
7483
+ {
7484
+ id: "ask-user-question",
7485
+ source: "npm:@juicesharp/rpiv-ask-user-question@2.9.0",
7486
+ packageName: "@juicesharp/rpiv-ask-user-question",
7487
+ version: "2.9.0"
7488
+ },
7489
+ {
7490
+ id: "todo",
7491
+ source: "npm:@juicesharp/rpiv-todo@2.9.0",
7492
+ packageName: "@juicesharp/rpiv-todo",
7493
+ version: "2.9.0"
7494
+ }
7495
+ ];
7496
+ var PI_GREP_MCP_ENTRY = {
7497
+ url: "https://mcp.grep.app",
7498
+ protocolVersion: "legacy",
7499
+ lifecycle: "lazy"
7500
+ };
7501
+ function getPiFirstPartySource(version) {
7502
+ return `npm:thoth-agents@${version}`;
7503
+ }
7504
+ function packageRootFor(options) {
7505
+ const root = options.packageRoot ?? findPackageRoot(dirname9(fileURLToPath5(import.meta.url)));
7506
+ if (!root)
7507
+ throw new Error(
7508
+ "Unable to locate the executing thoth-agents package root."
7509
+ );
7510
+ return root;
7511
+ }
7512
+ function packageVersionFor(root) {
7513
+ const value = JSON.parse(
7514
+ readFileSync11(join11(root, "package.json"), "utf8")
7515
+ );
7516
+ if (typeof value.version !== "string")
7517
+ throw new Error("Executing thoth-agents package version is invalid.");
7518
+ return value.version;
7519
+ }
7520
+ function sha2562(path) {
7521
+ return createHash4("sha256").update(readFileSync11(path)).digest("hex");
7522
+ }
7523
+ function verifyPiFirstPartyPackage(input) {
7524
+ try {
7525
+ const manifestPath = join11(input.packageRoot, "package.json");
7526
+ const extensionPath = join11(input.packageRoot, "dist", "pi.js");
7527
+ const assetsPath = join11(
7528
+ input.packageRoot,
7529
+ "pi",
7530
+ ".thoth-agents-assets.json"
7531
+ );
7532
+ for (const path of [manifestPath, extensionPath, assetsPath])
7533
+ if (!existsSync14(path) || !lstatSync3(path).isFile() || lstatSync3(path).isSymbolicLink())
7534
+ throw new Error(
7535
+ `Required regular package asset is missing or symlinked: ${path}`
7536
+ );
7537
+ const manifest = JSON.parse(readFileSync11(manifestPath, "utf8"));
7538
+ if (manifest.name !== "thoth-agents" || manifest.version !== input.version || JSON.stringify(manifest.pi) !== JSON.stringify({ extensions: ["./dist/pi.js"], skills: ["./skills"] }))
7539
+ throw new Error(
7540
+ "Installed package identity or Pi manifest is inconsistent."
7541
+ );
7542
+ const assets = JSON.parse(readFileSync11(assetsPath, "utf8"));
7543
+ const expectedAgentPaths = PI_SPECIALIST_ROLES.map(
7544
+ (role) => `agents/${piSpecialistName(role)}.md`
7545
+ );
7546
+ if (assets.schemaVersion !== 1 || assets.owner !== "thoth-agents" || !isRecord6(assets.files) || Object.keys(assets.files).sort().join("|") !== [...expectedAgentPaths].sort().join("|"))
7547
+ throw new Error("Installed Pi specialist provenance is invalid.");
7548
+ for (const relativePath of expectedAgentPaths) {
7549
+ const path = join11(input.packageRoot, "pi", relativePath);
7550
+ if (!existsSync14(path) || !lstatSync3(path).isFile() || lstatSync3(path).isSymbolicLink() || sha2562(path) !== assets.files[relativePath])
7551
+ throw new Error(
7552
+ `Installed Pi specialist asset is missing, symlinked, or stale: ${relativePath}`
7553
+ );
7554
+ }
7555
+ for (const skill of THOTH_OWNED_SKILL_NAMES) {
7556
+ const path = join11(input.packageRoot, "skills", skill, "SKILL.md");
7557
+ if (!existsSync14(path) || !lstatSync3(path).isFile() || lstatSync3(path).isSymbolicLink())
7558
+ throw new Error(
7559
+ `Installed Pi manifest skill is missing or symlinked: ${skill}`
7560
+ );
7561
+ }
7562
+ const manifestSha256 = sha2562(manifestPath);
7563
+ const extensionSha256 = sha2562(extensionPath);
7564
+ const load = defaultCommandExecutor4("node", [
7565
+ "--input-type=module",
7566
+ "--eval",
7567
+ `import(${JSON.stringify(pathToFileURL(extensionPath).href)}).then(m=>{if(typeof m.default!=="function")process.exit(2)})`
7568
+ ]);
7569
+ if (load.exitCode !== 0)
7570
+ throw new Error(`Compiled Pi extension is not loadable: ${load.stderr}`);
7571
+ const observation = observePiNativeRoot({
7572
+ extensionPath,
7573
+ manifestSha256,
7574
+ extensionSha256
7575
+ });
7576
+ if (observation.state !== "observed-at-install")
7577
+ throw new Error(observation.basis.join("; "));
7578
+ return {
7579
+ success: true,
7580
+ receipt: {
7581
+ schemaVersion: 1,
7582
+ owner: "thoth-agents",
7583
+ scope: "user",
7584
+ packageName: "thoth-agents",
7585
+ source: input.source,
7586
+ installSource: input.installSource,
7587
+ version: input.version,
7588
+ manifestSha256,
7589
+ extensionSha256
7590
+ }
7591
+ };
7592
+ } catch (error) {
7593
+ return {
7594
+ success: false,
7595
+ error: error instanceof Error ? error.message : String(error)
7596
+ };
7597
+ }
7598
+ }
7599
+ function isRecord6(value) {
7600
+ return typeof value === "object" && value !== null && !Array.isArray(value);
7601
+ }
7602
+ function readJsonObject(path) {
7603
+ if (!existsSync14(path)) return {};
7604
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
7605
+ if (!isRecord6(parsed)) throw new Error(`${path} must contain a JSON object.`);
7606
+ return parsed;
7607
+ }
7608
+ function mergePiGrepMcpConfig(current) {
7609
+ const rawServers = current.mcpServers;
7610
+ if (rawServers !== void 0 && !isRecord6(rawServers)) {
7611
+ throw new Error("Global MCP mcpServers must be a JSON object.");
7612
+ }
7613
+ const servers = isRecord6(rawServers) ? rawServers : {};
7614
+ const existing = servers.grep;
7615
+ if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(PI_GREP_MCP_ENTRY)) {
7616
+ throw new Error(
7617
+ 'Global MCP server "grep" is not owned by thoth-agents and conflicts with the required grep.app entry.'
7618
+ );
7619
+ }
7620
+ return {
7621
+ ...current,
7622
+ mcpServers: { ...servers, grep: { ...PI_GREP_MCP_ENTRY } }
7623
+ };
7624
+ }
7625
+ function frontmatterValue(content, field) {
7626
+ const lines = content.split(/\r?\n/);
7627
+ if (lines[0]?.trim() !== "---") return void 0;
7628
+ const end = lines.findIndex(
7629
+ (line2, index) => index > 0 && line2.trim() === "---"
7630
+ );
7631
+ if (end === -1) return void 0;
7632
+ const prefix = `${field}:`;
7633
+ const line = lines.slice(1, end).find((candidate) => candidate.startsWith(prefix));
7634
+ if (!line) return void 0;
7635
+ const value = line.slice(prefix.length).trim();
7636
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
7637
+ return value.slice(1, -1).trim();
7638
+ }
7639
+ return value || void 0;
7640
+ }
7641
+ function findAgentConflicts(paths) {
7642
+ const canonical = new Set(PI_SPECIALIST_ROLES.map(piSpecialistName));
7643
+ const conflicts = [];
7644
+ for (const root of [paths.agentsRoot, paths.alternateAgentsRoot]) {
7645
+ if (!existsSync14(root)) continue;
7646
+ for (const name of readdirSync3(root)) {
7647
+ if (!name.endsWith(".md")) continue;
7648
+ const path = join11(root, name);
7649
+ const content = readFileSync11(path, "utf8");
7650
+ const role = frontmatterValue(content, "name");
7651
+ const filenameRole = name.slice(0, -".md".length).toLowerCase();
7652
+ const canonicalRole = role && canonical.has(role) ? role : canonical.has(filenameRole) ? filenameRole : void 0;
7653
+ if (canonicalRole && frontmatterValue(content, "managed-by") !== PI_MANAGED_OWNER) {
7654
+ conflicts.push(
7655
+ `${path} defines canonical specialist ${canonicalRole} without thoth-agents ownership.`
7656
+ );
7657
+ }
7658
+ }
7659
+ }
7660
+ return conflicts;
7661
+ }
7662
+ function buildPiSetupPlan(options = {}) {
7663
+ const paths = resolvePiPaths(options);
7664
+ let packageRoot;
7665
+ let expectedVersion;
7666
+ try {
7667
+ packageRoot = packageRootFor(options);
7668
+ expectedVersion = options.expectedVersion ?? packageVersionFor(packageRoot);
7669
+ } catch {
7670
+ packageRoot = options.packageRoot ?? "";
7671
+ expectedVersion = options.expectedVersion ?? "";
7672
+ }
7673
+ const blockers = [];
7674
+ const diagnostics = [];
7675
+ if (!paths.skillsDestinationCompatible) {
7676
+ blockers.push(
7677
+ `PI_CODING_AGENT_DIR resolves to ${paths.piRoot}, but the required skills CLI targets ${paths.defaultPiRoot}. Remove the override or install the four external skills manually into ${paths.ownedSkillsRoot}.`
7678
+ );
7679
+ }
7680
+ blockers.push(...findAgentConflicts(paths));
7681
+ let mcpContent;
7682
+ try {
7683
+ mcpContent = `${JSON.stringify(
7684
+ mergePiGrepMcpConfig(readJsonObject(paths.mcpConfigPath)),
7685
+ null,
7686
+ 2
7687
+ )}
7688
+ `;
7689
+ } catch (error) {
7690
+ blockers.push(error instanceof Error ? error.message : String(error));
7691
+ }
7692
+ for (const path of [...paths.projectAgentRoots, ...paths.projectMcpPaths]) {
7693
+ if (existsSync14(path))
7694
+ diagnostics.push(
7695
+ `Project-local Pi resource may shadow global managed state: ${path}`
7696
+ );
7697
+ }
7698
+ const rendered = piAdapter.render({
7699
+ projectRoot: options.cwd ?? process.cwd()
7700
+ });
7701
+ if (!expectedVersion || !packageRoot)
7702
+ blockers.push(
7703
+ "The executing thoth-agents package identity is unavailable."
7704
+ );
7705
+ const firstPartySource = options.firstPartySource ?? getPiFirstPartySource(expectedVersion);
7706
+ const items = [
7707
+ {
7708
+ kind: "preflight",
7709
+ description: "Verify Node.js >=22.19 and Pi >=0.84.4 before mutation",
7710
+ target: "node/pi runtime"
7711
+ },
7712
+ {
7713
+ kind: "package",
7714
+ description: `Install and verify first-party native Pi package ${firstPartySource}`,
7715
+ target: firstPartySource,
7716
+ command: {
7717
+ command: "pi",
7718
+ args: ["install", firstPartySource, "--no-approve"]
7719
+ }
7720
+ },
7721
+ ...PI_PACKAGE_SPECS.map((pkg) => ({
7722
+ kind: "package",
7723
+ description: `Install and verify Pi package ${pkg.source}`,
7724
+ target: pkg.source,
7725
+ command: { command: "pi", args: ["install", pkg.source, "--no-approve"] }
7726
+ })),
7727
+ ...mcpContent === void 0 ? [] : [
7728
+ {
7729
+ kind: "mcp",
7730
+ description: "Merge attributable global grep.app MCP entry",
7731
+ target: paths.mcpConfigPath,
7732
+ content: mcpContent
7733
+ }
7734
+ ],
7735
+ ...rendered.artifacts.filter((artifact) => artifact.kind === "agent-config").map((artifact) => ({
7736
+ kind: "agent",
7737
+ description: artifact.description ?? `Install ${artifact.path}`,
7738
+ target: join11(paths.piRoot, artifact.path),
7739
+ content: String(artifact.content)
7740
+ }))
7741
+ ];
7742
+ return {
7743
+ dryRun: options.dryRun ?? false,
7744
+ ready: blockers.length === 0,
7745
+ paths,
7746
+ items,
7747
+ blockers,
7748
+ diagnostics,
7749
+ disclaimers: [
7750
+ "Pi extensions execute with the invoking user's system permissions; package pins and tool allowlists are not a security sandbox.",
7751
+ "Context7 and web access are native Pi extensions; only grep.app uses pi-mcp-adapter and directTools is intentionally omitted.",
7752
+ "Project-local resources require Pi trust and may shadow global resources."
7753
+ ],
7754
+ options: { ...options, expectedVersion, packageRoot, firstPartySource }
7755
+ };
7756
+ }
7757
+ function formatPiSetupPlan(plan) {
7758
+ return [
7759
+ "Pi setup plan:",
7760
+ ...plan.items.map(
7761
+ (item) => item.command ? `- ${item.kind}: ${item.command.command} ${item.command.args.join(" ")}` : `- ${item.kind}: ${item.target}`
7762
+ ),
7763
+ ...plan.blockers.map((blocker) => `- BLOCKED: ${blocker}`)
7764
+ ].join("\n");
7765
+ }
7766
+ function isVersionAtLeast(actual, expected) {
7767
+ const parts = (value) => value.replace(/^v/, "").split(".").slice(0, 3).map((part) => Number.parseInt(part, 10));
7768
+ const left = parts(actual);
7769
+ const right = parts(expected);
7770
+ if (left.some(Number.isNaN) || right.some(Number.isNaN)) return false;
7771
+ for (let index = 0; index < 3; index += 1) {
7772
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
7773
+ if (difference !== 0) return difference > 0;
7774
+ }
7775
+ return true;
7776
+ }
7777
+ function hasExactInstalledPiPackage(output, source) {
7778
+ return output.split(/\r?\n/).some((line) => line.trim() === source);
7779
+ }
7780
+ function parsePiPackageList(output) {
7781
+ const packages = [];
7782
+ let scope = "user";
7783
+ for (const line of output.split(/\r?\n/)) {
7784
+ const value = line.trim();
7785
+ if (!value || value === "No packages installed.") continue;
7786
+ if (value === "User packages:") {
7787
+ scope = "user";
7788
+ continue;
7789
+ }
7790
+ if (value === "Project packages:") {
7791
+ scope = "project";
7792
+ continue;
7793
+ }
7794
+ const previous = packages.at(-1);
7795
+ if (/^\s{4,}\S/.test(line) && previous && !previous.installedPath) {
7796
+ previous.installedPath = value;
7797
+ continue;
7798
+ }
7799
+ packages.push({ scope, source: value.replace(/ \(filtered\)$/, "") });
7800
+ }
7801
+ return packages;
7802
+ }
7803
+ function isThothPackageLocation(candidate, knownSource) {
7804
+ if (candidate.source === knownSource || /^npm:thoth-agents@/.test(candidate.source))
7805
+ return true;
7806
+ if (!candidate.installedPath) return false;
7807
+ try {
7808
+ const manifest = JSON.parse(
7809
+ readFileSync11(join11(candidate.installedPath, "package.json"), "utf8")
7810
+ );
7811
+ return manifest.name === "thoth-agents";
7812
+ } catch {
7813
+ return false;
7814
+ }
7815
+ }
7816
+ function configuredPackageIdentity(candidate) {
7817
+ if (!candidate.installedPath) return { packageName: "", packageVersion: "" };
7818
+ try {
7819
+ const manifest = JSON.parse(
7820
+ readFileSync11(join11(candidate.installedPath, "package.json"), "utf8")
7821
+ );
7822
+ return {
7823
+ packageName: typeof manifest.name === "string" ? manifest.name : "",
7824
+ packageVersion: typeof manifest.version === "string" ? manifest.version : ""
7825
+ };
7826
+ } catch {
7827
+ return { packageName: "", packageVersion: "" };
7828
+ }
7829
+ }
7830
+ function getPiFirstPartyPackages(packages, knownSource) {
7831
+ return packages.filter((candidate) => isThothPackageLocation(candidate, knownSource)).map((candidate) => ({
7832
+ ...candidate,
7833
+ ...configuredPackageIdentity(candidate)
7834
+ }));
7835
+ }
7836
+ function matchesInstallSource(candidate, installSource) {
7837
+ return isAbsolute3(installSource) ? candidate.installedPath !== void 0 && piPackagePathsEqual(candidate.installedPath, installSource) : candidate.source === installSource;
7838
+ }
7839
+ function defaultCommandExecutor4(command, args) {
7840
+ let executable = command;
7841
+ let executableArgs = [...args];
7842
+ if (process.platform === "win32" && command === "pi") {
7843
+ const located = spawnSync6("where.exe", ["pi.cmd"], { encoding: "utf8" }).stdout?.split(/\r?\n/).find(Boolean);
7844
+ const cli = located ? join11(
7845
+ dirname9(located),
7846
+ "node_modules",
7847
+ "@earendil-works",
7848
+ "pi-coding-agent",
7849
+ "dist",
7850
+ "bundle",
7851
+ "cli.js"
7852
+ ) : void 0;
7853
+ if (cli && existsSync14(cli)) {
7854
+ executable = process.execPath;
7855
+ executableArgs = [cli, ...args];
7856
+ }
7857
+ }
7858
+ const result = spawnSync6(executable, executableArgs, {
7859
+ encoding: "utf8",
7860
+ timeout: PI_COMMAND_TIMEOUT_MS
7861
+ });
7862
+ return {
7863
+ exitCode: result.status,
7864
+ stdout: result.stdout ?? "",
7865
+ stderr: result.stderr ?? "",
7866
+ error: result.error
7867
+ };
7868
+ }
7869
+ function writePiManagedText(path, content) {
7870
+ if (existsSync14(path) && readFileSync11(path, "utf8") === content) return false;
7871
+ mkdirSync7(dirname9(path), { recursive: true });
7872
+ const temporaryPath = `${path}.tmp`;
7873
+ const backupPath = `${path}.bak`;
7874
+ writeFileSync7(temporaryPath, content);
7875
+ try {
7876
+ if (existsSync14(path)) copyFileSync5(path, backupPath);
7877
+ renameSync6(temporaryPath, path);
7878
+ } catch (error) {
7879
+ rmSync6(temporaryPath, { force: true });
7880
+ throw error;
7881
+ }
7882
+ return true;
7883
+ }
7884
+ function applyPiSetup(plan) {
7885
+ const diagnostics = [...plan.diagnostics];
7886
+ if (!plan.ready)
7887
+ return {
7888
+ success: false,
7889
+ changed: [],
7890
+ diagnostics,
7891
+ error: plan.blockers.join("\n"),
7892
+ failedStep: "preflight",
7893
+ installedPackages: []
7894
+ };
7895
+ if (plan.dryRun)
7896
+ return { success: true, changed: [], diagnostics, installedPackages: [] };
7897
+ const execute = plan.options.commandExecutor ?? defaultCommandExecutor4;
7898
+ const changed = [];
7899
+ const installedPackages = [];
7900
+ let receiptCommitted = false;
7901
+ let configuredPackageRoot;
7902
+ try {
7903
+ const node = execute("node", ["--version"]);
7904
+ if (node.exitCode !== 0 || !isVersionAtLeast(node.stdout.trim(), PI_NODE_MINIMUM))
7905
+ throw new Error(
7906
+ `Node.js >=${PI_NODE_MINIMUM} is required; observed ${node.stdout.trim() || node.stderr.trim() || "unavailable"}.`
7907
+ );
7908
+ const pi = execute("pi", ["--version"]);
7909
+ if (pi.exitCode !== 0 || !isVersionAtLeast(pi.stdout.trim(), PI_MINIMUM_VERSION))
7910
+ throw new Error(
7911
+ `Pi >=${PI_MINIMUM_VERSION} is required; observed ${pi.stdout.trim() || pi.stderr.trim() || "unavailable"}.`
7912
+ );
7913
+ const desiredSource = plan.options.firstPartySource ?? getPiFirstPartySource(plan.options.expectedVersion ?? "");
7914
+ const before = execute("pi", ["list", "--no-approve"]);
7915
+ if (before.exitCode !== 0)
7916
+ throw new Error(
7917
+ `Unable to inspect Pi package ownership before mutation: ${before.stderr}`
7918
+ );
7919
+ const receiptOptions = {
7920
+ homeDir: plan.paths.homeDir,
7921
+ env: plan.options.env,
7922
+ ...plan.options.receiptOptions
7923
+ };
7924
+ const receipt = readPiPackageReceipt(receiptOptions);
7925
+ const configuredBefore = parsePiPackageList(before.stdout);
7926
+ const knownReceiptSource = receipt.status === "valid" ? receipt.receipt.source : void 0;
7927
+ const firstPartyBefore = getPiFirstPartyPackages(
7928
+ configuredBefore,
7929
+ knownReceiptSource
7930
+ );
7931
+ const globalPackages = firstPartyBefore.filter(
7932
+ ({ scope }) => scope === "user"
7933
+ );
7934
+ const projectPackages = firstPartyBefore.filter(
7935
+ ({ scope }) => scope === "project"
7936
+ );
7937
+ const ownership = classifyPiPackageOwnership({
7938
+ receipt,
7939
+ globalPackages,
7940
+ projectPackages
7941
+ });
7942
+ if (ownership.state === "configured-unowned" || ownership.state === "conflicting")
7943
+ throw new Error(
7944
+ `First-party Pi package ownership conflict: ${ownership.reason ?? ownership.state}`
7945
+ );
7946
+ const installed = execute("pi", ["install", desiredSource, "--no-approve"]);
7947
+ if (installed.exitCode !== 0)
7948
+ throw new Error(
7949
+ `Failed to install ${desiredSource}: ${installed.stderr.trim() || "unknown Pi error"}`
7950
+ );
7951
+ installedPackages.push(desiredSource);
7952
+ try {
7953
+ const listed = execute("pi", ["list", "--no-approve"]);
7954
+ const configuredAfter = parsePiPackageList(listed.stdout);
7955
+ const firstPartyAfter = getPiFirstPartyPackages(
7956
+ configuredAfter.filter(
7957
+ (candidate) => isThothPackageLocation(candidate, knownReceiptSource) || matchesInstallSource(candidate, desiredSource)
7958
+ ),
7959
+ knownReceiptSource
7960
+ );
7961
+ const projectCandidates = firstPartyAfter.filter(
7962
+ ({ scope }) => scope === "project"
7963
+ );
7964
+ const globalCandidates = firstPartyAfter.filter(
7965
+ ({ scope }) => scope === "user"
7966
+ );
7967
+ const candidates = globalCandidates.filter(
7968
+ (candidate) => matchesInstallSource(candidate, desiredSource)
7969
+ );
7970
+ if (listed.exitCode !== 0 || candidates.length !== 1 || globalCandidates.length !== 1 || projectCandidates.length !== 0)
7971
+ throw new Error(
7972
+ `Pi did not verify the exact installed package source ${desiredSource}.`
7973
+ );
7974
+ const resolvedPackageRoot = candidates[0]?.installedPath;
7975
+ if (!resolvedPackageRoot || !isAbsolute3(resolvedPackageRoot) || !existsSync14(resolvedPackageRoot) || !lstatSync3(resolvedPackageRoot).isDirectory() || lstatSync3(resolvedPackageRoot).isSymbolicLink())
7976
+ throw new Error(
7977
+ `Pi did not report one absolute regular installed directory for ${desiredSource}.`
7978
+ );
7979
+ const packageRoot = resolvedPackageRoot;
7980
+ configuredPackageRoot = packageRoot;
7981
+ const verified = (plan.options.verifyFirstParty ?? verifyPiFirstPartyPackage)({
7982
+ source: candidates[0]?.source ?? desiredSource,
7983
+ installSource: desiredSource,
7984
+ version: plan.options.expectedVersion ?? packageVersionFor(packageRoot),
7985
+ packageRoot
7986
+ });
7987
+ if (!verified.success) throw new Error(verified.error);
7988
+ const committed = writePiPackageReceipt(verified.receipt, receiptOptions);
7989
+ if (!committed.success)
7990
+ throw new Error(
7991
+ `Could not commit Pi package receipt: ${committed.error}`
7992
+ );
7993
+ receiptCommitted = true;
7994
+ changed.push(committed.path);
7995
+ } catch (originalError) {
7996
+ const restoreSource = ownership.state === "owned-current" && receipt.status === "valid" ? receipt.receipt.installSource : void 0;
7997
+ const rollback = restoreSource ? execute("pi", ["install", restoreSource, "--no-approve"]) : execute("pi", ["remove", desiredSource, "--no-approve"]);
7998
+ const afterRollback = execute("pi", ["list", "--no-approve"]);
7999
+ const rollbackPackages = parsePiPackageList(afterRollback.stdout);
8000
+ const restored = (() => {
8001
+ if (rollback.exitCode !== 0 || afterRollback.exitCode !== 0)
8002
+ return false;
8003
+ if (restoreSource && receipt.status === "valid") {
8004
+ const priorPackages = getPiFirstPartyPackages(
8005
+ rollbackPackages,
8006
+ receipt.receipt.source
8007
+ );
8008
+ return classifyPiPackageOwnership({
8009
+ receipt,
8010
+ globalPackages: priorPackages.filter(
8011
+ ({ scope }) => scope === "user"
8012
+ ),
8013
+ projectPackages: priorPackages.filter(
8014
+ ({ scope }) => scope === "project"
8015
+ )
8016
+ }).state === "owned-current";
8017
+ }
8018
+ return !rollbackPackages.some(
8019
+ (candidate) => matchesInstallSource(candidate, desiredSource)
8020
+ );
8021
+ })();
8022
+ if (!restored)
8023
+ return {
8024
+ success: false,
8025
+ changed,
8026
+ diagnostics,
8027
+ error: `${originalError instanceof Error ? originalError.message : String(originalError)}; rollback failed: ${rollback.stderr || afterRollback.stderr}`,
8028
+ failedStep: "package",
8029
+ installedPackages,
8030
+ rollbackFailed: true,
8031
+ manualRecovery: restoreSource ? `pi install ${restoreSource} --no-approve` : `pi remove ${desiredSource} --no-approve`
8032
+ };
8033
+ return {
8034
+ success: false,
8035
+ changed,
8036
+ diagnostics,
8037
+ error: originalError instanceof Error ? originalError.message : String(originalError),
8038
+ failedStep: "package",
8039
+ installedPackages,
8040
+ receiptCommitted: false
8041
+ };
8042
+ }
8043
+ if (!configuredPackageRoot)
8044
+ throw new Error("Verified Pi package root is unavailable.");
8045
+ const migration = migrateLegacyPiResources({
8046
+ packageRoot: configuredPackageRoot,
8047
+ piRoot: plan.paths.piRoot
8048
+ });
8049
+ diagnostics.push(...migration.manualActions);
8050
+ if (!migration.success)
8051
+ throw new Error(migration.error ?? "Pi legacy migration failed.");
8052
+ changed.push(...migration.changed);
8053
+ for (const pkg of PI_PACKAGE_SPECS) {
8054
+ const result = execute("pi", ["install", pkg.source, "--no-approve"]);
8055
+ if (result.exitCode !== 0)
8056
+ throw new Error(
8057
+ `Failed to install ${pkg.source}: ${result.stderr.trim() || "unknown Pi error"}`
8058
+ );
8059
+ const listed = execute("pi", ["list", "--no-approve"]);
8060
+ if (listed.exitCode !== 0 || !hasExactInstalledPiPackage(listed.stdout, pkg.source)) {
8061
+ throw new Error(
8062
+ `Pi did not verify the exact installed package source ${pkg.source}.`
8063
+ );
8064
+ }
8065
+ installedPackages.push(pkg.source);
8066
+ }
8067
+ for (const item of plan.items.filter(
8068
+ (candidate) => candidate.content !== void 0 && candidate.kind === "mcp"
8069
+ )) {
8070
+ if (writePiManagedText(item.target, item.content ?? ""))
8071
+ changed.push(item.target);
8072
+ }
8073
+ const resources = syncPiSpecialists({
8074
+ packageRoot: configuredPackageRoot,
8075
+ piRoot: plan.paths.piRoot,
8076
+ projectRoots: plan.paths.projectAgentRoots
8077
+ });
8078
+ diagnostics.push(...resources.diagnostics);
8079
+ if (!resources.success)
8080
+ throw new Error(
8081
+ resources.error ?? "Pi specialist synchronization failed."
8082
+ );
8083
+ changed.push(...resources.changed);
8084
+ return {
8085
+ success: true,
8086
+ changed,
8087
+ diagnostics,
8088
+ installedPackages,
8089
+ receiptCommitted,
8090
+ configuredPackageRoot
8091
+ };
8092
+ } catch (error) {
8093
+ return {
8094
+ success: false,
8095
+ changed,
8096
+ diagnostics,
8097
+ error: error instanceof Error ? error.message : String(error),
8098
+ failedStep: installedPackages.length < plan.items.filter((item) => item.kind === "package").length ? "package" : "managed-surface",
8099
+ installedPackages,
8100
+ receiptCommitted
8101
+ };
8102
+ }
8103
+ }
8104
+
8105
+ // src/cli/operations/pi.ts
8106
+ var PI_DISPLAY_NAME = "Pi";
8107
+ var MODEL_ROOT_LIMITATION = "Pi owns the ambient root session model; only thoth-agents-owned specialist definitions may be changed.";
8108
+ var piActions = [
8109
+ {
8110
+ id: "pi-status",
8111
+ kind: "status",
8112
+ label: "Status",
8113
+ description: "Inspect Pi packages, resources, research providers, and CLI ledger",
8114
+ dryRun: false,
8115
+ requiresConfirmation: false,
8116
+ supported: true
8117
+ },
8118
+ {
8119
+ id: "pi-list",
8120
+ kind: "list",
8121
+ label: "List",
8122
+ description: "List Pi-managed targets and truthful capability limits",
8123
+ dryRun: false,
8124
+ requiresConfirmation: false,
8125
+ supported: true
8126
+ },
8127
+ {
8128
+ id: "pi-install",
8129
+ kind: "install",
8130
+ label: "Install",
8131
+ description: "Preview complete ordered Pi setup",
8132
+ dryRun: true,
8133
+ requiresConfirmation: true,
8134
+ supported: true
8135
+ },
8136
+ {
8137
+ id: "pi-update",
8138
+ kind: "update",
8139
+ label: "Update",
8140
+ description: "Preview an installation-equivalent Pi refresh",
8141
+ dryRun: true,
8142
+ requiresConfirmation: true,
8143
+ supported: true
8144
+ },
8145
+ {
8146
+ id: "pi-sync",
8147
+ kind: "sync",
8148
+ label: "Sync",
8149
+ description: "Preview attributable root, specialist, skill, and grep configuration sync",
8150
+ dryRun: true,
8151
+ requiresConfirmation: true,
8152
+ supported: true
8153
+ },
8154
+ {
8155
+ id: "pi-model-config",
8156
+ kind: "model-config",
8157
+ label: "Model",
8158
+ description: "Configure attributable Pi specialist model and effort fields",
8159
+ dryRun: true,
8160
+ requiresConfirmation: true,
8161
+ supported: true,
8162
+ disabledReason: MODEL_ROOT_LIMITATION
8163
+ }
8164
+ ];
8165
+ var piOperationAdapter = {
8166
+ id: "pi",
8167
+ displayName: PI_DISPLAY_NAME,
8168
+ available: true,
8169
+ description: "Pi-native packages, ambient root, six specialists, skills, and hybrid research stack.",
8170
+ actions: piActions
8171
+ };
8172
+ function disclaimers() {
8173
+ return [
8174
+ {
8175
+ code: "pi-no-os-sandbox",
8176
+ message: "Pi extensions execute with the invoking user's system permissions; tool allowlists are not an OS or credential sandbox."
8177
+ },
8178
+ {
8179
+ code: "pi-runtime-owned",
8180
+ message: "Pi and pi-subagents own execution, concurrency, task/history storage, trust, and lifecycle."
8181
+ },
8182
+ {
8183
+ code: "pi-research-independent",
8184
+ message: "Context7, web access, and grep.app availability is reported independently; provider configuration and credentials remain operator-owned."
8185
+ },
8186
+ { code: "pi-root-model-owned", message: MODEL_ROOT_LIMITATION }
8187
+ ];
8188
+ }
8189
+ function warning3(message, code, severity = "important") {
8190
+ return { severity, message, code };
8191
+ }
8192
+ var RESEARCH_PROVIDER_LABEL = {
8193
+ context7: "Context7",
8194
+ "web-access": "Web access",
8195
+ grep: "grep.app"
8196
+ };
8197
+ function researchRuntimeState(provider, targets, evidence) {
8198
+ const observed = evidence.research?.[provider];
8199
+ if (observed) return observed;
8200
+ const packageId = provider === "grep" ? "grep-adapter" : provider;
8201
+ const packageSource = PI_PACKAGE_SPECS.find(
8202
+ ({ id }) => id === packageId
8203
+ )?.source;
8204
+ const packageReady = targets.some(
8205
+ ({ path, state }) => path === packageSource && state === "installed"
8206
+ );
8207
+ const configReady = provider !== "grep" || targets.some(
8208
+ ({ kind, label, state }) => kind === "config" && label?.includes("grep.app") && state === "installed"
8209
+ );
8210
+ if (!packageReady || !configReady)
8211
+ return {
8212
+ state: "drifted",
8213
+ basis: [
8214
+ "managed package or configuration preconditions are not satisfied"
8215
+ ]
8216
+ };
8217
+ return provider === "web-access" ? {
8218
+ state: "unverified",
8219
+ basis: [
8220
+ "exact package is installed; no live provider request was observed"
8221
+ ]
8222
+ } : {
8223
+ state: "ready",
8224
+ basis: [
8225
+ "managed package and configuration preconditions are satisfied; no live probe was requested"
8226
+ ]
8227
+ };
8228
+ }
8229
+ function runtimeTarget(provider, evidence) {
8230
+ const managedState = evidence.state === "ready" ? "installed" : evidence.state === "credential-required" ? "missing" : evidence.state === "drifted" ? "drift" : "unknown";
8231
+ return {
8232
+ kind: "surface",
8233
+ label: `${RESEARCH_PROVIDER_LABEL[provider]} runtime availability`,
8234
+ state: managedState,
8235
+ expected: "ready",
8236
+ observed: evidence.state,
8237
+ description: evidence.basis.join("; ")
8238
+ };
8239
+ }
8240
+ function runtimeDiagnostic(provider, evidence) {
8241
+ if (evidence.state === "ready") return void 0;
8242
+ const code = `pi-${provider}-runtime-${evidence.state}`;
8243
+ const severity = evidence.state === "failed" ? "critical" : evidence.state === "credential-required" || evidence.state === "unverified" ? "minor" : "important";
8244
+ return warning3(
8245
+ `${RESEARCH_PROVIDER_LABEL[provider]} runtime is ${evidence.state}: ${evidence.basis.join("; ")}.`,
8246
+ code,
8247
+ severity
8248
+ );
8249
+ }
8250
+ function statusFromPlan(plan, context2, evidence = {}) {
8251
+ const targets = plan.items.filter((item) => item.kind !== "preflight").map((item) => ({
8252
+ kind: item.kind === "package" ? "package" : item.kind === "agent" ? "file" : item.kind === "mcp" ? "config" : "file",
8253
+ path: item.target,
8254
+ label: item.description,
8255
+ state: item.kind === "package" ? "unknown" : existsSync15(item.target) ? item.content === void 0 || readFileSync12(item.target, "utf8") === item.content ? "installed" : "drift" : "missing",
8256
+ expected: item.kind === "package" ? item.target : "attributable managed content",
8257
+ description: item.kind === "package" ? "Exact installed-package evidence; this does not prove live tool availability." : void 0
8258
+ }));
8259
+ const execute = context2.piCommandExecutor ?? ((command, args) => {
8260
+ const result = spawnSync7(command, [...args], {
8261
+ encoding: "utf8",
8262
+ timeout: 5e3,
8263
+ env: {
8264
+ ...process.env,
8265
+ ...context2.env,
8266
+ ...context2.homeDir ? { HOME: context2.homeDir, USERPROFILE: context2.homeDir } : {},
8267
+ PI_CODING_AGENT_DIR: plan.paths.piRoot
8268
+ }
8269
+ });
8270
+ return {
8271
+ exitCode: result.status,
8272
+ stdout: result.stdout ?? "",
8273
+ stderr: result.stderr ?? "",
8274
+ error: result.error
8275
+ };
8276
+ });
8277
+ const node = execute("node", ["--version"]);
8278
+ const pi = execute("pi", ["--version"]);
8279
+ const packages = execute("pi", ["list", "--no-approve"]);
8280
+ targets.unshift(
8281
+ {
8282
+ kind: "surface",
8283
+ label: "Node.js runtime",
8284
+ state: node.exitCode !== 0 ? "missing" : isVersionAtLeast(node.stdout.trim(), PI_NODE_MINIMUM) ? "installed" : "drift",
8285
+ expected: `>=${PI_NODE_MINIMUM}`,
8286
+ observed: node.stdout.trim() || node.stderr.trim() || "unavailable"
8287
+ },
8288
+ {
8289
+ kind: "package",
8290
+ label: "Pi runtime",
8291
+ state: pi.exitCode !== 0 ? "missing" : isVersionAtLeast(pi.stdout.trim(), PI_MINIMUM_VERSION) ? "installed" : "drift",
8292
+ expected: `>=${PI_MINIMUM_VERSION}`,
8293
+ observed: pi.stdout.trim() || pi.stderr.trim() || "unavailable"
8294
+ }
8295
+ );
8296
+ for (const target of targets.filter(
8297
+ (candidate) => candidate.kind === "package" && candidate.path?.startsWith("npm:")
8298
+ )) {
8299
+ const packageName = target.path?.replace(/^npm:/, "").replace(/@[^@]+$/, "");
8300
+ target.state = packages.exitCode !== 0 ? "unknown" : hasExactInstalledPiPackage(packages.stdout, target.path ?? "") ? "installed" : packageName && packages.stdout.includes(packageName) ? "drift" : "missing";
8301
+ target.observed = packages.exitCode === 0 ? target.state : packages.stderr.trim() || "pi list unavailable";
8302
+ }
8303
+ const receiptOptions = context2.installLedgerOptions ?? {
8304
+ env: context2.env,
8305
+ homeDir: context2.homeDir
8306
+ };
8307
+ const receipt = readPiPackageReceipt(receiptOptions);
8308
+ const configuredPackages = packages.exitCode === 0 ? parsePiPackageList(packages.stdout) : [];
8309
+ const firstPartyPackages = getPiFirstPartyPackages(
8310
+ configuredPackages,
8311
+ receipt.status === "valid" ? receipt.receipt.source : void 0
8312
+ );
8313
+ const ownership = classifyPiPackageOwnership({
8314
+ receipt,
8315
+ globalPackages: firstPartyPackages.filter(({ scope }) => scope === "user"),
8316
+ projectPackages: firstPartyPackages.filter(
8317
+ ({ scope }) => scope === "project"
8318
+ )
8319
+ });
8320
+ targets.push({
8321
+ kind: "file",
8322
+ path: getPiPackageReceiptPath(receiptOptions),
8323
+ label: "First-party Pi ownership receipt",
8324
+ state: receipt.status === "valid" ? "installed" : receipt.status === "missing" ? "missing" : "drift",
8325
+ observed: receipt.status
8326
+ });
8327
+ targets.push({
8328
+ kind: "surface",
8329
+ label: "First-party Pi package ownership",
8330
+ state: ownership.state === "owned-current" ? "installed" : ownership.state === "missing" || ownership.state === "owned-missing" ? "missing" : "drift",
8331
+ expected: "owned-current",
8332
+ observed: ownership.state,
8333
+ description: ownership.reason
8334
+ });
8335
+ let validatedPackageRoot;
8336
+ if (receipt.status === "valid") {
8337
+ const configuredCandidates = firstPartyPackages.filter(
8338
+ ({ scope, source }) => scope === "user" && source === receipt.receipt.source
8339
+ );
8340
+ const configuredPackage = configuredCandidates[0];
8341
+ const configuredPath = configuredPackage?.installedPath;
8342
+ const localPathMatches = receipt.receipt.source === receipt.receipt.installSource || configuredPath !== void 0 && piPackagePathsEqual(configuredPath, receipt.receipt.installSource);
8343
+ const configured = ownership.state === "owned-current" && configuredCandidates.length === 1 && configuredPath !== void 0 && isAbsolute4(configuredPath) && localPathMatches;
8344
+ if (configured) validatedPackageRoot = configuredPath;
8345
+ targets.push({
8346
+ kind: "surface",
8347
+ path: configuredPath,
8348
+ label: "First-party Pi configured source",
8349
+ state: configured ? "installed" : configuredCandidates.length === 0 ? "missing" : "drift",
8350
+ expected: `${receipt.receipt.source} -> ${receipt.receipt.installSource}`,
8351
+ observed: configuredPackage ? `${configuredPackage.source} -> ${configuredPath ?? "unavailable"}` : "unavailable"
8352
+ });
8353
+ const extensionPath = configuredPath ? join12(configuredPath, "dist", "pi.js") : void 0;
8354
+ const manifestPath = configuredPath ? join12(configuredPath, "package.json") : void 0;
8355
+ let receiptMatches = false;
8356
+ if (extensionPath && manifestPath && existsSync15(extensionPath) && existsSync15(manifestPath))
8357
+ receiptMatches = createHash5("sha256").update(readFileSync12(extensionPath)).digest("hex") === receipt.receipt.extensionSha256 && createHash5("sha256").update(readFileSync12(manifestPath)).digest("hex") === receipt.receipt.manifestSha256;
8358
+ targets.push({
8359
+ kind: "surface",
8360
+ path: extensionPath,
8361
+ label: "Native Pi extension loadability",
8362
+ state: receiptMatches ? "installed" : extensionPath ? "drift" : "unknown",
8363
+ observed: receiptMatches ? "loadable" : "unavailable"
8364
+ });
8365
+ targets.push({
8366
+ kind: "surface",
8367
+ label: "Native root observation",
8368
+ state: configured && receiptMatches ? "installed" : "drift",
8369
+ expected: "observed-at-install",
8370
+ observed: configured && receiptMatches ? "observed-at-install (receipt-bound)" : "unobserved"
8371
+ });
8372
+ }
8373
+ const ownedSkills = validatedPackageRoot ? (() => {
8374
+ const inspection = inspectPiPackageSkills({
8375
+ packageRoot: validatedPackageRoot
8376
+ });
8377
+ return inspection.skills.map((skill) => {
8378
+ const issue = inspection.issues.find(({ name }) => name === skill.name) ?? (!inspection.success ? {
8379
+ state: "drift",
8380
+ message: inspection.error ?? "Package-declared Pi skill inspection is unavailable."
8381
+ } : void 0);
8382
+ return {
8383
+ kind: "skill",
8384
+ path: join12(skill.destinationPath, "SKILL.md"),
8385
+ label: `Pi package-declared skill: ${skill.name}`,
8386
+ state: issue?.state ?? "installed",
8387
+ ...issue ? { observed: issue.message } : {}
8388
+ };
8389
+ });
8390
+ })() : THOTH_OWNED_SKILL_NAMES.map((name) => ({
8391
+ kind: "skill",
8392
+ label: `Pi package-declared skill: ${name}`,
8393
+ state: "unknown",
8394
+ observed: "package ownership unavailable"
8395
+ }));
8396
+ targets.push(...ownedSkills);
8397
+ targets.push(
8398
+ getCliManagedInstallVersionTarget("pi", {
8399
+ env: context2.env,
8400
+ homeDir: context2.homeDir
8401
+ })
8402
+ );
8403
+ const fileStates = targets.map((target) => target.state);
8404
+ const state = !plan.ready ? "unknown" : fileStates.includes("missing") ? "missing" : fileStates.includes("drift") ? "drift" : fileStates.includes("unknown") ? "unknown" : "installed";
8405
+ const runtimeEvidence = ["context7", "web-access", "grep"].map(
8406
+ (provider) => ({
8407
+ provider,
8408
+ evidence: researchRuntimeState(provider, targets, evidence)
8409
+ })
8410
+ );
8411
+ targets.push(
8412
+ ...runtimeEvidence.map(
8413
+ ({ provider, evidence: runtime }) => runtimeTarget(provider, runtime)
8414
+ )
8415
+ );
8416
+ const diagnostics = [
8417
+ ...plan.blockers.map(
8418
+ (message) => warning3(message, "pi-preflight-blocked", "critical")
8419
+ ),
8420
+ ...plan.diagnostics.map(
8421
+ (message) => warning3(message, "pi-resource-shadowing")
8422
+ ),
8423
+ ...ownership.state === "configured-unowned" || ownership.state === "conflicting" ? [
8424
+ warning3(
8425
+ ownership.reason ?? `First-party Pi package ownership is ${ownership.state}; remove it manually or restore a matching receipt before Update.`,
8426
+ `pi-first-party-${ownership.state}`,
8427
+ "critical"
8428
+ )
8429
+ ] : ownership.state === "owned-missing" ? [
8430
+ warning3(
8431
+ "The receipt-owned first-party Pi package is not configured; Install or Update may repair it.",
8432
+ "pi-first-party-owned-missing"
8433
+ )
8434
+ ] : [],
8435
+ ...runtimeEvidence.map(
8436
+ ({ provider, evidence: runtime }) => runtimeDiagnostic(provider, runtime)
8437
+ ).filter((item) => item !== void 0)
8438
+ ];
8439
+ return {
8440
+ harness: "pi",
8441
+ displayName: PI_DISPLAY_NAME,
8442
+ state,
8443
+ summary: state === "installed" ? "Pi managed resources are present; native package and remote-provider health remain separately owned." : "Pi setup is missing, drifted, shadowed, or blocked; rerun the complete install after resolving diagnostics.",
8444
+ targets,
8445
+ diagnostics,
8446
+ actions: piActions,
8447
+ disclaimers: disclaimers()
8448
+ };
8449
+ }
8450
+ function contextPlan(context2, dryRun = true) {
8451
+ const build = context2.buildPiSetupPlan ?? buildPiSetupPlan;
8452
+ const version = resolveExecutingPackageVersion();
8453
+ return build({
8454
+ dryRun,
8455
+ cwd: context2.cwd,
8456
+ env: context2.env,
8457
+ homeDir: context2.homeDir,
8458
+ commandExecutor: context2.piCommandExecutor,
8459
+ packageRoot: context2.packageRoot ?? (version.ok ? version.packageRoot : void 0),
8460
+ expectedVersion: version.ok ? version.version : void 0,
8461
+ receiptOptions: context2.installLedgerOptions
8462
+ });
8463
+ }
8464
+ function getPiStatus(context2 = { cwd: process.cwd() }, evidence = {}) {
8465
+ const status = statusFromPlan(contextPlan(context2), context2, evidence);
8466
+ return {
8467
+ ...status,
8468
+ providerCapability: classifyProviderCapabilityEvidence(evidence)
8469
+ };
8470
+ }
8471
+ var planSources = /* @__PURE__ */ new WeakMap();
8472
+ function configuredPackageRootFromStatus(status) {
8473
+ const ownership = status.targets.find(
8474
+ ({ label }) => label === "First-party Pi package ownership"
8475
+ );
8476
+ const configured = status.targets.find(
8477
+ ({ label }) => label === "First-party Pi configured source"
8478
+ );
8479
+ return ownership?.observed === "owned-current" && configured?.state === "installed" && configured.path && isAbsolute4(configured.path) ? configured.path : void 0;
8480
+ }
8481
+ function piPlan(action, context2) {
8482
+ const setup = contextPlan(context2);
8483
+ const status = statusFromPlan(setup, context2);
8484
+ const configuredPackageRoot = configuredPackageRootFromStatus(status);
8485
+ const version = resolveExecutingPackageVersion();
8486
+ const complete = action !== "sync";
8487
+ const items = setup.items.filter(
8488
+ (item) => complete || item.kind === "root" || item.kind === "agent" || item.kind === "mcp"
8489
+ ).map((item) => ({
8490
+ title: item.description,
8491
+ target: {
8492
+ kind: item.kind === "package" ? "package" : item.kind === "mcp" ? "config" : "file",
8493
+ path: item.target,
8494
+ label: item.description
8495
+ },
8496
+ preview: item.command ? `${item.command.command} ${item.command.args.join(" ")}` : `write attributable ${item.kind} surface`,
8497
+ backup: {
8498
+ required: ["root", "agent", "mcp"].includes(item.kind),
8499
+ strategy: "managed-backup-file"
8500
+ }
8501
+ }));
8502
+ items.push({
8503
+ title: "Inspect five package-declared Pi skills",
8504
+ target: { kind: "skill", label: "Pi package-declared skills" },
8505
+ preview: configuredPackageRoot ? getPiOwnedSkillEntries({ packageRoot: configuredPackageRoot }).map((entry) => join12(entry.destinationPath, "SKILL.md")).join(", ") : "unavailable until the first-party Pi package is receipt-validated"
8506
+ });
8507
+ if (complete) {
8508
+ items.push(
8509
+ {
8510
+ title: "Install four required external skills for Pi",
8511
+ target: { kind: "skill", label: "Pi required external skills" },
8512
+ preview: JSON.stringify(
8513
+ REQUIRED_SKILLS.map(
8514
+ (skill) => getRequiredSkillInstallCommand(skill, "pi")
8515
+ ),
8516
+ null,
8517
+ 2
8518
+ )
8519
+ },
8520
+ {
8521
+ title: "Run provider-owned thoth-mem setup pi",
8522
+ target: { kind: "surface", label: "Provider setup" },
8523
+ preview: (() => {
8524
+ const cmd = getThothMemSetupCommand("pi", true);
8525
+ return `${cmd.command} ${cmd.args.join(" ")}`;
8526
+ })()
8527
+ },
8528
+ {
8529
+ title: "Record completed Pi CLI install",
8530
+ target: {
8531
+ kind: "file",
8532
+ path: getInstallLedgerPath(
8533
+ context2.installLedgerOptions ?? {
8534
+ env: context2.env,
8535
+ homeDir: context2.homeDir
8536
+ }
8537
+ ),
8538
+ label: "CLI-managed install version"
8539
+ },
8540
+ preview: version.ok ? JSON.stringify({ harness: "pi", version: version.version }) : "unavailable"
8541
+ }
8542
+ );
8543
+ }
8544
+ const ownershipState = status.targets.find(
8545
+ ({ label }) => label === "First-party Pi package ownership"
8546
+ )?.observed;
8547
+ const ownershipBlocked = ownershipState === "configured-unowned" || ownershipState === "conflicting";
8548
+ const ownershipBlockers = ownershipBlocked ? [
8549
+ {
8550
+ kind: "surface",
8551
+ label: "First-party Pi package ownership blocker",
8552
+ state: "drift",
8553
+ observed: `${ownershipState}; remove the configured first-party package manually or restore its matching receipt before applying ${action}.`
8554
+ }
8555
+ ] : [];
8556
+ const syncSkillsUnavailable = action === "sync" && (!configuredPackageRoot || status.targets.some(
8557
+ ({ label, state }) => label?.startsWith("Pi package-declared skill:") && state !== "installed"
8558
+ ));
8559
+ const syncSkillBlockers = syncSkillsUnavailable ? [
8560
+ {
8561
+ kind: "skill",
8562
+ label: "Pi package-declared skill evidence blocker",
8563
+ state: "unknown",
8564
+ observed: "A receipt-validated configured Pi package root with all five manifest-owned skills is required before Sync."
8565
+ }
8566
+ ] : [];
8567
+ const plan = {
8568
+ id: `pi-${action}-preview`,
8569
+ harness: "pi",
8570
+ action,
8571
+ title: `${action === "install" ? "Install" : action === "update" ? "Update" : "Sync"} Pi`,
8572
+ summary: complete ? "Preview complete pinned Pi packages, owned resources, skills, provider setup, and final ledger commit." : "Preview only attributable Pi root, specialist, package-skill evidence, and grep.app surfaces.",
8573
+ dryRun: true,
8574
+ canApply: setup.ready && (!complete || version.ok) && ownershipBlockers.length === 0 && syncSkillBlockers.length === 0,
8575
+ targets: status.targets,
8576
+ blockerTargets: [
8577
+ ...setup.blockers.map((message) => ({
8578
+ kind: "surface",
8579
+ label: "Pi preflight blocker",
8580
+ state: "unknown",
8581
+ observed: message
8582
+ })),
8583
+ ...ownershipBlockers,
8584
+ ...syncSkillBlockers
8585
+ ],
8586
+ surfaces: setup.items.filter((item) => item.kind !== "preflight").map((item) => ({
8587
+ id: `${item.kind}:${item.target}`,
8588
+ label: item.description,
8589
+ path: item.target
8590
+ })),
8591
+ backup: {
8592
+ required: true,
8593
+ strategy: "managed-backup-file",
8594
+ description: "Attributable shared files retain sibling .bak files when replaced; Pi package state remains manager-owned."
8595
+ },
8596
+ items,
8597
+ warnings: status.diagnostics,
8598
+ disclaimers: disclaimers()
8599
+ };
8600
+ if (!version.ok && complete)
8601
+ plan.warnings.push(
8602
+ warning3(
8603
+ version.error.message,
8604
+ "pi-package-version-unresolved",
8605
+ "critical"
8606
+ )
8607
+ );
8608
+ planSources.set(plan, {
8609
+ setup,
8610
+ context: context2,
8611
+ ...version.ok ? { version: version.version } : {},
8612
+ ...configuredPackageRoot ? { configuredPackageRoot } : {}
8613
+ });
8614
+ return plan;
8615
+ }
8616
+ function buildPiInstallPlan(context2 = { cwd: process.cwd() }) {
8617
+ return piPlan("install", context2);
8618
+ }
8619
+ function buildPiUpdatePlan(context2 = { cwd: process.cwd() }) {
8620
+ return piPlan("update", context2);
8621
+ }
8622
+ function buildPiSyncPlan(context2 = { cwd: process.cwd() }) {
8623
+ return piPlan("sync", context2);
8624
+ }
8625
+ function defaultPiModelRoles(context2 = { cwd: process.cwd() }) {
8626
+ const plan = contextPlan(context2);
8627
+ return getAgentPackContract().roles.filter(
8628
+ (role) => isPiSpecialistRole(role.name)
8629
+ ).map((role) => {
8630
+ const path = plan.items.find(
8631
+ (item) => item.kind === "agent" && basename3(item.target) === `${piSpecialistName(role.name)}.md`
8632
+ )?.target;
8633
+ const content = path && existsSync15(path) ? readFileSync12(path, "utf8") : "";
8634
+ const model = /^model:\s*["']?([^"'\r\n]+)/m.exec(content)?.[1]?.trim();
8635
+ return {
8636
+ role: role.name,
8637
+ model: model && model !== "default" ? model : "inherit",
8638
+ effort: (() => {
8639
+ const value = /^effort:\s*["']?([^"'\r\n]+)/m.exec(content)?.[1]?.trim();
8640
+ return value && value !== "default" && value !== "inherit" ? { kind: "effort", value } : { kind: "inherit" };
8641
+ })()
8642
+ };
8643
+ });
8644
+ }
8645
+ function buildPiModelPlan(input, context2 = { cwd: process.cwd() }) {
8646
+ const setup = contextPlan(context2);
8647
+ const specialistNames = new Set(
8648
+ getAgentPackContract().roles.filter((role) => role.name !== "orchestrator").map((role) => role.name)
8649
+ );
8650
+ const invalid = input.harness !== "pi" || input.roles.some((role) => !specialistNames.has(role.role));
8651
+ const effortErrors = input.roles.map((role) => ({ role: role.role, resolution: resolvePiEffort(role) })).filter(({ resolution }) => !resolution.ok);
8652
+ const items = input.roles.map((role) => {
8653
+ const specialist = isPiSpecialistRole(role.role) ? piSpecialistName(role.role) : void 0;
8654
+ return {
8655
+ title: `Configure Pi specialist ${role.role}`,
8656
+ target: {
8657
+ kind: "file",
8658
+ path: specialist ? setup.items.find(
8659
+ (item) => item.kind === "agent" && basename3(item.target) === `${specialist}.md`
8660
+ )?.target : void 0,
8661
+ label: `Pi ${role.role} specialist`
8662
+ },
8663
+ preview: JSON.stringify({
8664
+ model: role.model,
8665
+ effort: role.effort ?? null
8666
+ })
8667
+ };
8668
+ });
8669
+ const plan = {
8670
+ id: "pi-model-config-preview",
8671
+ harness: "pi",
8672
+ action: "model-config",
8673
+ title: "Configure Pi specialist models",
8674
+ summary: MODEL_ROOT_LIMITATION,
8675
+ dryRun: true,
8676
+ canApply: setup.ready && !invalid && effortErrors.length === 0 && items.every((item) => Boolean(item.target.path)),
8677
+ targets: items.map((item) => item.target),
8678
+ surfaces: [],
8679
+ backup: { required: true, strategy: "managed-backup-file" },
8680
+ items,
8681
+ warnings: [
8682
+ ...invalid ? [
8683
+ warning3(
8684
+ "Pi model configuration accepts only the six owned specialists and never the ambient orchestrator.",
8685
+ "pi-model-role-unsupported",
8686
+ "critical"
8687
+ )
8688
+ ] : [],
8689
+ ...effortErrors.map(
8690
+ ({ role, resolution }) => warning3(
8691
+ `Pi specialist ${role}: ${resolution.ok ? "" : resolution.message}`,
8692
+ "pi-model-effort-unsupported",
8693
+ "critical"
8694
+ )
8695
+ )
8696
+ ],
8697
+ disclaimers: disclaimers()
8698
+ };
8699
+ planSources.set(plan, { setup, context: context2, model: input });
8700
+ return plan;
8701
+ }
8702
+ function replaceFrontmatterField(content, field, value) {
8703
+ const newline = content.includes("\r\n") ? "\r\n" : "\n";
8704
+ const lines = content.split(/\r?\n/);
8705
+ if (lines[0]?.trim() !== "---") return content;
8706
+ const end = lines.findIndex(
8707
+ (line, index2) => index2 > 0 && line.trim() === "---"
8708
+ );
8709
+ if (end === -1) return content;
8710
+ const prefix = `${field}:`;
8711
+ const index = lines.slice(1, end).findIndex((line) => line.startsWith(prefix));
8712
+ const absoluteIndex = index === -1 ? -1 : index + 1;
8713
+ const nativeValue = value === void 0 || value === "inherit" ? "default" : value;
8714
+ const replacement = `${field}: ${JSON.stringify(nativeValue)}`;
8715
+ if (absoluteIndex === -1) lines.splice(1, 0, replacement);
8716
+ else lines[absoluteIndex] = replacement;
8717
+ return lines.join(newline);
8718
+ }
8719
+ function applyPiPlan(plan) {
8720
+ const source = planSources.get(plan);
8721
+ const fileTargets = (paths) => paths.map((path) => ({ kind: "file", path, state: "installed" }));
8722
+ const reject = (message, changedTargets = []) => ({
8723
+ harness: "pi",
8724
+ action: plan.action,
8725
+ applied: false,
8726
+ summary: message,
8727
+ changedTargets,
8728
+ backups: [],
8729
+ warnings: [
8730
+ ...plan.warnings,
8731
+ warning3(message, "pi-apply-rejected", "critical")
8732
+ ],
8733
+ disclaimers: disclaimers()
8734
+ });
8735
+ if (plan.harness !== "pi" || !plan.canApply || !source)
8736
+ return reject(
8737
+ "Pi operation plan is not applicable or was not built in this process."
8738
+ );
8739
+ if (plan.action === "model-config" && source.model) {
8740
+ const changedTargets = [];
8741
+ for (const role of source.model.roles) {
8742
+ const target = plan.items.find((item) => item.title.endsWith(role.role))?.target.path;
8743
+ if (!target || !existsSync15(target))
8744
+ return reject(
8745
+ `Owned Pi specialist definition is missing: ${role.role}.`
8746
+ );
8747
+ let content = readFileSync12(target, "utf8");
8748
+ content = replaceFrontmatterField(content, "model", role.model);
8749
+ content = replaceFrontmatterField(
8750
+ content,
8751
+ "effort",
8752
+ role.effort?.kind === "effort" ? role.effort.value : void 0
8753
+ );
8754
+ writePiManagedText(target, content);
8755
+ changedTargets.push({
8756
+ kind: "file",
8757
+ path: target,
8758
+ label: `Pi ${role.role} specialist`,
8759
+ state: "installed"
8760
+ });
8761
+ }
8762
+ return {
8763
+ harness: "pi",
8764
+ action: plan.action,
8765
+ applied: true,
8766
+ summary: "Applied Pi specialist model configuration; ambient root model remains Pi-owned.",
8767
+ changedTargets,
8768
+ backups: [],
8769
+ warnings: [],
8770
+ disclaimers: disclaimers()
8771
+ };
8772
+ }
8773
+ if (plan.action === "sync") {
8774
+ const fresh = (source.context.buildPiSetupPlan ?? buildPiSetupPlan)({
8775
+ ...source.setup.options,
8776
+ dryRun: false
8777
+ });
8778
+ if (!fresh.ready) return reject(fresh.blockers.join("\n"));
8779
+ const liveStatus = statusFromPlan(
8780
+ contextPlan(source.context),
8781
+ source.context
8782
+ );
8783
+ const packageRoot = configuredPackageRootFromStatus(liveStatus);
8784
+ if (!packageRoot)
8785
+ return reject(
8786
+ "Receipt-validated configured Pi package root is unavailable."
8787
+ );
8788
+ const packageSkills = (source.context.inspectPiPackageSkills ?? inspectPiPackageSkills)({ packageRoot });
8789
+ if (!packageSkills.success)
8790
+ return reject(
8791
+ packageSkills.error ?? "Pi package-declared skills are unavailable."
8792
+ );
8793
+ const changed = [];
8794
+ try {
8795
+ for (const item of fresh.items.filter(
8796
+ (item2) => item2.kind === "mcp" && item2.content !== void 0
8797
+ ))
8798
+ if (writePiManagedText(item.target, item.content ?? ""))
8799
+ changed.push(item.target);
8800
+ } catch (error) {
8801
+ return reject(
8802
+ error instanceof Error ? error.message : String(error),
8803
+ fileTargets(changed)
8804
+ );
8805
+ }
8806
+ const resources = syncPiSpecialists({
8807
+ packageRoot,
8808
+ piRoot: fresh.paths.piRoot,
8809
+ projectRoots: fresh.paths.projectAgentRoots
8810
+ });
8811
+ changed.push(...resources.changed);
8812
+ if (!resources.success)
8813
+ return reject(
8814
+ resources.error ?? "Pi specialist sync failed.",
8815
+ fileTargets(changed)
8816
+ );
8817
+ const migration = migrateLegacyPiResources({
8818
+ packageRoot,
8819
+ piRoot: fresh.paths.piRoot
8820
+ });
8821
+ changed.push(...migration.changed);
8822
+ if (!migration.success)
8823
+ return reject(
8824
+ migration.error ?? "Pi legacy migration failed.",
8825
+ fileTargets(changed)
8826
+ );
8827
+ return {
8828
+ harness: "pi",
8829
+ action: plan.action,
8830
+ applied: true,
8831
+ summary: "Applied attributable Pi managed-surface sync without changing packages, external skills, provider state, or ledger.",
8832
+ changedTargets: fileTargets(changed),
8833
+ diagnosticTargets: packageSkills.skills.map((skill) => ({
8834
+ kind: "skill",
8835
+ path: join12(skill.destinationPath, "SKILL.md"),
8836
+ label: `Pi package-declared skill: ${skill.name}`,
8837
+ state: "installed"
8838
+ })),
8839
+ backups: [],
8840
+ warnings: [],
8841
+ disclaimers: disclaimers()
8842
+ };
8843
+ }
8844
+ if (plan.action === "install" || plan.action === "update") {
8845
+ const fresh = (source.context.buildPiSetupPlan ?? buildPiSetupPlan)({
8846
+ ...source.setup.options,
8847
+ dryRun: false,
8848
+ commandExecutor: source.context.piCommandExecutor
8849
+ });
8850
+ const applied = (source.context.applyPiSetup ?? applyPiSetup)(fresh);
8851
+ const setupTargets = fileTargets(applied.changed);
8852
+ if (!applied.success)
8853
+ return reject(
8854
+ applied.error ?? "Pi package or managed-surface setup failed.",
8855
+ setupTargets
8856
+ );
8857
+ if (!applied.configuredPackageRoot)
8858
+ return reject(
8859
+ "Verified configured Pi package root is unavailable.",
8860
+ setupTargets
8861
+ );
8862
+ const packageSkills = (source.context.inspectPiPackageSkills ?? inspectPiPackageSkills)({ packageRoot: applied.configuredPackageRoot });
8863
+ if (!packageSkills.success)
8864
+ return reject(
8865
+ packageSkills.error ?? "Pi package-declared skills are unavailable.",
8866
+ setupTargets
8867
+ );
8868
+ const installSkill = source.context.installRequiredSkill ?? installRequiredSkill;
8869
+ const skillTargets = [];
8870
+ for (const skill of REQUIRED_SKILLS) {
8871
+ const installed = installSkill(skill, "pi", {
8872
+ homeDir: source.context.homeDir
8873
+ });
8874
+ if (installed.status === "failed")
8875
+ return reject(`Failed to install required Pi skill: ${skill.name}.`, [
8876
+ ...setupTargets,
8877
+ ...skillTargets
8878
+ ]);
8879
+ skillTargets.push({
8880
+ kind: "skill",
8881
+ path: installed.skillPath,
8882
+ label: `Pi required skill: ${skill.name}`,
8883
+ state: "installed"
8884
+ });
8885
+ }
8886
+ if (!source.version)
8887
+ return reject("Executing thoth-agents package version is unavailable.");
8888
+ const finalize = source.context.finalizeHarnessInstall ?? finalizeHarnessInstall;
8889
+ const completion = finalize({
8890
+ harness: "pi",
8891
+ version: source.version,
8892
+ dryRun: false,
8893
+ cwd: source.context.cwd,
8894
+ runThothMemSetup: source.context.runThothMemSetup,
8895
+ ledgerOptions: source.context.installLedgerOptions ?? {
8896
+ env: source.context.env,
8897
+ homeDir: source.context.homeDir
8898
+ }
8899
+ });
8900
+ if (!completion.success)
8901
+ return reject(
8902
+ completion.error ?? "Pi provider setup or ledger commit failed.",
8903
+ [...setupTargets, ...skillTargets]
8904
+ );
8905
+ return {
8906
+ harness: "pi",
8907
+ action: plan.action,
8908
+ applied: true,
8909
+ summary: `Applied complete ordered Pi ${plan.action} including provider evidence and final ledger commit.`,
8910
+ changedTargets: [
8911
+ ...setupTargets,
8912
+ ...skillTargets,
8913
+ {
8914
+ kind: "file",
8915
+ path: completion.ledger.path,
8916
+ label: "CLI-managed install version",
8917
+ state: "installed"
8918
+ }
8919
+ ],
8920
+ diagnosticTargets: packageSkills.skills.map((skill) => ({
8921
+ kind: "skill",
8922
+ path: join12(skill.destinationPath, "SKILL.md"),
8923
+ label: `Pi package-declared skill: ${skill.name}`,
8924
+ state: "installed"
8925
+ })),
8926
+ backups: [],
8927
+ warnings: [],
8928
+ disclaimers: disclaimers()
8929
+ };
8930
+ }
8931
+ return reject(`Unsupported Pi apply action: ${plan.action}.`);
8932
+ }
8933
+
8934
+ // src/cli/operations/index.ts
8935
+ var OPERATION_HARNESSES = {
8936
+ opencode: opencodeOperationAdapter,
8937
+ codex: codexOperationAdapter,
8938
+ claude: claudeCodeOperationAdapter,
8939
+ pi: piOperationAdapter
8940
+ };
8941
+ var SUPPORTED_OPERATION_HARNESSES = Object.keys(
8942
+ OPERATION_HARNESSES
8943
+ );
8944
+ function listOperationHarnesses() {
8945
+ return SUPPORTED_OPERATION_HARNESSES.map(
8946
+ (harness) => OPERATION_HARNESSES[harness]
8947
+ );
8948
+ }
8949
+ function getOperationHarness(harness) {
8950
+ return OPERATION_HARNESSES[harness];
8951
+ }
8952
+
8953
+ // src/cli/tui/model-catalog.ts
8954
+ import { execFileSync } from "child_process";
8955
+
8956
+ // src/cli/model-catalog/cache.ts
8957
+ import {
8958
+ existsSync as existsSync16,
8959
+ mkdirSync as mkdirSync8,
8960
+ readFileSync as readFileSync13,
8961
+ renameSync as renameSync7,
8962
+ unlinkSync,
8963
+ writeFileSync as writeFileSync8
8964
+ } from "fs";
8965
+ import { homedir as homedir7 } from "os";
8966
+ import { dirname as dirname10, posix, win32 } from "path";
8967
+ import { z as z2 } from "zod";
8968
+ var modelCatalogSourceSchema = z2.enum(["remote", "lkg", "manual"]);
8969
+ var cachedModelOptionSchema = z2.object({
8970
+ id: z2.string().min(1),
8971
+ catalogId: z2.string().min(1).optional(),
8972
+ label: z2.string().min(1),
8973
+ provider: z2.string().min(1),
8974
+ efforts: z2.array(z2.string().min(1)),
8975
+ source: modelCatalogSourceSchema
8976
+ }).passthrough();
8977
+ var modelsDevCacheSchema = z2.object({
8978
+ version: z2.literal(1),
8979
+ etag: z2.string().optional(),
8980
+ fetchedAt: z2.string().datetime(),
8981
+ catalog: z2.object({ models: z2.array(cachedModelOptionSchema) }).passthrough()
8982
+ }).passthrough();
8983
+ function resolveModelsDevCachePath(options = {}) {
8984
+ const platform = options.platform ?? process.platform;
8985
+ const env = options.env ?? process.env;
8986
+ const home = options.homeDir ?? homedir7();
8987
+ if (platform === "win32") {
8988
+ const path = win32;
8989
+ const base2 = env.LOCALAPPDATA ?? env.APPDATA ?? path.join(home, "AppData", "Local");
8990
+ return path.join(base2, "thoth-agents", "models-dev-v1.json");
8991
+ }
8992
+ const base = env.XDG_CACHE_HOME ?? posix.join(home, ".cache");
8993
+ return posix.join(base, "thoth-agents", "models-dev-v1.json");
8994
+ }
8995
+ function readModelsDevCache(cachePath) {
8996
+ if (!existsSync16(cachePath)) return void 0;
8997
+ try {
8998
+ const input = JSON.parse(readFileSync13(cachePath, "utf8"));
8999
+ const parsed = modelsDevCacheSchema.safeParse(input);
9000
+ return parsed.success ? parsed.data : void 0;
9001
+ } catch {
9002
+ return void 0;
9003
+ }
9004
+ }
9005
+ function writeModelsDevCacheAtomic(cachePath, cache) {
9006
+ const validated = modelsDevCacheSchema.safeParse(cache);
9007
+ if (!validated.success) {
9008
+ throw new Error(`Invalid models.dev cache: ${validated.error.message}`);
9009
+ }
9010
+ mkdirSync8(dirname10(cachePath), { recursive: true });
9011
+ const temporaryPath = `${cachePath}.${process.pid}.${Date.now()}.tmp`;
9012
+ try {
9013
+ writeFileSync8(
9014
+ temporaryPath,
9015
+ `${JSON.stringify(validated.data, null, 2)}
9016
+ `
9017
+ );
9018
+ renameSync7(temporaryPath, cachePath);
9019
+ } catch (error) {
9020
+ if (existsSync16(temporaryPath)) unlinkSync(temporaryPath);
9021
+ throw error;
9022
+ }
9023
+ }
9024
+
9025
+ // src/cli/model-catalog/models-dev.ts
9026
+ import { z as z3 } from "zod";
9027
+ var modelsDevModelSchema = z3.object({
9028
+ id: z3.string().min(1).optional(),
9029
+ name: z3.string().min(1).optional(),
9030
+ reasoning: z3.boolean().optional(),
9031
+ reasoning_options: z3.array(z3.unknown()).optional()
9032
+ }).passthrough();
9033
+ var modelsDevProviderSchema = z3.object({
9034
+ models: z3.record(z3.string(), modelsDevModelSchema)
9035
+ }).passthrough();
9036
+ var modelsDevCatalogSchema = z3.record(
9037
+ z3.string(),
9038
+ modelsDevProviderSchema
9039
+ );
9040
+ var effortOptionSchema = z3.object({
9041
+ type: z3.literal("effort"),
9042
+ values: z3.array(z3.string().min(1))
9043
+ }).passthrough();
9044
+ function normalizedCatalogId(provider, modelKey, declaredId) {
9045
+ const candidate = (declaredId ?? modelKey).trim().toLowerCase();
9046
+ return candidate.includes("/") ? candidate : `${provider}/${candidate}`;
9047
+ }
9048
+ function runtimeModelId(provider, catalogId) {
9049
+ const prefix = `${provider}/`;
9050
+ return catalogId.startsWith(prefix) ? catalogId.slice(prefix.length) : catalogId;
9051
+ }
9052
+ function effortValues(options) {
9053
+ const values = /* @__PURE__ */ new Set();
9054
+ for (const option of options ?? []) {
9055
+ const parsed = effortOptionSchema.safeParse(option);
9056
+ if (!parsed.success) continue;
9057
+ for (const value of parsed.data.values) {
9058
+ const normalized = value.trim();
9059
+ if (normalized) values.add(normalized);
6510
9060
  }
6511
9061
  }
6512
9062
  return [...values];
@@ -6660,6 +9210,7 @@ var CODEX_DOCUMENTED_EFFORTS2 = /* @__PURE__ */ new Set([
6660
9210
  "ultra"
6661
9211
  ]);
6662
9212
  var CLAUDE_CODE_EFFORTS2 = ["low", "medium", "high", "xhigh", "max"];
9213
+ var PI_EFFORTS2 = new Set(PI_EFFORT_VALUES);
6663
9214
  function parseOpenCodeModels(output) {
6664
9215
  const seen = /* @__PURE__ */ new Set();
6665
9216
  const options = [];
@@ -6695,6 +9246,9 @@ function writableEfforts(harness, option) {
6695
9246
  (effort) => CLAUDE_CODE_EFFORTS2.includes(effort)
6696
9247
  );
6697
9248
  }
9249
+ if (harness === "pi") {
9250
+ return option.efforts.filter((effort) => PI_EFFORTS2.has(effort));
9251
+ }
6698
9252
  return option.efforts.filter(
6699
9253
  (effort) => resolveOpenCodeEffort({
6700
9254
  model: option.id,
@@ -6769,6 +9323,12 @@ async function getModelOptions(harness) {
6769
9323
  efforts: writableEfforts(harness, option)
6770
9324
  }));
6771
9325
  }
9326
+ if (harness === "pi") {
9327
+ return loaded.models.map((option) => ({
9328
+ ...option,
9329
+ efforts: writableEfforts(harness, option)
9330
+ }));
9331
+ }
6772
9332
  const catalogById = new Map(
6773
9333
  loaded.models.map((option) => [option.catalogId ?? option.id, option])
6774
9334
  );
@@ -6780,7 +9340,7 @@ async function getModelOptions(harness) {
6780
9340
  }
6781
9341
 
6782
9342
  // src/cli/tui/operations.ts
6783
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
9343
+ import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
6784
9344
  var context = { cwd: process.cwd() };
6785
9345
  var codexContext = { cwd: process.cwd() };
6786
9346
  var claudeCodeContext = {
@@ -6822,7 +9382,7 @@ function getCodexModelRoles(source = codexContext) {
6822
9382
  const item = plan.items.find(
6823
9383
  (candidate) => candidate.action === "write-role-toml" && candidate.role === role
6824
9384
  );
6825
- const content = item && existsSync12(item.targetPath) ? readFileSync8(item.targetPath, "utf8") : item?.content;
9385
+ const content = item && existsSync17(item.targetPath) ? readFileSync14(item.targetPath, "utf8") : item?.content;
6826
9386
  const effort = content ? parseRoleTomlEffort(content) : void 0;
6827
9387
  return {
6828
9388
  role,
@@ -6837,6 +9397,9 @@ function getCodexModelRoles(source = codexContext) {
6837
9397
  function getClaudeCodeModelRoles(_source = claudeCodeContext) {
6838
9398
  return defaultClaudeCodeModelRoles();
6839
9399
  }
9400
+ function getPiModelRoles() {
9401
+ return defaultPiModelRoles(context);
9402
+ }
6840
9403
  function readRoleField(config, role, field) {
6841
9404
  if (!config || typeof config !== "object" || Array.isArray(config)) {
6842
9405
  return void 0;
@@ -6879,6 +9442,9 @@ function buildTuiModelPlan(harness, roles) {
6879
9442
  claudeCodeContext
6880
9443
  );
6881
9444
  }
9445
+ if (harness === "pi") {
9446
+ return buildPiModelPlan({ harness, dryRun: true, roles }, context);
9447
+ }
6882
9448
  return buildCodexModelPlan({ harness, dryRun: true, roles }, codexContext);
6883
9449
  }
6884
9450
  var defaultTuiOperations = {
@@ -6887,6 +9453,7 @@ var defaultTuiOperations = {
6887
9453
  if (harness === "claude") {
6888
9454
  return getClaudeCodeStatus(claudeCodeContext, evidence);
6889
9455
  }
9456
+ if (harness === "pi") return getPiStatus(context, evidence);
6890
9457
  return getCodexStatus(codexContext, evidence);
6891
9458
  },
6892
9459
  modelRoles(harness) {
@@ -6894,6 +9461,7 @@ var defaultTuiOperations = {
6894
9461
  if (harness === "claude") {
6895
9462
  return getClaudeCodeModelRoles(claudeCodeContext);
6896
9463
  }
9464
+ if (harness === "pi") return getPiModelRoles();
6897
9465
  return getCodexModelRoles(codexContext);
6898
9466
  },
6899
9467
  modelOptions(harness) {
@@ -6919,6 +9487,12 @@ var defaultTuiOperations = {
6919
9487
  getClaudeCodeModelRoles(claudeCodeContext)
6920
9488
  );
6921
9489
  }
9490
+ if (harness === "pi") {
9491
+ if (action === "install") return buildPiInstallPlan(context);
9492
+ if (action === "update") return buildPiUpdatePlan(context);
9493
+ if (action === "sync") return buildPiSyncPlan(context);
9494
+ return buildTuiModelPlan(harness, getPiModelRoles());
9495
+ }
6922
9496
  if (action === "install") return buildCodexInstallPlan(codexContext);
6923
9497
  if (action === "update") return buildCodexUpdatePlan(codexContext);
6924
9498
  if (action === "sync") return buildCodexSyncPlan(codexContext);
@@ -6930,6 +9504,7 @@ var defaultTuiOperations = {
6930
9504
  apply(plan) {
6931
9505
  if (plan.harness === "opencode") return applyOpenCodePlan(plan);
6932
9506
  if (plan.harness === "claude") return applyClaudeCodePlan(plan);
9507
+ if (plan.harness === "pi") return applyPiPlan(plan);
6933
9508
  return applyCodexPlan(plan);
6934
9509
  }
6935
9510
  };
@@ -6948,8 +9523,13 @@ export {
6948
9523
  buildCodexPluginSetupPlan,
6949
9524
  applyCodexPluginSetup,
6950
9525
  formatCodexPluginSetupPlan,
9526
+ recordHarnessInstallCompletion,
6951
9527
  finalizeHarnessInstall,
6952
9528
  syncOpenCodeOwnedSkills,
9529
+ inspectPiPackageSkills,
9530
+ buildPiSetupPlan,
9531
+ formatPiSetupPlan,
9532
+ applyPiSetup,
6953
9533
  REQUIRED_SKILLS,
6954
9534
  getRequiredSkillInstallCommand,
6955
9535
  installRequiredSkill,
@@ -6969,6 +9549,12 @@ export {
6969
9549
  buildOpenCodeSyncPlan,
6970
9550
  buildOpenCodeModelPlan,
6971
9551
  applyOpenCodePlan,
9552
+ getPiStatus,
9553
+ buildPiUpdatePlan,
9554
+ buildPiSyncPlan,
9555
+ defaultPiModelRoles,
9556
+ buildPiModelPlan,
9557
+ applyPiPlan,
6972
9558
  SUPPORTED_OPERATION_HARNESSES,
6973
9559
  listOperationHarnesses,
6974
9560
  getOperationHarness,
@@ -6976,6 +9562,7 @@ export {
6976
9562
  getModelOptions,
6977
9563
  getCodexModelRoles,
6978
9564
  getClaudeCodeModelRoles,
9565
+ getPiModelRoles,
6979
9566
  getOpenCodeModelRoles,
6980
9567
  defaultTuiOperations
6981
9568
  };