@xynogen/pix-optimizer 1.1.29 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ There is no text-arg form: the overlay is the only UI. Selecting a value calls
23
23
  the tool's `run()` handler, which persists the new value and repaints the
24
24
  shared status cell. Headless/test fallbacks print a plain status summary.
25
25
 
26
- State persists to `~/.pi/agent/optimizer.json` (caveman/rtk/ponytail). **Initial values** for a new session can be set in the `optimizer` section of `~/.pi/agent/pix.json` — the runtime toggle via `/optimizer` still persists changes to `optimizer.json` as before.
26
+ State (caveman/rtk/ponytail) persists in the `optimizer` section of `~/.pi/agent/pix.json`. That section is the only place it is stored; the old `optimizer.json` file is imported once by pix-runtime and never written again.
27
27
 
28
28
  ## Status bar
29
29
 
@@ -49,17 +49,21 @@ Icons follow the **global** `pix-pretty` mode. Set it via `/pix` or the
49
49
 
50
50
  ### Caveman Mode (`Cv`)
51
51
 
52
- Cuts ~75% of output tokens while keeping full technical accuracy.
52
+ Makes replies follow [ASD-STE100 Simplified Technical English](https://asd-ste100.org).
53
+ Two layers: Layer 1 sets the words and sentences, Layer 2 sets the reply shape
54
+ (next action first, numbered steps, no preamble or closer). Both govern prose
55
+ only, not code or command syntax. Keeps the article, unlike the old
56
+ article-dropping caveman prompt.
53
57
 
54
- | Level | Description |
55
- |-------|------------------------------|
56
- | lite | Professional, no fluff |
57
- | full | Classic caveman |
58
- | ultra | Maximum compression |
59
- | micro | Experimental prompt-minimized |
58
+ | Level | Description |
59
+ |-------|-----------------------------------|
60
+ | lite | STE-flavored words, light shape |
61
+ | full | STE words + full reply shape |
62
+ | ultra | Strict STE + full reply shape |
63
+ | micro | Experimental prompt-minimized |
60
64
 
61
65
  The `/optimizer` overlay opens a settings dialog when needed. Default level
62
- for new sessions is restored from `~/.pi/agent/optimizer.json`.
66
+ for new sessions is restored from `pix.json` → `optimizer`.
63
67
 
64
68
  ### RTK Tool Rewriting (`Rk`)
65
69
 
@@ -73,14 +77,26 @@ Two layers, both active automatically:
73
77
  `||`, `;` and `|`, and every known segment is prefixed** — e.g.
74
78
  `git add . && git push` becomes `rtk git add . && rtk git push`.
75
79
  Operators inside quotes are ignored, and unparseable commands are left
76
- untouched. Falls back gracefully when the `rtk` binary is missing
77
- (warns once).
80
+ untouched. Commands are never rewritten while `rtk` is missing.
78
81
 
79
- **Requirement:** the `rtk` binary must be on `PATH`.
82
+ **Binary:** pix finds `rtk` in this order: `binary.json` → `~/.pi/agent/bin` →
83
+ PATH (see pix-runtime, Binaries).
80
84
 
81
- ```bash
82
- cargo install rtk-ai
83
- ```
85
+ When RTK is on and `rtk` is missing, pix downloads the latest official
86
+ release from `rtk-ai/rtk` into `~/.pi/agent/bin` the first time Pi loads. It
87
+ verifies the download against the release's `checksums.txt`.
88
+
89
+ The download is visible: a footer status while it runs, then one line naming
90
+ the version and source. It never blocks startup or a tool call.
91
+
92
+ The download is skipped when:
93
+
94
+ - `PI_OFFLINE` is set;
95
+ - RTK is off in `/optimizer`;
96
+ - `binary.json` names a path that doesn't exist.
97
+
98
+ If you pin a path in `binary.json`, rewritten commands use that quoted path
99
+ instead of a bare `rtk`.
84
100
 
85
101
  ### Ponytail Mode (`Pt`)
86
102
 
@@ -101,7 +117,7 @@ PATH dependency.
101
117
 
102
118
  ## Configuration via `pix.json`
103
119
 
104
- Set the initial optimizer state for new sessions in `~/.pi/agent/pix.json`. These values are applied once at session start; subsequent changes via `/optimizer` persist to `optimizer.json` and take precedence.
120
+ Optimizer state lives in `~/.pi/agent/pix.json`. You can edit it by hand, or use `/optimizer`, which writes to the same section.
105
121
 
106
122
  ```jsonc
107
123
  {
@@ -113,18 +129,6 @@ Set the initial optimizer state for new sessions in `~/.pi/agent/pix.json`. Thes
113
129
  }
114
130
  ```
115
131
 
116
- ## Installation
117
-
118
- ```bash
119
- pi install npm:@xynogen/pix-optimizer
120
- ```
121
-
122
- > Also included in [`@xynogen/pix-core`](https://www.npmjs.com/package/@xynogen/pix-core):
123
- >
124
- > ```bash
125
- > pi install npm:@xynogen/pix-core
126
- > ```
127
-
128
132
  ## Architecture
129
133
 
130
134
  | File | Role |
@@ -135,7 +139,7 @@ pi install npm:@xynogen/pix-optimizer
135
139
  | `src/caveman.ts` | Caveman logic, levels, prompt |
136
140
  | `src/rtk.ts` | RTK prompt + bash command rewriting |
137
141
  | `src/ponytail.ts` | Ponytail logic, levels, prompt |
138
- | `src/persist.ts` | Disk-backed `~/.pi/agent/optimizer.json` persistence; seeds initial state from `pix.json` |
142
+ | `src/persist.ts` | Reads/writes the `optimizer` section of `pix.json` via pix-runtime |
139
143
  | `src/tool-result-filter.ts` | Strips model-guidance warnings from tool_result |
140
144
 
141
145
  Each tool registers its own lifecycle hooks and exposes an `OptimizerHandle`
@@ -168,16 +172,28 @@ All upstreams are MIT licensed. No codebase was copied directly — the logic wa
168
172
  rewritten and combined into a single extension with a unified `/optimizer`
169
173
  command and shared status bar. This package does not sync back to any upstream.
170
174
 
171
- ## Full distro
175
+ ## Install
176
+
177
+ ```bash
178
+ pi install npm:@xynogen/pix-optimizer
179
+ ```
180
+
181
+ > Bundled in [`@xynogen/pix-core`](https://www.npmjs.com/package/@xynogen/pix-core). Install it alone only if you do not use pix-core.
172
182
 
173
- Source: [github.com/xynogen/pix-mono](https://github.com/xynogen/pix-mono)
183
+ ## Full distro
174
184
 
175
- To install the complete pix suite (all packages + Pi itself):
185
+ This package is part of [Pix](https://github.com/xynogen/pix-mono). The installer sets up Pi and the full distro. See [Install](https://github.com/xynogen/pix-mono#install) for the notes for each OS.
176
186
 
177
187
  ```bash
188
+ # Linux / macOS
178
189
  curl -fsSL https://raw.githubusercontent.com/xynogen/pix-mono/main/scripts/install.sh | sh
179
190
  ```
180
191
 
192
+ ```powershell
193
+ # Windows
194
+ irm https://raw.githubusercontent.com/xynogen/pix-mono/main/scripts/install.ps1 | iex
195
+ ```
196
+
181
197
  ## License
182
198
 
183
- MIT
199
+ MIT. See [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-optimizer",
3
- "version": "1.1.29",
3
+ "version": "1.3.0",
4
4
  "description": "Performance optimization suite for Pi Coding Agent - caveman mode + RTK tool rewriting + ponytail lazy-dev mode",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -43,7 +43,8 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@xynogen/pix-pretty": "^1.11.2"
46
+ "@xynogen/pix-pretty": "^1.29.0",
47
+ "@xynogen/pix-runtime": "^0.12.2"
47
48
  },
48
49
  "peerDependencies": {
49
50
  "@earendil-works/pi-coding-agent": "*",
package/src/caveman.ts CHANGED
@@ -40,45 +40,67 @@ export const STATUS_LABELS: Record<Exclude<Level, "off">, string> = {
40
40
  // ── Prompt fragments ──────────────────────────────────────────────────────────
41
41
 
42
42
  const BASE = `\
43
- IMPORTANT: You are in CAVEMAN MODE. Respond terse like smart caveman. \
44
- All technical substance stay. Only fluff die.
45
-
46
- Rules:
47
- - Drop articles (a/an/the), filler (just/really/basically/actually/simply), \
48
- pleasantries, hedging
49
- - Fragments OK. Short synonyms preferred. Technical terms exact
50
- - Code blocks unchanged. Errors quoted exact
51
- - Pattern: [thing] [action] [reason]. [next step].
52
-
53
- Bad: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
54
- Good: "Bug in auth middleware. Token expiry check use \`<\` not \`<=\`. Fix:"`;
55
-
56
- const MICRO_PROMPT = `# Token efficiency
57
- Respond like smart caveman. Cut all filler, keep technical substance.
58
- - Drop articles (a, an, the), filler (just, really, basically, actually).
59
- - Drop pleasantries (sure, certainly, happy to).
60
- - No hedging. Fragments fine. Short synonyms.
61
- - Technical terms stay exact. Code blocks unchanged.
62
- - Pattern: [thing] [action] [reason]. [next step].`;
43
+ IMPORTANT: You write in ASD-STE100 Simplified Technical English. Two layers stay on. \
44
+ Both layers govern prose only. They do not touch code, identifiers, or command syntax.
45
+
46
+ LAYER 1 — words and sentences:
47
+ - Use one name for one thing. Do not rotate check / verify / validate for the same action.
48
+ - Use the short common word: start (not initiate), use (not utilize), help (not facilitate), \
49
+ make sure (not ensure), do (not perform), give (not provide), before (not prior to), \
50
+ about (not regarding), get (not obtain), show (not demonstrate), also (not moreover).
51
+ - No marketing adjectives: seamless, robust, powerful, cutting-edge, effortless.
52
+ - Use the active voice. Write "the parser reads the file", not "the file is read by the parser".
53
+ - Use simple tenses only. Write "we received the report", not "we have received the report".
54
+ - Use a verb for an action. Write "analyze the log", not "perform an analysis of the log".
55
+ - No phrasal verbs: spin up, dive into, kick off, roll out.
56
+ - One instruction per sentence. Max 20 words for an instruction, max 25 words for other text.
57
+ - Keep the article (a, an, the). Do not drop words to compress.
58
+ - No semicolons. Write two sentences.
59
+
60
+ LAYER 2 — reply shape:
61
+ - Lead with the next action. The first line is a command, a path, or a snippet the reader can do now.
62
+ - Number a multi-step task. One bounded action per step.
63
+ - No preamble, no recap, no closer. Start with the answer. Stop when the answer is done.
64
+ - Cap an action list at five items. Split into "do now" and "later" past five.
65
+ - Give an estimate in concrete units (minutes, hours, days). Do not write "some work".
66
+ - Restate the state of multi-turn work. Write "step 3 of 5 done".
67
+ - Stay matter-of-fact about an error. Give the cause and the fix.
68
+
69
+ Bad: "Sure! I'd be happy to help. The issue you are experiencing is likely caused by..."
70
+ Good: "Bug in the auth middleware. The token expiry check uses \`<\`, not \`<=\`. Fix:"`;
71
+
72
+ const MICRO_PROMPT = `# STE output
73
+ Write in Simplified Technical English. Use short common words, the active voice, and simple tenses.
74
+ - One instruction per sentence, max 20 words. Keep the article (a, an, the).
75
+ - No phrasal verbs, no semicolons, no marketing adjectives.
76
+ - Reply shape: lead with the next action (a command, a path, or a snippet). No preamble, no closer.
77
+ - Number a multi-step task. Give an estimate in concrete units.
78
+ - Preserve code, identifiers, and error strings exactly.`;
63
79
 
64
80
  const INTENSITY: Record<Exclude<Level, "off" | "micro">, string> = {
65
81
  lite: `\
66
- No filler/hedging. Keep articles + full sentences. Professional but tight.
67
- Example: "Your component re-renders because you create a new object reference each render. Wrap it in \`useMemo\`."`,
82
+ STE-flavored words. Keep the sentence, tense, active-voice, and no-phrasal-verb discipline. \
83
+ Relax the strict dictionary. Apply the reply shape lightly: lead with the answer, no preamble or closer.
84
+ Example: "The component re-renders because you create a new object reference each render. Wrap it in \`useMemo\`."`,
68
85
 
69
86
  full: `\
70
- Drop articles, fragments OK, short synonyms.
71
- Example: "New object ref each render. Inline object prop = new ref = re-render. Wrap in \`useMemo\`."`,
87
+ STE-flavored words with the full Layer 2 shape. Number the steps. Cap the action list at five items. \
88
+ Restate the multi-turn state.
89
+ Example: "Wrap the prop in \`useMemo\`. Cause: a new object reference each render forces a re-render."`,
72
90
 
73
91
  ultra: `\
74
- Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y).
75
- Example: "Inline obj prop → new ref → re-render. \`useMemo\`."`,
92
+ Strict STE. Apply the strict word set (but not however, because not since, can not may, \
93
+ must not should) and both length caps. Keep the full Layer 2 shape.
94
+ Example: "Wrap the prop in \`useMemo\`. A new object reference each render forces a re-render."`,
76
95
  };
77
96
 
78
97
  const SAFETY = `\
79
- Auto-clarity: drop caveman for security warnings, irreversible action confirmations, \
80
- or when user is confused. Resume after.
81
- Boundaries: write normal code. Only compress explanations. "stop caveman" or "normal mode" reverts.`;
98
+ When to break Layer 2: if the user asks you to explain, explain in full, but keep the \
99
+ no-preamble and no-closer rules. Before a destructive action, confirm first — safety beats brevity. \
100
+ In a debug spiral, name the wrong assumption and ask one question. On real ambiguity, ask one short question.
101
+ Guards: never drop a fact, a number, a condition, or a scope qualifier to satisfy a length cap. \
102
+ Preserve code, identifiers, units, and error strings exactly.
103
+ Boundaries: this governs prose, not code. "stop caveman" or "normal mode" reverts.`;
82
104
 
83
105
  /**
84
106
  * Build the system prompt injection for a given level.
@@ -87,6 +109,9 @@ Boundaries: write normal code. Only compress explanations. "stop caveman" or "no
87
109
  export function buildPrompt(level: Level): string {
88
110
  if (level === "off") return "";
89
111
  if (level === "micro") return MICRO_PROMPT;
112
+ // ponytail: lite relaxes most of BASE, so it ships the short MICRO rules + its intensity (~800 tokens less).
113
+ if (level === "lite")
114
+ return [MICRO_PROMPT, "", `Intensity: ${INTENSITY.lite}`, "", SAFETY].join("\n");
90
115
  return [BASE, "", `Intensity: ${INTENSITY[level]}`, "", SAFETY].join("\n");
91
116
  }
92
117
 
@@ -109,9 +134,9 @@ export function buildHelp(current: Level): string {
109
134
  `Caveman mode: ${statusLine}`,
110
135
  "",
111
136
  "Usage: /caveman <level>",
112
- " 1 lite - professional, no fluff",
113
- " 2 full - classic caveman",
114
- " 3 ultra - maximum compression",
137
+ " 1 lite - STE-flavored words, light reply shape",
138
+ " 2 full - STE words + full ADHD reply shape",
139
+ " 3 ultra - strict STE + full reply shape",
115
140
  " 0 off - disable (aliases: off, stop, quit)",
116
141
  "",
117
142
  "Other levels: micro",
package/src/mode.ts CHANGED
@@ -13,6 +13,8 @@ import type {
13
13
  ExtensionCommandContext,
14
14
  ExtensionContext,
15
15
  } from "@earendil-works/pi-coding-agent";
16
+ import { showTransientError } from "@xynogen/pix-pretty/transient-error";
17
+ import { getErrorMessage } from "@xynogen/pix-pretty/utils";
16
18
  import { loadOptValue, saveOptValue } from "./persist.ts";
17
19
  import type { OptimizerHandle, OptimizerStatus, OptimizerTool } from "./status.ts";
18
20
 
@@ -86,7 +88,7 @@ export function createMode<L extends string>(
86
88
  }
87
89
  }
88
90
  const saved = loadOptValue(name);
89
- if (saved && levels.includes(saved as L)) level = saved as L;
91
+ if (levels.includes(saved as L)) level = saved as L;
90
92
  syncStatus(ctx);
91
93
  });
92
94
 
@@ -100,7 +102,12 @@ export function createMode<L extends string>(
100
102
  level = resolved;
101
103
 
102
104
  pi.appendEntry(customType, { level });
103
- saveOptValue(name, level);
105
+ try {
106
+ await saveOptValue(name, level);
107
+ } catch (error) {
108
+ const detail = getErrorMessage(error);
109
+ showTransientError(ctx.ui, `optimizer: failed to save ${name}: ${detail}`);
110
+ }
104
111
  syncStatus(ctx);
105
112
 
106
113
  ctx.ui.notify(config.notify(level), "info");
package/src/opt.ts CHANGED
@@ -16,6 +16,7 @@
16
16
 
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
18
  import { type KeybindingsManager, matchesKey } from "@earendil-works/pi-tui";
19
+ import { icon } from "@xynogen/pix-pretty/icon-catalog";
19
20
  import {
20
21
  frameModal,
21
22
  MIN_MODAL_HEIGHT,
@@ -153,7 +154,7 @@ export function registerOptCommand(
153
154
  width,
154
155
  maxHeight: terminalModalHeight(tui.terminal?.rows),
155
156
  minHeight: MIN_MODAL_HEIGHT,
156
- header: [theme.fg("accent", theme.bold("󱎫 Optimizer")), ""],
157
+ header: [theme.fg("accent", theme.bold(`${icon("opt.title")} Optimizer`)), ""],
157
158
  body: rows,
158
159
  footer: [
159
160
  "",
package/src/persist.ts CHANGED
@@ -1,58 +1,27 @@
1
1
  /**
2
- * persist.ts — disk-backed persistence for the /optimizer tool states.
3
- *
4
- * caveman/ponytail previously saved only to the session log (lost on a fresh
5
- * session); rtk never persisted at all. This stores every tool's current
6
- * value in one file under the agent dir so the picker survives a full quit and
7
- * restart. Each tool reads its value on session_start and writes on run().
8
- *
9
- * ~/.pi/agent/optimizer.json → { "caveman": "lite", "rtk": "on", ... }
2
+ * persist.ts — /optimizer tool states live in pix.json `optimizer` (owned by
3
+ * pix-runtime). Replaces the old `optimizer.json` sidecar, which pix-runtime
4
+ * imports and archives on init; writing it again made two sources of truth
5
+ * that overwrote each other.
10
6
  */
11
7
 
12
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
- import { dirname, join } from "node:path";
14
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
8
+ import { config, updateConfig } from "@xynogen/pix-runtime/config";
9
+ import { type OptimizerConfig, optimizerSection } from "@xynogen/pix-runtime/sections";
15
10
  import type { OptimizerTool } from "./status.ts";
16
11
 
17
- type OptimizerFileConfig = Partial<Record<OptimizerTool, string>>;
18
-
19
- const OPTIMIZER_TOOLS: readonly OptimizerTool[] = ["caveman", "rtk", "ponytail"];
20
-
21
- function getStatePath(): string {
22
- return join(getAgentDir(), "optimizer.json");
23
- }
24
-
25
- function readFile(): OptimizerFileConfig {
26
- try {
27
- const sp = getStatePath();
28
- if (!existsSync(sp)) return {};
29
- const raw = JSON.parse(readFileSync(sp, "utf-8")) as unknown;
30
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
31
-
32
- const config: OptimizerFileConfig = {};
33
- for (const tool of OPTIMIZER_TOOLS) {
34
- const value = (raw as Record<string, unknown>)[tool];
35
- if (typeof value === "string") config[tool] = value;
36
- }
37
- return config;
38
- } catch {
39
- return {};
40
- }
12
+ /** Read a tool's value from pix.json (section defaults when unset). */
13
+ export function loadOptValue(tool: OptimizerTool): string {
14
+ return config(optimizerSection)[tool];
41
15
  }
42
16
 
43
- /** Read a single tool's persisted value from optimizer.json. */
44
- export function loadOptValue(tool: OptimizerTool): string | undefined {
45
- return readFile()[tool];
46
- }
47
-
48
- /** Persist a single tool's value, merging into the shared config file. */
49
- export function saveOptValue(tool: OptimizerTool, value: string): void {
50
- try {
51
- const sp = getStatePath();
52
- mkdirSync(dirname(sp), { recursive: true });
53
- const next = { ...readFile(), [tool]: value };
54
- writeFileSync(sp, JSON.stringify(next, null, 2), "utf-8");
55
- } catch (err) {
56
- console.warn(`optimizer: persist ${tool} failed:`, err);
57
- }
17
+ /**
18
+ * Persist a tool's value into pix.json. Invalid values are rejected by the
19
+ * section parser and snap to the default. Rejects on write failure so the UI
20
+ * caller can render it.
21
+ */
22
+ export async function saveOptValue(tool: OptimizerTool, value: string): Promise<void> {
23
+ await updateConfig(optimizerSection, { [tool]: value } as Partial<OptimizerConfig>, {
24
+ origin: "command",
25
+ source: "pix-optimizer",
26
+ });
58
27
  }
package/src/ponytail.ts CHANGED
@@ -44,47 +44,58 @@ export const STATUS_LABELS: Record<Exclude<Level, "off">, string> = {
44
44
 
45
45
  const BASE = `\
46
46
  PONYTAIL MODE ACTIVE. You are a lazy senior developer. Lazy means efficient, \
47
- not careless. The best code is the code never written.
47
+ not careless. The best code is the code you never write.
48
48
 
49
- Before writing any code, stop at the first rung that holds:
50
- 1. Does this need to exist at all? Speculative need = skip it, say so in one line. (YAGNI)
51
- 2. Stdlib does it? Use it.
52
- 3. Native platform feature covers it? Use it (\`<input type="date">\` over a picker lib, CSS over JS, DB constraint over app code).
53
- 4. Already-installed dependency solves it? Use it. Never add a new one for what a few lines can do.
54
- 5. Can it be one line? One line.
55
- 6. Only then: the minimum code that works.
49
+ Before you write any code, stop at the first rung that holds:
50
+ 1. Does this need to exist at all? A speculative need means you skip it. Say so in one line. (YAGNI)
51
+ 2. Does the standard library do it? Use it.
52
+ 3. Does a native platform feature cover it? Use it (\`<input type="date">\` over a picker library, \
53
+ CSS over JavaScript, a database constraint over application code).
54
+ 4. Does an installed dependency solve it? Use it. Never add a new one for what a few lines can do.
55
+ 5. Can it be one line? Write one line.
56
+ 6. Only then, write the minimum code that works.
56
57
 
57
- The ladder is a reflex, not a research project. Two rungs work → take the higher one and move on.
58
+ The ladder is a reflex, not a research project. If two rungs work, take the higher one and move on.
58
59
 
59
60
  Rules:
60
- - No unrequested abstractions: no interface with one impl, no factory for one product, no config for a value that never changes.
61
- - No boilerplate, no scaffolding "for later". Deletion over addition. Boring over clever. Fewest files possible.
62
- - Complex request? Ship the lazy version and question it in the same response. Never stall on an answer you can default.
63
- - Two same-size stdlib options? Take the one correct on edge cases. Lazy means less code, not the flimsier algorithm.
64
- - Mark deliberate simplifications with a \`ponytail:\` comment. A shortcut with a known ceiling names the ceiling and the upgrade path.`;
61
+ - No unrequested abstraction. No interface with one use, no factory for one product, \
62
+ no config for a value that never changes.
63
+ - No boilerplate. No scaffolding for later. Prefer deletion to addition. Prefer boring code to clever code. \
64
+ Use the fewest files.
65
+ - For a complex request, ship the lazy version and question it in the same reply. \
66
+ Never stop for an answer you can default.
67
+ - For two stdlib options of the same size, take the one that is correct on the edge cases. \
68
+ Lazy means less code, not the weaker algorithm.
69
+ - Mark a deliberate simplification with a \`ponytail:\` comment. \
70
+ A shortcut with a known ceiling names the ceiling and the upgrade path.`;
65
71
 
66
72
  const INTENSITY: Record<Exclude<Level, "off">, string> = {
67
73
  lite: `\
68
- Build what's asked, but name the lazier alternative in one line. User picks.
69
- Example: "Done, cache added. FYI: \`functools.lru_cache\` covers this in one line if you'd rather not own a cache class."`,
74
+ Build what the user asks, but name the lazier alternative in one line. The user picks.
75
+ Example: "Done. I added a cache. The \`functools.lru_cache\` decorator covers this in one line \
76
+ if you do not want to own a cache class."`,
70
77
 
71
78
  full: `\
72
- The ladder enforced. Stdlib and native first. Shortest diff, shortest explanation.
73
- Example: "\`@lru_cache(maxsize=1000)\` on the fetch function. Skipped custom cache class, add when lru_cache measurably falls short."`,
79
+ Enforce the ladder. Prefer the standard library and native features first. \
80
+ Write the shortest diff and the shortest explanation.
81
+ Example: "I put \`@lru_cache(maxsize=1000)\` on the fetch function. I skipped a custom cache class. \
82
+ Add one when lru_cache falls short in a measurement."`,
74
83
 
75
84
  ultra: `\
76
- YAGNI extremist. Deletion before addition. Ship the one-liner and challenge the rest of the requirement in the same breath.
77
- Example: "No cache until a profiler says so. When it does: \`@lru_cache\`. A hand-rolled TTL cache class is a bug farm with a hit rate."`,
85
+ YAGNI extremist. Prefer deletion to addition. Ship the one-liner and challenge the rest of the \
86
+ requirement in the same reply.
87
+ Example: "No cache until a profiler asks for one. When it does, use \`@lru_cache\`. \
88
+ A hand-rolled TTL cache class is a bug farm with a hit rate."`,
78
89
  };
79
90
 
80
91
  const SAFETY = `\
81
- When NOT to be lazy: never simplify away input validation at trust boundaries, \
82
- error handling that prevents data loss, security, accessibility, or anything \
83
- explicitly requested. Hardware is never the spec ideal — leave the calibration knob.
84
- Lazy code without its check is unfinished: non-trivial logic leaves ONE runnable check behind \
85
- (an assert-based self-check or one small test file; no frameworks). Trivial one-liners need no test.
86
- Output: code first, then at most three short lines — what was skipped, when to add it.
87
- Boundaries: ponytail governs what you build, not how you talk. "stop ponytail" / "normal mode" reverts.`;
92
+ When not to be lazy: never simplify away input validation at a trust boundary, \
93
+ error handling that prevents data loss, security, accessibility, or anything the user asks for. \
94
+ The hardware is never the spec ideal. Leave the calibration knob.
95
+ Lazy code without its check is unfinished. Non-trivial logic leaves ONE runnable check behind \
96
+ (an assert-based self-check or one small test file, no frameworks). A trivial one-liner needs no test.
97
+ Output: write the code first, then at most three short lines — what you skipped, and when to add it.
98
+ Boundaries: ponytail governs what you build, not how you talk. "stop ponytail" or "normal mode" reverts.`;
88
99
 
89
100
  /**
90
101
  * Build the system prompt injection for a given level.
package/src/rtk.ts CHANGED
@@ -11,7 +11,10 @@ import type {
11
11
  ExtensionCommandContext,
12
12
  ExtensionContext,
13
13
  } from "@earendil-works/pi-coding-agent";
14
- import { canExecute } from "./capability.ts";
14
+ import { reportToolStatus } from "@xynogen/pix-pretty/tool-status";
15
+ import { showTransientError } from "@xynogen/pix-pretty/transient-error";
16
+ import { getErrorMessage } from "@xynogen/pix-pretty/utils";
17
+ import { ensureTool, type ResolvedTool, resolveTool } from "@xynogen/pix-runtime/binaries";
15
18
  import { loadOptValue, saveOptValue } from "./persist.ts";
16
19
  import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
17
20
 
@@ -74,7 +77,7 @@ export function buildSudoBlockReason(sudoCmds: string[], hasSudoRunTool: boolean
74
77
 
75
78
  export function applyRtkRewrite(
76
79
  event: BashCallEvent,
77
- opts: { enabled: boolean; rtkAvailable: boolean },
80
+ opts: { enabled: boolean; rtkAvailable: boolean; rtkCommand?: string },
78
81
  ): boolean {
79
82
  if (!opts.enabled) return false;
80
83
  if (!opts.rtkAvailable) return false;
@@ -83,7 +86,7 @@ export function applyRtkRewrite(
83
86
  const command = event.input?.command;
84
87
  if (typeof command !== "string" || !command) return false;
85
88
 
86
- const rewritten = rewriteChain(command);
89
+ const rewritten = rewriteChain(command, opts.rtkCommand);
87
90
  if (rewritten === command) return false;
88
91
 
89
92
  event.input.command = rewritten;
@@ -127,12 +130,31 @@ const RTK_COMMANDS = new Set([
127
130
 
128
131
  interface RtkStatus {
129
132
  available: boolean;
133
+ /** Command prefix to inject: bare `rtk`, or the quoted path for a binary.json pin. */
134
+ command: string;
130
135
  checkedAt: number;
131
136
  }
132
137
 
133
- /** Probe the command we actually use instead of relying on a platform-specific locator. */
134
- export function probeRtkAvailability(pi: Pick<ExtensionAPI, "exec">): Promise<boolean> {
135
- return canExecute(pi, "rtk", ["--version"]);
138
+ /**
139
+ * Prefix used in rewritten commands. The bash tool puts `<agentDir>/bin` on
140
+ * PATH and PATH hits resolve by name, so those stay a readable bare `rtk`;
141
+ * only a user-chosen path (binary.json) must be spelled out, single-quoted
142
+ * with forward slashes so Git Bash on Windows runs it too.
143
+ */
144
+ export function rtkCommandFor(tool: Pick<ResolvedTool, "path" | "source">): string {
145
+ if (tool.source !== "user") return "rtk";
146
+ const posix = tool.path.replaceAll("\\", "/");
147
+ return `'${posix.replaceAll("'", "'\\''")}'`;
148
+ }
149
+
150
+ /** Locate rtk (binary.json → agent bin → PATH) without running anything. */
151
+ export function probeRtk(): RtkStatus {
152
+ const tool = resolveTool("rtk");
153
+ return {
154
+ available: tool !== undefined,
155
+ command: tool ? rtkCommandFor(tool) : "rtk",
156
+ checkedAt: Date.now(),
157
+ };
136
158
  }
137
159
 
138
160
  /**
@@ -194,7 +216,7 @@ const CHAIN_OPERATORS = new Set(["&&", "||", ";", "|"]);
194
216
  * RTK command and it is not already prefixed. Operators are preserved.
195
217
  * Returns the rewritten command, or the original if nothing changed.
196
218
  */
197
- export function rewriteChain(command: string): string {
219
+ export function rewriteChain(command: string, rtkCommand = "rtk"): string {
198
220
  const parts = splitChain(command);
199
221
  if (!parts) return command; // unparseable — leave untouched
200
222
 
@@ -207,11 +229,11 @@ export function rewriteChain(command: string): string {
207
229
  if (!body) return part;
208
230
 
209
231
  const firstWord = body.split(/\s+/)[0] ?? "";
210
- if (firstWord === "rtk") return part;
232
+ if (firstWord === "rtk" || firstWord === rtkCommand) return part;
211
233
  if (!RTK_COMMANDS.has(firstWord)) return part;
212
234
 
213
235
  changed = true;
214
- return `${leading}rtk ${body}`;
236
+ return `${leading}${rtkCommand} ${body}`;
215
237
  });
216
238
 
217
239
  return changed ? rewritten.join("") : command;
@@ -238,15 +260,33 @@ export function rtk(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle
238
260
  return rtkStatus;
239
261
  }
240
262
 
241
- const available = await probeRtkAvailability(pi);
242
- rtkStatus = {
243
- available,
244
- checkedAt: Date.now(),
245
- };
246
- if (available) warnedMissing = false;
263
+ rtkStatus = probeRtk();
264
+ if (rtkStatus.available) warnedMissing = false;
247
265
  return rtkStatus;
248
266
  };
249
267
 
268
+ // First load without rtk: download it once, visibly (footer status + one
269
+ // transient line naming repo and version). Never blocks startup or a tool
270
+ // call; PI_OFFLINE, a broken binary.json path, or RTK off skip it.
271
+ let installing: Promise<void> | undefined;
272
+ const installRtk = (ctx: Pick<ExtensionContext, "ui">) => {
273
+ installing ??= ensureTool("rtk", { onStatus: reportToolStatus(ctx.ui) })
274
+ .then(() => {
275
+ rtkStatus = null;
276
+ })
277
+ .catch((error: unknown) => {
278
+ if (warnedMissing) return;
279
+ warnedMissing = true;
280
+ const detail = getErrorMessage(error);
281
+ ctx.ui.notify(`RTK rewriting disabled: ${detail}`, "warning");
282
+ })
283
+ .then(async () => {
284
+ await checkRtkAvailability();
285
+ syncStatus(ctx);
286
+ });
287
+ return installing;
288
+ };
289
+
250
290
  // Detect sudo_run tool availability. No system-prompt injection — the
251
291
  // tool_call rewrite hook adds the rtk prefix on its own, so we keep zero
252
292
  // always-on context cost (ponytail: manual `rtk err`/`rtk summary`/`rtk
@@ -265,14 +305,8 @@ export function rtk(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle
265
305
  const saved = loadOptValue("rtk");
266
306
  if (saved === "on" || saved === "off") enabled = saved === "on";
267
307
  const probe = await checkRtkAvailability();
268
- if (!probe.available && !warnedMissing) {
269
- ctx.ui.notify(
270
- "rtk not found — RTK rewriting disabled. Install: cargo install rtk-ai",
271
- "warning",
272
- );
273
- warnedMissing = true;
274
- }
275
308
  syncStatus(ctx);
309
+ if (!probe.available && enabled) void installRtk(ctx);
276
310
  });
277
311
  pi.on("agent_start", async (_event, ctx) => {
278
312
  syncStatus(ctx);
@@ -285,11 +319,17 @@ export function rtk(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle
285
319
 
286
320
  async function run(value: string, ctx: ExtensionCommandContext): Promise<void> {
287
321
  enabled = value === "on";
288
- saveOptValue("rtk", enabled ? "on" : "off");
322
+ try {
323
+ await saveOptValue("rtk", enabled ? "on" : "off");
324
+ } catch (error) {
325
+ const detail = getErrorMessage(error);
326
+ showTransientError(ctx.ui, `optimizer: failed to save rtk: ${detail}`);
327
+ }
289
328
 
290
- await checkRtkAvailability();
329
+ const probe = await checkRtkAvailability();
291
330
  syncStatus(ctx);
292
331
  ctx.ui.notify(`RTK rewriting ${enabled ? "on" : "off"}.`, "info");
332
+ if (enabled && !probe.available) void installRtk(ctx);
293
333
  }
294
334
 
295
335
  // Rewrite bash commands to add rtk prefix.
@@ -333,7 +373,7 @@ export function rtk(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle
333
373
  // Rewrite every segment in the command chain that uses a known RTK
334
374
  // command (e.g. `git add . && git push` -> `rtk git add . && rtk git push`).
335
375
  // Mutates `event.input.command` in place — the SDK's supported patch path.
336
- applyRtkRewrite(event, { enabled, rtkAvailable: probe.available });
376
+ applyRtkRewrite(event, { enabled, rtkAvailable: probe.available, rtkCommand: probe.command });
337
377
  return undefined;
338
378
  });
339
379
 
package/src/capability.ts DELETED
@@ -1,16 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
-
3
- /** Test whether a command can perform a harmless operation through Pi's executor. */
4
- export async function canExecute(
5
- pi: Pick<ExtensionAPI, "exec">,
6
- command: string,
7
- args: string[],
8
- timeout = 3000,
9
- ): Promise<boolean> {
10
- try {
11
- const result = await pi.exec(command, args, { timeout });
12
- return result.code === 0;
13
- } catch {
14
- return false;
15
- }
16
- }