privateer-agent 0.12.27 → 0.12.29

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.
@@ -35,6 +35,21 @@ const HERE = path.dirname(fileURLToPath(import.meta.url)); // bin/
35
35
  const REPO = path.resolve(HERE, "..");
36
36
  const isWin = process.platform === "win32";
37
37
 
38
+ // On Windows, ensure the console output code page is UTF-8 (65001).
39
+ // Without this, raw UTF-8 writes to stdout/stderr (such as fs.writeSync(2, ...) in the
40
+ // boot splash worker) are decoded through the OEM code page (CP437/850), turning
41
+ // Unicode wave blocks and emojis into 3-byte mojibake that wraps and floods the console.
42
+ if (isWin) {
43
+ try {
44
+ const chcp = process.env.SystemRoot
45
+ ? path.join(process.env.SystemRoot, "System32", "chcp.com")
46
+ : "chcp.com";
47
+ spawnSync(chcp, ["65001"], { stdio: "ignore", windowsHide: true });
48
+ } catch {
49
+ /* best effort */
50
+ }
51
+ }
52
+
38
53
  const PRIVATEER_HOME = process.env.PRIVATEER_HOME || path.join(os.homedir(), ".privateer");
39
54
  const ENV_FILE = path.join(REPO, ".env"); // dev-only; a real install has none
40
55
  const AGENT_DIR = path.join(PRIVATEER_HOME, "agent");
@@ -64,6 +79,38 @@ function sweepLegacyShims() {
64
79
  }
65
80
  }
66
81
 
82
+ // Pi's shell tools spill oversized command output to `$TMPDIR/pi-{bash,powershell,
83
+ // output}-<hex>.log` and hand the model the path — a good design, with no other half:
84
+ // NOTHING in Pi ever deletes them. Two `grep -rn` calls that wandered into node_modules
85
+ // left 630MB of minified JS in one developer's tmpdir, inside a 640MB/22-file pile that
86
+ // had been accumulating for weeks. macOS clears /var/folders only on its own schedule
87
+ // and Linux distros vary, so on a long-lived machine this is simply a leak.
88
+ //
89
+ // A day is the useful window: the path is only ever quoted into a live session, so a log
90
+ // outlives its usefulness the moment that session ends. Silent and best-effort — the acp
91
+ // branch's stdout is a JSON-RPC stream, and a tmpdir we can't read is not a launch
92
+ // problem. Name-matched before stat so the common case is one readdir.
93
+ const BASH_LOG_MAX_AGE_MS = 24 * 60 * 60 * 1000;
94
+ const BASH_LOG_RE = /^pi-(?:bash|powershell|output)-[0-9a-f]{16}\.log$/;
95
+
96
+ function sweepBashLogs() {
97
+ try {
98
+ const dir = os.tmpdir();
99
+ const cutoff = Date.now() - BASH_LOG_MAX_AGE_MS;
100
+ for (const name of fs.readdirSync(dir)) {
101
+ if (!BASH_LOG_RE.test(name)) continue;
102
+ const file = path.join(dir, name);
103
+ try {
104
+ if (fs.statSync(file).mtimeMs < cutoff) fs.rmSync(file, { force: true });
105
+ } catch {
106
+ /* raced with another session, or not ours to delete — either way, skip it. */
107
+ }
108
+ }
109
+ } catch {
110
+ /* unreadable tmpdir; the logs stay. */
111
+ }
112
+ }
113
+
67
114
  // --- bundle detection ------------------------------------------------------
68
115
  // A self-contained bundle ships its own pinned Node at "$REPO/node[.exe]" plus a
69
116
  // BUNDLE_INFO.json marker (built by scripts/build-bundle.mjs). When present we use
