@oh-my-pi/pi-coding-agent 13.9.2 → 13.9.4

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/examples/sdk/02-custom-model.ts +2 -1
  3. package/package.json +7 -7
  4. package/src/cli/args.ts +10 -6
  5. package/src/cli/list-models.ts +2 -2
  6. package/src/commands/launch.ts +3 -3
  7. package/src/config/model-registry.ts +136 -38
  8. package/src/config/model-resolver.ts +47 -21
  9. package/src/config/settings-schema.ts +56 -2
  10. package/src/discovery/helpers.ts +3 -3
  11. package/src/extensibility/custom-tools/types.ts +2 -0
  12. package/src/extensibility/extensions/loader.ts +3 -2
  13. package/src/extensibility/extensions/types.ts +10 -7
  14. package/src/extensibility/hooks/types.ts +2 -0
  15. package/src/main.ts +5 -22
  16. package/src/memories/index.ts +7 -3
  17. package/src/modes/components/footer.ts +10 -8
  18. package/src/modes/components/model-selector.ts +33 -38
  19. package/src/modes/components/settings-defs.ts +32 -3
  20. package/src/modes/components/settings-selector.ts +16 -5
  21. package/src/modes/components/status-line/context-thresholds.ts +68 -0
  22. package/src/modes/components/status-line/segments.ts +11 -12
  23. package/src/modes/components/status-line.ts +2 -6
  24. package/src/modes/components/thinking-selector.ts +7 -7
  25. package/src/modes/components/tree-selector.ts +3 -2
  26. package/src/modes/controllers/command-controller.ts +11 -26
  27. package/src/modes/controllers/event-controller.ts +16 -3
  28. package/src/modes/controllers/input-controller.ts +4 -2
  29. package/src/modes/controllers/selector-controller.ts +5 -4
  30. package/src/modes/interactive-mode.ts +2 -2
  31. package/src/modes/rpc/rpc-client.ts +5 -10
  32. package/src/modes/rpc/rpc-types.ts +5 -5
  33. package/src/modes/theme/theme.ts +8 -3
  34. package/src/priority.json +1 -0
  35. package/src/prompts/system/auto-handoff-threshold-focus.md +1 -0
  36. package/src/prompts/system/system-prompt.md +18 -2
  37. package/src/prompts/tools/hashline.md +139 -83
  38. package/src/sdk.ts +24 -16
  39. package/src/session/agent-session.ts +261 -118
  40. package/src/session/agent-storage.ts +14 -14
  41. package/src/session/compaction/compaction.ts +500 -13
  42. package/src/session/messages.ts +12 -1
  43. package/src/session/session-manager.ts +77 -19
  44. package/src/slash-commands/builtin-registry.ts +48 -0
  45. package/src/task/agents.ts +3 -2
  46. package/src/task/executor.ts +2 -2
  47. package/src/task/types.ts +2 -1
  48. package/src/thinking.ts +87 -0
  49. package/src/tools/browser.ts +15 -6
  50. package/src/tools/fetch.ts +118 -100
  51. package/src/tools/index.ts +2 -1
  52. package/src/web/kagi.ts +62 -7
  53. package/src/web/search/providers/exa.ts +74 -3
@@ -3,7 +3,7 @@
3
3
  * Handles TUI rendering and user interaction, delegating business logic to AgentSession.
4
4
  */
5
5
  import * as path from "node:path";
