thoth-agents 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -65,7 +65,7 @@ The commands below install at **global/user scope**.
65
65
  | <a href="https://github.com/anomalyco/opencode"><picture><source media="(prefers-color-scheme: dark)" srcset="https://svgl.app/library/opencode-dark.svg"><img src="https://svgl.app/library/opencode.svg" alt="OpenCode logo" width="48" height="48"></picture></a><br>**OpenCode** | Native plugin, agent team, workflow skills, and memory setup. | `npx thoth-agents@latest install --agent=opencode` |
66
66
  | <a href="https://github.com/openai/codex"><img src="https://github.com/openai.png?size=120" alt="OpenAI logo — Codex" width="48" height="48"></a><br>**Codex** | Native plugin plus the required global agent and instruction setup. **Close Codex first.** | `npx thoth-agents@latest install --agent=codex` |
67
67
  | <a href="https://claude.com/product/claude-code"><img src="https://github.com/anthropics.png?size=120" alt="Anthropic logo — Claude Code" width="48" height="48"></a><br>**Claude Code** | Marketplace agents and skills, completed by the CLI's external skills and memory setup. **Run the prerequisites below first.** | `npx thoth-agents@latest install --agent=claude` |
68
- | <a href="https://github.com/earendil-works/pi"><img src="https://raw.githubusercontent.com/EremesNG/thoth-mem/master/img/pi.svg" alt="Pi logo" width="48" height="48"></a><br>**Pi** | Native package, five specialists, delegation and research extensions, workflow skills, and memory setup. **Recommended for the best experience.** | `npx thoth-agents@latest install --agent=pi` |
68
+ | <a href="https://github.com/earendil-works/pi"><img src="https://raw.githubusercontent.com/EremesNG/thoth-mem/master/img/pi.svg" alt="Pi logo" width="48" height="48"></a><br>**Pi** | Native package, five specialists, delegation, research, theme, sidebar and background-task extensions, workflow skills, and memory setup. **Recommended for the best experience.** | `npx thoth-agents@latest install --agent=pi` |
69
69
 
70
70
  ### Claude Code prerequisites
71
71
 
@@ -83,8 +83,11 @@ claude plugin install thoth-agents@thoth-plugins --scope user
83
83
 
84
84
  Inside Pi, use `/subagents-model` to edit model and effort profiles for global
