pi-hashline-edit-pro 6.3.0 → 6.4.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.
Files changed (41) hide show
  1. package/README.md +32 -31
  2. package/index.ts +32 -16
  3. package/package.json +1 -1
  4. package/src/auto-read-all.ts +1 -1
  5. package/src/batch.ts +6 -4
  6. package/src/config-ui.ts +1 -1
  7. package/src/constants.ts +5 -2
  8. package/src/copy-move.ts +90 -65
  9. package/src/edit-common.ts +93 -51
  10. package/src/edit-fidelity.ts +16 -6
  11. package/src/grep.ts +22 -11
  12. package/src/insert.ts +5 -4
  13. package/src/model-gate.ts +1 -1
  14. package/src/payload-contract.ts +13 -9
  15. package/src/prompts.ts +32 -0
  16. package/src/read.ts +5 -10
  17. package/src/replace-match.ts +2 -2
  18. package/src/replace-render.ts +4 -4
  19. package/src/replace-response.ts +1 -0
  20. package/src/replace-undo.ts +2 -2
  21. package/src/replace.ts +2 -2
  22. package/src/write-hook.ts +1 -1
  23. package/tool-prompts/copy-snippet.md +1 -1
  24. package/tool-prompts/copy.md +2 -2
  25. package/tool-prompts/grep-guidelines.md +2 -1
  26. package/tool-prompts/grep-snippet.md +1 -1
  27. package/tool-prompts/grep.md +1 -1
  28. package/tool-prompts/insert-guidelines.md +1 -1
  29. package/tool-prompts/insert.md +1 -1
  30. package/tool-prompts/move-snippet.md +1 -1
  31. package/tool-prompts/move.md +2 -2
  32. package/tool-prompts/read-guidelines.md +3 -3
  33. package/tool-prompts/read.md +1 -3
  34. package/tool-prompts/replace-match-guidelines.md +1 -0
  35. package/tool-prompts/replace-match-snippet.md +1 -1
  36. package/tool-prompts/replace-match.md +1 -1
  37. package/tool-prompts/replace.md +3 -2
  38. package/tool-prompts/transfer-guidelines.md +3 -3
  39. package/tool-prompts/undo-last-change-guidelines.md +1 -1
  40. package/tool-prompts/undo-last-change-snippet.md +1 -1
  41. package/tool-prompts/undo-last-change.md +1 -1
package/src/copy-move.ts CHANGED
@@ -20,11 +20,11 @@ import {
20
20
  } from "./hashline";
21
21
  import { formatAnchorReclaimNotice, servedForPath, takeReclaimedPaths, withAnchorSession } from "./anchor-registry";
22
22
  import { batchMemberFor, batchServedFor, ensureBatchBase, executeBatchMember, noteBatchFailure, pendingBatchMemberFor, registerBatchPlanner, type BatchBase, type BatchMemberPlanner, type PlannedMember } from "./batch";
23
- import { loadP, loadGuide } from "./prompts";
23
+ import { loadP, loadGuidelines } from "./prompts";
24
24
  import { assertTransferReq, normReq, type TransferReq } from "./payload-contract";
25
25
  import { abortIf, assertByteLimit, isRec, splitLines } from "./utils";
26
26
  import { toDisplayPath } from "./paths";
27
- import { getDiffContextLines, readConfig } from "./config";
27
+ import { getDiffContextLines } from "./config";
28
28
  import { resolveInCwd, writeAtomic } from "./fs-write";
29
29
  import { restoreEndings, type LineEnding } from "./normalize";
30
30
  import { applyEndingOverrides, endingsForRange, joinSeparators, separatorsForSpans } from "./line-endings";
@@ -33,6 +33,7 @@ import { buildChanged, buildMetrics, type TResult } from "./replace-response";
33
33
  import { servedHashesFromDiff, serveRows } from "./served";
