token-goat 2.9.18 → 2.9.20

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 (55) hide show
  1. package/README.md +5 -1
  2. package/dist/token-goat-chunk-2CLWWRFR.mjs +26 -0
  3. package/dist/{token-goat-chunk-LTYWLKAG.mjs → token-goat-chunk-2Q3N5KUQ.mjs} +1283 -1012
  4. package/dist/{token-goat-chunk-6LGVCCMY.mjs → token-goat-chunk-3OGZOFLS.mjs} +26 -16
  5. package/dist/{token-goat-chunk-QML7ITHJ.mjs → token-goat-chunk-3VTJC7KU.mjs} +693 -438
  6. package/dist/{token-goat-chunk-QIDLJGXZ.mjs → token-goat-chunk-6LJNM4ZZ.mjs} +29 -5
  7. package/dist/{token-goat-chunk-CA6V36KW.mjs → token-goat-chunk-74ADIZZX.mjs} +68 -50
  8. package/dist/{token-goat-chunk-52IMXJ2L.mjs → token-goat-chunk-ASTBLE2R.mjs} +4 -4
  9. package/dist/token-goat-chunk-AU2K2QHR.mjs +20 -0
  10. package/dist/token-goat-chunk-BQIC3AVL.mjs +292 -0
  11. package/dist/token-goat-chunk-CBXLE54Q.mjs +30 -0
  12. package/dist/{token-goat-chunk-LMFO66YD.mjs → token-goat-chunk-DA43OE7L.mjs} +1 -1
  13. package/dist/{token-goat-chunk-ARNRFR6M.mjs → token-goat-chunk-DNAF26L4.mjs} +1 -1
  14. package/dist/{token-goat-chunk-BFEWOMA3.mjs → token-goat-chunk-DPHJU3P7.mjs} +1186 -4573
  15. package/dist/{token-goat-chunk-OWSJJY5R.mjs → token-goat-chunk-DW4VKU7D.mjs} +5 -5
  16. package/dist/token-goat-chunk-DYUBZZNP.mjs +198 -0
  17. package/dist/{token-goat-chunk-W3E65QOW.mjs → token-goat-chunk-ERGHDBC6.mjs} +310 -47
  18. package/dist/token-goat-chunk-EUZKG7ZO.mjs +304 -0
  19. package/dist/{token-goat-chunk-5MMJAJF3.mjs → token-goat-chunk-FWYZVS3E.mjs} +324 -31
  20. package/dist/token-goat-chunk-G6SYQQO7.mjs +41 -0
  21. package/dist/{token-goat-chunk-YQZE5NS6.mjs → token-goat-chunk-GIPMOGOI.mjs} +6 -6
  22. package/dist/{token-goat-chunk-BWCRC23L.mjs → token-goat-chunk-H4VC5TR6.mjs} +29 -20
  23. package/dist/{token-goat-chunk-V3ZWBEHM.mjs → token-goat-chunk-I5PQRHFE.mjs} +103 -24
  24. package/dist/token-goat-chunk-ITWYDOKP.mjs +341 -0
  25. package/dist/{token-goat-chunk-I67CPTDD.mjs → token-goat-chunk-K3ZNBN4U.mjs} +19 -14
  26. package/dist/{token-goat-chunk-HPOFR6QX.mjs → token-goat-chunk-K5XPIUVB.mjs} +222 -61
  27. package/dist/token-goat-chunk-KT75DZK4.mjs +1989 -0
  28. package/dist/{token-goat-chunk-H3FONFBO.mjs → token-goat-chunk-MNJWG2ZO.mjs} +3 -3
  29. package/dist/{token-goat-chunk-GRI6HWX3.mjs → token-goat-chunk-MUJV7BR3.mjs} +1 -1
  30. package/dist/{token-goat-chunk-ERTXEKB6.mjs → token-goat-chunk-NMTKNYGF.mjs} +2 -0
  31. package/dist/{token-goat-chunk-LA3O4JGD.mjs → token-goat-chunk-ODF7H7WS.mjs} +3 -3
  32. package/dist/{token-goat-chunk-K4DG6EEF.mjs → token-goat-chunk-ONMU7BON.mjs} +7 -8
  33. package/dist/{token-goat-chunk-4EXFN2AW.mjs → token-goat-chunk-OZRSREKR.mjs} +2 -2
  34. package/dist/{token-goat-chunk-EPXNOKIV.mjs → token-goat-chunk-P4XBHZXU.mjs} +2 -2
  35. package/dist/{token-goat-chunk-CTOYJ232.mjs → token-goat-chunk-PAVWB4L4.mjs} +173 -1840
  36. package/dist/{token-goat-chunk-UENEL6KX.mjs → token-goat-chunk-PTW22MRC.mjs} +2 -2
  37. package/dist/token-goat-chunk-SL454EN5.mjs +1816 -0
  38. package/dist/token-goat-chunk-SP5QR2HN.mjs +1379 -0
  39. package/dist/{token-goat-chunk-7VLWNQDQ.mjs → token-goat-chunk-T46UKRFM.mjs} +1 -1
  40. package/dist/{token-goat-chunk-L7VCX33I.mjs → token-goat-chunk-T63VHOL3.mjs} +40 -10
  41. package/dist/token-goat-chunk-VSJGPEBB.mjs +248 -0
  42. package/dist/{token-goat-chunk-HIZXQVNU.mjs → token-goat-chunk-VYXGHGUY.mjs} +4 -4
  43. package/dist/token-goat-chunk-W6QYCWAT.mjs +128 -0
  44. package/dist/{token-goat-chunk-WFX2V3OF.mjs → token-goat-chunk-Z4ALQADF.mjs} +579 -422
  45. package/dist/token-goat-chunk-ZHFM4DHJ.mjs +324 -0
  46. package/dist/token-goat-hook.mjs +18 -12
  47. package/dist/token-goat.core.mjs +29 -20
  48. package/docs/C4_RUNTIME_ARCHITECTURE.md +1 -1
  49. package/docs/cli.md +15 -13
  50. package/docs/install.md +1 -1
  51. package/docs/security.md +2 -2
  52. package/package.json +3 -2
  53. package/dist/token-goat-chunk-2KTZ6J7T.mjs +0 -173
  54. package/dist/token-goat-chunk-34OUJ2IE.mjs +0 -35
  55. package/dist/token-goat-chunk-RHHFEXKF.mjs +0 -93
@@ -1,13 +1,16 @@
1
1
  import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
+ MAX_CAPTURE_BYTES,
4
5
  canRunWrappedShell,
5
6
  clipToDeliveryCap,
6
7
  deliveredOutputBytes
7
- } from "./token-goat-chunk-RHHFEXKF.mjs";
8
+ } from "./token-goat-chunk-ITWYDOKP.mjs";
8
9
  import {
9
10
  DEFAULT_RECONCILE_BUDGET_MS,
10
11
  HOOK_EVENTS,
12
+ MCP_TOOL_PATTERN,
13
+ RECALL_COMMAND,
11
14
  buildCommandManifest,
12
15
  checkSymbolBodySize,
13
16
  diagnoseSqlFailure,
@@ -17,32 +20,37 @@ import {
17
20
  readStdinJson,
18
21
  reconcileProject,
19
22
  storeWebOutput
20
- } from "./token-goat-chunk-V3ZWBEHM.mjs";
23
+ } from "./token-goat-chunk-I5PQRHFE.mjs";
24
+ import {
25
+ projectTranscriptsDir
26
+ } from "./token-goat-chunk-BQIC3AVL.mjs";
27
+ import {
28
+ BUDGET_ESCALATION_MARKER,
29
+ MANIFEST_PREAMBLE,
30
+ MANIFEST_RECOVERY_PREAMBLE,
31
+ buildManifest,
32
+ manifestPrintedPaths,
33
+ summaryBudgetDirective
34
+ } from "./token-goat-chunk-VSJGPEBB.mjs";
21
35
  import {
22
36
  BODY_FIRST_TOOL_RESPONSE_KEYS,
23
- GENERIC_SERVED_OUTPUT_KEY,
24
37
  OUTLINE_MAX_REPLACEMENT_RATIO,
25
38
  OUTLINE_MIN_HEADINGS,
26
39
  OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
27
- WEB_FETCH_KEY_SEP,
28
- accumulateResidentLines,
29
40
  appendDirtyPath,
30
41
  applyHintTracking,
31
42
  buildDeltaCapsule,
32
43
  buildProjectMap,
33
44
  classifyBashHint,
34
45
  classifyEditHint,
35
- clearCurlDownload,
36
46
  compactPathFor,
37
- computeAdaptiveBudget,
38
47
  contextOutput,
39
48
  denyOutput,
40
49
  emitRewrite,
41
50
  emitRewriteIfChanged,
42
51
  enqueueDirtyPathSafe,
43
52
  ensureWorkerAlive,
44
- estimateTokens,
45
- extractCompactFromMarker,
53
+ escapeHintName,
46
54
  extractToolResponseField,
47
55
  extractToolResultText,
48
56
  fenceUntrusted,
@@ -53,42 +61,19 @@ import {
53
61
  foldingEnabled,
54
62
  formatProjectMap,
55
63
  formatShrinkSummary,
56
- getBashOutputId,
57
- getContextPressure,
58
- getCurlDownloadPath,
59
64
  getCwd,
60
- getFileLineRanges,
61
65
  getFilePath,
62
- getFileServedOutputs,
63
- getGlobMatchCount,
64
- getGrepMatchCount,
65
- getOutstandingAgentSpawns,
66
- getSessionBashOutputs,
67
- getSessionBashReruns,
68
- getSessionFiles,
69
- getSessionWebFetches,
70
66
  getToolInput,
71
67
  getToolName,
72
68
  getTranscriptPath,
73
- hasSeenImage,
74
- hasSessionOutput,
75
69
  imageQualifiesForShrink,
76
- incrementSkillHit,
77
70
  indexServedBody,
78
- installedSkillPath,
79
71
  isMcpErrorResponse,
80
72
  isStructuralRewriteAccepted,
81
73
  isTextualContentBlock,
82
- lastTabContextMatches,
83
- lineMayCarryResidentSignal,
84
- listSiblingSessionStates,
85
- loadSessionCache,
86
- loadSessionState,
74
+ logSuppressedDetection,
87
75
  makeDedupHintHandlers,
88
76
  markCompactStale,
89
- markCompacted,
90
- markFileTruncated,
91
- markHintShown,
92
77
  matchesAllowPattern,
93
78
  matchesDenyPattern,
94
79
  mcpContentBlocks,
@@ -101,8 +86,53 @@ import {
101
86
  planSourceSkeleton,
102
87
  preToolPathDeclined,
103
88
  probeImageMeta,
104
- readCopilotHooksOwners,
105
- readTranscriptTail,
89
+ recordKnownRootThrottled,
90
+ registerHook,
91
+ resolveLineRegions,
92
+ runHook,
93
+ scanAndRecord,
94
+ serializeOutput,
95
+ servedRunNotice,
96
+ sessionStateKey,
97
+ shrinkImage,
98
+ visionTokens,
99
+ visionTokensSaved,
100
+ vscodePathAllowed,
101
+ withPreservedBlocks
102
+ } from "./token-goat-chunk-DPHJU3P7.mjs";
103
+ import {
104
+ compressOutput,
105
+ detectFromCommand,
106
+ filterByName,
107
+ isRewriteWorthwhile,
108
+ looksLikePytestOutput,
109
+ pytestFilter,
110
+ resolveMinNetSavingsBytes
111
+ } from "./token-goat-chunk-PAVWB4L4.mjs";
112
+ import {
113
+ readCopilotHooksOwners
114
+ } from "./token-goat-chunk-SP5QR2HN.mjs";
115
+ import {
116
+ GENERIC_SERVED_OUTPUT_KEY,
117
+ clearCurlDownload,
118
+ extractCompactFromMarker,
119
+ getBashOutputId,
120
+ getCurlDownloadPath,
121
+ getFileLineRanges,
122
+ getFileServedOutputs,
123
+ getGlobMatchCount,
124
+ getGrepMatchCount,
125
+ getOutstandingAgentSpawns,
126
+ getSessionBashOutputs,
127
+ hasSeenImage,
128
+ hasSessionOutput,
129
+ incrementSkillHit,
130
+ installedSkillPath,
131
+ lastTabContextMatches,
132
+ loadSessionState,
133
+ markCompacted,
134
+ markFileTruncated,
135
+ markHintShown,
106
136
  recordBashOutput,
107
137
  recordBashRerun,
108
138
  recordCliRead,
@@ -113,45 +143,38 @@ import {
113
143
  recordFileServedOutput,
114
144
  recordGlobQuery,
115
145
  recordGrepQuery,
116
- recordKnownRootThrottled,
117
146
  recordOutstandingAgentSpawn,
118
147
  recordScheduledPrompt,
119
148
  recordSeenImage,
120
149
  recordSymbolRead,
121
150
  recordWebFetch,
122
- registerHook,
123
151
  removeOutstandingAgentSpawn,
124
- repeatedSkillBodyHint,
125
- runHook,
126
152
  saveSessionState,
127
- scanAndRecord,
128
- serializeOutput,
129
- servedRunNotice,
130
153
  sessionOutputBodyBytes,
131
154
  sessionSidecarPath,
132
- sessionStateKey,
133
155
  setLastTabContext,
134
156
  setTranscriptPath,
135
- shrinkImage,
136
157
  storeOutput,
137
- summarizeResidentContext,
138
158
  takePendingLargeFileHint,
139
- taskListPruneHint,
140
- visionTokens,
141
- visionTokensSaved,
142
- vscodePathAllowed,
143
159
  wasCliReadThisSession,
144
160
  wasFileReadThisSession,
145
- wasHintShown,
146
- withPreservedBlocks
147
- } from "./token-goat-chunk-BFEWOMA3.mjs";
161
+ wasHintShown
162
+ } from "./token-goat-chunk-KT75DZK4.mjs";
163
+ import {
164
+ accumulateResidentLines,
165
+ claudeConfigDir,
166
+ estimateTokens,
167
+ lineMayCarryResidentSignal,
168
+ readTranscriptTail,
169
+ repeatedSkillBodyHint,
170
+ summarizeResidentContext,
171
+ taskListPruneHint
172
+ } from "./token-goat-chunk-ZHFM4DHJ.mjs";
148
173
  import {
149
174
  BASH_OUTPUT_SUBDIR,
175
+ bashOutputIdSync,
150
176
  commandHash,
151
- compressOutput,
152
177
  dedupeConsecutive,
153
- detectFromCommand,
154
- filterByName,
155
178
  getBashOutput,
156
179
  getMonitoringRecallHint,
157
180
  hasBareBackgroundOrNewline,
@@ -159,44 +182,47 @@ import {
159
182
  indexRecallEntry,
160
183
  isBashEntryStale,
161
184
  isBuildCommand,
162
- isRewriteWorthwhile,
185
+ isRedirectAmpersand,
163
186
  isScopedGitStatusOrDiffStatCommand,
164
187
  isTestRunnerCommand,
165
- resolveMinNetSavingsBytes,
188
+ pathStem,
166
189
  shlexSplit,
167
190
  splitOwnTrailingNotices,
168
191
  storeBashOutput,
169
192
  summarizeOutputDelta
170
- } from "./token-goat-chunk-CTOYJ232.mjs";
193
+ } from "./token-goat-chunk-SL454EN5.mjs";
171
194
  import {
172
195
  recordStat,
173
196
  redactSecrets,
174
197
  savedTokensFromBytes,
175
198
  storeBlob,
176
199
  stripAnsiEscapes
177
- } from "./token-goat-chunk-HPOFR6QX.mjs";
200
+ } from "./token-goat-chunk-K5XPIUVB.mjs";
178
201
  import {
179
202
  extractMarkdownHeadings,
180
203
  formatHeadingTreeParts
181
- } from "./token-goat-chunk-W3E65QOW.mjs";
204
+ } from "./token-goat-chunk-ERGHDBC6.mjs";
182
205
  import {
183
- countSymbols
184
- } from "./token-goat-chunk-I67CPTDD.mjs";
206
+ countSymbols,
207
+ getFileEntry,
208
+ querySymbols
209
+ } from "./token-goat-chunk-K3ZNBN4U.mjs";
185
210
  import {
186
211
  detectLanguage,
187
212
  getDb,
188
213
  languageHasFlag
189
- } from "./token-goat-chunk-L7VCX33I.mjs";
214
+ } from "./token-goat-chunk-T63VHOL3.mjs";
190
215
  import {
191
216
  detectHarness,
192
217
  envBool,
193
218
  envInt,
194
219
  findProject,
220
+ fingerprintFile,
195
221
  getHarnessName,
196
222
  isUnderSystemTemp,
197
223
  loadConfig,
198
224
  shortFingerprint
199
- } from "./token-goat-chunk-CA6V36KW.mjs";
225
+ } from "./token-goat-chunk-74ADIZZX.mjs";
200
226
  import {
201
227
  ENV_KEYS,
202
228
  IDENTICAL_READ_MIN_BODY_BYTES,
@@ -216,8 +242,9 @@ import {
216
242
  isInsideRoot,
217
243
  runGit,
218
244
  toKB
219
- } from "./token-goat-chunk-QIDLJGXZ.mjs";
245
+ } from "./token-goat-chunk-6LJNM4ZZ.mjs";
220
246
  import {
247
+ TOOL_RESULTS_ID_CHARS,
221
248
  UNTRUSTED_TOOL_TAG,
222
249
  displaySafePath,
223
250
  displaySafeText,
@@ -226,7 +253,7 @@ import {
226
253
  normalizePath,
227
254
  resolveIndexPath,
228
255
  toDisplayPath
229
- } from "./token-goat-chunk-ERTXEKB6.mjs";
256
+ } from "./token-goat-chunk-NMTKNYGF.mjs";
230
257
  import {
231
258
  init_define_import_meta_env
232
259
  } from "./token-goat-chunk-A37V4PBF.mjs";
