thoth-agents 0.5.1 → 0.7.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 (62) hide show
  1. package/README.md +88 -11
  2. package/dist/agents/prompt-dialects.d.ts +1 -0
  3. package/dist/{chunk-MRFCA3U2.js → chunk-3MHJGWS4.js} +2 -1
  4. package/dist/{chunk-V7HSGRMB.js → chunk-P3GDM5RH.js} +70 -174
  5. package/dist/{chunk-OW6I7ZUU.js → chunk-QOIZYJ5C.js} +322 -151
  6. package/dist/{chunk-VUNYH263.js → chunk-W5TSNGEE.js} +263 -44
  7. package/dist/cli/index.js +4 -4
  8. package/dist/cli/pi-install.d.ts +35 -67
  9. package/dist/cli/pi-tool-config.d.ts +19 -0
  10. package/dist/cli/tui/index.js +3 -3
  11. package/dist/config/constants.d.ts +30 -3
  12. package/dist/config/schema.d.ts +4 -0
  13. package/dist/harness/adapters/claude-code.d.ts +1 -1
  14. package/dist/harness/core/agent-pack.d.ts +6 -2
  15. package/dist/harness/core/result-contract.d.ts +366 -0
  16. package/dist/harness/core/sdd.d.ts +1 -1
  17. package/dist/harness/writers/pi-agent.d.ts +1 -0
  18. package/dist/index.js +2 -2
  19. package/dist/pi.d.ts +6 -35
  20. package/dist/pi.js +37 -433
  21. package/package.json +5 -3
  22. package/pi/.thoth-agents-assets.json +5 -5
  23. package/pi/agents/thoth-designer.md +15 -20
  24. package/pi/agents/thoth-explorer.md +15 -19
  25. package/pi/agents/thoth-librarian.md +15 -19
  26. package/pi/agents/thoth-oracle.md +30 -22
  27. package/pi/agents/thoth-worker.md +13 -18
  28. package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +6 -6
  29. package/plugin/.claude-plugin/plugin.json +1 -1
  30. package/plugin/.codex-plugin/plugin.json +1 -1
  31. package/plugin/agents/designer.md +9 -14
  32. package/plugin/agents/explorer.md +10 -14
  33. package/plugin/agents/librarian.md +9 -13
  34. package/plugin/agents/oracle.md +24 -18
  35. package/plugin/agents/orchestrator.md +38 -61
  36. package/plugin/agents/worker.md +9 -14
  37. package/plugin/skills/plan-reviewer/SKILL.md +16 -8
  38. package/plugin/skills/thoth-archive/SKILL.md +2 -1
  39. package/plugin/skills/thoth-constitution/SKILL.md +1 -1
  40. package/plugin/skills/thoth-constitution/templates/constitution.md +1 -1
  41. package/plugin/skills/thoth-init/SKILL.md +1 -1
  42. package/plugin/skills/thoth-sdd/SKILL.md +67 -2
  43. package/plugin/skills/thoth-sdd/references/phases/verify.md +11 -1
  44. package/plugin/skills/thoth-sdd/scripts/result-contract.json +210 -0
  45. package/plugin/skills/thoth-sdd/scripts/validate-result.d.mts +47 -0
  46. package/plugin/skills/thoth-sdd/scripts/validate-result.mjs +390 -0
  47. package/plugin/skills/thoth-sdd/scripts/validate.mjs +213 -8
  48. package/plugin/skills/thoth-sdd/templates/change.md +8 -0
  49. package/skills/plan-reviewer/SKILL.md +16 -8
  50. package/skills/thoth-archive/SKILL.md +2 -1
  51. package/skills/thoth-constitution/SKILL.md +1 -1
  52. package/skills/thoth-constitution/templates/constitution.md +1 -1
  53. package/skills/thoth-init/SKILL.md +1 -1
  54. package/skills/thoth-sdd/SKILL.md +67 -2
  55. package/skills/thoth-sdd/references/phases/verify.md +11 -1
  56. package/skills/thoth-sdd/scripts/result-contract.json +210 -0
  57. package/skills/thoth-sdd/scripts/validate-result.d.mts +47 -0
  58. package/skills/thoth-sdd/scripts/validate-result.mjs +390 -0
  59. package/skills/thoth-sdd/scripts/validate.mjs +213 -8
  60. package/skills/thoth-sdd/templates/change.md +8 -0
  61. package/thoth-agents.schema.json +14 -0
  62. package/dist/pi/tools-panel.d.ts +0 -51
