privateer-agent 0.10.0 → 0.12.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.
@@ -1,3 +1,72 @@
1
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/config.js b/node_modules/@earendil-works/pi-coding-agent/dist/config.js
2
+ index 4600b23..075ecae 100644
3
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/config.js
4
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/config.js
5
+ @@ -393,6 +393,64 @@ export const APP_NAME = piConfigName || "pi";
6
+ export const APP_TITLE = piConfigName ? APP_NAME : "π";
7
+ export const CONFIG_DIR_NAME = pkg.piConfig?.configDir || ".pi";
8
+ export const VERSION = pkg.version || "0.0.0";
9
+ +// --- Privateer patch: dual project config dirs -------------------------------
10
+ +//
11
+ +// Stock Pi resolves EVERY project-scoped path as `<cwd>/CONFIG_DIR_NAME/...`, where
12
+ +// CONFIG_DIR_NAME is a build-time constant read from this package's own package.json
13
+ +// (`piConfig.configDir`, default ".pi"). PI_CODING_AGENT_DIR moves only the USER dir,
14
+ +// which is why Privateer's global config lives in ~/.privateer/agent while every
15
+ +// project still had to spell its config `.pi/` — a directory named after a tool the
16
+ +// user never installed.
17
+ +//
18
+ +// We do not rename it (that would orphan every existing `.pi/` folder and break
19
+ +// interop with stock Pi). Instead BOTH names are live, `.privateer` taking precedence:
20
+ +//
21
+ +// read — every project dir that exists is searched, `.privateer` first; on a
22
+ +// conflict (same settings key, same skill/prompt/theme/extension name,
23
+ +// SYSTEM.md) the `.privateer` copy wins and the `.pi` one is ignored.
24
+ +// write — new project config is always created under `.privateer/`. Package
25
+ +// payloads (npm/, git/) stay in whichever dir already holds them, so an
26
+ +// existing `.pi/npm` tree is reused rather than re-downloaded.
27
+ +//
28
+ +// Precedence order, highest first. `.pi` is filtered out if a fork ever renames
29
+ +// CONFIG_DIR_NAME to `.privateer`, so the list never holds duplicates.
30
+ +export const PROJECT_CONFIG_DIR_NAMES = [".privateer", CONFIG_DIR_NAME].filter((name, i, all) => all.indexOf(name) === i);
31
+ +/** Every candidate project config dir under cwd, highest precedence first (may not exist). */
32
+ +export function projectConfigDirCandidates(cwd) {
33
+ + return PROJECT_CONFIG_DIR_NAMES.map((name) => join(cwd, name));
34
+ +}
35
+ +/** The project config dirs that actually exist under cwd, highest precedence first. */
36
+ +export function projectConfigDirs(cwd) {
37
+ + return projectConfigDirCandidates(cwd).filter((dir) => existsSync(dir));
38
+ +}
39
+ +/** Where NEW project config is written: always `.privateer/`. */
40
+ +export function projectConfigWriteDir(cwd) {
41
+ + return join(cwd, PROJECT_CONFIG_DIR_NAMES[0]);
42
+ +}
43
+ +/**
44
+ + * The highest-precedence project dir that already contains `...rel`, or undefined.
45
+ + * Use for "one file wins" lookups (settings.json, SYSTEM.md, an npm/ payload root).
46
+ + */
47
+ +export function findProjectConfigPath(cwd, ...rel) {
48
+ + for (const dir of projectConfigDirCandidates(cwd)) {
49
+ + const candidate = join(dir, ...rel);
50
+ + if (existsSync(candidate))
51
+ + return candidate;
52
+ + }
53
+ + return undefined;
54
+ +}
55
+ +/**
56
+ + * Where a project-scoped `...rel` lives: the existing copy if there is one, else the
57
+ + * path it would be created at under `.privateer/`.
58
+ + */
59
+ +export function resolveProjectConfigPath(cwd, ...rel) {
60
+ + return findProjectConfigPath(cwd, ...rel) ?? join(projectConfigWriteDir(cwd), ...rel);
61
+ +}
62
+ +/** Human-readable list of the project dir names, for prompts and help text. */
63
+ +export function formatProjectConfigDirNames() {
64
+ + return PROJECT_CONFIG_DIR_NAMES.map((n) => `${n}/`).join(" or ");
65
+ +}
66
+ +// --- end Privateer patch -----------------------------------------------------
67
+ // e.g., PI_CODING_AGENT_DIR or TAU_CODING_AGENT_DIR
68
+ export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
69
+ export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`;
1
70
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
2
71
  index e223ce1..cc853e7 100644
3
72
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
@@ -53,7 +122,7 @@ index e223ce1..cc853e7 100644
53
122
  }
54
123
  /**
55
124
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-guidance.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-guidance.js
56
- index 197bccc..27f6429 100644
125
+ index 197bccc..bc9ac3f 100644
57
126
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-guidance.js
58
127
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-guidance.js
59
128
  @@ -1,21 +1,33 @@
@@ -96,11 +165,773 @@ index 197bccc..27f6429 100644
96
165
  + return [join(getDocsPath(), "providers.md"), join(getDocsPath(), "models.md")];
97
166
  +}
98
167
  //# sourceMappingURL=auth-guidance.js.map
168
+
169
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js
170
+ index 4b79482..a81b452 100644
171
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js
172
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js
173
+ @@ -17,7 +17,7 @@ import { createJiti } from "jiti/static";
174
+ import * as _bundledTypebox from "typebox";
175
+ import * as _bundledTypeboxCompile from "typebox/compile";
176
+ import * as _bundledTypeboxValue from "typebox/value";
177
+ -import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.js";
178
+ +import { getAgentDir, isBunBinary, projectConfigDirCandidates } from "../../config.js";
179
+ // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
180
+ // avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent.
181
+ import * as _bundledPiCodingAgent from "../../index.js";
182
+ @@ -508,9 +508,23 @@ export async function discoverAndLoadExtensions(configuredPaths, cwd, agentDir =
183
+ }
184
+ }
185
+ };
186
+ - // 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/
187
+ - const localExtDir = path.join(resolvedCwd, CONFIG_DIR_NAME, "extensions");
188
+ - addPaths(discoverExtensionsInDir(localExtDir));
189
+ + // 1. Project-local extensions: cwd/.privateer/extensions/, then cwd/.pi/extensions/
190
+ + //
191
+ + // Privateer patch: both project dirs are discovered, `.privateer` first. Extensions
192
+ + // are files, so a same-named file in the lower-precedence dir would otherwise load
193
+ + // as a SECOND copy of the same extension — skip it by basename, which is what makes
194
+ + // `.privateer` genuinely supersede `.pi` here rather than merely precede it.
195
+ + const seenLocalNames = new Set();
196
+ + for (const projectDir of projectConfigDirCandidates(resolvedCwd)) {
197
+ + const discovered = discoverExtensionsInDir(path.join(projectDir, "extensions"));
198
+ + addPaths(discovered.filter((p) => {
199
+ + const name = path.basename(p);
200
+ + if (seenLocalNames.has(name))
201
+ + return false;
202
+ + seenLocalNames.add(name);
203
+ + return true;
204
+ + }));
205
+ + }
206
+ // 2. Global extensions: agentDir/extensions/
207
+ const globalExtDir = path.join(resolvedAgentDir, "extensions");
208
+ addPaths(discoverExtensionsInDir(globalExtDir));
209
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.js
210
+ index a125c9a..5ca0ccc 100644
211
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.js
212
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.js
213
+ @@ -25,7 +25,7 @@ import { globSync } from "glob";
214
+ import ignore from "ignore";
215
+ import { minimatch } from "minimatch";
216
+ import { maxSatisfying, rcompare, satisfies, valid, validRange } from "semver";
217
+ -import { CONFIG_DIR_NAME } from "../config.js";
218
+ +import { projectConfigDirs, projectConfigWriteDir, resolveProjectConfigPath } from "../config.js";
219
+ import { spawnProcess, spawnProcessSync } from "../utils/child-process.js";
220
+ import { parseGitUrl } from "../utils/git.js";
221
+ import { canonicalizePath, isLocalPath, markPathIgnoredByCloudSync, resolvePath } from "../utils/paths.js";
222
+ @@ -693,23 +693,29 @@ export class DefaultPackageManager {
223
+ const packageSources = this.dedupePackages(allPackages);
224
+ await this.resolvePackageSources(packageSources, accumulator, onMissing);
225
+ const globalBaseDir = this.agentDir;
226
+ - const projectBaseDir = join(this.cwd, CONFIG_DIR_NAME);
227
+ + // Privateer patch: relative entries in project settings are resolved against
228
+ + // every project config dir, `.privateer` first. Paths that don't exist under a
229
+ + // given dir are skipped silently (collectFilesFromPaths), so a `.pi`-relative
230
+ + // entry still resolves when `.privateer` exists but doesn't contain it.
231
+ + const projectBaseDirs = this.projectBaseDirs();
232
+ for (const resourceType of RESOURCE_TYPES) {
233
+ const target = this.getTargetMap(accumulator, resourceType);
234
+ const globalEntries = (globalSettings[resourceType] ?? []);
235
+ const projectEntries = (projectSettings[resourceType] ?? []);
236
+ - this.resolveLocalEntries(projectEntries, resourceType, target, {
237
+ - source: "local",
238
+ - scope: "project",
239
+ - origin: "top-level",
240
+ - }, projectBaseDir);
241
+ + for (const projectBaseDir of projectBaseDirs) {
242
+ + this.resolveLocalEntries(projectEntries, resourceType, target, {
243
+ + source: "local",
244
+ + scope: "project",
245
+ + origin: "top-level",
246
+ + }, projectBaseDir);
247
+ + }
248
+ this.resolveLocalEntries(globalEntries, resourceType, target, {
249
+ source: "local",
250
+ scope: "user",
251
+ origin: "top-level",
252
+ }, globalBaseDir);
253
+ }
254
+ - this.addAutoDiscoveredResources(accumulator, globalSettings, projectSettings, globalBaseDir, projectBaseDir);
255
+ + this.addAutoDiscoveredResources(accumulator, globalSettings, projectSettings, globalBaseDir, projectBaseDirs);
256
+ return this.toResolvedPaths(accumulator);
257
+ }
258
+ async resolveExtensionSources(sources, options) {
259
+ @@ -1586,13 +1592,30 @@ export class DefaultPackageManager {
260
+ writeFileSync(ignorePath, "*\n!.gitignore\n", "utf-8");
261
+ }
262
+ }
263
+ + // --- Privateer patch: project config dirs -------------------------------
264
+ + /**
265
+ + * Every project config dir that exists under cwd, `.privateer` before `.pi`.
266
+ + * Falls back to the write dir so callers always have a base to resolve against.
267
+ + */
268
+ + projectBaseDirs() {
269
+ + const dirs = projectConfigDirs(this.cwd);
270
+ + return dirs.length > 0 ? dirs : [projectConfigWriteDir(this.cwd)];
271
+ + }
272
+ + /** An existing project-scoped payload path, else the path to create under `.privateer`. */
273
+ + projectPayloadPath(...rel) {
274
+ + return resolveProjectConfigPath(this.cwd, ...rel);
275
+ + }
276
+ + // --- end Privateer patch -------------------------------------------------
277
+ getNpmInstallRoot(scope, temporary) {
278
+ if (temporary) {
279
+ return this.getTemporaryDir("npm");
280
+ }
281
+ if (scope === "project") {
282
+ this.assertProjectTrustedForScope(scope);
283
+ - return join(this.cwd, CONFIG_DIR_NAME, "npm");
284
+ + // Privateer patch: reuse an existing `.pi/npm` tree rather than
285
+ + // re-downloading everything under `.privateer/npm`; only a project with no
286
+ + // payload tree at all gets one created under `.privateer`.
287
+ + return this.projectPayloadPath("npm");
288
+ }
289
+ return join(this.agentDir, "npm");
290
+ }
291
+ @@ -1631,7 +1654,9 @@ export class DefaultPackageManager {
292
+ }
293
+ if (scope === "project") {
294
+ this.assertProjectTrustedForScope(scope);
295
+ - return join(this.cwd, CONFIG_DIR_NAME, "npm", "node_modules", source.name);
296
+ + // Privateer patch: an already-installed package is found in whichever
297
+ + // project dir holds it; a new one lands in the payload root above.
298
+ + return this.projectPayloadPath("npm", "node_modules", source.name);
299
+ }
300
+ return join(this.agentDir, "npm", "node_modules", source.name);
301
+ }
302
+ @@ -1667,7 +1692,7 @@ export class DefaultPackageManager {
303
+ }
304
+ if (scope === "project") {
305
+ this.assertProjectTrustedForScope(scope);
306
+ - return join(this.cwd, CONFIG_DIR_NAME, "git");
307
+ + return this.projectPayloadPath("git");
308
+ }
309
+ return join(this.agentDir, "git");
310
+ }
311
+ @@ -1690,7 +1715,7 @@ export class DefaultPackageManager {
312
+ getBaseDirForScope(scope) {
313
+ if (scope === "project") {
314
+ this.assertProjectTrustedForScope(scope);
315
+ - return join(this.cwd, CONFIG_DIR_NAME);
316
+ + return this.projectBaseDirs()[0];
317
+ }
318
+ if (scope === "user") {
319
+ return this.agentDir;
320
+ @@ -1701,7 +1726,30 @@ export class DefaultPackageManager {
321
+ return resolvePath(input, this.cwd, { homeDir: getHomeDir(), trim: true });
322
+ }
323
+ resolvePathFromBase(input, baseDir) {
324
+ - return resolvePath(input, baseDir, { homeDir: getHomeDir(), trim: true });
325
+ + const resolved = resolvePath(input, baseDir, { homeDir: getHomeDir(), trim: true });
326
+ + // Privateer patch: a relative entry lives relative to the settings file that
327
+ + // declared it, but the merged project scope has only ONE base — `.privateer`
328
+ + // once it exists. Without this fallback a `./foo` package or skill declared in
329
+ + // `.pi/settings.json` resolves to a non-existent `.privateer/foo` and silently
330
+ + // disappears. Try the other project config dirs and take the first real hit;
331
+ + // absolute entries and genuine typos are unaffected (both fall through).
332
+ + if (existsSync(resolved)) {
333
+ + return resolved;
334
+ + }
335
+ + const projectDirs = this.projectBaseDirs();
336
+ + if (!projectDirs.some((dir) => resolve(dir) === resolve(baseDir))) {
337
+ + return resolved;
338
+ + }
339
+ + for (const dir of projectDirs) {
340
+ + if (resolve(dir) === resolve(baseDir)) {
341
+ + continue;
342
+ + }
343
+ + const alternate = resolvePath(input, dir, { homeDir: getHomeDir(), trim: true });
344
+ + if (existsSync(alternate)) {
345
+ + return alternate;
346
+ + }
347
+ + }
348
+ + return resolved;
349
+ }
350
+ collectPackageResources(packageRoot, accumulator, filter, metadata) {
351
+ if (filter) {
352
+ @@ -1847,19 +1895,24 @@ export class DefaultPackageManager {
353
+ this.addResource(target, f, metadata, enabledPaths.has(f));
354
+ }
355
+ }
356
+ - addAutoDiscoveredResources(accumulator, globalSettings, projectSettings, globalBaseDir, projectBaseDir) {
357
+ + // Privateer patch: `projectBaseDirs` is every project config dir that exists,
358
+ + // highest precedence first (`.privateer` before `.pi`). Auto-discovery runs once
359
+ + // per dir; same-named resources collide downstream where the first one wins, so
360
+ + // `.privateer` supersedes `.pi`.
361
+ + addAutoDiscoveredResources(accumulator, globalSettings, projectSettings, globalBaseDir, projectBaseDirs) {
362
+ const userMetadata = {
363
+ source: "auto",
364
+ scope: "user",
365
+ origin: "top-level",
366
+ baseDir: globalBaseDir,
367
+ };
368
+ - const projectMetadata = {
369
+ + const projectMetadataFor = (baseDir) => ({
370
+ source: "auto",
371
+ scope: "project",
372
+ origin: "top-level",
373
+ - baseDir: projectBaseDir,
374
+ - };
375
+ + baseDir,
376
+ + });
377
+ + const projectMetadata = projectMetadataFor(projectBaseDirs[0]);
378
+ const userOverrides = {
379
+ extensions: (globalSettings.extensions ?? []),
380
+ skills: (globalSettings.skills ?? []),
381
+ @@ -1878,12 +1931,12 @@ export class DefaultPackageManager {
382
+ prompts: join(globalBaseDir, "prompts"),
383
+ themes: join(globalBaseDir, "themes"),
384
+ };
385
+ - const projectDirs = {
386
+ - extensions: join(projectBaseDir, "extensions"),
387
+ - skills: join(projectBaseDir, "skills"),
388
+ - prompts: join(projectBaseDir, "prompts"),
389
+ - themes: join(projectBaseDir, "themes"),
390
+ - };
391
+ + const projectDirsFor = (baseDir) => ({
392
+ + extensions: join(baseDir, "extensions"),
393
+ + skills: join(baseDir, "skills"),
394
+ + prompts: join(baseDir, "prompts"),
395
+ + themes: join(baseDir, "themes"),
396
+ + });
397
+ const userAgentsSkillsDir = join(getHomeDir(), ".agents", "skills");
398
+ const projectTrusted = this.settingsManager.isProjectTrusted();
399
+ const projectAgentsSkillDirs = projectTrusted
400
+ @@ -1897,10 +1950,13 @@ export class DefaultPackageManager {
401
+ }
402
+ };
403
+ if (projectTrusted) {
404
+ - // Project extensions from .pi/
405
+ - addResources("extensions", collectAutoExtensionEntries(projectDirs.extensions), projectMetadata, projectOverrides.extensions, projectBaseDir);
406
+ - // Project skills from .pi/
407
+ - addResources("skills", collectAutoSkillEntries(projectDirs.skills, "pi"), projectMetadata, projectOverrides.skills, projectBaseDir);
408
+ + for (const projectBaseDir of projectBaseDirs) {
409
+ + const projectDirs = projectDirsFor(projectBaseDir);
410
+ + // Project extensions from .privateer/ then .pi/
411
+ + addResources("extensions", collectAutoExtensionEntries(projectDirs.extensions), projectMetadataFor(projectBaseDir), projectOverrides.extensions, projectBaseDir);
412
+ + // Project skills from .privateer/ then .pi/
413
+ + addResources("skills", collectAutoSkillEntries(projectDirs.skills, "pi"), projectMetadataFor(projectBaseDir), projectOverrides.skills, projectBaseDir);
414
+ + }
415
+ }
416
+ // Project skills from .agents/ (each with its own baseDir)
417
+ for (const agentsSkillsDir of projectAgentsSkillDirs) {
418
+ @@ -1912,8 +1968,11 @@ export class DefaultPackageManager {
419
+ addResources("skills", collectAutoSkillEntries(agentsSkillsDir, "agents"), agentsMetadata, projectOverrides.skills, agentsBaseDir);
420
+ }
421
+ if (projectTrusted) {
422
+ - addResources("prompts", collectAutoPromptEntries(projectDirs.prompts), projectMetadata, projectOverrides.prompts, projectBaseDir);
423
+ - addResources("themes", collectAutoThemeEntries(projectDirs.themes), projectMetadata, projectOverrides.themes, projectBaseDir);
424
+ + for (const projectBaseDir of projectBaseDirs) {
425
+ + const projectDirs = projectDirsFor(projectBaseDir);
426
+ + addResources("prompts", collectAutoPromptEntries(projectDirs.prompts), projectMetadataFor(projectBaseDir), projectOverrides.prompts, projectBaseDir);
427
+ + addResources("themes", collectAutoThemeEntries(projectDirs.themes), projectMetadataFor(projectBaseDir), projectOverrides.themes, projectBaseDir);
428
+ + }
429
+ }
430
+ // User extensions from ~/.pi/agent/
431
+ addResources("extensions", collectAutoExtensionEntries(userDirs.extensions), userMetadata, userOverrides.extensions, globalBaseDir);
432
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/project-trust.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/project-trust.js
433
+ index c4f2509..a4402df 100644
434
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/project-trust.js
435
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/project-trust.js
436
+ @@ -1,8 +1,8 @@
437
+ -import { CONFIG_DIR_NAME } from "../config.js";
438
+ +import { formatProjectConfigDirNames } from "../config.js";
439
+ import { emitProjectTrustEvent } from "./extensions/runner.js";
440
+ import { getProjectTrustOptions, hasTrustRequiringProjectResources, } from "./trust-manager.js";
441
+ function formatProjectTrustPrompt(cwd) {
442
+ - return `Trust project folder?\n${cwd}\n\nThis allows pi to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`;
443
+ + return `Trust project folder?\n${cwd}\n\nThis allows pi to load ${formatProjectConfigDirNames()} settings and resources, install missing project packages, and execute project extensions.`;
444
+ }
445
+ async function selectProjectTrustOption(cwd, ctx) {
446
+ const options = getProjectTrustOptions(cwd, { includeSessionOnly: true });
447
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/prompt-templates.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/prompt-templates.js
448
+ index ae4a7d4..a251c37 100644
449
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/prompt-templates.js
450
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/prompt-templates.js
451
+ @@ -1,6 +1,6 @@
452
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
453
+ import { basename, dirname, join, resolve, sep } from "path";
454
+ -import { CONFIG_DIR_NAME } from "../config.js";
455
+ +import { projectConfigDirCandidates } from "../config.js";
456
+ import { parseFrontmatter } from "../utils/frontmatter.js";
457
+ import { resolvePath } from "../utils/paths.js";
458
+ import { createSyntheticSourceInfo } from "./source-info.js";
459
+ @@ -157,7 +157,8 @@ export function loadPromptTemplates(options) {
460
+ const includeDefaults = options.includeDefaults;
461
+ const templates = [];
462
+ const globalPromptsDir = join(resolvedAgentDir, "prompts");
463
+ - const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts");
464
+ + // Privateer patch: `.privateer/prompts` then `.pi/prompts`.
465
+ + const projectPromptsDirs = projectConfigDirCandidates(resolvedCwd).map((dir) => resolve(dir, "prompts"));
466
+ const isUnderPath = (target, root) => {
467
+ const normalizedRoot = resolve(root);
468
+ if (target === normalizedRoot) {
469
+ @@ -174,7 +175,8 @@ export function loadPromptTemplates(options) {
470
+ baseDir: globalPromptsDir,
471
+ });
472
+ }
473
+ - if (isUnderPath(resolvedPath, projectPromptsDir)) {
474
+ + const projectPromptsDir = projectPromptsDirs.find((dir) => isUnderPath(resolvedPath, dir));
475
+ + if (projectPromptsDir) {
476
+ return createSyntheticSourceInfo(resolvedPath, {
477
+ source: "local",
478
+ scope: "project",
479
+ @@ -188,7 +190,9 @@ export function loadPromptTemplates(options) {
480
+ };
481
+ if (includeDefaults) {
482
+ templates.push(...loadTemplatesFromDir(globalPromptsDir, getSourceInfo));
483
+ - templates.push(...loadTemplatesFromDir(projectPromptsDir, getSourceInfo));
484
+ + for (const projectPromptsDir of projectPromptsDirs) {
485
+ + templates.push(...loadTemplatesFromDir(projectPromptsDir, getSourceInfo));
486
+ + }
487
+ }
488
+ // 3. Load explicit prompt paths
489
+ for (const rawPath of promptPaths) {
490
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js
491
+ index 74e36fe..7dfef45 100644
492
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js
493
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js
494
+ @@ -1,7 +1,7 @@
495
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
496
+ import { join, resolve, sep } from "node:path";
497
+ import chalk from "chalk";
498
+ -import { CONFIG_DIR_NAME } from "../config.js";
499
+ +import { findProjectConfigPath, projectConfigDirCandidates, projectConfigDirs } from "../config.js";
500
+ import { loadThemeFromPath } from "../modes/interactive/theme/theme.js";
501
+ import { canonicalizePath, isLocalPath, resolvePath } from "../utils/paths.js";
502
+ import { createEventBus } from "./event-bus.js";
503
+ @@ -570,12 +570,14 @@ export class DefaultResourceLoader {
504
+ join(this.agentDir, "themes"),
505
+ join(this.agentDir, "extensions"),
506
+ ];
507
+ - const projectRoots = [
508
+ - join(this.cwd, CONFIG_DIR_NAME, "skills"),
509
+ - join(this.cwd, CONFIG_DIR_NAME, "prompts"),
510
+ - join(this.cwd, CONFIG_DIR_NAME, "themes"),
511
+ - join(this.cwd, CONFIG_DIR_NAME, "extensions"),
512
+ - ];
513
+ + // Privateer patch: attribute paths under EITHER project config dir to the
514
+ + // project scope, so a `.privateer/skills/…` file isn't reported as temporary.
515
+ + const projectRoots = projectConfigDirCandidates(this.cwd).flatMap((dir) => [
516
+ + join(dir, "skills"),
517
+ + join(dir, "prompts"),
518
+ + join(dir, "themes"),
519
+ + join(dir, "extensions"),
520
+ + ]);
521
+ for (const root of agentRoots) {
522
+ if (this.isUnderPath(normalizedPath, root)) {
523
+ return { path: filePath, source: "local", scope: "user", origin: "top-level", baseDir: root };
524
+ @@ -614,7 +616,11 @@ export class DefaultResourceLoader {
525
+ const themes = [];
526
+ const diagnostics = [];
527
+ if (includeDefaults) {
528
+ - const defaultDirs = [join(this.agentDir, "themes"), join(this.cwd, CONFIG_DIR_NAME, "themes")];
529
+ + // Privateer patch: `.privateer/themes` first, then `.pi/themes`.
530
+ + const defaultDirs = [
531
+ + join(this.agentDir, "themes"),
532
+ + ...projectConfigDirs(this.cwd).map((dir) => join(dir, "themes")),
533
+ + ];
534
+ for (const dir of defaultDirs) {
535
+ this.loadThemesFromDir(dir, themes, diagnostics);
536
+ }
537
+ @@ -749,8 +755,9 @@ export class DefaultResourceLoader {
538
+ return { themes: Array.from(seen.values()), diagnostics };
539
+ }
540
+ discoverSystemPromptFile() {
541
+ - const projectPath = join(this.cwd, CONFIG_DIR_NAME, "SYSTEM.md");
542
+ - if (this.settingsManager.isProjectTrusted() && existsSync(projectPath)) {
543
+ + // Privateer patch: `.privateer/SYSTEM.md` wins over `.pi/SYSTEM.md`.
544
+ + const projectPath = findProjectConfigPath(this.cwd, "SYSTEM.md");
545
+ + if (this.settingsManager.isProjectTrusted() && projectPath) {
546
+ return projectPath;
547
+ }
548
+ const globalPath = join(this.agentDir, "SYSTEM.md");
549
+ @@ -760,8 +767,9 @@ export class DefaultResourceLoader {
550
+ return undefined;
551
+ }
552
+ discoverAppendSystemPromptFile() {
553
+ - const projectPath = join(this.cwd, CONFIG_DIR_NAME, "APPEND_SYSTEM.md");
554
+ - if (this.settingsManager.isProjectTrusted() && existsSync(projectPath)) {
555
+ + // Privateer patch: `.privateer/APPEND_SYSTEM.md` wins over `.pi/`.
556
+ + const projectPath = findProjectConfigPath(this.cwd, "APPEND_SYSTEM.md");
557
+ + if (this.settingsManager.isProjectTrusted() && projectPath) {
558
+ return projectPath;
559
+ }
560
+ const globalPath = join(this.agentDir, "APPEND_SYSTEM.md");
561
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js
562
+ index 5910aac..f379c0f 100644
563
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js
564
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js
565
+ @@ -2,7 +2,7 @@ import { randomUUID } from "crypto";
566
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
567
+ import { dirname, join } from "path";
568
+ import lockfile from "proper-lockfile";
569
+ -import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
570
+ +import { getAgentDir, projectConfigDirCandidates, projectConfigWriteDir } from "../config.js";
571
+ import { normalizePath, resolvePath } from "../utils/paths.js";
572
+ import { DEFAULT_HTTP_IDLE_TIMEOUT_MS, parseHttpIdleTimeoutMs } from "./http-dispatcher.js";
573
+ /** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
574
+ @@ -40,14 +40,112 @@ function parseTimeoutSetting(value, settingName) {
575
+ }
576
+ return undefined;
577
+ }
578
+ +/**
579
+ + * Privateer patch: drop the keys of `settings` that are byte-identical to `base`.
580
+ + *
581
+ + * Project settings are read as `.pi/settings.json` ⊕ `.privateer/settings.json`, but
582
+ + * written back to `.privateer` alone. Without this reduction every save would copy the
583
+ + * whole inherited `.pi` file into `.privateer` — two files claiming to own the same
584
+ + * values, silently diverging the first time one is edited. Reducing keeps `.privateer`
585
+ + * holding only what it actually overrides.
586
+ + *
587
+ + * Known limit (shared with stock Pi's global→project layering): a key can be
588
+ + * overridden but not un-set — removing it from `.privateer` lets `.pi`'s value show
589
+ + * through again.
590
+ + */
591
+ +function subtractSettings(settings, base) {
592
+ + const result = {};
593
+ + for (const key of Object.keys(settings)) {
594
+ + const value = settings[key];
595
+ + const baseValue = base[key];
596
+ + if (value === undefined)
597
+ + continue;
598
+ + if (JSON.stringify(value) === JSON.stringify(baseValue))
599
+ + continue;
600
+ + if (typeof value === "object" &&
601
+ + value !== null &&
602
+ + !Array.isArray(value) &&
603
+ + typeof baseValue === "object" &&
604
+ + baseValue !== null &&
605
+ + !Array.isArray(baseValue)) {
606
+ + const nested = subtractSettings(value, baseValue);
607
+ + if (Object.keys(nested).length > 0) {
608
+ + result[key] = nested;
609
+ + }
610
+ + continue;
611
+ + }
612
+ + result[key] = value;
613
+ + }
614
+ + return result;
615
+ +}
616
+ export class FileSettingsStorage {
617
+ globalSettingsPath;
618
+ projectSettingsPath;
619
+ + // Privateer patch: lower-precedence project settings files (`.pi/settings.json`),
620
+ + // highest precedence first. Read and merged underneath projectSettingsPath, never
621
+ + // written to.
622
+ + projectBaseSettingsPaths;
623
+ constructor(cwd, agentDir) {
624
+ const resolvedCwd = resolvePath(cwd);
625
+ const resolvedAgentDir = resolvePath(agentDir);
626
+ this.globalSettingsPath = join(resolvedAgentDir, "settings.json");
627
+ - this.projectSettingsPath = join(resolvedCwd, CONFIG_DIR_NAME, "settings.json");
628
+ + // Privateer patch: writes always land in `.privateer/settings.json`.
629
+ + this.projectSettingsPath = join(projectConfigWriteDir(resolvedCwd), "settings.json");
630
+ + this.projectBaseSettingsPaths = projectConfigDirCandidates(resolvedCwd)
631
+ + .map((dir) => join(dir, "settings.json"))
632
+ + .filter((path) => path !== this.projectSettingsPath);
633
+ + }
634
+ + /**
635
+ + * Privateer patch: the project scope is the MERGE of every project settings file
636
+ + * that exists — `.pi/settings.json` first, `.privateer/settings.json` layered on
637
+ + * top — presented to callers as one document so the rest of Pi is unchanged.
638
+ + * Writes are reduced against the inherited base and land in `.privateer` only.
639
+ + */
640
+ + withProjectLock(fn) {
641
+ + const path = this.projectSettingsPath;
642
+ + let base = {};
643
+ + let sawBase = false;
644
+ + // Lowest precedence first, so higher-precedence files overwrite them.
645
+ + for (const basePath of [...this.projectBaseSettingsPaths].reverse()) {
646
+ + if (!existsSync(basePath))
647
+ + continue;
648
+ + const raw = readFileSync(basePath, "utf-8");
649
+ + if (!raw)
650
+ + continue;
651
+ + // A malformed `.pi/settings.json` throws here exactly as it would in stock
652
+ + // Pi, and is reported by the caller as a project settings load error.
653
+ + base = deepMergeSettings(base, JSON.parse(raw));
654
+ + sawBase = true;
655
+ + }
656
+ + const dir = dirname(path);
657
+ + let release;
658
+ + try {
659
+ + const fileExists = existsSync(path);
660
+ + if (fileExists) {
661
+ + release = this.acquireLockSyncWithRetry(path);
662
+ + }
663
+ + const ownRaw = fileExists ? readFileSync(path, "utf-8") : undefined;
664
+ + const merged = ownRaw ? deepMergeSettings(base, JSON.parse(ownRaw)) : base;
665
+ + // `undefined` means "no project settings at all" — the signal stock callers
666
+ + // use to fall back to an empty document.
667
+ + const current = ownRaw === undefined && !sawBase ? undefined : JSON.stringify(merged, null, 2);
668
+ + const next = fn(current);
669
+ + if (next !== undefined) {
670
+ + const reduced = subtractSettings(JSON.parse(next), base);
671
+ + if (!existsSync(dir)) {
672
+ + mkdirSync(dir, { recursive: true });
673
+ + }
674
+ + if (!release) {
675
+ + release = this.acquireLockSyncWithRetry(path);
676
+ + }
677
+ + writeFileSync(path, JSON.stringify(reduced, null, 2), "utf-8");
678
+ + }
679
+ + }
680
+ + finally {
681
+ + if (release) {
682
+ + release();
683
+ + }
684
+ + }
685
+ }
686
+ acquireLockSyncWithRetry(path) {
687
+ const maxAttempts = 10;
688
+ @@ -74,7 +172,13 @@ export class FileSettingsStorage {
689
+ throw lastError ?? new Error("Failed to acquire settings lock");
690
+ }
691
+ withLock(scope, fn) {
692
+ - const path = scope === "global" ? this.globalSettingsPath : this.projectSettingsPath;
693
+ + // Privateer patch: the project scope spans two directories, so it needs its own
694
+ + // read-merge / write-reduce path.
695
+ + if (scope !== "global") {
696
+ + this.withProjectLock(fn);
697
+ + return;
698
+ + }
699
+ + const path = this.globalSettingsPath;
700
+ const dir = dirname(path);
701
+ let release;
702
+ try {
703
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js
704
+ index 4e7e784..7383a2b 100644
705
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js
706
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js
707
+ @@ -1,7 +1,7 @@
708
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
709
+ import ignore from "ignore";
710
+ import { basename, dirname, join, relative, resolve, sep } from "path";
711
+ -import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
712
+ +import { getAgentDir, projectConfigDirCandidates } from "../config.js";
713
+ import { parseFrontmatter } from "../utils/frontmatter.js";
714
+ import { canonicalizePath, resolvePath } from "../utils/paths.js";
715
+ import { createSyntheticSourceInfo } from "./source-info.js";
716
+ @@ -328,10 +328,15 @@ export function loadSkills(options) {
717
+ }
718
+ if (includeDefaults) {
719
+ addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, "skills"), "user", true));
720
+ - addSkills(loadSkillsFromDirInternal(resolve(resolvedCwd, CONFIG_DIR_NAME, "skills"), "project", true));
721
+ + // Privateer patch: `.privateer/skills` is added before `.pi/skills`, and the
722
+ + // first skill to claim a name wins — so `.privateer` supersedes on a collision
723
+ + // (the loser is reported as a normal collision diagnostic).
724
+ + for (const projectDir of projectConfigDirCandidates(resolvedCwd)) {
725
+ + addSkills(loadSkillsFromDirInternal(resolve(projectDir, "skills"), "project", true));
726
+ + }
727
+ }
728
+ const userSkillsDir = join(resolvedAgentDir, "skills");
729
+ - const projectSkillsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "skills");
730
+ + const projectSkillsDirs = projectConfigDirCandidates(resolvedCwd).map((dir) => resolve(dir, "skills"));
731
+ const isUnderPath = (target, root) => {
732
+ const normalizedRoot = resolve(root);
733
+ if (target === normalizedRoot) {
734
+ @@ -344,7 +349,7 @@ export function loadSkills(options) {
735
+ if (!includeDefaults) {
736
+ if (isUnderPath(resolvedPath, userSkillsDir))
737
+ return "user";
738
+ - if (isUnderPath(resolvedPath, projectSkillsDir))
739
+ + if (projectSkillsDirs.some((dir) => isUnderPath(resolvedPath, dir)))
740
+ return "project";
741
+ }
742
+ return "path";
743
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/trust-manager.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/trust-manager.js
744
+ index 17af938..6d97caa 100644
745
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/trust-manager.js
746
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/trust-manager.js
747
+ @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
748
+ import { homedir } from "node:os";
749
+ import { dirname, join } from "node:path";
750
+ import lockfile from "proper-lockfile";
751
+ -import { CONFIG_DIR_NAME } from "../config.js";
752
+ +import { projectConfigDirCandidates } from "../config.js";
753
+ import { canonicalizePath, resolvePath } from "../utils/paths.js";
754
+ const TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES = [
755
+ "settings.json",
756
+ @@ -150,9 +150,13 @@ export function hasTrustRequiringProjectResources(cwd) {
757
+ const homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));
758
+ const userAgentsSkillsDir = join(homeDir, ".agents", "skills");
759
+ let currentDir = canonicalizePath(resolvePath(cwd));
760
+ - const configDir = join(currentDir, CONFIG_DIR_NAME);
761
+ - if (TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some((entry) => existsSync(join(configDir, entry)))) {
762
+ - return true;
763
+ + // Privateer patch: a project needs a trust decision if EITHER config dir carries
764
+ + // trust-requiring resources — otherwise `.privateer/extensions` would execute
765
+ + // without ever prompting.
766
+ + for (const configDir of projectConfigDirCandidates(currentDir)) {
767
+ + if (TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES.some((entry) => existsSync(join(configDir, entry)))) {
768
+ + return true;
769
+ + }
770
+ }
771
+ while (true) {
772
+ const agentsSkillsDir = join(currentDir, ".agents", "skills");
773
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/main.js b/node_modules/@earendil-works/pi-coding-agent/dist/main.js
774
+ index fc62585..8b08615 100644
775
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/main.js
776
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/main.js
777
+ @@ -693,7 +693,58 @@ export async function main(args, options) {
778
+ if (exitCode !== 0) {
779
+ process.exitCode = exitCode;
780
+ }
781
+ + // Privateer patch: one-shot runs must EXIT, not merely stop working.
782
+ + //
783
+ + // Stock Pi returns here and trusts the event loop to drain. It usually does —
784
+ + // but a single ref'd resource anywhere in the process pins it open forever, and
785
+ + // `pi -p` then hangs with the answer already printed, looking for all the world
786
+ + // like a stuck model call.
787
+ + //
788
+ + // Measured on a normal Privateer install: with `-ne`, or an agent dir holding no
789
+ + // extensions, one-shot runs exit. Load the moat and they never do. Bisecting the
790
+ + // moat one extension at a time puts it on privateer-gate; the other nine exit
791
+ + // cleanly. What the gate retains could not be identified from JS, because while
792
+ + // the process sits there BOTH process._getActiveHandles() and
793
+ + // process.getActiveResourcesInfo() come back empty — whatever holds libuv's loop
794
+ + // open is native, so nothing in JS can find it, let alone close it.
795
+ + //
796
+ + // It is not only ours to fix, either: extensions are arbitrary third-party code.
797
+ + // A user package on this same machine (context-mode) spawns MCP stdio servers on
798
+ + // before_agent_start, which is its own reason a one-shot run may never end.
799
+ + //
800
+ + // Exiting here is safe because the work is genuinely finished — runPrintMode's
801
+ + // finally block has already awaited disposeRuntime() (which runs extension
802
+ + // session_shutdown handlers, so session indexing and flushes complete) and
803
+ + // flushRawStdout(). All that remains is the exit itself. We still drain stdout
804
+ + // and stderr first, because with a pipe (`privateer -p … | tail`) the last write
805
+ + // may be buffered, and a bare process.exit() would truncate it.
806
+ + //
807
+ + // Interactive and RPC modes are untouched: the TUI owns its own teardown, and
808
+ + // an RPC server is supposed to stay up.
809
+ + await flushStdioAndExit(process.exitCode ?? 0);
810
+ return;
811
+ }
812
+ }
813
+ +// Privateer patch: flush stdout/stderr, then exit — even if something in the process
814
+ +// (native addon, extension, stray timer) would otherwise keep the event loop alive.
815
+ +// The 2s backstop covers the pathological case where a stream never drains; an
816
+ +// unref'd timer so it can never itself delay a clean exit.
817
+ +function flushStdioAndExit(code) {
818
+ + return new Promise(() => {
819
+ + let pending = 2;
820
+ + const done = () => {
821
+ + if (--pending === 0)
822
+ + process.exit(code);
823
+ + };
824
+ + const backstop = setTimeout(() => process.exit(code), 2000);
825
+ + backstop.unref?.();
826
+ + try {
827
+ + process.stdout.write("", done);
828
+ + process.stderr.write("", done);
829
+ + }
830
+ + catch {
831
+ + process.exit(code);
832
+ + }
833
+ + });
834
+ +}
835
+ //# sourceMappingURL=main.js.map
836
+
837
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js b/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js
838
+ index e3a5ccf..a561a8a 100644
839
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js
840
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js
841
+ @@ -4,7 +4,7 @@
842
+ import chalk from "chalk";
843
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
844
+ import { dirname, join } from "path";
845
+ -import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.js";
846
+ +import { getAgentDir, getBinDir, projectConfigDirs } from "./config.js";
847
+ import { migrateKeybindingsConfig } from "./core/keybindings.js";
848
+ const MIGRATION_GUIDE_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration";
849
+ const EXTENSIONS_DOC_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/extensions.md";
850
+ @@ -230,14 +230,17 @@ function checkDeprecatedExtensionDirs(baseDir, label) {
851
+ */
852
+ function migrateExtensionSystem(cwd) {
853
+ const agentDir = getAgentDir();
854
+ - const projectDir = join(cwd, CONFIG_DIR_NAME);
855
+ + // Privateer patch: migrate/scan every project config dir that exists.
856
+ + const projectDirs = projectConfigDirs(cwd);
857
+ // Migrate commands/ to prompts/
858
+ migrateCommandsToPrompts(agentDir, "Global");
859
+ - migrateCommandsToPrompts(projectDir, "Project");
860
+ + for (const projectDir of projectDirs) {
861
+ + migrateCommandsToPrompts(projectDir, "Project");
862
+ + }
863
+ // Check for deprecated directories
864
+ const warnings = [
865
+ ...checkDeprecatedExtensionDirs(agentDir, "Global"),
866
+ - ...checkDeprecatedExtensionDirs(projectDir, "Project"),
867
+ + ...projectDirs.flatMap((projectDir) => checkDeprecatedExtensionDirs(projectDir, "Project")),
868
+ ];
869
+ return warnings;
870
+ }
871
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.js b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.js
872
+ index 3355a47..8c6bba9 100644
873
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.js
874
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.js
875
+ @@ -4,7 +4,7 @@
876
+ import { homedir } from "node:os";
877
+ import { basename, dirname, join, relative } from "node:path";
878
+ import { Container, getKeybindings, Input, matchesKey, Spacer, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
879
+ -import { CONFIG_DIR_NAME } from "../../../config.js";
880
+ +import { formatProjectConfigDirNames, projectConfigDirs, projectConfigWriteDir } from "../../../config.js";
881
+ import { theme } from "../theme/theme.js";
882
+ import { DynamicBorder } from "./dynamic-border.js";
883
+ import { rawKeyHint } from "./keybinding-hints.js";
884
+ @@ -41,7 +41,7 @@ function getGroupLabel(metadata, agentDir) {
885
+ ? `User (${formatBaseDir(metadata.baseDir)})`
886
+ : `Project (${formatBaseDir(metadata.baseDir)})`;
887
+ }
888
+ - return metadata.scope === "user" ? `User (${formatBaseDir(agentDir)})` : `Project (${CONFIG_DIR_NAME}/)`;
889
+ + return metadata.scope === "user" ? `User (${formatBaseDir(agentDir)})` : `Project (${formatProjectConfigDirNames()})`;
890
+ }
891
+ return metadata.scope === "user" ? "User settings" : "Project settings";
892
+ }
893
+ @@ -458,7 +458,12 @@ class ResourceList {
894
+ }
895
+ }
896
+ getTopLevelBaseDir(scope) {
897
+ - return scope === "project" ? join(this.cwd, CONFIG_DIR_NAME) : this.agentDir;
898
+ + // Privateer patch: patterns are made relative to the highest-precedence project
899
+ + // dir that exists (`.privateer` when present), else where one would be created.
900
+ + if (scope !== "project") {
901
+ + return this.agentDir;
902
+ + }
903
+ + return projectConfigDirs(this.cwd)[0] ?? projectConfigWriteDir(this.cwd);
904
+ }
905
+ getResourcePattern(item) {
906
+ const scope = item.metadata.scope;
99
907
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
100
- index 5d65200..0a08971 100644
908
+ index 5d65200..f827cd7 100644
101
909
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
102
910
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
103
- @@ -301,9 +301,15 @@ export class InteractiveMode {
911
+ @@ -10,7 +10,7 @@ import { getProviders, } from "@earendil-works/pi-ai/compat";
912
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/pi-tui";
913
+ import chalk from "chalk";
914
+ import { spawn, spawnSync } from "child_process";
915
+ -import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
916
+ +import { APP_NAME, APP_TITLE, formatProjectConfigDirNames, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
917
+ import { parseSkillBlock } from "../../core/agent-session.js";
918
+ import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
919
+ import { FooterDataProvider } from "../../core/footer-data-provider.js";
920
+ @@ -110,7 +110,12 @@ export function formatResumeCommand(sessionManager) {
921
+ const sessionFile = sessionManager.getSessionFile();
922
+ if (!sessionFile || !fs.existsSync(sessionFile))
923
+ return undefined;
924
+ - const args = [APP_NAME];
925
+ + // Privateer rename: APP_NAME is "pi", and no `pi` binary exists on a Privateer
926
+ + // machine — the published bin is `privateer`. So this hint, whose entire job is to
927
+ + // be copy-pasted, resolved to "bash: pi: command not found". The launcher exports
928
+ + // PRIVATEER_CMD with the name it was actually invoked under (npm's bin symlink, or
929
+ + // a user's own symlink of bin/privateer-tui); fall back to the published name.
930
+ + const args = [process.env.PRIVATEER_CMD || "privateer"];
931
+ if (!sessionManager.usesDefaultSessionDir()) {
932
+ args.push("--session-dir", quoteIfNeeded(sessionManager.getSessionDir()));
933
+ }
934
+ @@ -301,9 +306,15 @@ export class InteractiveMode {
104
935
  }
105
936
  getBuiltInCommandConflictDiagnostics(extensionRunner) {
106
937
  const builtinNames = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name));
@@ -117,7 +948,7 @@ index 5d65200..0a08971 100644
117
948
  .map((command) => ({
118
949
  type: "warning",
119
950
  message: command.invocationName === command.name
120
- @@ -2042,7 +2048,17 @@ export class InteractiveMode {
951
+ @@ -2042,7 +2053,17 @@ export class InteractiveMode {
121
952
  if (text === "/model" || text.startsWith("/model ")) {
122
953
  const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
123
954
  this.editor.setText("");
@@ -136,7 +967,7 @@ index 5d65200..0a08971 100644
136
967
  return;
137
968
  }
138
969
  if (text === "/export" || text.startsWith("/export ")) {
139
- @@ -2105,14 +2121,44 @@ export class InteractiveMode {
970
+ @@ -2105,14 +2126,44 @@ export class InteractiveMode {
140
971
  this.editor.setText("");
141
972
  return;
142
973
  }
@@ -184,3 +1015,43 @@ index 5d65200..0a08971 100644
184
1015
  return;
185
1016
  }
186
1017
  if (text === "/new") {
1018
+ @@ -2650,7 +2701,7 @@ export class InteractiveMode {
1019
+ if (this.chatContainer.children.length > 0) {
1020
+ this.chatContainer.addChild(new Spacer(1));
1021
+ }
1022
+ - this.chatContainer.addChild(new Text(theme.fg("warning", `This project is not trusted. Project ${CONFIG_DIR_NAME} resources and packages are ignored. Use /trust to save a trust decision, then restart pi.`), 1, 0));
1023
+ + this.chatContainer.addChild(new Text(theme.fg("warning", `This project is not trusted. Project ${formatProjectConfigDirNames()} resources and packages are ignored. Use /trust to save a trust decision, then restart ${process.env.PRIVATEER_CMD || APP_NAME}.`), 1, 0));
1024
+ }
1025
+ async getUserInput() {
1026
+ const queuedInput = this.pendingUserInputs.shift();
1027
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js b/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js
1028
+ index f81b4b4..9712a37 100644
1029
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js
1030
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js
1031
+ @@ -2,7 +2,7 @@ import { Markdown } from "@earendil-works/pi-tui";
1032
+ import chalk from "chalk";
1033
+ import { selectConfig } from "./cli/config-selector.js";
1034
+ import { createProjectTrustContext } from "./cli/project-trust.js";
1035
+ -import { APP_NAME, CONFIG_DIR_NAME, detectInstallMethod, getAgentDir, getPackageDir, getSelfUpdateCommand, getSelfUpdateUnavailableInstruction, PACKAGE_NAME, VERSION, } from "./config.js";
1036
+ +import { APP_NAME, detectInstallMethod, getAgentDir, getPackageDir, getSelfUpdateCommand, getSelfUpdateUnavailableInstruction, PACKAGE_NAME, PROJECT_CONFIG_DIR_NAMES, VERSION, } from "./config.js";
1037
+ import { DefaultPackageManager } from "./core/package-manager.js";
1038
+ import { resolveProjectTrusted } from "./core/project-trust.js";
1039
+ import { DefaultResourceLoader } from "./core/resource-loader.js";
1040
+ @@ -57,7 +57,7 @@ function printPackageCommandHelp(command) {
1041
+ Install a package and add it to settings.
1042
+
1043
+ Options:
1044
+ - -l, --local Install project-locally (${CONFIG_DIR_NAME}/settings.json)
1045
+ + -l, --local Install project-locally (${PROJECT_CONFIG_DIR_NAMES[0]}/settings.json)
1046
+ -a, --approve Trust project-local files for this command
1047
+ -na, --no-approve Ignore project-local files for this command
1048
+
1049
+ @@ -78,7 +78,7 @@ Remove a package and its source from settings.
1050
+ Alias: ${APP_NAME} uninstall <source> [-l]
1051
+
1052
+ Options:
1053
+ - -l, --local Remove from project settings (${CONFIG_DIR_NAME}/settings.json)
1054
+ + -l, --local Remove from project settings (${PROJECT_CONFIG_DIR_NAMES[0]}/settings.json)
1055
+ -a, --approve Trust project-local files for this command
1056
+ -na, --no-approve Ignore project-local files for this command
1057
+