token-goat 2.9.28 → 2.9.30

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 (134) hide show
  1. package/README.md +36 -40
  2. package/SECURITY.md +28 -10
  3. package/THIRD_PARTY_NOTICES.md +40 -3
  4. package/dist/native/linux-arm64/tg-hook +0 -0
  5. package/dist/native/linux-x64/tg-hook +0 -0
  6. package/dist/ort-wasm-simd-threaded.mjs +59 -0
  7. package/dist/token-goat-chunk-3CHUNJ2O.mjs +53 -0
  8. package/dist/token-goat-chunk-3Z5S4KGM.mjs +918 -0
  9. package/dist/token-goat-chunk-4FVIAHSI.mjs +211 -0
  10. package/dist/{token-goat-chunk-66LODAUT.mjs → token-goat-chunk-4JJS54TR.mjs} +4 -3
  11. package/dist/token-goat-chunk-4LCCNRTG.mjs +313 -0
  12. package/dist/token-goat-chunk-54K3EUV3.mjs +35 -0
  13. package/dist/token-goat-chunk-56EIMS3E.mjs +159 -0
  14. package/dist/token-goat-chunk-5BCIJXMO.mjs +609 -0
  15. package/dist/token-goat-chunk-5KDMCMF2.mjs +362 -0
  16. package/dist/token-goat-chunk-5RG5SBL6.mjs +221 -0
  17. package/dist/{token-goat-chunk-RRNZMM3A.mjs → token-goat-chunk-5Y6ZYL6U.mjs} +21 -2
  18. package/dist/{token-goat-chunk-NILVK7XM.mjs → token-goat-chunk-64FBPDTK.mjs} +12 -6
  19. package/dist/{token-goat-chunk-LEG3JNJ6.mjs → token-goat-chunk-664RTTUV.mjs} +31 -15
  20. package/dist/token-goat-chunk-67TQJ76B.mjs +57 -0
  21. package/dist/token-goat-chunk-6QMAS7AB.mjs +42 -0
  22. package/dist/token-goat-chunk-7BXSNBY5.mjs +253 -0
  23. package/dist/{token-goat-chunk-F7QZRIWM.mjs → token-goat-chunk-7F2UBPD3.mjs} +42 -35
  24. package/dist/{token-goat-chunk-CC2PYJM7.mjs → token-goat-chunk-7IN2MKDS.mjs} +718 -595
  25. package/dist/token-goat-chunk-7NADEDKJ.mjs +124 -0
  26. package/dist/{token-goat-chunk-6SGLIPQB.mjs → token-goat-chunk-A3CK6N5Z.mjs} +621 -72
  27. package/dist/{token-goat-chunk-HXNAC4ZF.mjs → token-goat-chunk-AFLYLHDH.mjs} +48 -33
  28. package/dist/token-goat-chunk-AM5V6GSX.mjs +367 -0
  29. package/dist/{token-goat-chunk-I7CB3X4K.mjs → token-goat-chunk-APGZXSPS.mjs} +44 -148
  30. package/dist/{token-goat-chunk-BJBRGPBX.mjs → token-goat-chunk-ARDNEC2U.mjs} +11 -7
  31. package/dist/token-goat-chunk-ATCPY7EQ.mjs +134 -0
  32. package/dist/token-goat-chunk-B2RZZCSS.mjs +354 -0
  33. package/dist/{token-goat-chunk-QM76S6YS.mjs → token-goat-chunk-B45VMIIH.mjs} +6 -5
  34. package/dist/token-goat-chunk-BPGCUMR7.mjs +30 -0
  35. package/dist/{token-goat-chunk-GGLEE35R.mjs → token-goat-chunk-C63JQV5K.mjs} +49 -10
  36. package/dist/{token-goat-chunk-RJH64KQH.mjs → token-goat-chunk-COBQGL5S.mjs} +2661 -2440
  37. package/dist/token-goat-chunk-COTXZHTS.mjs +39 -0
  38. package/dist/token-goat-chunk-CYCEYAEO.mjs +265 -0
  39. package/dist/token-goat-chunk-CZ3LUXYZ.mjs +4115 -0
  40. package/dist/token-goat-chunk-DZX2YDPT.mjs +15 -0
  41. package/dist/token-goat-chunk-E3OQX2XT.mjs +824 -0
  42. package/dist/token-goat-chunk-EFHMMIGP.mjs +68 -0
  43. package/dist/token-goat-chunk-ETBHVDB6.mjs +328 -0
  44. package/dist/token-goat-chunk-EVMZJ27Z.mjs +147 -0
  45. package/dist/token-goat-chunk-EXKGNHEG.mjs +2096 -0
  46. package/dist/{token-goat-chunk-6Z5ULNE5.mjs → token-goat-chunk-FBNQNSJK.mjs} +2490 -4957
  47. package/dist/{token-goat-chunk-NMTKNYGF.mjs → token-goat-chunk-FNFBKLRD.mjs} +74 -9
  48. package/dist/{token-goat-chunk-XI2VFAV6.mjs → token-goat-chunk-FZ3NEW5O.mjs} +33 -19
  49. package/dist/token-goat-chunk-G5PJ7AGM.mjs +36 -0
  50. package/dist/{token-goat-chunk-VE27HPIF.mjs → token-goat-chunk-GFBSN7BD.mjs} +30 -398
  51. package/dist/{token-goat-chunk-L45ZQRT7.mjs → token-goat-chunk-GOLEOXZP.mjs} +780 -786
  52. package/dist/token-goat-chunk-H2K3QP6H.mjs +24 -0
  53. package/dist/token-goat-chunk-H2PHHGM5.mjs +188 -0
  54. package/dist/token-goat-chunk-HAVAOB3S.mjs +87 -0
  55. package/dist/token-goat-chunk-HG45AF2B.mjs +2228 -0
  56. package/dist/{token-goat-chunk-CBMY2FGI.mjs → token-goat-chunk-HOJLCGDP.mjs} +17113 -15554
  57. package/dist/{token-goat-chunk-HUDC5GS5.mjs → token-goat-chunk-HYBFA5X3.mjs} +120 -19
  58. package/dist/token-goat-chunk-IVALLT3S.mjs +48 -0
  59. package/dist/token-goat-chunk-IVE6U7KV.mjs +53 -0
  60. package/dist/{token-goat-chunk-UBUKHVYW.mjs → token-goat-chunk-IVKIZWAB.mjs} +3 -3
  61. package/dist/{token-goat-chunk-4JMWPWCS.mjs → token-goat-chunk-JB6QN6VF.mjs} +6 -4
  62. package/dist/{token-goat-chunk-Q4S3UQGO.mjs → token-goat-chunk-JDHE26RP.mjs} +10 -3
  63. package/dist/{token-goat-chunk-7R2XYKC5.mjs → token-goat-chunk-JDU3EXSU.mjs} +14 -10
  64. package/dist/token-goat-chunk-JDWCRD6M.mjs +253 -0
  65. package/dist/{token-goat-chunk-XBESNWEX.mjs → token-goat-chunk-JN4XMLSD.mjs} +7 -4
  66. package/dist/token-goat-chunk-JTQQGXZH.mjs +733 -0
  67. package/dist/token-goat-chunk-KKUFXAPD.mjs +25 -0
  68. package/dist/token-goat-chunk-KQNZDCBD.mjs +47 -0
  69. package/dist/{token-goat-chunk-4RN64ZND.mjs → token-goat-chunk-KWPNJBDH.mjs} +28 -3
  70. package/dist/token-goat-chunk-LOBGHXPB.mjs +242 -0
  71. package/dist/token-goat-chunk-MKPF4CL5.mjs +37 -0
  72. package/dist/token-goat-chunk-MNWKPRAL.mjs +13 -0
  73. package/dist/{token-goat-chunk-YWSP6IU7.mjs → token-goat-chunk-N2NMER56.mjs} +25 -15
  74. package/dist/token-goat-chunk-NYKZLXMK.mjs +218 -0
  75. package/dist/token-goat-chunk-OJBNYTJP.mjs +3087 -0
  76. package/dist/token-goat-chunk-P4QMM4WK.mjs +295 -0
  77. package/dist/token-goat-chunk-PD5VKH46.mjs +184 -0
  78. package/dist/token-goat-chunk-PEPTHRFF.mjs +129 -0
  79. package/dist/{token-goat-chunk-VWZKZPSR.mjs → token-goat-chunk-PU7V3FD6.mjs} +8752 -11098
  80. package/dist/token-goat-chunk-PY533T7F.mjs +46 -0
  81. package/dist/token-goat-chunk-Q5VTGRH7.mjs +761 -0
  82. package/dist/{token-goat-chunk-IHKZ3QNA.mjs → token-goat-chunk-QCLDAQTS.mjs} +33 -19
  83. package/dist/{token-goat-chunk-LKSXAMJB.mjs → token-goat-chunk-QSBRTGM2.mjs} +9 -2
  84. package/dist/{token-goat-chunk-4STDIGYC.mjs → token-goat-chunk-RC35RBUL.mjs} +7 -5
  85. package/dist/{token-goat-chunk-D5ICEPSB.mjs → token-goat-chunk-RHXKCZX3.mjs} +887 -507
  86. package/dist/{token-goat-chunk-RSS3TLXU.mjs → token-goat-chunk-RZRTDST7.mjs} +17 -7
  87. package/dist/{token-goat-chunk-MAMQPC43.mjs → token-goat-chunk-SJHWVNDQ.mjs} +15 -21
  88. package/dist/token-goat-chunk-SMVRN2HU.mjs +188 -0
  89. package/dist/{token-goat-chunk-EL7M26WP.mjs → token-goat-chunk-SQSSNQPE.mjs} +2100 -1442
  90. package/dist/{token-goat-chunk-OZRSREKR.mjs → token-goat-chunk-T4FK7VOV.mjs} +3 -3
  91. package/dist/token-goat-chunk-U4N3BN2W.mjs +92 -0
  92. package/dist/token-goat-chunk-UA5V6P3P.mjs +122 -0
  93. package/dist/{token-goat-chunk-XKY4VKJ4.mjs → token-goat-chunk-UQ5UK2T7.mjs} +15 -4
  94. package/dist/token-goat-chunk-USILZ3T3.mjs +53 -0
  95. package/dist/token-goat-chunk-VES7EL7O.mjs +205 -0
  96. package/dist/{token-goat-chunk-M56BWPBN.mjs → token-goat-chunk-VL3YIFHZ.mjs} +550 -49
  97. package/dist/{token-goat-chunk-IZXKVT4A.mjs → token-goat-chunk-VPBBTSJR.mjs} +362 -128
  98. package/dist/{token-goat-chunk-F2ARFHLJ.mjs → token-goat-chunk-W3QCJS6G.mjs} +10 -2
  99. package/dist/{token-goat-chunk-FSTWAOW6.mjs → token-goat-chunk-WY3L4DVT.mjs} +11 -5
  100. package/dist/{token-goat-chunk-OSUFN2FV.mjs → token-goat-chunk-XB5KKOCG.mjs} +5 -25
  101. package/dist/token-goat-chunk-XKI4SEMX.mjs +66 -0
  102. package/dist/token-goat-chunk-XU4P6Z24.mjs +133 -0
  103. package/dist/{token-goat-chunk-6CCQDAYY.mjs → token-goat-chunk-Y776AENC.mjs} +138 -33
  104. package/dist/token-goat-chunk-YARZFCZS.mjs +359 -0
  105. package/dist/{token-goat-chunk-EEIDFMEM.mjs → token-goat-chunk-YZAAZU2S.mjs} +12 -3
  106. package/dist/{token-goat-chunk-E3K3BTEQ.mjs → token-goat-chunk-Z44YO3QZ.mjs} +12 -6
  107. package/dist/token-goat-chunk-Z57YODA2.mjs +35 -0
  108. package/dist/{token-goat-chunk-DA43OE7L.mjs → token-goat-chunk-ZCDTCVIS.mjs} +49 -30
  109. package/dist/token-goat-chunk-ZEJVMUQH.mjs +48 -0
  110. package/dist/{token-goat-chunk-BGHSSJUA.mjs → token-goat-chunk-ZFOBKUWH.mjs} +1 -1
  111. package/dist/token-goat-chunk-ZMBL6UVV.mjs +110 -0
  112. package/dist/{token-goat-chunk-SJEKA7Y2.mjs → token-goat-chunk-ZQSJMO2R.mjs} +381 -201
  113. package/dist/token-goat-hook-client.cjs +539 -0
  114. package/dist/token-goat-hook-client.mjs +26 -0
  115. package/dist/token-goat-hook.mjs +48 -28
  116. package/dist/token-goat.core.mjs +10 -45
  117. package/docs/C4_RUNTIME_ARCHITECTURE.md +3 -3
  118. package/docs/cli.md +82 -47
  119. package/docs/install.md +58 -23
  120. package/docs/security.md +7 -7
  121. package/package.json +17 -7
  122. package/scripts/verify-native-dist.mjs +386 -0
  123. package/dist/token-goat-chunk-4JJBFT7V.mjs +0 -20
  124. package/dist/token-goat-chunk-5NNYIUGV.mjs +0 -143
  125. package/dist/token-goat-chunk-6DQNNFJ6.mjs +0 -294
  126. package/dist/token-goat-chunk-7MAFDRVR.mjs +0 -47
  127. package/dist/token-goat-chunk-BCN2YQC6.mjs +0 -32
  128. package/dist/token-goat-chunk-D7GHQKQD.mjs +0 -30
  129. package/dist/token-goat-chunk-GMOUBOX4.mjs +0 -386
  130. package/dist/token-goat-chunk-IFORDBGC.mjs +0 -192
  131. package/dist/token-goat-chunk-JXK2HQXS.mjs +0 -318
  132. package/dist/token-goat-chunk-L7SDVGBT.mjs +0 -430
  133. package/dist/token-goat-chunk-MMLMALWK.mjs +0 -1381
  134. package/dist/token-goat-chunk-MVSWBNMD.mjs +0 -1985
@@ -1,19 +1,159 @@
1
1
  import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
+ import {
4
+ applyProbeMarker
5
+ } from "./token-goat-chunk-ZEJVMUQH.mjs";
6
+ import {
7
+ MCP_TOOL_PATTERN,
8
+ buildCommandManifest,
9
+ buildReminder,
10
+ checkSymbolBodySize,
11
+ diagnoseSqlFailure,
12
+ flattenCommandNames,
13
+ isIndexedProject,
14
+ readStdinJson
15
+ } from "./token-goat-chunk-GFBSN7BD.mjs";
16
+ import {
17
+ commentSyntaxFor,
18
+ foldDetail,
19
+ mergeFolds,
20
+ planBodyFolds,
21
+ planCommentFolds,
22
+ planProseFolds
23
+ } from "./token-goat-chunk-5RG5SBL6.mjs";
24
+ import {
25
+ HOOK_EVENTS
26
+ } from "./token-goat-chunk-KKUFXAPD.mjs";
3
27
  import {
4
28
  MAX_CAPTURE_BYTES,
5
29
  canRunPowerShell,
6
- canRunWrappedShell,
30
+ canRunWrappedShell
31
+ } from "./token-goat-chunk-7F2UBPD3.mjs";
32
+ import {
7
33
  clipToDeliveryCap,
8
34
  deliveredOutputBytes
9
- } from "./token-goat-chunk-F7QZRIWM.mjs";
35
+ } from "./token-goat-chunk-54K3EUV3.mjs";
36
+ import {
37
+ DEFAULT_RECONCILE_BUDGET_MS,
38
+ isReconcileClean,
39
+ reconcileProject
40
+ } from "./token-goat-chunk-7BXSNBY5.mjs";
41
+ import {
42
+ readCopilotHooksOwners
43
+ } from "./token-goat-chunk-G5PJ7AGM.mjs";
44
+ import {
45
+ matchesAllowPattern,
46
+ matchesDenyPattern,
47
+ metadataEndpointRefusal,
48
+ recordKnownRootThrottled,
49
+ resolveLineRegions
50
+ } from "./token-goat-chunk-YARZFCZS.mjs";
51
+ import {
52
+ SLICE_ESTIMATE_SCAN_CAP_BYTES,
53
+ applyHintTracking,
54
+ buildDeltaCapsule,
55
+ buildProjectMap,
56
+ classifyBashHint,
57
+ classifyEditHint,
58
+ classifyReadHint,
59
+ compactPathFor,
60
+ countTextLines,
61
+ formatProjectMap,
62
+ formatShrinkSummary,
63
+ harnessNumbersReadContent,
64
+ headingTreeParts,
65
+ hintTarget,
66
+ imageQualifiesForShrink,
67
+ indexMatchesDisk,
68
+ isDiffableSource,
69
+ isSessionArtifactFile,
70
+ isSourceExtension,
71
+ isTruncatedReadDelivery,
72
+ isVirtualIndexedPath,
73
+ logSuppressedDetection,
74
+ markCompactStale,
75
+ meetsSavingsFloor,
76
+ numberedRenderBytes,
77
+ parseReadDelivery,
78
+ probeImageMeta,
79
+ quietContextOutput,
80
+ readEvidenceFileText,
81
+ readRequestedSliceWindow,
82
+ readStartLine,
83
+ readWindowFromDisk,
84
+ recordActualSlice,
85
+ recordEvidence,
86
+ relPathWithinRoot,
87
+ settleSelfScoredHints,
88
+ sharpenRepeatedDeny,
89
+ shrinkImage,
90
+ sliceCommand,
91
+ sliceForPath,
92
+ store,
93
+ uncorrelatedHint,
94
+ visionTokens,
95
+ visionTokensSaved
96
+ } from "./token-goat-chunk-FBNQNSJK.mjs";
10
97
  import {
11
- GH_VIEW_BATCH_HINT_KEY,
12
98
  SEARCH_COMMAND_RE,
99
+ classifyToolError
100
+ } from "./token-goat-chunk-PEPTHRFF.mjs";
101
+ import {
102
+ ORIG_HEAD_ELIGIBLE_GIT_RE,
103
+ ORIG_HEAD_REFLOG_MSG_RE,
104
+ PIPELINE_PASSTHROUGH_HEADS,
105
+ bashRecallKey,
106
+ cachedRunCoversCommand,
107
+ cdPrefixCwd,
108
+ commandRunDir,
109
+ deliveredLineNumbers,
110
+ detectUnbalancedShellSyntax,
111
+ enqueueNonHeadMovingRewrites,
112
+ extractCommand,
113
+ gitRepoRoot,
114
+ hasTestRunScopeOrBudget,
115
+ isDirectTestRunnerCommand,
116
+ isFullRecallCommand,
117
+ isHeadMovingGitCommand,
118
+ isRecallCommand,
119
+ isTokenGoatOwnCommand,
120
+ isWholeFileDump,
121
+ pipelineDivergenceNote,
122
+ pipelineShapeFilter,
123
+ pureFileReadPath,
124
+ resolveBashCall,
125
+ resolveCdHintPath,
126
+ stripCdPrefix,
127
+ stripCommandPrefix,
128
+ stripSubshellGroup,
129
+ stripTrailingStderrRedirect,
130
+ unwrapCompressCommand
131
+ } from "./token-goat-chunk-JTQQGXZH.mjs";
132
+ import {
133
+ fenceUntrusted,
134
+ fenceUntrustedSpans,
135
+ fenceWithMatches,
136
+ scanAndRecord
137
+ } from "./token-goat-chunk-67TQJ76B.mjs";
138
+ import {
139
+ appendDirtyPath,
140
+ enqueueDirtyPathSafe
141
+ } from "./token-goat-chunk-H2PHHGM5.mjs";
142
+ import {
143
+ registerHook,
144
+ runHook,
145
+ serializeOutput,
146
+ sessionStateKey
147
+ } from "./token-goat-chunk-P4QMM4WK.mjs";
148
+ import {
149
+ ensureWorkerAlive
150
+ } from "./token-goat-chunk-5KDMCMF2.mjs";
151
+ import {
152
+ GH_VIEW_BATCH_HINT_KEY,
13
153
  buildGhViewBatchAdvisory,
14
154
  buildRecallHint,
15
- classifyToolError,
16
- commandOptsOutOfCompression,
155
+ classifyCatPath,
156
+ compressionOptedOut,
17
157
  extractCatFile,
18
158
  extractCatFilesMulti,
19
159
  extractCatJsonPipe,
@@ -49,7 +189,6 @@ import {
49
189
  isCompressibleSingleCommand,
50
190
  isCurlGetCommand,
51
191
  isReadOnlyGhApi,
52
- isTscCommand,
53
192
  leadingLinesHint,
54
193
  sedOverlapHint,
55
194
  sedRangeHint,
@@ -60,142 +199,112 @@ import {
60
199
  surgicalHintFor,
61
200
  surgicalHintForConfigDoc,
62
201
  taskOutputIsJsonlTranscript
63
- } from "./token-goat-chunk-I7CB3X4K.mjs";
202
+ } from "./token-goat-chunk-APGZXSPS.mjs";
64
203
  import {
65
- DEFAULT_RECONCILE_BUDGET_MS,
66
- HOOK_EVENTS,
67
- MCP_TOOL_PATTERN,
68
- RECALL_COMMAND,
69
- buildCommandManifest,
70
- checkSymbolBodySize,
71
- diagnoseSqlFailure,
72
- flattenCommandNames,
73
- getWebOutput,
74
- isReconcileClean,
75
- readStdinJson,
76
- reconcileProject,
77
- storeWebOutput
78
- } from "./token-goat-chunk-VE27HPIF.mjs";
204
+ compressOutput,
205
+ detectFromCommand,
206
+ filterByName,
207
+ isRewriteWorthwhile,
208
+ looksLikePytestOutput,
209
+ pytestFilter,
210
+ resolveMinNetSavingsBytes
211
+ } from "./token-goat-chunk-COBQGL5S.mjs";
79
212
  import {
80
- OUTLINE_MAX_REPLACEMENT_RATIO,
81
- OUTLINE_MIN_HEADINGS,
82
- appendDirtyPath,
83
- applyHintTracking,
84
- buildDeltaCapsule,
85
- buildProjectMap,
86
- classifyBashHint,
87
- classifyEditHint,
88
- compactPathFor,
89
- enqueueDirtyPathSafe,
90
- ensureWorkerAlive,
91
- fenceUntrusted,
92
- fenceUntrustedSpans,
93
- fenceWithMatches,
94
- foldDelivery,
95
- foldDetail,
96
- foldingEnabled,
97
- formatProjectMap,
98
- formatShrinkSummary,
99
- hintTarget,
100
- imageQualifiesForShrink,
101
- indexMatchesDisk,
102
- indexServedBody,
103
- isStructuralRewriteAccepted,
104
- logSuppressedDetection,
105
- markCompactStale,
106
- matchesAllowPattern,
107
- matchesDenyPattern,
108
- meetsSavingsFloor,
109
- metadataEndpointRefusal,
110
- planMarkdownOutline,
111
- planServedElisions,
112
- planSourceSkeleton,
113
- probeImageMeta,
114
- recordKnownRootThrottled,
115
- registerHook,
116
- resolveLineRegions,
117
- runHook,
118
- scanAndRecord,
119
- serializeOutput,
120
- servedRunNotice,
121
- sessionStateKey,
122
- settleSelfScoredHints,
123
- sharpenRepeatedDeny,
124
- shrinkImage,
125
- sliceCommand,
126
- sliceForPath,
127
- uncorrelatedHint,
128
- visionTokens,
129
- visionTokensSaved
130
- } from "./token-goat-chunk-6Z5ULNE5.mjs";
213
+ commandPathIsTouchable,
214
+ docNavigation,
215
+ docSectionHint,
216
+ leadWithCommand,
217
+ preToolPathDeclined,
218
+ stripUnsafeSuggestions,
219
+ vscodePathAllowed
220
+ } from "./token-goat-chunk-ATCPY7EQ.mjs";
131
221
  import {
132
222
  BODY_FIRST_TOOL_RESPONSE_KEYS,
133
223
  OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
134
- commandPathIsTouchable,
135
224
  contextOutput,
225
+ copilotCliUserRoot,
136
226
  denyOutput,
137
227
  emitRewrite,
138
228
  emitRewriteIfChanged,
229
+ emitRewriteWithContext,
139
230
  estimateResultCount,
140
231
  extractToolResponseField,
141
232
  extractToolResultText,
142
233
  getCwd,
143
234
  getFilePath,
235
+ getFilePaths,
144
236
  getToolInput,
145
237
  getToolName,
146
238
  getTranscriptPath,
147
239
  isMcpErrorResponse,
148
240
  isTextualContentBlock,
149
- leadWithCommand,
150
241
  makeDedupHintHandlers,
151
242
  mcpContentBlocks,
152
243
  normalizePayload,
153
244
  passOutput,
154
- preToolPathDeclined,
155
- stripUnsafeSuggestions,
245
+ resolveEventPath,
156
246
  structuredResultCount,
157
- vscodePathAllowed,
158
247
  withPreservedBlocks
159
- } from "./token-goat-chunk-IZXKVT4A.mjs";
160
- import {
161
- compressOutput,
162
- detectFromCommand,
163
- filterByName,
164
- isRewriteWorthwhile,
165
- looksLikePytestOutput,
166
- pytestFilter,
167
- resolveMinNetSavingsBytes
168
- } from "./token-goat-chunk-RJH64KQH.mjs";
169
- import {
170
- projectTranscriptsDir
171
- } from "./token-goat-chunk-6DQNNFJ6.mjs";
172
- import {
173
- readCopilotHooksOwners
174
- } from "./token-goat-chunk-MMLMALWK.mjs";
248
+ } from "./token-goat-chunk-VPBBTSJR.mjs";
175
249
  import {
176
250
  BUDGET_ESCALATION_MARKER,
177
251
  MANIFEST_PREAMBLE,
178
252
  MANIFEST_RECOVERY_PREAMBLE,
179
253
  buildManifest,
254
+ getWebOutput,
180
255
  manifestPrintedPaths,
181
256
  projectNotesFor,
257
+ recordCompactDroppedPaths,
258
+ storeWebOutput,
182
259
  summaryBudgetDirective
183
- } from "./token-goat-chunk-L7SDVGBT.mjs";
260
+ } from "./token-goat-chunk-Q5VTGRH7.mjs";
184
261
  import {
262
+ COMPACTED_AT_SUFFIX,
185
263
  extractCompactFromMarker,
186
264
  hasSessionOutput,
187
265
  incrementSkillHit,
188
266
  installedSkillPath,
189
267
  loadSessionState,
268
+ safeSkillName,
190
269
  saveSessionState,
191
270
  sessionOutputBodyBytes,
192
271
  sessionSidecarPath,
193
- storeOutput
194
- } from "./token-goat-chunk-6CCQDAYY.mjs";
272
+ storeOutput,
273
+ writeSessionManifest
274
+ } from "./token-goat-chunk-Y776AENC.mjs";
275
+ import {
276
+ accumulateResidentLines,
277
+ lineMayCarryResidentSignal,
278
+ readTranscriptTail,
279
+ repeatedSkillBodyHint,
280
+ summarizeResidentContext,
281
+ taskListPruneHint
282
+ } from "./token-goat-chunk-SJHWVNDQ.mjs";
283
+ import {
284
+ BASH_OUTPUT_SUBDIR,
285
+ bashOutputIdSync,
286
+ commandHash,
287
+ getBashOutput,
288
+ getMonitoringRecallHint,
289
+ indexRecallEntry,
290
+ isBashEntryStale,
291
+ isBuildCommand,
292
+ isScopedGitStatusOrDiffStatCommand,
293
+ isTestRunnerCommand,
294
+ storeBashOutput,
295
+ storeBashOutputSync,
296
+ summarizeOutputDelta
297
+ } from "./token-goat-chunk-AFLYLHDH.mjs";
298
+ import {
299
+ claudeConfigDir,
300
+ projectTranscriptsDir
301
+ } from "./token-goat-chunk-BPGCUMR7.mjs";
195
302
  import {
196
303
  GENERIC_SERVED_OUTPUT_KEY,
197
304
  clearCurlDownload,
305
+ exportSessionState,
198
306
  getBashOutputId,
307
+ getCompactedAt,
199
308
  getCurlDownloadPath,
200
309
  getFileLineRanges,
201
310
  getFileServedOutputs,
@@ -210,6 +319,7 @@ import {
210
319
  markHintShown,
211
320
  recordBashOutput,
212
321
  recordBashRerun,
322
+ recordBashStartCwd,
213
323
  recordCliRead,
214
324
  recordCurlDownload,
215
325
  recordFileEdit,
@@ -227,108 +337,114 @@ import {
227
337
  resetFileLineRanges,
228
338
  setLastTabContext,
229
339
  setTranscriptPath,
340
+ takeBashStartCwd,
230
341
  takePendingLargeFileHint,
231
342
  wasCliReadThisSession,
232
343
  wasFileReadThisSession,
233
344
  wasHintShown
234
- } from "./token-goat-chunk-GGLEE35R.mjs";
235
- import {
236
- accumulateResidentLines,
237
- claudeConfigDir,
238
- lineMayCarryResidentSignal,
239
- readTranscriptTail,
240
- repeatedSkillBodyHint,
241
- summarizeResidentContext,
242
- taskListPruneHint
243
- } from "./token-goat-chunk-MAMQPC43.mjs";
244
- import {
245
- BASH_OUTPUT_SUBDIR,
246
- bashOutputIdSync,
247
- commandHash,
248
- getBashOutput,
249
- getMonitoringRecallHint,
250
- indexRecallEntry,
251
- isBashEntryStale,
252
- isBuildCommand,
253
- isScopedGitStatusOrDiffStatCommand,
254
- isTestRunnerCommand,
255
- storeBashOutput,
256
- summarizeOutputDelta
257
- } from "./token-goat-chunk-HXNAC4ZF.mjs";
258
- import {
259
- storeBlob
260
- } from "./token-goat-chunk-BJBRGPBX.mjs";
345
+ } from "./token-goat-chunk-C63JQV5K.mjs";
261
346
  import {
262
347
  estimateTokens
263
- } from "./token-goat-chunk-4JMWPWCS.mjs";
264
- import {
265
- dedupeConsecutive,
266
- hasBareBackground,
267
- hasBareBackgroundOrNewline,
268
- hasUnquotedOperator,
269
- pathStem,
270
- shlexSplit,
271
- splitOwnTrailingNotices
272
- } from "./token-goat-chunk-4RN64ZND.mjs";
273
- import {
274
- recordStat,
275
- redactSecrets,
276
- savedTokensFromBytes,
277
- stripAnsiEscapes
278
- } from "./token-goat-chunk-MVSWBNMD.mjs";
348
+ } from "./token-goat-chunk-JB6QN6VF.mjs";
279
349
  import {
280
350
  extractMarkdownHeadings,
281
- formatHeadingTreeParts
282
- } from "./token-goat-chunk-D5ICEPSB.mjs";
351
+ findContainingSection,
352
+ formatHeadingTreeParts,
353
+ isImagePath,
354
+ isTreeSitterAvailable,
355
+ parseSourceSymbolsTreeSitterOnly,
356
+ parserFingerprintForLanguage
357
+ } from "./token-goat-chunk-RHXKCZX3.mjs";
283
358
  import {
284
- countSymbols,
359
+ getFileEntry,
285
360
  querySymbols
286
- } from "./token-goat-chunk-HUDC5GS5.mjs";
361
+ } from "./token-goat-chunk-HYBFA5X3.mjs";
362
+ import {
363
+ storeBlob
364
+ } from "./token-goat-chunk-ARDNEC2U.mjs";
365
+ import {
366
+ HOOK_PROBE_ENV,
367
+ claimFirstSavingsReceipt,
368
+ recordStat,
369
+ savedTokensFromBytes
370
+ } from "./token-goat-chunk-3Z5S4KGM.mjs";
287
371
  import {
288
372
  buildLineIndex,
289
373
  detectLanguage,
290
374
  getDb,
291
375
  languageHasFlag,
292
376
  offsetToLine
293
- } from "./token-goat-chunk-M56BWPBN.mjs";
377
+ } from "./token-goat-chunk-VL3YIFHZ.mjs";
378
+ import {
379
+ hasPreciseSecret,
380
+ redactSecrets,
381
+ redactSerializedJson
382
+ } from "./token-goat-chunk-4FVIAHSI.mjs";
294
383
  import {
295
- detectHarness,
296
- envBool,
297
- envInt,
298
384
  findProject,
299
- getHarnessName,
385
+ fingerprintFile,
300
386
  isUnderSystemTemp,
301
387
  loadConfig,
388
+ makeProjectAt,
302
389
  shortFingerprint
303
- } from "./token-goat-chunk-CC2PYJM7.mjs";
390
+ } from "./token-goat-chunk-7IN2MKDS.mjs";
391
+ import {
392
+ detectHarness,
393
+ getHarnessName,
394
+ relaySeededSessionId,
395
+ setRelaySeededSessionId
396
+ } from "./token-goat-chunk-U4N3BN2W.mjs";
397
+ import {
398
+ envBool,
399
+ envInt
400
+ } from "./token-goat-chunk-IVE6U7KV.mjs";
304
401
  import {
305
- ENV_KEYS,
306
402
  IDENTICAL_READ_MIN_BODY_BYTES,
307
403
  PER_FILE_COUNTERFACTUAL_CEILING,
308
- VERSION,
309
404
  atomicWriteText,
310
405
  containsLineRun,
311
406
  countNoun,
312
- dataDir,
313
407
  ensureDirSync,
314
408
  extractErrorMessage,
315
- foldPath,
316
- globalDbPath,
317
409
  hasCreatedConfig,
318
- isInsideRoot,
319
410
  runGit,
411
+ statSize,
320
412
  toKB
321
- } from "./token-goat-chunk-L45ZQRT7.mjs";
413
+ } from "./token-goat-chunk-GOLEOXZP.mjs";
414
+ import {
415
+ foldPath,
416
+ isInsideRoot
417
+ } from "./token-goat-chunk-AM5V6GSX.mjs";
322
418
  import {
323
419
  UNTRUSTED_TOOL_TAG,
420
+ dirAtIndexKey,
324
421
  displaySafePath,
325
422
  displaySafeText,
423
+ fenceNumberedFileContent,
326
424
  fenceUntrustedFileContent,
425
+ hostPathOfIndexKey,
426
+ hostPathOfTypedPath,
427
+ isFileAtIndexKey,
327
428
  neutralizeSpokenMarkers,
328
429
  normalizePath,
329
430
  resolveIndexPath,
330
431
  toDisplayPath
331
- } from "./token-goat-chunk-NMTKNYGF.mjs";
432
+ } from "./token-goat-chunk-FNFBKLRD.mjs";
433
+ import {
434
+ ENV_KEYS,
435
+ VERSION,
436
+ dataDir,
437
+ globalDbPath
438
+ } from "./token-goat-chunk-VES7EL7O.mjs";
439
+ import {
440
+ dedupeConsecutive,
441
+ hasBareBackground,
442
+ pathStem,
443
+ splitOwnTrailingNotices
444
+ } from "./token-goat-chunk-KWPNJBDH.mjs";
445
+ import {
446
+ stripAnsiEscapes
447
+ } from "./token-goat-chunk-HAVAOB3S.mjs";
332
448
  import {
333
449
  init_define_import_meta_env
334
450
  } from "./token-goat-chunk-A37V4PBF.mjs";
@@ -340,535 +456,37 @@ init_define_import_meta_env();
340
456
  init_define_import_meta_env();
341
457
  import { readFileSync, writeFileSync } from "node:fs";
342
458
  import { dirname } from "node:path";