6
- import type { Agent, AgentMessage } from "@oh-my-pi/pi-agent-core";
6
+ import { type Agent, type AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
7
  import type { AssistantMessage, ImageContent, Message, Model, UsageReport } from "@oh-my-pi/pi-ai";
8
8
  import type { Component, Loader, SlashCommand } from "@oh-my-pi/pi-tui";
9
9
  import {
@@ -423,7 +423,7 @@ export class InteractiveMode implements InteractiveModeContext {
423
423
  } else if (this.isPythonMode) {
424
424
  this.editor.borderColor = theme.getPythonModeBorderColor();
425
425
  } else {
426
- const level = this.session.thinkingLevel || "off";
426
+ const level = this.session.thinkingLevel ?? ThinkingLevel.Off;
427
427
  this.editor.borderColor = theme.getThinkingBorderColor(level);
428
428
  }
429
429
  this.updateEditorTopBorder();
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Spawns the agent in RPC mode and provides a typed API for all operations.
5
5
  */
6
- import type { AgentEvent, AgentMessage } from "@oh-my-pi/pi-agent-core";
7
- import type { ImageContent, ThinkingLevel } from "@oh-my-pi/pi-ai";
6
+ import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
+ import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
8
8
  import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
9
9
  import type { BashResult } from "../../exec/bash-executor";
10
10
  import type { SessionStats } from "../../session/agent-session";
@@ -34,12 +34,7 @@ export interface RpcClientOptions {
34
34
  args?: string[];
35
35
  }
36
36
 
37
- export interface ModelInfo {
38
- provider: string;
39
- id: string;
40
- contextWindow: number;
41
- reasoning: boolean;
42
- }
37
+ export type ModelInfo = Pick<Model, "provider" | "id" | "contextWindow" | "reasoning" | "thinking">;
43
38
 
44
39
  export type RpcEventListener = (event: AgentEvent) => void;
45
40
 
@@ -284,7 +279,7 @@ export class RpcClient {
284
279
  */
285
280
  async cycleModel(): Promise<{
286
281
  model: { provider: string; id: string };
287
- thinkingLevel: ThinkingLevel;
282
+ thinkingLevel: ThinkingLevel | undefined;
288
283
  isScoped: boolean;
289
284
  } | null> {
290
285
  const response = await this.#send({ type: "cycle_model" });
@@ -309,7 +304,7 @@ export class RpcClient {
309
304
  /**
310
305
  * Cycle thinking level.
311
306
  */
312
- async cycleThinkingLevel(): Promise<{ level: ThinkingLevel } | null> {
307
+ async cycleThinkingLevel(): Promise<{ level: Effort } | null> {
313
308
  const response = await this.#send({ type: "cycle_thinking_level" });
314
309
  return this.#getData(response);
315
310
  }
@@ -4,8 +4,8 @@
4
4
  * Commands are sent as JSON lines on stdin.
5
5
  * Responses and events are emitted as JSON lines on stdout.
6
6
  */
7
- import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
8
- import type { ImageContent, Model, ThinkingLevel } from "@oh-my-pi/pi-ai";
7
+ import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
+ import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
9
9
  import type { BashResult } from "../../exec/bash-executor";
10
10
  import type { SessionStats } from "../../session/agent-session";
11
11
  import type { CompactionResult } from "../../session/compaction";
@@ -70,7 +70,7 @@ export type RpcCommand =
70
70
 
71
71
  export interface RpcSessionState {
72
72
  model?: Model;
73
- thinkingLevel: ThinkingLevel;
73
+ thinkingLevel: ThinkingLevel | undefined;
74
74
  isStreaming: boolean;
75
75
  isCompacting: boolean;
76
76
  steeringMode: "all" | "one-at-a-time";
@@ -114,7 +114,7 @@ export type RpcResponse =
114
114
  type: "response";
115
115
  command: "cycle_model";
116
116
  success: true;
117
- data: { model: Model; thinkingLevel: ThinkingLevel; isScoped: boolean } | null;
117
+ data: { model: Model; thinkingLevel: ThinkingLevel | undefined; isScoped: boolean } | null;
118
118
  }
119
119
  | {
120
120
  id?: string;
@@ -131,7 +131,7 @@ export type RpcResponse =
131
131
  type: "response";
132
132
  command: "cycle_thinking_level";
133
133
  success: true;
134
- data: { level: ThinkingLevel } | null;
134
+ data: { level: Effort } | null;
135
135
  }
136
136
 
137
137
  // Queue modes
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { ThinkingLevel } from "@oh-my-pi/pi-ai";
3
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
4
+ import type { Effort } from "@oh-my-pi/pi-ai";
4
5
  import {
5
6
  detectMacOSAppearance,
6
7
  type HighlightColors as NativeHighlightColors,
@@ -108,6 +109,7 @@ export type SymbolKey =
108
109
  | "icon.warning"
109
110
  | "icon.rewind"
110
111
  | "icon.auto"
112
+ | "icon.fast"
111
113
  | "icon.extensionSkill"
112
114
  | "icon.extensionTool"
113
115
  | "icon.extensionSlashCommand"
@@ -268,6 +270,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
268
270
  "icon.warning": "⚠",
269
271
  "icon.rewind": "↶",
270
272
  "icon.auto": "⟲",
273
+ "icon.fast": "⚡",
271
274
  "icon.extensionSkill": "✦",
272
275
  "icon.extensionTool": "🛠",
273
276
  "icon.extensionSlashCommand": "⌘",
@@ -499,7 +502,7 @@ const NERD_SYMBOLS: SymbolMap = {
499
502
  "icon.rewind": "\uf0e2",
500
503
  // pick: 󰁨 | alt:   
501
504
  "icon.auto": "\u{f0068}",
502
- // pick:  | alt:  
505
+ "icon.fast": "\uf0e7",
503
506
  "icon.extensionSkill": "\uf0eb",
504
507
  // pick:  | alt:  
505
508
  "icon.extensionTool": "\uf0ad",
@@ -680,6 +683,7 @@ const ASCII_SYMBOLS: SymbolMap = {
680
683
  "icon.warning": "[!]",
681
684
  "icon.rewind": "<-",
682
685
  "icon.auto": "[A]",
686
+ "icon.fast": ">>",
683
687
  "icon.extensionSkill": "SK",
684
688
  "icon.extensionTool": "TL",
685
689
  "icon.extensionSlashCommand": "/",
@@ -1220,7 +1224,7 @@ export class Theme {
1220
1224
  return this.mode;
1221
1225
  }
1222
1226
 
1223
- getThinkingBorderColor(level: ThinkingLevel): (str: string) => string {
1227
+ getThinkingBorderColor(level: ThinkingLevel | Effort): (str: string) => string {
1224
1228
  // Map thinking levels to dedicated theme colors
1225
1229
  switch (level) {
1226
1230
  case "off":
@@ -1381,6 +1385,7 @@ export class Theme {
1381
1385
  warning: this.#symbols["icon.warning"],
1382
1386
  rewind: this.#symbols["icon.rewind"],
1383
1387
  auto: this.#symbols["icon.auto"],
1388
+ fast: this.#symbols["icon.fast"],
1384
1389
  extensionSkill: this.#symbols["icon.extensionSkill"],
1385
1390
  extensionTool: this.#symbols["icon.extensionTool"],
1386
1391
  extensionSlashCommand: this.#symbols["icon.extensionSlashCommand"],
package/src/priority.json CHANGED
@@ -10,6 +10,7 @@
10
10
  "mini"
11
11
  ],
12
12
  "slow": [
13
+ "gpt-5.4",
13
14
  "gpt-5.3-codex",
14
15
  "gpt-5.3",
15
16
  "gpt-5.2-codex",
@@ -0,0 +1 @@
1
+ Threshold-triggered maintenance: preserve critical implementation state and immediate next actions.
@@ -186,7 +186,7 @@ Use the Task tool unless the change is:
186
186
  - A direct answer or explanation with no code changes
187
187
  - A command the user asked you to run yourself
188
188
 
189
- For everything else — multi-file changes, refactors, new features, test additions, investigations — break the work into tasks and delegate. Err on the side of delegating. You are an orchestrator first, a coder second.
189
+ For everything else — multi-file changes, refactors, new features, test additions, investigations — break the work into tasks and delegate once the target design is settled. Err on the side of delegating after the architectural direction is fixed.
190
190
  </eager-tasks>
191
191
  {{/if}}
192
192
 
@@ -218,6 +218,18 @@ These are inviolable. Violation is system failure.
218
218
  6. You **MUST NOT** ask for information obtainable from tools, repo context, or files. File referenced → you **MUST** locate and read it. Path implied → you **MUST** resolve it.
219
219
  7. Full CUTOVER is **REQUIRED**. You **MUST** replace old usage everywhere you touch — no backwards-compat shims, no gradual migration, no "keeping both for now." The old way is dead; lingering instances **MUST** be treated as bugs.
220
220
 
221
+ # Design Integrity
222
+ - You **MUST** prefer a coherent final design over a minimally invasive patch.
223
+ - You **MUST NOT** preserve obsolete abstractions to reduce edit scope.
224
+ - Temporary bridges are **PROHIBITED** unless the user explicitly asks for a migration path.
225
+ - If a refactor introduces a new canonical abstraction, you **MUST** migrate consumers to it instead of wrapping it in compatibility helpers.
226
+ - Parallel APIs that express the same concept are a bug, not a convenience.
227
+ - Boolean compatibility helpers that collapse richer capability models are **PROHIBITED**.
228
+ - You **MUST NOT** collapse structured capability data into lossy booleans or convenience wrappers unless the domain is truly boolean.
229
+ - If a change removes a field, type, or API, all fixtures, tests, docs, and callsites using it **MUST** be updated in the same change.
230
+ - You **MUST** optimize for the next maintainer's edit, not for minimizing the current diff.
231
+ - "Works" is insufficient. The result **MUST** also be singular, obvious, and maintainable.
232
+
221
233
  # Procedure
222
234
  ## 1. Scope
223
235
  {{#if skills.length}}- If a skill matches the domain, you **MUST** read it before starting.{{/if}}
@@ -245,6 +257,8 @@ Justify sequential work; default parallel. Cannot articulate why B depends on A
245
257
  - You **MUST** write idiomatic, simple, maintainable code. Complexity **MUST** earn its place.
246
258
  - You **MUST** fix in the place the bug lives. You **MUST NOT** bandaid the problem within the caller.
247
259
  - You **MUST** clean up unused code ruthlessly: dead parameters, unused helpers, orphaned types. You **MUST** delete them and update callers. Resulting code **MUST** be pristine.
260
+ - For every new abstraction, you **MUST** identify what becomes redundant: old helpers, fallback branches, compatibility adapters, duplicate tests, stale fixtures, and docs that describe removed behavior.
261
+ - You **MUST** delete or rewrite redundant code in the same change. Leaving obsolete code reachable, compilable, or tested is a failure of cutover.
248
262
  - You **MUST NOT** leave breadcrumbs. When you delete or move code, you **MUST** remove it cleanly — no `// moved to X` comments, no `// relocated` markers, no re-exports from the old location. The old location **MUST** be removed without trace.
249
263
  - You **MUST** fix from first principles. You **MUST NOT** apply bandaids. The root cause **MUST** be found and fixed at its source. A symptom suppressed is a bug deferred.
250
264
  - When a tool call fails or returns unexpected output, you **MUST** read the full error and diagnose it.
@@ -297,8 +311,10 @@ Today is '{{date}}', and your work begins now. Get it right.
297
311
 
298
312
  <critical>
299
313
  - You **MUST** use the most specialized tool, **NEVER** `cat` if there's tool.bash, `rg/grep`:tool.grep, `find`:tool.find, `sed`:tool.edit…
300
- - Every turn **MUST** advance the deliverable. A non-final turn without at least one side-effect is **PROHIBITED**.
314
+ - Every turn **MUST** materially advance the deliverable.
301
315
  - You **MUST** default to action. You **MUST NOT** ask for confirmation to continue work. If you hit an error, you **MUST** fix it. If you know the next step, you **MUST** take it. The user will intervene if needed.
316
+ - You **MUST NOT** make speculative edits before understanding the surrounding design.
317
+ - You **MUST** default to informed action. You **MUST NOT** ask for confirmation to continue work. If you hit an error, you **MUST** fix it. If you know the next step, you **MUST** take it. The user will intervene if needed.
302
318
  - You **MUST NOT** ask when the answer may be obtained from available tools or repo context/files.
303
319
  - You **MUST** verify the effect. When a task involves a behavioral change, you **MUST** confirm the change is observable before yielding: run the specific test, command, or scenario that covers your change.
304
320
  </critical>
@@ -1,12 +1,31 @@
1
1
  Applies precise, surgical file edits by referencing `LINE#ID` tags from `read` output. Each tag uniquely identifies a line, so edits remain stable even when lines shift.
2
2
 
3
+ <critical>
4
+ - Never anchor insertions on blank lines or lone closing delimiters like `}`, `]`, `)`, `};`, or `),` — they are mechanically valid tags but semantically unstable edit boundaries.
5
+ - For `append`/`prepend`, `lines` **MUST** contain only the newly introduced content. Do not re-emit surrounding braces, brackets, parentheses, or sibling declarations that already exist in the file.
6
+ - `append`/`prepend` are for self-contained new content only: sibling declarations, new object/list members, new test cases, or similar additions whose surrounding structure stays unchanged.
7
+ - When changing existing code near a block tail or closing delimiter, default to `replace` over the owned span instead of inserting around the boundary.
8
+ - When adding a sibling declaration, default to `prepend` on the next sibling declaration instead of `append` on the previous block's closing brace.
9
+ - If any inserted line is just a closing delimiter, stop and re-check the edit shape. A closing line is only valid when it belongs to newly introduced structure; if it belongs to surrounding existing structure, your edit should be a `replace` that consumes the old boundary.
10
+ </critical>
11
+
3
12
  <workflow>
4
13
  Follow these steps in order for every edit:
5
14
  1. You **SHOULD** issue a `read` call before editing to get fresh `LINE#ID` tags. Editing without current tags causes mismatches because other edits or external changes may have shifted line numbers since your last read.
6
15
  2. You **MUST** submit one `edit` call per file with all operations. Multiple calls to the same file require re-reading between each one (tags shift after each edit), so batching avoids wasted round-trips. Think your changes through before submitting.
7
- 3. You **MUST** pick the smallest operation per change site. Each operation should be one logical mutation a single replace, insert, or delete. Combining unrelated changes into one operation makes errors harder to diagnose and recover from.
16
+ 3. You **MUST** pick the operation that matches the owning structure, not merely the smallest textual diff. Use the smallest operation only when it still cleanly owns the changed syntax. If a tiny edit would patch around a block tail, delimiter, or neighboring structural line, expand it to the semantically correct `replace` span instead.
8
17
  </workflow>
9
18
 
19
+ <checklist>
20
+ Before choosing the payload, answer these questions in order:
21
+ 1. **Am I replacing existing lines or inserting new ones?** If any existing line changes, use `replace` for the full changed span.
22
+ 2. **What declaration or block owns this anchor line?** Prefer declaration/header lines over blank lines or delimiters.
23
+ 3. **Am I inserting self-contained new content, or changing an existing block?** Use `append`/`prepend` only for self-contained additions. If surrounding code, indentation, or closers also change, use `replace`.
24
+ 4. **Am I editing near a block tail or closing delimiter?** If yes, expand the edit to own that tail instead of patching just the last line or two.
25
+ 5. **Does `lines` contain only new content?** For `append`/`prepend`, do not include existing closing braces or other surrounding syntax from the file.
26
+ 6. **Would the replacement duplicate the line immediately after `end`?** If yes, extend the range to consume the old boundary.
27
+ </checklist>
28
+
10
29
  <operations>
11
30
  **`path`** — the path to the file to edit.
12
31
  **`move`** — if set, move the file to the given path.
@@ -25,9 +44,12 @@ Tags are applied bottom-up: later edits (by position) are applied first, so earl
25
44
  </operations>
26
45
 
27
46
  <rules>
28
- 1. **Anchor on unique, structural lines.** When inserting between blocks, anchor on the nearest unique declaration using `prepend` or `append`.
29
- 2. **Use `prepend`/`append` only when the anchor line itself is not changing.** Inserting near an unchanged boundary keeps the edit minimal.
30
- 3. **Use range `replace` when any line in the span changes.** If you need to both insert lines and modify a neighboring line, a range replace covering all lines to remove is way to go.
47
+ 1. **Anchor on unique declaration or header lines, not delimiters.** Safe anchors are lines like `function beta() {`, `if (…) {`, `const value =`, or other unique structural headers. Blank lines and lone closers like `}` are never good insertion anchors.
48
+ 2. **Use `prepend`/`append` only for self-contained additions whose surrounding structure stays unchanged.** If you are adding a sibling declaration, prefer `prepend` on the next sibling declaration instead of `append` on the previous block closer.
49
+ 3. **If the change touches existing code near a block tail, use range `replace` over the owned span.** Do not patch just the final line(s) before a closing delimiter when the surrounding structure, indentation, or control flow is also changing.
50
+ 4. **Match surrounding indentation for new lines.** When inserting via `prepend`/`append`, look at the anchor line and its neighbors in the `read` output. New `lines` entries **MUST** carry the same leading whitespace. If the context uses tabs at depth 1 (`\t`), your inserted declarations need `\t` and bodies need `\t\t`. Inserting at indent level 0 inside an indented block is always wrong.
51
+ 5. **Consume the old closing boundary when your replacement emits one.** If the replacement's final line is a closing delimiter like `}`, `]`, or `)`, the `end` line **MUST** include the original matching closer that would otherwise remain in the file. Before submitting, compare the replacement's last line with the line immediately after `end`; if they would be the same boundary, extend the range so the old closer is removed.
52
+ 6. **If you expect a second tiny cleanup edit for `}`, `};`, indentation, or a duplicated boundary, your first edit shape is wrong.** Expand the first `replace` so it owns the structural tail in one shot.
31
53
  </rules>
32
54
 
33
55
  <recovery>
@@ -36,17 +58,38 @@ Edits can fail in two ways. Here is exactly what to do for each:
36
58
  2. **No-op (`identical`):** Your replacement is identical to the existing content — nothing changed. You **MUST NOT** resubmit the same edit. Re-read the target lines to understand what is actually there, then adjust your edit.
37
59
  </recovery>
38
60
 
39
- <example name="single-line replace">
61
+ <examples>
62
+ All examples below reference the same file, `util.ts`:
40
63
  ```ts
41
- {{hlinefull 23 " const timeout: number = 5000;"}}
64
+ {{hlinefull 1 "// @ts-ignore"}}
65
+ {{hlinefull 2 "const timeout = 5000;"}}
66
+ {{hlinefull 3 "const tag = \"DO NOT SHIP\";"}}
67
+ {{hlinefull 4 ""}}
68
+ {{hlinefull 5 "function alpha() {"}}
69
+ {{hlinefull 6 "\tlog();"}}
70
+ {{hlinefull 7 "}"}}
71
+ {{hlinefull 8 ""}}
72
+ {{hlinefull 9 "function beta() {"}}
73
+ {{hlinefull 10 "\t// TODO: remove after migration"}}
74
+ {{hlinefull 11 "\tlegacy();"}}
75
+ {{hlinefull 12 "\ttry {"}}
76
+ {{hlinefull 13 "\t\treturn parse(data);"}}
77
+ {{hlinefull 14 "\t} catch (err) {"}}
78
+ {{hlinefull 15 "\t\tconsole.error(err);"}}
79
+ {{hlinefull 16 "\t\treturn null;"}}
80
+ {{hlinefull 17 "\t}"}}
81
+ {{hlinefull 18 "}"}}
42
82
  ```
83
+
84
+ <example name="single-line replace">
85
+ Change the timeout from `5000` to `30_000`:
43
86
  ```
44
87
  {
45
- path: "",
88
+ path: "util.ts",
46
89
  edits: [{
47
90
  op: "replace",
48
- pos: {{hlineref 23 " const timeout: number = 5000;"}},
49
- lines: [" const timeout: number = 30_000;"]
91
+ pos: {{hlineref 2 "const timeout = 5000;"}},
92
+ lines: ["const timeout = 30_000;"]
50
93
  }]
51
94
  }
52
95
  ```
@@ -56,22 +99,22 @@ Edits can fail in two ways. Here is exactly what to do for each:
56
99
  Single line — `lines: null` deletes entirely:
57
100
  ```
58
101
  {
59
- path: "",
102
+ path: "util.ts",
60
103
  edits: [{
61
104
  op: "replace",
62
- pos: {{hlineref 7 "// @ts-ignore"}},
105
+ pos: {{hlineref 1 "// @ts-ignore"}},
63
106
  lines: null
64
107
  }]
65
108
  }
66
109
  ```
67
- Range — add `end`:
110
+ Range — remove the legacy block (lines 10–11):
68
111
  ```
69
112
  {
70
- path: "",
113
+ path: "util.ts",
71
114
  edits: [{
72
115
  op: "replace",
73
- pos: {{hlineref 80 " // TODO: remove after migration"}},
74
- end: {{hlineref 83 " }"}},
116
+ pos: {{hlineref 10 "\t// TODO: remove after migration"}},
117
+ end: {{hlineref 11 "\tlegacy();"}},
75
118
  lines: null
76
119
  }]
77
120
  }
@@ -79,15 +122,13 @@ Range — add `end`:
79
122
  </example>
80
123
 
81
124
  <example name="clear text but keep the line break">
82
- ```ts
83
- {{hlinefull 14 " placeholder: \"DO NOT SHIP\","}}
84
- ```
125
+ Blank out a line without removing it:
85
126
  ```
86
127
  {
87
- path: "",
128
+ path: "util.ts",
88
129
  edits: [{
89
130
  op: "replace",
90
- pos: {{hlineref 14 " placeholder: \"DO NOT SHIP\","}},
131
+ pos: {{hlineref 3 "const tag = \"DO NOT SHIP\";"}},
91
132
  lines: [""]
92
133
  }]
93
134
  }
@@ -95,23 +136,52 @@ Range — add `end`:
95
136
  </example>
96
137
 
97
138
  <example name="rewrite a block">
98
- ```ts
99
- {{hlinefull 60 " } catch (err) {"}}
100
- {{hlinefull 61 " console.error(err);"}}
101
- {{hlinefull 62 " return null;"}}
102
- {{hlinefull 63 " }"}}
139
+ Replace the catch body with smarter error handling:
140
+ ```
141
+ {
142
+ path: "util.ts",
143
+ edits: [{
144
+ op: "replace",
145
+ pos: {{hlineref 15 "\t\tconsole.error(err);"}},
146
+ end: {{hlineref 17 "\t}"}},
147
+ lines: [
148
+ "\t\tif (isEnoent(err)) return null;",
149
+ "\t\tthrow err;",
150
+ "\t}"
151
+ ]
152
+ }]
153
+ }
154
+ ```
155
+ </example>
156
+
157
+ <example name="own the block tail instead of patching around it">
158
+ When changing the tail of an existing block, replace the owned span instead of appending just before the closer.
159
+
160
+ Bad — appending a new return before the existing closer leaves the old tail in place and often leads to a second cleanup edit:
161
+ ```
162
+ {
163
+ path: "util.ts",
164
+ edits: [{
165
+ op: "append",
166
+ pos: {{hlineref 16 "\t\treturn null;"}},
167
+ lines: [
168
+ "\t\treturn fallback;"
169
+ ]
170
+ }]
171
+ }
103
172
  ```
173
+ Good — replace the block tail so the new logic and the closing boundary are owned by one edit:
104
174
  ```
105
175
  {
106
- path: "",
176
+ path: "util.ts",
107
177
  edits: [{
108
178
  op: "replace",
109
- pos: {{hlineref 61 " console.error(err);"}},
110
- end: {{hlineref 63 " }"}},
179
+ pos: {{hlineref 15 "\t\tconsole.error(err);"}},
180
+ end: {{hlineref 17 "\t}"}},
111
181
  lines: [
112
- " if (isEnoent(err)) return null;",
113
- " throw err;",
114
- " }"
182
+ "\t\tif (isEnoent(err)) return null;",
183
+ "\t\treturn fallback;",
184
+ "\t}"
115
185
  ]
116
186
  }]
117
187
  }
@@ -119,40 +189,35 @@ Range — add `end`:
119
189
  </example>
120
190
 
121
191
  <example name="inclusive end avoids duplicate boundary">
122
- This example demonstrates why `end` must include the original closing line when your replacement also contains that closer.
123
- ```ts
124
- {{hlinefull 70 "if (ok) {"}}
125
- {{hlinefull 71 " run();"}}
126
- {{hlinefull 72 "}"}}
127
- {{hlinefull 73 "after();"}}
128
- ```
129
- Bad — `end` stops before `}` while `lines` already includes `}`:
192
+ Simplify `beta()` to a one-liner. `end` must include the original closing `}` when the replacement also ends with `}`.
193
+
194
+ Bad `end` stops at line 17 (`\t}`), so the replacement adds `}` and the original function closer on line 18 survives. Result: two consecutive `}` lines.
130
195
  ```
131
196
  {
132
- path: "",
197
+ path: "util.ts",
133
198
  edits: [{
134
199
  op: "replace",
135
- pos: {{hlineref 70 "if (ok) {"}},
136
- end: {{hlineref 71 " run();"}},
200
+ pos: {{hlineref 9 "function beta() {"}},
201
+ end: {{hlineref 17 "\t}"}},
137
202
  lines: [
138
- "if (ok) {",
139
- " runSafe();",
203
+ "function beta() {",
204
+ "\treturn parse(data);",
140
205
  "}"
141
206
  ]
142
207
  }]
143
208
  }
144
209
  ```
145
- Good — include original `}` in the replaced range when replacement keeps `}`:
210
+ Good — include the function's own `}` on line 18 in the range, so the old closing boundary is consumed:
146
211
  ```
147
212
  {
148
- path: "",
213
+ path: "util.ts",
149
214
  edits: [{
150
215
  op: "replace",
151
- pos: {{hlineref 70 "if (ok) {"}},
152
- end: {{hlineref 72 "}"}},
216
+ pos: {{hlineref 9 "function beta() {"}},
217
+ end: {{hlineref 18 "}"}},
153
218
  lines: [
154
- "if (ok) {",
155
- " runSafe();",
219
+ "function beta() {",
220
+ "\treturn parse(data);",
156
221
  "}"
157
222
  ]
158
223
  }]
@@ -161,66 +226,54 @@ Good — include original `}` in the replaced range when replacement keeps `}`:
161
226
  </example>
162
227
 
163
228
  <example name="insert between sibling declarations">
164
- ```ts
165
- {{hlinefull 44 "function x() {"}}
166
- {{hlinefull 45 " runX();"}}
167
- {{hlinefull 46 "}"}}
168
- {{hlinefull 47 ""}}
169
- {{hlinefull 48 "function y() {"}}
170
- {{hlinefull 49 " runY();"}}
171
- {{hlinefull 50 "}"}}
172
- ```
229
+ Add a `gamma()` function between `alpha()` and `beta()`:
173
230
  ```
174
231
  {
175
- path: "",
232
+ path: "util.ts",
176
233
  edits: [{
177
234
  op: "prepend",
178
- pos: {{hlineref 48 "function y() {"}},
235
+ pos: {{hlineref 9 "function beta() {"}},
179
236
  lines: [
180
- "function z() {",
181
- " runZ();",
237
+ "function gamma() {",
238
+ "\tvalidate();",
182
239
  "}",
183
240
  ""
184
241
  ]
185
242
  }]
186
243
  }
187
244
  ```
188
- Use a trailing `""` to preserve the blank line between top-level sibling declarations.
245
+ Use a trailing `""` to preserve the blank line between sibling declarations.
189
246
  </example>
190
247
 
191
- <example name="disambiguate anchors">
192
- Blank lines and repeated patterns (`}`, `return null;`) appear many times. Always anchor on a unique line nearby instead.
193
- ```ts
194
- {{hlinefull 101 "}"}}
195
- {{hlinefull 102 ""}}
196
- {{hlinefull 103 "export function serialize(data: unknown): string {"}}
197
- ```
198
- Bad — anchoring on the blank line (ambiguous, may shift):
248
+ <example name="avoid closer anchors">
249
+ When inserting a sibling declaration, do not anchor on the previous block's lone closing brace. Anchor on the next declaration instead.
250
+
251
+ Bad appending after line 7 (`}`) happens to land in the gap today, but the anchor is still the previous function's closer rather than a stable declaration boundary:
199
252
  ```
200
253
  {
201
- path: "",
254
+ path: "util.ts",
202
255
  edits: [{
203
256
  op: "append",
204
- pos: {{hlineref 102 ""}},
257
+ pos: {{hlineref 7 "}"}},
205
258
  lines: [
206
- "function validate(data: unknown): boolean {",
207
- " return data != null && typeof data === \"object\";",
208
- "}",
209
- ""
259
+ "",
260
+ "function gamma() {",
261
+ "\tvalidate();",
262
+ "}"
210
263
  ]
211
264
  }]
212
265
  }
213
266
  ```
214
- Good — anchor on the unique declaration line:
267
+ Good — prepend before the next declaration so the new sibling is anchored on a declaration header, not a block tail:
215
268
  ```
216
269
  {
217
- path: "",
270
+ path: "util.ts",
218
271
  edits: [{
219
272
  op: "prepend",
220
- pos: {{hlineref 103 "export function serialize(data: unknown): string {"}},
273
+ pos: {{hlineref 9 "function beta() {"}},
221
274
  lines: [
222
- "function validate(data: unknown): boolean {",
223
- " return data != null && typeof data === \"object\";",
275
+ "function gamma() {",
276
+ "\tvalidate();",
224
277
  "}",
225
278
  ""
226
279
  ]
@@ -228,6 +281,7 @@ Good — anchor on the unique declaration line:
228
281
  }
229
282
  ```
230
283
  </example>
284
+ </examples>
231
285
 
232
286
  <critical>
233
287
  - Edit payload: `{ path, edits[] }`. Each entry: `op`, `lines`, optional `pos`/`end`. No extra keys.
@@ -235,4 +289,6 @@ Good — anchor on the unique declaration line:
235
289
  - You **MUST** re-read the file after each edit call before issuing another on the same file. Tags shift after every edit, so reusing old tags produces mismatches.
236
290
  - You **MUST NOT** use this tool to reformat, reindent, or adjust whitespace — run the project's formatter instead. If the only difference is whitespace, it is formatting; leave it alone.
237
291
  - `lines` entries **MUST** be literal file content with indentation copied exactly from the `read` output. If the file uses tabs, use `\t` in JSON (a real tab character). Using `\\t` (backslash + t) writes the literal two-character string `\t` into the file.
292
+ - For `append`/`prepend`, `lines` **MUST NOT** repeat surrounding delimiters or existing sibling code. Insert only the new content.
293
+ - Before any range `replace`, you **MUST** check whether the replacement's last line duplicates the original line immediately after `end` (most often a closing `}`, `]`, or `)`). If it does, extend the range to consume that old boundary instead of leaving two closers behind.
238
294
  </critical>