@@ -279,6 +326,7 @@ if (sub === "update") {
279
326
  // `daemon` is a hidden back-compat alias for the pre-rename command name.
280
327
  else if (sub === "harbor" || sub === "daemon") {
281
328
  sweepLegacyShims(); // a harbor-only machine upgrades too — see the function's note
329
+ sweepBashLogs();
282
330
  const nodeArgs = fs.existsSync(ENV_FILE) ? [`--env-file=${ENV_FILE}`] : [];
283
331
  // A resident background process, stopped by launchd/systemd/scripts with a plain
284
332
  // `kill` — which reaches only this launcher. See runToCompletion.
@@ -304,6 +352,7 @@ else if (sub === "verify") {
304
352
  // stray stdout line breaks the JSON-RPC stream and the host disconnects.
305
353
  else if (sub === "acp") {
306
354
  sweepLegacyShims(); // silent: only ever removes files
355
+ sweepBashLogs(); // silent too
307
356
  const nodeArgs = fs.existsSync(ENV_FILE) ? [`--env-file=${ENV_FILE}`] : [];
308
357
  // Long-lived and driven over stdio by an editor, which stops it by terminating
309
358
  // the process rather than by a keystroke. Same leak, same fix.
@@ -329,6 +378,7 @@ else {
329
378
  // to drop one, and clear out any shim an older release left behind.
330
379
  fs.mkdirSync(EXT_DIR, { recursive: true });
331
380
  sweepLegacyShims();
381
+ sweepBashLogs();
332
382
 
333
383
  // Resolve every moat entry point to an absolute path, to be passed to Pi as `-e`.
334
384
  // Dependencies resolve by walking the node_modules chain, NOT as REPO/node_modules: npm
@@ -38,6 +38,8 @@
38
38
  //
39
39
  // The wave is drawn on STDERR; stdout belongs to the TUI's canvas.
40
40
 
41
+ import { spawnSync } from "node:child_process";
42
+ import path from "node:path";
41
43
  import { Worker } from "node:worker_threads";
42
44
 
43
45
  const enabled =
@@ -46,6 +48,20 @@ const enabled =
46
48
  !process.env.PRIVATEER_NO_SPLASH &&
47
49
  !process.env.CI;
48
50
 
51
+ // On Windows, ensure the console output code page is UTF-8 (65001) before the worker
52
+ // thread starts drawing. If privateer was launched via npm/npx or direct node rather
53
+ // than privateer.cmd, the console might still be on OEM CP437.
54
+ if (enabled && process.platform === "win32") {
55
+ try {
56
+ const chcp = process.env.SystemRoot
57
+ ? path.join(process.env.SystemRoot, "System32", "chcp.com")
58
+ : "chcp.com";
59
+ spawnSync(chcp, ["65001"], { stdio: "ignore", windowsHide: true });
60
+ } catch {
61
+ /* best effort */
62
+ }
63
+ }
64
+
49
65
  // Bytes of stdout after TUI.start() that mean "this is the first frame, not a control
50
66
  // sequence". Everything Pi writes between raw mode and the frame is short (the paste
51
67
  // toggle, a Kitty protocol query, the cursor hide, an OSC window title — 42 bytes all
@@ -72,7 +88,8 @@ if (enabled) {
72
88
 
73
89
  // Room for " ⚓ " + wave + message + elapsed, clamped so a narrow terminal doesn't
74
90
  // wrap (a wrapped line survives our `\r\x1b[K` erase only on its last row).
75
- const width = Math.max(12, Math.min(28, (err.columns || 80) - 34));
91
+ const cols = err.columns && err.columns > 0 ? err.columns : 80;
92
+ const width = Math.max(6, Math.min(28, cols - 34));
76
93
 
77
94
  // The worker source is plain logic with no escape sequences of its own — every ANSI
78
95
  // string is handed over in workerData, so nothing here has to survive two rounds of
package/bin/privateer.cmd CHANGED
@@ -1,4 +1,7 @@
1
1
  @echo off
2
+ REM Ensure console output code page is UTF-8 so Unicode blocks and emojis render
3
+ REM correctly without CP437 mojibake or line-wrapping cascades.
4
+ chcp 65001 >nul 2>&1
2
5
  REM Windows entry point for a Privateer bundle. Mirrors the unix bin/privateer-tui
3
6
  REM shim: run the bundled Node against the shared cross-platform launcher. %~dp0 is
4
7
  REM this file's dir (<app>\bin\), so ..\node.exe is the bundled runtime.
@@ -13,7 +13,14 @@
13
13
  // advertises /init. After /init we emit the shared context-changed signal so that line
14
14
  // refreshes at once. See src/context.ts for the discovery/formatting details.
15
15
 
16
- import { contextBlock, writeTemplate, emitContextChanged, CONTEXT_BLOCK_MARKER } from "../src/context.ts";
16
+ import {
17
+ contextBlock,
18
+ writeTemplate,
19
+ emitContextChanged,
20
+ CONTEXT_BLOCK_MARKER,
21
+ RUNTIME_GUIDELINES_MARKER,
22
+ runtimeGuidelinesBlock,
23
+ } from "../src/context.ts";
17
24
 
18
25
  // Honor Pi's own "disable context files" switch, so --no-context-files / -nc silences
19
26
  // PRIVATEER.md too (not just AGENTS.md/CLAUDE.md) — otherwise the flag would half-work.
@@ -25,13 +32,16 @@ export default function privateerContext(pi: any): void {
25
32
  // and chained across before_agent_start handlers, so appending here is idempotent for
26
33
  // the turn; the marker guard makes it a no-op if an earlier handler already added it.
27
34
  pi.on("before_agent_start", (event: any) => {
28
- if (CONTEXT_FILES_DISABLED) return;
29
- const cwd = event?.systemPromptOptions?.cwd ?? process.cwd();
30
- const base: string = event?.systemPrompt ?? "";
31
- if (base.includes(CONTEXT_BLOCK_MARKER)) return; // already injected this chain
32
- const block = contextBlock(cwd);
33
- if (!block) return; // no PRIVATEER.md anywhere — leave the prompt untouched
34
- return { systemPrompt: base + block };
35
+ let prompt: string = event?.systemPrompt ?? "";
36
+ if (!prompt.includes(RUNTIME_GUIDELINES_MARKER)) {
37
+ prompt += runtimeGuidelinesBlock();
38
+ }
39
+ if (!CONTEXT_FILES_DISABLED && !prompt.includes(CONTEXT_BLOCK_MARKER)) {
40
+ const cwd = event?.systemPromptOptions?.cwd ?? process.cwd();
41
+ const block = contextBlock(cwd);
42
+ if (block) prompt += block;
43
+ }
44
+ return { systemPrompt: prompt };
35
45
  });
36
46
 
37
47
  // /init — scaffold a PRIVATEER.md in the working directory. Never clobbers an existing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateer-agent",
3
- "version": "0.12.27",
3
+ "version": "0.12.29",
4
4
  "description": "Privacy-first terminal coding agent — bring your own model across 20 providers (Anthropic, OpenAI, OpenRouter, Google, local Ollama…). Safe-by-default permissions, MCP, sub-agents, workflows, and verifiable TEE inference. Built on the Pi toolkit.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -69,9 +69,9 @@
69
69
  "node": ">=22.19.0"
70
70
  },
71
71
  "dependencies": {
72
- "@earendil-works/pi-ai": "0.84.1",
73
- "@earendil-works/pi-coding-agent": "0.84.1",
74
- "@earendil-works/pi-tui": "0.84.1",
72
+ "@earendil-works/pi-ai": "0.84.4",
73
+ "@earendil-works/pi-coding-agent": "0.84.4",
74
+ "@earendil-works/pi-tui": "0.84.4",
75
75
  "@juicesharp/rpiv-ask-user-question": "2.2.0",
76
76
  "@juicesharp/rpiv-web-tools": "1.20.0",
77
77
  "@noble/ciphers": "2.2.0",
@@ -80,6 +80,7 @@
80
80
  "@phala/dcap-qvl": "0.5.2",
81
81
  "@zed-industries/agent-client-protocol": "0.4.5",
82
82
  "patch-package": "8.0.1",
83
+ "pi-background-tasks": "2.4.2",
83
84
  "pi-mcp-adapter": "2.11.0",
84
85
  "pi-privacy": "0.13.0",
85
86
  "pi-subagents": "0.34.0",
@@ -1,8 +1,8 @@
1
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
2
+ index f04269f..536e030 100644
3
3
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/config.js
4
4
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/config.js
5
- @@ -393,6 +393,64 @@ export const APP_NAME = piConfigName || "pi";
5
+ @@ -401,6 +401,64 @@ export const APP_NAME = piConfigName || "pi";
6
6
  export const APP_TITLE = piConfigName ? APP_NAME : "π";
7
7
  export const CONFIG_DIR_NAME = pkg.piConfig?.configDir || ".pi";
8
8
  export const VERSION = pkg.version || "0.0.0";
@@ -68,7 +68,7 @@ index 4600b23..075ecae 100644
68
68
  export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_DIR`;
69
69
  export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_CODING_AGENT_SESSION_DIR`;
70
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
71
- index ce8a9a2..679e584 100644
71
+ index fd82b71..5e0821c 100644
72
72
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
73
73
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js
74
74
  @@ -38,6 +38,111 @@ import { createLocalBashOperations } from "./tools/bash.js";
@@ -207,7 +207,7 @@ index ce8a9a2..679e584 100644
207
207
  throw new Error(`Authentication failed for "${model.provider}". ` +
208
208
  `Credentials may have expired or network is unavailable. ` +
209
209
  `Run '/login ${model.provider}' to re-authenticate.`);
210
- @@ -360,6 +475,16 @@ export class AgentSession {
210
+ @@ -380,6 +495,16 @@ export class AgentSession {
211
211
  }
212
212
  }
213
213
  }
@@ -224,7 +224,7 @@ index ce8a9a2..679e584 100644
224
224
  // Emit to extensions first
225
225
  await this._emitExtensionEvent(event);
226
226
  // Notify all listeners
227
- @@ -772,6 +897,33 @@ export class AgentSession {
227
+ @@ -801,6 +926,33 @@ export class AgentSession {
228
228
  finalError: msg.errorMessage,
229
229
  });
230
230
  this._retryAttempt = 0;
@@ -258,7 +258,7 @@ index ce8a9a2..679e584 100644
258
258
  }
259
259
  if (await this._checkCompaction(msg)) {
260
260
  return true;
261
- @@ -852,6 +1004,14 @@ export class AgentSession {
261
+ @@ -882,6 +1034,14 @@ export class AgentSession {
262
262
  if (!hasConfiguredAuth) {
263
263
  const isOAuth = this._modelRuntime.isUsingOAuth(this.model.provider);
264
264
  if (isOAuth) {
@@ -273,7 +273,7 @@ index ce8a9a2..679e584 100644
273
273
  throw new Error(`Authentication failed for "${this.model.provider}". ` +
274
274
  `Credentials may have expired or network is unavailable. ` +
275
275
  `Run '/login ${this.model.provider}' to re-authenticate.`);
276
- @@ -861,7 +1021,13 @@ export class AgentSession {
276
+ @@ -891,7 +1051,13 @@ export class AgentSession {
277
277
  // Check if we need to compact before sending (catches aborted responses).
278
278
  // The user's new prompt is sent below, so do not call agent.continue() here.
279
279
  const lastAssistant = this._findLastAssistantMessage();
@@ -288,7 +288,7 @@ index ce8a9a2..679e584 100644
288
288
  await this._checkCompaction(lastAssistant, false);
289
289
  }
290
290
  // Build messages array (custom message if any, then user message)
291
- @@ -2084,6 +2250,27 @@ export class AgentSession {
291
+ @@ -2242,6 +2408,27 @@ export class AgentSession {
292
292
  // Context overflow is handled by compaction, not retry.
293
293
  if (isContextOverflow(message, this.model?.contextWindow ?? 0))
294
294
  return false;
@@ -316,7 +316,7 @@ index ce8a9a2..679e584 100644
316
316
  return isRetryableAssistantError(message);
317
317
  }
318
318
  /**
319
- @@ -2129,7 +2316,11 @@ export class AgentSession {
319
+ @@ -2287,7 +2474,11 @@ export class AgentSession {
320
320
  this._retryAttempt--;
321
321
  return false;
322
322
  }
@@ -375,7 +375,7 @@ index 197bccc..bc9ac3f 100644
375
375
  //# sourceMappingURL=auth-guidance.js.map
376
376
 
377
377
  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
378
- index 324d30a..e613e92 100644
378
+ index bb022d4..382ff2b 100644
379
379
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js
380
380
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js
381
381
  @@ -18,7 +18,7 @@ import { createJiti } from "jiti/static";
@@ -387,7 +387,7 @@ index 324d30a..e613e92 100644
387
387
  // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
388
388
  // avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent.
389
389
  import * as _bundledPiCodingAgent from "../../index.js";
390
- @@ -554,9 +554,23 @@ export async function discoverAndLoadExtensions(configuredPaths, cwd, agentDir =
390
+ @@ -621,9 +621,23 @@ export async function discoverAndLoadExtensions(configuredPaths, cwd, agentDir =
391
391
  }
392
392
  }
393
393
  };
@@ -415,19 +415,19 @@ index 324d30a..e613e92 100644
415
415
  const globalExtDir = path.join(resolvedAgentDir, "extensions");
416
416
  addPaths(discoverExtensionsInDir(globalExtDir));
417
417
  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
418
- index e806af3..9b00ebd 100644
418
+ index 696e98c..7585930 100644
419
419
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.js
420
420
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.js
421
- @@ -25,7 +25,7 @@ import { globSync } from "glob";
421
+ @@ -24,7 +24,7 @@ import { basename, dirname, join, relative, resolve, sep } from "node:path";
422
422
  import ignore from "ignore";
423
423
  import { minimatch } from "minimatch";
424
- import { maxSatisfying, rcompare, satisfies, valid, validRange } from "semver";
424
+ import { gt, maxSatisfying, rcompare, satisfies, valid, validRange } from "semver";
425
425
  -import { CONFIG_DIR_NAME } from "../config.js";
426
426
  +import { projectConfigDirs, projectConfigWriteDir, resolveProjectConfigPath } from "../config.js";
427
427
  import { spawnProcess, spawnProcessSync } from "../utils/child-process.js";
428
428
  import { parseGitUrl } from "../utils/git.js";
429
429
  import { canonicalizePath, isLocalPath, markPathIgnoredByCloudSync, resolvePath } from "../utils/paths.js";
430
- @@ -698,23 +698,29 @@ export class DefaultPackageManager {
430
+ @@ -711,23 +711,29 @@ export class DefaultPackageManager {
431
431
  const packageSources = this.dedupePackages(allPackages);
432
432
  await this.resolvePackageSources(packageSources, accumulator, onMissing);
433
433
  const globalBaseDir = this.agentDir;
@@ -464,7 +464,7 @@ index e806af3..9b00ebd 100644
464
464
  return this.toResolvedPaths(accumulator);
465
465
  }
466
466
  async resolveExtensionSources(sources, options) {
467
- @@ -1668,13 +1674,30 @@ export class DefaultPackageManager {
467
+ @@ -1681,13 +1687,30 @@ export class DefaultPackageManager {
468
468
  writeFileSync(ignorePath, "*\n!.gitignore\n", "utf-8");
469
469
  }
470
470
  }
@@ -496,7 +496,7 @@ index e806af3..9b00ebd 100644
496
496
  }
497
497
  return join(this.agentDir, "npm");
498
498
  }
499
- @@ -1713,7 +1736,9 @@ export class DefaultPackageManager {
499
+ @@ -1726,7 +1749,9 @@ export class DefaultPackageManager {
500
500
  }
501
501
  if (scope === "project") {
502
502
  this.assertProjectTrustedForScope(scope);
@@ -507,7 +507,7 @@ index e806af3..9b00ebd 100644
507
507
  }
508
508
  return join(this.agentDir, "npm", "node_modules", source.name);
509
509
  }
510
- @@ -1749,7 +1774,7 @@ export class DefaultPackageManager {
510
+ @@ -1762,7 +1787,7 @@ export class DefaultPackageManager {
511
511
  }
512
512
  if (scope === "project") {
513
513
  this.assertProjectTrustedForScope(scope);
@@ -516,7 +516,7 @@ index e806af3..9b00ebd 100644
516
516
  }
517
517
  return join(this.agentDir, "git");
518
518
  }
519
- @@ -1772,7 +1797,7 @@ export class DefaultPackageManager {
519
+ @@ -1785,7 +1810,7 @@ export class DefaultPackageManager {
520
520
  getBaseDirForScope(scope) {
521
521
  if (scope === "project") {
522
522
  this.assertProjectTrustedForScope(scope);
@@ -525,7 +525,7 @@ index e806af3..9b00ebd 100644
525
525
  }
526
526
  if (scope === "user") {
527
527
  return this.agentDir;
528
- @@ -1783,7 +1808,30 @@ export class DefaultPackageManager {
528
+ @@ -1796,7 +1821,30 @@ export class DefaultPackageManager {
529
529
  return resolvePath(input, this.cwd, { homeDir: getHomeDir(), trim: true });
530
530
  }
531
531
  resolvePathFromBase(input, baseDir) {
@@ -557,7 +557,7 @@ index e806af3..9b00ebd 100644
557
557
  }
558
558
  collectPackageResources(packageRoot, accumulator, filter, metadata) {
559
559
  if (filter) {
560
- @@ -1928,19 +1976,24 @@ export class DefaultPackageManager {
560
+ @@ -1936,19 +1984,24 @@ export class DefaultPackageManager {
561
561
  this.addResource(target, f, metadata, enabledPaths.has(f));
562
562
  }
563
563
  }
@@ -586,7 +586,7 @@ index e806af3..9b00ebd 100644
586
586
  const userOverrides = {
587
587
  extensions: (globalSettings.extensions ?? []),
588
588
  skills: (globalSettings.skills ?? []),
589
- @@ -1959,12 +2012,12 @@ export class DefaultPackageManager {
589
+ @@ -1967,12 +2020,12 @@ export class DefaultPackageManager {
590
590
  prompts: join(globalBaseDir, "prompts"),
591
591
  themes: join(globalBaseDir, "themes"),
592
592
  };
@@ -605,7 +605,7 @@ index e806af3..9b00ebd 100644
605
605
  const userAgentsSkillsDir = join(getHomeDir(), ".agents", "skills");
606
606
  const projectTrusted = this.settingsManager.isProjectTrusted();
607
607
  const projectAgentsSkillDirs = projectTrusted
608
- @@ -1978,10 +2031,13 @@ export class DefaultPackageManager {
608
+ @@ -1986,10 +2039,13 @@ export class DefaultPackageManager {
609
609
  }
610
610
  };
611
611
  if (projectTrusted) {
@@ -623,7 +623,7 @@ index e806af3..9b00ebd 100644
623
623
  }
624
624
  // Project skills from .agents/ (each with its own baseDir)
625
625
  for (const agentsSkillsDir of projectAgentsSkillDirs) {
626
- @@ -1993,8 +2049,11 @@ export class DefaultPackageManager {
626
+ @@ -2001,8 +2057,11 @@ export class DefaultPackageManager {
627
627
  addResources("skills", collectAutoSkillEntries(agentsSkillsDir, "agents"), agentsMetadata, projectOverrides.skills, agentsBaseDir);
628
628
  }
629
629
  if (projectTrusted) {
@@ -638,17 +638,17 @@ index e806af3..9b00ebd 100644
638
638
  // User extensions from ~/.pi/agent/
639
639
  addResources("extensions", collectAutoExtensionEntries(userDirs.extensions), userMetadata, userOverrides.extensions, globalBaseDir);
640
640
  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
641
- index c4f2509..a4402df 100644
641
+ index 2f85768..328b27b 100644
642
642
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/project-trust.js
643
643
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/project-trust.js
644
644
  @@ -1,8 +1,8 @@
645
- -import { CONFIG_DIR_NAME } from "../config.js";
646
- +import { formatProjectConfigDirNames } from "../config.js";
645
+ -import { APP_NAME, CONFIG_DIR_NAME } from "../config.js";
646
+ +import { APP_NAME, formatProjectConfigDirNames } from "../config.js";
647
647
  import { emitProjectTrustEvent } from "./extensions/runner.js";
648
648
  import { getProjectTrustOptions, hasTrustRequiringProjectResources, } from "./trust-manager.js";
649
649
  function formatProjectTrustPrompt(cwd) {
650
- - 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.`;
651
- + return `Trust project folder?\n${cwd}\n\nThis allows pi to load ${formatProjectConfigDirNames()} settings and resources, install missing project packages, and execute project extensions.`;
650
+ - return `Trust project folder?\n${cwd}\n\nThis allows ${APP_NAME} to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`;
651
+ + return `Trust project folder?\n${cwd}\n\nThis allows ${APP_NAME} to load ${formatProjectConfigDirNames()} settings and resources, install missing project packages, and execute project extensions.`;
652
652
  }
653
653
  async function selectProjectTrustOption(cwd, ctx) {
654
654
  const options = getProjectTrustOptions(cwd, { includeSessionOnly: true });
@@ -696,7 +696,7 @@ index 3700c08..79cb85e 100644
696
696
  // 3. Load explicit prompt paths
697
697
  for (const rawPath of promptPaths) {
698
698
  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
699
- index 97af6cd..74d5d52 100644
699
+ index c5ed4d7..d9f50ae 100644
700
700
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js
701
701
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js
702
702
  @@ -1,7 +1,7 @@
@@ -707,8 +707,8 @@ index 97af6cd..74d5d52 100644
707
707
  +import { findProjectConfigPath, projectConfigDirCandidates, projectConfigDirs } from "../config.js";
708
708
  import { loadThemeFromPath } from "../modes/interactive/theme/theme.js";
709
709
  import { canonicalizePath, isLocalPath, resolvePath } from "../utils/paths.js";
710
- import { createEventBus } from "./event-bus.js";
711
- @@ -624,12 +624,14 @@ export class DefaultResourceLoader {
710
+ import { stripBom } from "../utils/text.js";
711
+ @@ -625,12 +625,14 @@ export class DefaultResourceLoader {
712
712
  join(this.agentDir, "themes"),
713
713
  join(this.agentDir, "extensions"),
714
714
  ];
@@ -729,7 +729,7 @@ index 97af6cd..74d5d52 100644
729
729
  for (const root of agentRoots) {
730
730
  if (this.isUnderPath(normalizedPath, root)) {
731
731
  return { path: filePath, source: "local", scope: "user", origin: "top-level", baseDir: root };
732
- @@ -668,7 +670,11 @@ export class DefaultResourceLoader {
732
+ @@ -669,7 +671,11 @@ export class DefaultResourceLoader {
733
733
  const themes = [];
734
734
  const diagnostics = [];
735
735
  if (includeDefaults) {
@@ -742,7 +742,7 @@ index 97af6cd..74d5d52 100644
742
742
  for (const dir of defaultDirs) {
743
743
  this.loadThemesFromDir(dir, themes, diagnostics);
744
744
  }
745
- @@ -806,8 +812,9 @@ export class DefaultResourceLoader {
745
+ @@ -807,8 +813,9 @@ export class DefaultResourceLoader {
746
746
  return { themes: Array.from(seen.values()), diagnostics };
747
747
  }
748
748
  discoverSystemPromptFile() {
@@ -754,7 +754,7 @@ index 97af6cd..74d5d52 100644
754
754
  return projectPath;
755
755
  }
756
756
  const globalPath = join(this.agentDir, "SYSTEM.md");
757
- @@ -817,8 +824,9 @@ export class DefaultResourceLoader {
757
+ @@ -818,8 +825,9 @@ export class DefaultResourceLoader {
758
758
  return undefined;
759
759
  }
760
760
  discoverAppendSystemPromptFile() {
@@ -767,7 +767,7 @@ index 97af6cd..74d5d52 100644
767
767
  }
768
768
  const globalPath = join(this.agentDir, "APPEND_SYSTEM.md");
769
769
  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
770
- index cb06c10..41fb90c 100644
770
+ index f0f936e..ad164d2 100644
771
771
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js
772
772
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js
773
773
  @@ -2,7 +2,7 @@ import { randomUUID } from "crypto";
@@ -777,11 +777,11 @@ index cb06c10..41fb90c 100644
777
777
  -import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
778
778
  +import { getAgentDir, projectConfigDirCandidates, projectConfigWriteDir } from "../config.js";
779
779
  import { normalizePath, resolvePath } from "../utils/paths.js";
780
+ import { stripBom } from "../utils/text.js";
780
781
  import { DEFAULT_HTTP_IDLE_TIMEOUT_MS, parseHttpIdleTimeoutMs } from "./http-dispatcher.js";
781
- function isMergeableObject(value) {
782
- @@ -37,14 +37,112 @@ function parseTimeoutSetting(value, settingName) {
783
- }
784
- return undefined;
782
+ @@ -45,14 +45,112 @@ function toSettingsError(scope, error, path) {
783
+ error: error instanceof Error ? error : new Error(String(error)),
784
+ };
785
785
  }
786
786
  +/**
787
787
  + * Privateer patch: drop the keys of `settings` that are byte-identical to `base`.
@@ -893,7 +893,7 @@ index cb06c10..41fb90c 100644
893
893
  }
894
894
  acquireLockSyncWithRetry(path) {
895
895
  const maxAttempts = 10;
896
- @@ -71,7 +169,13 @@ export class FileSettingsStorage {
896
+ @@ -79,7 +177,13 @@ export class FileSettingsStorage {
897
897
  throw lastError ?? new Error("Failed to acquire settings lock");
898
898
  }
899
899
  withLock(scope, fn) {
@@ -908,7 +908,16 @@ index cb06c10..41fb90c 100644
908
908
  const dir = dirname(path);
909
909
  let release;
910
910
  try {
911
- @@ -568,7 +672,15 @@ export class SettingsManager {
911
+ @@ -157,7 +261,7 @@ export class SettingsManager {
912
+ const storage = new FileSettingsStorage(resolvedCwd, resolvedAgentDir);
913
+ return SettingsManager.fromStorageWithPaths(storage, options, {
914
+ global: join(resolvedAgentDir, "settings.json"),
915
+ - project: join(resolvedCwd, CONFIG_DIR_NAME, "settings.json"),
916
+ + project: join(projectConfigWriteDir(resolvedCwd), "settings.json"),
917
+ });
918
+ }
919
+ /** Create a SettingsManager from an arbitrary storage backend */
920
+ @@ -610,7 +714,15 @@ export class SettingsManager {
912
921
  getProviderRetrySettings() {
913
922
  return {
914
923
  timeoutMs: this.settings.retry?.provider?.timeoutMs,
@@ -926,7 +935,7 @@ index cb06c10..41fb90c 100644
926
935
  };
927
936
  }
928
937
  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
929
- index 4e7e784..7383a2b 100644
938
+ index 56cf7a3..2a32170 100644
930
939
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js
931
940
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js
932
941
  @@ -1,7 +1,7 @@
@@ -938,7 +947,7 @@ index 4e7e784..7383a2b 100644
938
947
  import { parseFrontmatter } from "../utils/frontmatter.js";
939
948
  import { canonicalizePath, resolvePath } from "../utils/paths.js";
940
949
  import { createSyntheticSourceInfo } from "./source-info.js";
941
- @@ -328,10 +328,15 @@ export function loadSkills(options) {
950
+ @@ -346,10 +346,15 @@ export function loadSkills(options) {
942
951
  }
943
952
  if (includeDefaults) {
944
953
  addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, "skills"), "user", true));
@@ -956,7 +965,7 @@ index 4e7e784..7383a2b 100644
956
965
  const isUnderPath = (target, root) => {
957
966
  const normalizedRoot = resolve(root);
958
967
  if (target === normalizedRoot) {
959
- @@ -344,7 +349,7 @@ export function loadSkills(options) {
968
+ @@ -362,7 +367,7 @@ export function loadSkills(options) {
960
969
  if (!includeDefaults) {
961
970
  if (isUnderPath(resolvedPath, userSkillsDir))
962
971
  return "user";
@@ -965,8 +974,121 @@ index 4e7e784..7383a2b 100644
965
974
  return "project";
966
975
  }
967
976
  return "path";
977
+ diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/output-accumulator.js b/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/output-accumulator.js
978
+ index 7241668..7b79305 100644
979
+ --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/output-accumulator.js
980
+ +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/output-accumulator.js
981
+ @@ -10,6 +10,30 @@ function defaultTempFilePath(prefix) {
982
+ function byteLength(text) {
983
+ return Buffer.byteLength(text, "utf-8");
984
+ }
985
+ +// --- Privateer patch: per-line cap on the DISPLAY tail --------------------------
986
+ +//
987
+ +// Stock Pi bounds tool output two ways — 2000 lines and 50KB — but never bounds a
988
+ +// single LINE. One minified line is enough to spend the whole budget: a bare
989
+ +// `grep -rn <term> .` that wanders into node_modules matches inside
990
+ +// typescript.js or an Expo web bundle, and those "lines" are megabytes wide. The
991
+ +// observed shape is a 331MB capture whose entire visible output was 390 lines,
992
+ +// of which the model saw one — the tail of a single line of minified JS. The
993
+ +// context window pays 50KB for nothing, and the truncation notice reads as noise
994
+ +// because nothing about it says "your grep hit a bundle".
995
+ +//
996
+ +// So cap what goes into the display tail at one line's worth of characters and
997
+ +// mark the elision. Accounting (totalDecodedBytes, totalLines, currentLineBytes)
998
+ +// still measures the REAL stream, so "Showing lines X-Y of Z" stays honest, and
999
+ +// the temp file still receives every raw byte — `Full output:` means what it
1000
+ +// says. Only the bytes we hand the model change: 25+ real lines instead of one
1001
+ +// blob.
1002
+ +//
1003
+ +// 2000 rather than grep's 500 (GREP_MAX_LINE_LENGTH): a 2000-char line is already
1004
+ +// ~25 terminal rows, so ordinary output — stack traces, long JSON, compiler
1005
+ +// diagnostics — passes through untouched, while the pathological case is capped
1006
+ +// three orders of magnitude below where it hurts.
1007
+ +const DEFAULT_MAX_LINE_CHARS = 2000;
1008
+ +const LINE_ELISION = "... [line truncated]";
1009
+ /**
1010
+ * Incrementally tracks streaming output with bounded memory.
1011
+ *
1012
+ @@ -32,6 +56,11 @@ export class OutputAccumulator {
1013
+ completedLines = 0;
1014
+ totalLines = 0;
1015
+ currentLineBytes = 0;
1016
+ + // Privateer patch: per-line display cap, carried across chunk boundaries
1017
+ + // because a long line rarely arrives in one read().
1018
+ + maxLineChars;
1019
+ + lineCharsEmitted = 0;
1020
+ + lineElided = false;
1021
+ hasOpenLine = false;
1022
+ finished = false;
1023
+ tempFilePath;
1024
+ @@ -40,6 +69,8 @@ export class OutputAccumulator {
1025
+ this.maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
1026
+ this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
1027
+ this.maxRollingBytes = Math.max(this.maxBytes * 2, 1);
1028
+ + // Privateer patch: 0 or less disables the cap.
1029
+ + this.maxLineChars = options.maxLineChars ?? DEFAULT_MAX_LINE_CHARS;
1030
+ this.tempFilePrefix = options.tempFilePrefix ?? "pi-output";
1031
+ }
1032
+ append(data) {
1033
+ @@ -122,8 +153,11 @@ export class OutputAccumulator {
1034
+ }
1035
+ const bytes = byteLength(text);
1036
+ this.totalDecodedBytes += bytes;
1037
+ - this.tailText += text;
1038
+ - this.tailBytes += bytes;
1039
+ + // Privateer patch: the tail is what the model and the TUI see, so it is the
1040
+ + // only thing capped. Every counter below still sees the full `text`.
1041
+ + const display = this.capLongLines(text);
1042
+ + this.tailText += display;
1043
+ + this.tailBytes += byteLength(display);
1044
+ if (this.tailBytes > this.maxRollingBytes * 2) {
1045
+ this.trimTail();
1046
+ }
1047
+ @@ -145,6 +179,42 @@ export class OutputAccumulator {
1048
+ }
1049
+ this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
1050
+ }
1051
+ + /**
1052
+ + * Privateer patch: emit at most `maxLineChars` characters per line, followed by
1053
+ + * one elision marker, dropping the rest of that line until its newline.
1054
+ + *
1055
+ + * Streaming, so the per-line counters live on the instance: a 5MB line arrives
1056
+ + * as hundreds of chunks and every one of them must know the line is already
1057
+ + * spent.
1058
+ + */
1059
+ + capLongLines(text) {
1060
+ + if (this.maxLineChars <= 0) {
1061
+ + return text;
1062
+ + }
1063
+ + let out = "";
1064
+ + let i = 0;
1065
+ + while (i < text.length) {
1066
+ + const nl = text.indexOf("\n", i);
1067
+ + const segment = text.slice(i, nl === -1 ? text.length : nl);
1068
+ + const room = Math.max(this.maxLineChars - this.lineCharsEmitted, 0);
1069
+ + if (room > 0) {
1070
+ + out += segment.slice(0, room);
1071
+ + this.lineCharsEmitted += Math.min(segment.length, room);
1072
+ + }
1073
+ + if (segment.length > room && !this.lineElided) {
1074
+ + out += LINE_ELISION;
1075
+ + this.lineElided = true;
1076
+ + }
1077
+ + if (nl === -1) {
1078
+ + break;
1079
+ + }
1080
+ + out += "\n";
1081
+ + this.lineCharsEmitted = 0;
1082
+ + this.lineElided = false;
1083
+ + i = nl + 1;
1084
+ + }
1085
+ + return out;
1086
+ + }
1087
+ trimTail() {
1088
+ const buffer = Buffer.from(this.tailText, "utf-8");
1089
+ if (buffer.length <= this.maxRollingBytes) {
968
1090
  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
969
- index 17af938..6d97caa 100644
1091
+ index 0877ee7..faa2559 100644
970
1092
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/core/trust-manager.js
971
1093
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/core/trust-manager.js
972
1094
  @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
@@ -976,9 +1098,9 @@ index 17af938..6d97caa 100644
976
1098
  -import { CONFIG_DIR_NAME } from "../config.js";
977
1099
  +import { projectConfigDirCandidates } from "../config.js";
978
1100
  import { canonicalizePath, resolvePath } from "../utils/paths.js";
1101
+ import { stripBom } from "../utils/text.js";
979
1102
  const TRUST_REQUIRING_PROJECT_CONFIG_RESOURCES = [
980
- "settings.json",
981
- @@ -150,9 +150,13 @@ export function hasTrustRequiringProjectResources(cwd) {
1103
+ @@ -151,9 +151,13 @@ export function hasTrustRequiringProjectResources(cwd) {
982
1104
  const homeDir = canonicalizePath(resolvePath(process.env.HOME || homedir()));
983
1105
  const userAgentsSkillsDir = join(homeDir, ".agents", "skills");
984
1106
  let currentDir = canonicalizePath(resolvePath(cwd));
@@ -996,7 +1118,7 @@ index 17af938..6d97caa 100644
996
1118
  while (true) {
997
1119
  const agentsSkillsDir = join(currentDir, ".agents", "skills");
998
1120
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/index.d.ts b/node_modules/@earendil-works/pi-coding-agent/dist/index.d.ts
999
- index 7d3e24c..578ada5 100644
1121
+ index 3e9241d..bfbfcbf 100644
1000
1122
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/index.d.ts
1001
1123
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/index.d.ts
1002
1124
  @@ -1,7 +1,7 @@
@@ -1007,9 +1129,9 @@ index 7d3e24c..578ada5 100644
1007
1129
  +export { AuthStorage, readStoredCredential } from "./core/auth-storage.ts";
1008
1130
  export { type BranchPreparation, type BranchSummaryResult, type CollectEntriesResult, type CompactionResult, type CutPointResult, calculateContextTokens, collectEntriesForBranchSummary, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, type FileOperations, findCutPoint, findTurnStartIndex, type GenerateBranchSummaryOptions, generateBranchSummary, generateSummary, generateSummaryWithUsage, getLastAssistantUsage, prepareBranchEntries, serializeConversation, shouldCompact, } from "./core/compaction/index.ts";
1009
1131
  export { createEventBus, type EventBus, type EventBusController } from "./core/event-bus.ts";
1010
- export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextUsage, CustomToolCallEvent, EditToolCallEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, GrepToolCallEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, MarkdownTransformContext, MarkdownTransformer, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, RegisteredCommand, RegisteredTool, ResolvedCommand, SessionBeforeCompactEvent, SessionBeforeForkEvent, SessionBeforeSwitchEvent, SessionBeforeTreeEvent, SessionCompactEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SlashCommandInfo, SlashCommandSource, SourceInfo, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, TurnEndEvent, TurnStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, } from "./core/extensions/index.ts";
1132
+ export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextUsage, CustomToolCallEvent, EditToolCallEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, GrepToolCallEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, MarkdownTransformContext, MarkdownTransformer, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, PowerShellToolCallEvent, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, RegisteredCommand, RegisteredTool, ResolvedCommand, SessionBeforeCompactEvent, SessionBeforeForkEvent, SessionBeforeSwitchEvent, SessionBeforeTreeEvent, SessionCompactEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SlashCommandInfo, SlashCommandSource, SourceInfo, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, TurnEndEvent, TurnStartEvent, UIPromptEndEvent, UIPromptKind, UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, } from "./core/extensions/index.ts";
1011
1133
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/index.js b/node_modules/@earendil-works/pi-coding-agent/dist/index.js
1012
- index 76d728e..2b2c892 100644
1134
+ index 01017cd..c2b8722 100644
1013
1135
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/index.js
1014
1136
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/index.js
1015
1137
  @@ -3,7 +3,7 @@ export { parseArgs } from "./cli/args.js";
@@ -1022,10 +1144,10 @@ index 76d728e..2b2c892 100644
1022
1144
  export { calculateContextTokens, collectEntriesForBranchSummary, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, findCutPoint, findTurnStartIndex, generateBranchSummary, generateSummary, generateSummaryWithUsage, getLastAssistantUsage, prepareBranchEntries, serializeConversation, shouldCompact, } from "./core/compaction/index.js";
1023
1145
  export { createEventBus } from "./core/event-bus.js";
1024
1146
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/main.js b/node_modules/@earendil-works/pi-coding-agent/dist/main.js
1025
- index 5ff9c80..5b91d5f 100644
1147
+ index 940e0f4..c0f3d11 100644
1026
1148
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/main.js
1027
1149
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/main.js
1028
- @@ -786,7 +786,58 @@ export async function main(args, options) {
1150
+ @@ -795,7 +795,58 @@ export async function main(args, options) {
1029
1151
  if (exitCode !== 0) {
1030
1152
  process.exitCode = exitCode;
1031
1153
  }
@@ -1086,7 +1208,7 @@ index 5ff9c80..5b91d5f 100644
1086
1208
  //# sourceMappingURL=main.js.map
1087
1209
 
1088
1210
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js b/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js
1089
- index e3a5ccf..a561a8a 100644
1211
+ index 072268a..472ee6d 100644
1090
1212
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js
1091
1213
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/migrations.js
1092
1214
  @@ -4,7 +4,7 @@
@@ -1096,9 +1218,9 @@ index e3a5ccf..a561a8a 100644
1096
1218
  -import { CONFIG_DIR_NAME, getAgentDir, getBinDir } from "./config.js";
1097
1219
  +import { getAgentDir, getBinDir, projectConfigDirs } from "./config.js";
1098
1220
  import { migrateKeybindingsConfig } from "./core/keybindings.js";
1221
+ import { stripBom } from "./utils/text.js";
1099
1222
  const MIGRATION_GUIDE_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/CHANGELOG.md#extensions-migration";
1100
- const EXTENSIONS_DOC_URL = "https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/extensions.md";
1101
- @@ -230,14 +230,17 @@ function checkDeprecatedExtensionDirs(baseDir, label) {
1223
+ @@ -231,14 +231,17 @@ function checkDeprecatedExtensionDirs(baseDir, label) {
1102
1224
  */
1103
1225
  function migrateExtensionSystem(cwd) {
1104
1226
  const agentDir = getAgentDir();
@@ -1410,16 +1532,18 @@ index 1d9f046..c0326e3 100644
1410
1532
  return lines;
1411
1533
  }
1412
1534
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
1413
- index 3f93cc6..50a2a99 100644
1535
+ index c33fc39..060a38f 100644
1414
1536
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
1415
1537
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/tool-execution.js
1416
- @@ -1,8 +1,35 @@
1538
+ @@ -1,10 +1,37 @@
1417
1539
  -import { Box, Container, getCapabilities, Image, Spacer, Text } from "@earendil-works/pi-tui";
1418
1540
  +import { Box, Container, getCapabilities, Image, Spacer, visibleWidth, Text } from "@earendil-works/pi-tui";
1419
1541
  import { createAllToolDefinitions } from "../../../core/tools/index.js";
1420
1542
  import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.js";
1421
1543
  import { convertToPng } from "../../../utils/image-convert.js";
1422
1544
  import { theme } from "../theme/theme.js";
1545
+ import { keyHint } from "./keybinding-hints.js";
1546
+ const FALLBACK_PREVIEW_LINES = 10;
1423
1547
  +/**
1424
1548
  + * Privateer: mark a failed tool call inline on its title line.
1425
1549
  + *
@@ -1450,7 +1574,7 @@ index 3f93cc6..50a2a99 100644
1450
1574
  export class ToolExecutionComponent extends Container {
1451
1575
  contentBox;
1452
1576
  contentText;
1453
- @@ -43,8 +70,13 @@ export class ToolExecutionComponent extends Container {
1577
+ @@ -45,8 +72,13 @@ export class ToolExecutionComponent extends Container {
1454
1578
  // Always create all shell variants. contentBox is used for default renderer-based composition.
1455
1579
  // selfRenderContainer is used when the tool renders its own framing.
1456
1580
  // contentText is reserved for generic fallback rendering when no tool definition exists.
@@ -1466,7 +1590,7 @@ index 3f93cc6..50a2a99 100644
1466
1590
  this.selfRenderContainer = new Container();
1467
1591
  if (this.hasRendererDefinition()) {
1468
1592
  this.addChild(this.getRenderShell() === "self" ? this.selfRenderContainer : this.contentBox);
1469
- @@ -106,6 +138,13 @@ export class ToolExecutionComponent extends Container {
1593
+ @@ -108,6 +140,13 @@ export class ToolExecutionComponent extends Container {
1470
1594
  createCallFallback() {
1471
1595
  return new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0);
1472
1596
  }
@@ -1480,7 +1604,7 @@ index 3f93cc6..50a2a99 100644
1480
1604
  createResultFallback() {
1481
1605
  const output = this.getTextOutput();
1482
1606
  if (!output) {
1483
- @@ -218,19 +257,22 @@ export class ToolExecutionComponent extends Container {
1607
+ @@ -227,19 +266,22 @@ export class ToolExecutionComponent extends Container {
1484
1608
  renderContainer.clear();
1485
1609
  const callRenderer = this.getCallRenderer();
1486
1610
  if (!callRenderer) {
@@ -1506,7 +1630,7 @@ index 3f93cc6..50a2a99 100644
1506
1630
  hasContent = true;
1507
1631
  }
1508
1632
  }
1509
- @@ -302,7 +344,8 @@ export class ToolExecutionComponent extends Container {
1633
+ @@ -311,7 +353,8 @@ export class ToolExecutionComponent extends Container {
1510
1634
  return getRenderedTextOutput(this.result, this.showImages);
1511
1635
  }
1512
1636
  formatToolExecution() {
@@ -1517,19 +1641,19 @@ index 3f93cc6..50a2a99 100644
1517
1641
  if (content) {
1518
1642
  text += `\n\n${content}`;
1519
1643
  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
1520
- index 42e655d..606c7cc 100644
1644
+ index a27754f..61a1fff 100644
1521
1645
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
1522
1646
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js
1523
1647
  @@ -10,7 +10,7 @@ import * as TuiLayouts from "@earendil-works/pi-tui";
1524
- import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@earendil-works/pi-tui";
1648
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Markdown, matchesKey, ProcessTerminal, Spacer, setCapabilityOverrides, setKeybindings, Text, TruncatedText, TuiAltScreen, TuiMainScreen, visibleWidth, } from "@earendil-works/pi-tui";
1525
1649
  import chalk from "chalk";
1526
- import { spawn, spawnSync } from "child_process";
1527
- -import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
1528
- +import { APP_NAME, APP_TITLE, formatProjectConfigDirNames, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, getShareViewerUrl, VERSION, } from "../../config.js";
1650
+ import { spawn } from "child_process";
1651
+ -import { APP_NAME, APP_TITLE, CONFIG_DIR_NAME, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, VERSION, } from "../../config.js";
1652
+ +import { APP_NAME, APP_TITLE, formatProjectConfigDirNames, getAgentDir, getAuthPath, getDebugLogPath, getDocsPath, VERSION, } from "../../config.js";
1529
1653
  import { parseSkillBlock } from "../../core/agent-session.js";
1530
1654
  import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
1531
1655
  import { CACHE_TTL_MS, collectCacheMisses, computeCacheWaste, detectCacheMiss, } from "../../core/cache-stats.js";
1532
- @@ -119,7 +119,12 @@ export function formatResumeCommand(sessionManager) {
1656
+ @@ -126,7 +126,12 @@ export function formatResumeCommand(sessionManager) {
1533
1657
  const sessionFile = sessionManager.getSessionFile();
1534
1658
  if (!sessionFile || !fs.existsSync(sessionFile))
1535
1659
  return undefined;
@@ -1543,7 +1667,7 @@ index 42e655d..606c7cc 100644
1543
1667
  if (!sessionManager.usesDefaultSessionDir()) {
1544
1668
  args.push("--session-dir", quoteIfNeeded(sessionManager.getSessionDir()));
1545
1669
  }
1546
- @@ -207,6 +212,54 @@ export function createInteractiveTuiReference(getTui) {
1670
+ @@ -232,6 +237,54 @@ export function createInteractiveTuiReference(getTui) {
1547
1671
  getPrototypeOf: () => Reflect.getPrototypeOf(getTui()),
1548
1672
  });
1549
1673
  }
@@ -1598,7 +1722,7 @@ index 42e655d..606c7cc 100644
1598
1722
  export class InteractiveMode {
1599
1723
  runtimeHost;
1600
1724
  renderer;
1601
- @@ -405,9 +458,15 @@ export class InteractiveMode {
1725
+ @@ -439,9 +492,15 @@ export class InteractiveMode {
1602
1726
  }
1603
1727
  getBuiltInCommandConflictDiagnostics(extensionRunner) {
1604
1728
  const builtinNames = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name));
@@ -1615,7 +1739,7 @@ index 42e655d..606c7cc 100644
1615
1739
  .map((command) => ({
1616
1740
  type: "warning",
1617
1741
  message: command.invocationName === command.name
1618
- @@ -754,20 +813,18 @@ export class InteractiveMode {
1742
+ @@ -815,20 +874,18 @@ export class InteractiveMode {
1619
1743
  this.showNewVersionNotification(newRelease);
1620
1744
  }
1621
1745
  });
@@ -1648,7 +1772,7 @@ index 42e655d..606c7cc 100644
1648
1772
  // Check tmux keyboard setup asynchronously
1649
1773
  this.checkTmuxKeyboardSetup().then((warning) => {
1650
1774
  if (warning) {
1651
- @@ -2310,7 +2367,17 @@ export class InteractiveMode {
1775
+ @@ -2393,7 +2450,17 @@ export class InteractiveMode {
1652
1776
  if (text === "/model" || text.startsWith("/model ")) {
1653
1777
  const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
1654
1778
  this.editor.setText("");
@@ -1666,8 +1790,8 @@ index 42e655d..606c7cc 100644
1666
1790
  + }
1667
1791
  return;
1668
1792
  }
1669
- if (text === "/export" || text.startsWith("/export ")) {
1670
- @@ -2376,12 +2443,42 @@ export class InteractiveMode {
1793
+ if (text === "/thinking" || text.startsWith("/thinking ")) {
1794
+ @@ -2465,12 +2532,42 @@ export class InteractiveMode {
1671
1795
  if (text === "/login" || text.startsWith("/login ")) {
1672
1796
  const providerRef = text.startsWith("/login ") ? text.slice(7).trim() : undefined;
1673
1797
  this.editor.setText("");
@@ -1712,7 +1836,7 @@ index 42e655d..606c7cc 100644
1712
1836
  return;
1713
1837
  }
1714
1838
  if (text === "/new") {
1715
- @@ -3024,7 +3121,7 @@ export class InteractiveMode {
1839
+ @@ -3164,7 +3261,7 @@ export class InteractiveMode {
1716
1840
  if (this.chatContainer.children.length > 0) {
1717
1841
  this.chatContainer.addChild(new Spacer(1));
1718
1842
  }
@@ -1721,7 +1845,7 @@ index 42e655d..606c7cc 100644
1721
1845
  }
1722
1846
  async getUserInput() {
1723
1847
  const queuedInput = this.pendingUserInputs.shift();
1724
- @@ -3363,7 +3460,17 @@ export class InteractiveMode {
1848
+ @@ -3504,7 +3601,17 @@ export class InteractiveMode {
1725
1849
  }
1726
1850
  showError(errorMessage) {
1727
1851
  this.chatContainer.addChild(new Spacer(1));
@@ -1741,10 +1865,10 @@ index 42e655d..606c7cc 100644
1741
1865
  }
1742
1866
  showWarning(warningMessage) {
1743
1867
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
1744
- index 9db9cbd..b370180 100644
1868
+ index 01d1e02..c591e1d 100644
1745
1869
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
1746
1870
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/dark.json
1747
- @@ -39,9 +39,9 @@
1871
+ @@ -41,9 +41,9 @@
1748
1872
  "customMessageBg": "customMsgBg",
1749
1873
  "customMessageText": "text",
1750
1874
  "customMessageLabel": "#9575cd",
@@ -1758,10 +1882,10 @@ index 9db9cbd..b370180 100644
1758
1882
  "toolOutput": "gray",
1759
1883
 
1760
1884
  diff --git a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json
1761
- index 74ef3d1..709a21d 100644
1885
+ index 0fde42b..cc3b7c5 100644
1762
1886
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json
1763
1887
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/light.json
1764
- @@ -38,9 +38,9 @@
1888
+ @@ -40,9 +40,9 @@
1765
1889
  "customMessageBg": "customMsgBg",
1766
1890
  "customMessageText": "text",
1767
1891
  "customMessageLabel": "#7e57c2",
@@ -1775,11 +1899,11 @@ index 74ef3d1..709a21d 100644
1775
1899
  "toolOutput": "mediumGray",
1776
1900
 
1777
1901
  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
1778
- index 4230b18..f47e80f 100644
1902
+ index 3f101e6..51c7f73 100644
1779
1903
  --- a/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js
1780
1904
  +++ b/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js
1781
- @@ -3,7 +3,7 @@ import { Markdown } from "@earendil-works/pi-tui";
1782
- import chalk from "chalk";
1905
+ @@ -5,7 +5,7 @@ import chalk from "chalk";
1906
+ import lockfile from "proper-lockfile";
1783
1907
  import { selectConfig } from "./cli/config-selector.js";
1784
1908
  import { createProjectTrustContext } from "./cli/project-trust.js";
1785
1909
  -import { APP_NAME, CONFIG_DIR_NAME, detectInstallMethod, getAgentDir, getPackageDir, getSelfUpdateCommand, getSelfUpdateUnavailableInstruction, PACKAGE_NAME, VERSION, } from "./config.js";
@@ -1787,7 +1911,7 @@ index 4230b18..f47e80f 100644
1787
1911
  import { ModelRuntime } from "./core/model-runtime.js";
1788
1912
  import { DefaultPackageManager } from "./core/package-manager.js";
1789
1913
  import { resolveProjectTrusted } from "./core/project-trust.js";
1790
- @@ -60,7 +60,7 @@ Without -l, starts in global settings (~/${CONFIG_DIR_NAME}/agent/settings.json)
1914
+ @@ -221,7 +221,7 @@ Without -l, starts in global settings (~/${CONFIG_DIR_NAME}/agent/settings.json)
1791
1915
  Press Tab in the TUI to switch between global and project-local modes.
1792
1916
 
1793
1917
  Options:
@@ -1796,7 +1920,7 @@ index 4230b18..f47e80f 100644
1796
1920
  -a, --approve Trust project-local files for this command with -l
1797
1921
  -na, --no-approve Ignore project-local files for this command with -l
1798
1922
  `);
1799
- @@ -74,7 +74,7 @@ function printPackageCommandHelp(command) {
1923
+ @@ -235,7 +235,7 @@ function printPackageCommandHelp(command) {
1800
1924
  Install a package and add it to settings.
1801
1925
 
1802
1926
  Options:
@@ -1805,7 +1929,7 @@ index 4230b18..f47e80f 100644
1805
1929
  -a, --approve Trust project-local files for this command
1806
1930
  -na, --no-approve Ignore project-local files for this command
1807
1931
 
1808
- @@ -95,7 +95,7 @@ Remove a package and its source from settings.
1932
+ @@ -256,7 +256,7 @@ Remove a package and its source from settings.
1809
1933
  Alias: ${APP_NAME} uninstall <source> [-l]
1810
1934
 
1811
1935
  Options:
@@ -1,28 +1,31 @@
1
1
  {
2
2
  "$comment": "The one list of extensions Privateer ships. Read by bin/privateer-launch.mjs (shim install, dependency-free, pre-patch) and src/config/moatManifest.ts (RESERVED + profile composition). JSON so both a plain .mjs and the TS side can read it without a build step. See moatManifest.ts for the field contract.",
3
- "shims": [
4
- { "name": "privateer-brand", "entry": "extensions/privateer-brand.ts", "note": "banner, ⚓ badge, /signin /signout" },
5
- { "name": "privateer-context", "entry": "extensions/privateer-context.ts", "note": "PRIVATEER.md context + /init" },
6
- { "name": "privateer-gate", "entry": "extensions/privateer-gate.ts", "note": "the permission gate (moat)" },
7
- { "name": "privateer-account", "entry": "extensions/privateer-account.ts", "note": "account inference provider" },
8
- { "name": "privateer-models", "entry": "extensions/privateer-models.ts", "note": "/models picker w/ privacy shields" },
9
- { "name": "privateer-posture", "entry": "extensions/privateer-posture.ts", "note": "live attestation shield" },
10
- { "name": "privateer-tools", "entry": "extensions/privateer-tools.ts", "note": "Privateer tool pack" },
11
- { "name": "privateer-privacy", "entry": "extensions/privateer-privacy.ts", "note": "pi-privacy + account tier resolver" },
12
- { "name": "privateer-connect", "entry": "extensions/privateer-connect.ts", "note": "/connect — MCP connector manager" },
13
- { "name": "privateer-media", "entry": "extensions/privateer-media.ts", "note": "image/video/speech/music + ffmpeg compose" },
14
- { "name": "privateer-desktop", "entry": "extensions/privateer-desktop.ts", "note": "/desktop — open the Privateer desktop app" },
15
- { "name": "privateer-hints", "entry": "extensions/privateer-hints.ts", "note": "rotating tips in the working line + /hints" },
16
- { "name": "privateer-update", "entry": "extensions/privateer-update.ts", "note": "tool pack updates in place — banner flag + /update" },
17
- { "name": "privateer-speak", "entry": "extensions/privateer-speak.ts", "note": "spoken responses (/speak) + voice input (/talk) — pi-speak + confidential account TTS/STT" },
18
- { "name": "privateer-web", "entry": "extensions/privateer-web.ts", "note": "web_search/web_fetch — account search when signed in, else the user's own provider (rpiv-web-tools)" },
19
- { "name": "rpiv-ask-user-question", "dep": ["@juicesharp/rpiv-ask-user-question", "index.ts"], "note": "ask_user_question" },
20
- { "name": "pi-mcp-adapter", "dep": ["pi-mcp-adapter", "index.ts"], "note": "MCP servers as first-class tools" },
21
- { "name": "pi-subagents", "dep": ["pi-subagents", "src", "extension", "index.ts"], "note": "bounded parallel sub-agents" }
22
- ],
23
- "retired": ["pi-privacy", "pi-web-access", "pi-hypa", "rpiv-web-tools"],
24
3
  "reservedAliases": [
25
4
  "@juicesharp/rpiv-web-tools",
26
- "@juicesharp/rpiv-ask-user-question"
5
+ "@juicesharp/rpiv-ask-user-question",
6
+ "background-tasks"
7
+ ],
8
+ "retired": ["pi-privacy", "pi-web-access", "pi-hypa", "rpiv-web-tools"],
9
+ "shims": [
10
+ { "entry": "extensions/privateer-brand.ts", "name": "privateer-brand", "note": "banner, ⚓ badge, /signin /signout" },
11
+ { "entry": "extensions/privateer-context.ts", "name": "privateer-context", "note": "PRIVATEER.md context + /init" },
12
+ { "entry": "extensions/privateer-gate.ts", "name": "privateer-gate", "note": "the permission gate (moat)" },
13
+ { "entry": "extensions/privateer-account.ts", "name": "privateer-account", "note": "account inference provider" },
14
+ { "entry": "extensions/privateer-models.ts", "name": "privateer-models", "note": "/models picker w/ privacy shields" },
15
+ { "entry": "extensions/privateer-posture.ts", "name": "privateer-posture", "note": "live attestation shield" },
16
+ { "entry": "extensions/privateer-tools.ts", "name": "privateer-tools", "note": "Privateer tool pack" },
17
+ { "entry": "extensions/privateer-privacy.ts", "name": "privateer-privacy", "note": "pi-privacy + account tier resolver" },
18
+ { "entry": "extensions/privateer-connect.ts", "name": "privateer-connect", "note": "/connect — MCP connector manager" },
19
+ { "entry": "extensions/privateer-media.ts", "name": "privateer-media", "note": "image/video/speech/music + ffmpeg compose" },
20
+ { "entry": "extensions/privateer-desktop.ts", "name": "privateer-desktop", "note": "/desktop — open the Privateer desktop app" },
21
+ { "entry": "extensions/privateer-hints.ts", "name": "privateer-hints", "note": "rotating tips in the working line + /hints" },
22
+ { "entry": "extensions/privateer-update.ts", "name": "privateer-update", "note": "tool pack updates in place — banner flag + /update" },
23
+ { "entry": "extensions/privateer-speak.ts", "name": "privateer-speak", "note": "spoken responses (/speak) + voice input (/talk) — pi-speak + confidential account TTS/STT" },
24
+ { "entry": "extensions/privateer-web.ts", "name": "privateer-web", "note": "web_search/web_fetch — account search when signed in, else the user's own provider (rpiv-web-tools)" },
25
+ { "dep": ["@juicesharp/rpiv-ask-user-question", "index.ts"], "name": "rpiv-ask-user-question", "note": "ask_user_question" },
26
+ { "dep": ["pi-mcp-adapter", "index.ts"], "name": "pi-mcp-adapter", "note": "MCP servers as first-class tools" },
27
+ { "dep": ["pi-subagents", "src", "extension", "index.ts"], "name": "pi-subagents", "note": "bounded parallel sub-agents" },
28
+ { "dep": ["pi-background-tasks", "extensions", "anthropic-attribution.ts"], "name": "anthropic-attribution", "note": "Anthropic OAuth attribution and prompt sanitization" },
29
+ { "dep": ["pi-background-tasks", "extensions", "background-tasks.ts"], "name": "pi-background-tasks", "note": "durable background tasks, delegated agents, and fusion" }
27
30
  ]
28
31
  }
package/src/context.ts CHANGED
@@ -93,6 +93,15 @@ export function discoverContextFiles(cwd: string = process.cwd()): ContextFile[]
93
93
  // A unique sentinel opening the injected block, so before_agent_start can no-op if the
94
94
  // block is already present in the chained system prompt (defensive against re-entrancy).
95
95
  export const CONTEXT_BLOCK_MARKER = "<!-- privateer:PRIVATEER.md -->";
96
+ export const RUNTIME_GUIDELINES_MARKER = "<!-- privateer:runtime-guidelines -->";
97
+
98
+ export function runtimeGuidelinesBlock(): string {
99
+ return `\n\n${RUNTIME_GUIDELINES_MARKER}\n<environment_guidelines>
100
+ - Node.js environment: Node.js (v22+) is guaranteed to be available in Privateer. Prefer \`node -e "..."\` or small Node.js scripts for quick scripting, calculations, or JSON processing instead of assuming \`python\` or \`python3\` is installed.
101
+ - Search fallback: If \`rg\` (ripgrep) is missing or returns "command not found", fall back to standard POSIX \`grep -rn <pattern> <path>\` or \`find <path>\`.
102
+ - Missing system dependencies: If an essential external tool (e.g. \`git\`, \`python\`, \`rg\`) is missing and needed, check its presence (\`command -v <tool>\`), explain clearly what is missing, and offer to install it using the host package manager (e.g. \`brew install\`, \`xcode-select --install\`, \`winget install\`, \`apt install\`) upon user approval.
103
+ </environment_guidelines>\n`;
104
+ }
96
105
 
97
106
  // Format the discovered files into a system-prompt fragment using the same framing Pi
98
107
  // applies to AGENTS.md (see core/system-prompt.js), so the model can't tell the two
@@ -11,6 +11,7 @@
11
11
  // to "deny".
12
12
 
13
13
  import { randomUUID } from "node:crypto";
14
+ import { noQuarterActive, setNoQuarter } from "../permissions/noQuarter.ts";
14
15
  import type { EngineEvent } from "../engine/events.ts";
15
16
  import type { PermissionRequest } from "../permissions/gate.ts";
16
17
  import type { AskOutcome } from "../permissions/modeGate.ts";
@@ -169,7 +170,7 @@ export interface RemoteBridgeConfig {
169
170
  export class RemoteBridge {
170
171
  private relay?: RelayLike;
171
172
  private remote = false;
172
- private noQuarter = false;
173
+ private noQuarter = noQuarterActive();
173
174
  private readonly pending = new Map<string, (d: AskOutcome) => void>();
174
175
  private readonly pendingSelects = new Map<string, (v: string | null) => void>();
175
176
  private readonly pendingInputs = new Map<string, (v: string | null) => void>();
@@ -282,9 +283,15 @@ export class RemoteBridge {
282
283
  onFilesSearch: (id, query) => this.cfg.onFilesSearch?.(id, query),
283
284
  onNoQuarter: (on) => {
284
285
  this.noQuarter = on;
286
+ setNoQuarter(on);
285
287
  this.relay?.sendNoQuarter(on); // echo the ack back so the app's toggle syncs
286
288
  },
287
- onControllerAttached: () => this.cfg.onControllerAttached?.(),
289
+ onControllerAttached: () => {
290
+ if (this.noQuarter || noQuarterActive()) {
291
+ this.relay?.sendNoQuarter(true);
292
+ }
293
+ this.cfg.onControllerAttached?.();
294
+ },
288
295
  // The app left while we're still running. Same posture as a dropped socket: stop
289
296
  // treating the turn as remote (the gate must not wait on a controller that isn't
290
297
  // there) and fail every pending approval closed. The turn itself keeps going —
@@ -310,7 +317,7 @@ export class RemoteBridge {
310
317
  // ── gate hooks (passed into the GateController) ─────────────────────────────
311
318
 
312
319
  getRemote = (): boolean => this.remote;
313
- getNoQuarter = (): boolean => this.noQuarter;
320
+ getNoQuarter = (): boolean => this.noQuarter || noQuarterActive();
314
321
 
315
322
  // The gate's remote approver: relay the request to the app and await its
316
323
  // allow/deny. Fail closed if no controller, on abort, or on disconnect. (The gate