pi-profile-switch 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/bin/pi-profile.ts CHANGED
@@ -28,7 +28,7 @@ try {
28
28
  const agentDir = getAgentDir();
29
29
  // Fails before spawning when the profile is unknown or cannot activate.
30
30
  // --approve/--no-approve are consumed here as a one-run trust input.
31
- const { plan, discovery, projectSettings, projectDir, warnings } = await resolveInitialProfile(args.profile, {
31
+ const { plan, discovery, projectDir, warnings } = await resolveInitialProfile(args.profile, {
32
32
  agentDir,
33
33
  cwd: process.cwd(),
34
34
  trustOverride: args.trustOverride,
@@ -43,13 +43,14 @@ try {
43
43
  for (const warning of await sweepStaleInstances()) {
44
44
  console.error(`pi-profile: warning: ${warning}`);
45
45
  }
46
- const generated = await generateRuntimeDir(plan, { agentDir, discovery, projectSettings, projectDir });
46
+ const generated = await generateRuntimeDir(plan, { agentDir, discovery, projectDir });
47
47
  process.exitCode = await spawnPi({
48
48
  generated,
49
49
  piArgs: args.piArgs,
50
- // Only the default profile keeps trust behavior native (flag re-applied);
51
- // named profiles never forward it — the resolver is the trust gatekeeper.
52
- trustOverride: plan.filter === "none" ? args.trustOverride : undefined,
50
+ // Re-applied for every profile: the one-run trust input must decide both
51
+ // the resolver's project reads and Pi's project-scope visibility — two
52
+ // different answers in one launch would be a divergence, not a policy.
53
+ trustOverride: args.trustOverride,
53
54
  });
54
55
  } catch (error) {
55
56
  // Launcher input/selection failures (unknown profile, unresolvable
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-profile-switch",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Named profiles for Pi: reference skills, extensions, MCP servers, and tools per workflow, switched without restarting. Install: npm install -g pi-profile-switch (not pi install).",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -53,9 +53,6 @@ export interface InitialProfile {
53
53
  /** Full discovery results for the settings generator. Undefined for the
54
54
  * default profile (which applies no filtering). */
55
55
  discovery?: LauncherDiscovery;
56
- /** The trusted project's `.pi/settings.json` content, when trusted and
57
- * present. The generator merges it into the base for selection plans. */
58
- projectSettings?: Record<string, unknown>;
59
56
  /** The trusted project directory, when trusted. */
60
57
  projectDir?: string;
61
58
  /** Non-fatal notices for the user (e.g. a dangling restored profile that
@@ -63,13 +60,12 @@ export interface InitialProfile {
63
60
  warnings: string[];
64
61
  }
65
62
 
66
- /** Reads the real global `defaultProjectTrust` setting (a trust input) and,
67
- * when trusted, the project's `.pi/settings.json`. Exported for the
68
- * session-side surfaces (selector/list/status) that need the same trust
69
- * gate the launcher uses. */
63
+ /** Reads the real global `defaultProjectTrust` setting (a trust input) and
64
+ * resolves the project trust decision from Pi's own trust store. Exported
65
+ * for the session-side surfaces (selector/list/status) that need the same
66
+ * trust gate the launcher uses. */
70
67
  export async function readTrustInputs(context: LauncherContext): Promise<{
71
68
  projectTrusted: boolean;
72
- projectSettings?: Record<string, unknown>;
73
69
  }> {
74
70
  const globalSettingsPath = path.join(context.agentDir, "settings.json");
75
71
  const globalSettings = await readJsonFile(globalSettingsPath);
@@ -86,10 +82,9 @@ export async function readTrustInputs(context: LauncherContext): Promise<{
86
82
  if (!projectTrusted) {
87
83
  return { projectTrusted };
88
84
  }
89
- const projectSettingsResult = await readJsonFile(path.join(context.cwd, ".pi", "settings.json"));
90
- const projectSettings =
91
- projectSettingsResult.ok && isRecord(projectSettingsResult.value) ? projectSettingsResult.value : undefined;
92
- return { projectTrusted, projectSettings };
85
+ // The project's own `.pi/settings.json` is Pi's to read (natively, for the
86
+ // same trust decision): pi-profile neither merges nor narrows it.
87
+ return { projectTrusted };
93
88
  }
94
89
 
95
90
  export async function resolveInitialProfile(
@@ -97,7 +92,7 @@ export async function resolveInitialProfile(
97
92
  context: LauncherContext,
98
93
  options?: { overlay?: RuntimeOverlay },
99
94
  ): Promise<InitialProfile> {
100
- const { projectTrusted, projectSettings } = await readTrustInputs(context);
95
+ const { projectTrusted } = await readTrustInputs(context);
101
96
  const projectDir = projectTrusted ? context.cwd : undefined;
102
97
  const catalog = await ProfileCatalog.load(context.agentDir, { projectDir });
103
98
 
@@ -156,7 +151,7 @@ export async function resolveInitialProfile(
156
151
  overlay,
157
152
  });
158
153
  warnings.push(...discovery.extensions.warnings(), ...unmatchedWarnings(plan));
159
- return { plan, discovery, projectSettings, projectDir, warnings };
154
+ return { plan, discovery, projectDir, warnings };
160
155
  }
161
156
 
162
157
  const discovery = await discoverLauncherResources({ ...context, projectTrusted });
@@ -177,5 +172,5 @@ export async function resolveInitialProfile(
177
172
  // silently do nothing or leak through unfiltered.
178
173
  throw new MissingMcpAdapterError(plan.profile);
179
174
  }
180
- return { plan, discovery, projectSettings, projectDir, warnings };
175
+ return { plan, discovery, projectDir, warnings };
181
176
  }
@@ -29,7 +29,8 @@ const EXTENSION_ENTRY = fileURLToPath(new URL("../../extensions/pi-profile/index
29
29
  * trust re-application, then user args verbatim. */
30
30
  export function buildPiArgs(options: SpawnPiOptions): string[] {
31
31
  const args = ["-e", EXTENSION_ENTRY];
32
- // default profile keeps trust behavior native: re-apply the recorded flag.
32
+ // Re-apply the recorded one-run trust input for every profile: the same input
33
+ // decided the launcher's project reads, so Pi must decide the same way.
33
34
  if (options.trustOverride === true) args.push("--approve");
34
35
  if (options.trustOverride === false) args.push("--no-approve");
35
36
  args.push(...options.piArgs);
package/src/mcp-config.ts CHANGED
@@ -40,6 +40,9 @@ export interface McpDiscoveryOptions {
40
40
  export interface MergedMcpResult {
41
41
  servers: Record<string, Record<string, unknown>>;
42
42
  sharedServers: Set<string>;
43
+ /** Servers defined in a trusted project's own config (`.mcp.json`,
44
+ * `.pi/mcp.json`). They are not the profile's to narrow. */
45
+ projectServers: Set<string>;
43
46
  baseConfig?: Record<string, unknown>;
44
47
  }
45
48
 
@@ -47,6 +50,9 @@ export interface McpConfigSource {
47
50
  path: string;
48
51
  isShared: boolean;
49
52
  isAgentDir?: boolean;
53
+ /** Project-scope source: only read for a trusted project, and its servers
54
+ * stay enabled regardless of a profile's `mcps` declaration. */
55
+ isProject?: boolean;
50
56
  }
51
57
 
52
58
  /**
@@ -71,8 +77,8 @@ export function getStandardMcpConfigSources(
71
77
  { path: path.join(agentDir, "mcp.json"), isShared: false, isAgentDir: true },
72
78
  ];
73
79
  if (projectDir !== undefined) {
74
- sources.push({ path: path.join(projectDir, ".mcp.json"), isShared: true });
75
- sources.push({ path: path.join(projectDir, ".pi", "mcp.json"), isShared: false });
80
+ sources.push({ path: path.join(projectDir, ".mcp.json"), isShared: true, isProject: true });
81
+ sources.push({ path: path.join(projectDir, ".pi", "mcp.json"), isShared: false, isProject: true });
76
82
  }
77
83
  return sources;
78
84
  }
@@ -86,6 +92,7 @@ export async function loadMergedMcpServers(
86
92
  const seenPaths = new Set<string>();
87
93
  const servers: Record<string, Record<string, unknown>> = {};
88
94
  const sharedServers = new Set<string>();
95
+ const projectServers = new Set<string>();
89
96
  let baseConfig: Record<string, unknown> | undefined;
90
97
 
91
98
  for (const source of sources) {
@@ -112,6 +119,9 @@ export async function loadMergedMcpServers(
112
119
  if (source.isShared) {
113
120
  sharedServers.add(name);
114
121
  }
122
+ if (source.isProject === true) {
123
+ projectServers.add(name);
124
+ }
115
125
  if (isRecord(def)) {
116
126
  servers[name] = { ...(servers[name] ?? {}), ...def };
117
127
  } else {
@@ -120,7 +130,7 @@ export async function loadMergedMcpServers(
120
130
  }
121
131
  }
122
132
 
123
- return { servers, sharedServers, baseConfig };
133
+ return { servers, sharedServers, projectServers, baseConfig };
124
134
  }
125
135
 
126
136
  /** Server names the adapter would discover: standard global MCP configs,
@@ -1,27 +1,31 @@
1
1
  /**
2
- * Project trust resolution for the launcher — pi-profile is the sole
3
- * gatekeeper for project resources, because generated settings carry
4
- * `defaultProjectTrust: "never"` and Pi therefore never auto-discovers them.
2
+ * Project trust resolution for the launcher — pi-profile's gate for the
3
+ * project-scope content it reads itself: the project catalog, the project
4
+ * runtime state, and the project MCP config. Project-level resources
5
+ * (skills, extensions, prompts, themes, settings) are Pi's own business: its
6
+ * trust store decision, exposed through the linked `trust.json`, decides
7
+ * their visibility (see ADR-0011).
5
8
  *
6
9
  * Mirrors Pi's own trust decision order (`resolveProjectTrusted`), with
7
10
  * pi-profile's catalog/state files added to the trust-requiring
8
11
  * resource set (they are pi-profile's project attack surface; Pi doesn't
9
12
  * know about them):
10
13
  * 1. one-run `--approve` / `--no-approve` override (consumed by the
11
- * launcher, never forwarded for named profiles)
14
+ * launcher and re-applied to the spawned pi for every profile)
12
15
  * 2. a project with no trust-requiring resources at all is trusted —
13
16
  * there is nothing project-scoped to gate
14
17
  * 3. the stored decision in the real `trust.json` (nearest ancestor wins)
15
18
  * 4. the user's `defaultProjectTrust` setting — but only "always" grants;
16
19
  * "ask" cannot prompt here (the launcher has no trust UI), so it falls
17
20
  * through to untrusted
18
- * 5. otherwise untrusted: project catalogs, registries, resources, and
19
- * state files are not read at all
21
+ * 5. otherwise untrusted: project catalogs, project state, and project MCP
22
+ * config are not read at all
20
23
  *
21
24
  * Known divergence from Pi: extension `project_trust` event handlers are not
22
25
  * consulted — that would require executing extension code in the launcher,
23
- * which pi-profile never does. In named-profile sessions the event is moot
24
- * anyway (generated settings carry `defaultProjectTrust: "never"`).
26
+ * which pi-profile never does. A spawned Pi still runs those handlers with
27
+ * its own settings, so an extension that answers yes/no can decide the
28
+ * project scope of that session.
25
29
  */
26
30
 
27
31
  import { existsSync, readFileSync } from "node:fs";
@@ -17,18 +17,20 @@
17
17
  * the spawned pi behaves exactly like native `pi`.
18
18
  *
19
19
  * For named profiles the generated settings encode the profile's selection
20
- * per the filtering model (see docs/architecture/overview.md):
20
+ * over user-scope resources only (see docs/architecture/overview.md):
21
21
  * - agentDir-scope resources: additive allowlist paths (the discovery root
22
22
  * moved, so nothing auto-discovered from the real agent dir)
23
23
  * - `~/.agents` skills: always auto-discovered, so unselected ones are
24
24
  * force-excluded with `-<path>` entries
25
25
  * - packages: user-configured package entries rewritten to object form with
26
26
  * per-type allowlists (unmanaged types keep the user's key or Pi's default)
27
- * - `defaultProjectTrust: "never"` suppresses all project auto-discovery
28
- * (project resources enter only through the trust-gated resolver)
29
- * - project `packages` are stripped from the settings merge (project
30
- * packages are unsupported — the key would install into the global npm
31
- * root as a launch side effect)
27
+ * - project-scope resources (project `.pi/skills`, project `.pi/extensions`,
28
+ * ancestor `.agents/skills`) are never encoded: Pi discovers them natively
29
+ * whenever the project is trusted, and the profile neither adds nor
30
+ * excludes them
31
+ * - `defaultProjectTrust: "never"` only suppresses Pi's interactive trust
32
+ * prompt (a stored decision in the real trust.json still applies); the
33
+ * project's `.pi/settings.json` is not merged here — Pi reads it natively
32
34
  * - unmanaged kinds (prompts, themes) pass through: the user's arrays are
33
35
  * preserved and the real agent dir's prompts/themes dirs re-included
34
36
  * - tools/model are written to generated settings (defaultTools,
@@ -74,11 +76,6 @@ export interface GenerateOptions {
74
76
  projectDir?: string;
75
77
  /** Required for selection plans; unused for the default profile. */
76
78
  discovery?: DiscoveryContext;
77
- /** The trusted project's `.pi/settings.json` content (already parsed).
78
- * Only pass when the resolver's trust check passed; merged into the
79
- * generated base per Pi's merge rules for selection plans. Ignored for
80
- * the default profile (Pi reads project settings natively there). */
81
- projectSettings?: Record<string, unknown>;
82
79
  }
83
80
 
84
81
  export interface GeneratedRuntime {
@@ -150,21 +147,6 @@ function toPosix(filePath: string): string {
150
147
  return filePath.split(path.sep).join("/");
151
148
  }
152
149
 
153
- /** Mirrors Pi's own deepMergeSettings: plain objects merge recursively,
154
- * everything else (arrays, primitives) is replaced by the override. */
155
- function deepMergeSettings(base: Record<string, unknown>, overrides: Record<string, unknown>): Record<string, unknown> {
156
- const result: Record<string, unknown> = { ...base };
157
- for (const [key, overrideValue] of Object.entries(overrides)) {
158
- if (overrideValue === undefined) continue;
159
- const baseValue = result[key];
160
- result[key] =
161
- isRecord(baseValue) && isRecord(overrideValue)
162
- ? deepMergeSettings(baseValue, overrideValue)
163
- : overrideValue;
164
- }
165
- return result;
166
- }
167
-
168
150
  function tryRealpath(p: string): string {
169
151
  try {
170
152
  return realpathSync(p);
@@ -194,27 +176,28 @@ function buildSelectionSettings(
194
176
  agentDir: string,
195
177
  discovery: DiscoveryContext,
196
178
  runtimeDir: string,
179
+ projectDir?: string,
197
180
  ): Record<string, unknown> {
198
181
  const settings = { ...userSettings };
199
182
 
200
183
  // --- skills ---
201
- // Project-scope selections are emitted before user-scope ones: Pi's
202
- // same-name collision rule is first-wins, and project resources must keep
203
- // their native priority (ticket 03).
204
- const orderedSelectedSkills = [...plan.skills].sort((a, b) => {
205
- const aProject = a.scope === "project" ? 0 : 1;
206
- const bProject = b.scope === "project" ? 0 : 1;
207
- return aProject - bProject;
208
- });
184
+ // Only user-scope entries are encoded. Project-scope selections are
185
+ // skipped below: their visibility is Pi's, so the order they would have
186
+ // been emitted in carries no meaning.
209
187
  const selectedPaths = new Set(plan.skills.map((skill) => skill.filePath));
210
188
  const skillEntries: string[] = [];
211
- for (const skill of orderedSelectedSkills) {
189
+ for (const skill of plan.skills) {
212
190
  if (skill.origin === "package") continue; // encoded in the packages allowlist
191
+ // Project scope belongs to Pi: a trusted project's skills are discovered
192
+ // natively, so selecting one here would duplicate it and excluding one
193
+ // would contradict the profile's boundary.
194
+ if (skill.scope === "project") continue;
213
195
  if (isUnderPath(skill.filePath, homeAgentsSkillsDir())) continue; // auto-discovered anyway
214
196
  skillEntries.push(skill.filePath);
215
197
  }
216
198
  for (const skill of discovery.skills) {
217
199
  if (skill.origin === "package") continue;
200
+ if (skill.scope === "project") continue;
218
201
  if (selectedPaths.has(skill.filePath)) continue;
219
202
 
220
203
  // If the skill is in the real agentDir, Pi will discover it via the symlink.
@@ -240,7 +223,13 @@ function buildSelectionSettings(
240
223
  .map((pkg) => ({ ...pkg, root: pkg.root }));
241
224
  const packageExtensions = new Map<string, string[]>();
242
225
  const extensionEntries: string[] = [];
226
+ const projectExtensionsDir =
227
+ projectDir !== undefined ? path.join(projectDir, ".pi", "extensions") : undefined;
243
228
  for (const extension of plan.extensions) {
229
+ // Loose project extensions are discovered natively by Pi; an explicit
230
+ // path reference inside the project (outside `.pi/extensions`) is the
231
+ // profile's own selection and stays.
232
+ if (projectExtensionsDir !== undefined && isUnderPath(extension.entry, projectExtensionsDir)) continue;
244
233
  const owner = packageRoots.find((pkg) => isUnderPath(extension.entry, pkg.root));
245
234
  if (owner === undefined) {
246
235
  extensionEntries.push(extension.entry);
@@ -316,8 +305,6 @@ export interface RuntimeFileOptions {
316
305
  projectDir?: string;
317
306
  /** Required for selection plans; unused for the default profile. */
318
307
  discovery?: DiscoveryContext;
319
- /** The trusted project's `.pi/settings.json` content (already parsed). */
320
- projectSettings?: Record<string, unknown>;
321
308
  /** Extra launch-plan fields written by the in-session switch path:
322
309
  * `switchedFrom` triggers the one-shot change summary; `persistSelection`
323
310
  * tells the post-reload extension instance to save the selection;
@@ -363,21 +350,20 @@ async function computeSettings(
363
350
  return settings;
364
351
  }
365
352
 
366
- // Selection plans: the trusted project's settings merge into the base
367
- // per Pi's merge rules (project wins, nested objects merge), then the
368
- // filtering encoding replaces the managed keys on top. With
369
- // defaultProjectTrust: "never", Pi itself never reads project settings.
370
- //
371
- // The project's `packages` key is stripped: project packages install
372
- // under the project's .pi/npm and are unreferenceable in generated
373
- // global-scope settings — merging the key would make Pi install them
374
- // into the (symlinked) global npm root as a launch side effect.
375
- let base = { ...userSettings };
376
- if (options.projectSettings !== undefined) {
377
- const { packages: _stripped, ...mergeable } = options.projectSettings;
378
- base = deepMergeSettings(base, mergeable);
379
- }
380
- return buildSelectionSettings(plan, base, agentDir, options.discovery ?? { skills: [], packages: [] }, runtimeDir);
353
+ // Selection plans: only user-scope encoding is layered onto the user's own
354
+ // settings. The trusted project's `.pi/settings.json` is deliberately NOT
355
+ // merged here — Pi reads it natively for the same trust decision this
356
+ // process's Pi applies, and merging it would turn the project's `packages`
357
+ // into global-scope packages (installing them into the real agent dir's npm
358
+ // root as a launch side effect).
359
+ return buildSelectionSettings(
360
+ plan,
361
+ { ...userSettings },
362
+ agentDir,
363
+ options.discovery ?? { skills: [], packages: [] },
364
+ runtimeDir,
365
+ options.projectDir,
366
+ );
381
367
  }
382
368
 
383
369
  /** Resolved name sets, carried in the launch plan for glob-delta reporting. */
@@ -389,10 +375,9 @@ export interface ResolvedNames {
389
375
  }
390
376
 
391
377
  /** Writes settings.json + pi-profile.json into an existing runtime dir and
392
- * transitions the trust.json link to the plan's filter mode: linked for
393
- * `default` (native trust behavior), absent for named profiles (a stored
394
- * trust decision would beat the generated `defaultProjectTrust: "never"`
395
- * inside Pi and re-enable unfiltered project auto-discovery). */
378
+ * keeps the trust.json link in place for every profile: Pi reads its
379
+ * project-scope decision from that path, and project-level resources belong
380
+ * to Pi's trust gate rather than to the profile. */
396
381
  export async function writeRuntimeFiles(
397
382
  runtimeDir: string,
398
383
  plan: ActivationPlan,
@@ -433,18 +418,14 @@ export async function writeRuntimeFiles(
433
418
  )}\n`,
434
419
  );
435
420
 
421
+ // Every profile gets the link, dangling allowed: Pi's stored trust decision
422
+ // is what makes a trusted project's resources visible, and a decision Pi
423
+ // writes through the link must land in the real agent dir (same shape as the
424
+ // auth.json seed in ADR-0010). An entry that already exists is left alone —
425
+ // a real file Pi wrote during this session carries its own decision.
436
426
  const trustLink = path.join(runtimeDir, "trust.json");
437
- const trustTarget = path.join(options.agentDir, "trust.json");
438
- if (plan.filter === "none") {
439
- if ((await exists(trustTarget)) && !(await existsLexical(trustLink))) {
440
- await symlink(trustTarget, trustLink);
441
- }
442
- } else if (await existsLexical(trustLink)) {
443
- // Lexical check: a dangling trust.json symlink (real trust.json deleted
444
- // after the link was made) must still be removed — otherwise a later
445
- // re-created real trust.json silently resurrects stored trust inside a
446
- // named profile, defeating defaultProjectTrust: "never".
447
- await rm(trustLink);
427
+ if (!(await existsLexical(trustLink))) {
428
+ await symlink(path.join(options.agentDir, "trust.json"), trustLink);
448
429
  }
449
430
 
450
431
  // MCP Servers generation (Ticket 04)
@@ -459,7 +440,7 @@ export async function writeRuntimeFiles(
459
440
  } else {
460
441
  // Filter MCP servers
461
442
  try { await rm(mcpInstancePath); } catch {}
462
- const { servers, sharedServers, baseConfig } = await loadMergedMcpServers(
443
+ const { servers, sharedServers, projectServers, baseConfig } = await loadMergedMcpServers(
463
444
  options.agentDir,
464
445
  options.projectDir,
465
446
  options.homeDir !== undefined ? { homeDir: options.homeDir } : undefined,
@@ -478,6 +459,9 @@ export async function writeRuntimeFiles(
478
459
 
479
460
  for (const sharedName of sharedServers) {
480
461
  if (!allowedSet.has(sharedName)) {
462
+ // Project-level servers are not the profile's to narrow (the same
463
+ // boundary as project skills and extensions).
464
+ if (projectServers.has(sharedName)) continue;
481
465
  filteredServers[sharedName] = { disabled: true };
482
466
  }
483
467
  }
@@ -44,9 +44,9 @@ export interface DiscoverSkillsOptions {
44
44
  }
45
45
 
46
46
  export async function discoverSkills(options: DiscoverSkillsOptions): Promise<SkillEntry[]> {
47
- // Project trust comes from the caller's trust check; generated settings
48
- // carry defaultProjectTrust: "never", so discovery is the only place
49
- // project resources can enter a plan.
47
+ // Project trust comes from the caller's trust check: discovery is the only
48
+ // place project resources enter a plan's reference vocabulary (their
49
+ // visibility in the session is Pi's, not the plan's).
50
50
  const settingsManager = SettingsManager.create(options.cwd, options.agentDir, {
51
51
  projectTrusted: options.projectTrusted ?? false,
52
52
  });
@@ -73,10 +73,10 @@ export async function discoverSkills(options: DiscoverSkillsOptions): Promise<Sk
73
73
  else process.env.PI_OFFLINE = savedOffline;
74
74
  }
75
75
  return loader.getSkills().skills.flatMap((skill) => {
76
- // Project-scoped package skills are excluded: their packages install
77
- // under the project's .pi/npm, which generated global-scope settings
78
- // cannot reference. Project .pi/skills and ancestor .agents/skills are
79
- // unaffected. (Limitation documented in ticket 03's comments.)
76
+ // Project-scoped package skills stay out of the reference vocabulary:
77
+ // their packages live under the project's .pi/npm and Pi discovers their
78
+ // skills natively, so a profile reference would add nothing. Project
79
+ // .pi/skills and ancestor .agents/skills are referenceable.
80
80
  if (skill.sourceInfo.origin === "package" && skill.sourceInfo.scope === "project") {
81
81
  return [];
82
82
  }
@@ -204,7 +204,6 @@ export async function switchProfile(
204
204
  agentDir: deps.realAgentDir,
205
205
  projectDir: resolved.projectDir,
206
206
  discovery: resolved.discovery,
207
- projectSettings: resolved.projectSettings,
208
207
  planExtras: {
209
208
  ...(isSwitch && current.profile !== undefined ? { switchedFrom: current.profile } : {}),
210
209
  // `/profile use` persists; `/profile reload` keeps the current