@hheei/omp-optimizer 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+
7
+ - Added the `omp-optimizer` extension with Caveman, RTK, and Ponytail modes behind `/optimizer`.
8
+ - Added OMP-native status-bar, custom UI, session-entry, persistence, and execution integrations.
9
+
10
+ ### Changed
11
+
12
+ - Updated the optimizer panel to use OMP's native `SettingsList` interaction and theme styling.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xynogen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @hheei/omp-optimizer
2
+
3
+ `@hheei/omp-optimizer` is an Oh My Pi extension that combines five tools behind one
4
+ `/optimizer` command:
5
+
6
+ ## Fork provenance
7
+
8
+ This extension is forked from [`pix-optimizer`](https://github.com/xynogen/pix-mono/tree/main/packages/pix-optimizer) in [`xynogen/pix-mono`](https://github.com/xynogen/pix-mono). It has been adapted for Oh My Pi's public Extension API and does not retain the original pix host integration. The T2S tool is ported from [`@hheei/pi-t2s`](https://github.com/hheei/hepi-mono/tree/main/packages/pi-t2s).
9
+
10
+ - **Caveman** — injects a terse-output system prompt (`off`, `lite`, `full`,
11
+ `ultra`, `micro`).
12
+ - **RTK** — injects the RTK prompt and rewrites supported `bash` command chains
13
+ to use `rtk` when the binary is available. Direct `sudo` segments are blocked
14
+ so the model can use OMP's `sudo_run` flow when available.
15
+ - **Ponytail** — injects a minimal-code/YAGNI system prompt (`off`, `lite`,
16
+ `full`, `ultra`).
17
+ - **T2S** — converts interactive Traditional Chinese prose to Simplified Chinese while preserving inline and fenced code (`on`, `off`).
18
+ - **Edit Guard** — when enabled, aborts malformed `apply_patch`/unified-diff `@@` syntax in hashline edits and shell `apply_patch` calls when that tool is unavailable (`on`, `off`).
19
+
20
+ ## Install and load
21
+
22
+ From this checkout, load the extension file directly:
23
+
24
+ ```text
25
+ packages/hepi/omp-optimizer/src/index.ts
26
+ ```
27
+
28
+ For a published package, the manifest uses OMP's `omp.extensions` field:
29
+
30
+ ```json
31
+ {
32
+ "omp": {
33
+ "extensions": ["./src/index.ts"]
34
+ }
35
+ }
36
+ ```
37
+
38
+ After publishing, install it through OMP:
39
+
40
+ ```bash
41
+ ./scripts/omp install @hheei/omp-optimizer
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ Run `/optimizer` and use OMP's configured selection keys (normally `↑↓`) to
47
+ move between tools. `Enter` and `Space` cycle the selected value; `Esc` closes
48
+ the panel. The optional `q` shortcut also closes it. T2S and Edit Guard are
49
+ configured from the same panel. Edit Guard is enabled by default: it blocks
50
+ unified-diff syntax in hashline payloads and shell `apply_patch` calls when
51
+ `apply_patch` is not an active tool. In print/RPC mode, the command emits a
52
+ plain status summary instead of opening the interactive panel.
53
+
54
+ State is persisted by the extension under the active OMP agent directory as
55
+ `optimizer.json`. Session custom entries are also recorded through
56
+ `pi.appendEntry`, so branch/session navigation can restore the in-session value.
57
+
58
+ ## OMP integration
59
+
60
+ The implementation uses OMP's public extension APIs and built-in packages:
61
+
62
+ - `ExtensionAPI` lifecycle hooks and `appendEntry` for prompt injection and
63
+ session state.
64
+ - `ctx.ui.custom`, `ctx.ui.setStatus`, `ctx.ui.notify`, and the OMP theme for
65
+ the panel and status bar.
66
+ - `ExtensionAPI.exec` for the RTK availability probe.
67
+ - `@oh-my-pi/pi-utils` for the active agent directory and centralized logging.
68
+
69
+ No `pix-pretty` or Pi fork compatibility package is required.
70
+
71
+ The status bar and `/optimizer` panel use Nerd Font glyphs. Use a terminal font
72
+ with Nerd Font support for the intended icons.
73
+
74
+ ## Development
75
+
76
+ Run the focused tests from the repository root:
77
+
78
+ ```bash
79
+ bun test packages/hepi/omp-optimizer/src
80
+ ```
81
+
82
+ ## License
83
+
84
+ MIT. See `LICENSE`.
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@hheei/omp-optimizer",
3
+ "version": "0.1.0",
4
+ "description": "Token-optimization and Traditional-to-Simplified Chinese extension for Oh My Pi",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "scripts": {
8
+ "check": "biome check . --no-errors-on-unmatched && tsgo -p tsconfig.json --noEmit",
9
+ "test": "bun test src"
10
+ },
11
+ "omp": {
12
+ "extensions": ["./src/index.ts"]
13
+ },
14
+ "keywords": ["omp", "omp-extension", "optimizer", "caveman", "rtk", "ponytail", "t2s", "traditional-chinese"],
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/hheei/oh-my-pi.git",
19
+ "directory": "packages/hepi/omp-optimizer"
20
+ },
21
+ "dependencies": {
22
+ "opencc-js": "^1.4.0"
23
+ },
24
+ "peerDependencies": {
25
+ "@oh-my-pi/pi-coding-agent": "*",
26
+ "@oh-my-pi/pi-tui": "*",
27
+ "@oh-my-pi/pi-ai": "*",
28
+ "@oh-my-pi/pi-utils": "*"
29
+ }
30
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it, mock } from "bun:test";
2
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
3
+ import { canExecute } from "./capability.ts";
4
+
5
+ function executor(impl: ExtensionAPI["exec"]): Pick<ExtensionAPI, "exec"> {
6
+ return { exec: impl };
7
+ }
8
+
9
+ describe("canExecute", () => {
10
+ it("runs the requested capability probe without a shell", async () => {
11
+ const exec = mock(async () => ({
12
+ stdout: "jq-1.7",
13
+ stderr: "",
14
+ code: 0,
15
+ killed: false,
16
+ }));
17
+
18
+ expect(await canExecute(executor(exec), "jq", ["--version"])).toBe(true);
19
+ expect(exec).toHaveBeenCalledWith("jq", ["--version"], { timeout: 3000 });
20
+ });
21
+
22
+ it("supports a custom timeout", async () => {
23
+ const exec = mock(async () => ({ stdout: "rtk 0.30.0", stderr: "", code: 0, killed: false }));
24
+
25
+ expect(await canExecute(executor(exec), "rtk", ["--version"], 1000)).toBe(true);
26
+ expect(exec).toHaveBeenCalledWith("rtk", ["--version"], { timeout: 1000 });
27
+ });
28
+
29
+ it("returns false for a non-zero exit", async () => {
30
+ const exec = mock(async () => ({ stdout: "", stderr: "failed", code: 1, killed: false }));
31
+
32
+ expect(await canExecute(executor(exec), "jq", ["--version"])).toBe(false);
33
+ });
34
+
35
+ it("returns false when the executable cannot be spawned", async () => {
36
+ const exec = mock(async () => {
37
+ throw new Error("ENOENT");
38
+ });
39
+
40
+ expect(await canExecute(executor(exec), "rtk", ["--version"])).toBe(false);
41
+ });
42
+ });
@@ -0,0 +1,16 @@
1
+ import type { ExtensionAPI } from "@oh-my-pi/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
+ }
@@ -0,0 +1,203 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ buildHelp,
4
+ buildPrompt,
5
+ LEVEL_NUMBERS,
6
+ LEVELS,
7
+ type Level,
8
+ resolveLevel,
9
+ STATUS_LABELS,
10
+ STOP_ALIASES,
11
+ toggleLevel,
12
+ } from "./caveman.ts";
13
+
14
+ // ── LEVELS ────────────────────────────────────────────────────────────────────
15
+
16
+ describe("LEVELS", () => {
17
+ it("contains off as first entry", () => {
18
+ expect(LEVELS[0]).toBe("off");
19
+ });
20
+
21
+ it("contains all expected levels", () => {
22
+ const expected: Level[] = ["off", "lite", "full", "ultra", "micro"];
23
+ for (const l of expected) expect(LEVELS).toContain(l);
24
+ });
25
+ });
26
+
27
+ // ── STOP_ALIASES ──────────────────────────────────────────────────────────────
28
+
29
+ describe("STOP_ALIASES", () => {
30
+ it("includes off, stop, quit", () => {
31
+ expect(STOP_ALIASES.has("off")).toBe(true);
32
+ expect(STOP_ALIASES.has("stop")).toBe(true);
33
+ expect(STOP_ALIASES.has("quit")).toBe(true);
34
+ });
35
+
36
+ it("does not include active levels", () => {
37
+ expect(STOP_ALIASES.has("full")).toBe(false);
38
+ expect(STOP_ALIASES.has("ultra")).toBe(false);
39
+ });
40
+ });
41
+
42
+ // ── STATUS_LABELS ─────────────────────────────────────────────────────────────
43
+
44
+ describe("STATUS_LABELS", () => {
45
+ it("has a label for every non-off level", () => {
46
+ const nonOff = LEVELS.filter((l) => l !== "off") as Exclude<Level, "off">[];
47
+ for (const l of nonOff) {
48
+ expect(STATUS_LABELS[l]).toBeTruthy();
49
+ }
50
+ });
51
+
52
+ it("latin levels are uppercase", () => {
53
+ expect(STATUS_LABELS.lite).toBe("LITE");
54
+ expect(STATUS_LABELS.full).toBe("FULL");
55
+ expect(STATUS_LABELS.ultra).toBe("ULTRA");
56
+ expect(STATUS_LABELS.micro).toBe("MICRO");
57
+ });
58
+ });
59
+
60
+ // ── resolveLevel ──────────────────────────────────────────────────────────────
61
+
62
+ describe("resolveLevel", () => {
63
+ it("resolves valid levels", () => {
64
+ expect(resolveLevel("lite")).toBe("lite");
65
+ expect(resolveLevel("full")).toBe("full");
66
+ expect(resolveLevel("ultra")).toBe("ultra");
67
+ expect(resolveLevel("micro")).toBe("micro");
68
+ expect(resolveLevel("off")).toBe("off");
69
+ });
70
+
71
+ it("maps stop aliases to off", () => {
72
+ expect(resolveLevel("stop")).toBe("off");
73
+ expect(resolveLevel("quit")).toBe("off");
74
+ });
75
+
76
+ it("is case-insensitive", () => {
77
+ expect(resolveLevel("FULL")).toBe("full");
78
+ expect(resolveLevel("Ultra")).toBe("ultra");
79
+ expect(resolveLevel("STOP")).toBe("off");
80
+ });
81
+
82
+ it("trims whitespace", () => {
83
+ expect(resolveLevel(" full ")).toBe("full");
84
+ });
85
+
86
+ it("returns null for unknown input", () => {
87
+ expect(resolveLevel("unknown")).toBeNull();
88
+ expect(resolveLevel("")).toBeNull();
89
+ expect(resolveLevel("config")).toBeNull(); // config handled separately
90
+ });
91
+ });
92
+
93
+ // ── numeric levels ────────────────────────────────────────────────────────────
94
+
95
+ describe("numeric levels", () => {
96
+ it("maps 1/2/3 to lite/full/ultra", () => {
97
+ expect(resolveLevel("1")).toBe("lite");
98
+ expect(resolveLevel("2")).toBe("full");
99
+ expect(resolveLevel("3")).toBe("ultra");
100
+ });
101
+
102
+ it("maps 0 to off", () => {
103
+ expect(resolveLevel("0")).toBe("off");
104
+ });
105
+
106
+ it("LEVEL_NUMBERS only covers 1-3", () => {
107
+ expect(Object.keys(LEVEL_NUMBERS).sort()).toEqual(["1", "2", "3"]);
108
+ });
109
+
110
+ it("rejects out-of-range numbers", () => {
111
+ expect(resolveLevel("4")).toBeNull();
112
+ expect(resolveLevel("9")).toBeNull();
113
+ });
114
+
115
+ it("trims whitespace around numbers", () => {
116
+ expect(resolveLevel(" 2 ")).toBe("full");
117
+ });
118
+ });
119
+
120
+ // ── buildHelp ─────────────────────────────────────────────────────────────────
121
+
122
+ describe("buildHelp", () => {
123
+ it("lists numeric shortcuts", () => {
124
+ const help = buildHelp("off");
125
+ expect(help).toContain("1");
126
+ expect(help).toContain("lite");
127
+ expect(help).toContain("2");
128
+ expect(help).toContain("full");
129
+ expect(help).toContain("3");
130
+ expect(help).toContain("ultra");
131
+ });
132
+
133
+ it("shows current level when active", () => {
134
+ expect(buildHelp("ultra")).toContain("ULTRA");
135
+ });
136
+
137
+ it("shows off when disabled", () => {
138
+ expect(buildHelp("off")).toContain("off");
139
+ });
140
+
141
+ it("mentions config", () => {
142
+ expect(buildHelp("off")).toContain("config");
143
+ });
144
+
145
+ it("does not mention wenyan", () => {
146
+ expect(buildHelp("off").toLowerCase()).not.toContain("wenyan");
147
+ });
148
+ });
149
+
150
+ // ── toggleLevel ───────────────────────────────────────────────────────────────
151
+
152
+ describe("toggleLevel", () => {
153
+ it("off → full", () => {
154
+ expect(toggleLevel("off")).toBe("full");
155
+ });
156
+
157
+ it("full → off", () => {
158
+ expect(toggleLevel("full")).toBe("off");
159
+ });
160
+
161
+ it("any non-off level → off", () => {
162
+ const nonOff = LEVELS.filter((l) => l !== "off") as Level[];
163
+ for (const l of nonOff) {
164
+ expect(toggleLevel(l)).toBe("off");
165
+ }
166
+ });
167
+ });
168
+
169
+ // ── buildPrompt ───────────────────────────────────────────────────────────────
170
+
171
+ describe("buildPrompt", () => {
172
+ it("returns empty string for off", () => {
173
+ expect(buildPrompt("off")).toBe("");
174
+ });
175
+
176
+ it("returns MICRO_PROMPT for micro (no BASE)", () => {
177
+ const p = buildPrompt("micro");
178
+ expect(p).toContain("Token efficiency");
179
+ expect(p).not.toContain("CAVEMAN MODE");
180
+ });
181
+
182
+ it("includes BASE for all standard levels", () => {
183
+ const standard = ["lite", "full", "ultra"] as Level[];
184
+ for (const l of standard) {
185
+ expect(buildPrompt(l)).toContain("CAVEMAN MODE");
186
+ }
187
+ });
188
+
189
+ it("includes SAFETY clause for all standard levels", () => {
190
+ const standard = ["lite", "full", "ultra"] as Level[];
191
+ for (const l of standard) {
192
+ expect(buildPrompt(l)).toContain("Auto-clarity");
193
+ }
194
+ });
195
+
196
+ it("each level has distinct intensity instructions", () => {
197
+ const lite = buildPrompt("lite");
198
+ const ultra = buildPrompt("ultra");
199
+ expect(lite).toContain("Keep articles");
200
+ expect(ultra).toContain("Abbreviate");
201
+ expect(lite).not.toContain("Abbreviate");
202
+ });
203
+ });
package/src/caveman.ts ADDED
@@ -0,0 +1,140 @@
1
+ /**
2
+ * caveman.ts — pure logic + Pi extension
3
+ *
4
+ * Pure helpers exported for tests; caveman(pi) is the extension entry,
5
+ * called by index.ts alongside rtk(pi).
6
+ */
7
+
8
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
9
+ import {
10
+ createMode,
11
+ resolveLevel as resolveLevelGeneric,
12
+ toggleLevel as toggleLevelGeneric,
13
+ } from "./mode.ts";
14
+ import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
15
+
16
+ // ── Levels ────────────────────────────────────────────────────────────────────
17
+
18
+ export const LEVELS = ["off", "lite", "full", "ultra", "micro"] as const;
19
+
20
+ export type Level = (typeof LEVELS)[number];
21
+
22
+ export const STOP_ALIASES = new Set(["off", "stop", "quit", "0"]);
23
+
24
+ // Numeric shortcuts: /caveman 1|2|3
25
+ export const LEVEL_NUMBERS: Record<string, Level> = {
26
+ "1": "lite",
27
+ "2": "full",
28
+ "3": "ultra",
29
+ };
30
+
31
+ // ── Status labels ─────────────────────────────────────────────────────────────
32
+
33
+ export const STATUS_LABELS: Record<Exclude<Level, "off">, string> = {
34
+ lite: "LITE",
35
+ full: "FULL",
36
+ ultra: "ULTRA",
37
+ micro: "MICRO",
38
+ };
39
+
40
+ // ── Prompt fragments ──────────────────────────────────────────────────────────
41
+
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].`;
63
+
64
+ const INTENSITY: Record<Exclude<Level, "off" | "micro">, string> = {
65
+ 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\`."`,
68
+
69
+ 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\`."`,
72
+
73
+ 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\`."`,
76
+ };
77
+
78
+ 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.`;
82
+
83
+ /**
84
+ * Build the system prompt injection for a given level.
85
+ * Returns empty string when level is "off".
86
+ */
87
+ export function buildPrompt(level: Level): string {
88
+ if (level === "off") return "";
89
+ if (level === "micro") return MICRO_PROMPT;
90
+ return [BASE, "", `Intensity: ${INTENSITY[level]}`, "", SAFETY].join("\n");
91
+ }
92
+
93
+ // ── Level resolution ──────────────────────────────────────────────────────────
94
+
95
+ /**
96
+ * Resolve a raw command arg to a Level, or return null if unrecognised.
97
+ * Handles stop aliases (stop/quit → "off") and valid level names.
98
+ */
99
+ export function resolveLevel(arg: string): Level | null {
100
+ return resolveLevelGeneric(arg, LEVELS, LEVEL_NUMBERS, STOP_ALIASES);
101
+ }
102
+
103
+ /**
104
+ * Help text shown when /caveman is run with no argument.
105
+ */
106
+ export function buildHelp(current: Level): string {
107
+ const statusLine = current === "off" ? "off" : `${STATUS_LABELS[current]} (${current})`;
108
+ return [
109
+ `Caveman mode: ${statusLine}`,
110
+ "",
111
+ "Usage: /caveman <level>",
112
+ " 1 lite - professional, no fluff",
113
+ " 2 full - classic caveman",
114
+ " 3 ultra - maximum compression",
115
+ " 0 off - disable (aliases: off, stop, quit)",
116
+ "",
117
+ "Other levels: micro",
118
+ " config - open settings dialog",
119
+ ].join("\n");
120
+ }
121
+
122
+ /**
123
+ * Toggle: off → full, anything else → off.
124
+ */
125
+ export function toggleLevel(current: Level): Level {
126
+ return toggleLevelGeneric(current);
127
+ }
128
+
129
+ // ── Pi extension ────────────────────────────────────────────────────────────
130
+
131
+ export function caveman(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle {
132
+ return createMode(pi, status, {
133
+ name: "caveman",
134
+ help: "caveman — terse output",
135
+ levels: LEVELS,
136
+ buildPrompt,
137
+ resolve: resolveLevel,
138
+ notify: (level) => (level === "off" ? "Caveman mode off." : `Caveman: ${STATUS_LABELS[level]}`),
139
+ });
140
+ }
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
3
+ import type { OptimizerStatus } from "./status.ts";
4
+ import { editGuard, hasMalformedHashlineInput, hasStreamingApplyPatchCommand } from "./edit-guard.ts";
5
+
6
+ const historicalMalformedPayload = `[packages/hepi/omp-optimizer/src/index.ts#C557]\nPUT 2.=12:\n+const value = true;\n@@\nPUT 20.=20:\n+const next = false;`;
7
+
8
+ function callUpdate(
9
+ handler: (event: unknown, context: { abort(): void }) => void,
10
+ name: string,
11
+ arguments_: Record<string, string>,
12
+ abort: () => void,
13
+ ): void {
14
+ handler(
15
+ {
16
+ assistantMessageEvent: {
17
+ type: "toolcall_delta",
18
+ contentIndex: 0,
19
+ partial: {
20
+ content: [{ type: "toolCall", name, arguments: arguments_ }],
21
+ },
22
+ },
23
+ },
24
+ { abort },
25
+ );
26
+ }
27
+
28
+ function makeGuard(activeTools: readonly string[] = ["edit"]) {
29
+ const handlers = new Map<string, (event: unknown, context?: { abort(): void }) => unknown>();
30
+ const messages: unknown[] = [];
31
+ const pi = {
32
+ events: {},
33
+ on(event: string, handler: (event: unknown, context?: { abort(): void }) => unknown) {
34
+ handlers.set(event, handler);
35
+ },
36
+ getActiveTools: () => [...activeTools],
37
+ sendMessage(message: unknown) {
38
+ messages.push(message);
39
+ },
40
+ };
41
+ editGuard(pi as unknown as ExtensionAPI, { set: () => {} } as unknown as OptimizerStatus);
42
+ return { handlers, messages };
43
+ }
44
+
45
+ describe("edit guard detection", () => {
46
+ it("matches historical bare @@ hashline failures", () => {
47
+ expect(hasMalformedHashlineInput(historicalMalformedPayload)).toBe(true);
48
+ });
49
+
50
+ it("matches shell apply_patch commands but not plain mentions", () => {
51
+ expect(hasStreamingApplyPatchCommand("apply_patch <<'PATCH'\n...")).toBe(true);
52
+ expect(hasStreamingApplyPatchCommand("please do not use apply_patch")).toBe(false);
53
+ });
54
+
55
+ it("ignores valid hashline bodies and other edit formats", () => {
56
+ expect(hasMalformedHashlineInput("[src/a.ts#ABCD]\nPUT 1.=1:\n+const marker = '@@';")).toBe(false);
57
+ expect(hasMalformedHashlineInput("[src/a.ts#ABCD]\nPUT 1.=1:\n+@@")).toBe(false);
58
+ expect(hasMalformedHashlineInput("*** Begin Patch\n*** Update File: src/a.ts\n@@ -1 +1 @@")).toBe(false);
59
+ expect(hasMalformedHashlineInput("please avoid @@ when editing")).toBe(false);
60
+ });
61
+ });
62
+
63
+ describe("edit guard runtime", () => {
64
+ it("aborts malformed hashline once, then sends retry guidance", () => {
65
+ const { handlers, messages } = makeGuard();
66
+ const update = handlers.get("message_update") as (event: unknown, context: { abort(): void }) => void;
67
+ const settled = handlers.get("agent_end") as (event: { willContinue?: boolean }, context: unknown) => void;
68
+ let aborts = 0;
69
+
70
+ callUpdate(update, "edit", { input: historicalMalformedPayload }, () => {
71
+ aborts += 1;
72
+ });
73
+ callUpdate(update, "edit", { input: `${historicalMalformedPayload}\n@@` }, () => {
74
+ aborts += 1;
75
+ });
76
+ expect(aborts).toBe(1);
77
+
78
+ settled({ willContinue: false }, {});
79
+ expect(messages).toHaveLength(1);
80
+ expect(messages[0]).toEqual({
81
+ customType: "edit-guard",
82
+ content: expect.stringContaining("Do not retry the same payload"),
83
+ display: true,
84
+ });
85
+ });
86
+
87
+ it("aborts unavailable shell apply_patch and names active edit alternative", () => {
88
+ const { handlers, messages } = makeGuard(["edit"]);
89
+ const update = handlers.get("message_update") as (event: unknown, context: { abort(): void }) => void;
90
+ const settled = handlers.get("agent_end") as (event: { willContinue?: boolean }, context: unknown) => void;
91
+ let aborts = 0;
92
+
93
+ callUpdate(update, "bash", { command: "apply_patch <<'PATCH'\n..." }, () => {
94
+ aborts += 1;
95
+ });
96
+ expect(aborts).toBe(1);
97
+ settled({ willContinue: false }, {});
98
+ expect(messages[0]).toEqual({
99
+ customType: "edit-guard",
100
+ content: expect.stringContaining("Continue with `edit`"),
101
+ display: true,
102
+ });
103
+ });
104
+
105
+ it("does not block apply_patch when tool is active", () => {
106
+ const { handlers } = makeGuard(["apply_patch", "edit"]);
107
+ const update = handlers.get("message_update") as (event: unknown, context: { abort(): void }) => void;
108
+ let aborts = 0;
109
+ callUpdate(update, "bash", { command: "apply_patch <<'PATCH'\n..." }, () => {
110
+ aborts += 1;
111
+ });
112
+ expect(aborts).toBe(0);
113
+ });
114
+ });