pi-rtk-optimizer 0.7.0 → 0.8.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.
@@ -6,7 +6,7 @@ import { cloneDefaultConfig, runTest } from "./test-helpers.ts";
6
6
 
7
7
  const TEST_AGENT_DIR = "/tmp/.pi/agent";
8
8
 
9
- mock.module("@mariozechner/pi-coding-agent", () => ({
9
+ mock.module("@earendil-works/pi-coding-agent", () => ({
10
10
  getAgentDir: () => TEST_AGENT_DIR,
11
11
  }));
12
12
 
@@ -75,6 +75,14 @@ function assertNoOutputEmoji(text: string): void {
75
75
  }
76
76
  }
77
77
 
78
+ function assertNoPartialHashlineAnchors(text: string): void {
79
+ for (const line of text.split(/\r?\n/)) {
80
+ if (/^\s*\d+\s*#[A-Za-z0-9_-]{2,32}:/.test(line)) {
81
+ assert.equal(line.endsWith("..."), false, `Anchor line was partially truncated: ${line}`);
82
+ }
83
+ }
84
+ }
85
+
78
86
  runTest("precision read with offset keeps exact output (no source/smart/hard truncation)", () => {
79
87
  const config = cloneDefaultConfig();
80
88
  setReadCompaction(config, true);
@@ -168,6 +176,203 @@ runTest("normal read compacts and adds banner when read compaction is enabled",
168
176
  assert.ok(compacted.includes("source:minimal"));
169
177
  });
170
178
 
179
+ runTest("line-anchor read output compacts without corrupting LINE#HASH anchors", () => {
180
+ const config = cloneDefaultConfig();
181
+ setReadCompaction(config, true);
182
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
183
+ config.outputCompaction.sourceCodeFiltering = "minimal";
184
+ config.outputCompaction.smartTruncate.enabled = true;
185
+ config.outputCompaction.smartTruncate.maxLines = 40;
186
+ config.outputCompaction.truncate.enabled = true;
187
+ config.outputCompaction.truncate.maxChars = 5000;
188
+
189
+ const content = Array.from({ length: 120 }, (_value, index) => {
190
+ const lineNumber = index + 1;
191
+ const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`;
192
+ return `${String(lineNumber).padStart(3, " ")}#ZP:${sourceLine}`;
193
+ }).join("\n");
194
+ const result = compactToolResult(
195
+ {
196
+ toolName: "read",
197
+ input: { path: "sample.ts" },
198
+ content: [{ type: "text", text: content }],
199
+ },
200
+ config,
201
+ );
202
+
203
+ assert.equal(result.changed, true);
204
+ assert.ok(result.techniques.includes("source:minimal"));
205
+
206
+ const compacted = firstTextBlock(result.content);
207
+ assert.ok(compacted.startsWith("[RTK compacted output:"));
208
+ assert.ok(compacted.includes("source:minimal"));
209
+ assert.match(compacted, /\n\s*2#ZP:const value2 = 2;/);
210
+ assert.equal(compacted.includes("#ZP:// comment"), false);
211
+ assertNoPartialHashlineAnchors(compacted);
212
+ });
213
+
214
+ runTest("colon-pipe anchor read output compacts without requiring hashline extension", () => {
215
+ const config = cloneDefaultConfig();
216
+ setReadCompaction(config, true);
217
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
218
+ config.outputCompaction.sourceCodeFiltering = "minimal";
219
+ config.outputCompaction.smartTruncate.enabled = true;
220
+ config.outputCompaction.smartTruncate.maxLines = 40;
221
+ config.outputCompaction.truncate.enabled = true;
222
+ config.outputCompaction.truncate.maxChars = 5000;
223
+
224
+ const content = [
225
+ "Read sample.ts: 120 lines",
226
+ "",
227
+ ...Array.from({ length: 120 }, (_value, index) => {
228
+ const lineNumber = index + 1;
229
+ const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`;
230
+ return `${lineNumber}:${(lineNumber % 256).toString(16).padStart(2, "0")}|${sourceLine}`;
231
+ }),
232
+ ].join("\n");
233
+ const result = compactToolResult(
234
+ {
235
+ toolName: "read",
236
+ input: { path: "sample.ts" },
237
+ content: [{ type: "text", text: content }],
238
+ },
239
+ config,
240
+ );
241
+
242
+ assert.equal(result.changed, true);
243
+ assert.ok(result.techniques.includes("source:minimal"));
244
+
245
+ const compacted = firstTextBlock(result.content);
246
+ assert.ok(compacted.includes("Read sample.ts: 120 lines"));
247
+ assert.match(compacted, /\n2:02\|const value2 = 2;/);
248
+ assert.equal(compacted.includes("|// comment"), false);
249
+ });
250
+
251
+ runTest("compact LINEHASH pipe anchors from oh-my-pi style reads", () => {
252
+ const config = cloneDefaultConfig();
253
+ setReadCompaction(config, true);
254
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
255
+ config.outputCompaction.sourceCodeFiltering = "minimal";
256
+ config.outputCompaction.smartTruncate.enabled = true;
257
+ config.outputCompaction.smartTruncate.maxLines = 40;
258
+ config.outputCompaction.truncate.enabled = true;
259
+ config.outputCompaction.truncate.maxChars = 5000;
260
+
261
+ const content = Array.from({ length: 120 }, (_value, index) => {
262
+ const lineNumber = index + 1;
263
+ const hash = lineNumber % 2 === 0 ? "sr" : "ab";
264
+ const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`;
265
+ return `${lineNumber}${hash}|${sourceLine}`;
266
+ }).join("\n");
267
+ const result = compactToolResult(
268
+ {
269
+ toolName: "read",
270
+ input: { path: "sample.ts" },
271
+ content: [{ type: "text", text: content }],
272
+ },
273
+ config,
274
+ );
275
+
276
+ assert.equal(result.changed, true);
277
+ assert.ok(result.techniques.includes("source:minimal"));
278
+
279
+ const compacted = firstTextBlock(result.content);
280
+ assert.match(compacted, /\n2sr\|const value2 = 2;/);
281
+ assert.equal(compacted.includes("|// comment"), false);
282
+ });
283
+
284
+ runTest("compact hashline-tools file wrapper while preserving non-anchor wrapper lines", () => {
285
+ const config = cloneDefaultConfig();
286
+ setReadCompaction(config, true);
287
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
288
+ config.outputCompaction.sourceCodeFiltering = "minimal";
289
+ config.outputCompaction.smartTruncate.enabled = true;
290
+ config.outputCompaction.smartTruncate.maxLines = 40;
291
+ config.outputCompaction.truncate.enabled = true;
292
+ config.outputCompaction.truncate.maxChars = 5000;
293
+
294
+ const content = [
295
+ "<file>",
296
+ ...Array.from({ length: 120 }, (_value, index) => {
297
+ const lineNumber = index + 1;
298
+ const sourceLine = lineNumber % 2 === 0 ? `const value${lineNumber} = ${lineNumber};` : `// comment ${lineNumber}`;
299
+ return `${lineNumber}#ZM:${sourceLine}`;
300
+ }),
301
+ "",
302
+ "(End of file - 120 total lines)",
303
+ "</file>",
304
+ ].join("\n");
305
+ const result = compactToolResult(
306
+ {
307
+ toolName: "read",
308
+ input: { path: "sample.ts" },
309
+ content: [{ type: "text", text: content }],
310
+ },
311
+ config,
312
+ );
313
+
314
+ assert.equal(result.changed, true);
315
+ assert.ok(result.techniques.includes("source:minimal"));
316
+
317
+ const compacted = firstTextBlock(result.content);
318
+ assert.ok(compacted.includes("<file>"));
319
+ assert.ok(compacted.includes("(End of file - 120 total lines)"));
320
+ assert.ok(compacted.includes("</file>"));
321
+ assert.match(compacted, /\n2#ZM:const value2 = 2;/);
322
+ assert.equal(compacted.includes("#ZM:// comment"), false);
323
+ });
324
+
325
+ runTest("anchor-safe read hard truncation preserves whole hashline anchors", () => {
326
+ const config = cloneDefaultConfig();
327
+ setReadCompaction(config, true);
328
+ config.outputCompaction.sourceCodeFilteringEnabled = false;
329
+ config.outputCompaction.smartTruncate.enabled = false;
330
+ config.outputCompaction.truncate.enabled = true;
331
+ config.outputCompaction.truncate.maxChars = 350;
332
+
333
+ const content = Array.from({ length: 120 }, (_value, index) => {
334
+ const lineNumber = index + 1;
335
+ return `${lineNumber}#ZP:const value${lineNumber} = "${"x".repeat(40)}";`;
336
+ }).join("\n");
337
+ const result = compactToolResult(
338
+ {
339
+ toolName: "read",
340
+ input: { path: "sample.ts" },
341
+ content: [{ type: "text", text: content }],
342
+ },
343
+ config,
344
+ );
345
+
346
+ assert.equal(result.changed, true);
347
+ assert.ok(result.techniques.includes("truncate"));
348
+
349
+ const compacted = firstTextBlock(result.content);
350
+ assert.ok(compacted.includes("anchor-safe truncate"));
351
+ assertNoPartialHashlineAnchors(compacted);
352
+ });
353
+
354
+ runTest("incidental single anchor-like line does not disable normal read compaction", () => {
355
+ const config = cloneDefaultConfig();
356
+ setReadCompaction(config, true);
357
+ config.outputCompaction.sourceCodeFilteringEnabled = true;
358
+ config.outputCompaction.sourceCodeFiltering = "minimal";
359
+ config.outputCompaction.smartTruncate.enabled = true;
360
+ config.outputCompaction.smartTruncate.maxLines = 40;
361
+
362
+ const content = [`1#ZP:not an anchored read`, buildReadContent(120)].join("\n");
363
+ const result = compactToolResult(
364
+ {
365
+ toolName: "read",
366
+ input: { path: "sample.ts" },
367
+ content: [{ type: "text", text: content }],
368
+ },
369
+ config,
370
+ );
371
+
372
+ assert.equal(result.changed, true);
373
+ assert.ok(result.techniques.includes("source:minimal") || result.techniques.includes("smart-truncate"));
374
+ });
375
+
171
376
  runTest("short read output stays exact below threshold", () => {
172
377
  const config = cloneDefaultConfig();
173
378
  setReadCompaction(config, true);
@@ -435,7 +640,7 @@ runTest("rtk grep-style output sanitizes emoji file markers", () => {
435
640
  runTest("rtk git diff verbose summary sanitizes file markers", () => {
436
641
  const compacted = compactBashOutput(
437
642
  "rtk git diff -- agent/extensions/pi-mcp-adapter/package.json",
438
- "agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@mariozechner/pi-coding-agent\": \"^0.58.1\",\n",
643
+ "agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@earendil-works/pi-coding-agent\": \"^0.58.1\",\n",
439
644
  );
440
645
 
441
646
  assert.ok(compacted.includes("--- Changes ---"));
@@ -446,7 +651,7 @@ runTest("rtk git diff verbose summary sanitizes file markers", () => {
446
651
  runTest("git diff compaction skips already-compacted RTK-shaped output", () => {
447
652
  const compacted = compactBashOutput(
448
653
  "git diff -- agent/extensions/pi-mcp-adapter/package.json",
449
- "agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@mariozechner/pi-coding-agent\": \"^0.58.1\",\n",
654
+ "agent/extensions/pi-mcp-adapter/package.json | 2 +-\n\n--- Changes ---\n\n📄 agent/extensions/pi-mcp-adapter/package.json\n @@ -38,7 +38,7 @@\n - \"@earendil-works/pi-coding-agent\": \"^0.58.1\",\n",
450
655
  );
451
656
 
452
657
  assert.ok(compacted.includes("--- Changes ---"));
@@ -1,4 +1,4 @@
1
- import { getAgentDir } from "@mariozechner/pi-coding-agent";
1
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, resolve, sep } from "node:path";
4
4
  import {
@@ -48,6 +48,24 @@ export interface ToolResultCompactionOutcome {
48
48
  metadata?: ToolResultCompactionMetadata;
49
49
  }
50
50
 
51
+ interface AnchoredReadLine {
52
+ lineNumber: number;
53
+ content: string;
54
+ originalLine: string;
55
+ }
56
+
57
+ interface AnchorSafeReadLine {
58
+ text: string;
59
+ content: string;
60
+ }
61
+
62
+ interface AnchorSafeReadParts {
63
+ prefixLines: string[];
64
+ anchoredLines: AnchoredReadLine[];
65
+ suffixLines: string[];
66
+ trailingNewline: boolean;
67
+ }
68
+
51
69
  const LOSSY_TECHNIQUE_PREFIXES = [
52
70
  "build",
53
71
  "test",
@@ -61,6 +79,15 @@ const LOSSY_TECHNIQUE_PREFIXES = [
61
79
 
62
80
  const READ_EXACT_OUTPUT_LINE_THRESHOLD = 80;
63
81
  const READ_COMPACTION_BANNER_PREFIX = "[RTK compacted output:";
82
+ const ANCHORED_READ_LINE_MIN_MATCHES = 2;
83
+ const ANCHORED_READ_LINE_MIN_RATIO = 0.5;
84
+ const ANCHORED_READ_LINE_SAMPLE_LIMIT = 200;
85
+ const ANCHORED_READ_LINE_PATTERNS = [
86
+ /^\s*(?:>>>|>>|[>+\-*]+)?\s*(\d+)\s*#\s*[A-Za-z0-9_-]{2,32}:(.*)$/,
87
+ /^\s*(?:>>>|>>|[>+\-*]+)?\s*(\d+)\s*:\s*[A-Za-z0-9_-]{1,32}\|(.*)$/,
88
+ /^\s*(?:>>>|>>|[>+\-*]+)?\s*(\d+)[a-z]{2}\|(.*)$/,
89
+ ] as const;
90
+ const ANCHORED_READ_INFORMATIONAL_LINE_PATTERN = /^\s*(?:$|<\/?file>|\.{3}|\[[^\]]+\]|Read\s+.+:\s+\d+\s+lines\b)/;
64
91
  const USER_SKILL_ROOTS = [join(getAgentDir(), "skills"), join(homedir(), ".agents", "skills")];
65
92
 
66
93
  function normalizePathForComparison(path: string): string {
@@ -135,6 +162,87 @@ function hasExplicitReadRange(input: Record<string, unknown>): boolean {
135
162
  return input.offset !== undefined || input.limit !== undefined;
136
163
  }
137
164
 
165
+ function splitReadLines(text: string): { lines: string[]; trailingNewline: boolean } {
166
+ if (!text) {
167
+ return { lines: [], trailingNewline: false };
168
+ }
169
+
170
+ const trailingNewline = text.endsWith("\n");
171
+ const lines = text.split(/\r?\n/);
172
+ if (trailingNewline) {
173
+ lines.pop();
174
+ }
175
+
176
+ return { lines, trailingNewline };
177
+ }
178
+
179
+ function joinReadLines(lines: string[], trailingNewline: boolean): string {
180
+ const joined = lines.join("\n");
181
+ return trailingNewline && joined ? `${joined}\n` : joined;
182
+ }
183
+
184
+ function parseAnchoredReadLine(line: string): AnchoredReadLine | undefined {
185
+ for (const pattern of ANCHORED_READ_LINE_PATTERNS) {
186
+ const match = line.match(pattern);
187
+ if (!match) {
188
+ continue;
189
+ }
190
+
191
+ const lineNumber = Number.parseInt(match[1] ?? "", 10);
192
+ if (!Number.isSafeInteger(lineNumber) || lineNumber <= 0) {
193
+ continue;
194
+ }
195
+
196
+ const content = match[2] ?? "";
197
+ return {
198
+ lineNumber,
199
+ content,
200
+ originalLine: line,
201
+ };
202
+ }
203
+
204
+ return undefined;
205
+ }
206
+
207
+ function parseAnchoredReadLineNumber(line: string): number | undefined {
208
+ return parseAnchoredReadLine(line)?.lineNumber;
209
+ }
210
+
211
+ function looksLikeAnchoredLineOutput(text: string, parseLineNumber: (line: string) => number | undefined): boolean {
212
+ let matchCount = 0;
213
+ let relevantLineCount = 0;
214
+ let previousMatchedLineNumber: number | undefined;
215
+ let hasIncreasingAnchors = false;
216
+
217
+ for (const line of splitReadLines(text).lines.slice(0, ANCHORED_READ_LINE_SAMPLE_LIMIT)) {
218
+ if (!ANCHORED_READ_INFORMATIONAL_LINE_PATTERN.test(line)) {
219
+ relevantLineCount += 1;
220
+ }
221
+
222
+ const lineNumber = parseLineNumber(line);
223
+ if (lineNumber === undefined) {
224
+ continue;
225
+ }
226
+
227
+ matchCount += 1;
228
+ if (previousMatchedLineNumber !== undefined && lineNumber > previousMatchedLineNumber) {
229
+ hasIncreasingAnchors = true;
230
+ }
231
+ previousMatchedLineNumber = lineNumber;
232
+ }
233
+
234
+ if (matchCount < ANCHORED_READ_LINE_MIN_MATCHES || !hasIncreasingAnchors) {
235
+ return false;
236
+ }
237
+
238
+ const ratioBase = Math.max(relevantLineCount, matchCount);
239
+ return matchCount / ratioBase >= ANCHORED_READ_LINE_MIN_RATIO;
240
+ }
241
+
242
+ function looksLikeAnchoredReadOutput(text: string): boolean {
243
+ return looksLikeAnchoredLineOutput(text, parseAnchoredReadLineNumber);
244
+ }
245
+
138
246
  function shouldPreserveExactReadOutput(
139
247
  text: string,
140
248
  input: Record<string, unknown>,
@@ -165,6 +273,179 @@ function shouldApplyReadSourceFiltering(text: string, config: RtkIntegrationConf
165
273
  );
166
274
  }
167
275
 
276
+ function extractAnchoredReadParts(text: string): AnchorSafeReadParts | undefined {
277
+ if (!looksLikeAnchoredReadOutput(text)) {
278
+ return undefined;
279
+ }
280
+
281
+ const { lines, trailingNewline } = splitReadLines(text);
282
+ const parsedLines = lines.map((line) => parseAnchoredReadLine(line));
283
+ const firstAnchorIndex = parsedLines.findIndex((line) => line !== undefined);
284
+ if (firstAnchorIndex === -1) {
285
+ return undefined;
286
+ }
287
+
288
+ let lastAnchorIndex = firstAnchorIndex;
289
+ for (let index = parsedLines.length - 1; index >= firstAnchorIndex; index -= 1) {
290
+ if (parsedLines[index] !== undefined) {
291
+ lastAnchorIndex = index;
292
+ break;
293
+ }
294
+ }
295
+
296
+ const anchoredLines: AnchoredReadLine[] = [];
297
+ for (let index = firstAnchorIndex; index <= lastAnchorIndex; index += 1) {
298
+ const anchoredLine = parsedLines[index];
299
+ if (!anchoredLine) {
300
+ return undefined;
301
+ }
302
+ anchoredLines.push(anchoredLine);
303
+ }
304
+
305
+ return {
306
+ prefixLines: lines.slice(0, firstAnchorIndex),
307
+ anchoredLines,
308
+ suffixLines: lines.slice(lastAnchorIndex + 1),
309
+ trailingNewline,
310
+ };
311
+ }
312
+
313
+ function toAnchorSafeReadLines(anchoredLines: AnchoredReadLine[]): AnchorSafeReadLine[] {
314
+ return anchoredLines.map((line) => ({
315
+ text: line.originalLine,
316
+ content: line.content,
317
+ }));
318
+ }
319
+
320
+ function renderAnchorSafeReadBody(lines: AnchorSafeReadLine[]): string {
321
+ return lines.map((line) => line.text).join("\n");
322
+ }
323
+
324
+ function renderAnchorSafeReadText(parts: AnchorSafeReadParts, lines: AnchorSafeReadLine[]): string {
325
+ return joinReadLines(
326
+ [...parts.prefixLines, ...lines.map((line) => line.text), ...parts.suffixLines],
327
+ parts.trailingNewline,
328
+ );
329
+ }
330
+
331
+ function remapTransformedContentToAnchorSafeLines(
332
+ sourceLines: AnchorSafeReadLine[],
333
+ transformedContent: string,
334
+ ): AnchorSafeReadLine[] {
335
+ const transformedLines = splitReadLines(transformedContent).lines;
336
+ const remappedLines: AnchorSafeReadLine[] = [];
337
+ let searchStartIndex = 0;
338
+
339
+ for (const transformedLine of transformedLines) {
340
+ let matchedIndex = -1;
341
+ for (let index = searchStartIndex; index < sourceLines.length; index += 1) {
342
+ if (sourceLines[index]?.content === transformedLine) {
343
+ matchedIndex = index;
344
+ break;
345
+ }
346
+ }
347
+
348
+ if (matchedIndex === -1) {
349
+ remappedLines.push({
350
+ text: transformedLine,
351
+ content: transformedLine,
352
+ });
353
+ continue;
354
+ }
355
+
356
+ remappedLines.push(sourceLines[matchedIndex]!);
357
+ searchStartIndex = matchedIndex + 1;
358
+ }
359
+
360
+ return remappedLines;
361
+ }
362
+
363
+ function truncateAnchorSafeReadLines(lines: AnchorSafeReadLine[], maxChars: number): AnchorSafeReadLine[] {
364
+ if (renderAnchorSafeReadBody(lines).length <= maxChars) {
365
+ return lines;
366
+ }
367
+
368
+ const marker = "[RTK anchor-safe truncate: remaining anchored read lines omitted to preserve complete anchors]";
369
+ const truncatedLines: AnchorSafeReadLine[] = [];
370
+ let charCount = 0;
371
+
372
+ for (let index = 0; index < lines.length; index += 1) {
373
+ const line = lines[index]!;
374
+ const separatorLength = truncatedLines.length > 0 ? 1 : 0;
375
+ const nextCharCount = charCount + separatorLength + line.text.length;
376
+ const remainingAfter = lines.length - index - 1;
377
+ const markerLength = remainingAfter > 0 ? (nextCharCount > 0 ? 1 : 0) + marker.length : 0;
378
+
379
+ if (nextCharCount + markerLength > maxChars) {
380
+ const markerLine = { text: marker, content: marker };
381
+ return truncatedLines.length > 0 ? [...truncatedLines, markerLine] : [markerLine];
382
+ }
383
+
384
+ truncatedLines.push(line);
385
+ charCount = nextCharCount;
386
+ }
387
+
388
+ return truncatedLines;
389
+ }
390
+
391
+ function compactAnchoredReadText(
392
+ text: string,
393
+ filePath: string,
394
+ config: RtkIntegrationConfig,
395
+ ): { text: string; techniques: string[] } {
396
+ const parts = extractAnchoredReadParts(text);
397
+ if (!parts) {
398
+ return { text, techniques: [] };
399
+ }
400
+
401
+ let lines = toAnchorSafeReadLines(parts.anchoredLines);
402
+ const techniques: string[] = [];
403
+ const compaction = config.outputCompaction;
404
+ const language = detectLanguage(filePath);
405
+
406
+ if (
407
+ compaction.sourceCodeFilteringEnabled &&
408
+ compaction.sourceCodeFiltering !== "none" &&
409
+ shouldApplyReadSourceFiltering(text, config)
410
+ ) {
411
+ const currentSource = lines.map((line) => line.content).join("\n");
412
+ const filtered = normalizeTechniqueResult(
413
+ filterSourceCode(currentSource, language, compaction.sourceCodeFiltering),
414
+ currentSource,
415
+ );
416
+ const filteredLines = remapTransformedContentToAnchorSafeLines(lines, filtered);
417
+ if (renderAnchorSafeReadBody(filteredLines) !== renderAnchorSafeReadBody(lines)) {
418
+ lines = filteredLines;
419
+ techniques.push(`source:${compaction.sourceCodeFiltering}`);
420
+ }
421
+ }
422
+
423
+ if (compaction.smartTruncate.enabled && lines.length > compaction.smartTruncate.maxLines) {
424
+ const currentSource = lines.map((line) => line.content).join("\n");
425
+ const compacted = smartTruncate(currentSource, compaction.smartTruncate.maxLines, language);
426
+ const compactedLines = remapTransformedContentToAnchorSafeLines(lines, compacted);
427
+ if (renderAnchorSafeReadBody(compactedLines) !== renderAnchorSafeReadBody(lines)) {
428
+ lines = compactedLines;
429
+ techniques.push("smart-truncate");
430
+ }
431
+ }
432
+
433
+ if (compaction.truncate.enabled && renderAnchorSafeReadText(parts, lines).length > compaction.truncate.maxChars) {
434
+ const nonBodyOverhead = renderAnchorSafeReadText(parts, []).length;
435
+ const bodyMaxChars = Math.max(1, compaction.truncate.maxChars - nonBodyOverhead);
436
+ const truncatedLines = truncateAnchorSafeReadLines(lines, bodyMaxChars);
437
+ if (renderAnchorSafeReadBody(truncatedLines) !== renderAnchorSafeReadBody(lines)) {
438
+ lines = truncatedLines;
439
+ techniques.push("truncate");
440
+ }
441
+ }
442
+
443
+ return {
444
+ text: renderAnchorSafeReadText(parts, lines),
445
+ techniques,
446
+ };
447
+ }
448
+
168
449
  function formatReadCompactionBanner(techniques: string[]): string {
169
450
  return `${READ_COMPACTION_BANNER_PREFIX} ${techniques.join(", ")}]`;
170
451
  }
@@ -190,6 +471,10 @@ function hasLossyCompaction(techniques: string[]): boolean {
190
471
  );
191
472
  }
192
473
 
474
+ function normalizeTechniqueResult(result: string | null, currentText: string): string {
475
+ return result === null ? currentText : result;
476
+ }
477
+
193
478
  function compactBashText(
194
479
  text: string,
195
480
  command: string | undefined,
@@ -207,45 +492,45 @@ function compactBashText(
207
492
  }
208
493
  }
209
494
 
210
- const withoutRtkHookWarnings = stripRtkHookWarnings(nextText, command);
211
- if (withoutRtkHookWarnings !== null && withoutRtkHookWarnings !== nextText) {
495
+ const withoutRtkHookWarnings = normalizeTechniqueResult(stripRtkHookWarnings(nextText, command), nextText);
496
+ if (withoutRtkHookWarnings !== nextText) {
212
497
  nextText = withoutRtkHookWarnings;
213
498
  techniques.push("rtk-hook-warning");
214
499
  }
215
500
 
216
- const withoutRtkEmoji = sanitizeRtkEmojiOutput(nextText, command);
217
- if (withoutRtkEmoji !== null && withoutRtkEmoji !== nextText) {
501
+ const withoutRtkEmoji = normalizeTechniqueResult(sanitizeRtkEmojiOutput(nextText, command), nextText);
502
+ if (withoutRtkEmoji !== nextText) {
218
503
  nextText = withoutRtkEmoji;
219
504
  techniques.push("rtk-emoji");
220
505
  }
221
506
 
222
507
  if (compaction.filterBuildOutput) {
223
- const compacted = filterBuildOutput(nextText, command);
224
- if (compacted !== null && compacted !== nextText) {
508
+ const compacted = normalizeTechniqueResult(filterBuildOutput(nextText, command), nextText);
509
+ if (compacted !== nextText) {
225
510
  nextText = compacted;
226
511
  techniques.push("build");
227
512
  }
228
513
  }
229
514
 
230
515
  if (compaction.aggregateTestOutput) {
231
- const compacted = aggregateTestOutput(nextText, command);
232
- if (compacted !== null && compacted !== nextText) {
516
+ const compacted = normalizeTechniqueResult(aggregateTestOutput(nextText, command), nextText);
517
+ if (compacted !== nextText) {
233
518
  nextText = compacted;
234
519
  techniques.push("test");
235
520
  }
236
521
  }
237
522
 
238
523
  if (compaction.compactGitOutput) {
239
- const compacted = compactGitOutput(nextText, command);
240
- if (compacted !== null && compacted !== nextText) {
524
+ const compacted = normalizeTechniqueResult(compactGitOutput(nextText, command), nextText);
525
+ if (compacted !== nextText) {
241
526
  nextText = compacted;
242
527
  techniques.push("git");
243
528
  }
244
529
  }
245
530
 
246
531
  if (compaction.aggregateLinterOutput) {
247
- const compacted = aggregateLinterOutput(nextText, command);
248
- if (compacted !== null && compacted !== nextText) {
532
+ const compacted = normalizeTechniqueResult(aggregateLinterOutput(nextText, command), nextText);
533
+ if (compacted !== nextText) {
249
534
  nextText = compacted;
250
535
  techniques.push("linter");
251
536
  }
@@ -281,6 +566,18 @@ function compactReadText(
281
566
  }
282
567
  }
283
568
 
569
+ if (looksLikeAnchoredReadOutput(nextText)) {
570
+ const anchored = compactAnchoredReadText(nextText, filePath, config);
571
+ nextText = anchored.text;
572
+ techniques.push(...anchored.techniques);
573
+
574
+ if (techniques.length > 0 && !nextText.startsWith(READ_COMPACTION_BANNER_PREFIX)) {
575
+ nextText = `${formatReadCompactionBanner(techniques)}\n${nextText}`;
576
+ }
577
+
578
+ return { text: nextText, techniques };
579
+ }
580
+
284
581
  const language = detectLanguage(filePath);
285
582
  // Only apply lossy source filtering when a downstream line/char safeguard would otherwise trigger.
286
583
  if (
@@ -288,7 +585,10 @@ function compactReadText(
288
585
  compaction.sourceCodeFiltering !== "none" &&
289
586
  shouldApplyReadSourceFiltering(text, config)
290
587
  ) {
291
- const filtered = filterSourceCode(nextText, language, compaction.sourceCodeFiltering);
588
+ const filtered = normalizeTechniqueResult(
589
+ filterSourceCode(nextText, language, compaction.sourceCodeFiltering),
590
+ nextText,
591
+ );
292
592
  if (filtered !== nextText) {
293
593
  nextText = filtered;
294
594
  techniques.push(`source:${compaction.sourceCodeFiltering}`);
@@ -332,8 +632,8 @@ function compactGrepText(text: string, config: RtkIntegrationConfig): { text: st
332
632
  }
333
633
 
334
634
  if (compaction.groupSearchOutput) {
335
- const grouped = groupSearchResults(nextText);
336
- if (grouped !== null && grouped !== nextText) {
635
+ const grouped = normalizeTechniqueResult(groupSearchResults(nextText), nextText);
636
+ if (grouped !== nextText) {
337
637
  nextText = grouped;
338
638
  techniques.push("search");
339
639
  }