@@ -16,7 +16,7 @@ import {
16
16
  savePiModelConfig,
17
17
  syncPiSpecialists,
18
18
  writePiManagedText
19
- } from "./chunk-V7HSGRMB.js";
19
+ } from "./chunk-P3GDM5RH.js";
20
20
  import {
21
21
  ALL_AGENT_NAMES,
22
22
  CLAUDE_CODE_PROMPT_DIALECT,
@@ -24,6 +24,7 @@ import {
24
24
  CODEX_PROMPT_DIALECT,
25
25
  CONFIRMED_OPENAI_SUBAGENT_PRESET,
26
26
  DEFAULT_MODELS,
27
+ LEGACY_OPENAI_PRESET,
27
28
  PI_SPECIALIST_ROLES,
28
29
  SUBAGENT_NAMES,
29
30
  claudeCodeSubagentType,
@@ -47,7 +48,7 @@ import {
47
48
  updateOpenCodeMainConfig,
48
49
  writeConfig,
49
50
  writeLiteConfig
50
- } from "./chunk-OW6I7ZUU.js";
51
+ } from "./chunk-QOIZYJ5C.js";
51
52
 
52
53
  // src/cli/codex-paths.ts
53
54
  import { homedir } from "os";
@@ -1759,6 +1760,22 @@ function resolveRoleTomlContent(options) {
1759
1760
  const installedContent = readFileSync4(options.targetPath, "utf8");
1760
1761
  const currentModel = parseRoleTomlModel(installedContent);
1761
1762
  const currentEffort = parseRoleTomlEffort(installedContent);
1763
+ const legacy = LEGACY_OPENAI_PRESET[options.role];
1764
+ if (currentModel === legacy.model && currentEffort === legacy.effort) {
1765
+ options.nextState.models[key] = renderedModel;
1766
+ if (configuredModel !== void 0) {
1767
+ options.nextState.configuredModels ??= {};
1768
+ options.nextState.configuredModels[key] = renderedModel;
1769
+ }
1770
+ if (configuredEffort !== void 0) {
1771
+ options.nextState.configuredEfforts ??= {};
1772
+ const renderedEffort = parseRoleTomlEffort(options.renderedContent);
1773
+ if (renderedEffort !== void 0)
1774
+ options.nextState.configuredEfforts[key] = renderedEffort;
1775
+ else delete options.nextState.configuredEfforts[key];
1776
+ }
1777
+ return options.renderedContent;
1778
+ }
1762
1779
  if (configuredModel !== void 0) {
1763
1780
  options.nextState.models[key] = renderedModel;
1764
1781
  options.nextState.configuredModels ??= {};
@@ -1782,7 +1799,10 @@ function resolveRoleTomlContent(options) {
1782
1799
  );
1783
1800
  }
1784
1801
  options.nextState.models[key] = renderedModel;
1785
- return replaceRoleTomlEffort(options.renderedContent, currentEffort);
1802
+ return replaceRoleTomlEffort(
1803
+ currentModel === void 0 ? options.renderedContent : replaceRoleTomlModel(options.renderedContent, currentModel),
1804
+ currentEffort
1805
+ );
1786
1806
  }
1787
1807
  function roleArtifactContent(role, artifacts) {
1788
1808
  const path = `.codex/agents/thoth-agents-${role}.toml`;
@@ -1827,6 +1847,7 @@ function buildCodexSetupPlan(config) {
1827
1847
  roleArtifactContent(target.role, render.artifacts)
1828
1848
  ),
1829
1849
  content: resolveRoleTomlContent({
1850
+ role: target.role,
1830
1851
  renderedContent: roleArtifactContent(target.role, render.artifacts),
1831
1852
  targetPath: target.path,
1832
1853
  state: managedModelState2,
@@ -2150,7 +2171,7 @@ var CLAUDE_CODE_SUBAGENT_DEFAULT_MODELS = {
2150
2171
  worker: "sonnet"
2151
2172
  };
2152
2173
  var CLAUDE_CODE_SUBAGENT_DEFAULT_EFFORTS = {
2153
- explorer: "low",
2174
+ explorer: "medium",
2154
2175
  designer: "medium",
2155
2176
  worker: "medium",
2156
2177
  librarian: "high",
@@ -7174,6 +7195,21 @@ import { createHash as createHash6 } from "crypto";
7174
7195
  import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
7175
7196
  import { basename as basename4, dirname as dirname11, isAbsolute as isAbsolute5, join as join13 } from "path";
7176
7197
 
7198
+ // src/cli/pi-install.ts
7199
+ import { spawnSync as spawnSync6 } from "child_process";
7200
+ import { createHash as createHash5 } from "crypto";
7201
+ import {
7202
+ existsSync as existsSync14,
7203
+ lstatSync as lstatSync4,
7204
+ readdirSync as readdirSync3,
7205
+ readFileSync as readFileSync13,
7206
+ realpathSync as realpathSync3,
7207
+ statSync as statSync3
7208
+ } from "fs";
7209
+ import { basename as basename3, dirname as dirname10, isAbsolute as isAbsolute4, join as join12, resolve as resolve8, sep as sep3 } from "path";
7210
+ import { fileURLToPath as fileURLToPath5 } from "url";
7211
+ import { lt } from "semver";
7212
+
7177
7213
  // src/cli/pi-external-package.ts
7178
7214
  import { readFileSync as readFileSync9, realpathSync as realpathSync2 } from "fs";
7179
7215
  import { isAbsolute as isAbsolute2, join as join7 } from "path";
@@ -7289,21 +7325,6 @@ function inspectPiExternalPackage(packages, spec, requireManagedSource = true) {
7289
7325
  };
7290
7326
  }
7291
7327
 
7292
- // src/cli/pi-install.ts
7293
- import { spawnSync as spawnSync6 } from "child_process";
7294
- import { createHash as createHash5 } from "crypto";
7295
- import {
7296
- existsSync as existsSync14,
7297
- lstatSync as lstatSync4,
7298
- readdirSync as readdirSync3,
7299
- readFileSync as readFileSync13,
7300
- realpathSync as realpathSync3,
7301
- statSync as statSync3
7302
- } from "fs";
7303
- import { basename as basename3, dirname as dirname10, isAbsolute as isAbsolute4, join as join12, resolve as resolve8, sep as sep3 } from "path";
7304
- import { fileURLToPath as fileURLToPath5 } from "url";
7305
- import { lt } from "semver";
7306
-
7307
7328
  // src/cli/pi-git-source.ts
7308
7329
  import { resolve as resolve4, sep as sep2 } from "path";
7309
7330
  import hostedGitInfo from "hosted-git-info";
@@ -7945,9 +7966,9 @@ var PI_COMMAND_TIMEOUT_MS = 12e4;
7945
7966
  var PI_PACKAGE_SPECS = [
7946
7967
  {
7947
7968
  id: "delegation",
7948
- source: "npm:@thoth-agents/pi-subagents@>=0.1.0",
7969
+ source: "npm:@thoth-agents/pi-subagents@>=0.3.0",
7949
7970
  packageName: "@thoth-agents/pi-subagents",
7950
- version: "0.1.0"
7971
+ version: "0.3.0"
7951
7972
  },
7952
7973
  {
7953
7974
  id: "context7",
@@ -7969,15 +7990,36 @@ var PI_PACKAGE_SPECS = [
7969
7990
  },
7970
7991
  {
7971
7992
  id: "ask-user-question",
7972
- source: "npm:@thoth-agents/pi-questions-user@>=0.1.0",
7993
+ source: "npm:@thoth-agents/pi-questions-user@>=0.3.0",
7973
7994
  packageName: "@thoth-agents/pi-questions-user",
7974
- version: "0.1.0"
7995
+ version: "0.3.0"
7975
7996
  },
7976
7997
  {
7977
7998
  id: "todo",
7978
- source: "npm:@thoth-agents/pi-todo@>=0.1.0",
7999
+ source: "npm:@thoth-agents/pi-todo@>=0.3.0",
7979
8000
  packageName: "@thoth-agents/pi-todo",
7980
- version: "0.1.0"
8001
+ version: "0.3.0"
8002
+ },
8003
+ {
8004
+ id: "theme",
8005
+ source: "npm:@thoth-agents/pi-thoth-theme@>=0.3.0",
8006
+ packageName: "@thoth-agents/pi-thoth-theme",
8007
+ version: "0.3.0",
8008
+ preserveUserCopy: true
8009
+ },
8010
+ {
8011
+ id: "background-tasks",
8012
+ source: "npm:@thoth-agents/pi-background-tasks@>=0.3.0",
8013
+ packageName: "@thoth-agents/pi-background-tasks",
8014
+ version: "0.3.0",
8015
+ preserveUserCopy: true
8016
+ },
8017
+ {
8018
+ id: "sidebar",
8019
+ source: "npm:@thoth-agents/pi-sidebar@>=0.3.0",
8020
+ packageName: "@thoth-agents/pi-sidebar",
8021
+ version: "0.3.0",
8022
+ preserveUserCopy: true
7981
8023
  }
7982
8024
  ];
7983
8025
  var PI_GREP_MCP_ENTRY = {
@@ -8096,6 +8138,47 @@ function getPiExternalPackageSpecs(options = {}) {
8096
8138
  (spec) => spec.id === "delegation" ? { ...spec, source: runtimePackageRoot } : spec
8097
8139
  );
8098
8140
  }
8141
+ function sourceHintsAtPackage(source, spec) {
8142
+ return source.includes(
8143
+ spec.packageName.split("/").at(-1) ?? spec.packageName
8144
+ );
8145
+ }
8146
+ function hasPiResourceSelection(value) {
8147
+ return ["prompts", "skills", "themes"].some(
8148
+ (key) => Array.isArray(value[key]) && value[key].length > 0
8149
+ );
8150
+ }
8151
+ function inspectPiSetupPackage(packages, spec, requireManagedSource = true) {
8152
+ if (spec.preserveUserCopy) {
8153
+ const unidentified = packages.find((candidate) => {
8154
+ const manifest = configuredPackageManifest(candidate);
8155
+ if (typeof manifest?.name === "string" && manifest.name.trim())
8156
+ return false;
8157
+ const pi = manifest?.pi;
8158
+ const manifestIsResourceOnly = isRecord6(pi) && (pi.extensions === void 0 || Array.isArray(pi.extensions) && pi.extensions.length === 0) && hasPiResourceSelection(pi);
8159
+ return sourceHintsAtPackage(candidate.source, spec) || !candidate.resourceOnly && !manifestIsResourceOnly;
8160
+ });
8161
+ if (unidentified)
8162
+ return {
8163
+ state: "drift",
8164
+ source: unidentified.source,
8165
+ installedPath: unidentified.installedPath,
8166
+ reason: `installed manifest identity is unavailable for ${unidentified.source}${unidentified.installedPath ? ` at ${unidentified.installedPath}` : ""}`
8167
+ };
8168
+ }
8169
+ return inspectPiExternalPackage(
8170
+ packages,
8171
+ spec,
8172
+ requireManagedSource && !spec.preserveUserCopy
8173
+ );
8174
+ }
8175
+ function piPreservedPackageRecovery(spec, evidence) {
8176
+ if (!spec.preserveUserCopy || evidence.state !== "drift") return void 0;
8177
+ const diagnostic = `Cannot preserve and verify ${spec.packageName}: ${evidence.reason}. Existing copies are left untouched.`;
8178
+ if (evidence.version && evidence.source)
8179
+ return `${diagnostic} Upgrade this copy to >=${spec.version} (for local/git sources, upgrade the checkout), or review its ownership and switch with: pi remove ${evidence.source} --no-approve, then pi install ${spec.source} --no-approve. Verify with pi list --no-approve and the installed manifest, then rerun setup.`;
8180
+ return `${diagnostic} Inspect pi list --no-approve and the configured package manifests; resolve the package identity or source ambiguity, then rerun setup.`;
8181
+ }
8099
8182
  var PI_INCUMBENT_TODO_NAME = "@juicesharp/rpiv-todo";
8100
8183
  var PI_INCUMBENT_QUESTIONS_NAME = "@juicesharp/rpiv-ask-user-question";
8101
8184
  function piQuestionSourceName(source) {
@@ -8233,6 +8316,11 @@ function parsePiPackageSource(source, baseDir) {
8233
8316
  return gitPath ? { type: "git", path: gitPath } : { type: "local", path: source };
8234
8317
  }
8235
8318
  function resolvePiPackage(candidate, options, userSettings) {
8319
+ const declaration = Array.isArray(userSettings.packages) ? userSettings.packages.find(
8320
+ (entry) => isRecord6(entry) && entry.source === candidate.source
8321
+ ) : void 0;
8322
+ const resourceOnly = isRecord6(declaration) && Array.isArray(declaration.extensions) && declaration.extensions.length === 0 && hasPiResourceSelection(declaration);
8323
+ candidate = { ...candidate, resourceOnly };
8236
8324
  if (candidate.installedPath) return candidate;
8237
8325
  const paths = resolvePiPaths(options);
8238
8326
  const baseDir = candidate.scope === "user" ? paths.piRoot : join12(resolve8(options.cwd ?? process.cwd()), ".pi");
@@ -8274,9 +8362,15 @@ function resolvePiListedPackages(output, options) {
8274
8362
  `Pi package manifest identity cannot be verified: malformed pi list source ${source}. Rerun pi list --no-approve and inspect the configured sources before setup.`
8275
8363
  );
8276
8364
  }
8277
- return packages.map(
8278
- (candidate) => resolvePiPackage(candidate, options, settings)
8279
- );
8365
+ return packages.map((candidate) => {
8366
+ if (candidate.scope === "project")
8367
+ projectSettings ??= readJsonObject(join12(projectRoot, "settings.json"));
8368
+ return resolvePiPackage(
8369
+ candidate,
8370
+ options,
8371
+ candidate.scope === "project" ? projectSettings ?? {} : settings
8372
+ );
8373
+ });
8280
8374
  }
8281
8375
  function readPiProjectPackages(options) {
8282
8376
  const projectRoot = join12(resolve8(options.cwd ?? process.cwd()), ".pi");
@@ -8286,6 +8380,26 @@ function readPiProjectPackages(options) {
8286
8380
  options
8287
8381
  );
8288
8382
  }
8383
+ function resolvePiPreservationPackages(output, options) {
8384
+ const listed = resolvePiListedPackages(output, options);
8385
+ const paths = resolvePiPaths(options);
8386
+ const configured = [
8387
+ ...resolvePiConfiguredPackages(
8388
+ readJsonObject(paths.settingsPath),
8389
+ "user",
8390
+ options
8391
+ ),
8392
+ ...readPiProjectPackages(options)
8393
+ ];
8394
+ return [
8395
+ ...listed,
8396
+ ...configured.filter(
8397
+ (candidate) => !listed.some(
8398
+ ({ scope, source }) => scope === candidate.scope && source === candidate.source
8399
+ )
8400
+ )
8401
+ ];
8402
+ }
8289
8403
  function scanPiProjectInstalledIncumbents(options, configuredPackages) {
8290
8404
  const projectRoot = join12(resolve8(options.cwd ?? process.cwd()), ".pi");
8291
8405
  const incumbents = [];
@@ -8331,6 +8445,29 @@ function scanPiProjectInstalledIncumbents(options, configuredPackages) {
8331
8445
  }
8332
8446
  return incumbents;
8333
8447
  }
8448
+ function mergePiCodemodeSettings(current) {
8449
+ const tools = current.defaultTools;
8450
+ if (tools !== void 0 && (!Array.isArray(tools) || tools.some((tool) => typeof tool !== "string")))
8451
+ throw new Error("Global Pi defaultTools must be an array of strings.");
8452
+ if (current.codemode !== void 0 && !isRecord6(current.codemode))
8453
+ throw new Error("Global Pi codemode must be a JSON object.");
8454
+ const codemode = isRecord6(current.codemode) ? current.codemode : {};
8455
+ let defaultTools = tools ?? ["+codemode"];
8456
+ if (!defaultTools.some(
8457
+ (tool) => ["codemode", "+codemode", "-codemode"].includes(tool)
8458
+ )) {
8459
+ const additive = defaultTools.length > 0 && defaultTools.every((tool) => /^[+-]/.test(tool));
8460
+ defaultTools = [...defaultTools, additive ? "+codemode" : "codemode"];
8461
+ }
8462
+ return {
8463
+ ...current,
8464
+ defaultTools,
8465
+ codemode: {
8466
+ ...codemode,
8467
+ ...codemode.mode === void 0 ? { mode: "on" } : {}
8468
+ }
8469
+ };
8470
+ }
8334
8471
  function mergePiSubagentsConfig(current) {
8335
8472
  return {
8336
8473
  ...current,
@@ -8425,11 +8562,15 @@ function buildPiSetupPlan(options = {}) {
8425
8562
  );
8426
8563
  }
8427
8564
  }
8565
+ let userPackages = [];
8428
8566
  let plannedQuestionRemovals = [];
8567
+ let codemodeSettingsContent;
8429
8568
  let subagentsConfigContent;
8430
8569
  let mcpContent;
8431
8570
  try {
8432
8571
  const userSettings = readJsonObject(paths.settingsPath);
8572
+ codemodeSettingsContent = `${JSON.stringify(mergePiCodemodeSettings(userSettings), null, 2)}
8573
+ `;
8433
8574
  const incumbentSource = configuredPackageSource(
8434
8575
  userSettings,
8435
8576
  isPiIncumbentDelegationSource
@@ -8449,11 +8590,7 @@ function buildPiSetupPlan(options = {}) {
8449
8590
  );
8450
8591
  if (incumbentBlockers.length > 0)
8451
8592
  throw new Error(incumbentBlockers.join("\n"));
8452
- const userPackages = resolvePiConfiguredPackages(
8453
- userSettings,
8454
- "user",
8455
- options
8456
- );
8593
+ userPackages = resolvePiConfiguredPackages(userSettings, "user", options);
8457
8594
  blockers.push(...piQuestionIdentityBlockers(userPackages));
8458
8595
  plannedQuestionRemovals = findPiIncumbentQuestions(userPackages);
8459
8596
  subagentsConfigContent = `${JSON.stringify(
@@ -8490,6 +8627,19 @@ function buildPiSetupPlan(options = {}) {
8490
8627
  blockers.push(error instanceof Error ? error.message : String(error));
8491
8628
  }
8492
8629
  blockers.push(...piQuestionIdentityBlockers(projectPackages));
8630
+ for (const pkg of getPiExternalPackageSpecs(options)) {
8631
+ if (!pkg.preserveUserCopy) continue;
8632
+ const recovery = piPreservedPackageRecovery(
8633
+ pkg,
8634
+ inspectPiSetupPackage(
8635
+ [...userPackages, ...projectPackages].filter(
8636
+ ({ source, installedPath }) => installedPath !== void 0 || sourceHintsAtPackage(source, pkg)
8637
+ ),
8638
+ pkg
8639
+ )
8640
+ );
8641
+ if (recovery) blockers.push(recovery);
8642
+ }
8493
8643
  const projectIncumbentTodos = findPiIncumbentTodos(projectPackages);
8494
8644
  for (const incumbent of projectIncumbentTodos) {
8495
8645
  const location = incumbent.unmappedSource ? "was found in the project install roots" : `is configured in ${projectSettingsPath}`;
@@ -8542,10 +8692,18 @@ function buildPiSetupPlan(options = {}) {
8542
8692
  })),
8543
8693
  ...getPiExternalPackageSpecs(options).map((pkg) => ({
8544
8694
  kind: "package",
8545
- description: `Install and verify Pi package ${pkg.source}`,
8695
+ description: pkg.preserveUserCopy ? `Install Pi package ${pkg.source} if absent; otherwise preserve and verify its user copy` : `Install and verify Pi package ${pkg.source}`,
8546
8696
  target: pkg.source,
8547
8697
  command: { command: "pi", args: ["install", pkg.source, "--no-approve"] }
8548
8698
  })),
8699
+ ...codemodeSettingsContent === void 0 ? [] : [
8700
+ {
8701
+ kind: "settings",
8702
+ description: "Fill absent global codemode defaults",
8703
+ target: paths.settingsPath,
8704
+ content: codemodeSettingsContent
8705
+ }
8706
+ ],
8549
8707
  ...subagentsConfigContent === void 0 ? [] : [
8550
8708
  {
8551
8709
  kind: "settings",
@@ -8948,6 +9106,25 @@ function applyPiSetup(plan) {
8948
9106
  installedPackages,
8949
9107
  manualRecovery: identityRecovery
8950
9108
  };
9109
+ const preservationPackages = resolvePiPreservationPackages(
9110
+ before.stdout,
9111
+ plan.options
9112
+ );
9113
+ for (const pkg of getPiExternalPackageSpecs(plan.options)) {
9114
+ if (!pkg.preserveUserCopy) continue;
9115
+ const evidence = inspectPiSetupPackage(preservationPackages, pkg, false);
9116
+ const recovery = piPreservedPackageRecovery(pkg, evidence);
9117
+ if (recovery)
9118
+ return {
9119
+ success: false,
9120
+ changed,
9121
+ diagnostics,
9122
+ error: recovery,
9123
+ failedStep: "preflight",
9124
+ installedPackages,
9125
+ manualRecovery: recovery
9126
+ };
9127
+ }
8951
9128
  const installed = execute("pi", ["install", desiredSource, "--no-approve"]);
8952
9129
  if (installed.exitCode !== 0)
8953
9130
  throw new Error(
@@ -9117,16 +9294,22 @@ function applyPiSetup(plan) {
9117
9294
  );
9118
9295
  }
9119
9296
  for (const pkg of getPiExternalPackageSpecs(plan.options)) {
9297
+ const preserveUserCopy = pkg.preserveUserCopy === true;
9120
9298
  const beforeInstall = execute("pi", ["list", "--no-approve"]);
9121
9299
  if (beforeInstall.exitCode !== 0)
9122
9300
  throw new Error(
9123
9301
  `Unable to inspect ${pkg.packageName} before installation: ${beforeInstall.stderr.trim() || "pi list unavailable"}`
9124
9302
  );
9125
- const prior = inspectPiExternalPackage(
9126
- parsePiPackageList(beforeInstall.stdout),
9303
+ const prior = inspectPiSetupPackage(
9304
+ preserveUserCopy ? resolvePiPreservationPackages(beforeInstall.stdout, plan.options) : parsePiPackageList(beforeInstall.stdout),
9127
9305
  pkg,
9128
9306
  false
9129
9307
  );
9308
+ const recovery = piPreservedPackageRecovery(pkg, prior);
9309
+ if (recovery) {
9310
+ manualRecovery = recovery;
9311
+ throw new Error(recovery);
9312
+ }
9130
9313
  const priorSourceMatches = prior.state === "installed" && prior.source !== void 0 && prior.installedPath !== void 0 && piExternalSourceMatches(
9131
9314
  {
9132
9315
  scope: "user",
@@ -9135,7 +9318,8 @@ function applyPiSetup(plan) {
9135
9318
  },
9136
9319
  pkg.source
9137
9320
  );
9138
- if (!priorSourceMatches) {
9321
+ const preserving = preserveUserCopy && prior.state === "installed";
9322
+ if (!preserving && !priorSourceMatches) {
9139
9323
  const result = execute("pi", ["install", pkg.source, "--no-approve"]);
9140
9324
  if (result.exitCode !== 0)
9141
9325
  throw new Error(
@@ -9143,12 +9327,15 @@ function applyPiSetup(plan) {
9143
9327
  );
9144
9328
  }
9145
9329
  const listed = execute("pi", ["list", "--no-approve"]);
9146
- const verified = listed.exitCode === 0 ? inspectPiExternalPackage(parsePiPackageList(listed.stdout), pkg) : {
9330
+ const verified = listed.exitCode === 0 ? inspectPiSetupPackage(
9331
+ preserveUserCopy ? resolvePiPreservationPackages(listed.stdout, plan.options) : parsePiPackageList(listed.stdout),
9332
+ pkg
9333
+ ) : {
9147
9334
  state: "drift",
9148
9335
  version: void 0,
9149
9336
  reason: listed.stderr.trim() || "pi list unavailable"
9150
9337
  };
9151
- if (prior.state === "installed" && verified.version !== void 0 && lt(verified.version, prior.version)) {
9338
+ if (!preserveUserCopy && prior.state === "installed" && verified.version !== void 0 && lt(verified.version, prior.version)) {
9152
9339
  const restoreSource = prior.source ?? `npm:${pkg.packageName}@${prior.version}`;
9153
9340
  const restored = execute("pi", [
9154
9341
  "install",
@@ -9168,14 +9355,28 @@ function applyPiSetup(plan) {
9168
9355
  `Pi attempted to downgrade ${pkg.packageName} from ${prior.version} to ${verified.version}; exact-version recovery ${recoverySucceeded ? "verified" : "failed or unverifiable"}. ${recoveryGuidance}`
9169
9356
  );
9170
9357
  }
9171
- if (verified.state !== "installed")
9172
- throw new Error(`Pi did not verify ${pkg.source}: ${verified.reason}.`);
9358
+ if (verified.state !== "installed") {
9359
+ manualRecovery = piPreservedPackageRecovery(pkg, verified);
9360
+ throw new Error(
9361
+ `Pi did not verify ${pkg.source}: ${verified.reason}.${manualRecovery ? ` ${manualRecovery}` : ""}`
9362
+ );
9363
+ }
9364
+ if (preserving) {
9365
+ if (verified.source !== prior.source || !piPackagePathsEqual(verified.installedPath, prior.installedPath))
9366
+ throw new Error(
9367
+ `Preserved Pi package ${pkg.packageName} changed source or directory during verification; inspect pi list --no-approve, then rerun setup.`
9368
+ );
9369
+ diagnostics.push(
9370
+ `Preserved and verified ${pkg.packageName}@${verified.version} from ${verified.source}; no reinstall.`
9371
+ );
9372
+ }
9173
9373
  installedPackages.push(pkg.source);
9174
9374
  }
9175
9375
  for (const item of plan.items.filter(
9176
9376
  (candidate) => candidate.content !== void 0 && (candidate.kind === "settings" || candidate.kind === "mcp")
9177
9377
  )) {
9178
- const content = item.target === plan.paths.subagentsConfigPath ? `${JSON.stringify(mergePiSubagentsConfig(readJsonObject(item.target)), null, 2)}
9378
+ const content = item.target === plan.paths.settingsPath ? `${JSON.stringify(mergePiCodemodeSettings(readJsonObject(item.target)), null, 2)}
9379
+ ` : item.target === plan.paths.subagentsConfigPath ? `${JSON.stringify(mergePiSubagentsConfig(readJsonObject(item.target)), null, 2)}
9179
9380
  ` : item.target === plan.paths.mcpConfigPath ? `${JSON.stringify(mergePiGrepMcpConfig(readJsonObject(item.target)), null, 2)}
9180
9381
  ` : item.content ?? "";
9181
9382
  if (writePiManagedText(item.target, content)) changed.push(item.target);
@@ -9422,6 +9623,18 @@ function statusFromPlan(plan, context2, evidence = {}) {
9422
9623
  ...plan.projectIncumbentTodos ?? []
9423
9624
  ];
9424
9625
  const packageSpecs = getPiExternalPackageSpecs(plan.options);
9626
+ let preservationPackages = configuredPackages;
9627
+ let preservationError;
9628
+ if (packages.exitCode === 0) {
9629
+ try {
9630
+ preservationPackages = resolvePiPreservationPackages(packages.stdout, {
9631
+ ...plan.options,
9632
+ commandExecutor: execute
9633
+ });
9634
+ } catch (error) {
9635
+ preservationError = error instanceof Error ? error.message : String(error);
9636
+ }
9637
+ }
9425
9638
  for (const target of targets.filter(
9426
9639
  (candidate) => candidate.kind === "package" && packageSpecs.some(({ source }) => source === candidate.path)
9427
9640
  )) {
@@ -9432,8 +9645,14 @@ function statusFromPlan(plan, context2, evidence = {}) {
9432
9645
  target.state = "unknown";
9433
9646
  target.observed = packages.stderr.trim() || "pi list unavailable";
9434
9647
  } else if (externalSpec) {
9435
- const inspected = inspectPiExternalPackage(
9436
- configuredPackages,
9648
+ const preserveUserCopy = externalSpec.preserveUserCopy === true;
9649
+ if (preserveUserCopy && preservationError) {
9650
+ target.state = "drift";
9651
+ target.observed = preservationError;
9652
+ continue;
9653
+ }
9654
+ const inspected = inspectPiSetupPackage(
9655
+ preserveUserCopy ? preservationPackages : configuredPackages,
9437
9656
  externalSpec,
9438
9657
  externalSpec.id !== "delegation" || plan.options.runtimePackageRoot !== void 0
9439
9658
  );
package/dist/cli/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  getOpenCodeVersion,
5
5
  isOpenCodeInstalled,
6
6
  opencodeAdapter
7
- } from "../chunk-MRFCA3U2.js";
7
+ } from "../chunk-3MHJGWS4.js";
8
8
  import {
9
9
  CODEX_ROLE_NAMES,
10
10
  REQUIRED_SKILLS,
@@ -59,11 +59,11 @@ import {
59
59
  normalizeEffortSelection,
60
60
  recordHarnessInstallCompletion,
61
61
  syncOpenCodeOwnedSkills
62
- } from "../chunk-VUNYH263.js";
62
+ } from "../chunk-W5TSNGEE.js";
63
63
  import "../chunk-62L3ORXN.js";
64
64
  import {
65
65
  piAdapter
66
- } from "../chunk-V7HSGRMB.js";
66
+ } from "../chunk-P3GDM5RH.js";
67
67
  import {
68
68
  ALL_AGENT_NAMES,
69
69
  detectCurrentConfig,
@@ -74,7 +74,7 @@ import {
74
74
  resolveExecutingPackageVersion,
75
75
  updateOpenCodeMainConfig,
76
76
  writeLiteConfig
77
- } from "../chunk-OW6I7ZUU.js";
77
+ } from "../chunk-QOIZYJ5C.js";
78
78
 
79
79
  // src/cli/index.ts
80
80
  import { pathToFileURL } from "url";