@bacnh85/pi-model-tools 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,6 +92,9 @@ Rules:
92
92
  - Context+removed must match **uniquely** in the file (diverges from Codex's
93
93
  first-match for safety, matching `edit`'s philosophy). Add more context lines
94
94
  if a match is ambiguous.
95
+ - If the `@@` anchor text repeats as the immediately-following context or
96
+ removed line (common when models treat `@@` as a diff-style locator header),
97
+ the duplicate is auto-collapsed — only one occurrence is matched.
95
98
  - Matching is progressive-fuzzy (exact → strip-trailing-ws → strip-both-ws →
96
99
  Unicode-normalize), so minor whitespace/Unicode differences still apply.
97
100
 
@@ -220,7 +220,7 @@ export default function (pi: ExtensionAPI) {
220
220
  name: "apply_patch",
221
221
  label: "apply_patch",
222
222
  description: [
223
- "Apply a Codex-style V4D patch to edit one or more files. Emit only changed lines plus a little surrounding context (a small diff), which is easier to get right than reproducing a large verbatim block. Supported sections: `*** Add File: <path>` (only `+` lines), `*** Delete File: <path>` (no payload), `*** Update File: <path>` or `*** Update File: <old> → <new>` (rename). Inside an Update section, each hunk is preceded by a `@@` anchor line whose text is an unchanged context line, then `-` removed lines and `+` added lines. Leading-space context lines (` `) are also allowed. Context+removed must match UNIQUELY in the file. Wrap the whole patch in `*** Begin Patch` ... `*** End Patch`.",
223
+ "Apply a Codex-style V4D patch to edit one or more files. Emit only changed lines plus a little surrounding context (a small diff), which is easier to get right than reproducing a large verbatim block. Supported sections: `*** Add File: <path>` (only `+` lines), `*** Delete File: <path>` (no payload), `*** Update File: <path>` or `*** Update File: <old> → <new>` (rename). Inside an Update section, each hunk is preceded by a `@@` anchor line whose text is an unchanged context line, then `-` removed lines and `+` added lines. Leading-space context lines (` `) are also allowed. If the @@ anchor text is restated as the immediately-following context or removed line, the duplicate is auto-collapsed. Context+removed must match UNIQUELY in the file. Wrap the whole patch in `*** Begin Patch` ... `*** End Patch`.",
224
224
  "",
225
225
  "Example:",
226
226
  "*** Begin Patch", "*** Update File: src/foo.ts", "@@ export function foo()", "- return 1", "+ return 2", "*** End Patch",
@@ -229,6 +229,7 @@ export default function (pi: ExtensionAPI) {
229
229
  promptGuidelines: [
230
230
  "Use apply_patch for multi-line or multi-file edits: emit a small diff (context + -/+ lines) instead of reproducing large verbatim oldText blocks.",
231
231
  "Each Update hunk needs a unique anchor: include enough unchanged context lines so the context+removed block matches exactly once in the file.",
232
+ "If the @@ anchor repeats on the very next line (as space-context or -removed), the duplicate is auto-collapsed.",
232
233
  "For a single tiny one-line replacement, edit is fine; for anything larger or spanning multiple files, prefer apply_patch.",
233
234
  ],
234
235
  parameters: Type.Object({ patch: Type.String({ description: "The V4D patch text, wrapped in *** Begin Patch ... *** End Patch." }) }),
@@ -344,8 +345,12 @@ export default function (pi: ExtensionAPI) {
344
345
  if (readHint) systemPrompt = `${systemPrompt}\n\n${readHint}`;
345
346
  }
346
347
 
347
- // apply_patch preference — DeepSeek V4 Flash only (GLM is better with exact-match
348
- // edit; apply_patch context anchors confuse it and it lacks agentic fallback).
348
+ // apply_patch preference — all DeepSeek V4 (flash+pro); GLM excluded per eval.
349
+ // Eval (2026-07-29, 15 trials, 3 targets) showed all models use edit with zero
350
+ // edit_mismatch errors. DeepSeek keeps guidance as a safety net for real-world
351
+ // multi-file/frontmatter edits beyond the eval's scope; GLM excluded because it
352
+ // doesn't receive the suite of DeepSeek-specific steering (Super Power, selection
353
+ // guidance, semantic-miss blocking) and thus doesn't need the companion hint.
349
354
  if (activeFamily === "deepseek-v4") {
350
355
  const patchHint = applyPatchPreferenceGuidance(activeForHint);
351
356
  if (patchHint) systemPrompt = `${systemPrompt}\n\n${patchHint}`;
@@ -21,6 +21,10 @@
21
21
  * Divergence from Codex: context+removed blocks must match UNIQUELY (Codex
22
22
  * takes the first match). Silent wrong-location edits are worse than a clear
23
23
  * error, and this matches pi's own `edit` philosophy.
24
+ *
25
+ * Leniency: if the @@ anchor text repeats as the immediately-following context
26
+ * or removed line (common when models treat `@@` as a git-diff-style locator
27
+ * header), the duplicate is collapsed automatically.
24
28
  */
25
29
 
26
30
  import { readFile, writeFile, unlink } from "node:fs/promises";
@@ -208,6 +212,17 @@ function eqRstrip(a: string, b: string) { return a.trimEnd() === b.trimEnd(); }
208
212
  function eqTrim(a: string, b: string) { return a.trim() === b.trim(); }
209
213
  function eqUnicode(a: string, b: string) { return normalizeAscii(a) === normalizeAscii(b); }
210
214
 
215
+ /**
216
+ * Loose equality matching seekSequence's 4-pass progression. Used for dedup
217
+ * so the collapse is consistent with the matcher's own fuzzy boundaries.
218
+ */
219
+ function linesLooselyEqual(a: string, b: string): boolean {
220
+ return a === b
221
+ || a.trimEnd() === b.trimEnd()
222
+ || a.trim() === b.trim()
223
+ || normalizeAscii(a) === normalizeAscii(b);
224
+ }
225
+
211
226
  /**
212
227
  * Count matches of `pattern` lines within `lines`, trying progressively
213
228
  * lenient equality. Returns count and first start index under the strictest
@@ -325,12 +340,23 @@ export async function applyPatchToFiles(parsed: ParsedPatch, cwd: string): Promi
325
340
  const newHunks: { start: number; removedLen: number; added: string[] }[] = [];
326
341
 
327
342
  for (const h of hunks) {
328
- const anchor = [...h.context, ...h.removed];
329
- const { count, firstIndex, exact: hExact } = seekSequence(work, anchor);
343
+ // ponytail: collapse a redundant @@ anchor the model restated as
344
+ // the first payload line (context or removed). Models treat `@@` like
345
+ // a git-diff locator header and repeat it as context/removed, which
346
+ // would otherwise require the line to appear twice consecutively in
347
+ // the file. Real Codex makes @@ a pure cursor-advance locator; we keep
348
+ // it in the match block so unique-match disambiguation still works
349
+ // (@@ fn / -dup), but drop the duplicate. Upgrade to full cursor
350
+ // semantics if unique-match is ever relaxed.
351
+ let matchBlock = [...h.context, ...h.removed];
352
+ if (matchBlock.length >= 2 && linesLooselyEqual(matchBlock[0], matchBlock[1])) {
353
+ matchBlock = matchBlock.slice(1);
354
+ }
355
+ const { count, firstIndex, exact: hExact } = seekSequence(work, matchBlock);
330
356
  if (!hExact) exact = false;
331
357
  if (count === 0) throw new Error(`Hunk context not found in ${op.path}. Ensure context lines match the file.`);
332
358
  if (count > 1) throw new Error(`Hunk context is ambiguous (${count} matches) in ${op.path}. Add more surrounding context lines to make it unique.`);
333
- const removedStart = firstIndex + h.context.length;
359
+ const removedStart = firstIndex + (matchBlock.length - h.removed.length);
334
360
  newHunks.push({ start: removedStart, removedLen: h.removed.length, added: h.added });
335
361
  }
336
362
 
@@ -96,18 +96,21 @@ export function deepSeekSelectionGuidance(activeTools: readonly string[]): strin
96
96
  // ── apply_patch preference hint (DeepSeek/GLM) ──
97
97
 
98
98
  /**
99
- * When apply_patch is available, tell weaker models to prefer it over edit for
100
- * anything beyond a single tiny replacement. Returns undefined if the tool is
101
- * not active so it never misdirects. Gated to DeepSeek/GLM by the caller (see
102
- * index.ts before_agent_start), since Claude/OpenAI are already reliable with edit.
99
+ * When apply_patch is available, tell the model to prefer it over edit for
100
+ * anything beyond a small change (>3 lines, YAML frontmatter, or
101
+ * multi-hunk/multi-file). Returns undefined if the tool is not active so it
102
+ * never misdirects. Gated per family by the caller (see index.ts
103
+ * before_agent_start).
103
104
  */
104
105
  export function applyPatchPreferenceGuidance(activeTools: readonly string[]): string | undefined {
105
106
  if (!activeTools.includes("apply_patch")) return undefined;
106
107
  return [
107
108
  "apply_patch (preferred for non-trivial edits):",
108
- " • For multi-line, multi-hunk, or multi-file edits, emit a small V4D diff (context + -/+ lines) via apply_patch instead of reproducing large verbatim oldText blocks — far fewer match failures.",
109
+ " • For multi-line (>3 lines), multi-hunk, or multi-file edits, emit a small V4D diff (context + -/+ lines) via apply_patch instead of reproducing large verbatim oldText blocks — far fewer match failures.",
109
110
  " • Each Update hunk must anchor on enough unchanged context that the context+removed block matches UNIQUELY in the file.",
110
- " • Keep using edit for a single tiny one-line exact replacement.",
111
+ " • Keep using edit for a single tiny one-line exact replacement (≤3 lines).",
112
+ " • Touching frontmatter (YAML) — always use apply_patch. YAML is whitespace-sensitive; edit\'s exact-match oldText rarely succeeds on the first try.",
113
+ " • One-strike rule: if edit fails once, stop — switch to apply_patch, do not retry edit.",
111
114
  ].join("\n");
112
115
  }
113
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-model-tools",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Unified tool-wrapping, argument repair, reasoning management, DeepSeek V4 guidance + Super Power Mode, defensive leak-cleaning, edit mismatch repair (read-notice stripping + trim-tolerant retry), and a Codex-style apply_patch diff tool for DeepSeek V4 and GLM model families from any provider.",
5
5
  "type": "module",
6
6
  "license": "MIT",