343
-
344
- // src/hooks_bash_commands.ts
345
- init_define_import_meta_env();
346
- import { statSync } from "node:fs";
347
- var CD_PREFIX_RE = /^(?:cd\s+(?:"[^"]*"|'[^']*'|\S+)[ \t]*(?:&&|;|\r?\n)\s*)+/;
348
- function stripCdPrefix(cmd) {
349
- const stripped = cmd.replace(CD_PREFIX_RE, "");
350
- return stripped.trim() || cmd;
351
- }
352
- function stripCommandPrefix(rawCmd) {
353
- return stripLeadingAssignments(stripCdPrefix(rawCmd));
354
- }
355
- function stripTrailingStderrRedirect(cmd) {
356
- const stripped = cmd.replace(/\s*2>&1\s*$/, "");
357
- return stripped.trim() || cmd;
358
- }
359
- function hasTestRunScopeOrBudget(cmd) {
360
- const hasTimeout = /(?:^|\s)(?:timeout(?:\.exe)?\s+\S+|--(?:test)?timeout(?:=|\s)\S+)/i.test(cmd);
361
- const hasSelector = /(?:\s::\S+|\s(?:-k|-t|-run|--(?:testNamePattern|last-failed|test|project))(?:=|\s)|\b(?:tests?|spec)\S*\.(?:[cm]?[jt]sx?|py|go|rs)\b|(?:^|\s)\.\/\S+)/i.test(cmd);
362
- return hasTimeout || hasSelector;
363
- }
364
- function isDirectTestRunnerCommand(cmd) {
365
- return /^(?:pytest|(?:npx\s+)?(?:jest|vitest)|go\s+test|cargo\s+test)\b/i.test(cmd);
366
- }
367
- function extractCdPrefixDirs(rawCmd) {
368
- const prefixMatch = rawCmd.match(CD_PREFIX_RE);
369
- if (prefixMatch === null) return [];
370
- const dirs = [];
371
- const segmentPattern = /cd\s+(?:"([^"]*)"|'([^']*)'|(\S+))[ \t]*(?:&&|;|\r?\n)/g;
372
- let match;
373
- while ((match = segmentPattern.exec(prefixMatch[0])) !== null) {
374
- const dir = match[1] ?? match[2] ?? match[3];
375
- if (dir !== void 0) dirs.push(dir);
376
- }
377
- return dirs;
378
- }
379
- function resolveCdHintPath(rawCmd, filePath, cwd) {
380
- if (extractCdPrefixDirs(rawCmd).length === 0) return filePath;
381
- return resolveIndexPath(filePath, cdPrefixCwd(rawCmd, cwd));
382
- }
383
- function cdPrefixCwd(rawCmd, cwd) {
384
- let dir = cwd;
385
- for (const target of extractCdPrefixDirs(rawCmd)) dir = resolveIndexPath(target, dir);
386
- return dir;
387
- }
388
- function stripOutputPipeline(cmd) {
389
- let inSingle = false;
390
- let inDouble = false;
391
- let cut = cmd.length;
392
- let backslashes = 0;
393
- for (let i = 0; i < cmd.length; i++) {
394
- const ch = cmd[i];
395
- if (ch === "\\") {
396
- backslashes++;
397
- continue;
398
- }
399
- const escaped = backslashes % 2 === 1;
400
- backslashes = 0;
401
- if (ch === "'" && !inDouble) {
402
- if (!escaped) inSingle = !inSingle;
403
- } else if (ch === '"' && !inSingle) {
404
- if (!escaped) inDouble = !inDouble;
405
- } else if (ch === "|" && !inSingle && !inDouble) {
406
- if (cmd[i + 1] === "|") {
407
- i++;
408
- continue;
409
- }
410
- cut = i;
411
- break;
459
+ var SERIAL_GAP_MS = 1e3;
460
+ var SERIAL_READS_BEFORE_HINT = 3;
461
+ var ZERO_HIT_SEARCHES_BEFORE_BRAKE = 3;
462
+ var MAX_BATCH_HINTS_PER_SESSION = 2;
463
+ var MAX_SEARCH_BRAKES_PER_SESSION = 2;
464
+ var STREAK_SUFFIX = ".call-streak";
465
+ var TG_SEARCH_RE = /^(?:token-goat|tg)\s+(?:answer|semantic|symbol|refs|locate)\b/;
466
+ var TG_MEANING_SEARCH_RE = /^(?:token-goat|tg)\s+(?:answer|semantic)\b/;
467
+ var TG_READ_RE = /^(?:token-goat|tg)\s+(?:read|section|outline|skeleton|brief)\b/;
468
+ var WRITE_REDIRECT_RE = /\d?>>?(?!\s*(?:&\d|\/dev\/null\b))/;
469
+ function freshState() {
470
+ return { promptId: "", lastReadPostAt: 0, inFlight: false, serialRun: 0, batchShownInRun: false, batchShown: 0, batchVerdict: "none", zeroHits: 0, brakeShownInRun: false, brakeShown: 0, brakeVerdict: false };
471
+ }
472
+ function readState(target) {
473
+ const state = freshState();
474
+ try {
475
+ const parsed = JSON.parse(readFileSync(target, "utf8"));
476
+ if (parsed === null || typeof parsed !== "object") return state;
477
+ const p = parsed;
478
+ for (const key of Object.keys(state)) {
479
+ if (typeof p[key] === typeof state[key]) state[key] = p[key];
412
480
  }
481
+ if (!["none", "next_turn", "second_call"].includes(state.batchVerdict)) state.batchVerdict = "none";
482
+ } catch {
413
483
  }
414
- let base = cmd.slice(0, cut);
415
- let prev;
416
- do {
417
- prev = base;
418
- const masked = base.replace(/"((?:[^"\\]|\\.)*)"/g, (_m, inner) => '"' + " ".repeat(inner.length) + '"').replace(/'([^']*)'/g, (_m, inner) => "'" + " ".repeat(inner.length) + "'");
419
- const newMasked = masked.replace(/\s*(?:[0-9]*>&[0-9]+|[0-9&]*>>?\s*(?:"[^"]*"|'[^']*'|[^\s|]+))\s*$/, "");
420
- if (newMasked.length < masked.length) {
421
- base = base.slice(0, newMasked.length);
422
- }
423
- } while (base !== prev);
424
- return base.trim();
425
- }
426
- function pipelineDivergenceNote(cmd, entryCommand) {
427
- if (entryCommand === cmd) return "";
428
- const preview = entryCommand.length > 60 ? entryCommand.slice(0, 57) + "..." : entryCommand;
429
- return " (cached from a differently-piped run, `" + preview + "` \u2014 verify it covers what you need before trusting it)";
430
- }
431
- function extractCommand(event) {
432
- const cmd = event.toolInput["command"];
433
- return typeof cmd === "string" && cmd.trim() !== "" ? cmd.trim() : void 0;
434
- }
435
- var HEAD_MOVING_GIT_RE = /^\s*git\s+(?:checkout|switch|pull|merge|rebase|reset|cherry-pick)\b/i;
436
- var PATH_SCOPED_CHECKOUT_RE = /^\s*git\s+checkout\s+(?:\S.*?\s)?--(?:\s|$)/i;
437
- function isHeadMovingGitCommand(cmd) {
438
- if (!HEAD_MOVING_GIT_RE.test(cmd)) return false;
439
- if (PATH_SCOPED_CHECKOUT_RE.test(cmd)) return false;
440
- return true;
484
+ return state;
441
485
  }
442
- var ORIG_HEAD_ELIGIBLE_GIT_RE = /^\s*git\s+(?:pull|merge|rebase)\b/i;
443
- var ORIG_HEAD_REFLOG_MSG_RE = /^(merge\s|rebase\s\(|pull\s)/i;
444
- var GIT_RESTORE_RE = /^\s*git\s+restore\b/i;
445
- var GIT_RESTORE_STAGED_ONLY_RE = /(?:^|\s)(?:--staged|-S)(?:\s|$)/;
446
- var GIT_STASH_APPLY_RE = /^\s*git\s+stash\s+(?:pop|apply)\b/i;
447
- var PATCH_APPLY_SEGMENT_RE = /^\s*(?:\S*[/\\])?(?:git\s+apply|patch)\b/i;
448
- var PATCH_APPLY_INSPECT_ONLY_RE = /(?:^|\s)--(?:check|stat|numstat|summary|dry-run)(?:\s|=|$)/i;
449
- function isFormatterWriteSegment(segment) {
450
- return /(?:^|\s|[/\\])(?:prettier|eslint)\b/i.test(segment) && /(?:^|\s)--(?:write|fix)(?:\s|$)/i.test(segment);
451
- }
452
- function safeShlexSplit(segment) {
486
+ function writeState(target, state) {
453
487
  try {
454
- return shlexSplit(segment);
455
- } catch {
456
- return null;
457
- }
458
- }
459
- function segmentCommandIs(segment, name2) {
460
- return new RegExp("^\\s*(?:\\S*[/\\\\])?" + name2 + "(?:\\.exe)?\\b", "i").test(segment);
461
- }
462
- function extractSedInPlaceFiles(segment) {
463
- if (!segmentCommandIs(segment, "sed")) return [];
464
- const tokens = safeShlexSplit(segment);
465
- if (tokens === null) return [];
466
- const rest = tokens.slice(1);
467
- const inPlaceIdx = rest.findIndex((t) => t === "--in-place" || t.startsWith("--in-place=") || /^-[a-zA-Z]*i/.test(t));
468
- if (inPlaceIdx === -1) return [];
469
- const files = [];
470
- let scriptTaken = rest.some((t) => t === "-e" || t === "-f" || t.startsWith("--expression") || t.startsWith("--file"));
471
- for (let i = 0; i < rest.length; i++) {
472
- const t = rest[i];
473
- if (i === inPlaceIdx) {
474
- if (t === "-i" && rest[i + 1] === "") i++;
475
- continue;
476
- }
477
- if (t === "-e" || t === "-f" || t === "--expression" || t === "--file") {
478
- i++;
479
- continue;
480
- }
481
- if (t.length > 1 && t.startsWith("-")) continue;
482
- if (!scriptTaken) {
483
- scriptTaken = true;
484
- continue;
485
- }
486
- files.push(t);
487
- }
488
- return files;
489
- }
490
- function extractTeeFiles(segment) {
491
- if (!segmentCommandIs(segment, "tee")) return [];
492
- const tokens = safeShlexSplit(segment);
493
- if (tokens === null) return [];
494
- return tokens.slice(1).filter((t) => t.length > 0 && !t.startsWith("-"));
495
- }
496
- function extractRedirectTargets(segment) {
497
- const targets = [];
498
- const re = /(?:^|\s)[0-9]?>>?\s*("[^"]*"|'[^']*'|[^\s|&<>]+)/g;
499
- let m;
500
- while ((m = re.exec(segment)) !== null) {
501
- const raw = m[1];
502
- targets.push(raw.length >= 2 && (raw[0] === '"' || raw[0] === "'") ? raw.slice(1, -1) : raw);
503
- }
504
- return targets;
505
- }
506
- function extractGitRestorePathspecs(cmd) {
507
- const m = /^\s*git\s+restore\b(.*)$/i.exec(cmd);
508
- if (m === null) return [];
509
- const tokens = safeShlexSplit(m[1] ?? "");
510
- if (tokens === null) return [];
511
- const dashDash = tokens.indexOf("--");
512
- if (dashDash !== -1) return tokens.slice(dashDash + 1);
513
- const specs = [];
514
- let skipNext = false;
515
- for (const t of tokens) {
516
- if (skipNext) {
517
- skipNext = false;
518
- continue;
519
- }
520
- if (t === "-s" || t === "--source") {
521
- skipNext = true;
522
- continue;
523
- }
524
- if (t.startsWith("-")) continue;
525
- specs.push(t);
526
- }
527
- return specs;
528
- }
529
- function gitRepoRoot(gitDir) {
530
- const toplevel = runGit(["rev-parse", "--show-toplevel"], { cwd: gitDir, timeoutMs: 5e3 });
531
- return toplevel.exitCode === 0 && toplevel.stdout.trim() !== "" ? toplevel.stdout.trim() : gitDir;
532
- }
533
- function expandGitPathspecs(specs, gitDir) {
534
- if (specs.length === 0) return [];
535
- const listed = runGit(["ls-files", "--full-name", "--", ...specs], { cwd: gitDir, timeoutMs: 5e3 });
536
- if (listed.exitCode !== 0) return [];
537
- const repoRoot = gitRepoRoot(gitDir);
538
- return listed.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0).map((rel) => resolveIndexPath(rel, repoRoot));
539
- }
540
- function workingTreeStatusPaths(gitDir) {
541
- const status = runGit(["status", "--porcelain"], { cwd: gitDir, timeoutMs: 5e3 });
542
- if (status.exitCode !== 0) return [];
543
- const repoRoot = gitRepoRoot(gitDir);
544
- const out = [];
545
- for (const line of status.stdout.split("\n")) {
546
- if (line.length < 4) continue;
547
- let rel = line.slice(3).trim();
548
- if (rel.length === 0) continue;
549
- const arrow = rel.lastIndexOf(" -> ");
550
- if (arrow !== -1) rel = rel.slice(arrow + 4);
551
- if (rel.startsWith('"')) {
552
- try {
553
- rel = JSON.parse(rel);
554
- } catch {
555
- continue;
556
- }
557
- }
558
- out.push(resolveIndexPath(rel, repoRoot));
559
- }
560
- return out;
561
- }
562
- function enqueueRewrittenPath(absPath) {
563
- if (/(?:^|[/\\])(?:NUL|nul)$/.test(absPath) || absPath.replace(/\\/g, "/").endsWith("/dev/null")) return;
564
- try {
565
- if (!statSync(absPath).isFile()) return;
566
- } catch {
567
- return;
568
- }
569
- enqueueDirtyPathSafe(absPath, { alreadyResolved: true });
570
- }
571
- function enqueueNonHeadMovingRewrites(cmd, rawCmd, cwd) {
572
- const atCwd = (f) => resolveIndexPath(resolveCdHintPath(rawCmd, f, cwd), cwd);
573
- const paths = [];
574
- let needsStatusSweep = GIT_STASH_APPLY_RE.test(cmd);
575
- if (GIT_RESTORE_RE.test(cmd) && !GIT_RESTORE_STAGED_ONLY_RE.test(cmd)) {
576
- paths.push(...expandGitPathspecs(extractGitRestorePathspecs(cmd), cwd));
577
- }
578
- for (const segment of splitShellSegments(cmd)) {
579
- if (PATCH_APPLY_SEGMENT_RE.test(segment) && !PATCH_APPLY_INSPECT_ONLY_RE.test(segment)) needsStatusSweep = true;
580
- if (isFormatterWriteSegment(segment)) needsStatusSweep = true;
581
- for (const f of extractSedInPlaceFiles(segment)) paths.push(atCwd(f));
582
- for (const f of extractTeeFiles(segment)) paths.push(atCwd(f));
583
- for (const f of extractRedirectTargets(segment)) paths.push(atCwd(f));
584
- }
585
- if (needsStatusSweep) paths.push(...workingTreeStatusPaths(cwd));
586
- for (const p of paths) enqueueRewrittenPath(p);
587
- }
588
- var PIPELINE_PASSTHROUGH_HEADS = /* @__PURE__ */ new Set(["head", "tail", "cat", "tee", "less", "more"]);
589
- var CI_FILTER_NAMES = /^(?:generic-ci|jest|vitest|pytest|go_test|cargo_test|cargo|go|make|cmake|gradle|maven|dotnet|turbo|nx|lerna|webpack|eslint|ruff|clippy|flake8|mypy|prettier|tsc)$/;
590
- function isCiBuildTestSegment(cleaned, cwd) {
591
- if (isTestRunnerCommand(cleaned) || isBuildCommand(cleaned) || isTscCommand(cleaned)) return true;
592
- if (/^\s*(npm|pnpm|yarn|bun)\s+(run\s+)?(test|build|lint|typecheck|check|ci|guards)\b/i.test(cleaned)) return true;
593
- if (/^\s*(cargo|go|dotnet|make|gradle|mvn|pytest|vitest|jest|eslint|ruff)\b/i.test(cleaned)) return true;
594
- const detected = detectFromCommand(cleaned, cwd ?? void 0);
595
- if (detected !== null && CI_FILTER_NAMES.test(detected.filter.name)) return true;
596
- return false;
597
- }
598
- function pipelineShapeFilter(cmd, cwd) {
599
- if (hasUnquotedOperator(cmd, ["&&", "||", ";"])) {
600
- const forSplit2 = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
601
- if (hasBareBackgroundOrNewline(forSplit2)) return null;
602
- const segments2 = splitShellSegments(forSplit2);
603
- if (segments2.length >= 2) {
604
- let recognizedCiCount = 0;
605
- for (const segment of segments2) {
606
- const cleaned = stripOutputPipeline(segment.trim());
607
- if (cleaned.length === 0) continue;
608
- if (isCiBuildTestSegment(cleaned, cwd)) {
609
- recognizedCiCount++;
610
- }
611
- }
612
- if (recognizedCiCount > 0) {
613
- const ci = filterByName("generic-ci");
614
- return ci === null ? null : { filter: ci, argv: [] };
615
- }
616
- }
617
- return null;
618
- }
619
- const forSplit = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
620
- if (hasBareBackgroundOrNewline(forSplit)) return null;
621
- const segments = splitShellSegments(forSplit);
622
- if (segments.length < 2) return null;
623
- for (const segment of segments.slice(1)) {
624
- const head = safeShlexSplit(segment)?.[0];
625
- if (head === void 0) return null;
626
- if (!PIPELINE_PASSTHROUGH_HEADS.has(head.replace(/^.*[/\\]/, ""))) return null;
627
- }
628
- const detected = detectFromCommand(stripOutputPipeline(cmd), cwd ?? void 0);
629
- return detected === null ? null : { filter: detected.filter, argv: detected.argv };
630
- }
631
- function isFullRecallCommand(cmd) {
632
- const forSplit = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
633
- if (hasBareBackgroundOrNewline(forSplit)) return false;
634
- const first2 = splitShellSegments(forSplit)[0];
635
- if (first2 === void 0 || !segmentCommandIs(first2, "token-goat")) return false;
636
- const tokens = safeShlexSplit(stripOutputPipeline(first2));
637
- if (tokens === null) return false;
638
- return Object.values(RECALL_COMMAND).includes(tokens[1] ?? "") && tokens.includes("--full");
639
- }
640
- function pureFileReadPath(cmd) {
641
- const single = extractCatFile(cmd)?.filePath ?? extractHeadFile(cmd)?.filePath ?? extractTailFile(cmd)?.filePath ?? extractLineRangeRead(cmd)?.filePath;
642
- if (single !== void 0) return single;
643
- return singleFileCompoundReadPath(cmd);
644
- }
645
- function singleFileCompoundReadPath(cmd) {
646
- const reads = extractLineRangeReadsCompound(cmd);
647
- if (reads === null || reads.length !== 1) return null;
648
- return reads[0]?.filePath ?? null;
649
- }
650
- function deliveredLineNumbers(cmd, lineCount) {
651
- const ranged = extractLineRangeRead(cmd);
652
- if (ranged !== null) {
653
- const nums = [];
654
- for (const [lo, hi] of ranged.ranges) for (let n = lo; n <= hi; n++) nums.push(n);
655
- if (nums.length === 0) return null;
656
- while (nums.length < lineCount) nums.push((nums[nums.length - 1] ?? 0) + 1);
657
- return nums;
658
- }
659
- if (extractCatFile(cmd) !== null || extractHeadFile(cmd) !== null) return Array.from({ length: lineCount }, (_, i) => i + 1);
660
- if (singleFileCompoundReadPath(cmd) !== null) return Array.from({ length: lineCount }, () => null);
661
- return null;
662
- }
663
- var WHOLE_FILE_DUMP_RE = /^(?:(?:cat|type|Get-Content|gc)|(?:head|tail)[ \t]+-n[ \t]+\d+)[ \t]+(?:"([A-Za-z0-9._\-/\\:+@ ]+)"|'([A-Za-z0-9._\-/\\:+@ ]+)'|([A-Za-z0-9._\-/\\:+@]+))(?:[ \t]+2>(?:&1|\/dev\/null))?[ \t]*$/i;
664
- function isWholeFileDump(cmd, extractedPath) {
665
- const m = WHOLE_FILE_DUMP_RE.exec(cmd);
666
- if (m === null) return false;
667
- const shaped = m[1] ?? m[2] ?? m[3];
668
- return shaped !== void 0 && shaped === extractedPath;
669
- }
670
- function unwrapCompressCommand(executed) {
671
- const t = executed.trim();
672
- if (!/^(?:token-goat|tg)\s+compress\b/.test(t)) return null;
673
- let argv;
674
- try {
675
- argv = shlexSplit(t);
676
- } catch {
677
- return null;
678
- }
679
- for (let i = 0; i + 1 < argv.length; i++) {
680
- const tok = argv[i];
681
- if (tok === "-c" || tok === "--cmd") return argv[i + 1] ?? null;
682
- if (tok === "--cmd-b64") {
683
- try {
684
- return Buffer.from(argv[i + 1] ?? "", "base64").toString("utf8");
685
- } catch {
686
- return null;
687
- }
688
- }
689
- }
690
- return null;
691
- }
692
- function detectUnbalancedShellSyntax(cmd) {
693
- let inSingle = false;
694
- let inDouble = false;
695
- let i = 0;
696
- while (i < cmd.length) {
697
- const ch = cmd[i];
698
- if (inSingle) {
699
- if (ch === "'") {
700
- inSingle = false;
701
- }
702
- i++;
703
- continue;
704
- }
705
- if (inDouble) {
706
- if (ch === "\\" && i + 1 < cmd.length) {
707
- i += 2;
708
- continue;
709
- }
710
- if (ch === '"') {
711
- inDouble = false;
712
- }
713
- i++;
714
- continue;
715
- }
716
- if (ch === "\\" && i + 1 < cmd.length) {
717
- i += 2;
718
- continue;
719
- }
720
- if (ch === "$" && cmd[i + 1] === "(" && cmd[i + 2] === "(") {
721
- let depth = 2;
722
- let j = i + 3;
723
- while (j < cmd.length && depth > 0) {
724
- if (cmd[j] === "(") depth++;
725
- else if (cmd[j] === ")") depth--;
726
- j++;
727
- }
728
- i = j;
729
- continue;
730
- }
731
- if (ch === "$" && cmd[i + 1] === "'") {
732
- let j = i + 2;
733
- let closed = false;
734
- while (j < cmd.length) {
735
- if (cmd[j] === "\\") {
736
- j += 2;
737
- continue;
738
- }
739
- if (cmd[j] === "'") {
740
- j++;
741
- closed = true;
742
- break;
743
- }
744
- j++;
745
- }
746
- if (!closed) {
747
- inSingle = true;
748
- break;
749
- }
750
- i = j;
751
- continue;
752
- }
753
- if (ch === "#" && (i === 0 || /\s/.test(cmd[i - 1] ?? ""))) {
754
- const nl2 = cmd.indexOf("\n", i);
755
- i = nl2 === -1 ? cmd.length : nl2 + 1;
756
- continue;
757
- }
758
- if (ch === "'") {
759
- inSingle = true;
760
- i++;
761
- continue;
762
- }
763
- if (ch === '"') {
764
- inDouble = true;
765
- i++;
766
- continue;
767
- }
768
- if (ch === "<" && i + 1 < cmd.length && cmd[i + 1] === "<") {
769
- if (i + 2 < cmd.length && cmd[i + 2] === "<") {
770
- i += 3;
771
- continue;
772
- }
773
- let j = i + 2;
774
- let hasIndentModifier = false;
775
- if (j < cmd.length && (cmd[j] === "-" || cmd[j] === "~")) {
776
- hasIndentModifier = true;
777
- j++;
778
- }
779
- while (j < cmd.length && cmd[j] === " ") {
780
- j++;
781
- }
782
- if (j < cmd.length) {
783
- let delimStart = j;
784
- if (cmd[j] === '"' || cmd[j] === "'") {
785
- delimStart = j + 1;
786
- const quoteChar = cmd[j];
787
- let delimEnd = delimStart;
788
- while (delimEnd < cmd.length && cmd[delimEnd] !== quoteChar) {
789
- delimEnd++;
790
- }
791
- if (delimEnd >= cmd.length) {
792
- return "an unclosed quote in a heredoc opener";
793
- }
794
- j = delimEnd + 1;
795
- } else {
796
- let delimEnd = delimStart;
797
- while (delimEnd < cmd.length && /\w/.test(cmd.charAt(delimEnd))) {
798
- delimEnd++;
799
- }
800
- j = delimEnd;
801
- }
802
- const delimiter = cmd.slice(delimStart, j).replace(/["']/g, "");
803
- if (delimiter) {
804
- let scanPos = j;
805
- let terminatorEnd = -1;
806
- while (scanPos <= cmd.length) {
807
- const nl2 = cmd.indexOf("\n", scanPos);
808
- const lineEnd = nl2 === -1 ? cmd.length : nl2;
809
- let line = cmd.slice(scanPos, lineEnd);
810
- if (line.endsWith("\r")) {
811
- line = line.slice(0, -1);
812
- }
813
- const isMatch = hasIndentModifier ? line.trim() === delimiter : line === delimiter;
814
- if (isMatch) {
815
- terminatorEnd = nl2 === -1 ? cmd.length : nl2 + 1;
816
- break;
817
- }
818
- if (nl2 === -1) break;
819
- scanPos = nl2 + 1;
820
- }
821
- if (terminatorEnd === -1) {
822
- return `an unterminated heredoc (${delimiter} never appears on its own line)`;
823
- }
824
- i = terminatorEnd;
825
- continue;
826
- }
827
- }
828
- }
829
- i++;
830
- }
831
- if (inSingle) {
832
- return "an unclosed single quote";
833
- }
834
- if (inDouble) {
835
- return "an unclosed double quote";
836
- }
837
- return null;
838
- }
839
-
840
- // src/call_streak.ts
841
- var SERIAL_GAP_MS = 1e3;
842
- var SERIAL_READS_BEFORE_HINT = 3;
843
- var ZERO_HIT_SEARCHES_BEFORE_BRAKE = 3;
844
- var MAX_BATCH_HINTS_PER_SESSION = 2;
845
- var MAX_SEARCH_BRAKES_PER_SESSION = 2;
846
- var STREAK_SUFFIX = ".call-streak";
847
- var TG_SEARCH_RE = /^(?:token-goat|tg)\s+(?:answer|semantic|symbol|refs|locate)\b/;
848
- var TG_MEANING_SEARCH_RE = /^(?:token-goat|tg)\s+(?:answer|semantic)\b/;
849
- var TG_READ_RE = /^(?:token-goat|tg)\s+(?:read|section|outline|skeleton|brief)\b/;
850
- var WRITE_REDIRECT_RE = /\d?>>?(?!\s*(?:&\d|\/dev\/null\b))/;
851
- function freshState() {
852
- return { promptId: "", lastReadPostAt: 0, inFlight: false, serialRun: 0, batchShownInRun: false, batchShown: 0, batchVerdict: "none", zeroHits: 0, brakeShownInRun: false, brakeShown: 0, brakeVerdict: false };
853
- }
854
- function readState(target) {
855
- const state = freshState();
856
- try {
857
- const parsed = JSON.parse(readFileSync(target, "utf8"));
858
- if (parsed === null || typeof parsed !== "object") return state;
859
- const p = parsed;
860
- for (const key of Object.keys(state)) {
861
- if (typeof p[key] === typeof state[key]) state[key] = p[key];
862
- }
863
- if (!["none", "next_turn", "second_call"].includes(state.batchVerdict)) state.batchVerdict = "none";
864
- } catch {
865
- }
866
- return state;
867
- }
868
- function writeState(target, state) {
869
- try {
870
- ensureDirSync(dirname(target));
871
- writeFileSync(target, JSON.stringify(state), "utf8");
488
+ ensureDirSync(dirname(target));
489
+ writeFileSync(target, JSON.stringify(state), "utf8");
872
490
  } catch {
873
491
  }
874
492
  }
@@ -1044,15 +662,27 @@ function queuePendingContext(stateKey, text) {
1044
662
  const target = sessionSidecarPath(stateKey, PENDING_SUFFIX);
1045
663
  if (target === null) return;
1046
664
  try {
1047
- const existing = readPending(target);
1048
- const merged = existing === null ? trimmed : `${existing}
1049
- ${trimmed}`;
1050
- const capped = merged.length <= MAX_PENDING_CONTEXT_BYTES ? merged : merged.slice(merged.length - MAX_PENDING_CONTEXT_BYTES);
665
+ const capped = fitPending(readPending(target), trimmed);
1051
666
  ensureDirSync(dirname2(target));
1052
667
  writeFileSync2(target, capped, "utf8");
1053
668
  } catch {
1054
669
  }
1055
670
  }
671
+ function replacePendingContextBlock(stateKey, text, blockStart) {
672
+ const target = sessionSidecarPath(stateKey, PENDING_SUFFIX);
673
+ if (target === null) return;
674
+ try {
675
+ const existing = readPending(target);
676
+ const at = existing === null ? -1 : existing.indexOf(blockStart);
677
+ if (existing !== null && at >= 0) {
678
+ const kept = existing.slice(0, at).trim();
679
+ if (kept === "") rmSync(target, { force: true });
680
+ else writeFileSync2(target, kept, "utf8");
681
+ }
682
+ } catch {
683
+ }
684
+ queuePendingContext(stateKey, text);
685
+ }
1056
686
  function peekPendingContext(stateKey) {
1057
687
  const target = sessionSidecarPath(stateKey, PENDING_SUFFIX);
1058
688
  return target === null ? null : readPending(target);
@@ -1069,6 +699,18 @@ function commitPendingContext(stateKey, delivered) {
1069
699
  } catch {
1070
700
  }
1071
701
  }
702
+ function fitPending(existing, text) {
703
+ if (existing !== null) {
704
+ const merged = `${existing}
705
+ ${text}`;
706
+ if (merged.length <= MAX_PENDING_CONTEXT_BYTES) return merged;
707
+ }
708
+ if (text.length <= MAX_PENDING_CONTEXT_BYTES) return text;
709
+ const head = text.slice(0, MAX_PENDING_CONTEXT_BYTES);
710
+ const lineEnd = head.lastIndexOf("\n");
711
+ if (lineEnd > 0) return head.slice(0, lineEnd).trimEnd();
712
+ return /[\uD800-\uDBFF]$/.test(head) ? head.slice(0, -1) : head;
713
+ }
1072
714
  function readPending(target) {
1073
715
  try {
1074
716
  const raw = readFileSync2(target, "utf8").trim();
@@ -1192,107 +834,847 @@ function shouldSuppressDuplicateVscodeHook(event, harness) {
1192
834
  }
1193
835
  }
1194
836
 
1195
- // src/hooks_grep.ts
837
+ // src/hooks_read_post.ts
1196
838
  init_define_import_meta_env();
1197
- function grepIntInput(toolInput, key) {
1198
- const value = toolInput[key];
1199
- if (typeof value === "number" && Number.isFinite(value)) return value;
1200
- if (typeof value === "string" && value.trim() !== "") {
1201
- const n = Number(value);
1202
- if (Number.isFinite(n)) return n;
839
+ import * as fs2 from "node:fs";
840
+ import * as path2 from "node:path";
841
+
842
+ // src/served_lines.ts
843
+ init_define_import_meta_env();
844
+ var SERVED_RUN_COMPARISON_BUDGET = 4e5;
845
+ var SERVED_RUN_ANCHORS_PER_LINE = 32;
846
+ function indexServedBody(id, output) {
847
+ const lines = output.split("\n");
848
+ const positions = /* @__PURE__ */ new Map();
849
+ for (let j = 0; j < lines.length; j++) {
850
+ const key = lines[j] ?? "";
851
+ let at = positions.get(key);
852
+ if (at === void 0) {
853
+ at = [];
854
+ positions.set(key, at);
855
+ }
856
+ if (at.length < SERVED_RUN_ANCHORS_PER_LINE) at.push(j);
857
+ }
858
+ return { id, lines, positions };
859
+ }
860
+ function longestServedRun(rows, from, to, bodies, budget) {
861
+ let best = null;
862
+ for (const body of bodies) {
863
+ for (let i = from; i < to; i++) {
864
+ if (best !== null && to - i <= best.len) break;
865
+ const anchors = body.positions.get(rows[i]?.text ?? "");
866
+ if (anchors === void 0) continue;
867
+ for (const j of anchors) {
868
+ let k = 0;
869
+ while (i + k < to && j + k < body.lines.length && rows[i + k]?.text === body.lines[j + k]) {
870
+ k++;
871
+ if (--budget.left <= 0) return best !== null && best.len > 0 ? best : null;
872
+ }
873
+ if (best === null || k > best.len) best = { start: i, len: k, id: body.id };
874
+ }
875
+ }
876
+ }
877
+ return best !== null && best.len > 0 ? best : null;
878
+ }
879
+ var MAX_SERVED_ELISIONS = 3;
880
+ function servedRunNotice(firstLine, lastLine, id, len) {
881
+ const which = firstLine !== null && lastLine !== null ? "lines " + firstLine + "-" + lastLine : len + " lines here";
882
+ return "[token-goat] " + which + " were already served verbatim in this session; withheld here. Recall them with `token-goat bash-output " + id + " --full`.";
883
+ }
884
+ function renderedRunBytes(rows, start, len) {
885
+ let bytes = 0;
886
+ for (let i = start; i < start + len; i++) bytes += Buffer.byteLength(rows[i]?.raw ?? "", "utf-8") + 1;
887
+ return bytes;
888
+ }
889
+ function planServedElisions(rows, bodies) {
890
+ if (rows.length === 0 || bodies.length === 0) return [];
891
+ const budget = { left: SERVED_RUN_COMPARISON_BUDGET };
892
+ let spans = [[0, rows.length]];
893
+ const cuts = [];
894
+ for (let pass = 0; pass < MAX_SERVED_ELISIONS; pass++) {
895
+ let bestRun = null;
896
+ let bestSpan = -1;
897
+ for (let s = 0; s < spans.length; s++) {
898
+ const span = spans[s];
899
+ if (span === void 0) continue;
900
+ const run = longestServedRun(rows, span[0], span[1], bodies, budget);
901
+ if (run !== null && (bestRun === null || run.len > bestRun.len)) {
902
+ bestRun = run;
903
+ bestSpan = s;
904
+ }
905
+ }
906
+ if (bestRun === null || bestSpan < 0) break;
907
+ const first2 = rows[bestRun.start];
908
+ const last = rows[bestRun.start + bestRun.len - 1];
909
+ if (first2 === void 0 || last === void 0) break;
910
+ const noticeBytes = Buffer.byteLength(servedRunNotice(first2.no, last.no, bestRun.id, bestRun.len), "utf-8") + 1;
911
+ if (renderedRunBytes(rows, bestRun.start, bestRun.len) <= noticeBytes) break;
912
+ cuts.push(bestRun);
913
+ const chosen = spans[bestSpan];
914
+ if (chosen === void 0) break;
915
+ const rebuilt = [];
916
+ for (let s = 0; s < spans.length; s++) {
917
+ const span = spans[s];
918
+ if (span === void 0) continue;
919
+ if (s !== bestSpan) {
920
+ rebuilt.push(span);
921
+ continue;
922
+ }
923
+ if (bestRun.start > chosen[0]) rebuilt.push([chosen[0], bestRun.start]);
924
+ if (bestRun.start + bestRun.len < chosen[1]) rebuilt.push([bestRun.start + bestRun.len, chosen[1]]);
925
+ }
926
+ spans = rebuilt;
1203
927
  }
1204
- return void 0;
928
+ cuts.sort((a, b) => a.start - b.start);
929
+ return cuts;
1205
930
  }
1206
- function grepSignature(toolInput) {
1207
- const pattern = toolInput["pattern"];
1208
- if (typeof pattern !== "string" || pattern === "") return null;
1209
- const path6 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
1210
- const outputMode = typeof toolInput["output_mode"] === "string" ? toolInput["output_mode"] : "files_with_matches";
1211
- const glob = typeof toolInput["glob"] === "string" ? toolInput["glob"] : "";
1212
- const type = typeof toolInput["type"] === "string" ? toolInput["type"] : "";
1213
- const caseInsensitive = toolInput["-i"] === true;
1214
- const onlyMatching = toolInput["-o"] === true;
1215
- const multiline = toolInput["multiline"] === true;
1216
- const lineNumbers = toolInput["-n"] !== false;
1217
- const contextA = grepIntInput(toolInput, "-A");
1218
- const contextB = grepIntInput(toolInput, "-B");
1219
- const contextC = grepIntInput(toolInput, "-C") ?? grepIntInput(toolInput, "context");
1220
- const headLimit = grepIntInput(toolInput, "head_limit");
1221
- const offset = grepIntInput(toolInput, "offset");
1222
- return JSON.stringify([
1223
- pattern,
1224
- path6,
1225
- outputMode,
1226
- glob,
1227
- type,
1228
- caseInsensitive,
1229
- onlyMatching,
1230
- multiline,
1231
- lineNumbers,
1232
- contextA,
1233
- contextB,
1234
- contextC,
1235
- headLimit,
1236
- offset
1237
- ]);
931
+
932
+ // src/read_shape.ts
933
+ init_define_import_meta_env();
934
+ import { readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
935
+ import { dirname as dirname3 } from "node:path";
936
+ var SHAPES = /* @__PURE__ */ new Set(["outline", "skeleton", "fold", "truncated", "partial"]);
937
+ var SHAPE_SUFFIX = ".read-shape";
938
+ var MAX_ENTRIES = 512;
939
+ function load(target) {
940
+ try {
941
+ const parsed = JSON.parse(readFileSync3(target, "utf8"));
942
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
943
+ } catch {
944
+ return {};
945
+ }
1238
946
  }
1239
- var { post: dedupPostHandler, pre: preGrepDedupHandler } = makeDedupHintHandlers({
1240
- toolName: "Grep",
1241
- buildSignature: grepSignature,
1242
- recordQuery: recordGrepQuery,
1243
- getMatchCount: getGrepMatchCount,
1244
- minMatchesConfigKey: "grep_dedup_min_matches",
1245
- statName: "grep_dedup_hint"
1246
- });
1247
- var CONTENT_LINE_RE = /^(.+?):(\d+):(.*)$/s;
1248
- function foldGrepContentHandler(event) {
947
+ function recordReadShape(stateKey, file, shape) {
948
+ const target = sessionSidecarPath(stateKey, SHAPE_SUFFIX);
949
+ if (target === null) return;
1249
950
  try {
1250
- if (getToolName(event) !== "Grep") return passOutput();
1251
- const toolInput = getToolInput(event);
1252
- if (toolInput["output_mode"] !== "content") return passOutput();
1253
- if (toolInput["multiline"] === true) return passOutput();
1254
- if (toolInput["-A"] !== void 0 || toolInput["-B"] !== void 0 || toolInput["-C"] !== void 0 || toolInput["context"] !== void 0) {
1255
- return passOutput();
1256
- }
1257
- if (toolInput["-n"] === false) return passOutput();
1258
- const rawText = extractToolResponseField(event.raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
1259
- if (!rawText) return passOutput();
1260
- const text = redactSecrets(rawText).text;
1261
- const rawLines = text.split(/\r\n|\r|\n/);
1262
- const parsed = [];
1263
- for (const line of rawLines) {
1264
- if (line.trim() === "") continue;
1265
- const m = CONTENT_LINE_RE.exec(line);
1266
- if (!m) return passOutput();
1267
- parsed.push({ file: m[1], lineNo: m[2], rest: m[3] });
1268
- }
1269
- if (parsed.length === 0) return passOutput();
1270
- const order = [];
1271
- const groups = /* @__PURE__ */ new Map();
1272
- for (const p of parsed) {
1273
- let group = groups.get(p.file);
1274
- if (group === void 0) {
1275
- group = [];
1276
- groups.set(p.file, group);
1277
- order.push(p.file);
1278
- }
1279
- group.push({ lineNo: p.lineNo, rest: p.rest });
951
+ const shapes = load(target);
952
+ if (shape === null) {
953
+ if (!Object.hasOwn(shapes, file)) return;
954
+ delete shapes[file];
955
+ } else {
956
+ delete shapes[file];
957
+ shapes[file] = shape;
1280
958
  }
1281
- const hasSharedFile = order.some((f) => groups.get(f).length >= 2);
1282
- if (!hasSharedFile) return passOutput();
1283
- const foldedLines = [];
1284
- for (const f of order) {
1285
- foldedLines.push(f);
1286
- for (const { lineNo, rest } of groups.get(f)) {
1287
- foldedLines.push(` ${lineNo}: ${rest}`);
1288
- }
959
+ const keys = Object.keys(shapes);
960
+ if (keys.length === 0) {
961
+ rmSync3(target, { force: true });
962
+ return;
1289
963
  }
1290
- const rewritten = foldedLines.join("\n");
1291
- const originalBytes = Buffer.byteLength(text, "utf-8");
1292
- const rewrittenBytes = Buffer.byteLength(rewritten, "utf-8");
1293
- if (!isRewriteWorthwhile({
1294
- originalBytes,
1295
- rewrittenBytes,
964
+ for (const k of keys.slice(0, Math.max(0, keys.length - MAX_ENTRIES))) delete shapes[k];
965
+ ensureDirSync(dirname3(target));
966
+ writeFileSync3(target, JSON.stringify(shapes), "utf8");
967
+ } catch {
968
+ }
969
+ }
970
+ function readShapeOf(stateKey, file) {
971
+ const target = sessionSidecarPath(stateKey, SHAPE_SUFFIX);
972
+ if (target === null) return null;
973
+ const shapes = load(target);
974
+ const shape = Object.hasOwn(shapes, file) ? shapes[file] : void 0;
975
+ return shape !== void 0 && SHAPES.has(shape) ? shape : null;
976
+ }
977
+
978
+ // src/fold_delivery.ts
979
+ init_define_import_meta_env();
980
+ import { readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
981
+ var BODY_FOLD_KEEP_LINES = 8;
982
+ var BODY_FOLD_MIN_SPAN = 20;
983
+ var COMMENT_FOLD_KEEP_LINES = 2;
984
+ var COMMENT_FOLD_MIN_BLOCK = 12;
985
+ var BODY_FOLD_SYMBOL_LIMIT = 1e4;
986
+ function bodyFoldNotice(name2, firstLine, lastLine, shownPath, declLine) {
987
+ const n = lastLine - firstLine + 1;
988
+ const anchor = declLine === void 0 ? "" : `@${declLine}`;
989
+ const safeName = displaySafeText(name2);
990
+ return `... ${n} more lines of ${safeName} (${firstLine}-${lastLine}) folded -- token-goat read "${shownPath}::${safeName}${anchor}"`;
991
+ }
992
+ function foldingEnabled() {
993
+ const hints = loadConfig().hints;
994
+ return hints.fold_code_bodies || hints.fold_comment_blocks || hints.fold_prose_paragraphs;
995
+ }
996
+ var PROSE_FOLDABLE_EXT_RE = /\.(?:md|mdx|markdown)$/i;
997
+ function isProseFoldablePath(normalizedPath) {
998
+ return PROSE_FOLDABLE_EXT_RE.test(normalizedPath);
999
+ }
1000
+ function proseFoldNotice(keep, line, shownPath, normalizedPath) {
1001
+ const section = findContainingSection(normalizedPath, line, line);
1002
+ if (section === null) return null;
1003
+ return `${displaySafeText(keep)} ... rest of paragraph folded (line ${line}) -- token-goat section "${shownPath}::${displaySafeText(section.heading)}"`;
1004
+ }
1005
+ function commentFoldNotice(firstLine, lastLine, shownPath) {
1006
+ const n = lastLine - firstLine + 1;
1007
+ return `... ${n} more comment lines (${firstLine}-${lastLine}) folded -- Read "${shownPath}" with offset=${firstLine}, limit=${n}`;
1008
+ }
1009
+ var ALIGNED_LAYOUT_NOTE = "Every line shows its real line number: each withheld run opens with one notice line on the run's first line (a `...` pointer, or a `[token-goat] lines ... were already served` notice), and the rest of the run is left as empty lines.";
1010
+ function pushWithheld(out, notice, len, layout) {
1011
+ if (layout === "compact") {
1012
+ out.push(notice);
1013
+ return;
1014
+ }
1015
+ out.push(notice.replace(/\r?\n/g, " "));
1016
+ for (let i = 1; i < len; i++) out.push("");
1017
+ }
1018
+ function alignedPaddingBytes(len, layout) {
1019
+ return layout === "aligned" ? Math.max(0, len - 1) : 0;
1020
+ }
1021
+ var FOLD_SPAN_PARSE_MAX_BYTES = 4e5;
1022
+ function parseFoldSpansFromDisk(normalizedPath, rows) {
1023
+ try {
1024
+ const onDisk = hostPathOfIndexKey(normalizedPath);
1025
+ if (statSync2(onDisk).size > FOLD_SPAN_PARSE_MAX_BYTES) return [];
1026
+ const language = detectLanguage(normalizedPath);
1027
+ if (!isTreeSitterAvailable(language)) return [];
1028
+ const fileText = readFileSync4(onDisk, "utf-8");
1029
+ const diskLines = fileText.split("\n");
1030
+ for (const row of rows) {
1031
+ const disk = diskLines[row.no - 1];
1032
+ if (disk === void 0 || disk.replace(/\r$/, "") !== row.text.replace(/\r$/, "")) return [];
1033
+ }
1034
+ const symbols = parseSourceSymbolsTreeSitterOnly(fileText, normalizedPath, language);
1035
+ if (symbols === null) return [];
1036
+ return symbols.slice(0, BODY_FOLD_SYMBOL_LIMIT).map((s) => ({ name: s.name, kind: s.kind, lineStart: s.lineStart, lineEnd: s.lineEnd }));
1037
+ } catch {
1038
+ return [];
1039
+ }
1040
+ }
1041
+ function resolveFoldSpans(normalizedPath, hasCommentSyntax, rows) {
1042
+ try {
1043
+ const entry = getFileEntry(normalizedPath);
1044
+ if (entry !== null && entry.sha !== "" && !isVirtualIndexedPath(normalizedPath) && entry.sha === fingerprintFile(hostPathOfIndexKey(normalizedPath)) && entry.parserSha === parserFingerprintForLanguage(entry.language)) {
1045
+ return querySymbols({ filePath: normalizedPath, limit: BODY_FOLD_SYMBOL_LIMIT });
1046
+ }
1047
+ if (hasCommentSyntax) enqueueDirtyPathSafe(normalizedPath);
1048
+ } catch {
1049
+ }
1050
+ return parseFoldSpansFromDisk(normalizedPath, rows);
1051
+ }
1052
+ function strictlyInteriorOnWindow(folds, rowCount, windowed) {
1053
+ if (!windowed) return [...folds];
1054
+ return folds.filter((f) => f.startIdx > 0 && f.startIdx + f.len < rowCount);
1055
+ }
1056
+ function foldDelivery(rows, normalizedPath, shownPath, windowed = false, layout = "compact") {
1057
+ const syntax = commentSyntaxFor(normalizedPath);
1058
+ const foldBodies = loadConfig().hints.fold_code_bodies;
1059
+ const spans = foldBodies ? resolveFoldSpans(normalizedPath, syntax !== null, rows) : [];
1060
+ const bodyFolds = foldBodies ? planBodyFolds(rows, spans, BODY_FOLD_KEEP_LINES, BODY_FOLD_MIN_SPAN) : [];
1061
+ const claimed = /* @__PURE__ */ new Set();
1062
+ for (const fold of bodyFolds) for (let i = fold.startIdx; i < fold.startIdx + fold.len; i++) claimed.add(i);
1063
+ const commentFolds = loadConfig().hints.fold_comment_blocks ? strictlyInteriorOnWindow(planCommentFolds(rows, syntax, COMMENT_FOLD_KEEP_LINES, COMMENT_FOLD_MIN_BLOCK, claimed), rows.length, windowed) : [];
1064
+ for (const fold of commentFolds) for (let i = fold.startIdx; i < fold.startIdx + fold.len; i++) claimed.add(i);
1065
+ const proseFolds = isProseFoldablePath(normalizedPath) && loadConfig().hints.fold_prose_paragraphs ? strictlyInteriorOnWindow(planProseFolds(rows, claimed), rows.length, windowed).filter((f) => findContainingSection(normalizedPath, f.firstLine, f.firstLine) !== null) : [];
1066
+ const folds = mergeFolds(mergeFolds(bodyFolds, commentFolds), proseFolds);
1067
+ if (folds.length === 0) return null;
1068
+ const numbered = [];
1069
+ const raw = [];
1070
+ let at = 0;
1071
+ for (const fold of folds) {
1072
+ for (let i = at; i < fold.startIdx; i++) {
1073
+ numbered.push(rows[i]?.raw ?? "");
1074
+ raw.push(rows[i]?.text ?? "");
1075
+ }
1076
+ let notice;
1077
+ switch (fold.kind) {
1078
+ case "comment":
1079
+ notice = commentFoldNotice(fold.firstLine, fold.lastLine, shownPath);
1080
+ break;
1081
+ case "prose":
1082
+ notice = proseFoldNotice(fold.keep ?? "", fold.firstLine, shownPath, normalizedPath);
1083
+ break;
1084
+ case "body":
1085
+ notice = bodyFoldNotice(fold.name, fold.firstLine, fold.lastLine, shownPath, fold.declLine);
1086
+ break;
1087
+ default: {
1088
+ const unreachable = fold.kind;
1089
+ throw new Error(`unhandled fold kind: ${String(unreachable)}`);
1090
+ }
1091
+ }
1092
+ if (notice === null) {
1093
+ for (let i = fold.startIdx; i < fold.startIdx + fold.len; i++) {
1094
+ numbered.push(rows[i]?.raw ?? "");
1095
+ raw.push(rows[i]?.text ?? "");
1096
+ }
1097
+ at = fold.startIdx + fold.len;
1098
+ continue;
1099
+ }
1100
+ pushWithheld(numbered, notice, fold.len, layout);
1101
+ at = fold.startIdx + fold.len;
1102
+ }
1103
+ for (let i = at; i < rows.length; i++) {
1104
+ numbered.push(rows[i]?.raw ?? "");
1105
+ raw.push(rows[i]?.text ?? "");
1106
+ }
1107
+ return { numbered, raw, folds };
1108
+ }
1109
+
1110
+ // src/fold_structure.ts
1111
+ init_define_import_meta_env();
1112
+ var OUTLINE_MIN_BODY_BYTES = 8e3;
1113
+ var OUTLINE_MIN_HEADINGS = 6;
1114
+ var OUTLINE_MAX_REPLACEMENT_RATIO = 0.4;
1115
+ var SKELETON_MIN_BODY_BYTES = 12e3;
1116
+ var SKELETON_MIN_SYMBOLS = 8;
1117
+ var SKELETON_MAX_REPLACEMENT_RATIO = 0.4;
1118
+ function outlineLeadInRows(rows, headings) {
1119
+ const opensWithH1 = headings[0]?.level === 1;
1120
+ const boundary = opensWithH1 ? headings.find((h, i) => i > 0 && h.level === 2) : headings[0];
1121
+ if (boundary === void 0) return [];
1122
+ const boundaryIdx = rows.findIndex((r) => r.no === boundary.lineNumber);
1123
+ return boundaryIdx > 0 ? rows.slice(0, boundaryIdx) : [];
1124
+ }
1125
+ function capLeadIn(rows) {
1126
+ return { rows: [...rows], notice: null };
1127
+ }
1128
+ function planMarkdownOutline(rows, normalizedPath, shownPath, originalBytes, layout = "compact") {
1129
+ if (!loadConfig().hints.outline_large_documents) return null;
1130
+ if (!isProseFoldablePath(normalizedPath)) return null;
1131
+ if (originalBytes < OUTLINE_MIN_BODY_BYTES) return null;
1132
+ const fileText = rows.map((r) => r.text).join("\n");
1133
+ const headings = extractMarkdownHeadings(fileText);
1134
+ if (headings.length < OUTLINE_MIN_HEADINGS) return null;
1135
+ const { guidance, sectionsList } = headingTreeParts(headings, shownPath, fileText);
1136
+ const leadInRows = outlineLeadInRows(rows, headings);
1137
+ const { rows: cappedLeadIn, notice: capNotice } = capLeadIn(leadInRows);
1138
+ const leadInFolded = foldDelivery(cappedLeadIn, normalizedPath, shownPath, false, layout);
1139
+ const leadInNumbered = leadInFolded !== null ? leadInFolded.numbered : cappedLeadIn.map((r) => r.raw);
1140
+ const leadInRaw = leadInFolded !== null ? leadInFolded.raw : cappedLeadIn.map((r) => r.text);
1141
+ const headingCount = `at least ${headings.length} heading${headings.length === 1 ? "" : "s"} found`;
1142
+ const heading = hintTarget(normalizedPath, "section", { headings, content: fileText, placeholder: "<Heading>" }).name;
1143
+ if (layout === "aligned") {
1144
+ const body = fenceNumberedFileContent([...leadInNumbered, ...planOutlineAlignedRows(rows, headings, leadInNumbered.length, shownPath, normalizedPath)].join("\n"), ALIGNED_LAYOUT_NOTE);
1145
+ const alignedNotice = `Partial view: this ${originalBytes.toLocaleString("en-US")} B document was cut down to ${leadInRows.length > 0 ? "its lead-in and " : ""}its headings (${headingCount}), each on its real line, with the text under each heading withheld behind a pointer. Run token-goat section "${shownPath}::${heading}" to read one section verbatim.`;
1146
+ return { numbered: [body.body], raw: leadInRaw, kind: "read:markdown_outline", detail: shownPath, ratioCap: OUTLINE_MAX_REPLACEMENT_RATIO, context: `${body.preamble}
1147
+ ${alignedNotice}` };
1148
+ }
1149
+ const notice = leadInRows.length > 0 ? `Partial view: this ${originalBytes.toLocaleString("en-US")} B document was replaced with its lead-in (the content before its first section) and a heading tree (${headingCount}). Run token-goat section "${shownPath}::${heading}" to read one section verbatim.` : `Partial view: this ${originalBytes.toLocaleString("en-US")} B document has no lead-in before its first section, so it was replaced with a heading tree alone (${headingCount}). Run token-goat section "${shownPath}::${heading}" to read one section verbatim.`;
1150
+ return {
1151
+ // The notice leads so that token-goat speaks first: emitting the lead-in above it let a document open with forged `[token-goat: ...]` or `[tg]` lines that read as this rewrite's own preamble. The lead-in is file bytes like the heading tree, so it gets its own fence rather than riding in the tree's; both fences are part of `numbered`, so their cost is priced by the ratioCap gate below along with everything else.
1152
+ numbered: [
1153
+ notice,
1154
+ ...leadInNumbered.length > 0 ? [fenceUntrustedFileContent(leadInNumbered.join("\n"))] : [],
1155
+ ...capNotice !== null ? [capNotice] : [],
1156
+ guidance,
1157
+ fenceUntrustedFileContent(sectionsList)
1158
+ ],
1159
+ raw: leadInRaw,
1160
+ kind: "read:markdown_outline",
1161
+ detail: shownPath,
1162
+ ratioCap: OUTLINE_MAX_REPLACEMENT_RATIO
1163
+ };
1164
+ }
1165
+ function planOutlineAlignedRows(rows, headings, startIdx, shownPath, normalizedPath) {
1166
+ const listedLines = new Set(headings.map((h) => h.lineNumber));
1167
+ const out = [];
1168
+ let i = startIdx;
1169
+ while (i < rows.length) {
1170
+ const row = rows[i];
1171
+ if (row === void 0) break;
1172
+ if (listedLines.has(row.no)) {
1173
+ out.push(row.raw);
1174
+ i++;
1175
+ continue;
1176
+ }
1177
+ let j = i;
1178
+ while (j < rows.length && !listedLines.has(rows[j]?.no ?? -1)) j++;
1179
+ const firstLine = row.no;
1180
+ const lastLine = rows[j - 1]?.no ?? firstLine;
1181
+ const n = j - i;
1182
+ const under = findContainingSection(normalizedPath, firstLine, lastLine)?.heading ?? null;
1183
+ const notice = under !== null && !/["`$\\]/.test(under) && displaySafeText(under) === under ? `... ${n} line${n === 1 ? "" : "s"} (${firstLine}-${lastLine}) under "${under}" withheld -- token-goat section "${shownPath}::${under}"` : null;
1184
+ let runBytes = 0;
1185
+ for (let k = i; k < j; k++) runBytes += Buffer.byteLength(rows[k]?.raw ?? "", "utf-8") + 1;
1186
+ if (notice === null || Buffer.byteLength(notice, "utf-8") + alignedPaddingBytes(n, "aligned") >= runBytes) {
1187
+ for (let k = i; k < j; k++) out.push(rows[k]?.raw ?? "");
1188
+ } else {
1189
+ pushWithheld(out, notice, n, "aligned");
1190
+ }
1191
+ i = j;
1192
+ }
1193
+ return out;
1194
+ }
1195
+ function skeletonGapNotice(firstLine, lastLine, shownPath) {
1196
+ const n = lastLine - firstLine + 1;
1197
+ return `... ${n} line${n === 1 ? "" : "s"} (${firstLine}-${lastLine}) withheld from the skeleton -- Read "${shownPath}" with offset=${firstLine}, limit=${n}`;
1198
+ }
1199
+ function planSourceSkeletonRuns(rows, symbols, shownPath, layout) {
1200
+ const base = rows[0]?.no ?? 1;
1201
+ const keep = /* @__PURE__ */ new Set();
1202
+ const firstDeclLine = Math.min(...symbols.map((s) => s.lineStart));
1203
+ for (let n = base; n < firstDeclLine; n++) keep.add(n);
1204
+ for (const sym of symbols) keep.add(sym.lineStart);
1205
+ const bodyRunStart = /* @__PURE__ */ new Map();
1206
+ for (const sym of symbols) if (sym.lineEnd > sym.lineStart) bodyRunStart.set(sym.lineStart + 1, sym);
1207
+ const runEndsAfter = /* @__PURE__ */ new Set();
1208
+ for (const sym of symbols) if (sym.lineEnd > sym.lineStart) runEndsAfter.add(sym.lineEnd);
1209
+ const numbered = [];
1210
+ const raw = [];
1211
+ let withheldLines = 0;
1212
+ let i = 0;
1213
+ while (i < rows.length) {
1214
+ const row = rows[i];
1215
+ if (row === void 0) break;
1216
+ if (keep.has(row.no)) {
1217
+ numbered.push(row.raw);
1218
+ raw.push(row.text);
1219
+ i++;
1220
+ continue;
1221
+ }
1222
+ let j = i;
1223
+ while (j < rows.length && !keep.has(rows[j]?.no ?? -1)) {
1224
+ const stop = runEndsAfter.has(rows[j]?.no ?? -1);
1225
+ j++;
1226
+ if (stop) break;
1227
+ }
1228
+ const firstLine = row.no;
1229
+ const lastLine = rows[j - 1]?.no ?? firstLine;
1230
+ const sym = bodyRunStart.get(firstLine);
1231
+ const notice = sym !== void 0 && sym.lineEnd === lastLine ? bodyFoldNotice(sym.name, firstLine, lastLine, shownPath, sym.lineStart) : skeletonGapNotice(firstLine, lastLine, shownPath);
1232
+ let runBytes = 0;
1233
+ for (let k = i; k < j; k++) runBytes += Buffer.byteLength(rows[k]?.raw ?? "", "utf-8") + 1;
1234
+ if (Buffer.byteLength(notice, "utf-8") + alignedPaddingBytes(j - i, layout) >= runBytes) {
1235
+ for (let k = i; k < j; k++) {
1236
+ const kept = rows[k];
1237
+ if (kept === void 0) continue;
1238
+ numbered.push(kept.raw);
1239
+ raw.push(kept.text);
1240
+ }
1241
+ } else {
1242
+ pushWithheld(numbered, notice, j - i, layout);
1243
+ withheldLines += lastLine - firstLine + 1;
1244
+ }
1245
+ i = j;
1246
+ }
1247
+ return withheldLines === 0 ? null : { numbered, raw, withheldLines };
1248
+ }
1249
+ function planSourceSkeleton(rows, normalizedPath, shownPath, originalBytes, layout = "compact") {
1250
+ if (!loadConfig().hints.skeleton_large_sources) return null;
1251
+ if (originalBytes < SKELETON_MIN_BODY_BYTES) return null;
1252
+ const language = detectLanguage(normalizedPath);
1253
+ if (!isTreeSitterAvailable(language)) return null;
1254
+ const fileText = rows.map((r) => r.text).join("\n");
1255
+ const symbols = parseSourceSymbolsTreeSitterOnly(fileText, normalizedPath, language);
1256
+ if (symbols === null) return null;
1257
+ if (symbols.length < SKELETON_MIN_SYMBOLS) return null;
1258
+ const plan = planSourceSkeletonRuns(rows, symbols, shownPath, layout);
1259
+ if (plan === null) return null;
1260
+ const notice = `Partial view: this ${originalBytes.toLocaleString("en-US")} B source file was replaced with its structural skeleton, its preamble and one line per declaration, with ${plan.withheldLines.toLocaleString("en-US")} line${plan.withheldLines === 1 ? "" : "s"} of bodies withheld (at least ${symbols.length} declaration${symbols.length === 1 ? "" : "s"} found). Run token-goat read "${shownPath}::SymbolName" for one body verbatim, or Read "${shownPath}" with offset=1, limit=${rows.length} for the whole file.`;
1261
+ if (layout === "aligned") {
1262
+ const body = fenceNumberedFileContent(plan.numbered.join("\n"), ALIGNED_LAYOUT_NOTE);
1263
+ return { numbered: [body.body], raw: plan.raw, kind: "read:source_skeleton", detail: shownPath, ratioCap: SKELETON_MAX_REPLACEMENT_RATIO, context: `${body.preamble}
1264
+ ${notice}` };
1265
+ }
1266
+ return { numbered: [notice, fenceUntrustedFileContent(plan.numbered.join("\n"))], raw: plan.raw, kind: "read:source_skeleton", detail: shownPath, ratioCap: SKELETON_MAX_REPLACEMENT_RATIO };
1267
+ }
1268
+ function isStructuralRewriteAccepted(originalBytes, rewrittenBytes, ratioCap) {
1269
+ if (rewrittenBytes > originalBytes * ratioCap) return false;
1270
+ return isRewriteWorthwhile({ originalBytes, rewrittenBytes, noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() });
1271
+ }
1272
+
1273
+ // src/hooks_read_post.ts
1274
+ function extractReadOutput(raw) {
1275
+ return extractToolResponseField(raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
1276
+ }
1277
+ function readLayout(event) {
1278
+ return harnessNumbersReadContent(event) ? "aligned" : "compact";
1279
+ }
1280
+ function keepsLineNumbers(original, rewritten) {
1281
+ return original.split("\n").length === rewritten.split("\n").length;
1282
+ }
1283
+ function postReadHandlerInner(event, suppressStructuralHint) {
1284
+ const filePath = getFilePath(event);
1285
+ if (filePath === void 0) return passOutput();
1286
+ const normalized = normalizePath(filePath);
1287
+ const shown = displaySafePath(normalized);
1288
+ const respText = extractReadOutput(event.raw);
1289
+ if (isTruncatedReadDelivery(event, respText)) {
1290
+ markFileTruncated(normalized, !readRequestedSliceWindow(event).isExplicitSlice);
1291
+ }
1292
+ const postBasename = path2.basename(normalized);
1293
+ const diffSourcesEnabled = loadConfig().hints.serve_diff_on_reread;
1294
+ if (/\.(md|mdx|markdown|rst|txt)$/i.test(postBasename) || isSessionArtifactFile(normalized) || diffSourcesEnabled && isDiffableSource(postBasename)) {
1295
+ try {
1296
+ const onDisk = hostPathOfIndexKey(normalized);
1297
+ const sz = statSize(onDisk);
1298
+ if (sz !== null && sz <= 256 * 1024) {
1299
+ const content = fs2.readFileSync(onDisk);
1300
+ store(sessionStateKey(event), normalized, content);
1301
+ }
1302
+ } catch {
1303
+ }
1304
+ }
1305
+ if (loadConfig().hints.cross_session_read_dedup) {
1306
+ try {
1307
+ const cwd = getCwd(event) ?? process.cwd();
1308
+ const project = findProject(cwd) ?? makeProjectAt(cwd);
1309
+ const source = readEvidenceFileText(normalized);
1310
+ if (source !== null) recordEvidence({ projectRoot: project.root, source: normalized, representation: "file", text: source });
1311
+ } catch {
1312
+ }
1313
+ }
1314
+ if (loadConfig().hints.cross_session_read_dedup) {
1315
+ try {
1316
+ const cwd = getCwd(event) ?? process.cwd();
1317
+ let project = findProject(cwd);
1318
+ if (!project) {
1319
+ project = makeProjectAt(cwd);
1320
+ }
1321
+ const sessionState = exportSessionState();
1322
+ const mappedFiles = [];
1323
+ for (const fileEntry of sessionState.files) {
1324
+ const relPath = relPathWithinRoot(project.root, fileEntry.path);
1325
+ if (relPath !== null) {
1326
+ mappedFiles.push({
1327
+ rel_path: relPath,
1328
+ hit_count: fileEntry.readCount
1329
+ });
1330
+ }
1331
+ }
1332
+ writeSessionManifest(project.hash, sessionStateKey(event), { files: mappedFiles });
1333
+ } catch {
1334
+ }
1335
+ }
1336
+ if (isSourceExtension(postBasename)) {
1337
+ try {
1338
+ const onDisk = hostPathOfIndexKey(normalized);
1339
+ const sz = statSize(onDisk);
1340
+ if (sz !== null && sz <= SLICE_ESTIMATE_SCAN_CAP_BYTES) {
1341
+ const lineCount = countTextLines(fs2.readFileSync(onDisk, "utf8"));
1342
+ const minLines = loadConfig().post_read_code_compress.min_lines;
1343
+ if (lineCount >= minLines && !suppressStructuralHint) {
1344
+ if (!meetsSavingsFloor(sz)) {
1345
+ logSuppressedDetection("read_structural_nav", event.sessionId, normalized);
1346
+ } else {
1347
+ recordStat("session_hint", 0, 0);
1348
+ return quietContextOutput(
1349
+ shown + " is " + lineCount + ' lines. Use `token-goat skeleton "' + shown + '"` or `token-goat outline "' + shown + '"` for structural navigation instead of a future full re-read.'
1350
+ );
1351
+ }
1352
+ }
1353
+ }
1354
+ } catch {
1355
+ }
1356
+ }
1357
+ return passOutput();
1358
+ }
1359
+ function recordReadAsServedOutput(event, deliveredRaw = null) {
1360
+ try {
1361
+ const cfg = loadConfig().bash_compress;
1362
+ if (!cfg.enabled) return;
1363
+ const filePath = getFilePath(event);
1364
+ if (filePath === void 0) return;
1365
+ const normalized = normalizePath(filePath);
1366
+ if (isImagePath(normalized)) return;
1367
+ const respText = extractReadOutput(event.raw);
1368
+ if (isTruncatedReadDelivery(event, respText)) return;
1369
+ if (deliveredRaw === null) recordActualSlice(event, normalized);
1370
+ const served = deliveredRaw ?? readWindowFromDisk(event, normalized);
1371
+ if (served === null) return;
1372
+ if (Buffer.byteLength(served, "utf-8") < Math.max(cfg.cache_min_bytes, IDENTICAL_READ_MIN_BODY_BYTES)) return;
1373
+ const window = readRequestedSliceWindow(event);
1374
+ let syntheticCommand = "Read " + normalized;
1375
+ if (window.isExplicitSlice) {
1376
+ syntheticCommand += " --offset " + window.offset;
1377
+ if (window.limit !== void 0) syntheticCommand += " --limit " + window.limit;
1378
+ }
1379
+ const id = storeBashOutputSync(syntheticCommand, served, 0, getCwd(event) ?? process.cwd());
1380
+ recordFileServedOutput(normalized, id);
1381
+ } catch {
1382
+ }
1383
+ }
1384
+ function elideAlreadyServedLines(event, respText) {
1385
+ if (!loadConfig().hints.elide_served_lines) return null;
1386
+ const filePath = getFilePath(event);
1387
+ if (filePath === void 0) return null;
1388
+ const normalized = normalizePath(filePath);
1389
+ if (isImagePath(normalized)) return null;
1390
+ if (isTruncatedReadDelivery(event, respText)) return null;
1391
+ const ids = getFileServedOutputs(normalized);
1392
+ if (ids.length === 0) return null;
1393
+ const parsed = parseReadDelivery(event, respText);
1394
+ if (parsed === null) return null;
1395
+ if (hasPreciseSecret(respText)) return null;
1396
+ const bodies = [];
1397
+ for (let i = ids.length - 1; i >= 0; i--) {
1398
+ const id = ids[i];
1399
+ if (id === void 0) continue;
1400
+ const prior = getBashOutput(id);
1401
+ if (prior !== null) bodies.push(indexServedBody(id, prior.output));
1402
+ }
1403
+ if (bodies.length === 0) return null;
1404
+ const cuts = planServedElisions(parsed.rows, bodies);
1405
+ if (cuts.length === 0) return null;
1406
+ const layout = readLayout(event);
1407
+ const bodyRows = [];
1408
+ let at = 0;
1409
+ for (const cut of cuts) {
1410
+ for (let i = at; i < cut.start; i++) bodyRows.push(parsed.rows[i]?.raw ?? "");
1411
+ const first2 = parsed.rows[cut.start];
1412
+ const last = parsed.rows[cut.start + cut.len - 1];
1413
+ if (first2 === void 0 || last === void 0) return null;
1414
+ pushWithheld(bodyRows, servedRunNotice(first2.no, last.no, cut.id, cut.len), cut.len, layout);
1415
+ at = cut.start + cut.len;
1416
+ }
1417
+ for (let i = at; i < parsed.rows.length; i++) bodyRows.push(parsed.rows[i]?.raw ?? "");
1418
+ const fenced = fenceNumberedFileContent(bodyRows.join("\n"), ALIGNED_LAYOUT_NOTE);
1419
+ const rewritten = [...parsed.header, fenced.body, ...parsed.trailer].join("\n");
1420
+ if (layout === "aligned" && !keepsLineNumbers(respText, rewritten)) return null;
1421
+ const originalBytes = Buffer.byteLength(respText, "utf-8");
1422
+ if (layout === "aligned") {
1423
+ if (!isRewriteWorthwhile({
1424
+ originalBytes,
1425
+ rewrittenBytes: Buffer.byteLength(rewritten, "utf-8") + Buffer.byteLength(fenced.preamble, "utf-8"),
1426
+ noticeBytes: 0,
1427
+ minNetSavingsBytes: resolveMinNetSavingsBytes()
1428
+ })) {
1429
+ return null;
1430
+ }
1431
+ return emitRewriteWithContext(rewritten, fenced.preamble, "read", { kind: "read:served_elide", originalBytes });
1432
+ }
1433
+ if (!isRewriteWorthwhile({
1434
+ originalBytes,
1435
+ rewrittenBytes: Buffer.byteLength(rewritten, "utf-8"),
1436
+ noticeBytes: 0,
1437
+ minNetSavingsBytes: resolveMinNetSavingsBytes()
1438
+ })) {
1439
+ return null;
1440
+ }
1441
+ return emitRewrite(rewritten, "read", { kind: "read:served_elide", originalBytes });
1442
+ }
1443
+ function structuralFoldInputs(event, respText) {
1444
+ const filePath = getFilePath(event);
1445
+ if (filePath === void 0) return null;
1446
+ if (readRequestedSliceWindow(event).isExplicitSlice) return null;
1447
+ if (isTruncatedReadDelivery(event, respText)) return null;
1448
+ if (hasPreciseSecret(respText)) return null;
1449
+ const parsed = parseReadDelivery(event, respText);
1450
+ if (parsed === null) return null;
1451
+ const normalized = normalizePath(filePath);
1452
+ const shown = displaySafePath(toDisplayPath(findProject(getCwd(event) ?? process.cwd())?.root, normalized));
1453
+ return { rows: parsed.rows, header: parsed.header, trailer: parsed.trailer, normalized, shown, originalBytes: Buffer.byteLength(respText, "utf-8"), respText, layout: readLayout(event), startLine: readStartLine(event) };
1454
+ }
1455
+ function emitStructuralFold(inputs, fold) {
1456
+ const rewritten = [...inputs.header, ...fold.numbered, ...inputs.trailer].join("\n");
1457
+ if (fold.context !== void 0) {
1458
+ if (!keepsLineNumbers(inputs.respText, rewritten)) return null;
1459
+ const renderedOriginal = numberedRenderBytes(inputs.respText, inputs.startLine);
1460
+ const renderedRewrite = numberedRenderBytes(rewritten, inputs.startLine) + Buffer.byteLength(fold.context, "utf-8");
1461
+ if (!isStructuralRewriteAccepted(renderedOriginal, renderedRewrite, fold.ratioCap)) return null;
1462
+ return {
1463
+ output: emitRewriteWithContext(rewritten, fold.context, "read", { kind: fold.kind, originalBytes: inputs.originalBytes, detail: fold.detail }),
1464
+ deliveredRaw: fold.raw.join("\n")
1465
+ };
1466
+ }
1467
+ if (!isStructuralRewriteAccepted(inputs.originalBytes, Buffer.byteLength(rewritten, "utf-8"), fold.ratioCap)) return null;
1468
+ return {
1469
+ output: emitRewrite(rewritten, "read", { kind: fold.kind, originalBytes: inputs.originalBytes, detail: fold.detail }),
1470
+ deliveredRaw: fold.raw.join("\n")
1471
+ };
1472
+ }
1473
+ function foldMarkdownOutline(event, respText) {
1474
+ const inputs = structuralFoldInputs(event, respText);
1475
+ if (inputs === null) return null;
1476
+ const fold = planMarkdownOutline(inputs.rows, inputs.normalized, inputs.shown, inputs.originalBytes, inputs.layout);
1477
+ return fold === null ? null : emitStructuralFold(inputs, fold);
1478
+ }
1479
+ function foldSourceSkeleton(event, respText) {
1480
+ const inputs = structuralFoldInputs(event, respText);
1481
+ if (inputs === null) return null;
1482
+ const fold = planSourceSkeleton(inputs.rows, inputs.normalized, inputs.shown, inputs.originalBytes, inputs.layout);
1483
+ return fold === null ? null : emitStructuralFold(inputs, fold);
1484
+ }
1485
+ function foldCodeBodies(event, respText) {
1486
+ if (!foldingEnabled()) return null;
1487
+ const filePath = getFilePath(event);
1488
+ if (filePath === void 0) return null;
1489
+ const normalized = normalizePath(filePath);
1490
+ if (isImagePath(normalized)) return null;
1491
+ const sliceWin = readRequestedSliceWindow(event);
1492
+ const requestedOffset = sliceWin.offset;
1493
+ if (requestedOffset !== void 0 && readStartLine(event) !== requestedOffset) return null;
1494
+ if (isTruncatedReadDelivery(event, respText)) return null;
1495
+ if (hasPreciseSecret(respText)) return null;
1496
+ const parsed = parseReadDelivery(event, respText);
1497
+ if (parsed === null) return null;
1498
+ const shown = displaySafePath(toDisplayPath(findProject(getCwd(event) ?? process.cwd())?.root, normalized));
1499
+ const layout = readLayout(event);
1500
+ const folded = foldDelivery(parsed.rows, normalized, shown, sliceWin.isExplicitSlice, layout);
1501
+ if (folded === null) return null;
1502
+ const originalBytes = Buffer.byteLength(respText, "utf-8");
1503
+ if (layout === "aligned") {
1504
+ const fenced = fenceNumberedFileContent(folded.numbered.join("\n"), ALIGNED_LAYOUT_NOTE);
1505
+ const rewritten2 = [...parsed.header, fenced.body, ...parsed.trailer].join("\n");
1506
+ if (!keepsLineNumbers(respText, rewritten2)) return null;
1507
+ if (!isRewriteWorthwhile({
1508
+ originalBytes,
1509
+ rewrittenBytes: Buffer.byteLength(rewritten2, "utf-8") + Buffer.byteLength(fenced.preamble, "utf-8"),
1510
+ noticeBytes: 0,
1511
+ minNetSavingsBytes: resolveMinNetSavingsBytes()
1512
+ })) {
1513
+ return null;
1514
+ }
1515
+ return {
1516
+ output: emitRewriteWithContext(rewritten2, fenced.preamble, "read", { kind: "read:body_fold", originalBytes, detail: foldDetail(normalized, folded.folds) }),
1517
+ deliveredRaw: folded.raw.join("\n")
1518
+ };
1519
+ }
1520
+ const rewritten = [...parsed.header, fenceUntrustedFileContent(folded.numbered.join("\n")), ...parsed.trailer].join("\n");
1521
+ if (!isRewriteWorthwhile({
1522
+ originalBytes,
1523
+ rewrittenBytes: Buffer.byteLength(rewritten, "utf-8"),
1524
+ noticeBytes: 0,
1525
+ minNetSavingsBytes: resolveMinNetSavingsBytes()
1526
+ })) {
1527
+ return null;
1528
+ }
1529
+ return {
1530
+ output: emitRewrite(rewritten, "read", { kind: "read:body_fold", originalBytes, detail: foldDetail(normalized, folded.folds) }),
1531
+ deliveredRaw: folded.raw.join("\n")
1532
+ };
1533
+ }
1534
+ function postReadHandler(event) {
1535
+ const respText = extractReadOutput(event.raw);
1536
+ const elided = elideAlreadyServedLines(event, respText);
1537
+ const outlined = elided === null ? foldMarkdownOutline(event, respText) : null;
1538
+ const skeletoned = elided === null && outlined === null ? foldSourceSkeleton(event, respText) : null;
1539
+ const folded = elided === null && outlined === null && skeletoned === null ? foldCodeBodies(event, respText) : null;
1540
+ const rewrite = elided ?? outlined?.output ?? skeletoned?.output ?? folded?.output ?? null;
1541
+ const out = applyHintTracking(event, postReadHandlerInner(event, rewrite !== null), classifyReadHint);
1542
+ const deliveredRaw = outlined?.deliveredRaw ?? skeletoned?.deliveredRaw ?? folded?.deliveredRaw ?? null;
1543
+ if (deliveredRaw !== null) forgetFoldedWindow(event);
1544
+ recordReadAsServedOutput(event, deliveredRaw);
1545
+ recordLastReadShape(event, respText, outlined !== null ? "outline" : skeletoned !== null ? "skeleton" : folded !== null ? "fold" : null);
1546
+ return rewrite ?? out;
1547
+ }
1548
+ function recordLastReadShape(event, respText, rewrite) {
1549
+ try {
1550
+ const filePath = getFilePath(event);
1551
+ if (filePath === void 0) return;
1552
+ const normalized = normalizePath(filePath);
1553
+ if (isImagePath(normalized)) return;
1554
+ let shape = rewrite;
1555
+ if (shape === null && isTruncatedReadDelivery(event, respText)) shape = "truncated";
1556
+ if (shape === null && readRequestedSliceWindow(event).isExplicitSlice && windowCoversFile(event) !== true) shape = "partial";
1557
+ recordReadShape(sessionStateKey(event), normalized, shape);
1558
+ } catch {
1559
+ }
1560
+ }
1561
+ function windowCoversFile(event) {
1562
+ const resp = event.raw["tool_response"];
1563
+ const file = resp?.["file"];
1564
+ const start = file?.["startLine"];
1565
+ const num = file?.["numLines"];
1566
+ const total = file?.["totalLines"];
1567
+ if (typeof start !== "number" || typeof num !== "number" || typeof total !== "number") return void 0;
1568
+ return start <= 1 && start + num - 1 >= total;
1569
+ }
1570
+ function forgetFoldedWindow(event) {
1571
+ if (!readRequestedSliceWindow(event).isExplicitSlice) return;
1572
+ const filePath = getFilePath(event);
1573
+ if (filePath !== void 0) resetFileLineRanges(normalizePath(filePath));
1574
+ }
1575
+ registerHook("post_tool_use", postReadHandler, { toolName: "Read" });
1576
+
1577
+ // src/hooks_grep.ts
1578
+ init_define_import_meta_env();
1579
+ function grepIntInput(toolInput, key) {
1580
+ const value = toolInput[key];
1581
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1582
+ if (typeof value === "string" && value.trim() !== "") {
1583
+ const n = Number(value);
1584
+ if (Number.isFinite(n)) return n;
1585
+ }
1586
+ return void 0;
1587
+ }
1588
+ function grepSignature(toolInput) {
1589
+ const pattern = toolInput["pattern"];
1590
+ if (typeof pattern !== "string" || pattern === "") return null;
1591
+ const path8 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
1592
+ const outputMode = typeof toolInput["output_mode"] === "string" ? toolInput["output_mode"] : "files_with_matches";
1593
+ const glob = typeof toolInput["glob"] === "string" ? toolInput["glob"] : "";
1594
+ const type = typeof toolInput["type"] === "string" ? toolInput["type"] : "";
1595
+ const caseInsensitive = toolInput["-i"] === true;
1596
+ const onlyMatching = toolInput["-o"] === true;
1597
+ const multiline = toolInput["multiline"] === true;
1598
+ const lineNumbers = toolInput["-n"] !== false;
1599
+ const contextA = grepIntInput(toolInput, "-A");
1600
+ const contextB = grepIntInput(toolInput, "-B");
1601
+ const contextC = grepIntInput(toolInput, "-C") ?? grepIntInput(toolInput, "context");
1602
+ const headLimit = grepIntInput(toolInput, "head_limit");
1603
+ const offset = grepIntInput(toolInput, "offset");
1604
+ return JSON.stringify([
1605
+ pattern,
1606
+ path8,
1607
+ outputMode,
1608
+ glob,
1609
+ type,
1610
+ caseInsensitive,
1611
+ onlyMatching,
1612
+ multiline,
1613
+ lineNumbers,
1614
+ contextA,
1615
+ contextB,
1616
+ contextC,
1617
+ headLimit,
1618
+ offset
1619
+ ]);
1620
+ }
1621
+ var { post: dedupPostHandler, pre: preGrepDedupHandler } = makeDedupHintHandlers({
1622
+ toolName: "Grep",
1623
+ buildSignature: grepSignature,
1624
+ recordQuery: recordGrepQuery,
1625
+ getMatchCount: getGrepMatchCount,
1626
+ minMatchesConfigKey: "grep_dedup_min_matches",
1627
+ statName: "grep_dedup_hint"
1628
+ });
1629
+ var CONTENT_LINE_RE = /^(.+?):(\d+):(.*)$/s;
1630
+ function foldGrepContentHandler(event) {
1631
+ try {
1632
+ if (getToolName(event) !== "Grep") return passOutput();
1633
+ const toolInput = getToolInput(event);
1634
+ if (toolInput["output_mode"] !== "content") return passOutput();
1635
+ if (toolInput["multiline"] === true) return passOutput();
1636
+ if (toolInput["-A"] !== void 0 || toolInput["-B"] !== void 0 || toolInput["-C"] !== void 0 || toolInput["context"] !== void 0) {
1637
+ return passOutput();
1638
+ }
1639
+ if (toolInput["-n"] === false) return passOutput();
1640
+ const rawText = extractToolResponseField(event.raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
1641
+ if (!rawText) return passOutput();
1642
+ const text = redactSecrets(rawText).text;
1643
+ const rawLines = text.split(/\r\n|\r|\n/);
1644
+ const parsed = [];
1645
+ for (const line of rawLines) {
1646
+ if (line.trim() === "") continue;
1647
+ const m = CONTENT_LINE_RE.exec(line);
1648
+ if (!m) return passOutput();
1649
+ parsed.push({ file: m[1], lineNo: m[2], rest: m[3] });
1650
+ }
1651
+ if (parsed.length === 0) return passOutput();
1652
+ const order = [];
1653
+ const groups = /* @__PURE__ */ new Map();
1654
+ for (const p of parsed) {
1655
+ let group = groups.get(p.file);
1656
+ if (group === void 0) {
1657
+ group = [];
1658
+ groups.set(p.file, group);
1659
+ order.push(p.file);
1660
+ }
1661
+ group.push({ lineNo: p.lineNo, rest: p.rest });
1662
+ }
1663
+ const hasSharedFile = order.some((f) => groups.get(f).length >= 2);
1664
+ if (!hasSharedFile) return passOutput();
1665
+ const foldedLines = [];
1666
+ for (const f of order) {
1667
+ foldedLines.push(f);
1668
+ for (const { lineNo, rest } of groups.get(f)) {
1669
+ foldedLines.push(` ${lineNo}: ${rest}`);
1670
+ }
1671
+ }
1672
+ const rewritten = foldedLines.join("\n");
1673
+ const originalBytes = Buffer.byteLength(text, "utf-8");
1674
+ const rewrittenBytes = Buffer.byteLength(rewritten, "utf-8");
1675
+ if (!isRewriteWorthwhile({
1676
+ originalBytes,
1677
+ rewrittenBytes,
1296
1678
  noticeBytes: 0,
1297
1679
  minNetSavingsBytes: resolveMinNetSavingsBytes()
1298
1680
  })) {
@@ -1332,12 +1714,12 @@ function preGrepHandler(event) {
1332
1714
  if (getToolName(event) !== "Grep") return passOutput();
1333
1715
  const toolInput = getToolInput(event);
1334
1716
  const structSearch = extractGrepStructuralSearch(toolInput);
1335
- if (structSearch !== null) {
1717
+ if (structSearch !== null && (!structSearch.isDoc || docNavigation(structSearch.filePath).section)) {
1336
1718
  recordStat("session_hint", 0, 0);
1337
1719
  const { isDoc } = structSearch;
1338
1720
  const filePath = displaySafePath(structSearch.filePath);
1339
- const target = hintTarget(structSearch.filePath, isDoc ? "section" : "symbol", { cwd: getCwd(event) ?? process.cwd(), event });
1340
- const hint = isDoc ? leadWithCommand('token-goat section "' + filePath + "::" + target.name + '"', 'to read one section, or `token-goat outline "' + filePath + '"` to see the document outline', "Scanning a document for headings loads large match output.") : target.real ? leadWithCommand('token-goat read "' + filePath + "::" + target.name + '"', 'to inspect one symbol, or `token-goat skeleton "' + filePath + '"` to see the file structure', "Scanning a source file for symbols loads large match output.") : leadWithCommand('token-goat skeleton "' + filePath + '"', 'to see the file structure, or `token-goat read "' + filePath + '::SymbolName"` to inspect a specific symbol', "Scanning a source file for symbols loads large match output.");
1721
+ const target = hintTarget(structSearch.filePath, isDoc ? "section" : "symbol", { cwd: getCwd(event) ?? process.cwd(), event, pattern: String(toolInput["pattern"] ?? "") });
1722
+ const hint = isDoc ? docSectionHint(filePath, target.name, "Scanning a document for headings loads large match output.") : target.real ? leadWithCommand('token-goat read "' + filePath + "::" + target.name + '"', 'to inspect one symbol, or `token-goat skeleton "' + filePath + '"` to see the file structure', "Scanning a source file for symbols loads large match output.") : leadWithCommand('token-goat skeleton "' + filePath + '"', 'to see the file structure, or `token-goat read "' + filePath + '::SymbolName"` to inspect a specific symbol', "Scanning a source file for symbols loads large match output.");
1341
1723
  return contextOutput(hint);
1342
1724
  }
1343
1725
  return applyHintTracking(event, preGrepDedupHandler(event), uncorrelatedHint("grep_dedup_hint"));
@@ -1356,18 +1738,23 @@ registerHook("post_tool_use", postGrepHandler, { toolName: "Grep" });
1356
1738
 
1357
1739
  // src/hooks_glob.ts
1358
1740
  init_define_import_meta_env();
1359
- function isBroadCatchAllGlob(pattern, pathArg) {
1741
+ function isSameDirAsCwd(target, cwd) {
1742
+ const norm = (p) => foldPath(normalizePath(p).replace(/\/+$/, ""));
1743
+ return norm(target) === norm(cwd);
1744
+ }
1745
+ function isBroadCatchAllGlob(pattern, pathArg, cwd) {
1360
1746
  const p = pattern.trim();
1361
1747
  const isBroad = p === "*" || p === "**/*" || p === "**" || p === "*.*" || p === "**/*.*" || p === "**/*.**";
1362
1748
  if (!isBroad) return false;
1363
1749
  const target = (pathArg ?? "").trim().replace(/[\\/]+$/, "");
1364
- return target === "" || target === "." || target === "./" || target === ".\\" || !target.includes("/") && !target.includes("\\");
1750
+ if (target === "" || target === "." || target === "./" || target === ".\\" || !target.includes("/") && !target.includes("\\")) return true;
1751
+ return cwd !== void 0 && isSameDirAsCwd(target, cwd);
1365
1752
  }
1366
1753
  function globSignature(toolInput) {
1367
1754
  const pattern = toolInput["pattern"];
1368
1755
  if (typeof pattern !== "string" || pattern === "") return null;
1369
- const path6 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
1370
- return JSON.stringify([pattern, path6]);
1756
+ const path8 = typeof toolInput["path"] === "string" ? toolInput["path"] : "";
1757
+ return JSON.stringify([pattern, path8]);
1371
1758
  }
1372
1759
  var { post: postGlobHandler, pre: preGlobDedupHandler } = makeDedupHintHandlers({
1373
1760
  toolName: "Glob",
@@ -1383,7 +1770,7 @@ function preGlobHandler(event) {
1383
1770
  const toolInput = getToolInput(event);
1384
1771
  const pattern = typeof toolInput["pattern"] === "string" ? toolInput["pattern"] : "";
1385
1772
  const pathArg = typeof toolInput["path"] === "string" ? toolInput["path"] : typeof toolInput["paths"] === "string" ? toolInput["paths"] : Array.isArray(toolInput["paths"]) && toolInput["paths"].length === 1 && typeof toolInput["paths"][0] === "string" ? toolInput["paths"][0] : void 0;
1386
- if (pattern && isBroadCatchAllGlob(pattern, pathArg)) {
1773
+ if (pattern && isBroadCatchAllGlob(pattern, pathArg, getCwd(event))) {
1387
1774
  recordStat("session_hint", 0, 0);
1388
1775
  return contextOutput(
1389
1776
  'Broad recursive glob pattern "' + displaySafeText(pattern) + '" traverses entire directory trees and may dump thousands of paths into context. Use `token-goat map --compact` to inspect project directory structure efficiently, or narrow the glob path.'
@@ -1399,17 +1786,25 @@ registerHook("post_tool_use", postGlobHandler, { toolName: "Glob" });
1399
1786
 
1400
1787
  // src/hooks_edit.ts
1401
1788
  init_define_import_meta_env();
1402
- import * as path2 from "node:path";
1789
+ import * as path3 from "node:path";
1403
1790
  import { statSync as statSync3 } from "node:fs";
1404
1791
  function postEditHandlerInner(event) {
1405
- const filePath = getFilePath(event);
1406
- if (filePath === void 0) return passOutput();
1792
+ const filePaths = getFilePaths(event);
1793
+ if (filePaths.length === 0) return passOutput();
1794
+ const normalizedPaths = filePaths.map((p) => recordEditedFile(event, p));
1795
+ if (normalizedPaths.length !== 1) return passOutput();
1796
+ const normalized = normalizedPaths[0];
1797
+ return markdownSectionHint(event, normalized);
1798
+ }
1799
+ function recordEditedFile(event, filePath) {
1407
1800
  const normalized = normalizePath(filePath);
1408
1801
  recordFileEdit(normalized);
1409
- const underSystemTemp = isUnderSystemTemp(normalized);
1802
+ bookEditAfterPartialRead(event, normalized);
1803
+ const indexPath = resolveIndexPath(filePath, getCwd(event) ?? process.cwd());
1804
+ const underSystemTemp = isUnderSystemTemp(indexPath);
1410
1805
  if (!underSystemTemp) {
1411
1806
  try {
1412
- appendDirtyPath(normalized);
1807
+ appendDirtyPath(indexPath);
1413
1808
  } catch (e) {
1414
1809
  recordStat("dirty_queue_append_failed", 0, 0, void 0, extractErrorMessage(e));
1415
1810
  }
@@ -1419,7 +1814,7 @@ function postEditHandlerInner(event) {
1419
1814
  recordStat("worker_healthcheck_failed", 0, 0, void 0, extractErrorMessage(e));
1420
1815
  }
1421
1816
  try {
1422
- recordKnownRootThrottled(normalized);
1817
+ recordKnownRootThrottled(indexPath);
1423
1818
  } catch (e) {
1424
1819
  recordStat("known_root_record_failed", 0, 0, void 0, extractErrorMessage(e));
1425
1820
  }
@@ -1427,18 +1822,24 @@ function postEditHandlerInner(event) {
1427
1822
  if (loadConfig().hints.stable_doc_compacts) {
1428
1823
  markCompactStale(compactPathFor(normalized));
1429
1824
  }
1430
- const editedBasename = path2.basename(normalized);
1825
+ return normalized;
1826
+ }
1827
+ function markdownSectionHint(event, normalized) {
1828
+ const editedBasename = path3.basename(normalized);
1431
1829
  if (/\.(md|mdx|markdown|rst)$/i.test(editedBasename)) {
1432
1830
  let editedSize = Infinity;
1433
1831
  try {
1434
- editedSize = statSync3(normalized).size;
1832
+ editedSize = statSync3(hostPathOfIndexKey(normalized)).size;
1435
1833
  } catch {
1436
1834
  }
1835
+ const repeatKey = `edit-section-hint:${normalized}:${getCompactedAt()}`;
1836
+ if (wasHintShown(repeatKey)) return passOutput();
1437
1837
  if (!meetsSavingsFloor(editedSize)) {
1438
1838
  logSuppressedDetection("edit_reread_suggest", event.sessionId, normalized);
1439
1839
  } else {
1440
1840
  const escapedPath = displaySafePath(normalized).replace(/`/g, "\\`").replace(/"/g, '\\"');
1441
1841
  const heading = hintTarget(normalized, "section", { placeholder: "HeadingName" });
1842
+ markHintShown(repeatKey);
1442
1843
  return contextOutput(
1443
1844
  leadWithCommand('token-goat section "' + escapedPath + "::" + heading.name + '"', "to re-read a specific section rather than the full file", displaySafePath(editedBasename) + " was edited."),
1444
1845
  [escapedPath]
@@ -1447,6 +1848,16 @@ function postEditHandlerInner(event) {
1447
1848
  }
1448
1849
  return passOutput();
1449
1850
  }
1851
+ function bookEditAfterPartialRead(event, normalized) {
1852
+ try {
1853
+ const stateKey = sessionStateKey(event);
1854
+ const shape = readShapeOf(stateKey, normalized);
1855
+ if (shape === null) return;
1856
+ recordStat("edit_after_fold", 0, 0, void 0, `last_read=${shape} tool=${event.toolName ?? "unknown"}`);
1857
+ if (event.toolName === "Write") recordReadShape(stateKey, normalized, null);
1858
+ } catch {
1859
+ }
1860
+ }
1450
1861
  function postEditHandler(event) {
1451
1862
  return applyHintTracking(event, postEditHandlerInner(event), classifyEditHint);
1452
1863
  }
@@ -1457,7 +1868,7 @@ registerHook("post_tool_use", postEditHandler, { toolName: "NotebookEdit" });
1457
1868
 
1458
1869
  // src/hooks_write.ts
1459
1870
  init_define_import_meta_env();
1460
- import { readFileSync as readFileSync3, statSync as statSync4 } from "node:fs";
1871
+ import { readFileSync as readFileSync6, statSync as statSync4 } from "node:fs";
1461
1872
  var MAX_LINES_FOR_DIFF = 4e3;
1462
1873
  var MAX_OLD_FILE_BYTES_FOR_DIFF = 4 * 1024 * 1024;
1463
1874
  function splitLines(text) {
@@ -1498,7 +1909,7 @@ function preWriteRewriteHandler(event) {
1498
1909
  if (stat.size > MAX_OLD_FILE_BYTES_FOR_DIFF) return passOutput();
1499
1910
  let oldContent;
1500
1911
  try {
1501
- oldContent = readFileSync3(filePath, "utf-8");
1912
+ oldContent = readFileSync6(filePath, "utf-8");
1502
1913
  } catch {
1503
1914
  return passOutput();
1504
1915
  }
@@ -1524,7 +1935,8 @@ registerHook("pre_tool_use", preWriteRewriteHandler, { toolName: "Write" });
1524
1935
 
1525
1936
  // src/hooks_compact.ts
1526
1937
  init_define_import_meta_env();
1527
- import * as path3 from "node:path";
1938
+ import * as fs3 from "node:fs";
1939
+ import * as path4 from "node:path";
1528
1940
 
1529
1941
  // src/harness_channels.ts
1530
1942
  init_define_import_meta_env();
@@ -1536,8 +1948,21 @@ var PRE_COMPACT_CONTEXT_DROPPED = /* @__PURE__ */ new Set(["copilot_cli", "codex
1536
1948
  function dropsPreCompactContext() {
1537
1949
  return PRE_COMPACT_CONTEXT_DROPPED.has(getHarnessName());
1538
1950
  }
1951
+ var PRE_COMPACT_RESUMES_ON_PROMPT = /* @__PURE__ */ new Set(["copilot_cli"]);
1952
+ function resumesCompactionOnPrompt() {
1953
+ return PRE_COMPACT_RESUMES_ON_PROMPT.has(getHarnessName()) && !dropsPromptSubmitContext();
1954
+ }
1539
1955
 
1540
1956
  // src/hooks_compact.ts
1957
+ function recordSessionCompaction(sessionId, now) {
1958
+ const p = sessionSidecarPath(sessionId ?? "", COMPACTED_AT_SUFFIX);
1959
+ if (!p) return;
1960
+ try {
1961
+ ensureDirSync(path4.dirname(p));
1962
+ fs3.writeFileSync(p, String(now), "utf8");
1963
+ } catch {
1964
+ }
1965
+ }
1541
1966
  function preCompactHandler(event) {
1542
1967
  const cfg = loadConfig().compact_assist;
1543
1968
  let out = passOutput();
@@ -1548,12 +1973,14 @@ function preCompactHandler(event) {
1548
1973
 
1549
1974
  ${manifest}`);
1550
1975
  } else if (event.sessionId) {
1551
- queuePendingContext(sessionStateKey(event), `${MANIFEST_RECOVERY_PREAMBLE}
1976
+ replacePendingContextBlock(sessionStateKey(event), `${MANIFEST_RECOVERY_PREAMBLE}
1552
1977
 
1553
- ${manifest.slice(0, MAX_PENDING_CONTEXT_BYTES - MANIFEST_RECOVERY_PREAMBLE.length - 2)}`);
1978
+ ${manifest.slice(0, MAX_PENDING_CONTEXT_BYTES - MANIFEST_RECOVERY_PREAMBLE.length - 2)}`, MANIFEST_RECOVERY_PREAMBLE);
1554
1979
  }
1555
1980
  }
1556
- markCompacted();
1981
+ const now = Date.now();
1982
+ markCompacted(now);
1983
+ recordSessionCompaction(event.sessionId, now);
1557
1984
  return out;
1558
1985
  }
1559
1986
  registerHook("pre_compact", preCompactHandler);
@@ -1569,11 +1996,13 @@ function postCompactHandler(event) {
1569
1996
  const cwd = normalizePath(getCwd(event) ?? process.cwd());
1570
1997
  const spell = (p) => foldPath(p.replaceAll("\\", "/"));
1571
1998
  const haystack = spell(summary);
1572
- const survived = sample.filter((p) => {
1573
- const rel = path3.relative(cwd, p);
1574
- const forms = rel !== "" && !rel.startsWith("..") && !path3.isAbsolute(rel) ? [p, rel] : [p];
1999
+ const kept = sample.filter((p) => {
2000
+ const rel = path4.relative(cwd, p);
2001
+ const forms = rel !== "" && !rel.startsWith("..") && !path4.isAbsolute(rel) ? [p, rel] : [p];
1575
2002
  return forms.some((f) => haystack.includes(spell(f)));
1576
- }).length;
2003
+ });
2004
+ const survived = kept.length;
2005
+ recordCompactDroppedPaths(sessionStateKey(event), typeof raw === "string" ? sample.filter((p) => !kept.includes(p)) : null);
1577
2006
  const trigger = typeof event.raw["trigger"] === "string" ? event.raw["trigger"] : "unknown";
1578
2007
  const budget = loadConfig().compact_assist.summary_budget_chars;
1579
2008
  const over = budget > 0 && summary.length > budget ? 1 : 0;
@@ -1598,7 +2027,7 @@ init_define_import_meta_env();
1598
2027
  var TRACKED_SKILL = "token-goat";
1599
2028
  var MAX_COMMANDS_SHOWN = 8;
1600
2029
  async function currentCommandNames() {
1601
- const { buildProgram } = await import("./token-goat-chunk-IFORDBGC.mjs");
2030
+ const { buildProgram } = await import("./token-goat-chunk-HOJLCGDP.mjs");
1602
2031
  return flattenCommandNames(buildCommandManifest(buildProgram()));
1603
2032
  }
1604
2033
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -1747,10 +2176,14 @@ async function userPromptSubmitHandler(event) {
1747
2176
  }
1748
2177
  const driftNudge = await checkSkillVersionDrift(event.sessionId);
1749
2178
  const residentHints = residentContextHints(event);
1750
- if (parts.length === 0 && !driftNudge && residentHints.length === 0) {
2179
+ const resumed = resumesCompactionOnPrompt() ? peekPendingContext(sessionStateKey(event)) : null;
2180
+ if (parts.length === 0 && !driftNudge && residentHints.length === 0 && resumed === null) {
1751
2181
  return passOutput();
1752
2182
  }
1753
2183
  const lines = [];
2184
+ if (resumed !== null) {
2185
+ lines.push(resumed);
2186
+ }
1754
2187
  if (parts.length > 0) {
1755
2188
  lines.push("[" + parts.join(" | ") + "]");
1756
2189
  }
@@ -1823,20 +2256,6 @@ registerHook("post_tool_use", pendingContextHandler, { advisory: true, followsMa
1823
2256
 
1824
2257
  // src/hooks_session_start.ts
1825
2258
  init_define_import_meta_env();
1826
- var GENERIC_REMINDER = 'token-goat: prefer surgical reads over the Read/Grep tools on this codebase; shell commands like `rg`, `grep`, `fd`, `sed`, `cat`, `find`, and `ls` are just commands, not tool names -- `token-goat symbol <name>`, `token-goat read "file::symbol"`, `token-goat section "file::Heading"`, `token-goat semantic "description"`, `token-goat outline <file>`. Run `token-goat index .` if this project is not indexed yet.';
1827
- var INDEXED_REMINDER = 'token-goat: this project is indexed. Prefer `symbol <name>`, `read "file::symbol"`, `section "file::Heading"`, `semantic "description"`, or `outline <file>` over a full Read/Grep tool call; for JSON/YAML use `json-query file \'a.b.c\'` or `yaml-query` (nested keys are not symbols); shell commands like `rg`, `grep`, `fd`, `sed`, `cat`, `find`, and `ls` are still just commands.';
1828
- var NOTE_REMINDER = ' Record a finding that must outlive a compaction with `token-goat note set <key> "<finding>"`; notes come back at every session start.';
1829
- function isIndexedProject(cwd) {
1830
- if (cwd === void 0) return false;
1831
- try {
1832
- return countSymbols({ rootDir: cwd }, globalDbPath()) > 0;
1833
- } catch {
1834
- return false;
1835
- }
1836
- }
1837
- function buildReminder(indexed) {
1838
- return (indexed ? INDEXED_REMINDER : GENERIC_REMINDER) + NOTE_REMINDER;
1839
- }
1840
2259
  function reconcileNote(cwd, indexed) {
1841
2260
  if (!indexed) return null;
1842
2261
  if (!envBool(ENV_KEYS.RECONCILE, true)) return null;
@@ -1860,6 +2279,11 @@ function reconcileNote(cwd, indexed) {
1860
2279
  const clauses = [];
1861
2280
  if (drifted > 0) clauses.push(`${countNoun(drifted, "file")} that changed outside this session${breakdown}`);
1862
2281
  if (upgraded > 0) clauses.push(`${countNoun(upgraded, "file")} unchanged on disk but indexed by an older version of token-goat`);
2282
+ const unqueued = total - result.enqueued;
2283
+ if (unqueued > 0) {
2284
+ const refused = unqueued < total ? `${unqueued} of them` : total === 1 ? "it" : "they";
2285
+ return `token-goat: found ${clauses.join(" and ")}${truncated}, but ${refused} could not be queued for reindexing, so symbol lookups may stay stale until token-goat index runs here.`;
2286
+ }
1863
2287
  return `token-goat: reindexing ${clauses.join(" and ")}${truncated}. Symbol lookups may be briefly stale.`;
1864
2288
  } catch (e) {
1865
2289
  recordStat("reconcile_note_failed", 0, 0, void 0, extractErrorMessage(e));
@@ -1870,16 +2294,29 @@ async function postCompactRecovery(event) {
1870
2294
  if (event.raw["source"] !== "compact" || event.sessionId === void 0) return null;
1871
2295
  if (!loadConfig().compact_assist.enabled) return null;
1872
2296
  try {
1873
- const { buildResumePacket } = await import("./token-goat-chunk-D7GHQKQD.mjs");
2297
+ const { buildResumePacket } = await import("./token-goat-chunk-MKPF4CL5.mjs");
1874
2298
  return await buildResumePacket(event.sessionId);
1875
2299
  } catch {
1876
2300
  return null;
1877
2301
  }
1878
2302
  }
1879
2303
  async function sessionStartHandler(event) {
2304
+ return withSavingsReceipt(await sessionStartOutput(event));
2305
+ }
2306
+ function savingsReceiptText(tokens) {
2307
+ return `token-goat has saved about ${tokens.toLocaleString("en-US")} tokens so far. Run \`token-goat stats\` for the detail.`;
2308
+ }
2309
+ function withSavingsReceipt(output, homeDir) {
2310
+ if (output.hookType !== "pass" && output.hookType !== "context") return output;
2311
+ if (detectHarness() !== "claudecode") return output;
2312
+ const saved = claimFirstSavingsReceipt(void 0, homeDir);
2313
+ return saved === null ? output : { ...output, notice: savingsReceiptText(saved) };
2314
+ }
2315
+ async function sessionStartOutput(event) {
1880
2316
  const recovery = await postCompactRecovery(event);
1881
2317
  const cwd = getCwd(event);
1882
- const tail = [projectNotesFor(cwd), recovery].filter((part) => part !== null).join("\n\n");
2318
+ const { anchorStatus } = await import("./token-goat-chunk-USILZ3T3.mjs");
2319
+ const tail = [projectNotesFor(cwd, anchorStatus), recovery].filter((part) => part !== null).join("\n\n");
1883
2320
  const tailOnly = () => tail === "" ? passOutput() : contextOutput(tail);
1884
2321
  try {
1885
2322
  if (!loadConfig().hints.session_start_reminder) return tailOnly();
@@ -1902,6 +2339,16 @@ ${capsule}`;
1902
2339
  }
1903
2340
  } catch {
1904
2341
  }
2342
+ try {
2343
+ const { currentUpgradeDecision, getCachedUpdateStatus, updateAdvice } = await import("./token-goat-chunk-3CHUNJ2O.mjs");
2344
+ const update = getCachedUpdateStatus();
2345
+ const advice = update === null ? null : updateAdvice(currentUpgradeDecision(update));
2346
+ if (update?.latest && advice !== null) {
2347
+ context += `
2348
+ [token-goat update available: v${update.current} -> v${update.latest}. ${advice}]`;
2349
+ }
2350
+ } catch {
2351
+ }
1905
2352
  if (tail !== "") context += `
1906
2353
 
1907
2354
  ${tail}`;
@@ -6470,8 +6917,8 @@ function trimCharacterEnd(str, char) {
6470
6917
  function unicodeEscape(str) {
6471
6918
  return str.replace(/[\s\S]/g, (c) => "\\u" + c.charCodeAt().toString(16).padStart(4, "0"));
6472
6919
  }
6473
- function get(obj, path6) {
6474
- for (const key of path6) {
6920
+ function get(obj, path8) {
6921
+ for (const key of path8) {
6475
6922
  if (!obj) {
6476
6923
  return void 0;
6477
6924
  }
@@ -7672,8 +8119,8 @@ function withBrackets(str, brackets) {
7672
8119
  const rbr = typeof brackets[1] === "string" ? brackets[1] : "]";
7673
8120
  return lbr + str + rbr;
7674
8121
  }
7675
- function pathRewrite(path6, rewriter, baseUrl, metadata, elem) {
7676
- const modifiedPath = typeof rewriter === "function" ? rewriter(path6, metadata, elem) : path6;
8122
+ function pathRewrite(path8, rewriter, baseUrl, metadata, elem) {
8123
+ const modifiedPath = typeof rewriter === "function" ? rewriter(path8, metadata, elem) : path8;
7677
8124
  return modifiedPath[0] === "/" && baseUrl ? trimCharacterEnd(baseUrl, "/") + modifiedPath : modifiedPath;
7678
8125
  }
7679
8126
  function formatImage(elem, walk, builder, formatOptions) {
@@ -7997,9 +8444,9 @@ function handleDeprecatedOptions(options) {
7997
8444
  options.selectors.push(...tagDefinitions);
7998
8445
  options.selectors = mergeDuplicatesPreferLast(options.selectors, ((s) => s.selector));
7999
8446
  }
8000
- function set(obj, path6, value) {
8001
- const valueKey = path6.pop();
8002
- for (const key of path6) {
8447
+ function set(obj, path8, value) {
8448
+ const valueKey = path8.pop();
8449
+ for (const key of path8) {
8003
8450
  let nested = obj[key];
8004
8451
  if (!nested) {
8005
8452
  nested = {};
@@ -8067,19 +8514,30 @@ function resolveWebFetchContext(event) {
8067
8514
  }
8068
8515
  return resolveWebFetchUrl(event);
8069
8516
  }
8517
+ function webFetchPolicyUrls(event) {
8518
+ if (getToolName(event) !== "WebFetch") return [];
8519
+ const toolInput = getToolInput(event);
8520
+ const out = [];
8521
+ const single = toolInput["url"];
8522
+ if (typeof single === "string" && single !== "") out.push(single);
8523
+ const list = toolInput["urls"];
8524
+ if (Array.isArray(list)) {
8525
+ for (const entry of list) if (typeof entry === "string" && entry !== "" && !out.includes(entry)) out.push(entry);
8526
+ }
8527
+ return out;
8528
+ }
8070
8529
  function preFetchHandler(event) {
8071
8530
  try {
8072
- const urlOnlyCtx = resolveWebFetchUrl(event);
8073
- if (urlOnlyCtx !== null) {
8074
- const metadataRefusal = metadataEndpointRefusal(urlOnlyCtx.url);
8531
+ for (const url2 of webFetchPolicyUrls(event)) {
8532
+ const metadataRefusal = metadataEndpointRefusal(url2);
8075
8533
  if (metadataRefusal !== null) {
8076
8534
  return denyOutput(`WebFetch blocked: ${metadataRefusal}.`);
8077
8535
  }
8078
8536
  const wfCfg = loadConfig().webfetch;
8079
- if (wfCfg.deny.length > 0 && matchesDenyPattern(urlOnlyCtx.url, wfCfg.deny)) {
8537
+ if (wfCfg.deny.length > 0 && matchesDenyPattern(url2, wfCfg.deny)) {
8080
8538
  return denyOutput(`WebFetch blocked: URL matches a configured webfetch.deny pattern.`);
8081
8539
  }
8082
- if (wfCfg.allow.length > 0 && !matchesAllowPattern(urlOnlyCtx.url, wfCfg.allow)) {
8540
+ if (wfCfg.allow.length > 0 && !matchesAllowPattern(url2, wfCfg.allow)) {
8083
8541
  return denyOutput(`WebFetch blocked: URL does not match any configured webfetch.allow pattern.`);
8084
8542
  }
8085
8543
  }
@@ -8163,6 +8621,33 @@ registerHook("post_tool_use", postFetchHandler, { toolName: "WebFetch" });
8163
8621
  // src/hooks_skill.ts
8164
8622
  init_define_import_meta_env();
8165
8623
  import { readFile } from "node:fs/promises";
8624
+
8625
+ // src/copilot_skill_path.ts
8626
+ init_define_import_meta_env();
8627
+ import { access } from "node:fs/promises";
8628
+ import { homedir as homedir2 } from "node:os";
8629
+ import { resolve } from "node:path";
8630
+ async function copilotSkillPath(skillName, cwd) {
8631
+ const name2 = safeSkillName(skillName);
8632
+ if (!name2 || name2.includes(":")) return null;
8633
+ const roots = [];
8634
+ if (cwd) {
8635
+ roots.push(resolve(cwd, ".github", "skills"), resolve(cwd, ".agents", "skills"), resolve(cwd, ".claude", "skills"));
8636
+ }
8637
+ roots.push(resolve(copilotCliUserRoot(), "skills"), resolve(homedir2(), ".agents", "skills"));
8638
+ for (const root of roots) {
8639
+ const diskPath = resolve(root, name2, "SKILL.md");
8640
+ try {
8641
+ await access(diskPath);
8642
+ return diskPath;
8643
+ } catch {
8644
+ continue;
8645
+ }
8646
+ }
8647
+ return null;
8648
+ }
8649
+
8650
+ // src/hooks_skill.ts
8166
8651
  var OVERSIZED_FIRST_LOAD_THRESHOLD_BYTES = 6e3;
8167
8652
  var COMPACT_INLINE_MAX_BYTES = 24e3;
8168
8653
  function skillSectionArg(skillPath, body) {
@@ -8184,6 +8669,13 @@ function extractSkillName(toolInput) {
8184
8669
  function extractSkillBody(raw) {
8185
8670
  return extractToolResponseField(raw, BODY_FIRST_TOOL_RESPONSE_KEYS);
8186
8671
  }
8672
+ function skillResultIsConfirmationOnly() {
8673
+ return getHarnessName() === "copilot_cli";
8674
+ }
8675
+ function harnessSkillPath(event, skillName) {
8676
+ if (skillResultIsConfirmationOnly()) return copilotSkillPath(skillName, getCwd(event) ?? null);
8677
+ return installedSkillPath(skillName);
8678
+ }
8187
8679
  function resolveSkillContext(event) {
8188
8680
  const toolName = getToolName(event);
8189
8681
  if (toolName !== "Skill") {
@@ -8199,14 +8691,14 @@ function resolveSkillContext(event) {
8199
8691
  }
8200
8692
  return { skillName };
8201
8693
  }
8202
- function planHeadingTree(body, bodyBytes, skillName) {
8694
+ function planHeadingTree(body, bodyBytes, skillName, sourcePath) {
8203
8695
  const headings = extractMarkdownHeadings(body);
8204
8696
  if (headings.length < OUTLINE_MIN_HEADINGS) return null;
8205
8697
  const { sectionsList } = formatHeadingTreeParts(headings, skillName);
8206
8698
  const treeBytes = Buffer.byteLength(sectionsList, "utf-8");
8207
8699
  if (treeBytes > bodyBytes * OUTLINE_MAX_REPLACEMENT_RATIO) return null;
8208
8700
  const totalHeadings = extractMarkdownHeadings(body, Number.MAX_SAFE_INTEGER).length;
8209
- const phrase = totalHeadings > headings.length ? "its heading tree shows " + headings.length + " of " + totalHeadings + " headings below instead of the full body; the remaining sections are reachable only through `token-goat skill-body " + skillName + "`." : "its heading tree (" + headings.length + " headings) is inlined below instead of the full body.";
8701
+ const phrase = totalHeadings > headings.length ? "its heading tree shows " + headings.length + " of " + totalHeadings + " headings below instead of the full body; the other " + (totalHeadings - headings.length) + ' load by name just the same, and `token-goat outline "' + displaySafeText(sourcePath) + '"` lists all ' + totalHeadings + "." : "its heading tree (" + headings.length + " headings) is inlined below instead of the full body.";
8210
8702
  return { sectionsList, treeBytes, phrase };
8211
8703
  }
8212
8704
  async function preSkillHandler(event) {
@@ -8224,10 +8716,10 @@ async function preSkillHandler(event) {
8224
8716
  const denyCredit = cachedBytes !== null ? Math.min(cachedBytes, PER_FILE_COUNTERFACTUAL_CEILING) : 0;
8225
8717
  recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit), void 0, "skill-reload-deny");
8226
8718
  return denyOutput(
8227
- "Skill `" + skillName + "` was already loaded this session and is cached. Use `token-goat skill-section " + skillName + " " + skillSectionArg(await installedSkillPath(skillName)) + "` to recall a section, `token-goat skill-body " + skillName + " --compact` to recall the compact slice, or `token-goat skill-body " + skillName + "` for the full body instead of re-loading it."
8719
+ "Skill `" + skillName + "` was already loaded this session and is cached. Use `token-goat skill-section " + skillName + " " + skillSectionArg(await harnessSkillPath(event, skillName)) + "` to recall a section, `token-goat skill-body " + skillName + " --compact` to recall the compact slice, or `token-goat skill-body " + skillName + "` for the full body instead of re-loading it."
8228
8720
  );
8229
8721
  }
8230
- const sourcePath = await installedSkillPath(skillName);
8722
+ const sourcePath = await harnessSkillPath(event, skillName);
8231
8723
  if (sourcePath) {
8232
8724
  try {
8233
8725
  const body = await readFile(sourcePath, "utf-8");
@@ -8244,7 +8736,7 @@ async function preSkillHandler(event) {
8244
8736
  );
8245
8737
  }
8246
8738
  }
8247
- const tree = planHeadingTree(body, bodyBytes, skillName);
8739
+ const tree = planHeadingTree(body, bodyBytes, skillName, sourcePath);
8248
8740
  if (tree !== null) {
8249
8741
  const replaced = compact !== null ? Buffer.byteLength(compact, "utf-8") : bodyBytes;
8250
8742
  const savedBytes = Math.max(0, replaced - tree.treeBytes);
@@ -8274,13 +8766,23 @@ async function postSkillHandler(event) {
8274
8766
  if (ctx === null) {
8275
8767
  return passOutput();
8276
8768
  }
8277
- const { skillName } = ctx;
8278
- recordStat("skill_load");
8279
- const body = extractSkillBody(event.raw);
8769
+ const { skillName } = ctx;
8770
+ recordStat("skill_load");
8771
+ let body;
8772
+ let sourcePath;
8773
+ if (skillResultIsConfirmationOnly()) {
8774
+ sourcePath = await harnessSkillPath(event, skillName);
8775
+ if (sourcePath === null) {
8776
+ return passOutput();
8777
+ }
8778
+ body = await readFile(sourcePath, "utf-8");
8779
+ } else {
8780
+ body = extractSkillBody(event.raw);
8781
+ sourcePath = await installedSkillPath(skillName);
8782
+ }
8280
8783
  if (!body) {
8281
8784
  return passOutput();
8282
8785
  }
8283
- const sourcePath = await installedSkillPath(skillName);
8284
8786
  await storeOutput(event.sessionId, skillName, body, sourcePath ? { sourcePath } : void 0);
8285
8787
  await incrementSkillHit(skillName);
8286
8788
  await recordSkillVersionSnapshot(event.sessionId, skillName);
@@ -8309,7 +8811,7 @@ function dualShellArg(s) {
8309
8811
  if (SHELL_UNSAFE_IN_EITHER_QUOTING_RE.test(s)) return null;
8310
8812
  return `"${s}"`;
8311
8813
  }
8312
- function detectStructuralIndexRewrite(rawCmd, cwd) {
8814
+ function detectStructuralIndexRewrite(rawCmd, cwd, event) {
8313
8815
  try {
8314
8816
  const detected = detectFromCommand(rawCmd, cwd);
8315
8817
  if (detected === null || detected.filter.name !== "grep") return null;
@@ -8319,10 +8821,11 @@ function detectStructuralIndexRewrite(rawCmd, cwd) {
8319
8821
  if (bin === void 0 || pattern === void 0 || rawPath === void 0) return null;
8320
8822
  if (!ALLOWED_BINARIES.has(pathStem(bin).toLowerCase())) return null;
8321
8823
  if (/[*?[\]{}]/.test(rawPath)) return null;
8824
+ if (!commandPathIsTouchable(rawPath, event)) return null;
8322
8825
  const resolved = resolveIndexPath(rawPath, cwd);
8323
8826
  let stat;
8324
8827
  try {
8325
- stat = statSync5(resolved);
8828
+ stat = statSync5(hostPathOfIndexKey(resolved));
8326
8829
  } catch {
8327
8830
  return null;
8328
8831
  }
@@ -8367,16 +8870,17 @@ function detectStructuralIndexRewrite(rawCmd, cwd) {
8367
8870
 
8368
8871
  // src/bash_range_savings.ts
8369
8872
  init_define_import_meta_env();
8370
- import { statSync as statSync6, readFileSync as readFileSync4 } from "node:fs";
8873
+ import { statSync as statSync6, readFileSync as readFileSync7 } from "node:fs";
8371
8874
  var ALL_SYMBOLS_IN_FILE = -1;
8372
8875
  var REGION_HEADER_BYTES = 56;
8373
8876
  function rangeSubstituteFor(hintPath, cwd, ranges) {
8374
8877
  if (ranges.length === 0) return null;
8375
8878
  try {
8376
8879
  const resolved = resolveIndexPath(hintPath, cwd);
8377
- if (!statSync6(resolved).isFile()) return null;
8880
+ const onDisk = hostPathOfIndexKey(resolved);
8881
+ if (!statSync6(onDisk).isFile()) return null;
8378
8882
  if (!indexMatchesDisk(resolved)) return null;
8379
- const lines = readFileSync4(resolved, "utf8").split("\n");
8883
+ const lines = readFileSync7(onDisk, "utf8").split("\n");
8380
8884
  const symbols = querySymbols({ filePath: resolved, limit: ALL_SYMBOLS_IN_FILE });
8381
8885
  if (symbols.length === 0) return null;
8382
8886
  const bytesOf = (start, end) => Buffer.byteLength(lines.slice(start - 1, Math.min(end, lines.length)).join("\n"), "utf8");
@@ -8400,335 +8904,103 @@ function rangeSubstituteFor(hintPath, cwd, ranges) {
8400
8904
  const regions = [...regionBySpan.values()].sort((a, b) => a.start - b.start || a.end - b.end);
8401
8905
  const replacementBytes = regions.reduce((t, r) => t + bytesOf(r.start, r.end) + REGION_HEADER_BYTES, 0);
8402
8906
  const commands = regions.map(
8403
- (r) => r.name === null ? `token-goat read "${hintPath}:${r.start}-${r.end}"` : `token-goat read "${hintPath}::${r.name}"`
8404
- );
8405
- return { requestedBytes, replacementBytes, commands };
8406
- } catch {
8407
- return null;
8408
- }
8409
- }
8410
-
8411
- // src/hooks_bash.ts
8412
- import { statSync as statSync7, existsSync, readFileSync as readFileSync5 } from "node:fs";
8413
- import * as path4 from "node:path";
8414
- function compressionOptedOut(rawCmd) {
8415
- return process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0" || commandOptsOutOfCompression(rawCmd);
8416
- }
8417
- var INTERPRETER_READ_TOKEN_CAP = 2e3;
8418
- function cappedInterpreterRead(event, rawCmd, cmd, hint, denial) {
8419
- return maybeCompressRewrite(event, rawCmd, cmd, { maxTokens: INTERPRETER_READ_TOKEN_CAP, hint: stripUnsafeSuggestions(hint) }) ?? denyOutput(denial);
8420
- }
8421
- function maybeCompressRewrite(event, rawCmd, cmd, cap) {
8422
- if (compressionOptedOut(rawCmd)) return null;
8423
- if (event.raw["_tg_harness"] === "vscode") return null;
8424
- const isPwshHarness = process.platform === "win32" && (event.raw["_tg_harness"] === "codex" || event.raw["_tg_harness"] === "copilot_cli" || event.raw["tool_name"] === "powershell");
8425
- if (isPwshHarness) {
8426
- if (!canRunPowerShell()) return null;
8427
- } else {
8428
- if (!canRunWrappedShell()) return null;
8429
- }
8430
- let cfg;
8431
- try {
8432
- cfg = loadConfig().bash_compress;
8433
- } catch {
8434
- return null;
8435
- }
8436
- if (!cfg.enabled) return null;
8437
- let filterName;
8438
- if (cap !== void 0) {
8439
- if (hasBareBackground(cmd)) return null;
8440
- filterName = "passthrough";
8441
- } else {
8442
- const gateCmd = stripTrailingStderrRedirect(cmd);
8443
- const detected = detectFromCommand(gateCmd, getCwd(event));
8444
- if (detected !== null) {
8445
- filterName = detected.filter.name;
8446
- } else if (isCompressibleSingleCommand(gateCmd)) {
8447
- filterName = "generic";
8448
- } else {
8449
- return null;
8450
- }
8451
- }
8452
- if (cfg.disabled_filters.includes(filterName)) return null;
8453
- const capArgs = cap === void 0 ? "" : ` --max-tokens ${cap.maxTokens} --cap-hint-b64 ${Buffer.from(cap.hint, "utf8").toString("base64")}`;
8454
- if (isPwshHarness) {
8455
- const b64 = Buffer.from(rawCmd, "utf8").toString("base64");
8456
- if (Buffer.from(b64, "base64").toString("utf8") !== rawCmd) return null;
8457
- const wrapped2 = `token-goat compress -f ${filterName} --timeout ${cfg.timeout_seconds}${capArgs} --shell pwsh --cmd-b64 ${b64}`;
8458
- if (wrapped2.length > 24e3) return null;
8459
- return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: wrapped2 } };
8460
- }
8461
- const wrapped = `token-goat compress -f ${filterName} --timeout ${cfg.timeout_seconds}${capArgs} -c ${shellQuoteSingle(rawCmd)}`;
8462
- if (cap !== void 0 && wrapped.length > 24e3) return null;
8463
- return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: wrapped } };
8464
- }
8465
- function elideServedShellLines(cmd, output, priorIds, unknownLineNumbers = false) {
8466
- if (priorIds.length === 0) return null;
8467
- const lines = output.split("\n");
8468
- const numbers = unknownLineNumbers ? Array.from({ length: lines.length }, () => null) : deliveredLineNumbers(cmd, lines.length);
8469
- if (numbers === null) return null;
8470
- const rows = lines.map((text, i) => ({ no: numbers[i] ?? null, text, raw: text }));
8471
- const bodies = [];
8472
- for (let i = priorIds.length - 1; i >= 0; i--) {
8473
- const id = priorIds[i];
8474
- if (id === void 0) continue;
8475
- const prior = getBashOutput(id);
8476
- if (prior !== null) bodies.push(indexServedBody(id, prior.output));
8477
- }
8478
- const cuts = planServedElisions(rows, bodies);
8479
- if (cuts.length === 0) return null;
8480
- const spans = [];
8481
- const push = (text, own) => {
8482
- const sep = spans.length === 0 ? "" : "\n";
8483
- const prev = spans[spans.length - 1];
8484
- if (prev !== void 0 && prev.own === true === own) spans[spans.length - 1] = { text: prev.text + sep + text, own };
8485
- else spans.push({ text: sep + text, own });
8486
- };
8487
- let at = 0;
8488
- for (const cut of cuts) {
8489
- for (let i = at; i < cut.start; i++) push(rows[i]?.raw ?? "", false);
8490
- const first2 = rows[cut.start];
8491
- const last = rows[cut.start + cut.len - 1];
8492
- if (first2 === void 0 || last === void 0) return null;
8493
- push(servedRunNotice(first2.no, last.no, cut.id, cut.len), true);
8494
- at = cut.start + cut.len;
8495
- }
8496
- for (let i = at; i < rows.length; i++) push(rows[i]?.raw ?? "", false);
8497
- return spans;
8498
- }
8499
- function foldShellReadBodies(cmd, output, fileKey, cwd) {
8500
- if (!foldingEnabled()) return null;
8501
- if (redactSecrets(output).count > 0) return null;
8502
- const lines = output.split("\n");
8503
- const numbers = deliveredLineNumbers(cmd, lines.length);
8504
- if (numbers === null) return null;
8505
- const rows = [];
8506
- for (let i = 0; i < lines.length; i++) {
8507
- const no = numbers[i];
8508
- if (no === void 0 || no === null) return null;
8509
- rows.push({ no, text: lines[i] ?? "", raw: lines[i] ?? "" });
8510
- }
8511
- const headRead = extractHeadFile(cmd) ?? extractGetContentHead(cmd);
8512
- const windowed = extractLineRangeRead(cmd) !== null || headRead !== null && lines.length >= headRead.n;
8513
- const folded = foldDelivery(rows, fileKey, displaySafePath(toDisplayPath(findProject(cwd ?? process.cwd())?.root, fileKey)), windowed);
8514
- if (folded === null) return null;
8515
- return { text: folded.numbered.join("\n"), folds: folded.folds };
8516
- }
8517
- function foldShellReadStructure(cmd, filePath, output, fileKey, cwd) {
8518
- if (!isWholeFileDump(cmd, filePath)) return null;
8519
- const originalBytes = Buffer.byteLength(output, "utf-8");
8520
- let onDisk;
8521
- try {
8522
- onDisk = statSync7(fileKey).size;
8523
- } catch {
8524
- return null;
8525
- }
8526
- if (onDisk !== originalBytes) return null;
8527
- if (redactSecrets(output).count > 0) return null;
8528
- const lines = output.split("\n");
8529
- if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
8530
- const rows = lines.map((text2, i) => ({ no: i + 1, text: text2, raw: text2 }));
8531
- const shown = displaySafePath(toDisplayPath(findProject(cwd ?? process.cwd())?.root, fileKey));
8532
- const fold = planMarkdownOutline(rows, fileKey, shown, originalBytes) ?? planSourceSkeleton(rows, fileKey, shown, originalBytes);
8533
- if (fold === null) return null;
8534
- const text = fold.numbered.join("\n");
8535
- if (!isStructuralRewriteAccepted(originalBytes, Buffer.byteLength(text, "utf-8"), fold.ratioCap)) return null;
8536
- return { text, kind: fold.kind === "read:markdown_outline" ? "bash_compress:markdown-outline" : "bash_compress:source-skeleton", detail: fold.detail };
8537
- }
8538
- var REWRITE_CLIP_NOTE = "\n[token-goat: rewrite clipped to the harness delivery cap; the pointer above recalls the untouched output]";
8539
- function fenceRewriteWithinCap(spans) {
8540
- const joined = spans.map((s) => s.text).join("");
8541
- const fenced = fenceUntrustedSpans(spans, UNTRUSTED_TOOL_TAG);
8542
- if (fenced === joined) return joined;
8543
- const overhead = Buffer.byteLength(fenced, "utf-8") - Buffer.byteLength(joined, "utf-8") + Buffer.byteLength(REWRITE_CLIP_NOTE, "utf-8");
8544
- const cut = clipToDeliveryCap(joined, overhead);
8545
- if (!cut.clipped) return fenced;
8546
- const kept = [];
8547
- let left2 = cut.text.length;
8548
- for (const s of spans) {
8549
- if (left2 <= 0) break;
8550
- kept.push(s.text.length <= left2 ? s : { ...s, text: s.text.slice(0, left2) });
8551
- left2 -= s.text.length;
8552
- }
8553
- return fenceUntrustedSpans(kept, UNTRUSTED_TOOL_TAG) + REWRITE_CLIP_NOTE;
8554
- }
8555
- async function maybeCollapseIdenticalRead(cmd, rawCmd, optedOut, output, exitCode, cwd, cacheMinBytes, persisted = false) {
8556
- if (optedOut) return null;
8557
- if (exitCode !== null && exitCode !== 0) return null;
8558
- const filePath = pureFileReadPath(cmd);
8559
- if (filePath === null) return null;
8560
- const originalBytes = Buffer.byteLength(output, "utf-8");
8561
- if (originalBytes < Math.max(cacheMinBytes, IDENTICAL_READ_MIN_BODY_BYTES)) return null;
8562
- const fileKey = resolveIndexPath(filePath, cdPrefixCwd(rawCmd, cwd ?? process.cwd()));
8563
- const priorIds = getFileServedOutputs(fileKey);
8564
- let containerId = null;
8565
- let identical = false;
8566
- for (let i = priorIds.length - 1; i >= 0; i--) {
8567
- const id = priorIds[i];
8568
- if (id === void 0) continue;
8569
- const prior = getBashOutput(id);
8570
- if (prior === null || !containsLineRun(prior.output, output)) continue;
8571
- containerId = id;
8572
- identical = prior.output === output;
8573
- break;
8574
- }
8575
- const sessionKey = shortFingerprint(stripOutputPipeline(cmd));
8576
- if (containerId === null) {
8577
- const elided = elideServedShellLines(cmd, output, priorIds);
8578
- const priced = (text) => isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(text, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() });
8579
- let rewrite = null;
8580
- if (elided !== null) {
8581
- const fenced = fenceRewriteWithinCap(elided);
8582
- if (priced(fenced)) rewrite = { text: fenced, reason: "already-served file lines withheld", kind: "bash_compress:served-elide" };
8583
- } else {
8584
- const structural = foldShellReadStructure(cmd, filePath, output, fileKey, cwd);
8585
- if (structural !== null && priced(structural.text)) {
8586
- rewrite = { text: structural.text, reason: structural.kind === "bash_compress:markdown-outline" ? "document replaced with its heading tree" : "source replaced with its structural skeleton", kind: structural.kind, detail: structural.detail };
8587
- }
8588
- if (rewrite === null) {
8589
- const folded = foldShellReadBodies(cmd, output, fileKey, cwd);
8590
- if (folded !== null) {
8591
- const fenced = fenceRewriteWithinCap([{ text: folded.text }]);
8592
- if (priced(fenced)) rewrite = { text: fenced, reason: "code bodies folded", kind: "bash_compress:body-fold", detail: foldDetail(fileKey, folded.folds) };
8593
- }
8594
- }
8595
- }
8596
- const storedId = await storeBashOutput(cmd, rewrite?.text ?? output, exitCode ?? 0, cwd);
8597
- recordBashOutput(sessionKey, storedId, originalBytes);
8598
- if (!persisted) recordFileServedOutput(fileKey, storedId);
8599
- if (rewrite === null) return null;
8600
- return emitRewrite(rewrite.text, rewrite.reason, { kind: rewrite.kind, originalBytes: deliveredOutputBytes(originalBytes), ...rewrite.detail === void 0 ? {} : { detail: rewrite.detail } });
8601
- }
8602
- const pointer = identical ? "[token-goat] Identical to an earlier run of this command in this session; the file has not changed since. " + originalBytes + " bytes withheld -- recall them with `token-goat bash-output " + containerId + " --full`." : "[token-goat] These " + originalBytes + " bytes already appear verbatim inside a wider read of " + filePath + " served earlier in this session. Withheld -- recall the full earlier output with `token-goat bash-output " + containerId + " --full`.";
8603
- if (!isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(pointer, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) return null;
8604
- return emitRewrite(pointer, identical ? "identical file re-read collapsed" : "already-served file lines collapsed", { kind: identical ? "bash_compress:identical-reread" : "bash_compress:contained-reread", originalBytes: deliveredOutputBytes(originalBytes) });
8605
- }
8606
- async function maybeElideServedGenericOutput(cmd, optedOut, output, exitCode, cwd, cacheMinBytes, persisted = false) {
8607
- if (optedOut) return null;
8608
- if (!loadConfig().bash_compress.elide_served_shell_output) return null;
8609
- if (exitCode !== null && exitCode !== 0) return null;
8610
- if (redactSecrets(output).count > 0) return null;
8611
- const originalBytes = Buffer.byteLength(output, "utf-8");
8612
- if (originalBytes < cacheMinBytes) return null;
8613
- const priorIds = getFileServedOutputs(GENERIC_SERVED_OUTPUT_KEY).filter((id) => getBashOutput(id)?.command !== cmd);
8614
- let rewrittenText = null;
8615
- if (priorIds.length > 0) {
8616
- const elided = elideServedShellLines(cmd, output, priorIds, true);
8617
- const fenced = elided === null ? null : fenceRewriteWithinCap(elided);
8618
- if (fenced !== null && isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(fenced, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) {
8619
- rewrittenText = fenced;
8620
- }
8621
- }
8622
- if (!persisted) recordFileServedOutput(GENERIC_SERVED_OUTPUT_KEY, await storeBashOutput(cmd, rewrittenText ?? output, exitCode ?? 0, cwd));
8623
- if (rewrittenText === null) return null;
8624
- return emitRewrite(rewrittenText, "already-served shell output collapsed", { kind: "bash_compress:generic-served-elision", originalBytes: deliveredOutputBytes(originalBytes) });
8625
- }
8626
- async function maybeCompressCompoundOutput(cmd, optedOut, output, exitCode, cwd, cacheMinBytes, isUnwrapped = false) {
8627
- if (optedOut) return null;
8628
- if (!isUnwrapped) return null;
8629
- if (isFullRecallCommand(cmd)) return null;
8630
- if (exitCode !== null && exitCode !== 0) return null;
8631
- let cfg;
8632
- try {
8633
- cfg = loadConfig().bash_compress;
8634
- } catch {
8635
- return null;
8636
- }
8637
- if (!cfg.enabled || cfg.disabled_filters.includes("generic")) return null;
8638
- if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return null;
8639
- const shaped = pipelineShapeFilter(cmd, cwd) ?? (isUnwrapped ? detectFromCommand(cmd, cwd ?? void 0) ?? null : null);
8640
- const useShaped = shaped !== null && !cfg.disabled_filters.includes(shaped.filter.name);
8641
- const filter3 = useShaped ? shaped.filter : filterByName("generic");
8642
- if (filter3 === null) return null;
8643
- const filterArgv = useShaped ? shaped.argv : [];
8644
- const compressed = compressOutput(filter3, output, "", exitCode ?? 0, filterArgv, {
8645
- maxLines: cfg.max_lines,
8646
- maxBytes: cfg.max_bytes
8647
- });
8648
- const minNet = resolveMinNetSavingsBytes();
8649
- if (!compressed.worthApplying(minNet)) return null;
8650
- const id = bashOutputIdSync(cmd, output, cwd);
8651
- const { body: untrusted, notices } = splitOwnTrailingNotices(compressed.text);
8652
- const marker = compressed.withMarker(minNet).slice(compressed.text.length);
8653
- const body = fenceUntrusted(untrusted, UNTRUSTED_TOOL_TAG) + notices + marker + "\n[token-goat] full output: bash-output " + id + " --full";
8654
- const emittedBytes = Buffer.byteLength(body, "utf-8");
8655
- if (!isRewriteWorthwhile({
8656
- originalBytes: compressed.originalBytes,
8657
- rewrittenBytes: emittedBytes,
8658
- noticeBytes: 0,
8659
- minNetSavingsBytes: minNet
8660
- })) {
8661
- return null;
8662
- }
8663
- await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8664
- return emitRewrite(body, "bash", { kind: `bash_compress:${filter3.name}`, originalBytes: deliveredOutputBytes(compressed.originalBytes) });
8665
- }
8666
- function maybeStripAnsiOnly(output) {
8667
- if (!output.includes("\x1B")) return null;
8668
- let cfg;
8669
- try {
8670
- cfg = loadConfig().bash_compress;
8907
+ (r) => r.name === null ? `token-goat read "${hintPath}:${r.start}-${r.end}"` : `token-goat read "${hintPath}::${r.name}"`
8908
+ );
8909
+ return { requestedBytes, replacementBytes, commands };
8671
8910
  } catch {
8672
8911
  return null;
8673
8912
  }
8674
- if (!cfg.enabled || cfg.disabled_filters.includes("ansi")) return null;
8675
- const stripped = stripAnsiEscapes(output);
8676
- const originalBytes = Buffer.byteLength(output, "utf-8");
8677
- if (!isRewriteWorthwhile({
8678
- originalBytes,
8679
- rewrittenBytes: Buffer.byteLength(stripped, "utf-8"),
8680
- noticeBytes: 0,
8681
- minNetSavingsBytes: resolveMinNetSavingsBytes()
8682
- })) {
8683
- return null;
8684
- }
8685
- return emitRewrite(stripped, "ansi escapes stripped", { kind: "bash_compress:ansi", originalBytes: deliveredOutputBytes(originalBytes) }, "counted-elsewhere");
8686
8913
  }
8687
- function maybeFoldCurlHtml(cmd, output, id) {
8688
- if (!isCurlGetCommand(cmd) || !looksLikeHtml(output)) return null;
8914
+
8915
+ // src/hooks_bash.ts
8916
+ import { statSync as statSync7, existsSync, readFileSync as readFileSync8 } from "node:fs";
8917
+ import * as path5 from "node:path";
8918
+ var INTERPRETER_READ_TOKEN_CAP = 2e3;
8919
+ function cappedInterpreterRead(event, rawCmd, cmd, hint, denial) {
8920
+ return maybeCompressRewrite(event, rawCmd, cmd, { maxTokens: INTERPRETER_READ_TOKEN_CAP, hint: stripUnsafeSuggestions(hint) }) ?? denyOutput(denial);
8921
+ }
8922
+ function maybeCompressRewrite(event, rawCmd, cmd, cap) {
8923
+ if (/^(?:token-goat|tg|npx\s+token-goat)(?:\s|$)/.test(cmd.trimStart())) return null;
8924
+ if (compressionOptedOut(rawCmd)) return null;
8925
+ if (event.raw["_tg_harness"] === "vscode") return null;
8926
+ const isPwshHarness = process.platform === "win32" && (event.raw["_tg_harness"] === "codex" || event.raw["_tg_harness"] === "copilot_cli" || event.raw["tool_name"] === "powershell");
8927
+ if (isPwshHarness) {
8928
+ if (!canRunPowerShell()) return null;
8929
+ } else {
8930
+ if (!canRunWrappedShell()) return null;
8931
+ }
8689
8932
  let cfg;
8690
8933
  try {
8691
8934
  cfg = loadConfig().bash_compress;
8692
8935
  } catch {
8693
8936
  return null;
8694
8937
  }
8695
- if (!cfg.enabled || cfg.disabled_filters.includes("curl-html")) return null;
8696
- let cleaned;
8938
+ if (!cfg.enabled) return null;
8939
+ let filterName;
8940
+ if (cap !== void 0) {
8941
+ if (hasBareBackground(cmd)) return null;
8942
+ filterName = "passthrough";
8943
+ } else {
8944
+ const gateCmd = stripTrailingStderrRedirect(cmd);
8945
+ const detected = detectFromCommand(gateCmd, commandRunDir(rawCmd, getCwd(event) ?? null) ?? void 0);
8946
+ if (detected !== null) {
8947
+ filterName = detected.filter.name;
8948
+ } else if (isCompressibleSingleCommand(gateCmd)) {
8949
+ filterName = "generic";
8950
+ } else {
8951
+ return null;
8952
+ }
8953
+ }
8954
+ if (cfg.disabled_filters.includes(filterName)) return null;
8955
+ const capArgs = cap === void 0 ? "" : ` --max-tokens ${cap.maxTokens} --cap-hint-b64 ${Buffer.from(cap.hint, "utf8").toString("base64")}`;
8956
+ if (isPwshHarness) {
8957
+ const b64 = Buffer.from(rawCmd, "utf8").toString("base64");
8958
+ if (Buffer.from(b64, "base64").toString("utf8") !== rawCmd) return null;
8959
+ const wrapped2 = `token-goat compress -f ${filterName} --timeout ${cfg.timeout_seconds}${capArgs} --shell pwsh --cmd-b64 ${b64}`;
8960
+ if (wrapped2.length > 24e3) return null;
8961
+ return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: wrapped2 } };
8962
+ }
8963
+ const wrapped = `token-goat compress -f ${filterName} --timeout ${cfg.timeout_seconds}${capArgs} -c ${shellQuoteSingle(rawCmd)}`;
8964
+ if (cap !== void 0 && wrapped.length > 24e3) return null;
8965
+ return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: wrapped } };
8966
+ }
8967
+ function wholeFileRange(filePath, hintPath, cwd, ranges, tool) {
8968
+ const [range] = ranges;
8969
+ if (ranges.length !== 1 || range === void 0 || range[0] > 1) return null;
8970
+ const cat = classifyCatPath(filePath, tool);
8971
+ if (cat === null) return null;
8697
8972
  try {
8698
- cleaned = extractCleanText(output);
8973
+ const onDisk = hostPathOfTypedPath(hintPath, cwd);
8974
+ const st = statSync7(onDisk);
8975
+ if (!st.isFile() || st.size > SLICE_ESTIMATE_SCAN_CAP_BYTES) return null;
8976
+ const total = countTextLines(readFileSync8(onDisk, "utf8"));
8977
+ if (total === 0 || range[1] < total) return null;
8978
+ return { start: range[0], end: range[1], cat, reason: "`" + tool + "` over lines " + range[0] + "-" + range[1] + " is the whole file (" + total + " lines), and loads all of it into context." };
8699
8979
  } catch {
8700
8980
  return null;
8701
8981
  }
8702
- const originalBytes = Buffer.byteLength(output, "utf-8");
8703
- const notice = `[token-goat: curl HTML body cleaned via extractCleanText; use \`token-goat bash-output ${id} --full\` to recall the raw markup]
8704
- `;
8705
- const noticeBytes = Buffer.byteLength(notice, "utf-8");
8706
- const clipNote = "\n[token-goat: cleaned text clipped to the harness delivery cap; the notice at the top of this output recalls the full raw markup]";
8707
- const fenceOverhead = Buffer.byteLength(fenceUntrusted(cleaned, UNTRUSTED_TOOL_TAG), "utf-8") - Buffer.byteLength(cleaned, "utf-8");
8708
- const cut = clipToDeliveryCap(cleaned, noticeBytes + fenceOverhead + Buffer.byteLength(clipNote, "utf-8"));
8709
- const body = cut.text;
8710
- const clipped = cut.clipped ? clipNote : "";
8711
- const fenced = fenceUntrusted(body, UNTRUSTED_TOOL_TAG) + clipped;
8712
- if (!isRewriteWorthwhile({
8713
- originalBytes,
8714
- rewrittenBytes: Buffer.byteLength(fenced, "utf-8"),
8715
- noticeBytes,
8716
- minNetSavingsBytes: resolveMinNetSavingsBytes()
8717
- })) {
8718
- return null;
8719
- }
8720
- return emitRewrite(notice + fenced, "curl HTML body cleaned", { kind: "bash_compress:curl-html", originalBytes: deliveredOutputBytes(originalBytes) });
8721
8982
  }
8722
8983
  function preBashHandlerInner(event) {
8723
8984
  const rawCmd = extractCommand(event);
8724
8985
  if (rawCmd === void 0) return passOutput();
8725
8986
  const cmd = stripCommandPrefix(rawCmd);
8726
- const cdStripped = stripCdPrefix(rawCmd) !== rawCmd;
8987
+ const grouped = stripSubshellGroup(rawCmd);
8988
+ const cdStripped = stripCdPrefix(grouped) !== grouped;
8727
8989
  const preHookCwd = getCwd(event) ?? null;
8990
+ const toolUseId = event.raw["tool_use_id"];
8991
+ if (typeof toolUseId === "string" && toolUseId !== "" && preHookCwd !== null) recordBashStartCwd(toolUseId, preHookCwd);
8728
8992
  const hintCwd = preHookCwd ?? process.cwd();
8993
+ const runDir = commandRunDir(rawCmd, preHookCwd);
8729
8994
  const pathHint = (paths, text) => contextOutput(text, typeof paths === "string" ? [paths] : paths);
8730
8995
  const targetFor = (hintPath, slice = sliceForPath(hintPath)) => hintTarget(hintPath, slice, { cwd: hintCwd, event });
8996
+ const taskOutputPath = (typed) => {
8997
+ if (!commandPathIsTouchable(typed, event)) return null;
8998
+ if (path5.isAbsolute(typed) || path5.win32.isAbsolute(typed)) return { probe: typed, shown: displaySafePath(typed) };
8999
+ const probe = resolveIndexPath(typed, cdPrefixCwd(rawCmd, hintCwd));
9000
+ return { probe, shown: displaySafePath(probe) };
9001
+ };
8731
9002
  const pricedSubstitute = (hintPath, start, end) => {
9003
+ if (!commandPathIsTouchable(hintPath, event)) return null;
8732
9004
  const sub = rangeSubstituteFor(hintPath, hintCwd, [[start, end]]);
8733
9005
  return sub !== null && meetsSavingsFloor(sub.requestedBytes - sub.replacementBytes) ? sub : null;
8734
9006
  };
@@ -8743,6 +9015,12 @@ function preBashHandlerInner(event) {
8743
9015
  return false;
8744
9016
  };
8745
9017
  const cfg = loadConfig();
9018
+ if (cfg.hints.deny_bash_double_backslash && process.platform === "win32" && event.raw["_tg_harness"] === "claude" && rawCmd.includes("\\\\") && detectHarness() === "claudecode") {
9019
+ recordStat("session_hint", 0, 0);
9020
+ return denyOutput(
9021
+ "This command has two backslashes in a row. Claude Code's Bash tool on Windows halves every run of two or more backslashes before Git Bash sees the command, so it would run with different text than you wrote, with no error (anthropics/claude-code#85856). Write text containing backslashes to a file with the Write tool and run it by path, or use the PowerShell tool, which passes the command unchanged. For paths, use forward slashes: C:/Users/me works in Git Bash. To turn this check off, set hints.deny_bash_double_backslash = false or TOKEN_GOAT_DENY_BASH_DOUBLE_BACKSLASH=0."
9022
+ );
9023
+ }
8746
9024
  if (cfg.hints.warn_unbalanced_shell_quoting) {
8747
9025
  const quoteError = detectUnbalancedShellSyntax(cmd);
8748
9026
  if (quoteError !== null) {
@@ -8754,9 +9032,11 @@ function preBashHandlerInner(event) {
8754
9032
  }
8755
9033
  const taskOutput = extractTasksOutput(cmd);
8756
9034
  if (taskOutput !== null) {
8757
- const { id, path: outPath, n } = taskOutput;
9035
+ const { id, n } = taskOutput;
9036
+ const outPaths = taskOutputPath(taskOutput.path);
8758
9037
  recordStat("session_hint", 0, 0);
8759
- if (taskOutputIsJsonlTranscript(outPath)) {
9038
+ if (outPaths !== null && taskOutputIsJsonlTranscript(outPaths.probe)) {
9039
+ const outPath = outPaths.shown;
8760
9040
  const tail = n ?? 50;
8761
9041
  return denyOutput(
8762
9042
  "Task output " + id + ' is a JSONL agent transcript on disk. Use `token-goat bash-output --file "' + outPath + '" --transcript` to read the assistant text, then narrow with `--grep PATTERN` or `--tail ' + tail + '`, or read a specific line range (the only way to reach the MIDDLE of a large artifact) with `token-goat read "' + outPath + '@START-END"`, instead of reading the whole file.'
@@ -8814,13 +9094,22 @@ function preBashHandlerInner(event) {
8814
9094
  const hintPaths = [];
8815
9095
  for (const { filePath, ranges, tool } of sedReads) {
8816
9096
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9097
+ if (!commandPathIsTouchable(hintPath, event)) continue;
8817
9098
  hintPaths.push(hintPath);
8818
9099
  const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
9100
+ const whole = singleLineRangeRead === null ? null : wholeFileRange(filePath, hintPath, hintCwd, ranges, tool);
9101
+ if (whole !== null && findRangeOverlap(getFileLineRanges(sedDedupKey), whole.start, whole.end) === null) {
9102
+ recordStat("session_hint", 0, 0);
9103
+ if (whole.cat.isSql) return pathHint(hintPath, sqlTableHint(hintPath, targetFor(hintPath, "table"), whole.reason));
9104
+ const hint = surgicalHintFor(hintPath, whole.cat.isEnv, whole.cat.isConfig, whole.cat.isDoc, whole.cat.isXml, targetFor(hintPath, whole.cat.isEnv ? "key" : void 0), whole.reason);
9105
+ return cdStripped ? pathHint(hintPath, hint) : denyOutput(hint);
9106
+ }
8819
9107
  const overlapHints = [];
8820
9108
  const freshRanges = [];
9109
+ const onDisk = isFileAtIndexKey(sedDedupKey);
8821
9110
  for (const [start, end] of ranges) {
8822
9111
  const priorOverlap = findRangeOverlap(getFileLineRanges(sedDedupKey), start, end);
8823
- recordFileLineRange(sedDedupKey, start, end);
9112
+ if (onDisk) recordFileLineRange(sedDedupKey, start, end);
8824
9113
  if (priorOverlap !== null) {
8825
9114
  overlapHints.push(sedOverlapHint(hintPath, priorOverlap, start, end));
8826
9115
  } else {
@@ -8933,8 +9222,9 @@ function preBashHandlerInner(event) {
8933
9222
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
8934
9223
  recordStat("session_hint", 0, 0);
8935
9224
  if (isOutputFile) {
8936
- if (taskOutputIsJsonlTranscript(filePath)) {
8937
- 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.';
9225
+ const outPaths = taskOutputPath(filePath);
9226
+ if (outPaths !== null && taskOutputIsJsonlTranscript(outPaths.probe)) {
9227
+ const tHint = 'This `.output` file is a JSONL agent transcript. Use `token-goat bash-output --file "' + outPaths.shown + '" --transcript` to read the assistant text, then narrow with `--grep PATTERN` or `--tail N`, instead of hand-parsing the JSONL.';
8938
9228
  return cdStripped ? pathHint(hintPath, tHint) : denyOutput(tHint);
8939
9229
  }
8940
9230
  return pathHint(
@@ -8995,7 +9285,7 @@ function preBashHandlerInner(event) {
8995
9285
  recordStat("session_hint", 0, 0);
8996
9286
  return cdStripped ? pathHint(hintPath, denial) : cappedInterpreterRead(event, rawCmd, cmd, hint, denial);
8997
9287
  }
8998
- const structuralRewrite = detectStructuralIndexRewrite(rawCmd, hintCwd);
9288
+ const structuralRewrite = detectStructuralIndexRewrite(rawCmd, hintCwd, event);
8999
9289
  if (structuralRewrite !== null) {
9000
9290
  return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: structuralRewrite.command } };
9001
9291
  }
@@ -9010,7 +9300,7 @@ function preBashHandlerInner(event) {
9010
9300
  const { filePath } = mdHeadingGrep;
9011
9301
  const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9012
9302
  recordStat("session_hint", 0, 0);
9013
- return pathHint(hintPath, leadWithCommand('token-goat section "' + hintPath + "::" + targetFor(hintPath, "section").name + '"', 'to read one section, or `token-goat outline "' + hintPath + '"` for every heading with line ranges'));
9303
+ return pathHint(hintPath, docSectionHint(hintPath, targetFor(hintPath, "section").name));
9014
9304
  }
9015
9305
  const rgSymbol = extractRgSymbolSearch(cmd);
9016
9306
  if (rgSymbol !== null) {
@@ -9033,11 +9323,11 @@ function preBashHandlerInner(event) {
9033
9323
  }
9034
9324
  const monitoringHint = getMonitoringRecallHint(cmd);
9035
9325
  if (monitoringHint !== null) {
9036
- const monCmdHash = shortFingerprint(stripOutputPipeline(cmd));
9326
+ const monCmdHash = bashRecallKey(cmd, runDir);
9037
9327
  const monOutputId = getBashOutputId(monCmdHash);
9038
9328
  const monEntryRaw = monOutputId !== null ? getBashOutput(monOutputId) : null;
9039
- const monEntry = monEntryRaw !== null && !isBashEntryStale(monEntryRaw, cmd, preHookCwd) ? monEntryRaw : null;
9040
- if (monOutputId !== null && monEntry !== null && recallWorthShowing(monOutputId, monEntry)) {
9329
+ const monEntry = monEntryRaw !== null && !isBashEntryStale(monEntryRaw, cmd, runDir) ? monEntryRaw : null;
9330
+ if (monOutputId !== null && monEntry !== null && cachedRunCoversCommand(monEntry.command, cmd) && recallWorthShowing(monOutputId, monEntry)) {
9041
9331
  const monBytes = monEntry.sizeBytes;
9042
9332
  const catFile = extractCatSourceFile(cmd);
9043
9333
  if (catFile !== null) {
@@ -9046,7 +9336,7 @@ function preBashHandlerInner(event) {
9046
9336
  "Prior output from `" + cmd + "`" + pipelineDivergenceNote(cmd, monEntry.command) + " is cached. Use `token-goat bash-output " + monOutputId + "` to recall the full file, or `" + sliceCommand(displaySafePath(catFile), targetFor(displaySafePath(catFile))) + "` to extract only the part you need."
9047
9337
  );
9048
9338
  }
9049
- const cmdSummary = cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
9339
+ const cmdSummary = cmd.length > 60 ? cmd.slice(0, 57).trimEnd() + "..." : cmd;
9050
9340
  recordStat("bash_compress:recall", monBytes, savedTokensFromBytes(monBytes));
9051
9341
  return contextOutput(
9052
9342
  "Prior output from `" + cmdSummary + "`" + pipelineDivergenceNote(cmd, monEntry.command) + " is cached.\nUse `token-goat bash-output " + monOutputId + " " + monitoringHint + "` to re-inspect without re-running."
@@ -9056,10 +9346,10 @@ function preBashHandlerInner(event) {
9056
9346
  const curlDl = extractCurlDownload(cmd);
9057
9347
  if (curlDl !== null) {
9058
9348
  const prevPath = getCurlDownloadPath(curlDl.url);
9059
- const prevResolvedPath = prevPath !== null && commandPathIsTouchable(prevPath, event) ? resolveIndexPath(prevPath, preHookCwd ?? process.cwd()) : null;
9060
- if (prevPath !== null && prevResolvedPath !== null && !existsSync(prevResolvedPath)) {
9349
+ const prevOnDisk = prevPath !== null && commandPathIsTouchable(prevPath, event) ? hostPathOfTypedPath(prevPath, preHookCwd ?? process.cwd()) : null;
9350
+ if (prevPath !== null && prevOnDisk !== null && !existsSync(prevOnDisk)) {
9061
9351
  clearCurlDownload(curlDl.url);
9062
- } else if (prevPath !== null && prevResolvedPath !== null && statSync7(prevResolvedPath).size >= loadConfig().hints.bash_dedup_min_bytes) {
9352
+ } else if (prevPath !== null && prevOnDisk !== null && statSync7(prevOnDisk).size >= loadConfig().hints.bash_dedup_min_bytes) {
9063
9353
  recordStat("session_hint", 0, 0);
9064
9354
  return denyOutput(leadWithCommand(sliceCommand(prevPath, targetFor(prevPath)), "to read a part of it, or `rg '<pattern>' " + prevPath + "` to search it", "Already downloaded to " + prevPath + " earlier this session."));
9065
9355
  }
@@ -9069,7 +9359,7 @@ function preBashHandlerInner(event) {
9069
9359
  const curlHash = shortFingerprint(curlCacheKey);
9070
9360
  const curlOutputId = getBashOutputId(curlHash);
9071
9361
  const curlEntryRaw = curlOutputId !== null ? getBashOutput(curlOutputId) : null;
9072
- const curlEntry = curlEntryRaw !== null && !isBashEntryStale(curlEntryRaw, cmd, preHookCwd) ? curlEntryRaw : null;
9362
+ const curlEntry = curlEntryRaw !== null && !isBashEntryStale(curlEntryRaw, cmd, runDir) ? curlEntryRaw : null;
9073
9363
  if (curlOutputId !== null && curlEntry !== null && recallWorthShowing(curlOutputId, curlEntry)) {
9074
9364
  const curlBytes = curlEntry.sizeBytes;
9075
9365
  recordStat("bash_compress:recall", curlBytes, savedTokensFromBytes(curlBytes));
@@ -9081,10 +9371,10 @@ function preBashHandlerInner(event) {
9081
9371
  }
9082
9372
  }
9083
9373
  if (isReadOnlyGhApi(cmd)) {
9084
- const ghHash = shortFingerprint(stripOutputPipeline(cmd));
9374
+ const ghHash = bashRecallKey(cmd, runDir);
9085
9375
  const ghOutputId = getBashOutputId(ghHash);
9086
9376
  const ghEntryRaw = ghOutputId !== null ? getBashOutput(ghOutputId) : null;
9087
- const ghEntry = ghEntryRaw !== null && !isBashEntryStale(ghEntryRaw, cmd, preHookCwd) ? ghEntryRaw : null;
9377
+ const ghEntry = ghEntryRaw !== null && !isBashEntryStale(ghEntryRaw, cmd, runDir) ? ghEntryRaw : null;
9088
9378
  if (ghOutputId !== null && ghEntry !== null && recallWorthShowing(ghOutputId, ghEntry)) {
9089
9379
  const ghBytes = ghEntry.sizeBytes;
9090
9380
  recordStat("bash_compress:recall", ghBytes, savedTokensFromBytes(ghBytes));
@@ -9095,10 +9385,10 @@ function preBashHandlerInner(event) {
9095
9385
  }
9096
9386
  }
9097
9387
  if (isScopedGitStatusOrDiffStatCommand(cmd)) {
9098
- const gitScopedHash = shortFingerprint(stripOutputPipeline(cmd));
9388
+ const gitScopedHash = bashRecallKey(cmd, runDir);
9099
9389
  const gitScopedOutputId = getBashOutputId(gitScopedHash);
9100
9390
  const gitScopedEntryRaw = gitScopedOutputId !== null ? getBashOutput(gitScopedOutputId) : null;
9101
- const gitScopedEntry = gitScopedEntryRaw !== null && !isBashEntryStale(gitScopedEntryRaw, cmd, preHookCwd) ? gitScopedEntryRaw : null;
9391
+ const gitScopedEntry = gitScopedEntryRaw !== null && !isBashEntryStale(gitScopedEntryRaw, cmd, runDir) ? gitScopedEntryRaw : null;
9102
9392
  if (gitScopedOutputId !== null && gitScopedEntry !== null && recallWorthShowing(gitScopedOutputId, gitScopedEntry)) {
9103
9393
  const gitScopedBytes = gitScopedEntry.sizeBytes;
9104
9394
  recordStat("bash_compress:recall", gitScopedBytes, savedTokensFromBytes(gitScopedBytes));
@@ -9108,61 +9398,326 @@ function preBashHandlerInner(event) {
9108
9398
  );
9109
9399
  }
9110
9400
  }
9111
- const tgRead = extractTgSurgicalRead(cmd, preHookCwd);
9112
- if (tgRead !== null) {
9113
- const cliKey = tgRead.sub + "::" + tgRead.spec;
9114
- const notes = [];
9115
- if (wasCliReadThisSession(cliKey)) {
9116
- notes.push("You already ran this exact `token-goat " + tgRead.sub + "` query earlier this session \u2014 check your context above before re-running it.");
9117
- }
9118
- if (tgRead.filePath !== null && wasFileReadThisSession(tgRead.filePath)) {
9119
- notes.push("`" + tgRead.filePath + "` was already fully read via the Read tool this session \u2014 that content may already cover this.");
9120
- }
9121
- if (notes.length > 0) {
9122
- recordStat("session_hint", 0, 0);
9123
- return pathHint(tgRead.filePath !== null ? [tgRead.filePath] : [], notes.join(" "));
9401
+ const tgRead = extractTgSurgicalRead(cmd, runDir);
9402
+ if (tgRead !== null) {
9403
+ const cliKey = tgRead.sub + "::" + tgRead.spec;
9404
+ const notes = [];
9405
+ if (wasCliReadThisSession(cliKey)) {
9406
+ notes.push("You already ran this exact `token-goat " + tgRead.sub + "` query earlier this session \u2014 check your context above before re-running it.");
9407
+ }
9408
+ if (tgRead.filePath !== null && wasFileReadThisSession(tgRead.filePath)) {
9409
+ notes.push("`" + tgRead.filePath + "` was already fully read via the Read tool this session \u2014 that content may already cover this.");
9410
+ }
9411
+ if (notes.length > 0) {
9412
+ recordStat("session_hint", 0, 0);
9413
+ return pathHint(tgRead.filePath !== null ? [tgRead.filePath] : [], notes.join(" "));
9414
+ }
9415
+ return passOutput();
9416
+ }
9417
+ if (isTestRunnerCommand(cmd) && isDirectTestRunnerCommand(cmd) && !hasTestRunScopeOrBudget(cmd)) {
9418
+ const compression = maybeCompressRewrite(event, rawCmd, cmd);
9419
+ if (compression !== null) return compression;
9420
+ const key = `test-run-budget:${event.sessionId}:${shortFingerprint(cmd)}`;
9421
+ if (!wasHintShown(key)) {
9422
+ markHintShown(key);
9423
+ recordStat("session_hint", 0, 0);
9424
+ return contextOutput(
9425
+ "This test command has no targeted selector or explicit timeout. Prefer a focused test path/name or the runner\u2019s supported timeout before starting a potentially unbounded suite; run the full suite deliberately when it is needed."
9426
+ );
9427
+ }
9428
+ }
9429
+ if (!isBuildCommand(cmd) && detectFromCommand(stripTrailingStderrRedirect(cmd), runDir ?? void 0) === null) return passOutput();
9430
+ const cmdHash = bashRecallKey(cmd, runDir);
9431
+ const outputId = getBashOutputId(cmdHash);
9432
+ const entryRaw = outputId !== null ? getBashOutput(outputId) : null;
9433
+ const entry = entryRaw !== null && !isBashEntryStale(entryRaw, cmd, runDir) ? entryRaw : null;
9434
+ if (outputId !== null && entry !== null && recallWorthShowing(outputId, entry)) {
9435
+ const bytes = entry.sizeBytes;
9436
+ recordStat("bash_compress:recall", bytes, savedTokensFromBytes(bytes));
9437
+ return contextOutput(buildRecallHint(cmd, outputId));
9438
+ }
9439
+ return maybeCompressRewrite(event, rawCmd, cmd) ?? passOutput();
9440
+ }
9441
+ function preBashHandler(event) {
9442
+ return applyHintTracking(event, preBashHandlerInner(event), classifyBashHint);
9443
+ }
9444
+ registerHook("pre_tool_use", preBashHandler, { toolName: "Bash" });
9445
+
9446
+ // src/hooks_bash_post.ts
9447
+ init_define_import_meta_env();
9448
+ import { statSync as statSync8, existsSync as existsSync2, readFileSync as readFileSync9 } from "node:fs";
9449
+ import * as path6 from "node:path";
9450
+ function elideServedShellLines(cmd, output, priorIds, unknownLineNumbers = false) {
9451
+ if (priorIds.length === 0) return null;
9452
+ const lines = output.split("\n");
9453
+ const numbers = unknownLineNumbers ? Array.from({ length: lines.length }, () => null) : deliveredLineNumbers(cmd, lines.length);
9454
+ if (numbers === null) return null;
9455
+ const rows = lines.map((text, i) => ({ no: numbers[i] ?? null, text, raw: text }));
9456
+ const bodies = [];
9457
+ for (let i = priorIds.length - 1; i >= 0; i--) {
9458
+ const id = priorIds[i];
9459
+ if (id === void 0) continue;
9460
+ const prior = getBashOutput(id);
9461
+ if (prior !== null) bodies.push(indexServedBody(id, prior.output));
9462
+ }
9463
+ const cuts = planServedElisions(rows, bodies);
9464
+ if (cuts.length === 0) return null;
9465
+ const spans = [];
9466
+ const push = (text, own) => {
9467
+ const sep = spans.length === 0 ? "" : "\n";
9468
+ const prev = spans[spans.length - 1];
9469
+ if (prev !== void 0 && prev.own === true === own) spans[spans.length - 1] = { text: prev.text + sep + text, own };
9470
+ else spans.push({ text: sep + text, own });
9471
+ };
9472
+ let at = 0;
9473
+ for (const cut of cuts) {
9474
+ for (let i = at; i < cut.start; i++) push(rows[i]?.raw ?? "", false);
9475
+ const first2 = rows[cut.start];
9476
+ const last = rows[cut.start + cut.len - 1];
9477
+ if (first2 === void 0 || last === void 0) return null;
9478
+ push(servedRunNotice(first2.no, last.no, cut.id, cut.len), true);
9479
+ at = cut.start + cut.len;
9480
+ }
9481
+ for (let i = at; i < rows.length; i++) push(rows[i]?.raw ?? "", false);
9482
+ return spans;
9483
+ }
9484
+ function foldShellReadBodies(cmd, output, fileKey, cwd) {
9485
+ if (!foldingEnabled()) return null;
9486
+ if (redactSecrets(output).count > 0) return null;
9487
+ const lines = output.split("\n");
9488
+ const numbers = deliveredLineNumbers(cmd, lines.length);
9489
+ if (numbers === null) return null;
9490
+ const rows = [];
9491
+ for (let i = 0; i < lines.length; i++) {
9492
+ const no = numbers[i];
9493
+ if (no === void 0 || no === null) return null;
9494
+ rows.push({ no, text: lines[i] ?? "", raw: lines[i] ?? "" });
9495
+ }
9496
+ const headRead = extractHeadFile(cmd) ?? extractGetContentHead(cmd);
9497
+ const windowed = extractLineRangeRead(cmd) !== null || headRead !== null && lines.length >= headRead.n;
9498
+ const folded = foldDelivery(rows, fileKey, displaySafePath(toDisplayPath(findProject(cwd ?? process.cwd())?.root, fileKey)), windowed);
9499
+ if (folded === null) return null;
9500
+ return { text: folded.numbered.join("\n"), folds: folded.folds };
9501
+ }
9502
+ function foldShellReadStructure(cmd, filePath, output, fileKey, cwd) {
9503
+ if (!isWholeFileDump(cmd, filePath)) return null;
9504
+ const originalBytes = Buffer.byteLength(output, "utf-8");
9505
+ let onDisk;
9506
+ try {
9507
+ onDisk = statSync8(hostPathOfIndexKey(fileKey)).size;
9508
+ } catch {
9509
+ return null;
9510
+ }
9511
+ if (onDisk !== originalBytes) return null;
9512
+ if (redactSecrets(output).count > 0) return null;
9513
+ const lines = output.split("\n");
9514
+ if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
9515
+ const rows = lines.map((text2, i) => ({ no: i + 1, text: text2, raw: text2 }));
9516
+ const shown = displaySafePath(toDisplayPath(findProject(cwd ?? process.cwd())?.root, fileKey));
9517
+ const fold = planMarkdownOutline(rows, fileKey, shown, originalBytes) ?? planSourceSkeleton(rows, fileKey, shown, originalBytes);
9518
+ if (fold === null) return null;
9519
+ const text = fold.numbered.join("\n");
9520
+ if (!isStructuralRewriteAccepted(originalBytes, Buffer.byteLength(text, "utf-8"), fold.ratioCap)) return null;
9521
+ return { text, kind: fold.kind === "read:markdown_outline" ? "bash_compress:markdown-outline" : "bash_compress:source-skeleton", detail: fold.detail };
9522
+ }
9523
+ var REWRITE_CLIP_NOTE = "\n[token-goat: rewrite clipped to the harness delivery cap; the pointer above recalls the untouched output]";
9524
+ function fenceRewriteWithinCap(spans) {
9525
+ const joined = spans.map((s) => s.text).join("");
9526
+ const fenced = fenceUntrustedSpans(spans, UNTRUSTED_TOOL_TAG);
9527
+ if (fenced === joined) return joined;
9528
+ const overhead = Buffer.byteLength(fenced, "utf-8") - Buffer.byteLength(joined, "utf-8") + Buffer.byteLength(REWRITE_CLIP_NOTE, "utf-8");
9529
+ const cut = clipToDeliveryCap(joined, overhead);
9530
+ if (!cut.clipped) return fenced;
9531
+ const kept = [];
9532
+ let left2 = cut.text.length;
9533
+ for (const s of spans) {
9534
+ if (left2 <= 0) break;
9535
+ kept.push(s.text.length <= left2 ? s : { ...s, text: s.text.slice(0, left2) });
9536
+ left2 -= s.text.length;
9537
+ }
9538
+ return fenceUntrustedSpans(kept, UNTRUSTED_TOOL_TAG) + REWRITE_CLIP_NOTE;
9539
+ }
9540
+ async function maybeCollapseIdenticalRead(cmd, runDir, optedOut, output, exitCode, cwd, cacheMinBytes, persisted = false) {
9541
+ if (optedOut) return null;
9542
+ if (exitCode !== null && exitCode !== 0) return null;
9543
+ const filePath = pureFileReadPath(cmd);
9544
+ if (filePath === null) return null;
9545
+ const originalBytes = Buffer.byteLength(output, "utf-8");
9546
+ if (originalBytes < Math.max(cacheMinBytes, IDENTICAL_READ_MIN_BODY_BYTES)) return null;
9547
+ const fileKey = resolveIndexPath(filePath, runDir ?? process.cwd());
9548
+ const priorIds = getFileServedOutputs(fileKey);
9549
+ let containerId = null;
9550
+ let identical = false;
9551
+ for (let i = priorIds.length - 1; i >= 0; i--) {
9552
+ const id = priorIds[i];
9553
+ if (id === void 0) continue;
9554
+ const prior = getBashOutput(id);
9555
+ if (prior === null || !containsLineRun(prior.output, output)) continue;
9556
+ containerId = id;
9557
+ identical = prior.output === output;
9558
+ break;
9559
+ }
9560
+ const sessionKey = bashRecallKey(cmd, runDir);
9561
+ if (containerId === null) {
9562
+ const elided = elideServedShellLines(cmd, output, priorIds);
9563
+ const priced = (text) => isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(text, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() });
9564
+ let rewrite = null;
9565
+ if (elided !== null) {
9566
+ const fenced = fenceRewriteWithinCap(elided);
9567
+ if (priced(fenced)) rewrite = { text: fenced, reason: "already-served file lines withheld", kind: "bash_compress:served-elide" };
9568
+ } else {
9569
+ const structural = foldShellReadStructure(cmd, filePath, output, fileKey, cwd);
9570
+ if (structural !== null && priced(structural.text)) {
9571
+ rewrite = { text: structural.text, reason: structural.kind === "bash_compress:markdown-outline" ? "document replaced with its heading tree" : "source replaced with its structural skeleton", kind: structural.kind, detail: structural.detail };
9572
+ }
9573
+ if (rewrite === null) {
9574
+ const folded = foldShellReadBodies(cmd, output, fileKey, cwd);
9575
+ if (folded !== null) {
9576
+ const fenced = fenceRewriteWithinCap([{ text: folded.text }]);
9577
+ if (priced(fenced)) rewrite = { text: fenced, reason: "code bodies folded", kind: "bash_compress:body-fold", detail: foldDetail(fileKey, folded.folds) };
9578
+ }
9579
+ }
9580
+ if (rewrite !== null && (extractLineRangeRead(cmd) !== null || extractHeadFile(cmd) !== null)) resetFileLineRanges(fileKey);
9581
+ }
9582
+ const storedId = await storeBashOutput(cmd, rewrite?.text ?? output, exitCode ?? 0, runDir);
9583
+ recordBashOutput(sessionKey, storedId, originalBytes);
9584
+ if (!persisted) recordFileServedOutput(fileKey, storedId);
9585
+ if (rewrite === null) return null;
9586
+ return emitRewrite(rewrite.text, rewrite.reason, { kind: rewrite.kind, originalBytes: deliveredOutputBytes(originalBytes), ...rewrite.detail === void 0 ? {} : { detail: rewrite.detail } });
9587
+ }
9588
+ const pointer = identical ? "[token-goat] Identical to an earlier run of this command in this session; the file has not changed since. " + originalBytes + " bytes withheld -- recall them with `token-goat bash-output " + containerId + " --full`." : "[token-goat] These " + originalBytes + " bytes already appear verbatim inside a wider read of " + filePath + " served earlier in this session. Withheld -- recall the full earlier output with `token-goat bash-output " + containerId + " --full`.";
9589
+ if (!isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(pointer, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) return null;
9590
+ return emitRewrite(pointer, identical ? "identical file re-read collapsed" : "already-served file lines collapsed", { kind: identical ? "bash_compress:identical-reread" : "bash_compress:contained-reread", originalBytes: deliveredOutputBytes(originalBytes) });
9591
+ }
9592
+ async function maybeElideServedGenericOutput(cmd, optedOut, output, exitCode, runDir, cacheMinBytes, persisted = false) {
9593
+ if (optedOut) return null;
9594
+ if (isRecallCommand(cmd)) return null;
9595
+ if (!loadConfig().bash_compress.elide_served_shell_output) return null;
9596
+ if (exitCode !== null && exitCode !== 0) return null;
9597
+ if (redactSecrets(output).count > 0) return null;
9598
+ const originalBytes = Buffer.byteLength(output, "utf-8");
9599
+ if (originalBytes < cacheMinBytes) return null;
9600
+ const priorIds = getFileServedOutputs(GENERIC_SERVED_OUTPUT_KEY).filter((id) => getBashOutput(id)?.command !== cmd);
9601
+ let rewrittenText = null;
9602
+ if (priorIds.length > 0) {
9603
+ const elided = elideServedShellLines(cmd, output, priorIds, true);
9604
+ const fenced = elided === null ? null : fenceRewriteWithinCap(elided);
9605
+ if (fenced !== null && isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(fenced, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) {
9606
+ rewrittenText = fenced;
9124
9607
  }
9125
- return passOutput();
9126
9608
  }
9127
- if (isTestRunnerCommand(cmd) && isDirectTestRunnerCommand(cmd) && !hasTestRunScopeOrBudget(cmd)) {
9128
- const compression = maybeCompressRewrite(event, rawCmd, cmd);
9129
- if (compression !== null) return compression;
9130
- const key = `test-run-budget:${event.sessionId}:${shortFingerprint(cmd)}`;
9131
- if (!wasHintShown(key)) {
9132
- markHintShown(key);
9133
- recordStat("session_hint", 0, 0);
9134
- return contextOutput(
9135
- "This test command has no targeted selector or explicit timeout. Prefer a focused test path/name or the runner\u2019s supported timeout before starting a potentially unbounded suite; run the full suite deliberately when it is needed."
9136
- );
9137
- }
9609
+ if (!persisted) recordFileServedOutput(GENERIC_SERVED_OUTPUT_KEY, await storeBashOutput(cmd, rewrittenText ?? output, exitCode ?? 0, runDir));
9610
+ if (rewrittenText === null) return null;
9611
+ return emitRewrite(rewrittenText, "already-served shell output collapsed", { kind: "bash_compress:generic-served-elision", originalBytes: deliveredOutputBytes(originalBytes) });
9612
+ }
9613
+ async function maybeCompressCompoundOutput(cmd, optedOut, output, exitCode, runDir, cacheMinBytes, isUnwrapped = false) {
9614
+ if (optedOut) return null;
9615
+ if (!isUnwrapped) return null;
9616
+ if (isFullRecallCommand(cmd)) return null;
9617
+ if (isTokenGoatOwnCommand(cmd)) return null;
9618
+ if (exitCode !== null && exitCode !== 0) return null;
9619
+ let cfg;
9620
+ try {
9621
+ cfg = loadConfig().bash_compress;
9622
+ } catch {
9623
+ return null;
9138
9624
  }
9139
- if (!isBuildCommand(cmd) && detectFromCommand(stripTrailingStderrRedirect(cmd), preHookCwd ?? void 0) === null) return passOutput();
9140
- const cmdHash = shortFingerprint(stripOutputPipeline(cmd));
9141
- const outputId = getBashOutputId(cmdHash);
9142
- const entryRaw = outputId !== null ? getBashOutput(outputId) : null;
9143
- const entry = entryRaw !== null && !isBashEntryStale(entryRaw, cmd, preHookCwd) ? entryRaw : null;
9144
- if (outputId !== null && entry !== null && recallWorthShowing(outputId, entry)) {
9145
- const bytes = entry.sizeBytes;
9146
- recordStat("bash_compress:recall", bytes, savedTokensFromBytes(bytes));
9147
- return contextOutput(buildRecallHint(cmd, outputId));
9625
+ if (!cfg.enabled || cfg.disabled_filters.includes("generic")) return null;
9626
+ if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return null;
9627
+ const shaped = pipelineShapeFilter(cmd, runDir) ?? (isUnwrapped ? detectFromCommand(cmd, runDir ?? void 0) ?? null : null);
9628
+ const useShaped = shaped !== null && !cfg.disabled_filters.includes(shaped.filter.name);
9629
+ const filter3 = useShaped ? shaped.filter : filterByName("generic");
9630
+ if (filter3 === null) return null;
9631
+ const filterArgv = useShaped ? shaped.argv : [];
9632
+ const compressed = compressOutput(filter3, output, "", exitCode ?? 0, filterArgv, {
9633
+ maxLines: cfg.max_lines,
9634
+ maxBytes: cfg.max_bytes
9635
+ });
9636
+ const minNet = resolveMinNetSavingsBytes();
9637
+ if (!compressed.worthApplying(minNet)) return null;
9638
+ const id = bashOutputIdSync(cmd, output, runDir);
9639
+ const { body: untrusted, notices } = splitOwnTrailingNotices(compressed.text);
9640
+ const marker = compressed.withMarker(minNet).slice(compressed.text.length);
9641
+ const body = fenceUntrusted(untrusted, UNTRUSTED_TOOL_TAG) + notices + marker + "\n[token-goat] full output: bash-output " + id + " --full";
9642
+ const emittedBytes = Buffer.byteLength(body, "utf-8");
9643
+ if (!isRewriteWorthwhile({
9644
+ originalBytes: compressed.originalBytes,
9645
+ rewrittenBytes: emittedBytes,
9646
+ noticeBytes: 0,
9647
+ minNetSavingsBytes: minNet
9648
+ })) {
9649
+ return null;
9148
9650
  }
9149
- return maybeCompressRewrite(event, rawCmd, cmd) ?? passOutput();
9651
+ await storeBashOutput(cmd, output, exitCode ?? 0, runDir);
9652
+ return emitRewrite(body, "bash", { kind: `bash_compress:${filter3.name}`, originalBytes: deliveredOutputBytes(compressed.originalBytes) });
9150
9653
  }
9151
- function preBashHandler(event) {
9152
- return applyHintTracking(event, preBashHandlerInner(event), classifyBashHint);
9654
+ function maybeStripAnsiOnly(output) {
9655
+ if (!output.includes("\x1B")) return null;
9656
+ let cfg;
9657
+ try {
9658
+ cfg = loadConfig().bash_compress;
9659
+ } catch {
9660
+ return null;
9661
+ }
9662
+ if (!cfg.enabled || cfg.disabled_filters.includes("ansi")) return null;
9663
+ const stripped = stripAnsiEscapes(output);
9664
+ const originalBytes = Buffer.byteLength(output, "utf-8");
9665
+ if (!isRewriteWorthwhile({
9666
+ originalBytes,
9667
+ rewrittenBytes: Buffer.byteLength(stripped, "utf-8"),
9668
+ noticeBytes: 0,
9669
+ minNetSavingsBytes: resolveMinNetSavingsBytes()
9670
+ })) {
9671
+ return null;
9672
+ }
9673
+ return emitRewrite(stripped, "ansi escapes stripped", { kind: "bash_compress:ansi", originalBytes: deliveredOutputBytes(originalBytes) }, "counted-elsewhere");
9674
+ }
9675
+ function maybeFoldCurlHtml(cmd, output, id) {
9676
+ if (!isCurlGetCommand(cmd) || !looksLikeHtml(output)) return null;
9677
+ let cfg;
9678
+ try {
9679
+ cfg = loadConfig().bash_compress;
9680
+ } catch {
9681
+ return null;
9682
+ }
9683
+ if (!cfg.enabled || cfg.disabled_filters.includes("curl-html")) return null;
9684
+ let cleaned;
9685
+ try {
9686
+ cleaned = extractCleanText(output);
9687
+ } catch {
9688
+ return null;
9689
+ }
9690
+ const originalBytes = Buffer.byteLength(output, "utf-8");
9691
+ const notice = `[token-goat: curl HTML body cleaned via extractCleanText; use \`token-goat bash-output ${id} --full\` to recall the raw markup]
9692
+ `;
9693
+ const noticeBytes = Buffer.byteLength(notice, "utf-8");
9694
+ const clipNote = "\n[token-goat: cleaned text clipped to the harness delivery cap; the notice at the top of this output recalls the full raw markup]";
9695
+ const fenceOverhead = Buffer.byteLength(fenceUntrusted(cleaned, UNTRUSTED_TOOL_TAG), "utf-8") - Buffer.byteLength(cleaned, "utf-8");
9696
+ const cut = clipToDeliveryCap(cleaned, noticeBytes + fenceOverhead + Buffer.byteLength(clipNote, "utf-8"));
9697
+ const body = cut.text;
9698
+ const clipped = cut.clipped ? clipNote : "";
9699
+ const fenced = fenceUntrusted(body, UNTRUSTED_TOOL_TAG) + clipped;
9700
+ if (!isRewriteWorthwhile({
9701
+ originalBytes,
9702
+ rewrittenBytes: Buffer.byteLength(fenced, "utf-8"),
9703
+ noticeBytes,
9704
+ minNetSavingsBytes: resolveMinNetSavingsBytes()
9705
+ })) {
9706
+ return null;
9707
+ }
9708
+ return emitRewrite(notice + fenced, "curl HTML body cleaned", { kind: "bash_compress:curl-html", originalBytes: deliveredOutputBytes(originalBytes) });
9153
9709
  }
9154
- registerHook("pre_tool_use", preBashHandler, { toolName: "Bash" });
9155
9710
  function readPersistedBashOutput(resp, cwd, sessionId) {
9156
9711
  const persistedPath = resp["persistedOutputPath"];
9157
9712
  if (typeof persistedPath !== "string" || persistedPath === "" || sessionId === "") return null;
9158
- const root = path4.join(projectTranscriptsDir(cwd ?? process.cwd()), sessionId, "tool-results");
9713
+ const root = path6.join(projectTranscriptsDir(cwd ?? process.cwd()), sessionId, "tool-results");
9159
9714
  if (!isInsideRoot(persistedPath, root)) return null;
9160
9715
  try {
9161
- const stat = statSync7(persistedPath);
9716
+ const stat = statSync8(persistedPath);
9162
9717
  if (!stat.isFile() || stat.size > MAX_CAPTURE_BYTES) return null;
9163
9718
  const persistedSize = resp["persistedOutputSize"];
9164
9719
  if (typeof persistedSize === "number" && Math.abs(stat.size - persistedSize) > 8) return null;
9165
- return readFileSync5(persistedPath, "utf-8");
9720
+ return readFileSync9(persistedPath, "utf-8");
9166
9721
  } catch {
9167
9722
  return null;
9168
9723
  }
@@ -9230,63 +9785,71 @@ function forgetPersistedLineRangeReads(rawCmd, cmd, cwd) {
9230
9785
  resetFileLineRanges(resolveIndexPath(hintPath, hintCwd));
9231
9786
  }
9232
9787
  }
9233
- function recordBashFileReadsForSessionCache(cmd, cwd) {
9234
- const resolve2 = (p) => resolveIndexPath(p, cwd ?? process.cwd());
9788
+ function recordBashFileReadsForSessionCache(cmd, runDir) {
9789
+ const base = runDir ?? process.cwd();
9790
+ const record = (p, put) => {
9791
+ const file = resolveIndexPath(p, base);
9792
+ if (isFileAtIndexKey(file)) put(file);
9793
+ };
9235
9794
  const gcTail = extractGetContentTail(cmd);
9236
9795
  if (gcTail !== null) {
9237
- markFileTruncated(resolve2(gcTail.filePath));
9796
+ record(gcTail.filePath, markFileTruncated);
9238
9797
  return;
9239
9798
  }
9240
9799
  const tail = extractTailFile(cmd);
9241
9800
  if (tail !== null) {
9242
- markFileTruncated(resolve2(tail.filePath));
9801
+ record(tail.filePath, markFileTruncated);
9243
9802
  return;
9244
9803
  }
9245
9804
  const gcSelect = extractGetContentSelectFirst(cmd);
9246
9805
  if (gcSelect !== null) {
9247
- recordFileLineRange(resolve2(gcSelect.filePath), 1, gcSelect.n);
9806
+ record(gcSelect.filePath, (file) => recordFileLineRange(file, 1, gcSelect.n));
9248
9807
  return;
9249
9808
  }
9250
9809
  const gcHead = extractGetContentHead(cmd);
9251
9810
  if (gcHead !== null) {
9252
- recordFileLineRange(resolve2(gcHead.filePath), 1, gcHead.n);
9811
+ record(gcHead.filePath, (file) => recordFileLineRange(file, 1, gcHead.n));
9253
9812
  return;
9254
9813
  }
9255
9814
  const head = extractHeadFile(cmd);
9256
9815
  if (head !== null) {
9257
- recordFileLineRange(resolve2(head.filePath), 1, head.n);
9816
+ record(head.filePath, (file) => recordFileLineRange(file, 1, head.n));
9258
9817
  return;
9259
9818
  }
9260
9819
  const cat = extractCatFile(cmd);
9261
9820
  if (cat !== null) {
9262
- recordFileRead(resolve2(cat.filePath));
9821
+ record(cat.filePath, recordFileRead);
9263
9822
  return;
9264
9823
  }
9265
9824
  const catMulti = extractCatFilesMulti(cmd);
9266
9825
  if (catMulti !== null) {
9267
- for (const r of catMulti) recordFileRead(resolve2(r.filePath));
9826
+ for (const r of catMulti) record(r.filePath, recordFileRead);
9268
9827
  return;
9269
9828
  }
9270
9829
  const wslCat = extractWslCatFile(cmd);
9271
9830
  if (wslCat !== null) {
9272
- recordFileRead(resolve2(wslCat.filePath));
9831
+ record(wslCat.filePath, recordFileRead);
9273
9832
  return;
9274
9833
  }
9275
9834
  }
9276
- async function maybeEmitLargeUncompressedHint(cmd, rawCmd, optedOut, output, exitCode, cwd, isUnwrapped, event, ansiResult) {
9835
+ async function maybeEmitLargeUncompressedHint(cmd, rawCmd, optedOut, output, exitCode, runDir, isUnwrapped, event, ansiResult) {
9277
9836
  const outputBytes = Buffer.byteLength(output, "utf-8");
9278
9837
  if (outputBytes < 4096 || optedOut || exitCode !== null && exitCode !== 0 || // token-goat's own output is already the narrow form; telling the model to compress it is noise.
9279
- /^\s*token-goat\s/.test(cmd) || event.raw["_tg_harness"] !== "vscode" && (isCompressibleSingleCommand(cmd) || !isUnwrapped)) {
9838
+ isTokenGoatOwnCommand(cmd) || // a line-range read, piped or repeated, is already the narrow slice; compressing it saves nothing.
9839
+ extractLineRangeReadsCompound(cmd) !== null || event.raw["_tg_harness"] !== "vscode" && (isCompressibleSingleCommand(cmd) || !isUnwrapped)) {
9280
9840
  return null;
9281
9841
  }
9282
9842
  const key = `bash-uncompressed-4k:${event.sessionId ?? ""}:${shortFingerprint(cmd)}`;
9283
9843
  if (wasHintShown(key)) return null;
9284
9844
  markHintShown(key);
9285
9845
  recordStat("session_hint", 0, 0);
9286
- const id = await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
9846
+ const id = await storeBashOutput(cmd, output, exitCode ?? 0, runDir);
9287
9847
  const kb = Math.round(outputBytes / 1024);
9288
- const runCmd = stripCdPrefix(rawCmd);
9289
- const compressable = !/["`$\r\n]/.test(runCmd);
9848
+ const bare = stripCdPrefix(rawCmd);
9849
+ const cdDir = bare === rawCmd ? null : runDir;
9850
+ const cdHost = cdDir === null ? null : dirAtIndexKey(cdDir);
9851
+ const runCmd = cdHost === null ? bare : `cd '${cdHost}' && ${bare}`;
9852
+ const compressable = !/["`$\r\n]/.test(runCmd) && (cdDir === null || cdHost !== null && !cdHost.includes("'"));
9290
9853
  const msg = `[tg] Output was ${kb}KB uncompressed; \`token-goat bash-output ${id}\` recalls it` + (compressable ? `, and \`token-goat compress -c "${displaySafeText(runCmd)}"\` compresses the next run.` : ".");
9291
9854
  if (ansiResult !== null && ansiResult.hookType === "rewriteOutput") {
9292
9855
  return emitRewrite(msg + "\n" + fenceUntrusted(ansiResult.updatedOutput, UNTRUSTED_TOOL_TAG), "large uncompressed output hint", void 0, "counted-elsewhere");
@@ -9298,17 +9861,19 @@ async function postBashHandler(event) {
9298
9861
  const rawCmdRaw = extractCommand(event);
9299
9862
  if (rawCmdRaw === void 0) return passOutput();
9300
9863
  const isUnwrapped = unwrapCompressCommand(rawCmdRaw) === null;
9301
- const rawCmd = unwrapCompressCommand(rawCmdRaw) ?? rawCmdRaw;
9302
- const cmd = stripCommandPrefix(rawCmd);
9303
9864
  const output = extractBashOutput(event);
9304
9865
  const exitCode = extractExitCode(event.raw);
9305
9866
  const cwd = getCwd(event) ?? null;
9867
+ const toolUseId = event.raw["tool_use_id"];
9868
+ const heldStartCwd = typeof toolUseId === "string" && toolUseId !== "" ? takeBashStartCwd(toolUseId) : null;
9869
+ const startCwd = heldStartCwd ?? cwd;
9870
+ const { rawCmd, cmd, runDir } = resolveBashCall(rawCmdRaw, startCwd);
9306
9871
  const cacheMinBytes = loadConfig().bash_compress.cache_min_bytes;
9307
9872
  const resp = event.raw["tool_response"];
9308
9873
  const persisted = resp !== null && typeof resp === "object" && typeof resp["persistedOutputPath"] === "string" && resp["persistedOutputPath"] !== "";
9309
9874
  const optedOut = compressionOptedOut(rawCmd);
9310
9875
  if (isHeadMovingGitCommand(cmd) && (exitCode === null || exitCode === 0)) {
9311
- const gitDir = cwd ?? process.cwd();
9876
+ const gitDir = runDir ?? process.cwd();
9312
9877
  let diffBase = "HEAD@{1}";
9313
9878
  if (ORIG_HEAD_ELIGIBLE_GIT_RE.test(cmd)) {
9314
9879
  const reflogTop = runGit(["reflog", "-1", "--format=%gs", "HEAD"], { cwd: gitDir, timeoutMs: 5e3 });
@@ -9328,18 +9893,19 @@ async function postBashHandler(event) {
9328
9893
  }
9329
9894
  }
9330
9895
  if (exitCode === null || exitCode === 0) {
9331
- enqueueNonHeadMovingRewrites(cmd, rawCmd, cwd ?? process.cwd());
9896
+ enqueueNonHeadMovingRewrites(cmd, runDir ?? process.cwd());
9332
9897
  }
9333
9898
  const curlDl = extractCurlDownload(cmd);
9334
9899
  if (curlDl !== null && (exitCode === null || exitCode === 0)) {
9335
- const resolvedOutputPath = resolveIndexPath(curlDl.outputPath, cwd ?? process.cwd());
9336
- if (existsSync(resolvedOutputPath)) {
9337
- recordCurlDownload(curlDl.url, resolvedOutputPath);
9900
+ const outputOnDisk = hostPathOfTypedPath(curlDl.outputPath, runDir ?? process.cwd());
9901
+ if (existsSync2(outputOnDisk)) {
9902
+ recordCurlDownload(curlDl.url, outputOnDisk);
9338
9903
  }
9339
9904
  }
9340
- if (persisted) forgetPersistedLineRangeReads(rawCmd, cmd, cwd);
9341
- else if (exitCode === null || exitCode === 0) recordBashFileReadsForSessionCache(cmd, cwd);
9342
- const tgRead = extractTgSurgicalRead(cmd, cwd);
9905
+ if (persisted) forgetPersistedLineRangeReads(rawCmd, cmd, startCwd);
9906
+ else if (exitCode === null || exitCode === 0) recordBashFileReadsForSessionCache(cmd, runDir);
9907
+ let tgRead = extractTgSurgicalRead(cmd, runDir);
9908
+ if (heldStartCwd === null && cwd !== null && tgRead?.filePath != null && !isFileAtIndexKey(tgRead.filePath)) tgRead = extractTgSurgicalRead(cmd, cwd);
9343
9909
  if (tgRead !== null && (exitCode === null || exitCode === 0)) {
9344
9910
  recordCliRead(tgRead.sub + "::" + tgRead.spec);
9345
9911
  if (tgRead.filePath !== null) {
@@ -9354,8 +9920,8 @@ async function postBashHandler(event) {
9354
9920
  }
9355
9921
  }
9356
9922
  if (isReadOnlyGhApi(cmd) && (exitCode === null || exitCode === 0) && Buffer.byteLength(output, "utf-8") >= cacheMinBytes) {
9357
- const ghCacheHash = shortFingerprint(stripOutputPipeline(cmd));
9358
- const ghCacheId = await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
9923
+ const ghCacheHash = bashRecallKey(cmd, runDir);
9924
+ const ghCacheId = await storeBashOutput(cmd, output, exitCode ?? 0, runDir);
9359
9925
  recordBashOutput(ghCacheHash, ghCacheId, Buffer.byteLength(output, "utf-8"));
9360
9926
  }
9361
9927
  const ghHint = buildGhApiHint(cmd, output, exitCode);
@@ -9367,52 +9933,54 @@ async function postBashHandler(event) {
9367
9933
  if (ghView !== null && (exitCode === null || exitCode === 0) && !wasHintShown(GH_VIEW_BATCH_HINT_KEY)) {
9368
9934
  markHintShown(GH_VIEW_BATCH_HINT_KEY);
9369
9935
  if (Buffer.byteLength(output, "utf-8") >= cacheMinBytes) {
9370
- const ghViewId = await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
9371
- recordBashOutput(shortFingerprint(stripOutputPipeline(cmd)), ghViewId, Buffer.byteLength(output, "utf-8"));
9936
+ const ghViewId = await storeBashOutput(cmd, output, exitCode ?? 0, runDir);
9937
+ recordBashOutput(bashRecallKey(cmd, runDir), ghViewId, Buffer.byteLength(output, "utf-8"));
9372
9938
  }
9373
9939
  recordStat("session_hint", 0, 0);
9374
9940
  return contextOutput(buildGhViewBatchAdvisory(ghView.sub, ghView.ref));
9375
9941
  }
9376
9942
  if (isTestRunnerCommand(cmd) && exitCode !== null && exitCode !== 0 && Buffer.byteLength(output, "utf-8") >= cacheMinBytes) {
9377
- const testFailHash = shortFingerprint(stripOutputPipeline(cmd));
9378
- const testFailId = await storeBashOutput(cmd, output, exitCode, cwd);
9943
+ const testFailHash = bashRecallKey(cmd, runDir);
9944
+ const testFailId = await storeBashOutput(cmd, output, exitCode, runDir);
9379
9945
  recordBashOutput(testFailHash, testFailId, Buffer.byteLength(output, "utf-8"));
9380
9946
  recordStat("session_hint", 0, 0);
9381
9947
  return contextOutput(`[token-goat] Tests failed. Run \`token-goat bash-output ${testFailId} | token-goat failures\` to see just the failing blocks instead of the full output.`);
9382
9948
  }
9383
9949
  if (isScopedGitStatusOrDiffStatCommand(cmd) && (exitCode === null || exitCode === 0) && Buffer.byteLength(output, "utf-8") >= cacheMinBytes) {
9384
- const gitScopedCacheHash = shortFingerprint(stripOutputPipeline(cmd));
9385
- const gitScopedCacheId = await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
9950
+ const gitScopedCacheHash = bashRecallKey(cmd, runDir);
9951
+ const gitScopedCacheId = await storeBashOutput(cmd, output, exitCode ?? 0, runDir);
9386
9952
  recordBashOutput(gitScopedCacheHash, gitScopedCacheId, Buffer.byteLength(output, "utf-8"));
9387
9953
  }
9388
- if (isUnwrapped && /^git(?:\s+-[^\s]+|\s+--[^\s]+)*\s+diff\b/i.test(cmd)) {
9389
- const unwrappedCompressed = await maybeCompressCompoundOutput(cmd, optedOut, output, exitCode, cwd, cacheMinBytes, isUnwrapped);
9390
- if (unwrappedCompressed !== null) return unwrappedCompressed;
9954
+ if (isUnwrapped && (isTestRunnerCommand(cmd) && detectFromCommand(cmd, runDir ?? void 0) !== null || /^git(?:\s+-[^\s]+|\s+--[^\s]+)*\s+diff\b/i.test(cmd))) {
9955
+ const unwrappedCompressed = await maybeCompressCompoundOutput(cmd, optedOut, output, exitCode, runDir, cacheMinBytes, isUnwrapped);
9956
+ if (unwrappedCompressed !== null) {
9957
+ recordBashOutput(bashRecallKey(cmd, runDir), bashOutputIdSync(cmd, output, runDir), Buffer.byteLength(output, "utf-8"));
9958
+ return unwrappedCompressed;
9959
+ }
9391
9960
  }
9392
9961
  const isMonitoring = getMonitoringRecallHint(cmd) !== null;
9393
9962
  const isFileRead = pureFileReadPath(cmd) !== null;
9394
9963
  if (isFileRead || !isMonitoring && !isBuildCommand(cmd) && !isCurlGetCommand(cmd)) {
9395
- const identical = await maybeCollapseIdenticalRead(cmd, rawCmd, optedOut, output, exitCode, cwd, cacheMinBytes, persisted);
9964
+ const identical = await maybeCollapseIdenticalRead(cmd, runDir, optedOut, output, exitCode, cwd, cacheMinBytes, persisted);
9396
9965
  if (identical !== null) return identical;
9397
9966
  if (!isFileRead && (!isUnwrapped || !isCompressibleSingleCommand(cmd))) {
9398
- const compound = await maybeCompressCompoundOutput(cmd, optedOut, output, exitCode, cwd, cacheMinBytes, isUnwrapped);
9967
+ const compound = await maybeCompressCompoundOutput(cmd, optedOut, output, exitCode, runDir, cacheMinBytes, isUnwrapped);
9399
9968
  if (compound !== null) return compound;
9400
9969
  }
9401
9970
  if (!isFileRead && extractLineRangeReadsCompound(cmd) === null) {
9402
- const genericElision = await maybeElideServedGenericOutput(cmd, optedOut, output, exitCode, cwd, cacheMinBytes, persisted);
9971
+ const genericElision = await maybeElideServedGenericOutput(cmd, optedOut, output, exitCode, runDir, cacheMinBytes, persisted);
9403
9972
  if (genericElision !== null) return genericElision;
9404
9973
  }
9405
9974
  const ansiStripped = optedOut ? null : maybeStripAnsiOnly(output);
9406
- const largeHint2 = await maybeEmitLargeUncompressedHint(cmd, rawCmd, optedOut, output, exitCode, cwd, isUnwrapped, event, ansiStripped);
9975
+ const largeHint2 = await maybeEmitLargeUncompressedHint(cmd, rawCmd, optedOut, output, exitCode, runDir, isUnwrapped, event, ansiStripped);
9407
9976
  if (largeHint2 !== null) return largeHint2;
9408
9977
  return ansiStripped ?? passOutput();
9409
9978
  }
9410
9979
  if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return passOutput();
9411
- const cacheKey = isCurlGetCommand(cmd) ? extractCurlUrl(cmd) ?? cmd : stripOutputPipeline(cmd);
9412
- const simpleHash = shortFingerprint(cacheKey);
9413
- const priorId = await commandHash(cmd, cwd);
9980
+ const simpleHash = isCurlGetCommand(cmd) ? shortFingerprint(extractCurlUrl(cmd) ?? cmd) : bashRecallKey(cmd, runDir);
9981
+ const priorId = await commandHash(cmd, runDir);
9414
9982
  const priorEntry = getBashOutput(priorId);
9415
- const id = await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
9983
+ const id = await storeBashOutput(cmd, output, exitCode ?? 0, runDir);
9416
9984
  recordBashOutput(simpleHash, id, Buffer.byteLength(output, "utf-8"));
9417
9985
  if (priorEntry !== null) {
9418
9986
  recordBashRerun(simpleHash);
@@ -9425,7 +9993,7 @@ async function postBashHandler(event) {
9425
9993
  const curlHtmlFold = optedOut ? null : maybeFoldCurlHtml(cmd, output, id);
9426
9994
  if (curlHtmlFold !== null) return curlHtmlFold;
9427
9995
  const ansiOnly = optedOut ? null : maybeStripAnsiOnly(output);
9428
- const largeHint = await maybeEmitLargeUncompressedHint(cmd, rawCmd, optedOut, output, exitCode, cwd, isUnwrapped, event, ansiOnly);
9996
+ const largeHint = await maybeEmitLargeUncompressedHint(cmd, rawCmd, optedOut, output, exitCode, runDir, isUnwrapped, event, ansiOnly);
9429
9997
  if (largeHint !== null) return largeHint;
9430
9998
  if (ansiOnly !== null) return ansiOnly;
9431
9999
  } catch {
@@ -9436,14 +10004,13 @@ registerHook("post_tool_use", postBashHandler, { toolName: "Bash" });
9436
10004
 
9437
10005
  // src/hooks_bashoutput.ts
9438
10006
  init_define_import_meta_env();
9439
- function pollCacheId(sessionId, bashId) {
9440
- return `bgpoll_${shortFingerprint(`${sessionId}\0bashoutput\0${bashId}`)}`;
10007
+ function pollCacheId(tool, sessionId, pollId) {
10008
+ return `${tool.blobPrefix}_${shortFingerprint(`${sessionId}\0${tool.channel}\0${pollId}`)}`;
9441
10009
  }
9442
- function storePollSnapshot(sessionId, bashId, output) {
9443
- const id = pollCacheId(sessionId, bashId);
10010
+ function storePollSnapshot(tool, id, pollId, output) {
9444
10011
  const entry = {
9445
10012
  id,
9446
- command: `bashoutput:${bashId}`,
10013
+ command: `${tool.channel}:${pollId}`,
9447
10014
  output,
9448
10015
  exitCode: 0,
9449
10016
  storedAt: Date.now(),
@@ -9451,61 +10018,55 @@ function storePollSnapshot(sessionId, bashId, output) {
9451
10018
  };
9452
10019
  storeBlob(BASH_OUTPUT_SUBDIR, id, entry);
9453
10020
  }
10021
+ function rewritePoll(tool, sessionId, pollId, rawOutput) {
10022
+ if (!rawOutput) return passOutput();
10023
+ const output = redactSecrets(rawOutput).text;
10024
+ if (!output) return passOutput();
10025
+ const originalBytes = Buffer.byteLength(rawOutput, "utf-8");
10026
+ const outputBytes = Buffer.byteLength(output, "utf-8");
10027
+ const id = pollCacheId(tool, sessionId, pollId);
10028
+ const prior = getBashOutput(id);
10029
+ const minBytes = loadConfig().bash_compress.cache_min_bytes;
10030
+ storePollSnapshot(tool, id, pollId, output);
10031
+ if (prior === null) {
10032
+ if (tool.channel === "bashoutput") return passOutput();
10033
+ const collapsed = tool.compactFirst(output);
10034
+ if (collapsed === output) return passOutput();
10035
+ const collapsedBytes = Buffer.byteLength(collapsed, "utf-8");
10036
+ if (outputBytes - collapsedBytes < minBytes) return passOutput();
10037
+ if (!isRewriteWorthwhile({ originalBytes: outputBytes, rewrittenBytes: collapsedBytes, noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) return passOutput();
10038
+ return emitRewrite(collapsed, "taskoutput", { kind: "taskoutput:collapse", originalBytes });
10039
+ }
10040
+ if (outputBytes < minBytes) return passOutput();
10041
+ if (output === prior.output) {
10042
+ const unchangedNotice = `[token-goat: ${tool.idArg} ${pollId} unchanged since last poll -- no new output]`;
10043
+ if (!isRewriteWorthwhile({ originalBytes: outputBytes, rewrittenBytes: 0, noticeBytes: Buffer.byteLength(unchangedNotice, "utf-8"), minNetSavingsBytes: resolveMinNetSavingsBytes() })) return passOutput();
10044
+ return tool.channel === "bashoutput" ? emitRewrite(unchangedNotice, "bashoutput", { kind: "bashoutput:unchanged", originalBytes }) : emitRewrite(unchangedNotice, "taskoutput", { kind: "taskoutput:unchanged", originalBytes });
10045
+ }
10046
+ if (!output.startsWith(prior.output)) return passOutput();
10047
+ const deltaNotice = `[token-goat: ${tool.idArg} ${pollId} delta since last poll]
10048
+ `;
10049
+ const fenced = fenceUntrusted(tool.compactDelta(output.slice(prior.output.length)), UNTRUSTED_TOOL_TAG);
10050
+ const body = `${deltaNotice}${fenced}`;
10051
+ if (!isRewriteWorthwhile({ originalBytes: outputBytes, rewrittenBytes: Buffer.byteLength(body, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) return passOutput();
10052
+ return tool.channel === "bashoutput" ? emitRewrite(body, "bashoutput", { kind: "bashoutput:delta", originalBytes }) : emitRewrite(body, "taskoutput", { kind: "taskoutput:delta", originalBytes });
10053
+ }
9454
10054
  function getBashId(toolInput) {
9455
10055
  const value = toolInput["bash_id"];
9456
10056
  return typeof value === "string" && value !== "" ? value : void 0;
9457
10057
  }
10058
+ function compactPytestDelta(delta) {
10059
+ if (!looksLikePytestOutput(delta)) return delta;
10060
+ const compressed = pytestFilter.apply(delta, "", 0, []);
10061
+ return compressed.worthApplying(resolveMinNetSavingsBytes()) ? compressed.withMarker(resolveMinNetSavingsBytes()) : delta;
10062
+ }
10063
+ var BASH_OUTPUT_POLL = { channel: "bashoutput", blobPrefix: "bgpoll", idArg: "bash_id", compactDelta: compactPytestDelta };
9458
10064
  function postBashOutputHandler(event) {
9459
10065
  try {
9460
10066
  if (getToolName(event) !== "BashOutput" || !event.sessionId) return passOutput();
9461
10067
  const bashId = getBashId(getToolInput(event));
9462
10068
  if (bashId === void 0) return passOutput();
9463
- const rawOutput = extractToolResultText(event.raw);
9464
- if (!rawOutput) return passOutput();
9465
- const output = redactSecrets(rawOutput).text;
9466
- if (!output) return passOutput();
9467
- const prior = getBashOutput(pollCacheId(event.sessionId, bashId));
9468
- const minBytes = loadConfig().bash_compress.cache_min_bytes;
9469
- if (prior === null) {
9470
- storePollSnapshot(event.sessionId, bashId, output);
9471
- return passOutput();
9472
- }
9473
- if (output === prior.output) {
9474
- storePollSnapshot(event.sessionId, bashId, output);
9475
- if (Buffer.byteLength(output, "utf-8") < minBytes) return passOutput();
9476
- const unchangedNotice = `[token-goat: bash_id ${bashId} unchanged since last poll -- no new output]`;
9477
- if (!isRewriteWorthwhile({
9478
- originalBytes: Buffer.byteLength(output, "utf-8"),
9479
- rewrittenBytes: 0,
9480
- noticeBytes: Buffer.byteLength(unchangedNotice, "utf-8"),
9481
- minNetSavingsBytes: resolveMinNetSavingsBytes()
9482
- })) {
9483
- return passOutput();
9484
- }
9485
- return emitRewrite(unchangedNotice, "bashoutput", { kind: "bashoutput:unchanged", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
9486
- }
9487
- if (!output.startsWith(prior.output)) {
9488
- storePollSnapshot(event.sessionId, bashId, output);
9489
- return passOutput();
9490
- }
9491
- const delta = output.slice(prior.output.length);
9492
- storePollSnapshot(event.sessionId, bashId, output);
9493
- if (Buffer.byteLength(delta, "utf-8") < minBytes) return passOutput();
9494
- const compressedDelta = looksLikePytestOutput(delta) ? pytestFilter.apply(delta, "", 0, []) : null;
9495
- const deltaBody = compressedDelta !== null && compressedDelta.worthApplying(resolveMinNetSavingsBytes()) ? compressedDelta.withMarker(resolveMinNetSavingsBytes()) : delta;
9496
- const deltaNotice = `[token-goat: bash_id ${bashId} delta since last poll]
9497
- `;
9498
- const fenced = fenceUntrusted(deltaBody, UNTRUSTED_TOOL_TAG);
9499
- const body = `${deltaNotice}${fenced}`;
9500
- if (!isRewriteWorthwhile({
9501
- originalBytes: Buffer.byteLength(output, "utf-8"),
9502
- rewrittenBytes: Buffer.byteLength(body, "utf-8"),
9503
- noticeBytes: 0,
9504
- minNetSavingsBytes: resolveMinNetSavingsBytes()
9505
- })) {
9506
- return passOutput();
9507
- }
9508
- return emitRewrite(body, "bashoutput", { kind: "bashoutput:delta", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
10069
+ return rewritePoll(BASH_OUTPUT_POLL, event.sessionId, bashId, extractToolResultText(event.raw));
9509
10070
  } catch {
9510
10071
  return passOutput();
9511
10072
  }
@@ -9514,21 +10075,6 @@ registerHook("post_tool_use", postBashOutputHandler, { toolName: "BashOutput" })
9514
10075
 
9515
10076
  // src/hooks_taskoutput.ts
9516
10077
  init_define_import_meta_env();
9517
- function pollCacheId2(sessionId, taskId) {
9518
- return `taskpoll_${shortFingerprint(`${sessionId}\0taskoutput\0${taskId}`)}`;
9519
- }
9520
- function storePollSnapshot2(sessionId, taskId, output) {
9521
- const id = pollCacheId2(sessionId, taskId);
9522
- const entry = {
9523
- id,
9524
- command: `taskoutput:${taskId}`,
9525
- output,
9526
- exitCode: 0,
9527
- storedAt: Date.now(),
9528
- sizeBytes: Buffer.byteLength(output, "utf-8")
9529
- };
9530
- storeBlob(BASH_OUTPUT_SUBDIR, id, entry);
9531
- }
9532
10078
  function collapseRepeatedLines(text) {
9533
10079
  return dedupeConsecutive(text.split("\n")).join("\n");
9534
10080
  }
@@ -9547,68 +10093,13 @@ function extractTaskOutputText(raw) {
9547
10093
  }
9548
10094
  return extractToolResultText(raw);
9549
10095
  }
10096
+ var TASK_OUTPUT_POLL = { channel: "taskoutput", blobPrefix: "taskpoll", idArg: "task_id", compactDelta: collapseRepeatedLines, compactFirst: collapseRepeatedLines };
9550
10097
  function postTaskOutputHandler(event) {
9551
10098
  try {
9552
10099
  if (getToolName(event) !== "TaskOutput" || !event.sessionId) return passOutput();
9553
10100
  const taskId = getTaskId(getToolInput(event));
9554
10101
  if (taskId === void 0) return passOutput();
9555
- const rawOutput = extractTaskOutputText(event.raw);
9556
- if (!rawOutput) return passOutput();
9557
- const output = redactSecrets(rawOutput).text;
9558
- if (!output) return passOutput();
9559
- const prior = getBashOutput(pollCacheId2(event.sessionId, taskId));
9560
- const minBytes = loadConfig().bash_compress.cache_min_bytes;
9561
- if (prior === null) {
9562
- storePollSnapshot2(event.sessionId, taskId, output);
9563
- const collapsed = collapseRepeatedLines(output);
9564
- if (collapsed === output) return passOutput();
9565
- const savings = Buffer.byteLength(output, "utf-8") - Buffer.byteLength(collapsed, "utf-8");
9566
- if (savings < minBytes) return passOutput();
9567
- if (!isRewriteWorthwhile({
9568
- originalBytes: Buffer.byteLength(output, "utf-8"),
9569
- rewrittenBytes: Buffer.byteLength(collapsed, "utf-8"),
9570
- noticeBytes: 0,
9571
- minNetSavingsBytes: resolveMinNetSavingsBytes()
9572
- })) {
9573
- return passOutput();
9574
- }
9575
- return emitRewrite(collapsed, "taskoutput", { kind: "taskoutput:collapse", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
9576
- }
9577
- if (output === prior.output) {
9578
- storePollSnapshot2(event.sessionId, taskId, output);
9579
- if (Buffer.byteLength(output, "utf-8") < minBytes) return passOutput();
9580
- const unchangedNotice = `[token-goat: task_id ${taskId} unchanged since last poll -- no new output]`;
9581
- if (!isRewriteWorthwhile({
9582
- originalBytes: Buffer.byteLength(output, "utf-8"),
9583
- rewrittenBytes: 0,
9584
- noticeBytes: Buffer.byteLength(unchangedNotice, "utf-8"),
9585
- minNetSavingsBytes: resolveMinNetSavingsBytes()
9586
- })) {
9587
- return passOutput();
9588
- }
9589
- return emitRewrite(unchangedNotice, "taskoutput", { kind: "taskoutput:unchanged", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
9590
- }
9591
- if (!output.startsWith(prior.output)) {
9592
- storePollSnapshot2(event.sessionId, taskId, output);
9593
- return passOutput();
9594
- }
9595
- const delta = output.slice(prior.output.length);
9596
- storePollSnapshot2(event.sessionId, taskId, output);
9597
- if (Buffer.byteLength(delta, "utf-8") < minBytes) return passOutput();
9598
- const collapsedDelta = collapseRepeatedLines(delta);
9599
- const deltaNotice = `[token-goat: task_id ${taskId} delta since last poll]
9600
- `;
9601
- const fenced = fenceUntrusted(collapsedDelta, UNTRUSTED_TOOL_TAG);
9602
- const body = `${deltaNotice}${fenced}`;
9603
- if (!isRewriteWorthwhile({
9604
- originalBytes: Buffer.byteLength(output, "utf-8"),
9605
- rewrittenBytes: Buffer.byteLength(body, "utf-8"),
9606
- noticeBytes: 0,
9607
- minNetSavingsBytes: resolveMinNetSavingsBytes()
9608
- })) {
9609
- return passOutput();
9610
- }
9611
- return emitRewrite(body, "taskoutput", { kind: "taskoutput:delta", originalBytes: Buffer.byteLength(rawOutput, "utf-8") });
10102
+ return rewritePoll(TASK_OUTPUT_POLL, event.sessionId, taskId, extractTaskOutputText(event.raw));
9612
10103
  } catch {
9613
10104
  return passOutput();
9614
10105
  }
@@ -9617,8 +10108,8 @@ registerHook("post_tool_use", postTaskOutputHandler, { toolName: "TaskOutput" })
9617
10108
 
9618
10109
  // src/hooks_tool_failure.ts
9619
10110
  init_define_import_meta_env();
9620
- import { existsSync as existsSync2, readFileSync as readFileSync6, statSync as statSync8, writeFileSync as writeFileSync3 } from "node:fs";
9621
- import { dirname as dirname3, relative as relative2 } from "node:path";
10111
+ import { existsSync as existsSync3, readFileSync as readFileSync10, statSync as statSync9, writeFileSync as writeFileSync5 } from "node:fs";
10112
+ import { dirname as dirname5, relative as relative2 } from "node:path";
9622
10113
  var FAILURE_SUFFIX = ".tool-failures";
9623
10114
  var MAX_TRACKED_FAILURES = 64;
9624
10115
  var SIGNATURE_ERROR_CHARS = 200;
@@ -9645,7 +10136,7 @@ function ledgerPath(sessionId) {
9645
10136
  }
9646
10137
  function readLedger(target) {
9647
10138
  try {
9648
- const parsed = JSON.parse(readFileSync6(target, "utf8"));
10139
+ const parsed = JSON.parse(readFileSync10(target, "utf8"));
9649
10140
  if (parsed !== null && typeof parsed === "object") {
9650
10141
  const seen = parsed.seen;
9651
10142
  const order = parsed.order;
@@ -9659,8 +10150,8 @@ function readLedger(target) {
9659
10150
  }
9660
10151
  function writeLedger(target, ledger) {
9661
10152
  try {
9662
- ensureDirSync(dirname3(target));
9663
- writeFileSync3(target, JSON.stringify(ledger), "utf8");
10153
+ ensureDirSync(dirname5(target));
10154
+ writeFileSync5(target, JSON.stringify(ledger), "utf8");
9664
10155
  } catch {
9665
10156
  }
9666
10157
  }
@@ -9703,16 +10194,16 @@ function diagnoseEditFailure(event, errorText) {
9703
10194
  if (!isMultiple && !isNotFound) {
9704
10195
  return null;
9705
10196
  }
9706
- const filePath = getFilePath(event) ?? (typeof event.toolInput["path"] === "string" ? event.toolInput["path"] : void 0);
10197
+ const filePath = getFilePath(event) ?? (typeof event.toolInput["path"] === "string" && event.toolInput["path"] !== "" ? resolveEventPath(event, event.toolInput["path"]) : void 0);
9707
10198
  if (!filePath) return null;
9708
10199
  const oldString = typeof event.toolInput["old_string"] === "string" ? event.toolInput["old_string"] : typeof event.toolInput["old_str"] === "string" ? event.toolInput["old_str"] : typeof event.toolInput["target"] === "string" ? event.toolInput["target"] : void 0;
9709
10200
  if (oldString === void 0 || oldString === "") return null;
9710
- const absPath = normalizePath(filePath);
9711
- if (!existsSync2(absPath)) return null;
10201
+ const absPath = hostPathOfIndexKey(normalizePath(filePath));
10202
+ if (!existsSync3(absPath)) return null;
9712
10203
  try {
9713
- const stat = statSync8(absPath);
10204
+ const stat = statSync9(absPath);
9714
10205
  if (stat.size > MAX_EDIT_DIAGNOSE_BYTES) return null;
9715
- const fileContent = readFileSync6(absPath, "utf8");
10206
+ const fileContent = readFileSync10(absPath, "utf8");
9716
10207
  const relDisplay = displaySafeText(relative2(process.cwd(), absPath).replace(/\\/g, "/") || absPath);
9717
10208
  if (isMultiple) {
9718
10209
  const matchLines = [];
@@ -9884,7 +10375,7 @@ function mcpOutputId(sessionId, hash) {
9884
10375
  }
9885
10376
  function mcpInputPreview(toolInput) {
9886
10377
  try {
9887
- return redactSecrets(JSON.stringify(toolInput)).text.slice(0, 120);
10378
+ return redactSerializedJson(JSON.stringify(toolInput)).text.slice(0, 120);
9888
10379
  } catch {
9889
10380
  return "";
9890
10381
  }
@@ -9903,12 +10394,7 @@ function storeMcpOutput(sessionId, toolName, toolInput, resultText) {
9903
10394
  output: redactedOutput,
9904
10395
  exitCode: 0,
9905
10396
  storedAt: Date.now(),
9906
- // Sized off the redacted output, not the raw pre-redaction resultText (rawSizeBytes is only
9907
- // the cache-eligibility gate above) -- mirrors bash_output_cache.ts's storeBashOutput, whose
9908
- // sizeBytes is likewise computed from redactedOutput. Sizing off the raw text here left
9909
- // mcp-audit's per-call token estimate and `mcp-history`'s byte column reporting a stale byte
9910
- // count for any entry a secret was actually stripped from, out of sync with the stored/served
9911
- // (redacted, shorter) output.
10397
+ // Sized off the redacted output, not the raw pre-redaction resultText (rawSizeBytes is only the cache-eligibility gate above) -- mirrors bash_output_cache.ts's storeBashOutput, whose sizeBytes is likewise computed from redactedOutput. Sizing off the raw text here left mcp-audit's per-call token estimate and `mcp-history`'s byte column reporting a stale byte count for any entry a secret was actually stripped from, out of sync with the stored/served (redacted, shorter) output.
9912
10398
  sizeBytes: Buffer.byteLength(redactedOutput, "utf-8")
9913
10399
  };
9914
10400
  if (!storeBlob(BASH_OUTPUT_SUBDIR, id, entry)) return null;
@@ -10267,9 +10753,58 @@ function compressMcpResultWithPacks(toolName, resultText) {
10267
10753
 
10268
10754
  // src/hooks_mcp.ts
10269
10755
  var MCP_OVERSIZED_THRESHOLD_BYTES = 25e3;
10756
+ var TOKEN_GOAT_MCP_TOOL_NAMES = /* @__PURE__ */ new Set([
10757
+ "read",
10758
+ "symbol",
10759
+ "section",
10760
+ "outline",
10761
+ "skeleton",
10762
+ "refs",
10763
+ "brief",
10764
+ "map",
10765
+ "changed",
10766
+ "grep",
10767
+ "imports",
10768
+ "exports",
10769
+ "semantic",
10770
+ "index_status",
10771
+ "handoff_create",
10772
+ "handoff_apply",
10773
+ "retrieve_text"
10774
+ ]);
10775
+ function isTokenGoatMcpTool(toolName) {
10776
+ const lower = toolName.toLowerCase();
10777
+ let rest = lower;
10778
+ if (rest.startsWith("mcp__")) {
10779
+ rest = rest.slice(5);
10780
+ } else if (rest.startsWith("mcp_")) {
10781
+ rest = rest.slice(4);
10782
+ } else {
10783
+ if (lower.startsWith("token_goat_") || lower.startsWith("token-goat-") || lower.startsWith("tg_")) {
10784
+ const tool2 = lower.replace(/^(token_goat_|token-goat-|tg_)/, "");
10785
+ return TOKEN_GOAT_MCP_TOOL_NAMES.has(tool2);
10786
+ }
10787
+ return false;
10788
+ }
10789
+ const sepIdx = rest.indexOf("__");
10790
+ let server = "";
10791
+ let tool = "";
10792
+ if (sepIdx !== -1) {
10793
+ server = rest.slice(0, sepIdx);
10794
+ tool = rest.slice(sepIdx + 2);
10795
+ } else {
10796
+ const singleIdx = rest.indexOf("_");
10797
+ if (singleIdx !== -1) {
10798
+ server = rest.slice(0, singleIdx);
10799
+ tool = rest.slice(singleIdx + 1);
10800
+ }
10801
+ }
10802
+ const isTgServer = server === "token_goat" || server === "token-goat" || server === "tg" || server === "token_goat_mcp" || server === "token-goat-mcp";
10803
+ return isTgServer && TOKEN_GOAT_MCP_TOOL_NAMES.has(tool);
10804
+ }
10270
10805
  function preMcpHandler(event) {
10271
10806
  const toolName = getToolName(event);
10272
- if (!toolName || !event.sessionId) return passOutput();
10807
+ if (!toolName || !event.sessionId || isTokenGoatMcpTool(toolName)) return passOutput();
10273
10808
  const toolInput = getToolInput(event);
10274
10809
  if (!isMcpReadOnly(toolName, toolInput)) return passOutput();
10275
10810
  const ttlMs = loadConfig().hints.mcp_dedup_ttl_secs * 1e3;
@@ -10283,7 +10818,7 @@ function preMcpHandler(event) {
10283
10818
  }
10284
10819
  function postMcpHandler(event) {
10285
10820
  const toolName = getToolName(event);
10286
- if (!toolName || !toolName.startsWith("mcp__")) return passOutput();
10821
+ if (!toolName || !toolName.startsWith("mcp__") || isTokenGoatMcpTool(toolName)) return passOutput();
10287
10822
  const toolInput = getToolInput(event);
10288
10823
  const resultText = extractToolResultText(event.raw);
10289
10824
  if (!resultText) return passOutput();
@@ -10568,16 +11103,17 @@ registerHook("post_tool_use", postBrowserImageHandler, { toolPattern: "^mcp__" }
10568
11103
  // src/hooks_agent_spawn.ts
10569
11104
  init_define_import_meta_env();
10570
11105
  import { createHash as createHash2 } from "node:crypto";
10571
- import * as fs2 from "node:fs";
10572
- import * as path5 from "node:path";
11106
+ import * as fs4 from "node:fs";
11107
+ import * as path7 from "node:path";
10573
11108
  var BRIEFING_TARGET_TOKENS = 550;
10574
- function buildSubagentBriefing() {
11109
+ function buildSubagentBriefing(projectRoot) {
10575
11110
  try {
10576
11111
  const head = [];
10577
11112
  head.push("");
10578
11113
  head.push("## Session briefing (for context)");
10579
11114
  try {
10580
- const map2 = buildProjectMap(process.cwd(), { compact: true });
11115
+ if (projectRoot === null) throw new Error("no project root");
11116
+ const map2 = buildProjectMap(projectRoot, { compact: true });
10581
11117
  const mapText = formatProjectMap(map2, map2.compact);
10582
11118
  head.push(mapText);
10583
11119
  } catch {
@@ -10641,6 +11177,32 @@ function findDuplicateOutstandingPrompt(prompt) {
10641
11177
  function truncateForWarning(text, max) {
10642
11178
  return text.length > max ? text.slice(0, max) + "..." : text;
10643
11179
  }
11180
+ function briefingRoot(event) {
11181
+ if (getHarnessName() === "vscode") return getCwd(event) ?? null;
11182
+ return process.cwd();
11183
+ }
11184
+ function subagentStartWired() {
11185
+ const harness = getHarnessName();
11186
+ const dirVar = harness === "copilot_cli" ? "TOKEN_GOAT_COPILOT_HOOKS_DIR" : harness === "vscode" ? "TOKEN_GOAT_VSCODE_HOOKS_DIR" : void 0;
11187
+ const dir = dirVar === void 0 ? void 0 : process.env[dirVar];
11188
+ if (dir === void 0 || dir === "") return false;
11189
+ try {
11190
+ const config = JSON.parse(fs4.readFileSync(path7.join(dir, "token-goat.json"), "utf8"));
11191
+ const entries = config.hooks?.[harness === "vscode" ? "SubagentStart" : "subagentStart"];
11192
+ return Array.isArray(entries) && entries.length > 0;
11193
+ } catch {
11194
+ return false;
11195
+ }
11196
+ }
11197
+ function subagentStartHandler(event) {
11198
+ try {
11199
+ const root = getCwd(event) ?? (getHarnessName() === "vscode" ? null : process.cwd());
11200
+ const briefing = buildSubagentBriefing(root).trim();
11201
+ return briefing ? contextOutput(briefing) : passOutput();
11202
+ } catch {
11203
+ return passOutput();
11204
+ }
11205
+ }
10644
11206
  function isAgentTool(toolName) {
10645
11207
  return toolName === "Agent" || toolName === "task" || toolName === "Task";
10646
11208
  }
@@ -10650,9 +11212,11 @@ function preAgentHandler(event) {
10650
11212
  const prompt = toolInput["prompt"];
10651
11213
  if (typeof prompt !== "string" || prompt.trim() === "") return passOutput();
10652
11214
  try {
11215
+ const scopedDeny = buildScopedSpawnDeny(toolInput);
11216
+ if (scopedDeny) return scopedDeny;
10653
11217
  const duplicateOf = findDuplicateOutstandingPrompt(prompt);
10654
11218
  recordOutstandingAgentSpawn(prompt);
10655
- const briefing = buildSubagentBriefing();
11219
+ const briefing = subagentStartWired() ? "" : buildSubagentBriefing(briefingRoot(event));
10656
11220
  const advisory = duplicateOf ? `
10657
11221
 
10658
11222
  &#91;token-goat] A similar subagent spawn already appears to be outstanding this session (prompt starts: "${neutralizeSpokenMarkers(truncateForWarning(duplicateOf, 80))}"). This is advisory only -- proceeding is fine if intentional.` : "";
@@ -10794,8 +11358,8 @@ function parseAgentDefinition(text, fallbackName) {
10794
11358
  return { name: name2, restricted: false };
10795
11359
  }
10796
11360
  function findRestrictedAgentNames(roots) {
10797
- const projectAgentsRoot = path5.join(process.cwd(), ".claude", "agents");
10798
- const scanRoots = roots ?? [path5.join(claudeConfigDir(), "agents"), projectAgentsRoot];
11361
+ const projectAgentsRoot = path7.join(process.cwd(), ".claude", "agents");
11362
+ const scanRoots = roots ?? [path7.join(claudeConfigDir(), "agents"), projectAgentsRoot];
10799
11363
  const names = /* @__PURE__ */ new Set();
10800
11364
  const visited = /* @__PURE__ */ new Set();
10801
11365
  let filesSeen = 0;
@@ -10803,7 +11367,7 @@ function findRestrictedAgentNames(roots) {
10803
11367
  if (depth > ROSTER_WALK_MAX_DEPTH || filesSeen >= ROSTER_WALK_MAX_FILES) return;
10804
11368
  let real;
10805
11369
  try {
10806
- real = fs2.realpathSync(dir);
11370
+ real = fs4.realpathSync(dir);
10807
11371
  } catch {
10808
11372
  return;
10809
11373
  }
@@ -10813,16 +11377,16 @@ function findRestrictedAgentNames(roots) {
10813
11377
  visited.add(key);
10814
11378
  let entries;
10815
11379
  try {
10816
- entries = fs2.readdirSync(dir, { withFileTypes: true });
11380
+ entries = fs4.readdirSync(dir, { withFileTypes: true });
10817
11381
  } catch {
10818
11382
  return;
10819
11383
  }
10820
11384
  for (const entry of entries) {
10821
11385
  if (filesSeen >= ROSTER_WALK_MAX_FILES) return;
10822
- const full = path5.join(dir, entry.name);
11386
+ const full = path7.join(dir, entry.name);
10823
11387
  let stat;
10824
11388
  try {
10825
- stat = fs2.statSync(full);
11389
+ stat = fs4.statSync(full);
10826
11390
  } catch {
10827
11391
  continue;
10828
11392
  }
@@ -10834,30 +11398,30 @@ function findRestrictedAgentNames(roots) {
10834
11398
  filesSeen++;
10835
11399
  let text;
10836
11400
  try {
10837
- text = fs2.readFileSync(full, "utf-8");
11401
+ text = fs4.readFileSync(full, "utf-8");
10838
11402
  } catch {
10839
11403
  continue;
10840
11404
  }
10841
- const parsed = parseAgentDefinition(text, path5.basename(entry.name, path5.extname(entry.name)));
11405
+ const parsed = parseAgentDefinition(text, path7.basename(entry.name, path7.extname(entry.name)));
10842
11406
  if (parsed !== null && parsed.restricted) names.add(parsed.name);
10843
11407
  }
10844
11408
  };
10845
11409
  for (const root of scanRoots) {
10846
- const isProjectRoot = roots === void 0 && path5.resolve(root) === path5.resolve(projectAgentsRoot);
11410
+ const isProjectRoot = roots === void 0 && path7.resolve(root) === path7.resolve(projectAgentsRoot);
10847
11411
  if (!isProjectRoot) {
10848
11412
  walk(root, 0, null);
10849
11413
  continue;
10850
11414
  }
10851
11415
  let rootIsLink;
10852
11416
  try {
10853
- rootIsLink = fs2.lstatSync(root).isSymbolicLink();
11417
+ rootIsLink = fs4.lstatSync(root).isSymbolicLink();
10854
11418
  } catch {
10855
11419
  continue;
10856
11420
  }
10857
11421
  if (rootIsLink) continue;
10858
11422
  let confineTo;
10859
11423
  try {
10860
- confineTo = fs2.realpathSync(root);
11424
+ confineTo = fs4.realpathSync(root);
10861
11425
  } catch {
10862
11426
  continue;
10863
11427
  }
@@ -10867,7 +11431,8 @@ function findRestrictedAgentNames(roots) {
10867
11431
  }
10868
11432
  function buildUnrestrictedSpawnAdvisory(toolInput) {
10869
11433
  try {
10870
- if (getHarnessName() === "copilot_cli") return "";
11434
+ const harness = getHarnessName();
11435
+ if (harness === "copilot_cli" || harness === "vscode") return "";
10871
11436
  const rawType = toolInput["subagent_type"];
10872
11437
  const spawnType = typeof rawType === "string" ? rawType.trim() : "";
10873
11438
  if (spawnType !== "" && spawnType !== "general-purpose") return "";
@@ -10883,6 +11448,71 @@ function buildUnrestrictedSpawnAdvisory(toolInput) {
10883
11448
  return "";
10884
11449
  }
10885
11450
  }
11451
+ var SCOPED_SPAWN_DENY_KEY = "agent-scoped-spawn-deny";
11452
+ function buildScopedSpawnDeny(toolInput) {
11453
+ if (!loadConfig().hints.agent_scoped_spawn_deny) return null;
11454
+ const harness = getHarnessName();
11455
+ if (harness === "copilot_cli" || harness === "vscode") return null;
11456
+ const rawType = toolInput["subagent_type"];
11457
+ if (typeof rawType === "string" && rawType.trim() !== "") return null;
11458
+ if (wasHintShown(SCOPED_SPAWN_DENY_KEY)) return null;
11459
+ const names = findRestrictedAgentNames();
11460
+ if (names.length === 0) return null;
11461
+ markHintShown(SCOPED_SPAWN_DENY_KEY);
11462
+ markHintShown(SPAWN_RESTRICT_HINT_KEY);
11463
+ recordStat("agent_scoped_spawn_deny", 0, 0);
11464
+ const shown = neutralizeSpokenMarkers(names.slice(0, SPAWN_RESTRICT_MAX_NAMES).join(", "));
11465
+ const more = names.length > SPAWN_RESTRICT_MAX_NAMES ? ` and ${names.length - SPAWN_RESTRICT_MAX_NAMES} more` : "";
11466
+ return denyOutput(`This Agent call omits subagent_type, so it would run as general-purpose, which is unrestricted: its lane starts by paying for every tool and MCP schema on the machine. Tools-restricted agent definitions exist here: ${shown}${more}. Re-issue the call with one of them as subagent_type if it fits the task. If none fits, re-issue it unchanged and it will run: this refusal fires once per session.`);
11467
+ }
11468
+ function reportNotice(resultText, recallHint, spawnAdvisory) {
11469
+ return `[token-goat] This subagent report (${toKB(resultText.length)}KB) is cached for later recall: ${recallHint}${spawnAdvisory === "" ? "" : `
11470
+ ${spawnAdvisory}`}`;
11471
+ }
11472
+ var REWRITTEN_REPORT_RE = /\n\n\[token-goat\] This subagent report \([0-9.]+KB\) is cached for later recall: token-goat mcp-output mcp_[0-9a-f]{16} --full$/;
11473
+ function planReportCompaction(resultText, recallHint, notice) {
11474
+ const cfg = loadConfig().agent_report;
11475
+ const collapsed = collapseFencedBlocks(resultText, recallHint, cfg.fence_collapse_min_lines, cfg.fence_collapse_keep_lines);
11476
+ const deduped = dedupeFencedBlocks(collapsed, resultText, recallHint);
11477
+ const final = collapseBlankRunsInFences(deduped);
11478
+ if (final === resultText) return { final, worthwhile: false };
11479
+ const worthwhile = isRewriteWorthwhile({
11480
+ originalBytes: Buffer.byteLength(resultText, "utf-8"),
11481
+ rewrittenBytes: Buffer.byteLength(final, "utf-8"),
11482
+ noticeBytes: Buffer.byteLength(notice, "utf-8"),
11483
+ minNetSavingsBytes: resolveMinNetSavingsBytes()
11484
+ });
11485
+ return { final, worthwhile };
11486
+ }
11487
+ function recordReportCompaction(resultText, updatedOutput) {
11488
+ const savedBytes = Buffer.byteLength(resultText, "utf-8") - Buffer.byteLength(updatedOutput, "utf-8");
11489
+ if (savedBytes > 0) recordStat("agent_report_compact", savedBytes, savedTokensFromBytes(savedBytes));
11490
+ }
11491
+ function subagentReportRewriteHandler(event) {
11492
+ try {
11493
+ if (getHarnessName() !== "copilot_cli" || !event.sessionId) return passOutput();
11494
+ const raw = event.raw["last_assistant_message"];
11495
+ if (typeof raw !== "string" || raw === "") return passOutput();
11496
+ const redactedReport = redactSecrets(raw);
11497
+ const resultText = redactedReport.text;
11498
+ if (resultText.length < loadConfig().agent_report.min_bytes) return passOutput();
11499
+ const toolInput = { agent_id: event.agentId ?? "" };
11500
+ const id = mcpOutputId(event.sessionId, mcpHash("task", toolInput));
11501
+ const recallHint = `token-goat mcp-output ${id} --full`;
11502
+ const notice = reportNotice(resultText, recallHint, "");
11503
+ const { final, worthwhile } = planReportCompaction(resultText, recallHint, notice);
11504
+ if (!worthwhile) return passOutput();
11505
+ if (storeMcpOutput(event.sessionId, "task", toolInput, resultText) !== id) return passOutput();
11506
+ if (redactedReport.count > 0) recordStat("secret_redacted", 0, redactedReport.count, void 0, "agent");
11507
+ const updatedOutput = `${final}
11508
+
11509
+ ${notice}`;
11510
+ recordReportCompaction(resultText, updatedOutput);
11511
+ return emitRewrite(updatedOutput, "agent", void 0, "counted-elsewhere");
11512
+ } catch {
11513
+ return passOutput();
11514
+ }
11515
+ }
10886
11516
  function postAgentHandler(event) {
10887
11517
  try {
10888
11518
  if (!isAgentTool(event.toolName) || !event.sessionId) return passOutput();
@@ -10892,6 +11522,7 @@ function postAgentHandler(event) {
10892
11522
  }
10893
11523
  const redactedReport = redactSecrets(extractToolResultText(event.raw));
10894
11524
  const resultText = redactedReport.text;
11525
+ if (REWRITTEN_REPORT_RE.test(resultText)) return passOutput();
10895
11526
  const agentReportCfg = loadConfig().agent_report;
10896
11527
  const spawnAdvisory = buildUnrestrictedSpawnAdvisory(event.toolInput);
10897
11528
  if (!resultText || resultText.length < agentReportCfg.min_bytes) {
@@ -10902,25 +11533,14 @@ function postAgentHandler(event) {
10902
11533
  if (redactedReport.count > 0) recordStat("secret_redacted", 0, redactedReport.count, void 0, "agent");
10903
11534
  recordStat("session_hint", 0, 0);
10904
11535
  const recallHint = `token-goat mcp-output ${id} --full`;
10905
- const notice = `[token-goat] This subagent report (${toKB(resultText.length)}KB) is cached for later recall: ${recallHint}${spawnAdvisory === "" ? "" : `
10906
- ${spawnAdvisory}`}`;
10907
- const collapsed = collapseFencedBlocks(resultText, recallHint, agentReportCfg.fence_collapse_min_lines, agentReportCfg.fence_collapse_keep_lines);
10908
- const deduped = dedupeFencedBlocks(collapsed, resultText, recallHint);
10909
- const final = collapseBlankRunsInFences(deduped);
10910
- const originalBytes = Buffer.byteLength(resultText, "utf-8");
11536
+ const notice = reportNotice(resultText, recallHint, spawnAdvisory);
11537
+ const { final, worthwhile } = planReportCompaction(resultText, recallHint, notice);
10911
11538
  if (final !== resultText) {
10912
- const worthwhile = isRewriteWorthwhile({
10913
- originalBytes,
10914
- rewrittenBytes: Buffer.byteLength(final, "utf-8"),
10915
- noticeBytes: Buffer.byteLength(notice, "utf-8"),
10916
- minNetSavingsBytes: resolveMinNetSavingsBytes()
10917
- });
10918
11539
  if (worthwhile) {
10919
11540
  const updatedOutput = `${final}
10920
11541
 
10921
11542
  ${notice}`;
10922
- const savedBytes = originalBytes - Buffer.byteLength(updatedOutput, "utf-8");
10923
- if (savedBytes > 0) recordStat("agent_report_compact", savedBytes, savedTokensFromBytes(savedBytes));
11543
+ recordReportCompaction(resultText, updatedOutput);
10924
11544
  return emitRewrite(updatedOutput, "agent", void 0, "counted-elsewhere");
10925
11545
  }
10926
11546
  recordStat("agent_report_compact_declined", 0, 0);
@@ -10930,6 +11550,8 @@ ${notice}`;
10930
11550
  return passOutput();
10931
11551
  }
10932
11552
  }
11553
+ registerHook("subagent_start", subagentStartHandler);
11554
+ registerHook("subagent_stop", subagentReportRewriteHandler, { advisory: true });
10933
11555
  registerHook("pre_tool_use", preAgentHandler, { toolName: "Agent" });
10934
11556
  registerHook("pre_tool_use", preAgentHandler, { toolName: "task" });
10935
11557
  registerHook("pre_tool_use", preAgentHandler, { toolName: "Task" });
@@ -10950,7 +11572,7 @@ function buildEvent(eventName, payload) {
10950
11572
  const rawSession = obj["session_id"] ?? obj["sessionId"];
10951
11573
  const sessionId = typeof rawSession === "string" ? rawSession : "";
10952
11574
  const rawAgentId = obj["agent_id"] ?? obj["agentId"];
10953
- const agentId = typeof rawAgentId === "string" && rawAgentId !== "" ? rawAgentId : void 0;
11575
+ const agentId = eventName !== "subagent_start" && typeof rawAgentId === "string" && rawAgentId !== "" ? rawAgentId : void 0;
10954
11576
  const rawTraceparent = obj["traceparent"] ?? obj["traceParent"] ?? process.env["TRACEPARENT"] ?? process.env["traceparent"];
10955
11577
  const traceparent = typeof rawTraceparent === "string" && rawTraceparent.trim() !== "" ? rawTraceparent.trim() : void 0;
10956
11578
  const rawTracestate = obj["tracestate"] ?? obj["traceState"] ?? process.env["TRACESTATE"] ?? process.env["tracestate"];
@@ -10966,21 +11588,41 @@ function harnessForNormalization() {
10966
11588
  if (detected === "qwen") return "qwen";
10967
11589
  if (detected === "copilot_cli") return "copilot_cli";
10968
11590
  if (detected === "vscode") return "vscode";
11591
+ if (detected === "antigravity") return "antigravity";
10969
11592
  return "claude";
10970
11593
  }
10971
11594
  function safeSuggestions(output) {
10972
11595
  if (output.hookType === "deny") return { hookType: "deny", message: stripUnsafeSuggestions(output.message) };
10973
- if (output.hookType === "context") return { hookType: "context", context: stripUnsafeSuggestions(output.context) };
11596
+ if (output.hookType === "context") return { hookType: "context", context: stripUnsafeSuggestions(output.context), ...output.notice !== void 0 ? { notice: output.notice } : {} };
11597
+ if (output.hookType === "rewriteOutput" && output.context !== void 0) return { ...output, context: stripUnsafeSuggestions(output.context) };
10974
11598
  return output;
10975
11599
  }
10976
11600
  var relayQueue = Promise.resolve();
10977
- function relayInProcess(eventName, rawPayload, harnessWaitMs) {
11601
+ var RELAY_QUEUE_WAIT_MS = 12e4;
11602
+ function afterPredecessor(previous) {
11603
+ return new Promise((resolve3) => {
11604
+ const timer = setTimeout(resolve3, RELAY_QUEUE_WAIT_MS);
11605
+ timer.unref();
11606
+ void previous.then(() => {
11607
+ clearTimeout(timer);
11608
+ resolve3();
11609
+ });
11610
+ });
11611
+ }
11612
+ var NATIVE_FALLBACK_ENV = "TOKEN_GOAT_NATIVE_FALLBACK";
11613
+ function nativeFallbackDetail(env = process.env) {
11614
+ const reason = env[NATIVE_FALLBACK_ENV];
11615
+ return reason !== void 0 && /^[a-z0-9-]{1,40}$/.test(reason) ? `native-fallback:${reason}` : void 0;
11616
+ }
11617
+ function relayInProcess(eventName, rawPayload, harnessWaitMs, opts = {}) {
10978
11618
  const receivedAt = Date.now();
10979
- const run = relayQueue.then(() => relayOne(eventName, rawPayload, harnessWaitMs, receivedAt));
11619
+ const detail = opts.detail ?? nativeFallbackDetail();
11620
+ const probe = process.env[HOOK_PROBE_ENV] === "1";
11621
+ const run = afterPredecessor(relayQueue).then(() => relayOne(eventName, rawPayload, harnessWaitMs, receivedAt, opts.elapsedMs ?? (() => performance.now()), opts.afterReply, detail, probe));
10980
11622
  relayQueue = run.catch(() => void 0);
10981
11623
  return run;
10982
11624
  }
10983
- async function relayOne(eventName, rawPayload, harnessWaitMs, receivedAt) {
11625
+ async function relayOne(eventName, rawPayload, harnessWaitMs, receivedAt, elapsedMs, afterReply, detail, probe = false) {
10984
11626
  if (!isHookEventName(eventName)) {
10985
11627
  return "{}";
10986
11628
  }
@@ -10989,8 +11631,16 @@ async function relayOne(eventName, rawPayload, harnessWaitMs, receivedAt) {
10989
11631
  const payload = eventName === "pre_tool_use" || eventName === "post_tool_use" ? normalizePayload(rawPayload, harnessForNormalization()) : rawPayload;
10990
11632
  const event = buildEvent(eventName, payload);
10991
11633
  if (shouldSuppressDuplicateVscodeHook(event, harness)) return "{}";
10992
- if (!process.env["CLAUDE_CODE_SESSION_ID"] && event.sessionId) {
10993
- process.env["CLAUDE_CODE_SESSION_ID"] = event.sessionId;
11634
+ const envSessionId = process.env["CLAUDE_CODE_SESSION_ID"];
11635
+ const seededSessionId = relaySeededSessionId();
11636
+ if (!envSessionId || envSessionId === seededSessionId) {
11637
+ if (event.sessionId) {
11638
+ process.env["CLAUDE_CODE_SESSION_ID"] = event.sessionId;
11639
+ setRelaySeededSessionId(event.sessionId);
11640
+ } else if (seededSessionId !== void 0) {
11641
+ delete process.env["CLAUDE_CODE_SESSION_ID"];
11642
+ setRelaySeededSessionId(void 0);
11643
+ }
10994
11644
  }
10995
11645
  const rawTranscriptPath = event.raw["transcript_path"];
10996
11646
  if (typeof rawTranscriptPath === "string" && rawTranscriptPath !== "") {
@@ -11007,7 +11657,7 @@ async function relayOne(eventName, rawPayload, harnessWaitMs, receivedAt) {
11007
11657
  loadSessionState(stateKey);
11008
11658
  } catch {
11009
11659
  }
11010
- const output = safeSuggestions(sharpenRepeatedDeny(event, applyCallStreak(event, await runHook(event), receivedAt)));
11660
+ const output = applyProbeMarker(event.eventName, safeSuggestions(sharpenRepeatedDeny(event, applyCallStreak(event, await runHook(event), receivedAt))));
11011
11661
  if (event.sessionId) {
11012
11662
  commitPendingContext(stateKey, output.hookType === "context" ? output.context : null);
11013
11663
  }
@@ -11019,7 +11669,12 @@ async function relayOne(eventName, rawPayload, harnessWaitMs, receivedAt) {
11019
11669
  } catch {
11020
11670
  return "{}";
11021
11671
  } finally {
11022
- recordStat(`hook:${eventName}`, 0, 0, void 0, void 0, void 0, harnessWaitMs ?? performance.now());
11672
+ const durationMs = harnessWaitMs ?? elapsedMs();
11673
+ const record = () => {
11674
+ if (!probe) recordStat(`hook:${eventName}`, 0, 0, void 0, detail, void 0, durationMs);
11675
+ };
11676
+ if (afterReply) afterReply(record);
11677
+ else record();
11023
11678
  }
11024
11679
  }
11025
11680
  async function relay(eventName) {
@@ -11045,6 +11700,9 @@ async function relay(eventName) {
11045
11700
 
11046
11701
  export {
11047
11702
  buildEvent,
11703
+ RELAY_QUEUE_WAIT_MS,
11704
+ NATIVE_FALLBACK_ENV,
11705
+ nativeFallbackDetail,
11048
11706
  relayInProcess,
11049
11707
  relay
11050
11708
  };