34
34
  import {
35
35
  assertBoundaryLinesServed,
36
+ assertPathOption,
36
37
  DEFAULT_EDIT_FLAGS,
37
38
  editRenderResultWrapper,
38
39
  editToolBase,
@@ -52,7 +53,8 @@ export type TransferKind = "copy" | "move";
52
53
  interface TransferRefs {
53
54
  sourceFrom: Anchor;
54
55
  sourceTo: Anchor;
55
- insertAfter: Anchor;
56
+ anchor: Anchor;
57
+ direction: "before" | "after";
56
58
  }
57
59
 
58
60
  export interface TransferPlan {
@@ -69,6 +71,7 @@ interface PairFileCommit {
69
71
  absolutePath: string;
70
72
  mutationTargetPath: string;
71
73
  foldedAnchorLines: number;
74
+ anchorCarry?: number;
72
75
  endingOverrides?: (LineEnding | undefined)[];
73
76
  }
74
77
 
@@ -87,12 +90,13 @@ export function parseTransferAnchors(req: TransferReq): { refs: TransferRefs; wa
87
90
  const warnings: string[] = [];
88
91
  const from = stripAnchorRow(req.source_from.trim(), "source_from entry", warnings);
89
92
  const to = stripAnchorRow(req.source_to.trim(), "source_to entry", warnings);
90
- const after = stripAnchorRow(req.insert_after.trim(), "insert_after entry", warnings);
93
+ const destination = stripAnchorRow(req.anchor.trim(), "anchor entry", warnings);
91
94
  return {
92
95
  refs: {
93
96
  sourceFrom: parseHashRef(from),
94
97
  sourceTo: parseHashRef(to),
95
- insertAfter: parseHashRef(after),
98
+ anchor: parseHashRef(destination),
99
+ direction: req.direction,
96
100
  },
97
101
  warnings,
98
102
  };
@@ -118,13 +122,14 @@ export function buildTransferEdit(input: {
118
122
  const toLine = resolveAnchorLine(refs.sourceTo, fileLines, preload.fileHashes, displayPath);
119
123
  const sourceStart = Math.min(fromLine, toLine);
120
124
  const sourceEnd = Math.max(fromLine, toLine);
121
- const insertLine = resolveAnchorLine(refs.insertAfter, fileLines, preload.fileHashes, displayPath);
125
+ const anchorLine = resolveAnchorLine(refs.anchor, fileLines, preload.fileHashes, displayPath);
126
+ const insertLine = refs.direction === "after" ? anchorLine : anchorLine - 1;
122
127
  if (insertLine >= sourceStart && insertLine < sourceEnd) {
123
128
  throw new Error(
124
- `[E_BAD_SHAPE] "insert_after" resolves to line ${insertLine}, inside the source range (lines ${sourceStart}-${sourceEnd}). Use a line before source_from, or source_to to place the block right after itself.`,
129
+ `[E_BAD_SHAPE] "anchor" resolves to line ${anchorLine}, which places the block inside the source range (lines ${sourceStart}-${sourceEnd}). Use a line outside the source range.`,
125
130
  );
126
131
  }
127
- assertBoundaryLinesServed(fileLines, preload.fileHashes, served, insertLine, insertLine, displayPath);
132
+ assertBoundaryLinesServed(fileLines, preload.fileHashes, served, anchorLine, anchorLine, displayPath);
128
133
  assertBoundaryLinesServed(fileLines, preload.fileHashes, served, sourceStart, sourceStart, displayPath);
129
134
  assertBoundaryLinesServed(fileLines, preload.fileHashes, served, sourceEnd, sourceEnd, displayPath);
130
135
  const sourceLines = fileLines.slice(sourceStart - 1, sourceEnd);
@@ -132,13 +137,17 @@ export function buildTransferEdit(input: {
132
137
  if (kind === "copy") {
133
138
  return {
134
139
  editParams: {
135
- remove_from: preload.fileHashes[insertLine - 1]!,
136
- remove_to: preload.fileHashes[insertLine - 1]!,
137
- text: [fileLines[insertLine - 1]!, ...sourceLines],
140
+ remove_from: preload.fileHashes[anchorLine - 1]!,
141
+ remove_to: preload.fileHashes[anchorLine - 1]!,
142
+ text: refs.direction === "after"
143
+ ? [fileLines[anchorLine - 1]!, ...sourceLines]
144
+ : [...sourceLines, fileLines[anchorLine - 1]!],
138
145
  },
139
146
  foldedAnchorLines: 1,
140
- anchorCarry: 0,
141
- endingOverrides: [undefined, ...sourceEndings],
147
+ anchorCarry: refs.direction === "after" ? 0 : sourceLines.length,
148
+ endingOverrides: refs.direction === "after"
149
+ ? [undefined, ...sourceEndings]
150
+ : [...sourceEndings, undefined],
142
151
  };
143
152
  }
144
153
  if (insertLine < sourceStart) {
@@ -169,40 +178,34 @@ export function buildTransferEdit(input: {
169
178
  };
170
179
  }
171
180
 
181
+ function describeTransferTargets(sourcePath: string, destinationPath: string): string {
182
+ return sourcePath === destinationPath
183
+ ? `the file "${sourcePath}"`
184
+ : `the source file "${sourcePath}" or the destination file "${destinationPath}"`;
185
+ }
186
+
172
187
  async function resolveTransferTargets(req: TransferReq, cwd: string): Promise<{ sourcePath: string; destinationPath: string }> {
173
- const { requirePath } = await readConfig();
174
- if (!requirePath && req.path !== undefined) {
175
- throw new Error("[E_BAD_SHAPE] Edit request contains unknown or unsupported fields: path. Path resolution is anchor-only; retry without `path`.");
176
- }
177
- if (requirePath && (typeof req.path !== "string" || req.path.length === 0)) {
178
- throw new Error('[E_BAD_SHAPE] Copy/move request requires a non-empty "path" string when require-path mode is on. Provide `path` matching the source or destination file the anchors were served for.');
179
- }
180
188
  const sourcePath = resolveEditTarget(req.source_from, req.source_to);
181
- const destinationPath = resolveEditTarget(req.insert_after);
182
- if (requirePath) {
183
- const { resolved } = await resolveInCwd(req.path as string, cwd);
184
- if (resolved !== sourcePath && resolved !== destinationPath) {
185
- throw new Error(`[E_BAD_SHAPE] Provided "path" "${req.path}" does not match the source file "${sourcePath}" or the destination file "${destinationPath}".`);
186
- }
187
- }
189
+ const destinationPath = resolveEditTarget(req.anchor);
190
+ await assertPathOption({
191
+ providedPath: req.path,
192
+ cwd,
193
+ targets: [sourcePath, destinationPath],
194
+ expected: describeTransferTargets(sourcePath, destinationPath),
195
+ requestLabel: "Copy/move request",
196
+ });
188
197
  return { sourcePath, destinationPath };
189
198
  }
190
199
 
191
200
  async function assertTransferPathOption(req: TransferReq, cwd: string, member: PlannedMember): Promise<void> {
192
- const { requirePath } = await readConfig();
193
- if (!requirePath && req.path !== undefined) {
194
- throw new Error("[E_BAD_SHAPE] Edit request contains unknown or unsupported fields: path. Path resolution is anchor-only; retry without `path`.");
195
- }
196
- if (requirePath && (typeof req.path !== "string" || req.path.length === 0)) {
197
- throw new Error('[E_BAD_SHAPE] Copy/move request requires a non-empty "path" string when require-path mode is on. Provide `path` matching the source or destination file the anchors were served for.');
198
- }
199
- if (requirePath) {
200
- const sourceTarget = member.sourceTarget ?? member.target;
201
- const { resolved } = await resolveInCwd(req.path as string, cwd);
202
- if (resolved !== sourceTarget && resolved !== member.target) {
203
- throw new Error(`[E_BAD_SHAPE] Provided "path" "${req.path}" does not match the source file "${sourceTarget}" or the destination file "${member.target}".`);
204
- }
205
- }
201
+ const sourceTarget = member.sourceTarget ?? member.target;
202
+ await assertPathOption({
203
+ providedPath: req.path,
204
+ cwd,
205
+ targets: [sourceTarget, member.target],
206
+ expected: describeTransferTargets(sourceTarget, member.target),
207
+ requestLabel: "Copy/move request",
208
+ });
206
209
  }
207
210
 
208
211
  interface CrossTransferPreparation {
@@ -212,6 +215,7 @@ interface CrossTransferPreparation {
212
215
  destinationDisplay: string;
213
216
  destinationEdit: HTEdit;
214
217
  destinationFolded: number;
218
+ anchorCarry?: number;
215
219
  endingOverrides: (LineEnding | undefined)[];
216
220
  sourceEdit?: HTEdit;
217
221
  }
@@ -268,15 +272,15 @@ async function prepareCrossTransfer(input: {
268
272
  throw error;
269
273
  }
270
274
 
271
- let insertLine = 0;
275
+ let anchorLine = 0;
272
276
  try {
273
- insertLine = resolveAnchorLine(refs.insertAfter, destinationLines, destinationPreload.fileHashes, destinationDisplay);
277
+ anchorLine = resolveAnchorLine(refs.anchor, destinationLines, destinationPreload.fileHashes, destinationDisplay);
274
278
  } catch (error) {
275
279
  await adopt(destinationPreload.absolutePath, error);
276
280
  throw error;
277
281
  }
278
282
  try {
279
- assertBoundaryLinesServed(destinationLines, destinationPreload.fileHashes, destinationServed, insertLine, insertLine, destinationDisplay);
283
+ assertBoundaryLinesServed(destinationLines, destinationPreload.fileHashes, destinationServed, anchorLine, anchorLine, destinationDisplay);
280
284
  } catch (error) {
281
285
  await adopt(destinationPreload.absolutePath, error);
282
286
  throw error;
@@ -289,23 +293,33 @@ async function prepareCrossTransfer(input: {
289
293
  remove_to: destinationPreload.fileHashes[0]!,
290
294
  text: [...moved],
291
295
  }
292
- : {
293
- remove_from: destinationPreload.fileHashes[insertLine - 1]!,
294
- remove_to: destinationPreload.fileHashes[insertLine - 1]!,
295
- text: [destinationLines[insertLine - 1]!, ...moved],
296
- };
296
+ : refs.direction === "after"
297
+ ? {
298
+ remove_from: destinationPreload.fileHashes[anchorLine - 1]!,
299
+ remove_to: destinationPreload.fileHashes[anchorLine - 1]!,
300
+ text: [destinationLines[anchorLine - 1]!, ...moved],
301
+ }
302
+ : {
303
+ remove_from: destinationPreload.fileHashes[anchorLine - 1]!,
304
+ remove_to: destinationPreload.fileHashes[anchorLine - 1]!,
305
+ text: [...moved, destinationLines[anchorLine - 1]!],
306
+ };
297
307
  const destinationFolded = destinationPreload.normalized.length === 0 ? 0 : 1;
298
308
  const sourceEndings = endingsForRange(sourcePreload.endingSeparators, sourceStart, sourceEnd);
299
- const endingOverrides = destinationFolded === 1 ? offsetEndings(1, sourceEndings) : sourceEndings;
309
+ const endingOverrides = destinationFolded === 0
310
+ ? sourceEndings
311
+ : refs.direction === "after"
312
+ ? offsetEndings(1, sourceEndings)
313
+ : sourceEndings;
300
314
  if (kind === "copy") {
301
- return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, endingOverrides };
315
+ return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, ...(destinationFolded === 1 ? { anchorCarry: refs.direction === "after" ? 0 : moved.length } : {}), endingOverrides };
302
316
  }
303
317
  const sourceEdit: HTEdit = {
304
318
  remove_from: sourcePreload.fileHashes[sourceStart - 1]!,
305
319
  remove_to: sourcePreload.fileHashes[sourceEnd - 1]!,
306
320
  text: [],
307
321
  };
308
- return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, sourceEdit, endingOverrides };
322
+ return { sourcePreload, destinationPreload, sourceDisplay, destinationDisplay, destinationEdit, destinationFolded, ...(destinationFolded === 1 ? { anchorCarry: refs.direction === "after" ? 0 : moved.length } : {}), sourceEdit, endingOverrides };
309
323
  }
310
324
 
311
325
  function pairEntry(pipe: PipelineResult, separators: LineEnding[] | undefined): UndoEntry {
@@ -333,6 +347,7 @@ async function commitMovePair(input: {
333
347
  ]);
334
348
  const destinationSpan = input.destination.pipe.spans?.[0];
335
349
  const sourceSpan = input.source.pipe.spans?.[0];
350
+ if (destinationSpan && input.destination.anchorCarry !== undefined) destinationSpan.carry = input.destination.anchorCarry;
336
351
  const destinationSeparators = destinationSpan
337
352
  ? separatorsForSpans(input.destination.pipe.originalSeparators, input.destination.pipe.originalHashes.length, [destinationSpan], input.destination.pipe.result, input.destination.pipe.originalEnding, splitLines(input.destination.pipe.originalNormalized))
338
353
  : undefined;
@@ -558,7 +573,7 @@ async function executeCrossFile(
558
573
  absolutePath: destination.absolute,
559
574
  mutationTargetPath: destination.resolved,
560
575
  editAnchors: [prepared.destinationEdit.remove_from, prepared.destinationEdit.remove_to],
561
- ...(prepared.destinationFolded === 1 ? { anchorCarry: 0 } : {}),
576
+ ...(prepared.anchorCarry !== undefined ? { anchorCarry: prepared.anchorCarry } : {}),
562
577
  endingOverrides: prepared.endingOverrides,
563
578
  signal,
564
579
  verb: "copied",
@@ -581,6 +596,7 @@ async function executeCrossFile(
581
596
  absolutePath: destination.absolute,
582
597
  mutationTargetPath: destination.resolved,
583
598
  foldedAnchorLines: prepared.destinationFolded,
599
+ ...(prepared.anchorCarry !== undefined ? { anchorCarry: prepared.anchorCarry } : {}),
584
600
  endingOverrides: prepared.endingOverrides,
585
601
  },
586
602
  signal,
@@ -624,6 +640,7 @@ function planTransferBatchMember(kind: TransferKind): BatchMemberPlanner {
624
640
  warnings: [...warnings, ...resWarnings],
625
641
  planned,
626
642
  foldedLines: transferPlan.foldedAnchorLines,
643
+ direction: refs.direction,
627
644
  ...(transferPlan.anchorCarry !== undefined ? { carryIndex: transferPlan.anchorCarry } : {}),
628
645
  ...(transferPlan.servedOverride !== undefined ? { servedOverride: transferPlan.servedOverride } : {}),
629
646
  ...(transferPlan.endingOverrides !== undefined ? { contentSeparators: transferPlan.endingOverrides } : {}),
@@ -686,7 +703,7 @@ async function executeBatchCrossCopy(
686
703
  hedit,
687
704
  warnings: [...warnings, ...resWarnings],
688
705
  foldedLines: prepared.destinationFolded,
689
- ...(prepared.destinationFolded === 1 ? { carryIndex: 0 } : {}),
706
+ ...(prepared.anchorCarry !== undefined ? { carryIndex: prepared.anchorCarry } : {}),
690
707
  contentSeparators: prepared.endingOverrides,
691
708
  },
692
709
  });
@@ -751,7 +768,7 @@ async function executeBatchCrossMove(
751
768
  hedit,
752
769
  warnings: [...warnings, ...resWarnings, ...sourceWarnings],
753
770
  foldedLines: prepared.destinationFolded,
754
- ...(prepared.destinationFolded === 1 ? { carryIndex: 0 } : {}),
771
+ ...(prepared.anchorCarry !== undefined ? { carryIndex: prepared.anchorCarry } : {}),
755
772
  contentSeparators: prepared.endingOverrides,
756
773
  sourceMove: {
757
774
  displayPath: prepared.sourceDisplay,
@@ -825,7 +842,7 @@ export async function transferPreview(kind: TransferKind, request: unknown, cwd:
825
842
  }
826
843
  }
827
844
 
828
- function getTransferInput(args: unknown): { path?: string; source_from?: string; source_to?: string; insert_after?: string } | null {
845
+ function getTransferInput(args: unknown): { path?: string; source_from?: string; source_to?: string; anchor?: string; direction?: string } | null {
829
846
  let normalized: unknown;
830
847
  try {
831
848
  normalized = normReq(args);
@@ -836,7 +853,8 @@ function getTransferInput(args: unknown): { path?: string; source_from?: string;
836
853
  if (
837
854
  typeof normalized.source_from !== "string" ||
838
855
  typeof normalized.source_to !== "string" ||
839
- typeof normalized.insert_after !== "string"
856
+ typeof normalized.anchor !== "string" ||
857
+ typeof normalized.direction !== "string"
840
858
  ) {
841
859
  return null;
842
860
  }
@@ -844,22 +862,27 @@ function getTransferInput(args: unknown): { path?: string; source_from?: string;
844
862
  ...(typeof normalized.path === "string" ? { path: normalized.path } : {}),
845
863
  source_from: normalized.source_from,
846
864
  source_to: normalized.source_to,
847
- insert_after: normalized.insert_after,
865
+ anchor: normalized.anchor,
866
+ direction: normalized.direction,
848
867
  };
849
868
  }
850
869
 
851
870
  const transferSourceFromSchema = Type.String({
852
871
  description:
853
- "4-char anchor of the FIRST source line to copy (never the row content).",
872
+ "4-char anchor of the FIRST source line to copy.",
854
873
  });
855
874
  const transferSourceToSchema = Type.String({
856
875
  description:
857
876
  "4-char anchor of the LAST source line to copy.",
858
877
  });
859
- const transferInsertAfterSchema = Type.String({
878
+ const transferAnchorSchema = Type.String({
860
879
  description:
861
- "4-char anchor of the destination line; the block goes after it and may live in another file.",
880
+ "4-char anchor of the destination line; the block lands before or after it and it may live in another file.",
862
881
  });
882
+ const transferDirectionSchema = Type.Union(
883
+ [Type.Literal("after"), Type.Literal("before")],
884
+ { description: '"after" places the block below the anchor line, "before" above it' },
885
+ );
863
886
  const transferPathRequiredSchema = Type.String({
864
887
  description:
865
888
  "Path to the source or destination file the anchors were served for; required and must match anchor ownership.",
@@ -869,7 +892,8 @@ const transferToolSchema = Type.Object(
869
892
  {
870
893
  source_from: transferSourceFromSchema,
871
894
  source_to: transferSourceToSchema,
872
- insert_after: transferInsertAfterSchema,
895
+ anchor: transferAnchorSchema,
896
+ direction: transferDirectionSchema,
873
897
  },
874
898
  { additionalProperties: true },
875
899
  );
@@ -881,7 +905,8 @@ export function buildTransferToolSchema(requirePath: boolean): typeof transferTo
881
905
  path: transferPathRequiredSchema,
882
906
  source_from: transferSourceFromSchema,
883
907
  source_to: transferSourceToSchema,
884
- insert_after: transferInsertAfterSchema,
908
+ anchor: transferAnchorSchema,
909
+ direction: transferDirectionSchema,
885
910
  },
886
911
  { additionalProperties: true },
887
912
  ) as typeof transferToolSchema;
@@ -895,8 +920,8 @@ export function buildTransferToolDef(kind: TransferKind, flags: EditToolFlags =
895
920
  description: loadP(`../tool-prompts/${kind}.md`),
896
921
  snippet: loadP(`../tool-prompts/${kind}-snippet.md`),
897
922
  guidelines: [
898
- ...loadGuide(`../tool-prompts/${kind}-guidelines.md`),
899
- ...loadGuide("../tool-prompts/transfer-guidelines.md"),
923
+ ...loadGuidelines(`../tool-prompts/${kind}-guidelines.md`),
924
+ ...loadGuidelines("../tool-prompts/transfer-guidelines.md"),
900
925
  ],
901
926
  },
902
927
  flags,
@@ -6,6 +6,7 @@ import { assertRangeServed, lineChecksum, parseHashRef, stripAnchorRow } from ".
6
6
  import { readConfig } from "./config";
7
7
  import { makeRenderCall, renderEditResult, type RPreview, type FgT } from "./replace-render";
8
8
  import type { ReplaceDetails } from "./replace";
9
+ import type { Guideline } from "./prompts";
9
10
  export const editPrepare = makePrepareArguments();
10
11
 
11
12
  export interface EditToolFlags {
@@ -13,6 +14,7 @@ export interface EditToolFlags {
13
14
  strictInput: boolean;
14
15
  autoRead: boolean;
15
16
  autoReadAllActive: boolean;
17
+ autoReadAllOutline: boolean;
16
18
  replaceMatchEnabled: boolean;
17
19
  copyMoveEnabled: boolean;
18
20
  codemode: boolean;
@@ -23,6 +25,7 @@ export const DEFAULT_EDIT_FLAGS: EditToolFlags = {
23
25
  strictInput: false,
24
26
  autoRead: true,
25
27
  autoReadAllActive: false,
28
+ autoReadAllOutline: false,
26
29
  replaceMatchEnabled: true,
27
30
  copyMoveEnabled: true,
28
31
  codemode: false,
@@ -35,6 +38,7 @@ export async function currentEditFlags(codemode = false): Promise<EditToolFlags>
35
38
  strictInput: config.strictInput === true,
36
39
  autoRead: config.autoRead !== false,
37
40
  autoReadAllActive: (config.autoReadAll ?? "off") !== "off",
41
+ autoReadAllOutline: (config.autoReadAll ?? "off") === "outline",
38
42
  replaceMatchEnabled: config.replaceMatchEnabled !== false,
39
43
  copyMoveEnabled: config.copyMoveEnabled !== false,
40
44
  codemode
@@ -43,7 +47,7 @@ export async function currentEditFlags(codemode = false): Promise<EditToolFlags>
43
47
 
44
48
  function preferenceGuideline(flags: EditToolFlags): string {
45
49
  const tools = gatedEditOps(["read", "replace", "replace_match", "insert", "copy", "move", "undo_last_change"], flags);
46
- return `Prefer the hashline tools for anything that touches files: ${joinOps(tools, { backtick: true })}.`;
50
+ return `Prefer the hashline tools for every file read and edit: ${joinOps(tools, { backtick: true })}.`;
47
51
  }
48
52
 
49
53
  const SHARED_EDIT_OPS = ["replace", "replace_match", "insert", "copy", "move"];
@@ -58,6 +62,17 @@ const SCRIPT_TRANSFER_GUIDELINE =
58
62
  const SCRIPT_UNDO_GUIDELINE =
59
63
  "`undo_last_change`: each edit from one codemode script takes the undo slot, so only the most recent is undoable.";
60
64
 
65
+ const READ_REREAD_ID = "read-reread";
66
+ const READ_OUTLINE_ID = "read-outline";
67
+ const READ_MATCH_ID = "read-match-served";
68
+ const UNDO_WRITE_CLEARS_ID = "undo-write-clears";
69
+ const UNDO_CROSS_FILE_MOVE_ID = "undo-cross-file-move";
70
+ const NEUTRAL_READ_REREAD = "`read`: call again after an edit when you need anchors you lack.";
71
+ const NEUTRAL_UNDO_WRITE_CLEARS = "`undo_last_change`: only the last `replace`/`replace_match`/`insert`/`copy`/`move` per file is undoable; a `write` clears it, so undo before any other edit or write.";
72
+ const GREP_OPS_PHRASE = "replace, insert, copy, or move";
73
+ const UNDO_OPS_PHRASE = "replace, replace_match, insert, copy, or move";
74
+ const UNDO_GUIDELINE_OPS_PHRASE = "`replace`/`replace_match`/`insert`/`copy`/`move`";
75
+
61
76
  function operationNames(ops: string[], flags: EditToolFlags): string {
62
77
  return joinOps(gatedEditOps(ops, flags), { backtick: true, separator: "/" });
63
78
  }
@@ -66,24 +81,24 @@ function batchGuideline(flags: EditToolFlags): string {
66
81
  const tools = operationNames(SHARED_EDIT_OPS, flags);
67
82
  const outcome = flags.autoRead ? "diff" : "result";
68
83
  const script = flags.codemode ? ` ${SCRIPT_BATCH_GUIDELINE}` : "";
69
- return `${tools}: same-file calls in one message are grouped into one batch; earlier calls reply \`In batch N (queued)\` and the last applied call shows the combined ${outcome}, with one undo for the whole batch. A call whose own request is invalid (anchors, shape, or \`old_string\`) fails on its own and is left out of the batch; the rest still commits.${script}`;
84
+ return `${tools}: same-file calls in one message commit as one batch; earlier calls reply \`In batch N (queued)\`, the last shows the combined ${outcome}, and one undo reverts all. A request-invalid call (anchors, shape, or \`old_string\`) fails alone; a failure while the batch runs aborts it.${script}`;
70
85
  }
71
86
 
72
87
  function diffGuideline(flags: EditToolFlags): string {
73
88
  const tools = operationNames(SHARED_DIFF_OPS, flags);
74
- return `${tools}: in the post-edit diff, \`-anchor│\` rows are dead anchors; \`+anchor│\` and \` anchor│\` rows are live anchors for the next edit.`;
89
+ return `${tools}: in the post-edit diff, \`-anchor│\` rows are dead anchors; \`+anchor│\` and \` anchor│\` rows carry live anchors for the next edit.`;
75
90
  }
76
91
 
77
92
  function pathGuideline(flags: EditToolFlags): string {
78
93
  const tools = operationNames(SHARED_EDIT_OPS, flags);
79
94
  return flags.requirePath
80
95
  ? `${tools}: pass \`path\` matching the file the anchors were served for; it is required and must match anchor ownership.`
81
- : `${tools}: path resolution is anchor-only; do not pass \`path\`.`;
96
+ : `${tools}: anchors resolve the file; \`path\` is optional but must match anchor ownership when passed.`;
82
97
  }
83
98
 
84
99
  function payloadGuideline(flags: EditToolFlags): string {
85
100
  const tools = operationNames(SHARED_PAYLOAD_OPS, flags);
86
- return `${tools}: JSON decoding happens once, before the tool; the tool writes the string it receives and never decodes — \`\\uXXXX\` is the character, \`\\\\uXXXX\` the literal text.`;
101
+ return `${tools}: \`text\` is written exactly as received; escapes are decoded once when the call is parsed, and the tool never decodes again — \`\\uXXXX\` is the character, \`\\\\uXXXX\` the literal text.`;
87
102
  }
88
103
 
89
104
  function strictInputGuideline(flags: EditToolFlags): string {
@@ -106,36 +121,45 @@ function finalizePrompts(
106
121
  return { description, snippet, guidelines: [...guidelines, ...shared] };
107
122
  }
108
123
 
109
- export function withReplacePrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
124
+ export function withReplacePrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
110
125
  let description = base.description;
111
- const guidelines = [preferenceGuideline(flags), ...base.guidelines];
126
+ const guidelines = [preferenceGuideline(flags), ...base.guidelines.map((guideline) => guideline.text)];
112
127
  if (!flags.replaceMatchEnabled) {
113
- description = description.replace(/\n?To change only part of a line without retyping the rest, use `replace_match` instead; it preserves every character the request does not name\./, "");
128
+ description = description
129
+ .split(/(?<=\.)\s+/)
130
+ .filter((sentence) => !sentence.includes("`replace_match`"))
131
+ .join(" ");
114
132
  }
115
133
  return finalizePrompts(description, base.snippet, guidelines, flags);
116
134
  }
117
135
 
118
- export function withReadPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
136
+ function readGuidelineApplies(guideline: Guideline, flags: EditToolFlags): boolean {
137
+ if (guideline.id === READ_OUTLINE_ID) return flags.autoReadAllOutline;
138
+ if (guideline.id === READ_MATCH_ID) return flags.replaceMatchEnabled;
139
+ return true;
140
+ }
141
+
142
+ export function withReadPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
119
143
  const preference = preferenceGuideline(flags);
120
144
  const script = flags.codemode ? [RESULT_CONTRACT_GUIDELINE] : [];
145
+ const applicable = base.guidelines.filter((guideline) => readGuidelineApplies(guideline, flags));
121
146
  if (flags.autoReadAllActive) {
122
- const rewritten = base.guidelines
123
- .filter((guideline) => !guideline.includes("call again after an edit"))
147
+ const rewritten = applicable.filter((guideline) => guideline.id !== READ_REREAD_ID).map((guideline) => guideline.text);
124
148
  return { description: base.description, snippet: base.snippet, guidelines: [preference, ...rewritten, ...script] };
125
149
  }
126
- if (flags.autoRead) return { description: base.description, snippet: base.snippet, guidelines: [preference, ...base.guidelines, ...script] };
127
- const guidelines = [preference, ...base.guidelines, ...script];
128
- const mapped = guidelines.map((guideline) => guideline.startsWith("`read`: call again after an edit") ? "`read`: call again after an edit when you need anchors you lack." : guideline);
129
- return { description: base.description, snippet: base.snippet, guidelines: mapped };
150
+ const guidelines = applicable.map((guideline) =>
151
+ guideline.id === READ_REREAD_ID && !flags.autoRead ? NEUTRAL_READ_REREAD : guideline.text,
152
+ );
153
+ return { description: base.description, snippet: base.snippet, guidelines: [preference, ...guidelines, ...script] };
130
154
  }
131
155
 
132
- export function withInsertPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
133
- const guidelines = [...base.guidelines];
156
+ export function withInsertPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
157
+ const guidelines = [preferenceGuideline(flags), ...base.guidelines.map((guideline) => guideline.text)];
134
158
  return finalizePrompts(base.description, base.snippet, guidelines, flags);
135
159
  }
136
160
 
137
- export function withReplaceMatchPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
138
- const guidelines = [...base.guidelines];
161
+ export function withReplaceMatchPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
162
+ const guidelines = [preferenceGuideline(flags), ...base.guidelines.map((guideline) => guideline.text)];
139
163
  return finalizePrompts(base.description, base.snippet, guidelines, flags);
140
164
  }
141
165
 
@@ -156,35 +180,34 @@ function joinOps(ops: string[], options?: { backtick?: boolean; separator?: "/"
156
180
  return formatted.length === 2 ? `${head} or ${last}` : `${head}, or ${last}`;
157
181
  }
158
182
 
159
- export function withGrepPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
160
- if (!flags.codemode && flags.copyMoveEnabled) return base;
161
- const guidelines = flags.codemode ? [...base.guidelines, RESULT_CONTRACT_GUIDELINE] : base.guidelines;
162
- if (flags.copyMoveEnabled) return { ...base, guidelines };
163
- return { ...base, guidelines, description: base.description.replaceAll("replace, insert, copy, or move", joinOps(gatedEditOps(["replace", "insert", "copy", "move"], flags))) };
183
+ export function withGrepPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
184
+ const guidelines = [preferenceGuideline(flags), ...base.guidelines.map((guideline) => guideline.text)];
185
+ if (flags.codemode) guidelines.push(RESULT_CONTRACT_GUIDELINE);
186
+ if (flags.copyMoveEnabled) return { description: base.description, snippet: base.snippet, guidelines };
187
+ const description = base.description.replaceAll(GREP_OPS_PHRASE, joinOps(gatedEditOps(["replace", "insert", "copy", "move"], flags)));
188
+ return { description, snippet: base.snippet, guidelines };
164
189
  }
165
190
 
166
- export function withUndoPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
191
+ export function withUndoPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
167
192
  const ops = gatedEditOps(["replace", "replace_match", "insert", "copy", "move"], flags);
168
- let description = base.description;
169
- let snippet = base.snippet;
170
193
  const script = flags.codemode ? [RESULT_CONTRACT_GUIDELINE, SCRIPT_UNDO_GUIDELINE] : [];
171
- let guidelines = [...base.guidelines, ...script];
172
- if (!flags.autoRead) {
173
- guidelines = guidelines.map((guideline) => guideline.includes("bad diff") ? "`undo_last_change`: only the last `replace`/`replace_match`/`insert`/`copy`/`move` per file is undoable; a `write` clears it, so undo right after a bad edit — review what you're restoring." : guideline);
174
- }
194
+ let description = base.description;
195
+ let guidelines = [
196
+ preferenceGuideline(flags),
197
+ ...base.guidelines
198
+ .filter((guideline) => flags.copyMoveEnabled || guideline.id !== UNDO_CROSS_FILE_MOVE_ID)
199
+ .map((guideline) => (guideline.id === UNDO_WRITE_CLEARS_ID && !flags.autoRead ? NEUTRAL_UNDO_WRITE_CLEARS : guideline.text)),
200
+ ...script,
201
+ ];
175
202
  if (ops.length !== 5) {
176
- description = description.replaceAll("replace, replace_match, insert, copy, or move", joinOps(ops));
177
- snippet = snippet.replaceAll("`replace`, `replace_match`, `insert`, `copy`, or `move`", joinOps(ops, { backtick: true }));
178
- guidelines = guidelines.map((guideline) => guideline.replaceAll("`replace`/`replace_match`/`insert`/`copy`/`move`", joinOps(ops, { backtick: true, separator: "/" })));
179
- }
180
- if (!flags.copyMoveEnabled) {
181
- guidelines = guidelines.filter((guideline) => !guideline.includes("cross-file `move`"));
203
+ description = description.replaceAll(UNDO_OPS_PHRASE, joinOps(ops));
204
+ guidelines = guidelines.map((guideline) => guideline.replaceAll(UNDO_GUIDELINE_OPS_PHRASE, joinOps(ops, { backtick: true, separator: "/" })));
182
205
  }
183
- return { description, snippet, guidelines };
206
+ return { description, snippet: base.snippet, guidelines };
184
207
  }
185
208
 
186
- export function withTransferPrompts(base: { description: string; snippet: string; guidelines: string[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
187
- const guidelines = flags.codemode ? [...base.guidelines, SCRIPT_TRANSFER_GUIDELINE] : [...base.guidelines];
209
+ export function withTransferPrompts(base: { description: string; snippet: string; guidelines: Guideline[] }, flags: EditToolFlags): { description: string; snippet: string; guidelines: string[] } {
210
+ const guidelines = [preferenceGuideline(flags), ...base.guidelines.map((guideline) => guideline.text), ...(flags.codemode ? [SCRIPT_TRANSFER_GUIDELINE] : [])];
188
211
  return finalizePrompts(base.description, base.snippet, guidelines, flags, { stringPayload: false });
189
212
  }
190
213
 
@@ -236,24 +259,43 @@ export interface PathRequirementInput {
236
259
  anchorTarget?: string;
237
260
  }
238
261
 
239
- export async function resolveEditTargetWithRequirement(input: PathRequirementInput): Promise<string> {
262
+ export interface PathOptionInput {
263
+ providedPath: unknown;
264
+ cwd: string;
265
+ targets: readonly string[];
266
+ expected: string;
267
+ requestLabel: string;
268
+ }
269
+
270
+ export async function assertPathOption(input: PathOptionInput): Promise<void> {
240
271
  const { requirePath } = await readConfig();
241
- if (!requirePath && input.providedPath !== undefined) {
242
- throw new Error("[E_BAD_SHAPE] Edit request contains unknown or unsupported fields: path. Path resolution is anchor-only; retry without `path`.");
272
+ if (input.providedPath === undefined || (requirePath && input.providedPath === "")) {
273
+ if (requirePath) {
274
+ throw new Error(`[E_BAD_SHAPE] ${input.requestLabel} requires a non-empty "path" string when require-path mode is on. Provide \`path\` matching ${input.expected}.`);
275
+ }
276
+ return;
243
277
  }
244
- if (requirePath && (typeof input.providedPath !== "string" || input.providedPath.length === 0)) {
245
- throw new Error('[E_BAD_SHAPE] Edit request requires a non-empty "path" string when require-path mode is on. Provide `path` matching the file the anchors were served for.');
278
+ if (typeof input.providedPath !== "string" || input.providedPath.length === 0) {
279
+ throw new Error(`[E_BAD_SHAPE] ${input.requestLabel} field "path" must be a non-empty string when provided.`);
246
280
  }
281
+ const { resolved } = await resolveInCwd(input.providedPath, input.cwd);
282
+ if (!input.targets.includes(resolved)) {
283
+ throw new Error(`[E_BAD_SHAPE] Provided "path" "${input.providedPath}" does not match ${input.expected}.`);
284
+ }
285
+ }
286
+
287
+ export async function resolveEditTargetWithRequirement(input: PathRequirementInput): Promise<string> {
247
288
  const anchorTarget = input.anchorTarget
248
289
  ?? (typeof input.anchor === "string"
249
290
  ? resolveEditTarget(input.anchor)
250
291
  : resolveEditTarget(input.removeFrom as string, input.removeTo));
251
- if (requirePath) {
252
- const { resolved } = await resolveInCwd(input.providedPath as string, input.cwd);
253
- if (resolved !== anchorTarget) {
254
- throw new Error(`[E_BAD_SHAPE] Provided "path" "${input.providedPath}" does not match anchor ownership "${anchorTarget}".`);
255
- }
256
- }
292
+ await assertPathOption({
293
+ providedPath: input.providedPath,
294
+ cwd: input.cwd,
295
+ targets: [anchorTarget],
296
+ expected: `anchor ownership "${anchorTarget}"`,
297
+ requestLabel: "Edit request",
298
+ });
257
299
  return anchorTarget;
258
300
  }
259
301