@@ -276,6 +303,52 @@ function stripUnsafeSuggestions(text) {
276
303
  }
277
304
  }
278
305
 
306
+ // src/pending_context.ts
307
+ init_define_import_meta_env();
308
+ import { readFileSync, rmSync, writeFileSync } from "node:fs";
309
+ import { dirname } from "node:path";
310
+ var PENDING_SUFFIX = ".pending-context.txt";
311
+ var MAX_PENDING_CONTEXT_BYTES = 4096;
312
+ function queuePendingContext(stateKey, text) {
313
+ const trimmed = text.trim();
314
+ if (trimmed === "") return;
315
+ const target = sessionSidecarPath(stateKey, PENDING_SUFFIX);
316
+ if (target === null) return;
317
+ try {
318
+ const existing = readPending(target);
319
+ const merged = existing === null ? trimmed : `${existing}
320
+ ${trimmed}`;
321
+ const capped = merged.length <= MAX_PENDING_CONTEXT_BYTES ? merged : merged.slice(merged.length - MAX_PENDING_CONTEXT_BYTES);
322
+ ensureDirSync(dirname(target));
323
+ writeFileSync(target, capped, "utf8");
324
+ } catch {
325
+ }
326
+ }
327
+ function peekPendingContext(stateKey) {
328
+ const target = sessionSidecarPath(stateKey, PENDING_SUFFIX);
329
+ return target === null ? null : readPending(target);
330
+ }
331
+ function commitPendingContext(stateKey, delivered) {
332
+ if (delivered === null || delivered === "") return;
333
+ const target = sessionSidecarPath(stateKey, PENDING_SUFFIX);
334
+ if (target === null) return;
335
+ const queued = readPending(target);
336
+ if (queued === null) return;
337
+ if (!delivered.includes(queued) && !delivered.includes(stripUnsafeSuggestions(queued))) return;
338
+ try {
339
+ rmSync(target, { force: true });
340
+ } catch {
341
+ }
342
+ }
343
+ function readPending(target) {
344
+ try {
345
+ const raw = readFileSync(target, "utf8").trim();
346
+ return raw === "" ? null : raw;
347
+ } catch {
348
+ return null;
349
+ }
350
+ }
351
+
279
352
  // src/vscode_duplicate.ts
280
353
  init_define_import_meta_env();
281
354
  import * as fs from "node:fs";
