thoth-agents 0.3.12 → 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 (50) hide show
  1. package/README.md +181 -474
  2. package/dist/agents/prompt-dialects.d.ts +4 -1
  3. package/dist/agents/prompt-sections.d.ts +2 -1
  4. package/dist/{chunk-5UBJB5QO.js → chunk-5EBXGMOK.js} +1 -1
  5. package/dist/chunk-62L3ORXN.js +30 -0
  6. package/dist/{chunk-DX4U3AD3.js → chunk-Q6SDM4KP.js} +114 -57
  7. package/dist/chunk-SECDI3TL.js +312 -0
  8. package/dist/{chunk-VPVBYBPU.js → chunk-WJ2YMM2E.js} +2298 -188
  9. package/dist/cli/index.js +184 -13
  10. package/dist/cli/install-completion.d.ts +13 -0
  11. package/dist/cli/install-ledger.d.ts +1 -1
  12. package/dist/cli/install.d.ts +11 -1
  13. package/dist/cli/operations/pi.d.ts +43 -0
  14. package/dist/cli/owned-skills.d.ts +17 -0
  15. package/dist/cli/pi-effort.d.ts +10 -0
  16. package/dist/cli/pi-install.d.ts +131 -0
  17. package/dist/cli/pi-migration.d.ts +12 -0
  18. package/dist/cli/pi-native-probe.d.ts +34 -0
  19. package/dist/cli/pi-package-receipt.d.ts +59 -0
  20. package/dist/cli/pi-paths.d.ts +20 -0
  21. package/dist/cli/pi-resources.d.ts +15 -0
  22. package/dist/cli/skills.d.ts +1 -1
  23. package/dist/cli/tui/index.js +12 -4
  24. package/dist/cli/tui/operations.d.ts +1 -0
  25. package/dist/cli/types.d.ts +4 -2
  26. package/dist/harness/adapters/pi.d.ts +8 -0
  27. package/dist/harness/pi-specialists.d.ts +6 -0
  28. package/dist/harness/types.d.ts +2 -2
  29. package/dist/harness/writers/pi-agent.d.ts +16 -0
  30. package/dist/index.js +6 -4
  31. package/dist/pi.d.ts +11 -0
  32. package/dist/pi.js +46 -0
  33. package/package.json +14 -3
  34. package/pi/.thoth-agents-assets.json +12 -0
  35. package/pi/agents/thoth-deep.md +81 -0
  36. package/pi/agents/thoth-designer.md +81 -0
  37. package/pi/agents/thoth-explorer.md +81 -0
  38. package/pi/agents/thoth-librarian.md +83 -0
  39. package/pi/agents/thoth-oracle.md +83 -0
  40. package/pi/agents/thoth-quick.md +81 -0
  41. package/plugin/.claude-plugin/plugin.json +1 -1
  42. package/plugin/.codex-plugin/plugin.json +1 -1
  43. package/plugin/skills/thoth-archive/SKILL.md +1 -1
  44. package/plugin/skills/thoth-constitution/SKILL.md +1 -1
  45. package/plugin/skills/thoth-init/SKILL.md +1 -1
  46. package/plugin/skills/thoth-sdd/SKILL.md +1 -1
  47. package/skills/thoth-archive/SKILL.md +1 -1
  48. package/skills/thoth-constitution/SKILL.md +1 -1
  49. package/skills/thoth-init/SKILL.md +1 -1
  50. package/skills/thoth-sdd/SKILL.md +1 -1
@@ -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-DX4U3AD3.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 = [];
@@ -953,9 +932,9 @@ function stripProviderMcpValues(surfaceId, values) {
953
932
  const { thoth_mem: _provider, ...mcpServers } = servers;
954
933
  return { ...values, mcp_servers: mcpServers };
955
934
  }
