@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 +49 -33
- package/package.json +3 -2
- package/src/caveman.ts +57 -32
- package/src/mode.ts +9 -2
- package/src/opt.ts +2 -1
- package/src/persist.ts +19 -50
- package/src/ponytail.ts +38 -27
- package/src/rtk.ts +65 -25
- package/src/capability.ts +0 -16
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
|
|
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
|
-
|
|
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 |
|
|
57
|
-
| full |
|
|
58
|
-
| ultra |
|
|
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
|
|
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.
|
|
77
|
-
(warns once).
|
|
80
|
+
untouched. Commands are never rewritten while `rtk` is missing.
|
|
78
81
|
|
|
79
|
-
**
|
|
82
|
+
**Binary:** pix finds `rtk` in this order: `binary.json` → `~/.pi/agent/bin` →
|
|
83
|
+
PATH (see pix-runtime, Binaries).
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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` |
|
|
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
|
-
##
|
|
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
|
-
|
|
183
|
+
## Full distro
|
|
174
184
|
|
|
175
|
-
|
|
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.
|
|
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.
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 -
|
|
113
|
-
" 2 full -
|
|
114
|
-
" 3 ultra -
|
|
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 (
|
|
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
|
-
|
|
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("
|
|
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 —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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 {
|
|
13
|
-
import {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
47
|
+
not careless. The best code is the code you never write.
|
|
48
48
|
|
|
49
|
-
Before
|
|
50
|
-
1. Does this need to exist at all?
|
|
51
|
-
2.
|
|
52
|
-
3.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
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
|
|
61
|
-
|
|
62
|
-
-
|
|
63
|
-
|
|
64
|
-
-
|
|
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
|
|
69
|
-
Example: "Done
|
|
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
|
-
|
|
73
|
-
|
|
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.
|
|
77
|
-
|
|
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
|
|
82
|
-
error handling that prevents data loss, security, accessibility, or anything \
|
|
83
|
-
|
|
84
|
-
Lazy code without its check is unfinished
|
|
85
|
-
(an assert-based self-check or one small test file
|
|
86
|
-
Output: code first, then at most three short lines — what
|
|
87
|
-
Boundaries: ponytail governs what you build, not how you talk. "stop ponytail"
|
|
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 {
|
|
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
|
-
/**
|
|
134
|
-
|
|
135
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|