85
85
  and project subagent definitions. See [subagent profile configuration](docs/installation.md#configure-subagent-model-profiles-inside-pi).
86
- Use `/subagents-tools` to edit each specialist's explicit tool list, including
87
- registered active and inactive extension and MCP tools. Globs (including `*`) are
86
+ Both commands are registered by pi-subagents on pi-core's shared list-editor
87
+ shell, with native CSI-u/kitty and legacy navigation keys and wheel support in
88
+ regular and fullscreen TUI mode. Use `/subagents-tools` to edit resolved global/project definitions' tool
89
+ lists, including registered active and inactive extension and MCP tools. Thoth
90
+ supplies managed-specialist validation and reset defaults through an optional adapter. Globs (including `*`) are
88
91
  manual advanced selections over all registered root tools; the panel preserves
89
92
  them read-only. Edit `disallowed_tools` manually for injected tools and trimming
90
93
  globs; only Oracle denies `ask_orchestrator` by default. See
@@ -93,14 +96,37 @@ globs; only Oracle denies `ask_orchestrator` by default. See
93
96
  Pi setup currently supports the default `~/.pi/agent` root. See the
94
97
  [Pi installation guide](docs/installation.md#pi) for runtime requirements,
95
98
  existing-package conflicts, and recovery. Thoth manages its separate Pi
96
- delegation runtime as package `@thoth-agents/pi-subagents` (`0.1.0`) through
97
- `npm:@thoth-agents/pi-subagents@>=0.1.0`. Existing `pi-subagents` and
99
+ delegation runtime as package `@thoth-agents/pi-subagents` through
100
+ `npm:@thoth-agents/pi-subagents@>=0.3.0`, the first release owning
101
+ `/subagents-tools`. Upgrade both packages together and `/reload` if Pi reports
102
+ missing or duplicate command ownership. Existing `pi-subagents` and
98
103
  `pi-subagents-j0k3r` installs need manual recovery through Pi's package manager
99
104
  before setup. Local checkout development uses `pnpm run setup:pi:local`, which
100
105
  points Pi at the fork under `pi-packages/pi-subagents`; publishing the fork is
101
106
  not required for that path. Pi extensions run with your user's system
102
107
  permissions; agent tool allowlists are not an OS sandbox.
103
108
 
109
+ ### Optional Pi provider extensions (manual install)
110
+
111
+ The installer does **not** install or manage the Claude/Antigravity bridges or
112
+ pi-openai-fast. Install only those you want, then restart Pi:
113
+
114
+ ```bash
115
+ pi install npm:@thoth-agents/pi-claude-bridge
116
+ pi install npm:@thoth-agents/pi-antigravity-bridge
117
+ pi install npm:@thoth-agents/pi-openai-fast
118
+ ```
119
+
120
+ Authenticate Claude Code or the `agy` CLI separately. Claude rate-limit events
121
+ from root and child sessions share pi-core's process-wide, account-wide registry
122
+ (not a bus channel); affected subagent rows/details show active warnings and reset
123
+ times without changing task status. Quota is on demand: `/claude quota` uses an
124
+ experimental SDK API without sending a prompt; `/agy quota` shows Antigravity
125
+ usage. Antigravity costs are API-equivalent estimates from an explicit catalog
126
+ mapping, not subscription charges. The theme marks the total `(sub)` by default
127
+ for both `claude-bridge` and `antigravity`. See
128
+ [provider commands and caveats](docs/installation.md#manual-pi-provider-extensions).
129
+
104
130
  For scopes, troubleshooting, or local checkout installation, see the
105
131
  [installation guide](docs/installation.md). Local Pi checkout installs keep
106
132
  thoth-mem setup separate.
@@ -245,13 +271,58 @@ do not prove those other pieces are current. Use `status` to inspect the last
245
271
  complete CLI-managed installation and follow any reported recovery actions.
246
272
 
247
273
  For Pi, the first-party `thoth-agents` package remains exact and receipt-verified.
248
- The five mandatory external extensions use stable minimum-only `>=` ranges, so
274
+ The nine selected extensions use stable minimum-only `>=` ranges, so
249
275
  Pi's native package manager can update them independently without waiting for a
250
- Thoth release. Status validates each installed manifest's package name and
251
- SemVer floor; newer stable versions are healthy, while prerelease, malformed,
276
+ Thoth release. Install and applied Update now include
277
+ `@thoth-agents/pi-thoth-theme`, `@thoth-agents/pi-background-tasks` and
278
+ `@thoth-agents/pi-sidebar` at `>=0.3.0`.
279
+ Existing copies of these three packages from any source are preserved and verified
280
+ without reinstalling when compatible; older copies are left untouched and block
281
+ completion with manual upgrade guidance. Ambiguous sources fail closed. Status
282
+ validates each installed manifest's package name and SemVer floor; newer stable
283
+ versions are healthy, while prerelease, malformed,
252
284
  missing, or older versions are not. Re-running Install or applying Update
253
- migrates legacy exact external sources through Pi while preserving package
254
- resource filters and unrelated settings.
285
+ migrates legacy exact sources for the other selected extensions through Pi while
286
+ preserving package resource filters and unrelated settings.
287
+
288
+ Subagents (v2) and background tasks (v1) publish summary-only snapshots through
289
+ [pi-core's task channels](pi-packages/pi-core/README.md#task-summary-and-usage-channels).
290
+ The status line uses `thoth:subagents:usage`, not the removed raw
291
+ `thoth:subagent-usage` bus event. Upgrade pi-subagents and pi-thoth-theme to
292
+ `>=0.3.0` together and `/reload` to retain subagent cost display; the usage
293
+ checkpoint discriminator is unchanged.
294
+
295
+ The shared Pi Work panel uses pi-core's v2 data-only provider contract and
296
+ [discovery/action API](pi-packages/pi-core/README.md#discovery-and-actions).
297
+ Upgrade pi-subagents, pi-background-tasks and pi-todo together to `>=0.3.0`, then
298
+ `/reload`; first-owner arbitration hides incompatible sections in mixed v1/v2
299
+ installs. Subagent state is v2; background state, usage, requests and Render KIT remain v1.
300
+
301
+ The [Pi sidebar](pi-packages/pi-sidebar/README.md) adds read-only Session,
302
+ Workspace, Cost and discovered Todos/Subagents/Background panels in fullscreen and
303
+ regular mode. `/sidebar` toggles, `/sidebar panels` lists ids for show/hide/order
304
+ commands, and `/sidebar resize` resizes. `/sidebar settings` edits panel order,
305
+ visibility, startup and default width; `/sidebar cost` opens task-cost curves
306
+ against elapsed time per task, labeled by display name or a short task-summary name.
307
+ Themed Agents/Background headers show static status-icon counts, omitting zero
308
+ counts (Todos keeps completed/total), and detail commands; Workspace shows a
309
+ `~`-abbreviated path and tracked file/line changes versus HEAD. Agent metrics
310
+ include all available metrics with semantic icons (ASCII labels) in shared
311
+ aligned columns that wrap without dropping metrics, stably across digit changes. Only displayed source panels absorb their
312
+ Work sections; hiding the sidebar restores them. Panel order/visibility and
313
+ startup policy/default width live in `~/.pi/agent/thoth-sidebar.json`. Subagent
314
+ state v2 adds up to 100 cost-ranked persisted session summaries in `history`. Private Pi layout seams
315
+ have guarded fallback; regular mode covers the live viewport, not historical
316
+ scrollback. Update related extensions together to avoid mixed old UI owners.
317
+
318
+ Pi's question UI replaces the editor without covering chat. `Ctrl+]` collapses it
319
+ to a one-line dock and returns input to the editor; during an active run, `Enter`
320
+ queues a steering message without answering the question. `Ctrl+]` re-expands
321
+ with answers and drafts preserved, and the open tool-call card shows collapsed
322
+ state. While open, `Ctrl+]` shadows editor `jumpForward`; collapsed `Esc` belongs
323
+ to the editor or focused overlay. History/detail overlays close back to the
324
+ expanded question or otherwise the editor; collapse and teardown preserve
325
+ visible foreign-overlay focus. See [question controls](pi-packages/pi-questions-user/README.md#keyboard-controls).
255
326
 
256
327
  ## Documentation
257
328
 
@@ -16,7 +16,7 @@ import {
16
16
  savePiModelConfig,
17
17
  syncPiSpecialists,
18
18
  writePiManagedText
19
- } from "./chunk-V7HSGRMB.js";
19
+ } from "./chunk-U7FNFTM5.js";
20
20
  import {
21
21
  ALL_AGENT_NAMES,
22
22
  CLAUDE_CODE_PROMPT_DIALECT,
@@ -7174,6 +7174,21 @@ import { createHash as createHash6 } from "crypto";
7174
7174
  import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
7175
7175
  import { basename as basename4, dirname as dirname11, isAbsolute as isAbsolute5, join as join13 } from "path";
7176
7176
 
7177
+ // src/cli/pi-install.ts
7178
+ import { spawnSync as spawnSync6 } from "child_process";
7179
+ import { createHash as createHash5 } from "crypto";
7180
+ import {
7181
+ existsSync as existsSync14,
7182
+ lstatSync as lstatSync4,
7183
+ readdirSync as readdirSync3,
7184
+ readFileSync as readFileSync13,
7185
+ realpathSync as realpathSync3,
7186
+ statSync as statSync3
7187
+ } from "fs";
7188
+ import { basename as basename3, dirname as dirname10, isAbsolute as isAbsolute4, join as join12, resolve as resolve8, sep as sep3 } from "path";
7189
+ import { fileURLToPath as fileURLToPath5 } from "url";
7190
+ import { lt } from "semver";
7191
+
7177
7192
  // src/cli/pi-external-package.ts
7178
7193
  import { readFileSync as readFileSync9, realpathSync as realpathSync2 } from "fs";
7179
7194
  import { isAbsolute as isAbsolute2, join as join7 } from "path";
@@ -7289,21 +7304,6 @@ function inspectPiExternalPackage(packages, spec, requireManagedSource = true) {
7289
7304
  };
7290
7305
  }
7291
7306
 
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
7307
  // src/cli/pi-git-source.ts
7308
7308
  import { resolve as resolve4, sep as sep2 } from "path";
7309
7309
  import hostedGitInfo from "hosted-git-info";
@@ -7945,9 +7945,9 @@ var PI_COMMAND_TIMEOUT_MS = 12e4;
7945
7945
  var PI_PACKAGE_SPECS = [
7946
7946
  {
7947
7947
  id: "delegation",
7948
- source: "npm:@thoth-agents/pi-subagents@>=0.1.0",
7948
+ source: "npm:@thoth-agents/pi-subagents@>=0.3.0",
7949
7949
  packageName: "@thoth-agents/pi-subagents",
7950
- version: "0.1.0"
7950
+ version: "0.3.0"
7951
7951
  },
7952
7952
  {
7953
7953
  id: "context7",
@@ -7969,15 +7969,36 @@ var PI_PACKAGE_SPECS = [
7969
7969
  },
7970
7970
  {
7971
7971
  id: "ask-user-question",
7972
- source: "npm:@thoth-agents/pi-questions-user@>=0.1.0",
7972
+ source: "npm:@thoth-agents/pi-questions-user@>=0.3.0",
7973
7973
  packageName: "@thoth-agents/pi-questions-user",
7974
- version: "0.1.0"
7974
+ version: "0.3.0"
7975
7975
  },
7976
7976
  {
7977
7977
  id: "todo",
7978
- source: "npm:@thoth-agents/pi-todo@>=0.1.0",
7978
+ source: "npm:@thoth-agents/pi-todo@>=0.3.0",
7979
7979
  packageName: "@thoth-agents/pi-todo",
7980
- version: "0.1.0"
7980
+ version: "0.3.0"
7981
+ },
7982
+ {
7983
+ id: "theme",
7984
+ source: "npm:@thoth-agents/pi-thoth-theme@>=0.3.0",
7985
+ packageName: "@thoth-agents/pi-thoth-theme",
7986
+ version: "0.3.0",
7987
+ preserveUserCopy: true
7988
+ },
7989
+ {
7990
+ id: "background-tasks",
7991
+ source: "npm:@thoth-agents/pi-background-tasks@>=0.3.0",
7992
+ packageName: "@thoth-agents/pi-background-tasks",
7993
+ version: "0.3.0",
7994
+ preserveUserCopy: true
7995
+ },
7996
+ {
7997
+ id: "sidebar",
7998
+ source: "npm:@thoth-agents/pi-sidebar@>=0.3.0",
7999
+ packageName: "@thoth-agents/pi-sidebar",
8000
+ version: "0.3.0",
8001
+ preserveUserCopy: true
7981
8002
  }
7982
8003
  ];
7983
8004
  var PI_GREP_MCP_ENTRY = {
@@ -8096,6 +8117,47 @@ function getPiExternalPackageSpecs(options = {}) {
8096
8117
  (spec) => spec.id === "delegation" ? { ...spec, source: runtimePackageRoot } : spec
8097
8118
  );
8098
8119
  }
8120
+ function sourceHintsAtPackage(source, spec) {
8121
+ return source.includes(
8122
+ spec.packageName.split("/").at(-1) ?? spec.packageName
8123
+ );
8124
+ }
8125
+ function hasPiResourceSelection(value) {
8126
+ return ["prompts", "skills", "themes"].some(
8127
+ (key) => Array.isArray(value[key]) && value[key].length > 0
8128
+ );
8129
+ }
8130
+ function inspectPiSetupPackage(packages, spec, requireManagedSource = true) {
8131
+ if (spec.preserveUserCopy) {
8132
+ const unidentified = packages.find((candidate) => {
8133
+ const manifest = configuredPackageManifest(candidate);
8134
+ if (typeof manifest?.name === "string" && manifest.name.trim())
8135
+ return false;
8136
+ const pi = manifest?.pi;
8137
+ const manifestIsResourceOnly = isRecord6(pi) && (pi.extensions === void 0 || Array.isArray(pi.extensions) && pi.extensions.length === 0) && hasPiResourceSelection(pi);
8138
+ return sourceHintsAtPackage(candidate.source, spec) || !candidate.resourceOnly && !manifestIsResourceOnly;
8139
+ });
8140
+ if (unidentified)
8141
+ return {
8142
+ state: "drift",
8143
+ source: unidentified.source,
8144
+ installedPath: unidentified.installedPath,
8145
+ reason: `installed manifest identity is unavailable for ${unidentified.source}${unidentified.installedPath ? ` at ${unidentified.installedPath}` : ""}`
8146
+ };
8147
+ }
8148
+ return inspectPiExternalPackage(
8149
+ packages,
8150
+ spec,
8151
+ requireManagedSource && !spec.preserveUserCopy
8152
+ );
8153
+ }
8154
+ function piPreservedPackageRecovery(spec, evidence) {
8155
+ if (!spec.preserveUserCopy || evidence.state !== "drift") return void 0;
8156
+ const diagnostic = `Cannot preserve and verify ${spec.packageName}: ${evidence.reason}. Existing copies are left untouched.`;
8157
+ if (evidence.version && evidence.source)
8158
+ 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.`;
8159
+ return `${diagnostic} Inspect pi list --no-approve and the configured package manifests; resolve the package identity or source ambiguity, then rerun setup.`;
8160
+ }
8099
8161
  var PI_INCUMBENT_TODO_NAME = "@juicesharp/rpiv-todo";
8100
8162
  var PI_INCUMBENT_QUESTIONS_NAME = "@juicesharp/rpiv-ask-user-question";
8101
8163
  function piQuestionSourceName(source) {
@@ -8233,6 +8295,11 @@ function parsePiPackageSource(source, baseDir) {
8233
8295
  return gitPath ? { type: "git", path: gitPath } : { type: "local", path: source };
8234
8296
  }
8235
8297
  function resolvePiPackage(candidate, options, userSettings) {
8298
+ const declaration = Array.isArray(userSettings.packages) ? userSettings.packages.find(
8299
+ (entry) => isRecord6(entry) && entry.source === candidate.source
8300
+ ) : void 0;
8301
+ const resourceOnly = isRecord6(declaration) && Array.isArray(declaration.extensions) && declaration.extensions.length === 0 && hasPiResourceSelection(declaration);
8302
+ candidate = { ...candidate, resourceOnly };
8236
8303
  if (candidate.installedPath) return candidate;
8237
8304
  const paths = resolvePiPaths(options);
8238
8305
  const baseDir = candidate.scope === "user" ? paths.piRoot : join12(resolve8(options.cwd ?? process.cwd()), ".pi");
@@ -8274,9 +8341,15 @@ function resolvePiListedPackages(output, options) {
8274
8341
  `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
8342
  );
8276
8343
  }
8277
- return packages.map(
8278
- (candidate) => resolvePiPackage(candidate, options, settings)
8279
- );
8344
+ return packages.map((candidate) => {
8345
+ if (candidate.scope === "project")
8346
+ projectSettings ??= readJsonObject(join12(projectRoot, "settings.json"));
8347
+ return resolvePiPackage(
8348
+ candidate,
8349
+ options,
8350
+ candidate.scope === "project" ? projectSettings ?? {} : settings
8351
+ );
8352
+ });
8280
8353
  }
8281
8354
  function readPiProjectPackages(options) {
8282
8355
  const projectRoot = join12(resolve8(options.cwd ?? process.cwd()), ".pi");
@@ -8286,6 +8359,26 @@ function readPiProjectPackages(options) {
8286
8359
  options
8287
8360
  );
8288
8361
  }
8362
+ function resolvePiPreservationPackages(output, options) {
8363
+ const listed = resolvePiListedPackages(output, options);
8364
+ const paths = resolvePiPaths(options);
8365
+ const configured = [
8366
+ ...resolvePiConfiguredPackages(
8367
+ readJsonObject(paths.settingsPath),
8368
+ "user",
8369
+ options
8370
+ ),
8371
+ ...readPiProjectPackages(options)
8372
+ ];
8373
+ return [
8374
+ ...listed,
8375
+ ...configured.filter(
8376
+ (candidate) => !listed.some(
8377
+ ({ scope, source }) => scope === candidate.scope && source === candidate.source
8378
+ )
8379
+ )
8380
+ ];
8381
+ }
8289
8382
  function scanPiProjectInstalledIncumbents(options, configuredPackages) {
8290
8383
  const projectRoot = join12(resolve8(options.cwd ?? process.cwd()), ".pi");
8291
8384
  const incumbents = [];
@@ -8425,6 +8518,7 @@ function buildPiSetupPlan(options = {}) {
8425
8518
  );
8426
8519
  }
8427
8520
  }
8521
+ let userPackages = [];
8428
8522
  let plannedQuestionRemovals = [];
8429
8523
  let subagentsConfigContent;
8430
8524
  let mcpContent;
@@ -8449,11 +8543,7 @@ function buildPiSetupPlan(options = {}) {
8449
8543
  );
8450
8544
  if (incumbentBlockers.length > 0)
8451
8545
  throw new Error(incumbentBlockers.join("\n"));
8452
- const userPackages = resolvePiConfiguredPackages(
8453
- userSettings,
8454
- "user",
8455
- options
8456
- );
8546
+ userPackages = resolvePiConfiguredPackages(userSettings, "user", options);
8457
8547
  blockers.push(...piQuestionIdentityBlockers(userPackages));
8458
8548
  plannedQuestionRemovals = findPiIncumbentQuestions(userPackages);
8459
8549
  subagentsConfigContent = `${JSON.stringify(
@@ -8490,6 +8580,19 @@ function buildPiSetupPlan(options = {}) {
8490
8580
  blockers.push(error instanceof Error ? error.message : String(error));
8491
8581
  }
8492
8582
  blockers.push(...piQuestionIdentityBlockers(projectPackages));
8583
+ for (const pkg of getPiExternalPackageSpecs(options)) {
8584
+ if (!pkg.preserveUserCopy) continue;
8585
+ const recovery = piPreservedPackageRecovery(
8586
+ pkg,
8587
+ inspectPiSetupPackage(
8588
+ [...userPackages, ...projectPackages].filter(
8589
+ ({ source, installedPath }) => installedPath !== void 0 || sourceHintsAtPackage(source, pkg)
8590
+ ),
8591
+ pkg
8592
+ )
8593
+ );
8594
+ if (recovery) blockers.push(recovery);
8595
+ }
8493
8596
  const projectIncumbentTodos = findPiIncumbentTodos(projectPackages);
8494
8597
  for (const incumbent of projectIncumbentTodos) {
8495
8598
  const location = incumbent.unmappedSource ? "was found in the project install roots" : `is configured in ${projectSettingsPath}`;
@@ -8542,7 +8645,7 @@ function buildPiSetupPlan(options = {}) {
8542
8645
  })),
8543
8646
  ...getPiExternalPackageSpecs(options).map((pkg) => ({
8544
8647
  kind: "package",
8545
- description: `Install and verify Pi package ${pkg.source}`,
8648
+ 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
8649
  target: pkg.source,
8547
8650
  command: { command: "pi", args: ["install", pkg.source, "--no-approve"] }
8548
8651
  })),
@@ -8948,6 +9051,25 @@ function applyPiSetup(plan) {
8948
9051
  installedPackages,
8949
9052
  manualRecovery: identityRecovery
8950
9053
  };
9054
+ const preservationPackages = resolvePiPreservationPackages(
9055
+ before.stdout,
9056
+ plan.options
9057
+ );
9058
+ for (const pkg of getPiExternalPackageSpecs(plan.options)) {
9059
+ if (!pkg.preserveUserCopy) continue;
9060
+ const evidence = inspectPiSetupPackage(preservationPackages, pkg, false);
9061
+ const recovery = piPreservedPackageRecovery(pkg, evidence);
9062
+ if (recovery)
9063
+ return {
9064
+ success: false,
9065
+ changed,
9066
+ diagnostics,
9067
+ error: recovery,
9068
+ failedStep: "preflight",
9069
+ installedPackages,
9070
+ manualRecovery: recovery
9071
+ };
9072
+ }
8951
9073
  const installed = execute("pi", ["install", desiredSource, "--no-approve"]);
8952
9074
  if (installed.exitCode !== 0)
8953
9075
  throw new Error(
@@ -9117,16 +9239,22 @@ function applyPiSetup(plan) {
9117
9239
  );
9118
9240
  }
9119
9241
  for (const pkg of getPiExternalPackageSpecs(plan.options)) {
9242
+ const preserveUserCopy = pkg.preserveUserCopy === true;
9120
9243
  const beforeInstall = execute("pi", ["list", "--no-approve"]);
9121
9244
  if (beforeInstall.exitCode !== 0)
9122
9245
  throw new Error(
9123
9246
  `Unable to inspect ${pkg.packageName} before installation: ${beforeInstall.stderr.trim() || "pi list unavailable"}`
9124
9247
  );
9125
- const prior = inspectPiExternalPackage(
9126
- parsePiPackageList(beforeInstall.stdout),
9248
+ const prior = inspectPiSetupPackage(
9249
+ preserveUserCopy ? resolvePiPreservationPackages(beforeInstall.stdout, plan.options) : parsePiPackageList(beforeInstall.stdout),
9127
9250
  pkg,
9128
9251
  false
9129
9252
  );
9253
+ const recovery = piPreservedPackageRecovery(pkg, prior);
9254
+ if (recovery) {
9255
+ manualRecovery = recovery;
9256
+ throw new Error(recovery);
9257
+ }
9130
9258
  const priorSourceMatches = prior.state === "installed" && prior.source !== void 0 && prior.installedPath !== void 0 && piExternalSourceMatches(
9131
9259
  {
9132
9260
  scope: "user",
@@ -9135,7 +9263,8 @@ function applyPiSetup(plan) {
9135
9263
  },
9136
9264
  pkg.source
9137
9265
  );
9138
- if (!priorSourceMatches) {
9266
+ const preserving = preserveUserCopy && prior.state === "installed";
9267
+ if (!preserving && !priorSourceMatches) {
9139
9268
  const result = execute("pi", ["install", pkg.source, "--no-approve"]);
9140
9269
  if (result.exitCode !== 0)
9141
9270
  throw new Error(
@@ -9143,12 +9272,15 @@ function applyPiSetup(plan) {
9143
9272
  );
9144
9273
  }
9145
9274
  const listed = execute("pi", ["list", "--no-approve"]);
9146
- const verified = listed.exitCode === 0 ? inspectPiExternalPackage(parsePiPackageList(listed.stdout), pkg) : {
9275
+ const verified = listed.exitCode === 0 ? inspectPiSetupPackage(
9276
+ preserveUserCopy ? resolvePiPreservationPackages(listed.stdout, plan.options) : parsePiPackageList(listed.stdout),
9277
+ pkg
9278
+ ) : {
9147
9279
  state: "drift",
9148
9280
  version: void 0,
9149
9281
  reason: listed.stderr.trim() || "pi list unavailable"
9150
9282
  };
9151
- if (prior.state === "installed" && verified.version !== void 0 && lt(verified.version, prior.version)) {
9283
+ if (!preserveUserCopy && prior.state === "installed" && verified.version !== void 0 && lt(verified.version, prior.version)) {
9152
9284
  const restoreSource = prior.source ?? `npm:${pkg.packageName}@${prior.version}`;
9153
9285
  const restored = execute("pi", [
9154
9286
  "install",
@@ -9168,8 +9300,21 @@ function applyPiSetup(plan) {
9168
9300
  `Pi attempted to downgrade ${pkg.packageName} from ${prior.version} to ${verified.version}; exact-version recovery ${recoverySucceeded ? "verified" : "failed or unverifiable"}. ${recoveryGuidance}`
9169
9301
  );
9170
9302
  }
9171
- if (verified.state !== "installed")
9172
- throw new Error(`Pi did not verify ${pkg.source}: ${verified.reason}.`);
9303
+ if (verified.state !== "installed") {
9304
+ manualRecovery = piPreservedPackageRecovery(pkg, verified);
9305
+ throw new Error(
9306
+ `Pi did not verify ${pkg.source}: ${verified.reason}.${manualRecovery ? ` ${manualRecovery}` : ""}`
9307
+ );
9308
+ }
9309
+ if (preserving) {
9310
+ if (verified.source !== prior.source || !piPackagePathsEqual(verified.installedPath, prior.installedPath))
9311
+ throw new Error(
9312
+ `Preserved Pi package ${pkg.packageName} changed source or directory during verification; inspect pi list --no-approve, then rerun setup.`
9313
+ );
9314
+ diagnostics.push(
9315
+ `Preserved and verified ${pkg.packageName}@${verified.version} from ${verified.source}; no reinstall.`
9316
+ );
9317
+ }
9173
9318
  installedPackages.push(pkg.source);
9174
9319
  }
9175
9320
  for (const item of plan.items.filter(
@@ -9422,6 +9567,18 @@ function statusFromPlan(plan, context2, evidence = {}) {
9422
9567
  ...plan.projectIncumbentTodos ?? []
9423
9568
  ];
9424
9569
  const packageSpecs = getPiExternalPackageSpecs(plan.options);
9570
+ let preservationPackages = configuredPackages;
9571
+ let preservationError;
9572
+ if (packages.exitCode === 0) {
9573
+ try {
9574
+ preservationPackages = resolvePiPreservationPackages(packages.stdout, {
9575
+ ...plan.options,
9576
+ commandExecutor: execute
9577
+ });
9578
+ } catch (error) {
9579
+ preservationError = error instanceof Error ? error.message : String(error);
9580
+ }
9581
+ }
9425
9582
  for (const target of targets.filter(
9426
9583
  (candidate) => candidate.kind === "package" && packageSpecs.some(({ source }) => source === candidate.path)
9427
9584
  )) {
@@ -9432,8 +9589,14 @@ function statusFromPlan(plan, context2, evidence = {}) {
9432
9589
  target.state = "unknown";
9433
9590
  target.observed = packages.stderr.trim() || "pi list unavailable";
9434
9591
  } else if (externalSpec) {
9435
- const inspected = inspectPiExternalPackage(
9436
- configuredPackages,
9592
+ const preserveUserCopy = externalSpec.preserveUserCopy === true;
9593
+ if (preserveUserCopy && preservationError) {
9594
+ target.state = "drift";
9595
+ target.observed = preservationError;
9596
+ continue;
9597
+ }
9598
+ const inspected = inspectPiSetupPackage(
9599
+ preserveUserCopy ? preservationPackages : configuredPackages,
9437
9600
  externalSpec,
9438
9601
  externalSpec.id !== "delegation" || plan.options.runtimePackageRoot !== void 0
9439
9602
  );