956
- function fieldOrder(fields, keys) {
935
+ function fieldOrder(fields, keys2) {
957
936
  const orderedRoots = fields.map((field) => field.split(".")[0]);
958
- return [...keys].sort((left, right) => {
937
+ return [...keys2].sort((left, right) => {
959
938
  const leftIndex = orderedRoots.indexOf(left);
960
939
  const rightIndex = orderedRoots.indexOf(right);
961
940
  if (leftIndex !== -1 || rightIndex !== -1) {
@@ -2482,11 +2461,12 @@ var INSTALL_LEDGER_SCHEMA_VERSION = 1;
2482
2461
  var INSTALL_HARNESSES = /* @__PURE__ */ new Set([
2483
2462
  "opencode",
2484
2463
  "codex",
2485
- "claude"
2464
+ "claude",
2465
+ "pi"
2486
2466
  ]);
2487
- function hasOnlyKeys(value, keys) {
2467
+ function hasOnlyKeys(value, keys2) {
2488
2468
  const actual = Object.keys(value);
2489
- return actual.length === keys.length && actual.every((key) => keys.includes(key));
2469
+ return actual.length === keys2.length && actual.every((key) => keys2.includes(key));
2490
2470
  }
2491
2471
  function parseInstallLedger(serialized) {
2492
2472
  let parsed;
@@ -2678,8 +2658,6 @@ function getThothMemSetupCommand(harness, dryRun, options = {}) {
2678
2658
  "thoth-mem@latest",
2679
2659
  "setup",
2680
2660
  harness,
2681
- "--scope",
2682
- "global",
2683
2661
  ...dryRun ? ["--plan"] : [],
2684
2662
  "--json"
2685
2663
  ],
@@ -2757,26 +2735,11 @@ function runThothMemSetup(options) {
2757
2735
  function isConsistentProviderSuccess(result, harness) {
2758
2736
  return result.success && result.evidenceValid && result.status === "complete" && result.exitCode === 0 && result.harness === harness;
2759
2737
  }
2760
- function finalizeHarnessInstall(options) {
2738
+ function recordHarnessInstallCompletion(options) {
2761
2739
  const ledgerPath = getInstallLedgerPath(options.ledgerOptions);
2762
- const setup = options.runThothMemSetup ?? runThothMemSetup;
2763
- const provider = setup({
2764
- harness: options.harness,
2765
- dryRun: options.dryRun,
2766
- cwd: options.cwd
2767
- });
2768
- if (!isConsistentProviderSuccess(provider, options.harness)) {
2769
- return {
2770
- success: false,
2771
- provider,
2772
- ledger: { status: "not-attempted", path: ledgerPath },
2773
- error: provider.error ?? "thoth-mem setup did not return consistent complete evidence."
2774
- };
2775
- }
2776
2740
  if (options.dryRun) {
2777
2741
  return {
2778
2742
  success: true,
2779
- provider,
2780
2743
  ledger: { status: "planned", path: ledgerPath }
2781
2744
  };
2782
2745
  }
@@ -2789,7 +2752,6 @@ function finalizeHarnessInstall(options) {
2789
2752
  if (!ledger.success) {
2790
2753
  return {
2791
2754
  success: false,
2792
- provider,
2793
2755
  ledger: {
2794
2756
  status: "failed",
2795
2757
  path: ledger.path,
@@ -2800,7 +2762,6 @@ function finalizeHarnessInstall(options) {
2800
2762
  }
2801
2763
  return {
2802
2764
  success: true,
2803
- provider,
2804
2765
  ledger: {
2805
2766
  status: "recorded",
2806
2767
  path: ledger.path,
@@ -2809,6 +2770,34 @@ function finalizeHarnessInstall(options) {
2809
2770
  }
2810
2771
  };
2811
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
+ }
2812
2801
 
2813
2802
  // src/cli/skills.ts
2814
2803
  import { spawnSync as spawnSync3 } from "child_process";
@@ -2818,7 +2807,8 @@ import { join as join3 } from "path";
2818
2807
  var SKILLS_CLI_AGENT = {
2819
2808
  opencode: "opencode",
2820
2809
  codex: "codex",
2821
- claude: "claude-code"
2810
+ claude: "claude-code",
2811
+ pi: "pi"
2822
2812
  };
2823
2813
  var GLOBAL_SKILL_ROOTS = {
2824
2814
  opencode: [
@@ -2826,7 +2816,8 @@ var GLOBAL_SKILL_ROOTS = {
2826
2816
  [".agents", "skills"]
2827
2817
  ],
2828
2818
  codex: [[".agents", "skills"]],
2829
- claude: [[".claude", "skills"]]
2819
+ claude: [[".claude", "skills"]],
2820
+ pi: [[".pi", "agent", "skills"]]
2830
2821
  };
2831
2822
  var REQUIRED_SKILLS = [
2832
2823
  {
@@ -2883,7 +2874,8 @@ function getRequiredSkillInstallCommand(skill, harness, options = {}) {
2883
2874
  "--global",
2884
2875
  "--agent",
2885
2876
  SKILLS_CLI_AGENT[harness],
2886
- "--yes"
2877
+ "--yes",
2878
+ ...harness === "pi" ? ["--copy"] : []
2887
2879
  ],
2888
2880
  options
2889
2881
  );
@@ -5124,8 +5116,10 @@ function resolveOpenCodeEffort(input) {
5124
5116
  import {
5125
5117
  cpSync,
5126
5118
  existsSync as existsSync9,
5119
+ lstatSync as lstatSync2,
5127
5120
  mkdirSync as mkdirSync4,
5128
5121
  mkdtempSync,
5122
+ readFileSync as readFileSync8,
5129
5123
  renameSync as renameSync3,
5130
5124
  rmSync as rmSync3,
5131
5125
  statSync
@@ -5144,6 +5138,68 @@ var THOTH_OWNED_SKILL_NAMES = [
5144
5138
  ];
5145
5139
 
5146
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
+ }
5147
5203
  function resolveOwnedSkillPackageRoot(moduleUrl) {
5148
5204
  const packageRoot = findPackageRoot(dirname5(fileURLToPath4(moduleUrl)));
5149
5205
  if (!packageRoot) {
@@ -5165,6 +5221,14 @@ function getOpenCodeOwnedSkillEntries(options = {}) {
5165
5221
  destinationPath: join5(homeDir, ".config", "opencode", "skills", name)
5166
5222
  }));
5167
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)
5230
+ }));
5231
+ }
5168
5232
  function validateCanonicalSkill(entry) {
5169
5233
  if (!existsSync9(entry.sourcePath) || !statSync(entry.sourcePath).isDirectory()) {
5170
5234
  throw new Error(
@@ -5235,6 +5299,37 @@ function syncOpenCodeOwnedSkills(options = {}) {
5235
5299
  };
5236
5300
  }
5237
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
+ }
5238
5333
 
5239
5334
  // src/cli/operations/opencode.ts
5240
5335
  var PACKAGE_NAME = "thoth-agents";
@@ -5286,7 +5381,7 @@ function canonicalData(value, active) {
5286
5381
  if (ownKeys.some((key) => typeof key === "symbol")) {
5287
5382
  throw new NonCanonicalDataError("Symbol keys are not canonical.");
5288
5383
  }
5289
- const keys = ownKeys;
5384
+ const keys2 = ownKeys;
5290
5385
  active.add(value);
5291
5386
  try {
5292
5387
  if (array) {
@@ -5298,7 +5393,7 @@ function canonicalData(value, active) {
5298
5393
  if (!Number.isSafeInteger(length) || length < 0) {
5299
5394
  throw new NonCanonicalDataError("Array length is invalid.");
5300
5395
  }
5301
- const indexKeys = keys.filter((key) => key !== "length");
5396
+ const indexKeys = keys2.filter((key) => key !== "length");
5302
5397
  if (indexKeys.length !== length || indexKeys.some(
5303
5398
  (key) => !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= length || String(Number(key)) !== key
5304
5399
  )) {
@@ -5320,7 +5415,7 @@ function canonicalData(value, active) {
5320
5415
  return `[${parts2.join(",")}]`;
5321
5416
  }
5322
5417
  const parts = [];
5323
- for (const key of keys.sort()) {
5418
+ for (const key of keys2.sort()) {
5324
5419
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
5325
5420
  if (!descriptor || !("value" in descriptor)) {
5326
5421
  throw new NonCanonicalDataError("Accessors are not canonical.");
@@ -6859,143 +6954,2121 @@ function applyOpenCodePlan(plan) {
6859
6954
  };
6860
6955
  }
6861
6956
 
6862
- // src/cli/operations/index.ts
6863
- var OPERATION_HARNESSES = {
6864
- opencode: opencodeOperationAdapter,
6865
- codex: codexOperationAdapter,
6866
- claude: claudeCodeOperationAdapter
6867
- };
6868
- var SUPPORTED_OPERATION_HARNESSES = Object.keys(
6869
- OPERATION_HARNESSES
6870
- );
6871
- function listOperationHarnesses() {
6872
- return SUPPORTED_OPERATION_HARNESSES.map(
6873
- (harness) => OPERATION_HARNESSES[harness]
6874
- );
6875
- }
6876
- function getOperationHarness(harness) {
6877
- 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 };
6878
6991
  }
6879
6992
 
6880
- // src/cli/tui/model-catalog.ts
6881
- 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";
6882
7009
 
6883
- // src/cli/model-catalog/cache.ts
7010
+ // src/cli/pi-migration.ts
6884
7011
  import {
7012
+ copyFileSync as copyFileSync4,
6885
7013
  existsSync as existsSync11,
6886
7014
  mkdirSync as mkdirSync5,
6887
- readFileSync as readFileSync8,
7015
+ readdirSync as readdirSync2,
7016
+ readFileSync as readFileSync9,
6888
7017
  renameSync as renameSync4,
6889
- unlinkSync,
7018
+ statSync as statSync2,
6890
7019
  writeFileSync as writeFileSync4
6891
7020
  } from "fs";
6892
- import { homedir as homedir5 } from "os";
6893
- import { dirname as dirname6, posix, win32 } from "path";
6894
- import { z as z2 } from "zod";
6895
- var modelCatalogSourceSchema = z2.enum(["remote", "lkg", "manual"]);
6896
- var cachedModelOptionSchema = z2.object({
6897
- id: z2.string().min(1),
6898
- catalogId: z2.string().min(1).optional(),
6899
- label: z2.string().min(1),
6900
- provider: z2.string().min(1),
6901
- efforts: z2.array(z2.string().min(1)),
6902
- source: modelCatalogSourceSchema
6903
- }).passthrough();
6904
- var modelsDevCacheSchema = z2.object({
6905
- version: z2.literal(1),
6906
- etag: z2.string().optional(),
6907
- fetchedAt: z2.string().datetime(),
6908
- catalog: z2.object({ models: z2.array(cachedModelOptionSchema) }).passthrough()
6909
- }).passthrough();
6910
- function resolveModelsDevCachePath(options = {}) {
6911
- const platform = options.platform ?? process.platform;
6912
- const env = options.env ?? process.env;
6913
- const home = options.homeDir ?? homedir5();
6914
- if (platform === "win32") {
6915
- const path = win32;
6916
- const base2 = env.LOCALAPPDATA ?? env.APPDATA ?? path.join(home, "AppData", "Local");
6917
- return path.join(base2, "thoth-agents", "models-dev-v1.json");
6918
- }
6919
- const base = env.XDG_CACHE_HOME ?? posix.join(home, ".cache");
6920
- 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
+ );
6921
7035
  }
6922
- function readModelsDevCache(cachePath) {
6923
- if (!existsSync11(cachePath)) return void 0;
6924
- try {
6925
- const input = JSON.parse(readFileSync8(cachePath, "utf8"));
6926
- const parsed = modelsDevCacheSchema.safeParse(input);
6927
- return parsed.success ? parsed.data : void 0;
6928
- } catch {
6929
- return void 0;
6930
- }
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/, "")}`;
6931
7044
  }
6932
- function writeModelsDevCacheAtomic(cachePath, cache) {
6933
- const validated = modelsDevCacheSchema.safeParse(cache);
6934
- if (!validated.success) {
6935
- 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 });
6936
7069
  }
6937
- mkdirSync5(dirname6(cachePath), { recursive: true });
6938
- 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
+ };
6939
7079
  try {
6940
- writeFileSync4(
6941
- temporaryPath,
6942
- `${JSON.stringify(validated.data, null, 2)}
6943
- `
6944
- );
6945
- 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 };
6946
7093
  } catch (error) {
6947
- if (existsSync11(temporaryPath)) unlinkSync(temporaryPath);
6948
- 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
+ };
6949
7108
  }
6950
7109
  }
6951
7110
 
6952
- // src/cli/model-catalog/models-dev.ts
6953
- import { z as z3 } from "zod";
6954
- var modelsDevModelSchema = z3.object({
6955
- id: z3.string().min(1).optional(),
6956
- name: z3.string().min(1).optional(),
6957
- reasoning: z3.boolean().optional(),
6958
- reasoning_options: z3.array(z3.unknown()).optional()
6959
- }).passthrough();
6960
- var modelsDevProviderSchema = z3.object({
6961
- models: z3.record(z3.string(), modelsDevModelSchema)
6962
- }).passthrough();
6963
- var modelsDevCatalogSchema = z3.record(
6964
- z3.string(),
6965
- modelsDevProviderSchema
6966
- );
6967
- var effortOptionSchema = z3.object({
6968
- type: z3.literal("effort"),
6969
- values: z3.array(z3.string().min(1))
6970
- }).passthrough();
6971
- function normalizedCatalogId(provider, modelKey, declaredId) {
6972
- const candidate = (declaredId ?? modelKey).trim().toLowerCase();
6973
- return candidate.includes("/") ? candidate : `${provider}/${candidate}`;
6974
- }
6975
- function runtimeModelId(provider, catalogId) {
6976
- const prefix = `${provider}/`;
6977
- return catalogId.startsWith(prefix) ? catalogId.slice(prefix.length) : catalogId;
6978
- }
6979
- function effortValues(options) {
6980
- const values = /* @__PURE__ */ new Set();
6981
- for (const option of options ?? []) {
6982
- const parsed = effortOptionSchema.safeParse(option);
6983
- if (!parsed.success) continue;
6984
- for (const value of parsed.data.values) {
6985
- const normalized = value.trim();
6986
- 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];
6987
7133
  }
6988
7134
  }
6989
- return [...values];
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
+ };
6990
7146
  }
6991
- function normalizeModelsDevCatalog(input) {
6992
- const parsed = modelsDevCatalogSchema.safeParse(input);
6993
- if (!parsed.success) {
6994
- return {
6995
- ok: false,
6996
- issues: parsed.error.issues.map(
6997
- (issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`
6998
- )
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);
9060
+ }
9061
+ }
9062
+ return [...values];
9063
+ }
9064
+ function normalizeModelsDevCatalog(input) {
9065
+ const parsed = modelsDevCatalogSchema.safeParse(input);
9066
+ if (!parsed.success) {
9067
+ return {
9068
+ ok: false,
9069
+ issues: parsed.error.issues.map(
9070
+ (issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`
9071
+ )
6999
9072
  };
7000
9073
  }
7001
9074
  const models = [];
@@ -7137,6 +9210,7 @@ var CODEX_DOCUMENTED_EFFORTS2 = /* @__PURE__ */ new Set([
7137
9210
  "ultra"
7138
9211
  ]);
7139
9212
  var CLAUDE_CODE_EFFORTS2 = ["low", "medium", "high", "xhigh", "max"];
9213
+ var PI_EFFORTS2 = new Set(PI_EFFORT_VALUES);
7140
9214
  function parseOpenCodeModels(output) {
7141
9215
  const seen = /* @__PURE__ */ new Set();
7142
9216
  const options = [];
@@ -7172,6 +9246,9 @@ function writableEfforts(harness, option) {
7172
9246
  (effort) => CLAUDE_CODE_EFFORTS2.includes(effort)
7173
9247
  );
7174
9248
  }
9249
+ if (harness === "pi") {
9250
+ return option.efforts.filter((effort) => PI_EFFORTS2.has(effort));
9251
+ }
7175
9252
  return option.efforts.filter(
7176
9253
  (effort) => resolveOpenCodeEffort({
7177
9254
  model: option.id,
@@ -7246,6 +9323,12 @@ async function getModelOptions(harness) {
7246
9323
  efforts: writableEfforts(harness, option)
7247
9324
  }));
7248
9325
  }
9326
+ if (harness === "pi") {
9327
+ return loaded.models.map((option) => ({
9328
+ ...option,
9329
+ efforts: writableEfforts(harness, option)
9330
+ }));
9331
+ }
7249
9332
  const catalogById = new Map(
7250
9333
  loaded.models.map((option) => [option.catalogId ?? option.id, option])
7251
9334
  );
@@ -7257,7 +9340,7 @@ async function getModelOptions(harness) {
7257
9340
  }
7258
9341
 
7259
9342
  // src/cli/tui/operations.ts
7260
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
9343
+ import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
7261
9344
  var context = { cwd: process.cwd() };
7262
9345
  var codexContext = { cwd: process.cwd() };
7263
9346
  var claudeCodeContext = {
@@ -7299,7 +9382,7 @@ function getCodexModelRoles(source = codexContext) {
7299
9382
  const item = plan.items.find(
7300
9383
  (candidate) => candidate.action === "write-role-toml" && candidate.role === role
7301
9384
  );
7302
- const content = item && existsSync12(item.targetPath) ? readFileSync9(item.targetPath, "utf8") : item?.content;
9385
+ const content = item && existsSync17(item.targetPath) ? readFileSync14(item.targetPath, "utf8") : item?.content;
7303
9386
  const effort = content ? parseRoleTomlEffort(content) : void 0;
7304
9387
  return {
7305
9388
  role,
@@ -7314,6 +9397,9 @@ function getCodexModelRoles(source = codexContext) {
7314
9397
  function getClaudeCodeModelRoles(_source = claudeCodeContext) {
7315
9398
  return defaultClaudeCodeModelRoles();
7316
9399
  }
9400
+ function getPiModelRoles() {
9401
+ return defaultPiModelRoles(context);
9402
+ }
7317
9403
  function readRoleField(config, role, field) {
7318
9404
  if (!config || typeof config !== "object" || Array.isArray(config)) {
7319
9405
  return void 0;
@@ -7356,6 +9442,9 @@ function buildTuiModelPlan(harness, roles) {
7356
9442
  claudeCodeContext
7357
9443
  );
7358
9444
  }
9445
+ if (harness === "pi") {
9446
+ return buildPiModelPlan({ harness, dryRun: true, roles }, context);
9447
+ }
7359
9448
  return buildCodexModelPlan({ harness, dryRun: true, roles }, codexContext);
7360
9449
  }
7361
9450
  var defaultTuiOperations = {
@@ -7364,6 +9453,7 @@ var defaultTuiOperations = {
7364
9453
  if (harness === "claude") {
7365
9454
  return getClaudeCodeStatus(claudeCodeContext, evidence);
7366
9455
  }
9456
+ if (harness === "pi") return getPiStatus(context, evidence);
7367
9457
  return getCodexStatus(codexContext, evidence);
7368
9458
  },
7369
9459
  modelRoles(harness) {
@@ -7371,6 +9461,7 @@ var defaultTuiOperations = {
7371
9461
  if (harness === "claude") {
7372
9462
  return getClaudeCodeModelRoles(claudeCodeContext);
7373
9463
  }
9464
+ if (harness === "pi") return getPiModelRoles();
7374
9465
  return getCodexModelRoles(codexContext);
7375
9466
  },
7376
9467
  modelOptions(harness) {
@@ -7396,6 +9487,12 @@ var defaultTuiOperations = {
7396
9487
  getClaudeCodeModelRoles(claudeCodeContext)
7397
9488
  );
7398
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
+ }
7399
9496
  if (action === "install") return buildCodexInstallPlan(codexContext);
7400
9497
  if (action === "update") return buildCodexUpdatePlan(codexContext);
7401
9498
  if (action === "sync") return buildCodexSyncPlan(codexContext);
@@ -7407,6 +9504,7 @@ var defaultTuiOperations = {
7407
9504
  apply(plan) {
7408
9505
  if (plan.harness === "opencode") return applyOpenCodePlan(plan);
7409
9506
  if (plan.harness === "claude") return applyClaudeCodePlan(plan);
9507
+ if (plan.harness === "pi") return applyPiPlan(plan);
7410
9508
  return applyCodexPlan(plan);
7411
9509
  }
7412
9510
  };
@@ -7425,8 +9523,13 @@ export {
7425
9523
  buildCodexPluginSetupPlan,
7426
9524
  applyCodexPluginSetup,
7427
9525
  formatCodexPluginSetupPlan,
9526
+ recordHarnessInstallCompletion,
7428
9527
  finalizeHarnessInstall,
7429
9528
  syncOpenCodeOwnedSkills,
9529
+ inspectPiPackageSkills,
9530
+ buildPiSetupPlan,
9531
+ formatPiSetupPlan,
9532
+ applyPiSetup,
7430
9533
  REQUIRED_SKILLS,
7431
9534
  getRequiredSkillInstallCommand,
7432
9535
  installRequiredSkill,
@@ -7446,6 +9549,12 @@ export {
7446
9549
  buildOpenCodeSyncPlan,
7447
9550
  buildOpenCodeModelPlan,
7448
9551
  applyOpenCodePlan,
9552
+ getPiStatus,
9553
+ buildPiUpdatePlan,
9554
+ buildPiSyncPlan,
9555
+ defaultPiModelRoles,
9556
+ buildPiModelPlan,
9557
+ applyPiPlan,
7449
9558
  SUPPORTED_OPERATION_HARNESSES,
7450
9559
  listOperationHarnesses,
7451
9560
  getOperationHarness,
@@ -7453,6 +9562,7 @@ export {
7453
9562
  getModelOptions,
7454
9563
  getCodexModelRoles,
7455
9564
  getClaudeCodeModelRoles,
9565
+ getPiModelRoles,
7456
9566
  getOpenCodeModelRoles,
7457
9567
  defaultTuiOperations
7458
9568
  };