@@ -404,7 +477,7 @@ function grepIntInput(toolInput, key) {
404
477
  function grepSignature(toolInput) {
405
478
  const pattern = toolInput["pattern"];
406
479
  if (typeof pattern !== "string" || pattern === "") return null;
407
- const path5 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
480
+ const path6 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
408
481
  const outputMode = typeof toolInput["output_mode"] === "string" ? toolInput["output_mode"] : "files_with_matches";
409
482
  const glob = typeof toolInput["glob"] === "string" ? toolInput["glob"] : "";
410
483
  const type = typeof toolInput["type"] === "string" ? toolInput["type"] : "";
@@ -419,7 +492,7 @@ function grepSignature(toolInput) {
419
492
  const offset = grepIntInput(toolInput, "offset");
420
493
  return JSON.stringify([
421
494
  pattern,
422
- path5,
495
+ path6,
423
496
  outputMode,
424
497
  glob,
425
498
  type,
@@ -563,8 +636,8 @@ function isBroadCatchAllGlob(pattern, pathArg) {
563
636
  function globSignature(toolInput) {
564
637
  const pattern = toolInput["pattern"];
565
638
  if (typeof pattern !== "string" || pattern === "") return null;
566
- const path5 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
567
- return JSON.stringify([pattern, path5]);
639
+ const path6 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
640
+ return JSON.stringify([pattern, path6]);
568
641
  }
569
642
  var { post: postGlobHandler, pre: preGlobDedupHandler } = makeDedupHintHandlers({
570
643
  toolName: "Glob",
@@ -631,7 +704,9 @@ function postEditHandlerInner(event) {
631
704
  editedSize = statSync2(normalized).size;
632
705
  } catch {
633
706
  }
634
- if (meetsSavingsFloor(editedSize)) {
707
+ if (!meetsSavingsFloor(editedSize)) {
708
+ logSuppressedDetection("edit_reread_suggest", event.sessionId, normalized);
709
+ } else {
635
710
  const escapedPath = displaySafePath(normalized).replace(/`/g, "\\`").replace(/"/g, '\\"');
636
711
  return contextOutput(
637
712
  displaySafePath(editedBasename) + ' was edited. Use `token-goat section "' + escapedPath + '::HeadingName"` to re-read a specific section rather than the full file.'
@@ -650,7 +725,7 @@ registerHook("post_tool_use", postEditHandler, { toolName: "NotebookEdit" });
650
725
 
651
726
  // src/hooks_write.ts
652
727
  init_define_import_meta_env();
653
- import { readFileSync, statSync as statSync3 } from "node:fs";
728
+ import { readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
654
729
  var MAX_LINES_FOR_DIFF = 4e3;
655
730
  var MAX_OLD_FILE_BYTES_FOR_DIFF = 4 * 1024 * 1024;
656
731
  function splitLines(text) {
@@ -691,7 +766,7 @@ function preWriteRewriteHandler(event) {
691
766
  if (stat.size > MAX_OLD_FILE_BYTES_FOR_DIFF) return passOutput();
692
767
  let oldContent;
693
768
  try {
694
- oldContent = readFileSync(filePath, "utf-8");
769
+ oldContent = readFileSync2(filePath, "utf-8");
695
770
  } catch {
696
771
  return passOutput();
697
772
  }
@@ -717,210 +792,48 @@ registerHook("pre_tool_use", preWriteRewriteHandler, { toolName: "Write" });
717
792
 
718
793
  // src/hooks_compact.ts
719
794
  init_define_import_meta_env();
720
- import { spawnSync } from "node:child_process";
721
795
  import * as path3 from "node:path";
722
- var MEM_EPOCH_TIMEOUT_MS = 800;
723
- var MAX_ROWS = 40;
724
- function appendCappedSection(lines, header, rows, cap) {
725
- if (rows.length === 0) return;
726
- lines.push("");
727
- lines.push(header);
728
- for (const row of rows.slice(0, cap)) lines.push(row);
729
- if (rows.length > cap) lines.push(`- ...and ${rows.length - cap} more`);
730
- }
731
- function renderReadRow(entry) {
732
- const kb = Math.max(1, toKB(entry.sizeBytes));
733
- const plural = entry.readCount === 1 ? "read" : "reads";
734
- const edited = entry.wasEdited ? ", edited" : "";
735
- return `- ${displaySafePath(entry.path)} (${kb}kb, ${entry.readCount} ${plural}${edited})`;
736
- }
737
- function renderSymbolReadRow(entry) {
738
- return `- ${displaySafePath(entry.path)} (symbols: ${(entry.symbols_read ?? []).map(displaySafeText).join(", ")})`;
739
- }
740
- function mergeManifestFiles(parent, siblingFiles) {
741
- const byPath = /* @__PURE__ */ new Map();
742
- for (const f of parent) byPath.set(foldPath(f.path), f);
743
- for (const f of siblingFiles) {
744
- const key = foldPath(f.path);
745
- const prev = byPath.get(key);
746
- if (prev === void 0) {
747
- byPath.set(key, f);
748
- continue;
749
- }
750
- const mergedSymbols = [.../* @__PURE__ */ new Set([...prev.symbols_read ?? [], ...f.symbols_read ?? []])];
751
- byPath.set(key, {
752
- path: prev.path,
753
- readCount: Math.max(prev.readCount, f.readCount),
754
- lastReadAt: Math.max(prev.lastReadAt, f.lastReadAt),
755
- wasEdited: prev.wasEdited || f.wasEdited,
756
- sizeBytes: f.lastReadAt >= prev.lastReadAt ? f.sizeBytes : prev.sizeBytes,
757
- ...prev.wasTruncated || f.wasTruncated ? { wasTruncated: true } : {},
758
- ...mergedSymbols.length > 0 ? { symbols_read: mergedSymbols } : {}
759
- });
760
- }
761
- return Array.from(byPath.values());
762
- }
763
- function buildManifest(sessionId, cwd, transcriptPath) {
764
- const ownFiles = [...getSessionFiles().values()];
765
- const siblingFiles = sessionId !== void 0 ? listSiblingSessionStates(sessionId).flatMap((s) => s.files) : [];
766
- const files = siblingFiles.length > 0 ? mergeManifestFiles(ownFiles, siblingFiles) : ownFiles;
767
- const editedFiles = files.filter((f) => f.wasEdited);
768
- const readFiles = files.filter((f) => f.readCount > 0 && !f.wasEdited);
769
- const symbolOnlyFiles = files.filter((f) => f.readCount === 0 && !f.wasEdited && (f.symbols_read?.length ?? 0) > 0);
770
- const webFetches = [...getSessionWebFetches().entries()];
771
- const lines = [];
772
- lines.push("## Session context");
773
- lines.push(`Files read: ${readFiles.length}`);
774
- lines.push(`Files edited: ${editedFiles.length}`);
775
- appendCappedSection(lines, "### Read files", readFiles.map(renderReadRow), MAX_ROWS);
776
- appendCappedSection(
777
- lines,
778
- "### Edited files",
779
- editedFiles.map((entry) => `- ${displaySafePath(entry.path)}`),
780
- MAX_ROWS
781
- );
782
- appendCappedSection(lines, "### Surgically read files (symbol/section reads, never read whole)", symbolOnlyFiles.map(renderSymbolReadRow), MAX_ROWS);
783
- appendCappedSection(
784
- lines,
785
- "### Web URLs fetched",
786
- webFetches.map(([key, cacheId]) => {
787
- const [url = key, prompt = ""] = key.split(WEB_FETCH_KEY_SEP);
788
- const promptSuffix = prompt ? `, prompt: ${neutralizeSpokenMarkers(JSON.stringify(prompt))}` : "";
789
- return `- ${displaySafeText(url)} (cacheId: ${cacheId}${promptSuffix})`;
790
- }),
791
- MAX_ROWS
792
- );
793
- lines.push(...buildSafeToDiscardSection(files));
794
- lines.push(...buildMemEpochSection());
795
- return capManifestChars(lines.join("\n"), sessionId, cwd, transcriptPath);
796
- }
797
- function gitDirtySignals(cwd) {
798
- const timeoutMs = loadConfig().hints.git_hint_max_ms;
799
- let hasPendingDiff = false;
800
- try {
801
- const diffResult = runGit(["diff", "--no-color", "--stat", "HEAD"], { cwd, timeoutMs });
802
- hasPendingDiff = diffResult.exitCode === 0 && diffResult.stdout.trim() !== "";
803
- } catch {
804
- }
805
- let hasUncommittedChanges = false;
806
- try {
807
- const statusResult = runGit(["status", "--porcelain"], { cwd, timeoutMs });
808
- hasUncommittedChanges = statusResult.exitCode === 0 && statusResult.stdout.trim() !== "";
809
- } catch {
810
- }
811
- return { hasPendingDiff, hasUncommittedChanges };
812
- }
813
- function adaptiveCharBonus(sessionId, cwd, transcriptPath) {
814
- if (!cwd) return 0;
815
- const cache = loadSessionCache(sessionId ?? "") ?? {};
816
- const ageSecs = cache.created_ts !== void 0 ? Math.max(0, Date.now() / 1e3 - cache.created_ts) : 0;
817
- const contextPressure = getContextPressure(cache, transcriptPath);
818
- const { hasPendingDiff, hasUncommittedChanges } = gitDirtySignals(cwd);
819
- if (!hasPendingDiff && !hasUncommittedChanges) return 0;
820
- const baseline = computeAdaptiveBudget(cache, ageSecs, { contextPressure });
821
- const withGitSignal = computeAdaptiveBudget(cache, ageSecs, { hasPendingDiff, hasUncommittedChanges, contextPressure });
822
- const deltaTokens = Math.max(0, withGitSignal - baseline);
823
- return deltaTokens * 3;
824
- }
825
- function capManifestChars(manifest, sessionId, cwd, transcriptPath) {
826
- const cap = loadConfig().compact_assist.max_manifest_chars;
827
- if (cap <= 0) return manifest;
828
- if (manifest.length <= cap) return manifest;
829
- const effectiveCap = cap + adaptiveCharBonus(sessionId, cwd, transcriptPath);
830
- if (manifest.length <= effectiveCap) return manifest;
831
- const omitted = manifest.length - effectiveCap;
832
- return manifest.slice(0, effectiveCap) + `
833
- ...(manifest truncated at ${effectiveCap} chars; ${omitted} chars omitted)`;
834
- }
835
- function buildSafeToDiscardSection(files) {
836
- const rerunHashes = getSessionBashReruns();
837
- const bashOutputs = getSessionBashOutputs();
838
- const rerunHashSet = new Set(rerunHashes);
839
- const rerunRows = [];
840
- for (const hash of rerunHashes) {
841
- const id = bashOutputs.find(([h]) => h === hash)?.[1];
842
- if (id === void 0) continue;
843
- const entry = getBashOutput(id);
844
- if (entry === null) continue;
845
- const flatCommand = neutralizeSpokenMarkers(entry.command.replace(/[\t\r\n]+/g, " "));
846
- rerunRows.push("- `" + flatCommand + "` \u2014 an older run of this exact command was superseded; recall the surviving copy with `bash-output " + id + " --full`");
847
- }
848
- const supersededReadRows = [];
849
- for (const f of files) {
850
- if (f.readCount > 1 || f.wasEdited) {
851
- const reason = f.wasEdited ? "edited after being read" : "re-read " + f.readCount + "x";
852
- supersededReadRows.push("- " + displaySafePath(f.path) + " (" + reason + " \u2014 only the latest content already in context is current)");
853
- }
854
- }
855
- const cachedOutputRows = [];
856
- for (const [hash, id] of bashOutputs) {
857
- if (rerunHashSet.has(hash)) continue;
858
- const entry = getBashOutput(id);
859
- if (entry === null) continue;
860
- const flatCommand = neutralizeSpokenMarkers(entry.command.replace(/[\t\r\n]+/g, " "));
861
- cachedOutputRows.push("- `" + flatCommand + "` \u2014 recallable via `bash-output " + id + " --full`");
862
- }
863
- const total = rerunRows.length + supersededReadRows.length + cachedOutputRows.length;
864
- if (total === 0) return [];
865
- const lines = [];
866
- lines.push("");
867
- lines.push("### SAFE_TO_DISCARD (" + total + " items \u2014 provably inert; each is recallable, not gone)");
868
- appendCappedSection(lines, "Superseded reruns (" + rerunRows.length + "):", rerunRows, MAX_ROWS);
869
- appendCappedSection(lines, "Superseded file reads (" + supersededReadRows.length + "):", supersededReadRows, MAX_ROWS);
870
- appendCappedSection(lines, "Other cached bash outputs (" + cachedOutputRows.length + "):", cachedOutputRows, MAX_ROWS);
871
- return lines;
796
+
797
+ // src/harness_channels.ts
798
+ init_define_import_meta_env();
799
+ var PROMPT_SUBMIT_CONTEXT_DROPPED = /* @__PURE__ */ new Set([]);
800
+ function dropsPromptSubmitContext() {
801
+ return PROMPT_SUBMIT_CONTEXT_DROPPED.has(getHarnessName());
872
802
  }
873
- function buildMemEpochSection() {
874
- let result;
875
- try {
876
- result = spawnSync("mem", ["epoch"], {
877
- encoding: "utf-8",
878
- timeout: MEM_EPOCH_TIMEOUT_MS,
879
- windowsHide: true
880
- });
881
- } catch {
882
- return [];
883
- }
884
- if (result.error !== void 0 || result.status !== 0) return [];
885
- const stdout = typeof result.stdout === "string" ? result.stdout.trim() : "";
886
- if (!/^\d+$/.test(stdout)) return [];
887
- return [
888
- "",
889
- "### mem epoch",
890
- `mem epoch: ${stdout} (no live TGMEM block is tracked in this session -- only the epoch counter is folded in; see buildMemEpochSection's doc comment)`
891
- ];
892
- }
893
- var MANIFEST_PREAMBLE = "When summarizing this session, keep the file paths and symbol names below exactly as written -- they are the handles the next turn needs to resume work. Do not paraphrase them into prose.";
894
- var BUDGET_ESCALATION_MARKER = "TG-BUDGET-ESCALATION:";
895
- function summaryBudgetDirective(budgetChars) {
896
- if (budgetChars <= 0) return "";
897
- return ` Aim for at most ${budgetChars} characters. Prefer a shorter summary that keeps every path, symbol, command and decision over a longer one that reproduces tool output verbatim: cite the recall id (\`token-goat bash-output <id> --full\`) rather than quoting the output again. If this session genuinely cannot be summarized within that target without losing state the next turn needs, exceed it and open the summary with a single line reading \`${BUDGET_ESCALATION_MARKER} <one-sentence reason>\`.`;
803
+ var PRE_COMPACT_CONTEXT_DROPPED = /* @__PURE__ */ new Set(["copilot_cli", "codex"]);
804
+ function dropsPreCompactContext() {
805
+ return PRE_COMPACT_CONTEXT_DROPPED.has(getHarnessName());
898
806
  }
807
+
808
+ // src/hooks_compact.ts
899
809
  function preCompactHandler(event) {
900
- const out = loadConfig().compact_assist.enabled ? contextOutput(`${MANIFEST_PREAMBLE}${summaryBudgetDirective(loadConfig().compact_assist.summary_budget_chars)}
810
+ const cfg = loadConfig().compact_assist;
811
+ let out = passOutput();
812
+ if (cfg.enabled) {
813
+ const manifest = buildManifest(event.sessionId, getCwd(event), getTranscriptPath(event));
814
+ if (!dropsPreCompactContext()) {
815
+ out = contextOutput(`${MANIFEST_PREAMBLE}${summaryBudgetDirective(cfg.summary_budget_chars)}
901
816
 
902
- ${buildManifest(event.sessionId, getCwd(event), getTranscriptPath(event))}`) : passOutput();
817
+ ${manifest}`);
818
+ } else if (event.sessionId) {
819
+ queuePendingContext(sessionStateKey(event), `${MANIFEST_RECOVERY_PREAMBLE}
820
+
821
+ ${manifest.slice(0, MAX_PENDING_CONTEXT_BYTES - MANIFEST_RECOVERY_PREAMBLE.length - 2)}`);
822
+ }
823
+ }
903
824
  markCompacted();
904
825
  return out;
905
826
  }
906
827
  registerHook("pre_compact", preCompactHandler);
907
828
  var MANIFEST_SURVIVAL_SAMPLE = 64;
908
- function manifestPathSample(sessionId) {
909
- const ownFiles = [...getSessionFiles().values()];
910
- const siblingFiles = sessionId !== void 0 ? listSiblingSessionStates(sessionId).flatMap((s) => s.files) : [];
911
- const files = siblingFiles.length > 0 ? mergeManifestFiles(ownFiles, siblingFiles) : ownFiles;
912
- const seen = /* @__PURE__ */ new Set();
913
- for (const entry of files) {
914
- if (entry.path) seen.add(entry.path);
915
- if (seen.size >= MANIFEST_SURVIVAL_SAMPLE) break;
916
- }
917
- return [...seen];
829
+ function manifestPathSample(event) {
830
+ return manifestPrintedPaths(event.sessionId, MANIFEST_SURVIVAL_SAMPLE, getCwd(event), getTranscriptPath(event));
918
831
  }
919
832
  function postCompactHandler(event) {
920
833
  const raw = event.raw["compact_summary"];
921
834
  const summary = typeof raw === "string" ? raw : "";
922
835
  const bytes = Buffer.byteLength(summary, "utf-8");
923
- const sample = manifestPathSample(event.sessionId);
836
+ const sample = typeof raw === "string" ? manifestPathSample(event) : [];
924
837
  const cwd = normalizePath(getCwd(event) ?? process.cwd());
925
838
  const spell = (p) => foldPath(p.replaceAll("\\", "/"));
926
839
  const haystack = spell(summary);
@@ -953,7 +866,7 @@ init_define_import_meta_env();
953
866
  var TRACKED_SKILL = "token-goat";
954
867
  var MAX_COMMANDS_SHOWN = 8;
955
868
  async function currentCommandNames() {
956
- const { buildProgram } = await import("./token-goat-chunk-BWCRC23L.mjs");
869
+ const { buildProgram } = await import("./token-goat-chunk-H4VC5TR6.mjs");
957
870
  return flattenCommandNames(buildCommandManifest(buildProgram()));
958
871
  }
959
872
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -1005,55 +918,11 @@ async function checkSkillVersionDrift(sessionId) {
1005
918
  }
1006
919
  }
1007
920
 
1008
- // src/pending_context.ts
1009
- init_define_import_meta_env();
1010
- import { readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync } from "node:fs";
1011
- import { dirname } from "node:path";
1012
- var PENDING_SUFFIX = ".pending-context.txt";
1013
- var MAX_PENDING_CONTEXT_BYTES = 4096;
1014
- function queuePendingContext(sessionId, text) {
1015
- const trimmed = text.trim();
1016
- if (trimmed === "") return;
1017
- const target = sessionSidecarPath(sessionId, PENDING_SUFFIX);
1018
- if (target === null) return;
1019
- try {
1020
- const existing = readPending(target);
1021
- const merged = existing === null ? trimmed : `${existing}
1022
- ${trimmed}`;
1023
- const capped = merged.length <= MAX_PENDING_CONTEXT_BYTES ? merged : merged.slice(merged.length - MAX_PENDING_CONTEXT_BYTES);
1024
- ensureDirSync(dirname(target));
1025
- writeFileSync(target, capped, "utf8");
1026
- } catch {
1027
- }
1028
- }
1029
- function drainPendingContext(sessionId) {
1030
- const target = sessionSidecarPath(sessionId, PENDING_SUFFIX);
1031
- if (target === null) return null;
1032
- const text = readPending(target);
1033
- try {
1034
- rmSync2(target, { force: true });
1035
- } catch {
1036
- }
1037
- return text;
1038
- }
1039
- function readPending(target) {
1040
- try {
1041
- const raw = readFileSync2(target, "utf8").trim();
1042
- return raw === "" ? null : raw;
1043
- } catch {
1044
- return null;
1045
- }
1046
- }
1047
-
1048
921
  // src/hooks_session.ts
1049
- var PROMPT_SUBMIT_CONTEXT_DROPPED = /* @__PURE__ */ new Set([]);
1050
- function dropsPromptSubmitContext() {
1051
- return PROMPT_SUBMIT_CONTEXT_DROPPED.has(getHarnessName());
1052
- }
1053
922
  function pendingContextHandler(event) {
1054
923
  try {
1055
- if (!event.sessionId || !dropsPromptSubmitContext()) return passOutput();
1056
- const pending = drainPendingContext(event.sessionId);
924
+ if (!event.sessionId || !dropsPromptSubmitContext() && !dropsPreCompactContext()) return passOutput();
925
+ const pending = peekPendingContext(sessionStateKey(event));
1057
926
  return pending === null ? passOutput() : contextOutput(pending);
1058
927
  } catch {
1059
928
  return passOutput();
@@ -1161,7 +1030,7 @@ async function userPromptSubmitHandler(event) {
1161
1030
  }
1162
1031
  const text = lines.join("\n");
1163
1032
  if (dropsPromptSubmitContext()) {
1164
- queuePendingContext(event.sessionId, text);
1033
+ queuePendingContext(sessionStateKey(event), text);
1165
1034
  return passOutput();
1166
1035
  }
1167
1036
  return contextOutput(text);
@@ -1259,13 +1128,25 @@ function reconcileNote(cwd, indexed) {
1259
1128
  if (drifted > 0) clauses.push(`${countNoun(drifted, "file")} that changed outside this session${breakdown}`);
1260
1129
  if (upgraded > 0) clauses.push(`${countNoun(upgraded, "file")} unchanged on disk but indexed by an older version of token-goat`);
1261
1130
  return `token-goat: reindexing ${clauses.join(" and ")}${truncated}. Symbol lookups may be briefly stale.`;
1131
+ } catch (e) {
1132
+ recordStat("reconcile_note_failed", 0, 0, void 0, extractErrorMessage(e));
1133
+ return null;
1134
+ }
1135
+ }
1136
+ async function postCompactRecovery(event) {
1137
+ if (event.raw["source"] !== "compact" || event.sessionId === void 0) return null;
1138
+ if (!loadConfig().compact_assist.enabled) return null;
1139
+ try {
1140
+ const { buildResumePacket } = await import("./token-goat-chunk-2CLWWRFR.mjs");
1141
+ return await buildResumePacket(event.sessionId);
1262
1142
  } catch {
1263
1143
  return null;
1264
1144
  }
1265
1145
  }
1266
- function sessionStartHandler(event) {
1146
+ async function sessionStartHandler(event) {
1147
+ const recovery = await postCompactRecovery(event);
1267
1148
  try {
1268
- if (!loadConfig().hints.session_start_reminder) return passOutput();
1149
+ if (!loadConfig().hints.session_start_reminder) return recovery === null ? passOutput() : contextOutput(recovery);
1269
1150
  const cwd = getCwd(event);
1270
1151
  const indexed = isIndexedProject(cwd);
1271
1152
  let context = buildReminder(indexed);
@@ -1286,9 +1167,12 @@ ${capsule}`;
1286
1167
  }
1287
1168
  } catch {
1288
1169
  }
1170
+ if (recovery !== null) context += `
1171
+
1172
+ ${recovery}`;
1289
1173
  return contextOutput(context);
1290
1174
  } catch {
1291
- return passOutput();
1175
+ return recovery === null ? passOutput() : contextOutput(recovery);
1292
1176
  }
1293
1177
  }
1294
1178
  registerHook("session_start", sessionStartHandler);
@@ -1870,11 +1754,11 @@ function mapInner(r, f) {
1870
1754
  function mapOuter(r, f) {
1871
1755
  return r.matched ? f(r) : r;
1872
1756
  }
1873
- function ab(pa, pb, join3) {
1874
- return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join3(ma.value, vb, data, i, j)));
1757
+ function ab(pa, pb, join4) {
1758
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapInner(pb(data, ma.position), (vb, j) => join4(ma.value, vb, data, i, j)));
1875
1759
  }
1876
- function abc(pa, pb, pc, join3) {
1877
- return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join3(ma.value, mb.value, vc, data, i, j))));
1760
+ function abc(pa, pb, pc, join4) {
1761
+ return (data, i) => mapOuter(pa(data, i), (ma) => mapOuter(pb(data, ma.position), (mb) => mapInner(pc(data, mb.position), (vc, j) => join4(ma.value, mb.value, vc, data, i, j))));
1878
1762
  }
1879
1763
  function ahead(p) {
1880
1764
  return (data, i) => mapOuter(p(data, i), (m1) => ({
@@ -5851,8 +5735,8 @@ function trimCharacterEnd(str, char) {
5851
5735
  function unicodeEscape(str) {
5852
5736
  return str.replace(/[\s\S]/g, (c) => "\\u" + c.charCodeAt().toString(16).padStart(4, "0"));
5853
5737
  }
5854
- function get(obj, path5) {
5855
- for (const key of path5) {
5738
+ function get(obj, path6) {
5739
+ for (const key of path6) {
5856
5740
  if (!obj) {
5857
5741
  return void 0;
5858
5742
  }
@@ -7053,8 +6937,8 @@ function withBrackets(str, brackets) {
7053
6937
  const rbr = typeof brackets[1] === "string" ? brackets[1] : "]";
7054
6938
  return lbr + str + rbr;
7055
6939
  }
7056
- function pathRewrite(path5, rewriter, baseUrl, metadata, elem) {
7057
- const modifiedPath = typeof rewriter === "function" ? rewriter(path5, metadata, elem) : path5;
6940
+ function pathRewrite(path6, rewriter, baseUrl, metadata, elem) {
6941
+ const modifiedPath = typeof rewriter === "function" ? rewriter(path6, metadata, elem) : path6;
7058
6942
  return modifiedPath[0] === "/" && baseUrl ? trimCharacterEnd(baseUrl, "/") + modifiedPath : modifiedPath;
7059
6943
  }
7060
6944
  function formatImage(elem, walk, builder, formatOptions) {
@@ -7378,9 +7262,9 @@ function handleDeprecatedOptions(options) {
7378
7262
  options.selectors.push(...tagDefinitions);
7379
7263
  options.selectors = mergeDuplicatesPreferLast(options.selectors, ((s) => s.selector));
7380
7264
  }
7381
- function set(obj, path5, value) {
7382
- const valueKey = path5.pop();
7383
- for (const key of path5) {
7265
+ function set(obj, path6, value) {
7266
+ const valueKey = path6.pop();
7267
+ for (const key of path6) {
7384
7268
  let nested = obj[key];
7385
7269
  if (!nested) {
7386
7270
  nested = {};
@@ -7670,20 +7554,149 @@ registerHook("post_tool_use", postSkillHandler, { toolName: "Skill" });
7670
7554
 
7671
7555
  // src/hooks_bash.ts
7672
7556
  init_define_import_meta_env();
7673
- import { statSync as statSync6, existsSync } from "node:fs";
7674
7557
 
7675
- // src/hooks_bash_commands.ts
7558
+ // src/bash_structural_index.ts
7559
+ init_define_import_meta_env();
7560
+
7561
+ // src/index_freshness.ts
7676
7562
  init_define_import_meta_env();
7677
- import { statSync as statSync5 } from "node:fs";
7563
+ function indexMatchesDisk(resolved) {
7564
+ const entry = getFileEntry(resolved);
7565
+ if (entry === null) return false;
7566
+ if (entry.sha === "") return true;
7567
+ const diskSha = fingerprintFile(resolved);
7568
+ return diskSha !== null && diskSha === entry.sha;
7569
+ }
7570
+
7571
+ // src/bash_structural_index.ts
7572
+ import { statSync as statSync4 } from "node:fs";
7573
+ var ALLOWED_BINARIES = /* @__PURE__ */ new Set(["rg", "grep", "egrep", "fgrep"]);
7574
+ var PY_DEF_RE = /^\^?(?:async\s+)?def\b(?:\s+([A-Za-z_]\w*))?$/;
7575
+ var PY_CLASS_RE = /^\^?class\b(?:\s+([A-Za-z_]\w*))?$/;
7576
+ var IMPORT_RE = /^\^import\b/;
7577
+ var SHELL_SAFE_UNQUOTED_RE = /^[A-Za-z0-9_.:/+,=@^-]+$/;
7578
+ var SHELL_UNSAFE_IN_EITHER_QUOTING_RE = /[$`"\\\r\n]/;
7579
+ function dualShellArg(s) {
7580
+ if (SHELL_SAFE_UNQUOTED_RE.test(s)) return s;
7581
+ if (SHELL_UNSAFE_IN_EITHER_QUOTING_RE.test(s)) return null;
7582
+ return `"${s}"`;
7583
+ }
7584
+ function detectStructuralIndexRewrite(rawCmd, cwd) {
7585
+ try {
7586
+ const detected = detectFromCommand(rawCmd, cwd);
7587
+ if (detected === null || detected.filter.name !== "grep") return null;
7588
+ const argv = detected.argv;
7589
+ if (argv.length !== 3) return null;
7590
+ const [bin, pattern, rawPath] = argv;
7591
+ if (bin === void 0 || pattern === void 0 || rawPath === void 0) return null;
7592
+ if (!ALLOWED_BINARIES.has(pathStem(bin).toLowerCase())) return null;
7593
+ if (/[*?[\]{}]/.test(rawPath)) return null;
7594
+ const resolved = resolveIndexPath(rawPath, cwd);
7595
+ let stat;
7596
+ try {
7597
+ stat = statSync4(resolved);
7598
+ } catch {
7599
+ return null;
7600
+ }
7601
+ if (!stat.isFile()) return null;
7602
+ if (!indexMatchesDisk(resolved)) return null;
7603
+ const pathArg = dualShellArg(rawPath);
7604
+ if (pathArg === null) return null;
7605
+ let target = null;
7606
+ if (DOC_EXT_RE.test(rawPath) && STRUCTURAL_DOC_PATTERN_RE.test(pattern)) {
7607
+ target = { command: `outline ${pathArg}`, kind: "headings" };
7608
+ } else {
7609
+ const lang = detectLanguage(resolved);
7610
+ const trimmedPattern = pattern.trim();
7611
+ if (lang === "python" && languageHasFlag(lang, "grepSource")) {
7612
+ const defMatch = PY_DEF_RE.exec(trimmedPattern);
7613
+ const classMatch = defMatch === null ? PY_CLASS_RE.exec(trimmedPattern) : null;
7614
+ const nameMatch = defMatch ?? classMatch;
7615
+ if (nameMatch !== null) {
7616
+ const name2 = nameMatch[1];
7617
+ if (name2) {
7618
+ const grepArg = dualShellArg("^" + name2);
7619
+ target = grepArg === null ? null : { command: `outline ${pathArg} --grep ${grepArg}`, kind: `symbols named starting with ${name2} (functions, classes or any other kind sharing that name)` };
7620
+ } else {
7621
+ target = { command: `outline ${pathArg}`, kind: "all symbols in the file (not just functions/classes)" };
7622
+ }
7623
+ } else if (IMPORT_RE.test(trimmedPattern)) {
7624
+ target = { command: `imports ${pathArg}`, kind: "imports" };
7625
+ }
7626
+ } else if (lang !== "unknown" && languageHasFlag(lang, "grepSource") && IMPORT_RE.test(trimmedPattern)) {
7627
+ target = { command: `imports ${pathArg}`, kind: "imports" };
7628
+ }
7629
+ }
7630
+ if (target === null) return null;
7631
+ const notice = `[token-goat: rewrote this rg/grep search to an indexed answer (${target.kind}) instead of running it -- re-run your original command yourself for a literal text search]`;
7632
+ const noticeArg = dualShellArg(notice);
7633
+ if (noticeArg === null) return null;
7634
+ return { command: `token-goat --notice ${noticeArg} ${target.command}`, kind: target.kind };
7635
+ } catch {
7636
+ return null;
7637
+ }
7638
+ }
7639
+
7640
+ // src/bash_range_savings.ts
7641
+ init_define_import_meta_env();
7642
+ import { statSync as statSync5, readFileSync as readFileSync3 } from "node:fs";
7643
+ var ALL_SYMBOLS_IN_FILE = -1;
7644
+ var REGION_HEADER_BYTES = 56;
7645
+ function rangeSubstituteFor(hintPath, cwd, ranges) {
7646
+ if (ranges.length === 0) return null;
7647
+ try {
7648
+ const resolved = resolveIndexPath(hintPath, cwd);
7649
+ if (!statSync5(resolved).isFile()) return null;
7650
+ if (!indexMatchesDisk(resolved)) return null;
7651
+ const lines = readFileSync3(resolved, "utf8").split("\n");
7652
+ const symbols = querySymbols({ filePath: resolved, limit: ALL_SYMBOLS_IN_FILE });
7653
+ if (symbols.length === 0) return null;
7654
+ const bytesOf = (start, end) => Buffer.byteLength(lines.slice(start - 1, Math.min(end, lines.length)).join("\n"), "utf8");
7655
+ const requestedLines = /* @__PURE__ */ new Set();
7656
+ const regionBySpan = /* @__PURE__ */ new Map();
7657
+ for (const [start, end] of ranges) {
7658
+ for (let l = start; l <= Math.min(end, lines.length); l++) requestedLines.add(l);
7659
+ for (const r of resolveLineRegions(symbols, lines.length, start, end)) {
7660
+ const named = symbols.find((s) => s.lineStart === r.start && s.lineEnd === r.end);
7661
+ regionBySpan.set(`${r.start}-${r.end}`, {
7662
+ kind: r.kind,
7663
+ name: r.kind === "symbol" && r.end > r.start && named !== void 0 ? named.name : null,
7664
+ start: r.start,
7665
+ end: r.end
7666
+ });
7667
+ }
7668
+ }
7669
+ if (regionBySpan.size === 0) return null;
7670
+ let requestedBytes = 0;
7671
+ for (const l of requestedLines) requestedBytes += Buffer.byteLength(lines[l - 1] ?? "", "utf8") + 1;
7672
+ const regions = [...regionBySpan.values()].sort((a, b) => a.start - b.start || a.end - b.end);
7673
+ const replacementBytes = regions.reduce((t, r) => t + bytesOf(r.start, r.end) + REGION_HEADER_BYTES, 0);
7674
+ const commands = regions.map(
7675
+ (r) => r.name === null ? `token-goat read "${hintPath}:${r.start}-${r.end}"` : `token-goat read "${hintPath}::${r.name}"`
7676
+ );
7677
+ return { requestedBytes, replacementBytes, commands };
7678
+ } catch {
7679
+ return null;
7680
+ }
7681
+ }
7682
+
7683
+ // src/bash_surgical_target.ts
7684
+ init_define_import_meta_env();
7685
+ import { statSync as statSync7 } from "node:fs";
7678
7686
 
7679
7687
  // src/bash_extractors.ts
7680
7688
  init_define_import_meta_env();
7681
- import { statSync as statSync4, openSync as openSync2, readSync, closeSync as closeSync2 } from "node:fs";
7682
- function surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml = false) {
7683
- return isXml ? 'Use `token-goat xml-outline "' + hintPath + '"` to inspect structure, or `token-goat xml-query "' + hintPath + '" "<selector>"` to query specific nodes.' : isEnv ? 'Use `token-goat config-get "' + hintPath + '" KEY_NAME` to read a specific variable.' : isConfig ? 'Use `token-goat config-get "' + hintPath + '" KEY_NAME` or `token-goat section "' + hintPath + '::sectionName"` to read a specific value.' : isDoc ? 'Use `token-goat section "' + hintPath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + hintPath + '::SymbolName"` to read one function or class.';
7689
+ import { statSync as statSync6, openSync as openSync2, readSync, closeSync as closeSync2 } from "node:fs";
7690
+ function genericSurgicalFallback(shown) {
7691
+ return '`token-goat outline "' + shown + '"`';
7692
+ }
7693
+ function surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml = false, target = null) {
7694
+ const key = target ?? "KEY_NAME";
7695
+ const section = target ?? "SectionHeading";
7696
+ return isXml ? 'Use `token-goat xml-outline "' + hintPath + '"` to inspect structure, or `token-goat xml-query "' + hintPath + '" "<selector>"` to query specific nodes.' : isEnv ? 'Use `token-goat config-get "' + hintPath + '" ' + key + "` to read a specific variable." : isConfig ? target === null ? 'Use `token-goat config-get "' + hintPath + '" KEY_NAME` or `token-goat section "' + hintPath + '::sectionName"` to read a specific value.' : 'Use `token-goat config-get "' + hintPath + '" ' + key + "` to read a specific value, or " + genericSurgicalFallback(hintPath) + " for every key with line ranges." : isDoc ? 'Use `token-goat section "' + hintPath + "::" + section + '"` to read one section' + (target === null ? "." : ", or " + genericSurgicalFallback(hintPath) + " for every heading with line ranges.") : "Use " + genericSurgicalFallback(hintPath) + " to read one function or class.";
7684
7697
  }
7685
7698
  function surgicalHintForConfigDoc(filePath, isConfig, isDoc, isSql, isXml = false) {
7686
- return isXml ? 'Use `token-goat xml-outline "' + filePath + '"` to inspect structure, or `token-goat xml-query "' + filePath + '" "<selector>"` to query specific nodes.' : isConfig ? 'Use `token-goat config-get "' + filePath + '" KEY_NAME` or `token-goat section "' + filePath + '::sectionName"` to read a specific value.' : isSql ? 'Use `token-goat section "' + filePath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.' : isDoc ? 'Use `token-goat section "' + filePath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + filePath + '::SymbolName"` or `token-goat skeleton "' + filePath + '"` to see the file structure.';
7699
+ return isXml ? 'Use `token-goat xml-outline "' + filePath + '"` to inspect structure, or `token-goat xml-query "' + filePath + '" "<selector>"` to query specific nodes.' : isConfig ? 'Use `token-goat config-get "' + filePath + '" KEY_NAME` or `token-goat section "' + filePath + '::sectionName"` to read a specific value.' : isSql ? 'Use `token-goat section "' + filePath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.' : isDoc ? 'Use `token-goat section "' + filePath + '::SectionHeading"` to read one section.' : "Use " + genericSurgicalFallback(filePath) + ' or `token-goat skeleton "' + filePath + '"` to see the file structure.';
7687
7700
  }
7688
7701
  function splitShellSegments(cmd) {
7689
7702
  const segments = [];
@@ -7706,6 +7719,10 @@ function splitShellSegments(cmd) {
7706
7719
  i++;
7707
7720
  continue;
7708
7721
  }
7722
+ if (ch === "&" && isRedirectAmpersand(cmd, i)) {
7723
+ cur += ch;
7724
+ continue;
7725
+ }
7709
7726
  if (ch === "|" || ch === ";" || ch === "\n" || ch === "&") {
7710
7727
  segments.push(cur);
7711
7728
  cur = "";
@@ -7756,6 +7773,7 @@ function classifyCatPath(filePath, cmd0) {
7756
7773
  return { filePath, ...flags, cmd0 };
7757
7774
  }
7758
7775
  function extractCatFile(cmd) {
7776
+ if (/-(?:TotalCount|Head|First|Tail)\b/i.test(cmd)) return null;
7759
7777
  const m = /^(cat|bat|type|Get-Content|gc)(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+-[a-zA-Z].*?)?(?:\s+2>(&1|\/dev\/null))?\s*$/i.exec(cmd);
7760
7778
  if (!m) return null;
7761
7779
  const cmd0 = m[1];
@@ -7782,7 +7800,7 @@ var PS_FILE_METHOD_RE = /\[(?:System\.)?IO\.File\]::(?:ReadAllText|ReadAllLines|
7782
7800
  var PS_TEMP_READ_FLOOD_BYTES = 16 * 1024;
7783
7801
  function isLargeFileOnDisk(filePath, floor) {
7784
7802
  try {
7785
- return statSync4(filePath).size >= floor;
7803
+ return statSync6(filePath).size >= floor;
7786
7804
  } catch {
7787
7805
  return false;
7788
7806
  }
@@ -7796,6 +7814,7 @@ function extractPowerShellWrappedGetContent(cmd, event) {
7796
7814
  if (!w) return null;
7797
7815
  const inner = (w[1] ?? w[2] ?? "").trim();
7798
7816
  if (!inner) return null;
7817
+ if (/-(?:TotalCount|Head|First|Tail)\b/i.test(inner)) return null;
7799
7818
  const m = PS_GETCONTENT_INNER_RE.exec(inner);
7800
7819
  if (!m) return null;
7801
7820
  const filePath = m[1] ?? m[2] ?? m[3];
@@ -8215,27 +8234,21 @@ function extractLineRangeReadsCompound(cmd) {
8215
8234
  }
8216
8235
  return [...merged.values()];
8217
8236
  }
8218
- function sedRangeHint(filePath, ranges, tool) {
8219
- const lang = detectLanguage(filePath);
8220
- const rangeReads = ranges.map(([s, e]) => '`token-goat read "' + filePath + "@" + s + "-" + e + '"`');
8221
- const allReads = rangeReads.length === 2 ? rangeReads.join(" and ") : rangeReads.length >= 3 ? rangeReads.slice(0, -1).join(", ") + ", and " + rangeReads[rangeReads.length - 1] : rangeReads[0];
8222
- const prefix = "`" + (tool === "awk" ? "awk" : "sed -n") + "` line-range reads bypass read hooks. ";
8223
- if (lang === "markdown") {
8224
- return prefix + 'For Markdown, `token-goat section "' + filePath + '::<heading>"` extracts a whole section by name (robust to line shifts); or ' + allReads + " for exactly those lines.";
8225
- }
8226
- if (lang === "toml" || lang === "json" || lang === "yaml" || lang === "ini") {
8227
- return prefix + 'For config, `token-goat config-get "' + filePath + '" <key>` or `token-goat section "' + filePath + '::<block>"` extracts one value; or ' + allReads + " for exactly those lines.";
8228
- }
8229
- if (languageHasFlag(lang, "symbolBearing")) {
8230
- return prefix + 'For a whole function/class, `token-goat symbol <name>` or `token-goat read "' + filePath + '::<Symbol>"` is robust to line shifts; or ' + allReads + " for exactly those lines.";
8231
- }
8232
- return prefix + "Use " + allReads + " to read exactly those lines.";
8237
+ function sedRangeHint(filePath, ranges, tool, sub) {
8238
+ return "`" + (tool === "awk" ? "awk" : "sed -n") + "` line-range reads bypass read hooks. " + substituteSentence(filePath, ranges, sub);
8239
+ }
8240
+ function substituteSentence(filePath, ranges, sub) {
8241
+ const spans = ranges.map(([s, e]) => s + "-" + e).join(", ");
8242
+ const reads = sub.commands.map((c) => "`" + c + "`");
8243
+ const allReads = reads.length === 2 ? reads.join(" and ") : reads.length >= 3 ? reads.slice(0, -1).join(", ") + ", and " + reads[reads.length - 1] : reads[0];
8244
+ return "Lines " + spans + " of " + filePath + " cost " + sub.requestedBytes + " bytes; " + allReads + " returns the same content as " + sub.replacementBytes + " bytes, resolved to whole regions and robust to line shifts.";
8233
8245
  }
8234
- function leadingLinesHint(lead, hintPath, start, end, flags, preHookCwd) {
8246
+ function leadingLinesHint(lead, hintPath, start, end, preHookCwd, substitute) {
8235
8247
  const key = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
8236
8248
  const prior = findRangeOverlap(getFileLineRanges(key), start, end);
8237
8249
  if (prior !== null) return sedOverlapHint(hintPath, prior, start, end);
8238
- return lead + surgicalHintForConfigDoc(hintPath, flags.isConfig, flags.isDoc, flags.isSql, flags.isXml ?? false);
8250
+ if (substitute === null) return null;
8251
+ return lead + substituteSentence(hintPath, [[start, end]], substitute);
8239
8252
  }
8240
8253
  function findRangeOverlap(prior, start, end) {
8241
8254
  let best = null;
@@ -8326,6 +8339,25 @@ function extractGetContentSelectFirst(cmd) {
8326
8339
  const { isDoc, isConfig, isSql, isXml } = classifyDocConfig(filePath);
8327
8340
  return { filePath, isDoc, isConfig, isSql, isXml, n };
8328
8341
  }
8342
+ function extractGetContentHead(cmd) {
8343
+ const headMatch = /-(?:TotalCount|Head|First)\s+(\d+)/i.exec(cmd);
8344
+ if (!headMatch) return null;
8345
+ const n = parseInt(headMatch[1], 10);
8346
+ if (n <= 10) return null;
8347
+ const getnMatch = /^(Get-Content|gc)\s+/i.exec(cmd);
8348
+ if (!getnMatch) return null;
8349
+ const afterCmd = cmd.slice(getnMatch[0].length).replace(/-Path\s+/i, "");
8350
+ const beforeHead = afterCmd.split(/-(?:TotalCount|Head|First)/i)[0]?.trim() ?? "";
8351
+ const afterHead = afterCmd.split(/-(?:TotalCount|Head|First)\s+\d+/i)[1]?.trim() ?? "";
8352
+ const cleanedBefore = beforeHead.replace(/-(?:Encoding|Delimiter|Wait)\s+\S+/gi, "").replace(/-[a-zA-Z]+/g, "").trim();
8353
+ const cleanedAfter = afterHead.replace(/-(?:Encoding|Delimiter|Wait)\s+\S+/gi, "").replace(/-[a-zA-Z]+/g, "").trim();
8354
+ const filePath = (cleanedBefore || cleanedAfter).replace(/^["']|["']$/g, "");
8355
+ if (!filePath) return null;
8356
+ if (isTempPath(filePath)) return null;
8357
+ if (!/\.(?:ts|tsx|js|jsx|py|go|java|rs|rb|cs|md|mdx|rst|txt|json|yaml|yml|toml|sql|sh|ps1|psm1|xml|dtsx|ampkg|xaml)$/i.test(filePath)) return null;
8358
+ const { isDoc, isConfig, isSql, isXml } = classifyDocConfig(filePath);
8359
+ return { filePath, isDoc, isConfig, isSql, isXml, n };
8360
+ }
8329
8361
  function taskOutputIsJsonlTranscript(outPath) {
8330
8362
  let fd = null;
8331
8363
  try {
@@ -8379,7 +8411,7 @@ function extractTasksOutput(cmd) {
8379
8411
  return null;
8380
8412
  }
8381
8413
  function extractToolResultsFile(cmd) {
8382
- const toolResultsRe = /[/\\]tool-results[/\\]([a-z0-9-]+)\.txt$/i;
8414
+ const toolResultsRe = new RegExp("[/\\\\]tool-results[/\\\\](" + TOOL_RESULTS_ID_CHARS + ")\\.txt$", "i");
8383
8415
  const catM = /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
8384
8416
  if (catM) {
8385
8417
  const fp = catM[1] ?? catM[2] ?? catM[3];
@@ -8733,7 +8765,38 @@ function extractTerminalXmlParsing(cmd) {
8733
8765
  return null;
8734
8766
  }
8735
8767
 
8768
+ // src/bash_surgical_target.ts
8769
+ var ALL_SYMBOLS_IN_FILE2 = -1;
8770
+ function quotableName(name2) {
8771
+ const safe = escapeHintName(name2);
8772
+ return safe === "" || safe.includes("::") ? null : safe;
8773
+ }
8774
+ function runnableTargetFor(hintPath, cwd, event) {
8775
+ try {
8776
+ if (!commandPathIsTouchable(hintPath, event)) return null;
8777
+ const resolved = resolveIndexPath(hintPath, cwd);
8778
+ if (!statSync7(resolved).isFile()) return null;
8779
+ if (!indexMatchesDisk(resolved)) return null;
8780
+ const symbols = querySymbols({ filePath: resolved, limit: ALL_SYMBOLS_IN_FILE2 });
8781
+ if (symbols.length === 0) return null;
8782
+ const inLineOrder = [...symbols].sort((a, b) => a.lineStart - b.lineStart);
8783
+ for (const s of inLineOrder) {
8784
+ const safe = quotableName(s.name);
8785
+ if (safe !== null) return safe;
8786
+ }
8787
+ return null;
8788
+ } catch {
8789
+ return null;
8790
+ }
8791
+ }
8792
+
8793
+ // src/hooks_bash.ts
8794
+ import { statSync as statSync9, existsSync, readFileSync as readFileSync4 } from "node:fs";
8795
+ import * as path4 from "node:path";
8796
+
8736
8797
  // src/hooks_bash_commands.ts
8798
+ init_define_import_meta_env();
8799
+ import { statSync as statSync8 } from "node:fs";
8737
8800
  var CD_PREFIX_RE = /^(?:cd\s+(?:"[^"]*"|'[^']*'|\S+)[ \t]*(?:&&|;|\r?\n)\s*)+/;
8738
8801
  function stripCdPrefix(cmd) {
8739
8802
  const stripped = cmd.replace(CD_PREFIX_RE, "");
@@ -8949,7 +9012,7 @@ function workingTreeStatusPaths(gitDir) {
8949
9012
  function enqueueRewrittenPath(absPath) {
8950
9013
  if (/(?:^|[/\\])(?:NUL|nul)$/.test(absPath) || absPath.replace(/\\/g, "/").endsWith("/dev/null")) return;
8951
9014
  try {
8952
- if (!statSync5(absPath).isFile()) return;
9015
+ if (!statSync8(absPath).isFile()) return;
8953
9016
  } catch {
8954
9017
  return;
8955
9018
  }
@@ -8997,7 +9060,8 @@ function pipelineShapeFilter(cmd, cwd) {
8997
9060
  }
8998
9061
  }
8999
9062
  if (recognizedCiCount > 0) {
9000
- return filterByName("generic-ci");
9063
+ const ci = filterByName("generic-ci");
9064
+ return ci === null ? null : { filter: ci, argv: [] };
9001
9065
  }
9002
9066
  }
9003
9067
  return null;
@@ -9012,7 +9076,16 @@ function pipelineShapeFilter(cmd, cwd) {
9012
9076
  if (!PIPELINE_PASSTHROUGH_HEADS.has(head.replace(/^.*[/\\]/, ""))) return null;
9013
9077
  }
9014
9078
  const detected = detectFromCommand(stripOutputPipeline(cmd), cwd ?? void 0);
9015
- return detected === null ? null : detected.filter;
9079
+ return detected === null ? null : { filter: detected.filter, argv: detected.argv };
9080
+ }
9081
+ function isFullRecallCommand(cmd) {
9082
+ const forSplit = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
9083
+ if (hasBareBackgroundOrNewline(forSplit)) return false;
9084
+ const first2 = splitShellSegments(forSplit)[0];
9085
+ if (first2 === void 0 || !segmentCommandIs(first2, "token-goat")) return false;
9086
+ const tokens = safeShlexSplit(stripOutputPipeline(first2));
9087
+ if (tokens === null) return false;
9088
+ return Object.values(RECALL_COMMAND).includes(tokens[1] ?? "") && tokens.includes("--full");
9016
9089
  }
9017
9090
  function pureFileReadPath(cmd) {
9018
9091
  const single = extractCatFile(cmd)?.filePath ?? extractHeadFile(cmd)?.filePath ?? extractTailFile(cmd)?.filePath ?? extractLineRangeRead(cmd)?.filePath;
@@ -9218,6 +9291,7 @@ function detectUnbalancedShellSyntax(cmd) {
9218
9291
  function maybeCompressRewrite(event, rawCmd, cmd) {
9219
9292
  if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
9220
9293
  if (event.raw["_tg_harness"] === "vscode") return null;
9294
+ if ((event.raw["_tg_harness"] === "codex" || event.raw["_tg_harness"] === "copilot_cli") && process.platform === "win32") return null;
9221
9295
  let cfg;
9222
9296
  try {
9223
9297
  cfg = loadConfig().bash_compress;
@@ -9286,7 +9360,7 @@ function foldShellReadBodies(cmd, output, fileKey, cwd) {
9286
9360
  if (no === void 0 || no === null) return null;
9287
9361
  rows.push({ no, text: lines[i] ?? "", raw: lines[i] ?? "" });
9288
9362
  }
9289
- const headRead = extractHeadFile(cmd);
9363
+ const headRead = extractHeadFile(cmd) ?? extractGetContentHead(cmd);
9290
9364
  const windowed = extractLineRangeRead(cmd) !== null || headRead !== null && lines.length >= headRead.n;
9291
9365
  const folded = foldDelivery(rows, fileKey, displaySafePath(toDisplayPath(findProject(cwd ?? process.cwd())?.root, fileKey)), windowed);
9292
9366
  if (folded === null) return null;
@@ -9297,7 +9371,7 @@ function foldShellReadStructure(cmd, filePath, output, fileKey, cwd) {
9297
9371
  const originalBytes = Buffer.byteLength(output, "utf-8");
9298
9372
  let onDisk;
9299
9373
  try {
9300
- onDisk = statSync6(fileKey).size;
9374
+ onDisk = statSync9(fileKey).size;
9301
9375
  } catch {
9302
9376
  return null;
9303
9377
  }
@@ -9405,6 +9479,7 @@ async function maybeElideServedGenericOutput(cmd, output, exitCode, cwd, cacheMi
9405
9479
  async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes, isUnwrapped = false) {
9406
9480
  if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
9407
9481
  if (!isUnwrapped && isCompressibleSingleCommand(cmd)) return null;
9482
+ if (isFullRecallCommand(cmd)) return null;
9408
9483
  if (exitCode !== null && exitCode !== 0) return null;
9409
9484
  let cfg;
9410
9485
  try {
@@ -9414,16 +9489,18 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
9414
9489
  }
9415
9490
  if (!cfg.enabled || cfg.disabled_filters.includes("generic")) return null;
9416
9491
  if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return null;
9417
- const shaped = pipelineShapeFilter(cmd, cwd) ?? (isUnwrapped ? detectFromCommand(cmd, cwd ?? void 0)?.filter ?? null : null);
9418
- const filter3 = shaped !== null && !cfg.disabled_filters.includes(shaped.name) ? shaped : filterByName("generic");
9492
+ const shaped = pipelineShapeFilter(cmd, cwd) ?? (isUnwrapped ? detectFromCommand(cmd, cwd ?? void 0) ?? null : null);
9493
+ const useShaped = shaped !== null && !cfg.disabled_filters.includes(shaped.filter.name);
9494
+ const filter3 = useShaped ? shaped.filter : filterByName("generic");
9419
9495
  if (filter3 === null) return null;
9420
- const compressed = compressOutput(filter3, output, "", exitCode ?? 0, [], {
9496
+ const filterArgv = useShaped ? shaped.argv : [];
9497
+ const compressed = compressOutput(filter3, output, "", exitCode ?? 0, filterArgv, {
9421
9498
  maxLines: cfg.max_lines,
9422
9499
  maxBytes: cfg.max_bytes
9423
9500
  });
9424
9501
  const minNet = resolveMinNetSavingsBytes();
9425
9502
  if (!compressed.worthApplying(minNet)) return null;
9426
- const id = await commandHash(cmd, cwd);
9503
+ const id = bashOutputIdSync(cmd, output, cwd);
9427
9504
  const { body: untrusted, notices } = splitOwnTrailingNotices(compressed.text);
9428
9505
  const marker = compressed.withMarker(minNet).slice(compressed.text.length);
9429
9506
  const body = fenceUntrusted(untrusted, UNTRUSTED_TOOL_TAG) + notices + marker + "\n[token-goat] full output: bash-output " + id + " --full";
@@ -9502,6 +9579,21 @@ function preBashHandlerInner(event) {
9502
9579
  const cdStripped = cmd !== rawCmd;
9503
9580
  const preHookCwd = getCwd(event) ?? null;
9504
9581
  const hintCwd = preHookCwd ?? process.cwd();
9582
+ const pathHint = (paths, text) => contextOutput(text, typeof paths === "string" ? [paths] : paths);
9583
+ const pricedSubstitute = (hintPath, start, end) => {
9584
+ const sub = rangeSubstituteFor(hintPath, hintCwd, [[start, end]]);
9585
+ return sub !== null && meetsSavingsFloor(sub.requestedBytes - sub.replacementBytes) ? sub : null;
9586
+ };
9587
+ const declineUnpriced = (paths) => {
9588
+ for (const p of typeof paths === "string" ? [paths] : paths) logSuppressedDetection("bash_redirect", event.sessionId, p);
9589
+ return passOutput();
9590
+ };
9591
+ const recallWorthShowing = (outputId2, entry2) => {
9592
+ if (entry2.sizeBytes < loadConfig().hints.bash_dedup_min_bytes) return false;
9593
+ if (meetsSavingsFloor(entry2.sizeBytes)) return true;
9594
+ logSuppressedDetection("bash_recall", event.sessionId, outputId2);
9595
+ return false;
9596
+ };
9505
9597
  const cfg = loadConfig();
9506
9598
  if (cfg.hints.warn_unbalanced_shell_quoting) {
9507
9599
  const quoteError = detectUnbalancedShellSyntax(cmd);
@@ -9562,17 +9654,19 @@ function preBashHandlerInner(event) {
9562
9654
  const { filePath, toolOrScript } = terminalXml;
9563
9655
  recordStat("session_hint", 0, 0);
9564
9656
  const target = filePath ? displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath) : "<file>";
9565
- return contextOutput(
9657
+ return pathHint(
9658
+ filePath ? [target] : [],
9566
9659
  `token-goat available for this file type, consider 'token-goat xml-query "${target}" "<xpath>"' or 'token-goat xml-outline "${target}"' first instead of terminal XML parsing (${toolOrScript}).`
9567
9660
  );
9568
9661
  }
9569
9662
  const singleLineRangeRead = extractLineRangeRead(cmd);
9570
9663
  const sedReads = singleLineRangeRead !== null ? [singleLineRangeRead] : extractLineRangeReadsCompound(cmd);
9571
9664
  if (sedReads !== null) {
9572
- recordStat("session_hint", 0, 0);
9573
9665
  const hints = [];
9666
+ const hintPaths = [];
9574
9667
  for (const { filePath, ranges, tool } of sedReads) {
9575
9668
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9669
+ hintPaths.push(hintPath);
9576
9670
  const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
9577
9671
  const overlapHints = [];
9578
9672
  const freshRanges = [];
@@ -9586,23 +9680,39 @@ function preBashHandlerInner(event) {
9586
9680
  }
9587
9681
  }
9588
9682
  hints.push(...overlapHints);
9589
- if (freshRanges.length > 0) hints.push(sedRangeHint(hintPath, freshRanges, tool));
9683
+ const sub = freshRanges.length > 0 ? rangeSubstituteFor(hintPath, hintCwd, freshRanges) : null;
9684
+ if (sub !== null && meetsSavingsFloor(sub.requestedBytes - sub.replacementBytes)) {
9685
+ hints.push(sedRangeHint(hintPath, freshRanges, tool, sub));
9686
+ }
9590
9687
  }
9591
- return contextOutput(hints.join(" "));
9688
+ if (hints.length === 0) return declineUnpriced(hintPaths);
9689
+ recordStat("session_hint", 0, 0);
9690
+ return pathHint(hintPaths, hints.join(" "));
9592
9691
  }
9593
9692
  const gcTailResult = extractGetContentTail(cmd);
9594
9693
  if (gcTailResult !== null) {
9595
9694
  const { filePath, isDoc, isConfig, isSql, isXml } = gcTailResult;
9596
9695
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9597
9696
  recordStat("session_hint", 0, 0);
9598
- return contextOutput("`Get-Content -Tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql, isXml));
9697
+ return pathHint(hintPath, "`Get-Content -Tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql, isXml));
9599
9698
  }
9600
9699
  const gcSelectResult = extractGetContentSelectFirst(cmd);
9601
9700
  if (gcSelectResult !== null) {
9602
- const { filePath, isDoc, isConfig, isSql, isXml, n } = gcSelectResult;
9701
+ const { filePath, n } = gcSelectResult;
9603
9702
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9703
+ const gcSelectHint = leadingLinesHint("`Select-Object -First` bypasses read hooks. ", hintPath, 1, n, preHookCwd, pricedSubstitute(hintPath, 1, n));
9704
+ if (gcSelectHint === null) return declineUnpriced(hintPath);
9604
9705
  recordStat("session_hint", 0, 0);
9605
- return contextOutput(leadingLinesHint("`Select-Object -First` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql, isXml }, preHookCwd));
9706
+ return pathHint(hintPath, gcSelectHint);
9707
+ }
9708
+ const gcHeadResult = extractGetContentHead(cmd);
9709
+ if (gcHeadResult !== null) {
9710
+ const { filePath, n } = gcHeadResult;
9711
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9712
+ const gcHeadHint = leadingLinesHint("`Get-Content -TotalCount` bypasses read hooks. ", hintPath, 1, n, preHookCwd, pricedSubstitute(hintPath, 1, n));
9713
+ if (gcHeadHint === null) return declineUnpriced(hintPath);
9714
+ recordStat("session_hint", 0, 0);
9715
+ return pathHint(hintPath, gcHeadHint);
9606
9716
  }
9607
9717
  const catJsonPipe = extractCatJsonPipe(cmd);
9608
9718
  if (catJsonPipe !== null) {
@@ -9610,7 +9720,8 @@ function preBashHandlerInner(event) {
9610
9720
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9611
9721
  recordStat("session_hint", 0, 0);
9612
9722
  const label = isDirectJq ? "`jq`" : "`cat | jq`";
9613
- return contextOutput(
9723
+ return pathHint(
9724
+ hintPath,
9614
9725
  label + ' loads the whole file. Use `token-goat json-query "' + hintPath + '" "<key>"` or `token-goat config-get "' + hintPath + '" KEY_NAME` to slice one value.'
9615
9726
  );
9616
9727
  }
@@ -9619,7 +9730,8 @@ function preBashHandlerInner(event) {
9619
9730
  recordStat("session_hint", 0, 0);
9620
9731
  if (psJsonPipe.filePath) {
9621
9732
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, psJsonPipe.filePath, hintCwd) : psJsonPipe.filePath);
9622
- return contextOutput(
9733
+ return pathHint(
9734
+ hintPath,
9623
9735
  'PowerShell `ConvertFrom-Json` pipeline detected. Use `token-goat json-query "' + hintPath + '" "<selector>"` to extract fields directly without shell conversion scripts.'
9624
9736
  );
9625
9737
  }
@@ -9633,12 +9745,13 @@ function preBashHandlerInner(event) {
9633
9745
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9634
9746
  recordStat("session_hint", 0, 0);
9635
9747
  if (isSql) {
9636
- return contextOutput(
9748
+ return pathHint(
9749
+ hintPath,
9637
9750
  "`" + cmd0 + '` loads the entire file into context. Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.'
9638
9751
  );
9639
9752
  }
9640
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9641
- return cdStripped || advisoryOnly ? contextOutput("`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
9753
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml, runnableTargetFor(hintPath, hintCwd, event));
9754
+ return cdStripped || advisoryOnly ? pathHint(hintPath, "`" + cmd0 + "` loads the entire file into context. " + hint) : denyOutput("`" + cmd0 + "` loads the entire file into context. " + hint);
9642
9755
  }
9643
9756
  const catMulti = extractCatFilesMulti(cmd);
9644
9757
  if (catMulti !== null) {
@@ -9650,7 +9763,7 @@ function preBashHandlerInner(event) {
9650
9763
  return " " + hintPath + " -> `" + how + "`";
9651
9764
  });
9652
9765
  const msg = "`" + cmd0 + "` on multiple files loads them all into context. Read each surgically instead:\n" + perPath.join("\n");
9653
- return cdStripped ? contextOutput(msg) : denyOutput(msg);
9766
+ return cdStripped ? pathHint(catMulti.map((m) => displaySafePath(resolveCdHintPath(rawCmd, m.filePath, hintCwd))), msg) : denyOutput(msg);
9654
9767
  }
9655
9768
  const psGetContentResult = extractPowerShellWrappedGetContent(cmd, event);
9656
9769
  if (psGetContentResult !== null) {
@@ -9659,10 +9772,10 @@ function preBashHandlerInner(event) {
9659
9772
  recordStat("session_hint", 0, 0);
9660
9773
  const lead = "`Get-Content` via a `powershell -Command` wrapper bypasses read hooks and loads the entire file into context. ";
9661
9774
  if (isSql) {
9662
- return contextOutput(lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9775
+ return pathHint(hintPath, lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9663
9776
  }
9664
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9665
- return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9777
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml, runnableTargetFor(hintPath, hintCwd, event));
9778
+ return cdStripped ? pathHint(hintPath, lead + hint) : denyOutput(lead + hint);
9666
9779
  }
9667
9780
  const wslCatResult = extractWslCatFile(cmd);
9668
9781
  if (wslCatResult !== null) {
@@ -9670,12 +9783,13 @@ function preBashHandlerInner(event) {
9670
9783
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9671
9784
  recordStat("session_hint", 0, 0);
9672
9785
  if (isSql) {
9673
- return contextOutput(
9786
+ return pathHint(
9787
+ hintPath,
9674
9788
  '`cat` loads the entire file into context. Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.'
9675
9789
  );
9676
9790
  }
9677
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9678
- return cdStripped ? contextOutput("`cat` loads the entire file into context. " + hint) : denyOutput("`cat` loads the entire file into context. " + hint);
9791
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml, runnableTargetFor(hintPath, hintCwd, event));
9792
+ return cdStripped ? pathHint(hintPath, "`cat` loads the entire file into context. " + hint) : denyOutput("`cat` loads the entire file into context. " + hint);
9679
9793
  }
9680
9794
  const pyRead = extractPythonFileRead(cmd);
9681
9795
  if (pyRead !== null) {
@@ -9685,33 +9799,37 @@ function preBashHandlerInner(event) {
9685
9799
  if (isOutputFile) {
9686
9800
  if (taskOutputIsJsonlTranscript(filePath)) {
9687
9801
  const tHint = 'This `.output` file is a JSONL agent transcript. Use `token-goat bash-output --file "' + hintPath + '" --transcript` to read the assistant text, then narrow with `--grep PATTERN` or `--tail N`, instead of hand-parsing the JSONL.';
9688
- return cdStripped ? contextOutput(tHint) : denyOutput(tHint);
9802
+ return cdStripped ? pathHint(hintPath, tHint) : denyOutput(tHint);
9689
9803
  }
9690
- return contextOutput(
9804
+ return pathHint(
9805
+ hintPath,
9691
9806
  "This `.output` file is a background command's stdout. Use `token-goat bash-output --file \"" + hintPath + '"` to narrow it with `--grep PATTERN`, `--tail N` or `--head N`, instead of reading the whole file.'
9692
9807
  );
9693
9808
  }
9694
9809
  if (isSql) {
9695
- return contextOutput(
9810
+ return pathHint(
9811
+ hintPath,
9696
9812
  'Python `open()` file reads bypass read hooks. Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.'
9697
9813
  );
9698
9814
  }
9699
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9700
- return cdStripped ? contextOutput("Python `open()` file reads bypass read hooks. " + hint) : denyOutput("Python `open()` file reads bypass read hooks. " + hint);
9815
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml, runnableTargetFor(hintPath, hintCwd, event));
9816
+ return cdStripped ? pathHint(hintPath, "Python `open()` file reads bypass read hooks. " + hint) : denyOutput("Python `open()` file reads bypass read hooks. " + hint);
9701
9817
  }
9702
9818
  const tailResult = extractTailFile(cmd);
9703
9819
  if (tailResult !== null) {
9704
9820
  const { filePath, isDoc, isConfig, isSql, isXml } = tailResult;
9705
9821
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9706
9822
  recordStat("session_hint", 0, 0);
9707
- return contextOutput("`tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql, isXml));
9823
+ return pathHint(hintPath, "`tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql, isXml));
9708
9824
  }
9709
9825
  const headResult = extractHeadFile(cmd);
9710
9826
  if (headResult !== null) {
9711
- const { filePath, isDoc, isConfig, isSql, isXml, n } = headResult;
9827
+ const { filePath, n } = headResult;
9712
9828
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9829
+ const headHint = leadingLinesHint("`head` bypasses read hooks. ", hintPath, 1, n, preHookCwd, pricedSubstitute(hintPath, 1, n));
9830
+ if (headHint === null) return declineUnpriced(hintPath);
9713
9831
  recordStat("session_hint", 0, 0);
9714
- return contextOutput(leadingLinesHint("`head` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql, isXml }, preHookCwd));
9832
+ return pathHint(hintPath, headHint);
9715
9833
  }
9716
9834
  const nodeRead = extractNodeFileRead(cmd);
9717
9835
  if (nodeRead !== null) {
@@ -9720,11 +9838,11 @@ function preBashHandlerInner(event) {
9720
9838
  const lead = "Node.js `fs.readFileSync()` bypasses read hooks. ";
9721
9839
  if (isSql) {
9722
9840
  recordStat("session_hint", 0, 0);
9723
- return contextOutput(lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9841
+ return pathHint(hintPath, lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9724
9842
  }
9725
- const hint = isDoc ? 'Use `token-goat section "' + hintPath + '::SectionHeading"` to read one section.' : isConfig ? 'Use `token-goat config-get "' + hintPath + '" KEY_NAME` or `token-goat section "' + hintPath + '::sectionName"` to read a specific value.' : 'Use `token-goat read "' + hintPath + '::SymbolName"` to extract a specific symbol.';
9843
+ const hint = surgicalHintFor(hintPath, false, isConfig, isDoc, false, runnableTargetFor(hintPath, hintCwd, event));
9726
9844
  recordStat("session_hint", 0, 0);
9727
- return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9845
+ return cdStripped ? pathHint(hintPath, lead + hint) : denyOutput(lead + hint);
9728
9846
  }
9729
9847
  const psMethodRead = extractPowerShellFileMethodRead(cmd, event);
9730
9848
  if (psMethodRead !== null) {
@@ -9733,11 +9851,15 @@ function preBashHandlerInner(event) {
9733
9851
  const lead = "PowerShell `[IO.File]::ReadAllText()` bypasses read hooks. ";
9734
9852
  if (isSql) {
9735
9853
  recordStat("session_hint", 0, 0);
9736
- return contextOutput(lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9854
+ return pathHint(hintPath, lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9737
9855
  }
9738
- const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml);
9856
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc, isXml, runnableTargetFor(hintPath, hintCwd, event));
9739
9857
  recordStat("session_hint", 0, 0);
9740
- return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9858
+ return cdStripped ? pathHint(hintPath, lead + hint) : denyOutput(lead + hint);
9859
+ }
9860
+ const structuralRewrite = detectStructuralIndexRewrite(rawCmd, hintCwd);
9861
+ if (structuralRewrite !== null) {
9862
+ return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: structuralRewrite.command } };
9741
9863
  }
9742
9864
  if (extractGrepPipeChain(cmd)) {
9743
9865
  recordStat("session_hint", 0, 0);
@@ -9750,7 +9872,8 @@ function preBashHandlerInner(event) {
9750
9872
  const { filePath } = mdHeadingGrep;
9751
9873
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9752
9874
  recordStat("session_hint", 0, 0);
9753
- return contextOutput(
9875
+ return pathHint(
9876
+ hintPath,
9754
9877
  'Use `token-goat outline "' + hintPath + '"` to get all headings with line ranges \u2014 then `token-goat section "' + hintPath + '::Heading"` to read one section.'
9755
9878
  );
9756
9879
  }
@@ -9759,7 +9882,8 @@ function preBashHandlerInner(event) {
9759
9882
  const { identifier } = rgSymbol;
9760
9883
  recordStat("session_hint", 0, 0);
9761
9884
  return contextOutput(
9762
- "Use `token-goat symbol " + identifier + "` to jump directly to the definition without scanning the file."
9885
+ "Use `token-goat symbol " + identifier + "` to jump directly to the definition without scanning the file.",
9886
+ [identifier]
9763
9887
  );
9764
9888
  }
9765
9889
  const rgStructural = extractRgStructuralSearch(cmd);
@@ -9767,7 +9891,8 @@ function preBashHandlerInner(event) {
9767
9891
  const { filePath } = rgStructural;
9768
9892
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9769
9893
  recordStat("session_hint", 0, 0);
9770
- return contextOutput(
9894
+ return pathHint(
9895
+ hintPath,
9771
9896
  'Searching for code definitions with `rg`/`grep` is slower than surgical reads. Use `token-goat skeleton "' + hintPath + '"` to see all symbols with line numbers, or `token-goat outline "' + hintPath + '"` for symbols with docstrings and line ranges.'
9772
9897
  );
9773
9898
  }
@@ -9777,7 +9902,7 @@ function preBashHandlerInner(event) {
9777
9902
  const monOutputId = getBashOutputId(monCmdHash);
9778
9903
  const monEntryRaw = monOutputId !== null ? getBashOutput(monOutputId) : null;
9779
9904
  const monEntry = monEntryRaw !== null && !isBashEntryStale(monEntryRaw, cmd, preHookCwd) ? monEntryRaw : null;
9780
- if (monOutputId !== null && monEntry !== null && monEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(monEntry.sizeBytes)) {
9905
+ if (monOutputId !== null && monEntry !== null && recallWorthShowing(monOutputId, monEntry)) {
9781
9906
  const monBytes = monEntry.sizeBytes;
9782
9907
  const catFile = extractCatSourceFile(cmd);
9783
9908
  if (catFile !== null) {
@@ -9799,7 +9924,7 @@ function preBashHandlerInner(event) {
9799
9924
  const prevResolvedPath = prevPath !== null && commandPathIsTouchable(prevPath, event) ? resolveIndexPath(prevPath, preHookCwd ?? process.cwd()) : null;
9800
9925
  if (prevPath !== null && prevResolvedPath !== null && !existsSync(prevResolvedPath)) {
9801
9926
  clearCurlDownload(curlDl.url);
9802
- } else if (prevPath !== null && prevResolvedPath !== null && statSync6(prevResolvedPath).size >= loadConfig().hints.bash_dedup_min_bytes) {
9927
+ } else if (prevPath !== null && prevResolvedPath !== null && statSync9(prevResolvedPath).size >= loadConfig().hints.bash_dedup_min_bytes) {
9803
9928
  recordStat("session_hint", 0, 0);
9804
9929
  return denyOutput(
9805
9930
  "Already downloaded to " + prevPath + " earlier this session. Use `rg '<pattern>' " + prevPath + '` to search it, or `token-goat read "' + prevPath + '::SectionName"` to read a part of it.'
@@ -9812,7 +9937,7 @@ function preBashHandlerInner(event) {
9812
9937
  const curlOutputId = getBashOutputId(curlHash);
9813
9938
  const curlEntryRaw = curlOutputId !== null ? getBashOutput(curlOutputId) : null;
9814
9939
  const curlEntry = curlEntryRaw !== null && !isBashEntryStale(curlEntryRaw, cmd, preHookCwd) ? curlEntryRaw : null;
9815
- if (curlOutputId !== null && curlEntry !== null && curlEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(curlEntry.sizeBytes)) {
9940
+ if (curlOutputId !== null && curlEntry !== null && recallWorthShowing(curlOutputId, curlEntry)) {
9816
9941
  const curlBytes = curlEntry.sizeBytes;
9817
9942
  recordStat("bash_compress:recall", curlBytes, savedTokensFromBytes(curlBytes));
9818
9943
  const curlPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
@@ -9826,7 +9951,7 @@ function preBashHandlerInner(event) {
9826
9951
  const ghOutputId = getBashOutputId(ghHash);
9827
9952
  const ghEntryRaw = ghOutputId !== null ? getBashOutput(ghOutputId) : null;
9828
9953
  const ghEntry = ghEntryRaw !== null && !isBashEntryStale(ghEntryRaw, cmd, preHookCwd) ? ghEntryRaw : null;
9829
- if (ghOutputId !== null && ghEntry !== null && ghEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(ghEntry.sizeBytes)) {
9954
+ if (ghOutputId !== null && ghEntry !== null && recallWorthShowing(ghOutputId, ghEntry)) {
9830
9955
  const ghBytes = ghEntry.sizeBytes;
9831
9956
  recordStat("bash_compress:recall", ghBytes, savedTokensFromBytes(ghBytes));
9832
9957
  const ghPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
@@ -9840,7 +9965,7 @@ function preBashHandlerInner(event) {
9840
9965
  const gitScopedOutputId = getBashOutputId(gitScopedHash);
9841
9966
  const gitScopedEntryRaw = gitScopedOutputId !== null ? getBashOutput(gitScopedOutputId) : null;
9842
9967
  const gitScopedEntry = gitScopedEntryRaw !== null && !isBashEntryStale(gitScopedEntryRaw, cmd, preHookCwd) ? gitScopedEntryRaw : null;
9843
- if (gitScopedOutputId !== null && gitScopedEntry !== null && gitScopedEntry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(gitScopedEntry.sizeBytes)) {
9968
+ if (gitScopedOutputId !== null && gitScopedEntry !== null && recallWorthShowing(gitScopedOutputId, gitScopedEntry)) {
9844
9969
  const gitScopedBytes = gitScopedEntry.sizeBytes;
9845
9970
  recordStat("bash_compress:recall", gitScopedBytes, savedTokensFromBytes(gitScopedBytes));
9846
9971
  const gitScopedPreview = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
@@ -9861,7 +9986,7 @@ function preBashHandlerInner(event) {
9861
9986
  }
9862
9987
  if (notes.length > 0) {
9863
9988
  recordStat("session_hint", 0, 0);
9864
- return contextOutput(notes.join(" "));
9989
+ return pathHint(tgRead.filePath !== null ? [tgRead.filePath] : [], notes.join(" "));
9865
9990
  }
9866
9991
  return passOutput();
9867
9992
  }
@@ -9882,7 +10007,7 @@ function preBashHandlerInner(event) {
9882
10007
  const outputId = getBashOutputId(cmdHash);
9883
10008
  const entryRaw = outputId !== null ? getBashOutput(outputId) : null;
9884
10009
  const entry = entryRaw !== null && !isBashEntryStale(entryRaw, cmd, preHookCwd) ? entryRaw : null;
9885
- if (outputId !== null && entry !== null && entry.sizeBytes >= loadConfig().hints.bash_dedup_min_bytes && meetsSavingsFloor(entry.sizeBytes)) {
10010
+ if (outputId !== null && entry !== null && recallWorthShowing(outputId, entry)) {
9886
10011
  const bytes = entry.sizeBytes;
9887
10012
  recordStat("bash_compress:recall", bytes, savedTokensFromBytes(bytes));
9888
10013
  return contextOutput(buildRecallHint(cmd, outputId));
@@ -9893,7 +10018,28 @@ function preBashHandler(event) {
9893
10018
  return applyHintTracking(event, preBashHandlerInner(event), classifyBashHint);
9894
10019
  }
9895
10020
  registerHook("pre_tool_use", preBashHandler, { toolName: "Bash" });
9896
- function extractBashOutput(raw) {
10021
+ function readPersistedBashOutput(resp, cwd, sessionId) {
10022
+ const persistedPath = resp["persistedOutputPath"];
10023
+ if (typeof persistedPath !== "string" || persistedPath === "" || sessionId === "") return null;
10024
+ const root = path4.join(projectTranscriptsDir(cwd ?? process.cwd()), sessionId, "tool-results");
10025
+ if (!isInsideRoot(persistedPath, root)) return null;
10026
+ try {
10027
+ const stat = statSync9(persistedPath);
10028
+ if (!stat.isFile() || stat.size > MAX_CAPTURE_BYTES) return null;
10029
+ const persistedSize = resp["persistedOutputSize"];
10030
+ if (typeof persistedSize === "number" && Math.abs(stat.size - persistedSize) > 8) return null;
10031
+ return readFileSync4(persistedPath, "utf-8");
10032
+ } catch {
10033
+ return null;
10034
+ }
10035
+ }
10036
+ function extractBashOutput(event) {
10037
+ const raw = event.raw;
10038
+ const resp = raw["tool_response"];
10039
+ if (resp !== null && typeof resp === "object") {
10040
+ const persisted = readPersistedBashOutput(resp, getCwd(event) ?? null, event.sessionId);
10041
+ if (persisted !== null) return persisted;
10042
+ }
9897
10043
  return extractToolResponseField(raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
9898
10044
  }
9899
10045
  function extractExitCode(raw) {
@@ -9959,6 +10105,11 @@ function recordBashFileReadsForSessionCache(cmd, cwd) {
9959
10105
  recordFileLineRange(resolve2(gcSelect.filePath), 1, gcSelect.n);
9960
10106
  return;
9961
10107
  }
10108
+ const gcHead = extractGetContentHead(cmd);
10109
+ if (gcHead !== null) {
10110
+ recordFileLineRange(resolve2(gcHead.filePath), 1, gcHead.n);
10111
+ return;
10112
+ }
9962
10113
  const head = extractHeadFile(cmd);
9963
10114
  if (head !== null) {
9964
10115
  recordFileLineRange(resolve2(head.filePath), 1, head.n);
@@ -10021,7 +10172,7 @@ async function postBashHandler(event) {
10021
10172
  const isUnwrapped = unwrapCompressCommand(rawCmdRaw) === null;
10022
10173
  const rawCmd = unwrapCompressCommand(rawCmdRaw) ?? rawCmdRaw;
10023
10174
  const cmd = stripCdPrefix(rawCmd);
10024
- const output = extractBashOutput(event.raw);
10175
+ const output = extractBashOutput(event);
10025
10176
  const exitCode = extractExitCode(event.raw);
10026
10177
  const cwd = getCwd(event) ?? null;
10027
10178
  const cacheMinBytes = loadConfig().bash_compress.cache_min_bytes;
@@ -10208,9 +10359,11 @@ function postBashOutputHandler(event) {
10208
10359
  const delta = output.slice(prior.output.length);
10209
10360
  storePollSnapshot(event.sessionId, bashId, output);
10210
10361
  if (Buffer.byteLength(delta, "utf-8") < minBytes) return passOutput();
10362
+ const compressedDelta = looksLikePytestOutput(delta) ? pytestFilter.apply(delta, "", 0, []) : null;
10363
+ const deltaBody = compressedDelta !== null && compressedDelta.worthApplying(resolveMinNetSavingsBytes()) ? compressedDelta.withMarker(resolveMinNetSavingsBytes()) : delta;
10211
10364
  const deltaNotice = `[token-goat: bash_id ${bashId} delta since last poll]
10212
10365
  `;
10213
- const fenced = fenceUntrusted(delta, UNTRUSTED_TOOL_TAG);
10366
+ const fenced = fenceUntrusted(deltaBody, UNTRUSTED_TOOL_TAG);
10214
10367
  const body = `${deltaNotice}${fenced}`;
10215
10368
  if (!isRewriteWorthwhile({
10216
10369
  originalBytes: Buffer.byteLength(output, "utf-8"),
@@ -10332,7 +10485,7 @@ registerHook("post_tool_use", postTaskOutputHandler, { toolName: "TaskOutput" })
10332
10485
 
10333
10486
  // src/hooks_tool_failure.ts
10334
10487
  init_define_import_meta_env();
10335
- import { existsSync as existsSync2, readFileSync as readFileSync3, statSync as statSync7, writeFileSync as writeFileSync2 } from "node:fs";
10488
+ import { existsSync as existsSync2, readFileSync as readFileSync5, statSync as statSync10, writeFileSync as writeFileSync2 } from "node:fs";
10336
10489
  import { dirname as dirname2, relative as relative2 } from "node:path";
10337
10490
  var FAILURE_SUFFIX = ".tool-failures.json";
10338
10491
  var MAX_TRACKED_FAILURES = 64;
@@ -10360,7 +10513,7 @@ function ledgerPath(sessionId) {
10360
10513
  }
10361
10514
  function readLedger(target) {
10362
10515
  try {
10363
- const parsed = JSON.parse(readFileSync3(target, "utf8"));
10516
+ const parsed = JSON.parse(readFileSync5(target, "utf8"));
10364
10517
  if (parsed !== null && typeof parsed === "object") {
10365
10518
  const seen = parsed.seen;
10366
10519
  const order = parsed.order;
@@ -10401,9 +10554,9 @@ function diagnoseEditFailure(event, errorText) {
10401
10554
  const absPath = normalizePath(filePath);
10402
10555
  if (!existsSync2(absPath)) return null;
10403
10556
  try {
10404
- const stat = statSync7(absPath);
10557
+ const stat = statSync10(absPath);
10405
10558
  if (stat.size > MAX_EDIT_DIAGNOSE_BYTES) return null;
10406
- const fileContent = readFileSync3(absPath, "utf8");
10559
+ const fileContent = readFileSync5(absPath, "utf8");
10407
10560
  const relDisplay = displaySafeText(relative2(process.cwd(), absPath).replace(/\\/g, "/") || absPath);
10408
10561
  if (isMultiple) {
10409
10562
  const matchLines = [];
@@ -11051,8 +11204,8 @@ The full payload was cached to prevent harness context spill. Slicing commands:
11051
11204
  }
11052
11205
  return passOrFence();
11053
11206
  }
11054
- registerHook("pre_tool_use", preMcpHandler, { toolPattern: "^mcp__" });
11055
- registerHook("post_tool_use", postMcpHandler, { toolPattern: "^mcp__" });
11207
+ registerHook("pre_tool_use", preMcpHandler, { toolPattern: MCP_TOOL_PATTERN });
11208
+ registerHook("post_tool_use", postMcpHandler, { toolPattern: MCP_TOOL_PATTERN });
11056
11209
 
11057
11210
  // src/hooks_websearch.ts
11058
11211
  init_define_import_meta_env();
@@ -11253,8 +11406,7 @@ registerHook("post_tool_use", postBrowserImageHandler, { toolPattern: "^mcp__" }
11253
11406
  init_define_import_meta_env();
11254
11407
  import { createHash as createHash2 } from "node:crypto";
11255
11408
  import * as fs2 from "node:fs";
11256
- import * as os2 from "node:os";
11257
- import * as path4 from "node:path";
11409
+ import * as path5 from "node:path";
11258
11410
  var BRIEFING_TARGET_TOKENS = 550;
11259
11411
  function buildSubagentBriefing() {
11260
11412
  try {
@@ -11479,8 +11631,8 @@ function parseAgentDefinition(text, fallbackName) {
11479
11631
  return { name: name2, restricted: false };
11480
11632
  }
11481
11633
  function findRestrictedAgentNames(roots) {
11482
- const projectAgentsRoot = path4.join(process.cwd(), ".claude", "agents");
11483
- const scanRoots = roots ?? [path4.join(os2.homedir(), ".claude", "agents"), projectAgentsRoot];
11634
+ const projectAgentsRoot = path5.join(process.cwd(), ".claude", "agents");
11635
+ const scanRoots = roots ?? [path5.join(claudeConfigDir(), "agents"), projectAgentsRoot];
11484
11636
  const names = /* @__PURE__ */ new Set();
11485
11637
  const visited = /* @__PURE__ */ new Set();
11486
11638
  let filesSeen = 0;
@@ -11504,7 +11656,7 @@ function findRestrictedAgentNames(roots) {
11504
11656
  }
11505
11657
  for (const entry of entries) {
11506
11658
  if (filesSeen >= ROSTER_WALK_MAX_FILES) return;
11507
- const full = path4.join(dir, entry.name);
11659
+ const full = path5.join(dir, entry.name);
11508
11660
  let stat;
11509
11661
  try {
11510
11662
  stat = fs2.statSync(full);
@@ -11523,12 +11675,12 @@ function findRestrictedAgentNames(roots) {
11523
11675
  } catch {
11524
11676
  continue;
11525
11677
  }
11526
- const parsed = parseAgentDefinition(text, path4.basename(entry.name, path4.extname(entry.name)));
11678
+ const parsed = parseAgentDefinition(text, path5.basename(entry.name, path5.extname(entry.name)));
11527
11679
  if (parsed !== null && parsed.restricted) names.add(parsed.name);
11528
11680
  }
11529
11681
  };
11530
11682
  for (const root of scanRoots) {
11531
- const isProjectRoot = roots === void 0 && path4.resolve(root) === path4.resolve(projectAgentsRoot);
11683
+ const isProjectRoot = roots === void 0 && path5.resolve(root) === path5.resolve(projectAgentsRoot);
11532
11684
  if (!isProjectRoot) {
11533
11685
  walk(root, 0, null);
11534
11686
  continue;
@@ -11658,11 +11810,11 @@ function safeSuggestions(output) {
11658
11810
  if (output.hookType === "context") return { hookType: "context", context: stripUnsafeSuggestions(output.context) };
11659
11811
  return output;
11660
11812
  }
11661
- async function relayInProcess(eventName, rawPayload) {
11813
+ async function relayInProcess(eventName, rawPayload, harnessWaitMs) {
11814
+ if (!isHookEventName(eventName)) {
11815
+ return "{}";
11816
+ }
11662
11817
  try {
11663
- if (!isHookEventName(eventName)) {
11664
- return "{}";
11665
- }
11666
11818
  const harness = detectHarness();
11667
11819
  const payload = eventName === "pre_tool_use" || eventName === "post_tool_use" ? normalizePayload(rawPayload, harnessForNormalization()) : rawPayload;
11668
11820
  const event = buildEvent(eventName, payload);
@@ -11686,6 +11838,9 @@ async function relayInProcess(eventName, rawPayload) {
11686
11838
  } catch {
11687
11839
  }
11688
11840
  const output = safeSuggestions(await runHook(event));
11841
+ if (event.sessionId) {
11842
+ commitPendingContext(stateKey, output.hookType === "context" ? output.context : null);
11843
+ }
11689
11844
  try {
11690
11845
  saveSessionState(stateKey);
11691
11846
  } catch {
@@ -11693,6 +11848,8 @@ async function relayInProcess(eventName, rawPayload) {
11693
11848
  return serializeOutput(output, event.eventName, harness, event);
11694
11849
  } catch {
11695
11850
  return "{}";
11851
+ } finally {
11852
+ recordStat(`hook:${eventName}`, 0, 0, void 0, void 0, void 0, harnessWaitMs ?? performance.now());
11696
11853
  }
11697
11854
  }
11698
11855
  async function relay(eventName) {