@oh-my-pi/pi-coding-agent 15.5.0 → 15.5.1
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 +6 -0
- package/dist/types/hashline/executor.d.ts +6 -3
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +0 -125
- package/src/hashline/executor.ts +46 -24
- package/src/prompts/tools/hashline.md +43 -94
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.5.1] - 2026-05-26
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Removed the `href`, `hrefr`, and `hline` Handlebars prompt helpers along with the shared hashline anchor state; none were referenced by any built-in or user prompt template
|
|
10
|
+
|
|
5
11
|
## [15.5.0] - 2026-05-26
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -28,9 +28,12 @@ export declare class HashlineExecutor {
|
|
|
28
28
|
*/
|
|
29
29
|
feed(token: HashlineToken): void;
|
|
30
30
|
/**
|
|
31
|
-
* Flush any open pending op (
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Flush any open pending op (with its full accumulated payload, blanks
|
|
32
|
+
* included) and return the accumulated edits and warnings. The executor
|
|
33
|
+
* is single-use; reset() is required for reuse.
|
|
34
|
+
* Throws if two replace/delete ops target the same line — that pattern
|
|
35
|
+
* means the diff is painting a before/after picture instead of stating
|
|
36
|
+
* the final state, and applying both would silently duplicate content.
|
|
34
37
|
*/
|
|
35
38
|
end(): {
|
|
36
39
|
edits: HashlineEdit[];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "15.5.
|
|
4
|
+
"version": "15.5.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
48
|
"@babel/parser": "^7.29.3",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/omp-stats": "15.5.
|
|
51
|
-
"@oh-my-pi/pi-agent-core": "15.5.
|
|
52
|
-
"@oh-my-pi/pi-ai": "15.5.
|
|
53
|
-
"@oh-my-pi/pi-natives": "15.5.
|
|
54
|
-
"@oh-my-pi/pi-tui": "15.5.
|
|
55
|
-
"@oh-my-pi/pi-utils": "15.5.
|
|
50
|
+
"@oh-my-pi/omp-stats": "15.5.1",
|
|
51
|
+
"@oh-my-pi/pi-agent-core": "15.5.1",
|
|
52
|
+
"@oh-my-pi/pi-ai": "15.5.1",
|
|
53
|
+
"@oh-my-pi/pi-natives": "15.5.1",
|
|
54
|
+
"@oh-my-pi/pi-tui": "15.5.1",
|
|
55
|
+
"@oh-my-pi/pi-utils": "15.5.1",
|
|
56
56
|
"@puppeteer/browsers": "^2.13.0",
|
|
57
57
|
"@types/turndown": "5.0.6",
|
|
58
58
|
"@xterm/headless": "^6.0.0",
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
parseFrontmatter,
|
|
9
9
|
prompt,
|
|
10
10
|
} from "@oh-my-pi/pi-utils";
|
|
11
|
-
import { HL_LINE_BODY_SEP } from "../hashline/hash";
|
|
12
11
|
import { jtdToTypeScript } from "../tools/jtd-to-typescript";
|
|
13
12
|
import { parseCommandArgs, substituteArgs } from "../utils/command-args";
|
|
14
13
|
|
|
@@ -30,130 +29,6 @@ prompt.registerHelper("jtdToTypeScript", (schema: unknown): string => {
|
|
|
30
29
|
}
|
|
31
30
|
});
|
|
32
31
|
|
|
33
|
-
function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; text: string; ref: string } {
|
|
34
|
-
const num = typeof lineNum === "number" ? lineNum : Number.parseInt(String(lineNum), 10);
|
|
35
|
-
const raw = typeof content === "string" ? content : String(content ?? "");
|
|
36
|
-
const text = raw.replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r");
|
|
37
|
-
const ref = `${num}`;
|
|
38
|
-
return { num, text, ref };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface HashlineHelperRef {
|
|
42
|
-
line: number;
|
|
43
|
-
ref: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface HashlineHelperState {
|
|
47
|
-
last?: HashlineHelperRef;
|
|
48
|
-
byLine: Map<number, HashlineHelperRef>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const HL_HELPER_STATE = Symbol("hashlineHelperState");
|
|
52
|
-
|
|
53
|
-
interface HashlineHelperStateHolder {
|
|
54
|
-
[HL_HELPER_STATE]?: HashlineHelperState;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function isHelperOptions(value: unknown): value is prompt.HelperOptions {
|
|
58
|
-
return typeof value === "object" && value !== null && "hash" in value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function splitHelperArgs(args: unknown[]): { positional: unknown[]; options?: prompt.HelperOptions } {
|
|
62
|
-
const maybeOptions = args.at(-1);
|
|
63
|
-
if (!isHelperOptions(maybeOptions)) return { positional: args };
|
|
64
|
-
return { positional: args.slice(0, -1), options: maybeOptions };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getHashlineHelperState(context: unknown, options: prompt.HelperOptions | undefined): HashlineHelperState {
|
|
68
|
-
const data = options?.data;
|
|
69
|
-
const root = data?.root;
|
|
70
|
-
const holderTarget = data && typeof data === "object" ? data : root && typeof root === "object" ? root : context;
|
|
71
|
-
if (!holderTarget || typeof holderTarget !== "object") {
|
|
72
|
-
throw new Error("hashline prompt helpers require an object render context");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const holder = holderTarget as HashlineHelperStateHolder;
|
|
76
|
-
if (!holder[HL_HELPER_STATE]) {
|
|
77
|
-
holder[HL_HELPER_STATE] = { byLine: new Map() };
|
|
78
|
-
}
|
|
79
|
-
return holder[HL_HELPER_STATE];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function isLineNumberArg(value: unknown): boolean {
|
|
83
|
-
const num = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
84
|
-
return Number.isFinite(num);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function rememberHashlineRef(state: HashlineHelperState, line: number, ref: string): void {
|
|
88
|
-
const entry = { line, ref };
|
|
89
|
-
state.last = entry;
|
|
90
|
-
state.byLine.set(line, entry);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function requireStoredHashlineRef(state: HashlineHelperState, lineArg?: unknown): string {
|
|
94
|
-
if (lineArg === undefined) {
|
|
95
|
-
if (!state.last) {
|
|
96
|
-
throw new Error("{{href}} requires a previous {{hline}} call in the same prompt render");
|
|
97
|
-
}
|
|
98
|
-
return state.last.ref;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const line = typeof lineArg === "number" ? lineArg : Number.parseInt(String(lineArg), 10);
|
|
102
|
-
const entry = state.byLine.get(line);
|
|
103
|
-
if (!entry) {
|
|
104
|
-
throw new Error(`{{href ${line}}} requires a previous {{hline ${line} ...}} call in the same prompt render`);
|
|
105
|
-
}
|
|
106
|
-
return entry.ref;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function wrapHashlineRef(ref: string, args: unknown[]): string {
|
|
110
|
-
const preStr = typeof args[0] === "string" ? args[0] : "";
|
|
111
|
-
const postStr = typeof args[1] === "string" ? args[1] : "";
|
|
112
|
-
return `${preStr}${ref}${postStr}`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function resolveHashlineRef(state: HashlineHelperState, args: unknown[]): string {
|
|
116
|
-
if (args.length === 0) return requireStoredHashlineRef(state);
|
|
117
|
-
const [first, second, ...rest] = args;
|
|
118
|
-
if (isLineNumberArg(first)) {
|
|
119
|
-
if (second === undefined) return requireStoredHashlineRef(state, first);
|
|
120
|
-
const { ref } = formatHashlineRef(first, second);
|
|
121
|
-
return wrapHashlineRef(ref, rest);
|
|
122
|
-
}
|
|
123
|
-
return wrapHashlineRef(requireStoredHashlineRef(state), args);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* {{href lineNum "content"}} — compute a hashline line ref for prompt examples.
|
|
128
|
-
* {{href lineNum}} — quote the ref remembered by the earlier {{hline lineNum "..."}}
|
|
129
|
-
* {{href}} — quote the ref from the previous {{hline}} call.
|
|
130
|
-
* {{href "[" "]"}} — wrap the previous {{hline}} ref with pre/post chars.
|
|
131
|
-
* Returns `"lineNum"` (e.g., `"42"`), or `"[42]"` when pre/post are supplied.
|
|
132
|
-
*/
|
|
133
|
-
prompt.registerHelper("href", function (this: unknown, ...args: unknown[]): string {
|
|
134
|
-
const { positional, options } = splitHelperArgs(args);
|
|
135
|
-
const state = getHashlineHelperState(this, options);
|
|
136
|
-
return JSON.stringify(resolveHashlineRef(state, positional));
|
|
137
|
-
});
|
|
138
|
-
prompt.registerHelper("hrefr", function (this: unknown, ...args: unknown[]): string {
|
|
139
|
-
const { positional, options } = splitHelperArgs(args);
|
|
140
|
-
const state = getHashlineHelperState(this, options);
|
|
141
|
-
return resolveHashlineRef(state, positional);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* {{hline lineNum "content"}} — format a full read-style line with prefix.
|
|
146
|
-
* Returns `"lineNum:content"` (colon between line number and content).
|
|
147
|
-
*/
|
|
148
|
-
prompt.registerHelper("hline", function (this: unknown, ...args: unknown[]): string {
|
|
149
|
-
const { positional, options } = splitHelperArgs(args);
|
|
150
|
-
const [lineNum, content] = positional;
|
|
151
|
-
const { num, ref, text } = formatHashlineRef(lineNum, content);
|
|
152
|
-
const state = getHashlineHelperState(this, options);
|
|
153
|
-
rememberHashlineRef(state, num, ref);
|
|
154
|
-
return `${ref}${HL_LINE_BODY_SEP}${text}`;
|
|
155
|
-
});
|
|
156
|
-
|
|
157
32
|
const INLINE_ARG_SHELL_PATTERN = /\$(?:ARGUMENTS|@(?:\[\d+(?::\d*)?\])?|\d+)/;
|
|
158
33
|
const INLINE_ARG_TEMPLATE_PATTERN = /\{\{[\s\S]*?(?:\b(?:arguments|ARGUMENTS|args)\b|\barg\s+[^}]+)[\s\S]*?\}\}/;
|
|
159
34
|
|
package/src/hashline/executor.ts
CHANGED
|
@@ -30,7 +30,6 @@ type PendingOp =
|
|
|
30
30
|
interface Pending {
|
|
31
31
|
op: PendingOp;
|
|
32
32
|
payload: string[];
|
|
33
|
-
pendingBlanks: number;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
/**
|
|
@@ -81,16 +80,16 @@ export class HashlineExecutor {
|
|
|
81
80
|
this.#terminated = true;
|
|
82
81
|
return;
|
|
83
82
|
case "header":
|
|
84
|
-
this.#flushPending(
|
|
83
|
+
this.#flushPending();
|
|
85
84
|
return;
|
|
86
85
|
case "blank":
|
|
87
|
-
if (this.#pending) this.#pending.
|
|
86
|
+
if (this.#pending) this.#pending.payload.push("");
|
|
88
87
|
return;
|
|
89
88
|
case "payload":
|
|
90
89
|
this.#handlePayload(token.text, token.lineNum);
|
|
91
90
|
return;
|
|
92
91
|
case "op-delete":
|
|
93
|
-
this.#flushPending(
|
|
92
|
+
this.#flushPending();
|
|
94
93
|
if (token.trailingPayload) {
|
|
95
94
|
throw new Error(
|
|
96
95
|
`line ${token.lineNum}: ${HL_OP_DELETE} deletes only. Payload is forbidden after ${HL_OP_DELETE}; use ${HL_OP_REPLACE} to replace.`,
|
|
@@ -102,32 +101,34 @@ export class HashlineExecutor {
|
|
|
102
101
|
}
|
|
103
102
|
return;
|
|
104
103
|
case "op-insert":
|
|
105
|
-
this.#flushPending(
|
|
104
|
+
this.#flushPending();
|
|
106
105
|
this.#pending = {
|
|
107
106
|
op: { kind: "insert", cursor: token.cursor, lineNum: token.lineNum },
|
|
108
107
|
payload: [token.inlineBody ?? ""],
|
|
109
|
-
pendingBlanks: 0,
|
|
110
108
|
};
|
|
111
109
|
return;
|
|
112
110
|
case "op-replace":
|
|
113
|
-
this.#flushPending(
|
|
111
|
+
this.#flushPending();
|
|
114
112
|
validateRangeOrder(token.range, token.lineNum);
|
|
115
113
|
this.#pending = {
|
|
116
114
|
op: { kind: "replace", range: token.range, lineNum: token.lineNum },
|
|
117
115
|
payload: [token.inlineBody ?? ""],
|
|
118
|
-
pendingBlanks: 0,
|
|
119
116
|
};
|
|
120
117
|
return;
|
|
121
118
|
}
|
|
122
119
|
}
|
|
123
120
|
|
|
124
121
|
/**
|
|
125
|
-
* Flush any open pending op (
|
|
126
|
-
*
|
|
127
|
-
*
|
|
122
|
+
* Flush any open pending op (with its full accumulated payload, blanks
|
|
123
|
+
* included) and return the accumulated edits and warnings. The executor
|
|
124
|
+
* is single-use; reset() is required for reuse.
|
|
125
|
+
* Throws if two replace/delete ops target the same line — that pattern
|
|
126
|
+
* means the diff is painting a before/after picture instead of stating
|
|
127
|
+
* the final state, and applying both would silently duplicate content.
|
|
128
128
|
*/
|
|
129
129
|
end(): { edits: HashlineEdit[]; warnings: string[] } {
|
|
130
|
-
this.#flushPending(
|
|
130
|
+
this.#flushPending();
|
|
131
|
+
this.#validateNoOverlappingDeletes();
|
|
131
132
|
return { edits: this.#edits, warnings: this.#warnings };
|
|
132
133
|
}
|
|
133
134
|
|
|
@@ -140,16 +141,44 @@ export class HashlineExecutor {
|
|
|
140
141
|
this.#terminated = false;
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Each `:` / `!` op contributes a delete edit per line in its range; if
|
|
146
|
+
* any line ends up targeted by deletes originating from two different
|
|
147
|
+
* source ops (distinguished by their `lineNum`), the patch is internally
|
|
148
|
+
* inconsistent. Common shape: a "before" `A-B:` followed by an "after"
|
|
149
|
+
* `A-B:` over the same range, or an `A-B:` that overlaps a later `N!` /
|
|
150
|
+
* `N:`. The applier would run both literally and the file would end up
|
|
151
|
+
* with two copies of the line, not a chosen winner.
|
|
152
|
+
*/
|
|
153
|
+
#validateNoOverlappingDeletes(): void {
|
|
154
|
+
const sourceLinesByAnchor = new Map<number, number[]>();
|
|
155
|
+
for (const edit of this.#edits) {
|
|
156
|
+
if (edit.kind !== "delete") continue;
|
|
157
|
+
let sourceLines = sourceLinesByAnchor.get(edit.anchor.line);
|
|
158
|
+
if (sourceLines === undefined) {
|
|
159
|
+
sourceLines = [];
|
|
160
|
+
sourceLinesByAnchor.set(edit.anchor.line, sourceLines);
|
|
161
|
+
}
|
|
162
|
+
if (!sourceLines.includes(edit.lineNum)) sourceLines.push(edit.lineNum);
|
|
163
|
+
}
|
|
164
|
+
for (const [anchorLine, sourceLines] of sourceLinesByAnchor) {
|
|
165
|
+
if (sourceLines.length < 2) continue;
|
|
166
|
+
const [firstOp, secondOp] = [...sourceLines].sort((a, b) => a - b);
|
|
167
|
+
throw new Error(
|
|
168
|
+
`line ${secondOp}: anchor line ${anchorLine} is already targeted by the ${HL_OP_REPLACE}/${HL_OP_DELETE} op on line ${firstOp}. ` +
|
|
169
|
+
`Issue ONE op per range; payload is only the final desired content, never a before/after pair.`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
143
174
|
#handlePayload(text: string, lineNum: number): void {
|
|
144
175
|
if (this.#pending) {
|
|
145
|
-
this.#flushPendingBlanks();
|
|
146
176
|
this.#pending.payload.push(text);
|
|
147
177
|
return;
|
|
148
178
|
}
|
|
149
179
|
|
|
150
|
-
// Whitespace-only payload outside any pending op is
|
|
151
|
-
//
|
|
152
|
-
// only fully-empty lines arrive as `blank` tokens.
|
|
180
|
+
// Whitespace-only payload outside any pending op is silently dropped;
|
|
181
|
+
// fully empty lines arrive as `blank` tokens.
|
|
153
182
|
if (text.trim().length === 0) return;
|
|
154
183
|
// Orphan payload outside any pending op: pick the most specific
|
|
155
184
|
// diagnostic so the model sees the actionable hint.
|
|
@@ -174,16 +203,9 @@ export class HashlineExecutor {
|
|
|
174
203
|
);
|
|
175
204
|
}
|
|
176
205
|
|
|
177
|
-
#
|
|
178
|
-
if (!this.#pending) return;
|
|
179
|
-
for (let count = 0; count < this.#pending.pendingBlanks; count++) this.#pending.payload.push("");
|
|
180
|
-
this.#pending.pendingBlanks = 0;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
#flushPending(includeTrailingBlanks: boolean): void {
|
|
206
|
+
#flushPending(): void {
|
|
184
207
|
const pending = this.#pending;
|
|
185
208
|
if (!pending) return;
|
|
186
|
-
if (includeTrailingBlanks) this.#flushPendingBlanks();
|
|
187
209
|
|
|
188
210
|
const { op, payload } = pending;
|
|
189
211
|
const linesToInsert = payload;
|
|
@@ -1,110 +1,59 @@
|
|
|
1
1
|
Your patch language is a compact, line-anchored edit format.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<payload>
|
|
4
|
+
Patch payload is a series of hunks: `¶PATH#HASH` header followed by any number of operations. `HASH` should be copied as is from read/search. Missing? Re-`read`.
|
|
5
|
+
- No context rows, no gutters.
|
|
6
|
+
- NEVER prefix payload with diff syntax.
|
|
7
|
+
- NEVER restate unchanged lines "for context".
|
|
8
|
+
- Payload indentation is literal.
|
|
9
|
+
</payload>
|
|
6
10
|
|
|
7
11
|
<ops>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
A-B:PAYLOAD replace the inclusive range A..B with PAYLOAD
|
|
13
|
-
A:PAYLOAD shorthand for A-A:PAYLOAD
|
|
14
|
-
A-B! delete the inclusive range A..B (payload forbidden)
|
|
15
|
-
A! shorthand for A-A!
|
|
12
|
+
LINE↑PAYLOAD insert before (or BOF↑)
|
|
13
|
+
LINE↓PAYLOAD insert after (or EOF↓)
|
|
14
|
+
A-B:PAYLOAD replace A..B (or A: == A..A)
|
|
15
|
+
A-B! delete A..B (or A! == A..A)
|
|
16
16
|
</ops>
|
|
17
17
|
|
|
18
|
-
<payload>
|
|
19
|
-
- The first payload line is whatever follows the sigil on the op line. Additional payload lines follow on the next lines and append after the first.
|
|
20
|
-
- An empty inline IS an empty first line. So bare `A↓` / `A↑` insert one blank line; bare `A:` / `A-B:` replace with one blank line. `A↓\nfoo` inserts blank-then-`foo`, NOT just `foo`.
|
|
21
|
-
- Payload ends at the next op, next `¶PATH`, envelope marker, or EOF. Blank lines immediately before a next op or `¶PATH` are dropped; blank lines between content lines are preserved.
|
|
22
|
-
</payload>
|
|
23
|
-
|
|
24
18
|
<rules>
|
|
25
|
-
-
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
- Smallest op wins: add with `↑`/`↓`; replace with `:`; delete with `!`.
|
|
29
|
-
- Anchors reference the file as last read. ONE patch, ONE coordinate space — later ops still use original line numbers.
|
|
19
|
+
- **Payload is only what's NEW.** `:` replaces inside; `↑`/`↓` add at anchor. NEVER repeat anchor lines or neighbors.
|
|
20
|
+
- **Go small.** Add → `↑`/`↓`; replace → `:`; delete → `!`.
|
|
21
|
+
- **Line numbers are frozen references to what you have seen.** Later ops still use original line numbers.
|
|
30
22
|
</rules>
|
|
31
23
|
|
|
32
24
|
<common-failures>
|
|
33
|
-
- **NEVER replay past your range.** Stop before B+1; extend B if
|
|
34
|
-
- **
|
|
35
|
-
- **Read lines look like replace ops.** `84:content` already means "make line 84 equal to content" — don't echo a context line before it.
|
|
25
|
+
- **NEVER replay past your range.** Stop before B+1; extend B if needed.
|
|
26
|
+
- **Read lines look like replace ops.** `84:content` = "make line 84 content" — don't echo context before it.
|
|
36
27
|
- **NEVER fabricate file hashes.** Missing? Re-`read`.
|
|
37
|
-
- **`A!` deletes silently.** Deleting a line that closes/opens a block (`}`, `} else {`, `})`, `*/`) breaks structure with no parse error.
|
|
38
|
-
- **Pure removal uses `A-B!`, NEVER `A-B:something`.** If you have nothing to put in the range, use `!`. `A-B:X` where line `A-1` or `B+1` already reads `X` silently produces two copies of `X` — the tool trusts your payload literally. Before writing `A-B:payload`, glance at `A-1` and `B+1` and confirm payload doesn't echo either.
|
|
39
28
|
</common-failures>
|
|
40
29
|
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
{{hrefr 1}}:const TITLE = "Mrs";
|
|
56
|
-
|
|
57
|
-
# Replace a multiline statement — first line inline, rest below
|
|
58
|
-
¶mod.ts#1a2b
|
|
59
|
-
{{hrefr 3}}-{{hrefr 6}}: return [
|
|
60
|
-
"Mrs",
|
|
61
|
-
name?.trim() || "guest",
|
|
62
|
-
].join(" ");
|
|
63
|
-
|
|
64
|
-
# Insert ABOVE / BELOW a line
|
|
65
|
-
¶mod.ts#1a2b
|
|
66
|
-
{{hrefr 4}}↓ "Dr",
|
|
67
|
-
{{hrefr 5}}↑ "Dr",
|
|
68
|
-
|
|
69
|
-
# Delete one line / blank a line / insert a blank line
|
|
70
|
-
¶mod.ts#1a2b
|
|
71
|
-
{{hrefr 5}}!
|
|
72
|
-
{{hrefr 6}}:
|
|
73
|
-
{{hrefr 7}}↑
|
|
74
|
-
|
|
75
|
-
# Create a file / append to one (hash optional for boundary-only inserts)
|
|
76
|
-
¶new.ts
|
|
77
|
-
BOF↓export const done = true;
|
|
78
|
-
¶mod.ts
|
|
79
|
-
EOF↓export const done = true;
|
|
80
|
-
|
|
81
|
-
# Multi-file patch
|
|
82
|
-
¶src/a.ts#1a2b
|
|
83
|
-
12:const enabled = true;
|
|
84
|
-
¶src/b.ts#3c4d
|
|
85
|
-
20!
|
|
86
|
-
</examples>
|
|
30
|
+
<example>
|
|
31
|
+
```a.ts#1a2b
|
|
32
|
+
1:const X = "a";
|
|
33
|
+
2:export function f() { return X; }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
# replace, insert after, delete
|
|
37
|
+
```
|
|
38
|
+
¶a.ts#1a2b
|
|
39
|
+
1:const X = "b";
|
|
40
|
+
1↓const Y = "c";
|
|
41
|
+
2!
|
|
42
|
+
```
|
|
43
|
+
</example>
|
|
87
44
|
|
|
88
45
|
<anti-pattern>
|
|
89
|
-
# WRONG —
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# RIGHT — one-line insert
|
|
96
|
-
¶mod.ts#1a2b
|
|
97
|
-
{{hrefr 1}}↓const DEBUG = false;
|
|
98
|
-
|
|
99
|
-
# WRONG — bisects a multiline statement
|
|
100
|
-
¶mod.ts#1a2b
|
|
101
|
-
{{hrefr 4}}-{{hrefr 5}}: "Dr",
|
|
102
|
-
name?.trim() || "guest",
|
|
103
|
-
|
|
104
|
-
# RIGHT — widen to the full statement
|
|
105
|
-
¶mod.ts#1a2b
|
|
106
|
-
{{hrefr 3}}-{{hrefr 6}}: return [
|
|
107
|
-
"Dr",
|
|
108
|
-
name?.trim() || "guest",
|
|
109
|
-
].join(" ");
|
|
46
|
+
# WRONG — INSERT used to change a line (old line survives)
|
|
47
|
+
1↓const X = "b";
|
|
48
|
+
# WRONG — echoing read-style lines as context before the real op
|
|
49
|
+
1:const X = "a";
|
|
50
|
+
1-2:const X = "b";
|
|
51
|
+
export const Y = X;
|
|
110
52
|
</anti-pattern>
|
|
53
|
+
|
|
54
|
+
<critical>
|
|
55
|
+
- One op per range, ever.
|
|
56
|
+
- Pick op precisely. Update: `:`, add: `↑`/`↓`, remove: `!`.
|
|
57
|
+
- Payload is only what's NEW; never repeat anchor lines or neighbors.
|
|
58
|
+
- Anchor exactly; don't anchor neighbors.
|
|
59
|
+
</critical>
|