token-goat 2.9.30 → 2.9.31

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 (172) hide show
  1. package/README.md +24 -7
  2. package/SECURITY.md +10 -2
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/dist/native/linux-arm64/tg-hook +0 -0
  5. package/dist/native/linux-x64/tg-hook +0 -0
  6. package/dist/native/win32-arm64/tg-hook.exe +0 -0
  7. package/dist/native/win32-x64/tg-hook.exe +0 -0
  8. package/dist/{token-goat-chunk-QSBRTGM2.mjs → token-goat-chunk-25TRAWET.mjs} +17 -7
  9. package/dist/token-goat-chunk-2KXO6V2B.mjs +203 -0
  10. package/dist/{token-goat-chunk-IVKIZWAB.mjs → token-goat-chunk-2M7E4PF7.mjs} +41 -12
  11. package/dist/{token-goat-chunk-SMVRN2HU.mjs → token-goat-chunk-2UPZGKKY.mjs} +2 -2
  12. package/dist/token-goat-chunk-2WLPWKHV.mjs +135 -0
  13. package/dist/token-goat-chunk-2XBMMDBD.mjs +126 -0
  14. package/dist/token-goat-chunk-32ZOY4N5.mjs +23 -0
  15. package/dist/token-goat-chunk-3EZRNKTK.mjs +188 -0
  16. package/dist/token-goat-chunk-4ADZHI7J.mjs +63 -0
  17. package/dist/token-goat-chunk-4GBVPH3D.mjs +242 -0
  18. package/dist/{token-goat-chunk-KWPNJBDH.mjs → token-goat-chunk-4GILLLWY.mjs} +7 -4
  19. package/dist/{token-goat-chunk-67TQJ76B.mjs → token-goat-chunk-4NAPX7BA.mjs} +36 -3
  20. package/dist/{token-goat-chunk-PU7V3FD6.mjs → token-goat-chunk-4SC7UJYV.mjs} +3354 -2553
  21. package/dist/token-goat-chunk-4SMRO4TO.mjs +366 -0
  22. package/dist/{token-goat-chunk-AFLYLHDH.mjs → token-goat-chunk-4ZBFWLXF.mjs} +283 -427
  23. package/dist/{token-goat-chunk-JDWCRD6M.mjs → token-goat-chunk-57BIAXJL.mjs} +73 -40
  24. package/dist/token-goat-chunk-5AC66HNP.mjs +115 -0
  25. package/dist/token-goat-chunk-5IMLKSOP.mjs +145 -0
  26. package/dist/token-goat-chunk-646E6U44.mjs +398 -0
  27. package/dist/token-goat-chunk-65N4RHP4.mjs +33 -0
  28. package/dist/{token-goat-chunk-JN4XMLSD.mjs → token-goat-chunk-6DG6HWP3.mjs} +8 -2
  29. package/dist/token-goat-chunk-6ENQSNRZ.mjs +573 -0
  30. package/dist/token-goat-chunk-6ZY4GNF2.mjs +91 -0
  31. package/dist/{token-goat-chunk-VPBBTSJR.mjs → token-goat-chunk-76FGDLRO.mjs} +262 -255
  32. package/dist/{token-goat-chunk-7IN2MKDS.mjs → token-goat-chunk-7EZTZOMC.mjs} +121 -289
  33. package/dist/{token-goat-chunk-OJBNYTJP.mjs → token-goat-chunk-7MAOA6X4.mjs} +640 -80
  34. package/dist/token-goat-chunk-7R2ZVVAX.mjs +218 -0
  35. package/dist/{token-goat-chunk-PD5VKH46.mjs → token-goat-chunk-AUNGYYQX.mjs} +2 -2
  36. package/dist/{token-goat-chunk-HG45AF2B.mjs → token-goat-chunk-AUQYRMMH.mjs} +794 -37
  37. package/dist/token-goat-chunk-B3PEKBYG.mjs +128 -0
  38. package/dist/{token-goat-chunk-A3CK6N5Z.mjs → token-goat-chunk-BA4HI6BW.mjs} +1309 -110
  39. package/dist/{token-goat-chunk-JB6QN6VF.mjs → token-goat-chunk-BFCWY73P.mjs} +59 -23
  40. package/dist/{token-goat-chunk-B45VMIIH.mjs → token-goat-chunk-BWGFUZZE.mjs} +7 -5
  41. package/dist/{token-goat-chunk-P4QMM4WK.mjs → token-goat-chunk-C75NA7PL.mjs} +50 -23
  42. package/dist/token-goat-chunk-CBIQ7WTD.mjs +87 -0
  43. package/dist/token-goat-chunk-CETM74KC.mjs +307 -0
  44. package/dist/{token-goat-chunk-H2PHHGM5.mjs → token-goat-chunk-CIMQ2BB3.mjs} +57 -12
  45. package/dist/{token-goat-chunk-N2NMER56.mjs → token-goat-chunk-CJLS3CDL.mjs} +46 -4
  46. package/dist/{token-goat-chunk-VES7EL7O.mjs → token-goat-chunk-CO3UYX6G.mjs} +1 -1
  47. package/dist/token-goat-chunk-COPNXRJM.mjs +154 -0
  48. package/dist/token-goat-chunk-CSJ5L3NL.mjs +479 -0
  49. package/dist/{token-goat-chunk-4LCCNRTG.mjs → token-goat-chunk-CVCEVUGK.mjs} +8 -6
  50. package/dist/{token-goat-chunk-B2RZZCSS.mjs → token-goat-chunk-CXHOFN7R.mjs} +37 -20
  51. package/dist/token-goat-chunk-D735C4QM.mjs +603 -0
  52. package/dist/{token-goat-chunk-APGZXSPS.mjs → token-goat-chunk-DGS2LVNA.mjs} +47 -30
  53. package/dist/{token-goat-chunk-54K3EUV3.mjs → token-goat-chunk-DOWRSMWO.mjs} +1 -1
  54. package/dist/token-goat-chunk-E45INMY3.mjs +71 -0
  55. package/dist/token-goat-chunk-EC5F37F4.mjs +244 -0
  56. package/dist/{token-goat-chunk-XTQAOTSO.mjs → token-goat-chunk-EDBDRVH5.mjs} +4 -1
  57. package/dist/{token-goat-chunk-5BCIJXMO.mjs → token-goat-chunk-EELTBKDM.mjs} +14 -10
  58. package/dist/{token-goat-chunk-W3QCJS6G.mjs → token-goat-chunk-F4HYL64R.mjs} +8 -6
  59. package/dist/token-goat-chunk-F6PAUIEK.mjs +624 -0
  60. package/dist/token-goat-chunk-FDSREWLX.mjs +692 -0
  61. package/dist/token-goat-chunk-GGGG7RWB.mjs +236 -0
  62. package/dist/{token-goat-chunk-C63JQV5K.mjs → token-goat-chunk-GL3ETKSC.mjs} +233 -15
  63. package/dist/token-goat-chunk-GVB2MUST.mjs +1298 -0
  64. package/dist/token-goat-chunk-GWFLAUMQ.mjs +307 -0
  65. package/dist/{token-goat-chunk-ZEJVMUQH.mjs → token-goat-chunk-HLPW63A7.mjs} +2 -2
  66. package/dist/{token-goat-chunk-GOLEOXZP.mjs → token-goat-chunk-HU4TORC5.mjs} +44 -26
  67. package/dist/{token-goat-chunk-Z57YODA2.mjs → token-goat-chunk-I7VKZZAW.mjs} +2 -2
  68. package/dist/{token-goat-chunk-NYKZLXMK.mjs → token-goat-chunk-IA6XSPE3.mjs} +6 -6
  69. package/dist/{token-goat-chunk-E3OQX2XT.mjs → token-goat-chunk-IAGXMHIK.mjs} +81 -42
  70. package/dist/{token-goat-chunk-HYBFA5X3.mjs → token-goat-chunk-IK2DRMFX.mjs} +24 -68
  71. package/dist/{token-goat-chunk-COBQGL5S.mjs → token-goat-chunk-INNPTRUX.mjs} +249 -299
  72. package/dist/{token-goat-chunk-7NADEDKJ.mjs → token-goat-chunk-JJ5BX4F4.mjs} +6 -1
  73. package/dist/{token-goat-chunk-PEPTHRFF.mjs → token-goat-chunk-JVFSHVFJ.mjs} +6 -6
  74. package/dist/token-goat-chunk-KGEFFSHD.mjs +50 -0
  75. package/dist/token-goat-chunk-KQBLRPBN.mjs +433 -0
  76. package/dist/{token-goat-chunk-Q5VTGRH7.mjs → token-goat-chunk-LCNQ2CNN.mjs} +55 -319
  77. package/dist/token-goat-chunk-M3LFJDPU.mjs +58 -0
  78. package/dist/token-goat-chunk-M52KPT3C.mjs +87 -0
  79. package/dist/token-goat-chunk-MBEG42H7.mjs +150 -0
  80. package/dist/{token-goat-chunk-EVC4TOLE.mjs → token-goat-chunk-METECDOH.mjs} +3 -12
  81. package/dist/{token-goat-chunk-T4FK7VOV.mjs → token-goat-chunk-MHX7XCC3.mjs} +2 -1
  82. package/dist/token-goat-chunk-N5LSOVSU.mjs +137 -0
  83. package/dist/{token-goat-chunk-LOBGHXPB.mjs → token-goat-chunk-N5MMR2IW.mjs} +1 -1
  84. package/dist/token-goat-chunk-N7B3JQ2C.mjs +160 -0
  85. package/dist/{token-goat-chunk-RHXKCZX3.mjs → token-goat-chunk-NISNMD7T.mjs} +2182 -2959
  86. package/dist/token-goat-chunk-NMD42SFQ.mjs +434 -0
  87. package/dist/token-goat-chunk-NOSS5VWY.mjs +174 -0
  88. package/dist/{token-goat-chunk-KQNZDCBD.mjs → token-goat-chunk-NSNSPPBG.mjs} +25 -3
  89. package/dist/token-goat-chunk-OBOFIHKN.mjs +38 -0
  90. package/dist/token-goat-chunk-OBRFJ5O5.mjs +76 -0
  91. package/dist/{token-goat-chunk-JTQQGXZH.mjs → token-goat-chunk-ODBT2USM.mjs} +124 -57
  92. package/dist/token-goat-chunk-OS5EM5PP.mjs +107 -0
  93. package/dist/token-goat-chunk-OUKDTVNU.mjs +1178 -0
  94. package/dist/token-goat-chunk-OXFMP75N.mjs +566 -0
  95. package/dist/{token-goat-chunk-DZX2YDPT.mjs → token-goat-chunk-OYIYKDDT.mjs} +2 -2
  96. package/dist/{token-goat-chunk-QCLDAQTS.mjs → token-goat-chunk-P6HQSWQ3.mjs} +50 -53
  97. package/dist/token-goat-chunk-P6VGNTQT.mjs +71 -0
  98. package/dist/{token-goat-chunk-7BXSNBY5.mjs → token-goat-chunk-PFWIMH2E.mjs} +23 -15
  99. package/dist/token-goat-chunk-PJZXIS3G.mjs +39 -0
  100. package/dist/token-goat-chunk-PUO5INXP.mjs +606 -0
  101. package/dist/{token-goat-chunk-GFBSN7BD.mjs → token-goat-chunk-PX5PQWTY.mjs} +22 -250
  102. package/dist/token-goat-chunk-Q5QDBC6O.mjs +111 -0
  103. package/dist/token-goat-chunk-QFNQXSDT.mjs +40 -0
  104. package/dist/{token-goat-chunk-FBNQNSJK.mjs → token-goat-chunk-QH7YVYOY.mjs} +2198 -2782
  105. package/dist/{token-goat-chunk-64FBPDTK.mjs → token-goat-chunk-QLMUHEPB.mjs} +12 -9
  106. package/dist/{token-goat-chunk-RZRTDST7.mjs → token-goat-chunk-QQOJIHL3.mjs} +37 -11
  107. package/dist/{token-goat-chunk-XKI4SEMX.mjs → token-goat-chunk-QQTWJY2O.mjs} +19 -0
  108. package/dist/{token-goat-chunk-U4N3BN2W.mjs → token-goat-chunk-QYHO5CPH.mjs} +3 -2
  109. package/dist/token-goat-chunk-RELAVWAD.mjs +27 -0
  110. package/dist/{token-goat-chunk-ETBHVDB6.mjs → token-goat-chunk-RSLPOBKQ.mjs} +22 -11
  111. package/dist/token-goat-chunk-SAMFDIS2.mjs +712 -0
  112. package/dist/token-goat-chunk-SDELXLDM.mjs +48 -0
  113. package/dist/{token-goat-chunk-4JJS54TR.mjs → token-goat-chunk-SOAFZNNZ.mjs} +161 -38
  114. package/dist/{token-goat-chunk-664RTTUV.mjs → token-goat-chunk-SRTHT7KV.mjs} +21 -16
  115. package/dist/{token-goat-chunk-4FVIAHSI.mjs → token-goat-chunk-T5P5ACNQ.mjs} +7 -6
  116. package/dist/token-goat-chunk-T5YGG4V3.mjs +45 -0
  117. package/dist/token-goat-chunk-TAUZ4OFS.mjs +110 -0
  118. package/dist/{token-goat-chunk-COTXZHTS.mjs → token-goat-chunk-TM6MC754.mjs} +4 -3
  119. package/dist/token-goat-chunk-TWUKBOMP.mjs +72 -0
  120. package/dist/{token-goat-chunk-3CHUNJ2O.mjs → token-goat-chunk-U2KSB4SG.mjs} +8 -5
  121. package/dist/{token-goat-chunk-3Z5S4KGM.mjs → token-goat-chunk-U4WHMG6Y.mjs} +29 -49
  122. package/dist/{token-goat-chunk-PY533T7F.mjs → token-goat-chunk-UGJHGR5A.mjs} +21 -15
  123. package/dist/{token-goat-chunk-CYCEYAEO.mjs → token-goat-chunk-UOUDVA5U.mjs} +1 -1
  124. package/dist/{token-goat-chunk-JDHE26RP.mjs → token-goat-chunk-URRJ33GR.mjs} +16 -5
  125. package/dist/{token-goat-chunk-56EIMS3E.mjs → token-goat-chunk-UZOROKIS.mjs} +23 -17
  126. package/dist/{token-goat-chunk-CZ3LUXYZ.mjs → token-goat-chunk-VIHGGGEI.mjs} +427 -308
  127. package/dist/token-goat-chunk-VTJLAA7P.mjs +21 -0
  128. package/dist/token-goat-chunk-VWDXLT4F.mjs +678 -0
  129. package/dist/{token-goat-chunk-YARZFCZS.mjs → token-goat-chunk-VWZA2H4P.mjs} +62 -53
  130. package/dist/{token-goat-chunk-SQSSNQPE.mjs → token-goat-chunk-VXEXMJDL.mjs} +3979 -4745
  131. package/dist/token-goat-chunk-WBA5SKT4.mjs +547 -0
  132. package/dist/{token-goat-chunk-SJHWVNDQ.mjs → token-goat-chunk-WDH44VAU.mjs} +7 -4
  133. package/dist/token-goat-chunk-X2SUP3EI.mjs +107 -0
  134. package/dist/token-goat-chunk-X3BELZSJ.mjs +125 -0
  135. package/dist/{token-goat-chunk-FZ3NEW5O.mjs → token-goat-chunk-XLRBNZCC.mjs} +13 -8
  136. package/dist/{token-goat-chunk-Z44YO3QZ.mjs → token-goat-chunk-XRKUX6CM.mjs} +1 -1
  137. package/dist/token-goat-chunk-XX6O25RN.mjs +82 -0
  138. package/dist/token-goat-chunk-XZSUHBVS.mjs +65 -0
  139. package/dist/{token-goat-chunk-WY3L4DVT.mjs → token-goat-chunk-Y2ZTQOCL.mjs} +13 -8
  140. package/dist/{token-goat-chunk-UQ5UK2T7.mjs → token-goat-chunk-YB4VMGK2.mjs} +1 -1
  141. package/dist/token-goat-chunk-YBIIY3HR.mjs +51 -0
  142. package/dist/{token-goat-chunk-ZQSJMO2R.mjs → token-goat-chunk-YHVDKQBR.mjs} +105 -105
  143. package/dist/{token-goat-chunk-H2K3QP6H.mjs → token-goat-chunk-YJB5XLWI.mjs} +8 -6
  144. package/dist/{token-goat-chunk-ZCDTCVIS.mjs → token-goat-chunk-YKB6AQG3.mjs} +5 -2
  145. package/dist/token-goat-chunk-YMTG2YA5.mjs +35 -0
  146. package/dist/token-goat-chunk-YPBZYQRV.mjs +40 -0
  147. package/dist/{token-goat-chunk-HOJLCGDP.mjs → token-goat-chunk-YY6AVUFL.mjs} +2033 -2251
  148. package/dist/token-goat-chunk-Z7N5OMMP.mjs +98 -0
  149. package/dist/token-goat-chunk-ZNMNUPAZ.mjs +938 -0
  150. package/dist/token-goat-hook-client.cjs +1 -1
  151. package/dist/token-goat-hook-client.mjs +3 -3
  152. package/dist/token-goat-hook.mjs +67 -38
  153. package/dist/token-goat.core.mjs +4 -4
  154. package/docs/C4_RUNTIME_ARCHITECTURE.md +1 -1
  155. package/docs/cli.md +40 -21
  156. package/docs/install.md +19 -9
  157. package/docs/security.md +5 -3
  158. package/package.json +2 -1
  159. package/dist/token-goat-chunk-5KDMCMF2.mjs +0 -362
  160. package/dist/token-goat-chunk-6QMAS7AB.mjs +0 -42
  161. package/dist/token-goat-chunk-7F2UBPD3.mjs +0 -397
  162. package/dist/token-goat-chunk-ARDNEC2U.mjs +0 -248
  163. package/dist/token-goat-chunk-ATCPY7EQ.mjs +0 -134
  164. package/dist/token-goat-chunk-EFHMMIGP.mjs +0 -68
  165. package/dist/token-goat-chunk-IVALLT3S.mjs +0 -48
  166. package/dist/token-goat-chunk-JDU3EXSU.mjs +0 -65
  167. package/dist/token-goat-chunk-MKPF4CL5.mjs +0 -37
  168. package/dist/token-goat-chunk-RC35RBUL.mjs +0 -312
  169. package/dist/token-goat-chunk-USILZ3T3.mjs +0 -53
  170. package/dist/token-goat-chunk-VL3YIFHZ.mjs +0 -2975
  171. package/dist/token-goat-chunk-XU4P6Z24.mjs +0 -133
  172. package/dist/token-goat-chunk-Y776AENC.mjs +0 -1597
@@ -2,16 +2,7 @@ import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
4
  savedTokensFromBytes
5
- } from "./token-goat-chunk-3Z5S4KGM.mjs";
6
- import {
7
- redactSecrets
8
- } from "./token-goat-chunk-4FVIAHSI.mjs";
9
- import {
10
- loadConfig
11
- } from "./token-goat-chunk-7IN2MKDS.mjs";
12
- import {
13
- resolveIndexPath
14
- } from "./token-goat-chunk-FNFBKLRD.mjs";
5
+ } from "./token-goat-chunk-U4WHMG6Y.mjs";
15
6
  import {
16
7
  DEFAULT_MAX_BYTES,
17
8
  DEFAULT_MAX_LINES,
@@ -57,10 +48,26 @@ import {
57
48
  trimRepeatedPrefix,
58
49
  truncateMiddleSmart,
59
50
  truncateTableRows
60
- } from "./token-goat-chunk-KWPNJBDH.mjs";
51
+ } from "./token-goat-chunk-4GILLLWY.mjs";
61
52
  import {
62
53
  stripAnsiEscapes
63
54
  } from "./token-goat-chunk-HAVAOB3S.mjs";
55
+ import {
56
+ redactSecrets
57
+ } from "./token-goat-chunk-T5P5ACNQ.mjs";
58
+ import {
59
+ loadConfig
60
+ } from "./token-goat-chunk-7EZTZOMC.mjs";
61
+ import {
62
+ countNoun
63
+ } from "./token-goat-chunk-HU4TORC5.mjs";
64
+ import {
65
+ echoedValue,
66
+ fencedCommand
67
+ } from "./token-goat-chunk-GGGG7RWB.mjs";
68
+ import {
69
+ resolveIndexPath
70
+ } from "./token-goat-chunk-FNFBKLRD.mjs";
64
71
  import {
65
72
  init_define_import_meta_env
66
73
  } from "./token-goat-chunk-A37V4PBF.mjs";
@@ -119,21 +126,15 @@ var CompressedOutput = class {
119
126
  if (this.originalBytes <= 0) return 0;
120
127
  return 100 * this.bytesSaved / this.originalBytes;
121
128
  }
122
- /**
123
- * Byte cost of the trailing marker {@link withMarker} would append for this result's filter name and percentage. The marker is not free — a rewrite whose `bytesSaved` doesn't even cover its own marker is strictly worse than doing nothing.
124
- */
129
+ /** Byte cost of the trailing marker {@link withMarker} would append for this result's filter name and percentage. The marker is not free — a rewrite whose `bytesSaved` doesn't even cover its own marker is strictly worse than doing nothing. */
125
130
  get markerBytes() {
126
131
  return byteLength(compressionMarker(this.filterName, this.percentSaved));
127
132
  }
128
- /**
129
- * Net savings after subtracting the marker's own byte cost — the true benefit of shipping the rewrite instead of the untouched original.
130
- */
133
+ /** Net savings after subtracting the marker's own byte cost — the true benefit of shipping the rewrite instead of the untouched original. */
131
134
  get netSavingsBytes() {
132
135
  return this.bytesSaved - this.markerBytes;
133
136
  }
134
- /**
135
- * Single definition of "is this rewrite worth shipping". `minNetSavingsBytes` is the configured floor (`bash_compress.min_net_savings_bytes`) below which a rewrite is considered too trivial to justify destabilising the bytes (breaks provider prefix caching) for — every consumer that chooses between the compressed body and the original output must go through this, not re-derive its own `bytesSaved > 0` check.
136
- */
137
+ /** Single definition of "is this rewrite worth shipping". `minNetSavingsBytes` is the configured floor (`bash_compress.min_net_savings_bytes`) below which a rewrite is considered too trivial to justify destabilising the bytes (breaks provider prefix caching) for — every consumer that chooses between the compressed body and the original output must go through this, not re-derive its own `bytesSaved > 0` check. */
137
138
  worthApplying(minNetSavingsBytes) {
138
139
  return isRewriteWorthwhile({
139
140
  originalBytes: this.originalBytes,
@@ -142,9 +143,7 @@ var CompressedOutput = class {
142
143
  minNetSavingsBytes
143
144
  });
144
145
  }
145
- /**
146
- * `text` with the trailing compression-summary marker appended. Skipped entirely when the rewrite doesn't clear `minNetSavingsBytes` (default 0, i.e. the marker must at least pay for itself) so raw output never carries a marker for a trivial or net-negative rewrite.
147
- */
146
+ /** `text` with the trailing compression-summary marker appended. Skipped entirely when the rewrite doesn't clear `minNetSavingsBytes` (default 0, i.e. the marker must at least pay for itself) so raw output never carries a marker for a trivial or net-negative rewrite. */
148
147
  withMarker(minNetSavingsBytes = 0) {
149
148
  if (!this.worthApplying(minNetSavingsBytes)) return this.text;
150
149
  return this.text + compressionMarker(this.filterName, this.percentSaved);
@@ -157,9 +156,7 @@ var ToolFilter = class {
157
156
  subcommands = /* @__PURE__ */ new Set();
158
157
  /** When true, pass raw error output through on non-zero exit. */
159
158
  errorPassthrough = false;
160
- /**
161
- * Return true when this filter should run for `argv`. Checks `binaries` against the lowercased stem (and full name, for dot-in-name binaries like `py.test`) of `argv[0]`; when `subcommands` is non-empty, requires one in the first three positional args.
162
- */
159
+ /** Return true when this filter should run for `argv`. Checks `binaries` against the lowercased stem (and full name, for dot-in-name binaries like `py.test`) of `argv[0]`; when `subcommands` is non-empty, requires one in the first three positional args. */
163
160
  matches(argv) {
164
161
  if (argv.length === 0) return false;
165
162
  const first = argv[0];
@@ -189,15 +186,11 @@ var ToolFilter = class {
189
186
  preClip(stdout, _argv) {
190
187
  return stdout;
191
188
  }
192
- /**
193
- * Hook applied to each stream right after {@link normalise}. Identity by default; the git filter family overrides it to strip CRLF warnings. Keeps `apply` free of per-family name checks.
194
- */
189
+ /** Hook applied to each stream right after {@link normalise}. Identity by default; the git filter family overrides it to strip CRLF warnings. Keeps `apply` free of per-family name checks. */
195
190
  postNormalise(text) {
196
191
  return text;
197
192
  }
198
- /**
199
- * Template: when {@link errorPassthrough} is set, return the raw combined error output on non-zero exit before delegating to {@link compressBody}. Filters that handle errors structurally (pytest, cargo) override this directly and leave `errorPassthrough` false.
200
- */
193
+ /** Template: when {@link errorPassthrough} is set, return the raw combined error output on non-zero exit before delegating to {@link compressBody}. Filters that handle errors structurally (pytest, cargo) override this directly and leave `errorPassthrough` false. */
201
194
  compress(stdout, stderr, exitCode, argv, ctx = {}) {
202
195
  if (this.errorPassthrough) {
203
196
  const err = preserveStderrOnError(stdout, stderr, exitCode);
@@ -205,18 +198,14 @@ var ToolFilter = class {
205
198
  }
206
199
  return this.compressBody(stdout, stderr, exitCode, argv, ctx);
207
200
  }
208
- /**
209
- * Inner compression logic, called after the error-passthrough guard. Default is a passthrough that joins the two streams — useful when the only compression is the ANSI / progress strip `apply` already performed.
210
- */
201
+ /** Inner compression logic, called after the error-passthrough guard. Default is a passthrough that joins the two streams — useful when the only compression is the ANSI / progress strip `apply` already performed. */
211
202
  compressBody(stdout, stderr, _exitCode, _argv, _ctx = {}) {
212
203
  if (stderr && stdout) return `${stdout.replace(/\s+$/, "")}
213
204
  ---
214
205
  ${stderr.replace(/\s+$/, "")}`;
215
206
  return stdout || stderr;
216
207
  }
217
- /**
218
- * Top-level entry: sanitise → input cap → normalise → compress → line/byte cap → wrap in {@link CompressedOutput}. Faithful port of the Python `apply` 10-step pipeline. Errors from {@link compress} fall back to a truncated view so the agent always sees something.
219
- */
208
+ /** Top-level entry: sanitise → input cap → normalise → compress → line/byte cap → wrap in {@link CompressedOutput}. Faithful port of the Python `apply` 10-step pipeline. Errors from {@link compress} fall back to a truncated view so the agent always sees something. */
220
209
  apply(stdout, stderr, exitCode, argv, opts = {}) {
221
210
  const maxLines = opts.maxLines ?? DEFAULT_MAX_LINES;
222
211
  const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
@@ -251,7 +240,7 @@ ${stderr.replace(/\s+$/, "")}`;
251
240
  if (soClipped !== so || seClipped !== se) {
252
241
  so = soClipped;
253
242
  se = seClipped;
254
- notes.push(`clipped line(s) wider than ${INPUT_MAX_LINE_CHARS} chars`);
243
+ notes.push(`clipped lines wider than ${INPUT_MAX_LINE_CHARS} chars`);
255
244
  }
256
245
  const originalBytes = soBytes.length + seBytes.length;
257
246
  if (!so.trim() && !se.trim()) {
@@ -290,7 +279,7 @@ ${stderr.replace(/\s+$/, "")}`;
290
279
  const redacted = redactSecrets(body);
291
280
  body = redacted.text;
292
281
  redactedCount += redacted.count;
293
- if (redactedCount > 0) notes.push(`redacted ${redactedCount} secret-shaped value(s)`);
282
+ if (redactedCount > 0) notes.push(`redacted ${countNoun(redactedCount, "secret-shaped value")}`);
294
283
  const shown = notes.filter((n) => n !== EARLY_EXIT_NOTE);
295
284
  if (shown.length) body = `[${shown.join("; ")}]
296
285
  ${body}`;
@@ -550,7 +539,7 @@ var GoTestFilter = class extends ToolFilter {
550
539
  maybeNote(
551
540
  notes,
552
541
  raceCount,
553
- `kept ${raceCount} DATA RACE block(s) verbatim (goroutine stacks collapsed)`
542
+ `kept ${countNoun(raceCount, "DATA RACE block")} verbatim (goroutine stacks collapsed)`
554
543
  );
555
544
  maybeNote(notes, passCount, `collapsed ${passCount} PASS testcases`);
556
545
  maybeNote(notes, skipCount, `collapsed ${skipCount} SKIP testcases`);
@@ -1002,7 +991,7 @@ var ghCopilotFilter = makeAiCliFilter({
1002
991
  name: "gh-copilot",
1003
992
  binaries: ["gh"],
1004
993
  dropRules: [_GH_COPILOT_SPINNER_RE, _GH_COPILOT_BANNER_RE, _GH_COPILOT_DISCLAIMER_RE],
1005
- droppedNoiseNote: (n) => `dropped ${n} boilerplate/disclaimer line(s)`,
994
+ droppedNoiseNote: (n) => `dropped ${countNoun(n, "boilerplate/disclaimer line")}`,
1006
995
  // GhCopilotFilter only fires for `gh copilot explain/suggest`; the broader GhFilter in CI_FILTERS claims all other `gh` commands.
1007
996
  customMatches: (argv) => {
1008
997
  if (!argv.length) return false;
@@ -1019,7 +1008,7 @@ var copilotFilter = makeAiCliFilter({
1019
1008
  binaries: ["copilot"],
1020
1009
  dropRules: [_COPILOT_WORKSPACE_NOISE_RE, _GH_COPILOT_SPINNER_RE, _GH_COPILOT_BANNER_RE, _GH_COPILOT_DISCLAIMER_RE],
1021
1010
  keepLastRules: [{ re: _COPILOT_COMPLETION_STATS_RE, note: (v) => `stats: ${v}` }],
1022
- droppedNoiseNote: (n) => `dropped ${n} boilerplate/disclaimer line(s)`
1011
+ droppedNoiseNote: (n) => `dropped ${countNoun(n, "boilerplate/disclaimer line")}`
1023
1012
  });
1024
1013
  var aiderFilter = makeAiCliFilter({
1025
1014
  name: "aider",
@@ -1029,14 +1018,14 @@ var aiderFilter = makeAiCliFilter({
1029
1018
  {
1030
1019
  re: _AIDER_APPLYING_RE,
1031
1020
  position: "prepend",
1032
- note: (n) => `[token-goat: ${n} 'applying edits' progress line(s) collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
1021
+ note: (n) => `[token-goat: ${countNoun(n, "'applying edits' progress line")} collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
1033
1022
  }
1034
1023
  ],
1035
1024
  keepLastRules: [
1036
1025
  { re: _AIDER_TOKENS_RE, note: (v) => `token usage: ${v}` },
1037
1026
  { re: _AIDER_COST_RE, note: (v) => `cost: ${v}` }
1038
1027
  ],
1039
- droppedNoiseNote: (n) => `dropped ${n} noise line(s)`
1028
+ droppedNoiseNote: (n) => `dropped ${countNoun(n, "noise line")}`
1040
1029
  });
1041
1030
  var geminiCliFilter = makeAiCliFilter({
1042
1031
  name: "gemini-cli",
@@ -1046,16 +1035,16 @@ var geminiCliFilter = makeAiCliFilter({
1046
1035
  {
1047
1036
  re: _GEMINI_STARTUP_RE,
1048
1037
  position: "prepend",
1049
- note: (n) => `[token-goat: ${n} Gemini CLI startup status line(s) collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
1038
+ note: (n) => `[token-goat: ${countNoun(n, "Gemini CLI startup status line")} collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
1050
1039
  },
1051
1040
  {
1052
1041
  re: _GEMINI_TOOL_SPINNER_RE,
1053
1042
  position: "prepend",
1054
- note: (n) => `[token-goat: ${n} tool-call spinner line(s) collapsed]`
1043
+ note: (n) => `[token-goat: ${countNoun(n, "tool-call spinner line")} collapsed]`
1055
1044
  }
1056
1045
  ],
1057
1046
  keepLastRules: [{ re: _GEMINI_TOKEN_METER_RE, note: (v) => `context: ${v}` }],
1058
- droppedNoiseNote: (n) => `dropped ${n} noise line(s)`
1047
+ droppedNoiseNote: (n) => `dropped ${countNoun(n, "noise line")}`
1059
1048
  });
1060
1049
  var claudeCliFilter = makeAiCliFilter({
1061
1050
  name: "claude-cli",
@@ -1065,14 +1054,14 @@ var claudeCliFilter = makeAiCliFilter({
1065
1054
  {
1066
1055
  re: _CLAUDE_CLI_TOOL_LOG_RE,
1067
1056
  position: "note",
1068
- note: (n) => `collapsed ${n} tool-call log line(s)`
1057
+ note: (n) => `collapsed ${countNoun(n, "tool-call log line")}`
1069
1058
  }
1070
1059
  ],
1071
1060
  keepLastRules: [
1072
1061
  { re: _CLAUDE_CLI_STATS_RE, note: (v) => `stats: ${v}` },
1073
1062
  { re: _CLAUDE_CLI_CONTEXT_RE, note: (v) => `context: ${v}` }
1074
1063
  ],
1075
- droppedNoiseNote: (n) => `dropped ${n} noise line(s)`,
1064
+ droppedNoiseNote: (n) => `dropped ${countNoun(n, "noise line")}`,
1076
1065
  // Exact 'claude' stem only; skip management subcommands (install, update, etc.)
1077
1066
  customMatches: (argv) => {
1078
1067
  if (!argv.length) return false;
@@ -1085,7 +1074,7 @@ var cursorFilter = makeAiCliFilter({
1085
1074
  name: "cursor",
1086
1075
  binaries: ["cursor"],
1087
1076
  dropRules: [_CURSOR_BANNER_RE, _CURSOR_STARTUP_RE, _CURSOR_TELEMETRY_RE],
1088
- droppedNoiseNote: (n) => `dropped ${n} startup/telemetry noise line(s)`
1077
+ droppedNoiseNote: (n) => `dropped ${countNoun(n, "startup/telemetry noise line")}`
1089
1078
  });
1090
1079
  var windsurfFilter = makeAiCliFilter({
1091
1080
  name: "windsurf",
@@ -1103,11 +1092,11 @@ var windsurfFilter = makeAiCliFilter({
1103
1092
  {
1104
1093
  re: _WINDSURF_CASCADE_TOOL_RE,
1105
1094
  position: "note",
1106
- note: (n) => `collapsed ${n} Cascade tool-call line(s); disable via TOKEN_GOAT_BASH_COMPRESS for full output`
1095
+ note: (n) => `collapsed ${countNoun(n, "Cascade tool-call line")}; disable via TOKEN_GOAT_BASH_COMPRESS for full output`
1107
1096
  }
1108
1097
  ],
1109
1098
  keepLastRules: [{ re: _WINDSURF_CONTEXT_RE, note: (v) => `context: ${v}` }],
1110
- droppedNoiseNote: (n) => `dropped ${n} startup/activation noise line(s)`
1099
+ droppedNoiseNote: (n) => `dropped ${countNoun(n, "startup/activation noise line")}`
1111
1100
  });
1112
1101
  var openCodeFilter = makeAiCliFilter({
1113
1102
  name: "opencode",
@@ -1118,7 +1107,7 @@ var openCodeFilter = makeAiCliFilter({
1118
1107
  // Tool calls and tool results share a single counter.
1119
1108
  res: [_OPENCODE_TOOL_CALL_RE, _OPENCODE_TOOL_RESULT_RE],
1120
1109
  position: "append",
1121
- note: (n) => `[token-goat: ${n} tool call/result line(s) collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
1110
+ note: (n) => `[token-goat: ${countNoun(n, "tool call/result line")} collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
1122
1111
  }
1123
1112
  ],
1124
1113
  keepLastRules: [
@@ -1126,7 +1115,7 @@ var openCodeFilter = makeAiCliFilter({
1126
1115
  { re: _OPENCODE_MODEL_RE_KL, note: (v) => `model: ${v}` },
1127
1116
  { re: _OPENCODE_CONTEXT_RE, note: (v) => `context: ${v}` }
1128
1117
  ],
1129
- droppedNoiseNote: (n) => `dropped ${n} noise line(s)`
1118
+ droppedNoiseNote: (n) => `dropped ${countNoun(n, "noise line")}`
1130
1119
  });
1131
1120
  var continueFilter = makeAiCliFilter({
1132
1121
  name: "continue",
@@ -1138,13 +1127,13 @@ var continueFilter = makeAiCliFilter({
1138
1127
  position: "append",
1139
1128
  keepLast: true,
1140
1129
  note: (n, last) => {
1141
- const summary = last ?? `${n} indexing progress line(s)`;
1142
- return `[token-goat: ${n} indexing progress line(s) collapsed; last: ${summary}; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`;
1130
+ const summary = last ?? `${countNoun(n, "indexing progress line")}`;
1131
+ return `[token-goat: ${countNoun(n, "indexing progress line")} collapsed; last: ${summary}; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`;
1143
1132
  }
1144
1133
  }
1145
1134
  ],
1146
1135
  keepLastRules: [{ re: _CONTINUE_TOKENS_RE, note: (v) => `tokens: ${v}` }],
1147
- droppedNoiseNote: (n) => `dropped ${n} noise line(s)`
1136
+ droppedNoiseNote: (n) => `dropped ${countNoun(n, "noise line")}`
1148
1137
  });
1149
1138
  var clineFilter = makeAiCliFilter({
1150
1139
  name: "cline",
@@ -1155,7 +1144,7 @@ var clineFilter = makeAiCliFilter({
1155
1144
  {
1156
1145
  re: _CLINE_FILE_READ_RE,
1157
1146
  position: "append",
1158
- note: (n) => `[token-goat: ${n} file-read progress line(s) collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
1147
+ note: (n) => `[token-goat: ${countNoun(n, "file-read progress line")} collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
1159
1148
  }
1160
1149
  ],
1161
1150
  keepLastRules: [
@@ -1163,7 +1152,7 @@ var clineFilter = makeAiCliFilter({
1163
1152
  { re: _CLINE_COST_RE, note: (v) => `cost: ${v}` },
1164
1153
  { re: _CLINE_CONTEXT_RE, note: (v) => `context: ${v}` }
1165
1154
  ],
1166
- droppedNoiseNote: (n) => `dropped ${n} noise line(s)`
1155
+ droppedNoiseNote: (n) => `dropped ${countNoun(n, "noise line")}`
1167
1156
  });
1168
1157
  var CodexExecFilter = class extends ToolFilter {
1169
1158
  name = "codex-exec";
@@ -1227,7 +1216,7 @@ var CodexExecFilter = class extends ToolFilter {
1227
1216
  droppedTurns = labelIdxs.length - 1;
1228
1217
  answerLines = _trimBlankEdges(lines.slice(lastCodexIdx + 1, scanEnd));
1229
1218
  }
1230
- const dropped = droppedTurns > 0 ? `, ${droppedTurns} earlier turn(s) dropped` : "";
1219
+ const dropped = droppedTurns > 0 ? `, ${countNoun(droppedTurns, "earlier turn")} dropped` : "";
1231
1220
  const summary = `[codex: model=${model}, tokens=${tokensCount}${dropped}]`;
1232
1221
  return this.finalize([summary, ...answerLines]);
1233
1222
  }
@@ -1299,10 +1288,10 @@ var GradleFilter = class extends ToolFilter {
1299
1288
  const posArgs = positionalArgs(argv.slice(1));
1300
1289
  const sub = posArgs[0]?.toLowerCase() ?? "";
1301
1290
  if (sub === "dependencies" || sub === "deps") {
1302
- return headTailCompress(merged.split("\n"), 10, 10, "lines");
1291
+ return headTailCompress(merged.split("\n"), 10, 10, "line");
1303
1292
  }
1304
1293
  if (sub === "tasks") {
1305
- return headTailCompress(merged.split("\n"), 20, 5, "lines");
1294
+ return headTailCompress(merged.split("\n"), 20, 5, "line");
1306
1295
  }
1307
1296
  return this._compressBuild(merged);
1308
1297
  }
@@ -1321,7 +1310,7 @@ var GradleFilter = class extends ToolFilter {
1321
1310
  let droppedTestSkipped = 0;
1322
1311
  const flushSuppressedFrames = () => {
1323
1312
  if (suppressedInTrace > 0) {
1324
- kept.push(`[token-goat: \u2026and ${suppressedInTrace} more stack frames]`);
1313
+ kept.push(`[token-goat: \u2026and ${countNoun(suppressedInTrace, "more stack frame")}]`);
1325
1314
  suppressedInTrace = 0;
1326
1315
  }
1327
1316
  };
@@ -1418,15 +1407,15 @@ var MavenFilter = class extends ToolFilter {
1418
1407
  const posArgs = positionalArgs(argv.slice(1));
1419
1408
  const sub = posArgs[0] ?? "";
1420
1409
  if (sub === "dependency:tree") {
1421
- return headTailCompress(lines, 10, 10, "lines");
1410
+ return headTailCompress(lines, 10, 10, "line");
1422
1411
  }
1423
1412
  if (sub === "install") {
1424
- return headTailCompress(lines, 5, 30, "lines");
1413
+ return headTailCompress(lines, 5, 30, "line");
1425
1414
  }
1426
1415
  if (sub === "test" || sub === "verify" || sub === "package") {
1427
1416
  return this._compressTest(lines);
1428
1417
  }
1429
- return headTailCompress(lines, 10, 10, "lines");
1418
+ return headTailCompress(lines, 10, 10, "line");
1430
1419
  }
1431
1420
  _compressTest(lines) {
1432
1421
  const kept = [];
@@ -2000,7 +1989,7 @@ var LernaFilter = class extends ToolFilter {
2000
1989
  kept.push(line);
2001
1990
  }
2002
1991
  const out = [...ranSample];
2003
- if (ranExtra) out.push(`[token-goat: \u2026and ${ranExtra} more 'info run Ran' lines]`);
1992
+ if (ranExtra) out.push(`[token-goat: \u2026and ${countNoun(ranExtra, "more 'info run Ran' line")}]`);
2004
1993
  out.push(...kept);
2005
1994
  return this.finalize(out);
2006
1995
  }
@@ -2063,7 +2052,7 @@ var TurboFilter = class extends ToolFilter {
2063
2052
  kept.push(line);
2064
2053
  }
2065
2054
  const notes = [];
2066
- maybeNote(notes, cacheHitTasks.size, `collapsed ${cacheHitTasks.size} cache-hit task(s)`);
2055
+ maybeNote(notes, cacheHitTasks.size, `collapsed ${countNoun(cacheHitTasks.size, "cache-hit task")}`);
2067
2056
  this.emitNotes(kept, notes);
2068
2057
  return this.finalize(kept);
2069
2058
  }
@@ -2668,7 +2657,7 @@ ${stdout.replace(/\s+$/, "")}` : stderr : stdout;
2668
2657
  maybeNote(notes, droppedProgress, `dropped ${droppedProgress} Checking/Downloading/progress lines`);
2669
2658
  const failed = exitCode !== 0 || kept.some((l) => CARGO_ERROR_HEADER_RE.test(l));
2670
2659
  const collapsedWarnings = failed ? collapseCargoWarnings(kept) : 0;
2671
- maybeNote(notes, collapsedWarnings, `collapsed ${collapsedWarnings} warning block(s) because the build failed`);
2660
+ maybeNote(notes, collapsedWarnings, `collapsed ${countNoun(collapsedWarnings, "warning block")} because the build failed`);
2672
2661
  this.emitNotes(kept, notes);
2673
2662
  return this.finalize(kept);
2674
2663
  }
@@ -3048,11 +3037,7 @@ var GhRunLogFilter = class extends ToolFilter {
3048
3037
  const name = pathName(first).toLowerCase();
3049
3038
  if (stem !== "gh" && name !== "gh") return false;
3050
3039
  const positionals = positionalArgs(argv.slice(1));
3051
- return positionals.length >= 2 && positionals[0] === "run" && positionals[1] === "view" && // `--log-failed` is the CI-triage spelling and emits the identical
3052
- // job/step/timestamp column format, so it needs this filter too. A plain
3053
- // `argv.includes('--log')` is a whole-token test and never matches it,
3054
- // which sent it to GhFilter -- whose pass-step regex anchors at line
3055
- // start and so matches nothing while the column prefix is still present.
3040
+ return positionals.length >= 2 && positionals[0] === "run" && positionals[1] === "view" && // `--log-failed` is the CI-triage spelling and emits the identical job/step/timestamp column format, so it needs this filter too. A plain `argv.includes('--log')` is a whole-token test and never matches it, which sent it to GhFilter -- whose pass-step regex anchors at line start and so matches nothing while the column prefix is still present.
3056
3041
  (argv.includes("--log") || argv.includes("--log-failed"));
3057
3042
  }
3058
3043
  compress(stdout, stderr, _exitCode, _argv) {
@@ -3122,13 +3107,13 @@ var GhRunLogFilter = class extends ToolFilter {
3122
3107
  }
3123
3108
  flushGroup();
3124
3109
  if (setupActions.length) {
3125
- kept.push(`[token-goat: Setup: ${setupActions.length} action(s) collapsed]`);
3110
+ kept.push(`[token-goat: Setup: ${countNoun(setupActions.length, "action")} collapsed]`);
3126
3111
  }
3127
3112
  const notes = [];
3128
3113
  maybeNote(notes, droppedBoilerplate, `dropped ${droppedBoilerplate} boilerplate lines`);
3129
3114
  maybeNote(notes, droppedCommands, `dropped ${droppedCommands} ##[command] echo lines`);
3130
3115
  maybeNote(notes, droppedCleanup, `dropped ${droppedCleanup} cleanup lines`);
3131
- maybeNote(notes, collapsedGroups, `collapsed ${collapsedGroups} log group(s)`);
3116
+ maybeNote(notes, collapsedGroups, `collapsed ${countNoun(collapsedGroups, "log group")}`);
3132
3117
  this.emitNotes(kept, notes);
3133
3118
  return this.finalize(kept);
3134
3119
  }
@@ -3247,7 +3232,7 @@ var PreCommitFilter = class extends ToolFilter {
3247
3232
  const status = m.groups?.["status"] ?? "";
3248
3233
  if (status === "Failed" || status === "Pre-commit hook failed") {
3249
3234
  if (passed || skipped) {
3250
- kept.push(`[token-goat: collapsed ${passed} Passed, ${skipped} Skipped hook(s)]`);
3235
+ kept.push(`[token-goat: collapsed ${passed} Passed, ${countNoun(skipped, "Skipped hook")}]`);
3251
3236
  passed = 0;
3252
3237
  skipped = 0;
3253
3238
  }
@@ -3275,7 +3260,7 @@ var PreCommitFilter = class extends ToolFilter {
3275
3260
  kept.push(line);
3276
3261
  }
3277
3262
  if (passed || skipped) {
3278
- kept.push(`[token-goat: collapsed ${passed} Passed, ${skipped} Skipped hook(s)]`);
3263
+ kept.push(`[token-goat: collapsed ${passed} Passed, ${countNoun(skipped, "Skipped hook")}]`);
3279
3264
  }
3280
3265
  if (infoDropped) {
3281
3266
  kept.push(`[token-goat: dropped ${infoDropped} pre-commit [INFO] env-setup lines]`);
@@ -3361,7 +3346,7 @@ var BanditFilter = class extends ToolFilter {
3361
3346
  }
3362
3347
  if (inIssue) flushIssue();
3363
3348
  const notes = [];
3364
- maybeNote(notes, lowDropped, `collapsed ${lowDropped} LOW severity issue block(s)`);
3349
+ maybeNote(notes, lowDropped, `collapsed ${countNoun(lowDropped, "LOW severity issue block")}`);
3365
3350
  this.emitNotes(kept, notes);
3366
3351
  return this.finalize(kept);
3367
3352
  }
@@ -3492,7 +3477,7 @@ var SnykFilter = class extends ToolFilter {
3492
3477
  if (_SNYK_LICENSE_RE.test(line) && !_SNYK_TREE_LINE_RE.test(line)) {
3493
3478
  if (inMoreAbout) {
3494
3479
  if (moreAboutDropped) {
3495
- kept.push(`[token-goat: collapsed ${moreAboutDropped} 'More about' URL line(s)]`);
3480
+ kept.push(`[token-goat: collapsed ${countNoun(moreAboutDropped, "'More about' URL line")}]`);
3496
3481
  moreAboutDropped = 0;
3497
3482
  }
3498
3483
  inMoreAbout = false;
@@ -3509,7 +3494,7 @@ var SnykFilter = class extends ToolFilter {
3509
3494
  if (line.trim() && !line.trim().startsWith("http")) {
3510
3495
  inMoreAbout = false;
3511
3496
  if (moreAboutDropped) {
3512
- kept.push(`[token-goat: collapsed ${moreAboutDropped} 'More about' URL line(s)]`);
3497
+ kept.push(`[token-goat: collapsed ${countNoun(moreAboutDropped, "'More about' URL line")}]`);
3513
3498
  moreAboutDropped = 0;
3514
3499
  }
3515
3500
  } else {
@@ -3539,14 +3524,14 @@ var SnykFilter = class extends ToolFilter {
3539
3524
  treeHidden = 0;
3540
3525
  }
3541
3526
  if (moreAboutDropped) {
3542
- kept.push(`[token-goat: collapsed ${moreAboutDropped} 'More about' URL line(s)]`);
3527
+ kept.push(`[token-goat: collapsed ${countNoun(moreAboutDropped, "'More about' URL line")}]`);
3543
3528
  moreAboutDropped = 0;
3544
3529
  }
3545
3530
  kept.push(line);
3546
3531
  }
3547
3532
  if (treeHidden) kept.push(`[token-goat: +${treeHidden} dependency tree lines collapsed]`);
3548
3533
  if (moreAboutDropped) {
3549
- kept.push(`[token-goat: collapsed ${moreAboutDropped} 'More about' URL line(s)]`);
3534
+ kept.push(`[token-goat: collapsed ${countNoun(moreAboutDropped, "'More about' URL line")}]`);
3550
3535
  }
3551
3536
  return this.finalize(kept);
3552
3537
  }
@@ -3582,7 +3567,7 @@ var SemgrepFilter = class extends ToolFilter {
3582
3567
  }
3583
3568
  }
3584
3569
  if (localDropped) {
3585
- blockOut.push(` [token-goat: collapsed ${localDropped} Details/annotation URL line(s)]`);
3570
+ blockOut.push(` [token-goat: collapsed ${countNoun(localDropped, "Details/annotation URL line")}]`);
3586
3571
  }
3587
3572
  kept.push(...blockOut);
3588
3573
  ruleCounts.set(currentRule, count + 1);
@@ -3607,7 +3592,7 @@ var SemgrepFilter = class extends ToolFilter {
3607
3592
  currentBlock = [];
3608
3593
  for (const [ruleId, supCnt] of [...ruleSuppressed.entries()].sort()) {
3609
3594
  kept.push(
3610
- `[token-goat: ${ruleId} \u2014 ${supCnt} additional match(es) collapsed (kept first ${this._maxPerRule})]`
3595
+ `[token-goat: ${ruleId} \u2014 ${countNoun(supCnt, "additional match", "additional matches")} collapsed (kept first ${this._maxPerRule})]`
3611
3596
  );
3612
3597
  }
3613
3598
  ruleSuppressed.clear();
@@ -3626,7 +3611,7 @@ var SemgrepFilter = class extends ToolFilter {
3626
3611
  flushBlock();
3627
3612
  for (const [ruleId, supCnt] of [...ruleSuppressed.entries()].sort()) {
3628
3613
  kept.push(
3629
- `[token-goat: ${ruleId} \u2014 ${supCnt} additional match(es) collapsed (kept first ${this._maxPerRule})]`
3614
+ `[token-goat: ${ruleId} \u2014 ${countNoun(supCnt, "additional match", "additional matches")} collapsed (kept first ${this._maxPerRule})]`
3630
3615
  );
3631
3616
  }
3632
3617
  return this.finalize(kept);
@@ -3807,10 +3792,10 @@ ${stderr.replace(/\s+$/, "")}` : stderr;
3807
3792
  kept.push(line);
3808
3793
  }
3809
3794
  const notes = [];
3810
- maybeNote(notes, uploadCount, `uploaded ${uploadCount} file(s)`);
3811
- maybeNote(notes, downloadCount, `downloaded ${downloadCount} file(s)`);
3812
- maybeNote(notes, failedCount, `${failedCount} transfer(s) failed`);
3813
- maybeNote(notes, progressDropped, `dropped ${progressDropped} progress line(s)`);
3795
+ maybeNote(notes, uploadCount, `uploaded ${countNoun(uploadCount, "file")}`);
3796
+ maybeNote(notes, downloadCount, `downloaded ${countNoun(downloadCount, "file")}`);
3797
+ maybeNote(notes, failedCount, `${countNoun(failedCount, "transfer")} failed`);
3798
+ maybeNote(notes, progressDropped, `dropped ${countNoun(progressDropped, "progress line")}`);
3814
3799
  this.emitNotes(kept, notes);
3815
3800
  return this.finalize(kept);
3816
3801
  }
@@ -3848,7 +3833,7 @@ ${stderr.replace(/\s+$/, "")}` : stderr;
3848
3833
  inProgressRun.delete(resourceId);
3849
3834
  if (prevCount) {
3850
3835
  keptEvents.push({
3851
- __token_goat__: `${prevCount} repeated ${prevStatus} event(s) for ${resourceId} collapsed`
3836
+ __token_goat__: `${prevCount} repeated ${prevStatus} ${prevCount === 1 ? "event" : "events"} for ${resourceId} collapsed`
3852
3837
  });
3853
3838
  }
3854
3839
  keptEvents.push(event);
@@ -3858,7 +3843,7 @@ ${stderr.replace(/\s+$/, "")}` : stderr;
3858
3843
  if (prevCount) {
3859
3844
  const prevStatus = lastResourceStatus.get(resourceId) ?? "IN_PROGRESS";
3860
3845
  keptEvents.push({
3861
- __token_goat__: `${prevCount} repeated ${prevStatus} event(s) for ${resourceId} collapsed`
3846
+ __token_goat__: `${prevCount} repeated ${prevStatus} ${prevCount === 1 ? "event" : "events"} for ${resourceId} collapsed`
3862
3847
  });
3863
3848
  }
3864
3849
  keptEvents.push(event);
@@ -3869,7 +3854,7 @@ ${stderr.replace(/\s+$/, "")}` : stderr;
3869
3854
  if (count) {
3870
3855
  const prevStatus = lastResourceStatus.get(resourceId) ?? "IN_PROGRESS";
3871
3856
  keptEvents.push({
3872
- __token_goat__: `${count} repeated ${prevStatus} event(s) for ${resourceId} collapsed`
3857
+ __token_goat__: `${count} repeated ${prevStatus} ${count === 1 ? "event" : "events"} for ${resourceId} collapsed`
3873
3858
  });
3874
3859
  }
3875
3860
  }
@@ -3920,8 +3905,8 @@ ${stderr.replace(/\s+$/, "")}` : stderr;
3920
3905
  }
3921
3906
  kept = this._maybeCollapseStructured(kept);
3922
3907
  const notes = [];
3923
- maybeNote(notes, spinnersDropped, `dropped ${spinnersDropped} spinner line(s)`);
3924
- maybeNote(notes, apiEnableDropped, `collapsed ${apiEnableDropped} API enablement line(s)`);
3908
+ maybeNote(notes, spinnersDropped, `dropped ${countNoun(spinnersDropped, "spinner line")}`);
3909
+ maybeNote(notes, apiEnableDropped, `collapsed ${countNoun(apiEnableDropped, "API enablement line")}`);
3925
3910
  this.emitNotes(kept, notes);
3926
3911
  return this.finalize(kept);
3927
3912
  }
@@ -3994,7 +3979,7 @@ ${stderr.replace(/\s+$/, "")}` : stderr;
3994
3979
  }
3995
3980
  if (inProgressRun && lastProgressStatus) kept.push(lastProgressStatus);
3996
3981
  const notes = [];
3997
- maybeNote(notes, previewDropped, `collapsed ${previewDropped} preview warning(s)`);
3982
+ maybeNote(notes, previewDropped, `collapsed ${countNoun(previewDropped, "preview warning")}`);
3998
3983
  this.emitNotes(kept, notes);
3999
3984
  return this.finalize(kept);
4000
3985
  }
@@ -4036,7 +4021,7 @@ var TerraformFilter = class extends ToolFilter {
4036
4021
  const filtered = lines.filter((ln) => !_TF_REFRESH_RE.test(ln));
4037
4022
  if (filtered.length > 30) {
4038
4023
  const nonEmpty = filtered.filter((ln) => ln.trim());
4039
- text = headTailCompress(nonEmpty, 10, 20, "lines");
4024
+ text = headTailCompress(nonEmpty, 10, 20, "line");
4040
4025
  } else if (filtered.length < lines.length) {
4041
4026
  text = filtered.join("\n");
4042
4027
  }
@@ -4143,7 +4128,7 @@ ${stderr.replace(/\s+$/, "")}` : stderr;
4143
4128
  }
4144
4129
  const notes = [];
4145
4130
  maybeNote(notes, droppedRefresh, `dropped ${droppedRefresh} terraform refresh/read lines`);
4146
- maybeNote(notes, droppedNoChange, `collapsed ${droppedNoChange} unchanged/read-only block(s)`);
4131
+ maybeNote(notes, droppedNoChange, `collapsed ${countNoun(droppedNoChange, "unchanged/read-only block")}`);
4147
4132
  maybeNote(notes, droppedKaa, `collapsed ${droppedKaa} (known after apply) attribute lines`);
4148
4133
  this.emitNotes(finalKept, notes);
4149
4134
  return this.finalize(finalKept);
@@ -4185,7 +4170,7 @@ ${stderr.replace(/\s+$/, "")}` : stderr;
4185
4170
  for (const line of stillLast.values()) kept.push(line);
4186
4171
  const notes = [];
4187
4172
  maybeNote(notes, droppedRefresh, `dropped ${droppedRefresh} terraform refresh/read lines`);
4188
- maybeNote(notes, stillDropped, `collapsed ${stillDropped} Still creating/modifying line(s)`);
4173
+ maybeNote(notes, stillDropped, `collapsed ${countNoun(stillDropped, "Still creating/modifying line")}`);
4189
4174
  this.emitNotes(kept, notes);
4190
4175
  return this.finalize(kept);
4191
4176
  }
@@ -4205,7 +4190,7 @@ ${stderr.replace(/\s+$/, "")}` : stderr;
4205
4190
  const notes = [];
4206
4191
  maybeNote(notes, providerCollapsed, `collapsed ${providerCollapsed} provider install/find lines`);
4207
4192
  if (nonEmpty.length > 12) {
4208
- let compressed = headTailCompress(nonEmpty, 5, 5, "lines");
4193
+ let compressed = headTailCompress(nonEmpty, 5, 5, "line");
4209
4194
  if (notes.length) compressed = `${compressed.replace(/\s+$/, "")}
4210
4195
  [token-goat: ${notes.join("; ")}]`;
4211
4196
  return compressed;
@@ -4257,7 +4242,7 @@ ${stderr.replace(/\s+$/, "")}` : stderr;
4257
4242
  if (collapsedTotal === 0) {
4258
4243
  const nonEmpty = kept.filter((ln) => ln.trim());
4259
4244
  if (nonEmpty.length > 30) {
4260
- return headTailCompress(nonEmpty, 20, 10, "lines");
4245
+ return headTailCompress(nonEmpty, 20, 10, "line");
4261
4246
  }
4262
4247
  }
4263
4248
  const notes = [];
@@ -4438,7 +4423,7 @@ var AnsibleFilter = class extends ToolFilter {
4438
4423
  const merged = this.combineOutput(stdout, stderr);
4439
4424
  const nonEmpty = merged.split("\n").filter((ln) => ln.trim());
4440
4425
  if (nonEmpty.length > 10) {
4441
- return headTailCompress(nonEmpty, 5, 5, "galaxy lines");
4426
+ return headTailCompress(nonEmpty, 5, 5, "galaxy line");
4442
4427
  }
4443
4428
  return nonEmpty.join("\n");
4444
4429
  }
@@ -4596,13 +4581,13 @@ var VaultFilter = class extends ToolFilter {
4596
4581
  } else {
4597
4582
  kept.push(...listItems.slice(0, 5));
4598
4583
  kept.push(
4599
- `[token-goat: ${listItems.length - 5} more secret path(s) omitted; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`
4584
+ `[token-goat: ${countNoun(listItems.length - 5, "more secret path")} omitted; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`
4600
4585
  );
4601
4586
  }
4602
4587
  }
4603
4588
  const notes = [];
4604
- maybeNote(notes, metaCount, `collapsed ${metaCount} Vault lease/token metadata line(s)`);
4605
- maybeNote(notes, dividerCount, `dropped ${dividerCount} table divider line(s)`);
4589
+ maybeNote(notes, metaCount, `collapsed ${countNoun(metaCount, "Vault lease/token metadata line")}`);
4590
+ maybeNote(notes, dividerCount, `dropped ${countNoun(dividerCount, "table divider line")}`);
4606
4591
  this.emitNotes(kept, notes);
4607
4592
  return this.finalize(kept);
4608
4593
  }
@@ -4656,15 +4641,15 @@ var PackerFilter = class extends ToolFilter {
4656
4641
  const out = [];
4657
4642
  if (waitingCount) {
4658
4643
  out.push(
4659
- `[token-goat: ${waitingCount} SSH/WinRM connection-wait poll line(s) collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
4644
+ `[token-goat: ${countNoun(waitingCount, "SSH/WinRM connection-wait poll line")} collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
4660
4645
  );
4661
4646
  }
4662
4647
  if (provisionerCount) {
4663
- out.push(`[token-goat: collapsed ${provisionerCount} provisioner step announcement(s)]`);
4648
+ out.push(`[token-goat: collapsed ${countNoun(provisionerCount, "provisioner step announcement")}]`);
4664
4649
  }
4665
4650
  out.push(...kept);
4666
4651
  const notes = [];
4667
- maybeNote(notes, noiseCount, `dropped ${noiseCount} network/heartbeat/pause noise line(s)`);
4652
+ maybeNote(notes, noiseCount, `dropped ${countNoun(noiseCount, "network/heartbeat/pause noise line")}`);
4668
4653
  this.emitNotes(out, notes);
4669
4654
  return this.finalize(out);
4670
4655
  }
@@ -4735,18 +4720,18 @@ var NixFilter = class extends ToolFilter {
4735
4720
  const out = [];
4736
4721
  if (fetchCount) {
4737
4722
  out.push(
4738
- `[token-goat: fetched/substituted ${fetchCount} store path(s) from binary cache; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`
4723
+ `[token-goat: fetched/substituted ${countNoun(fetchCount, "store path")} from binary cache; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`
4739
4724
  );
4740
4725
  }
4741
4726
  if (buildCount) {
4742
- out.push(`[token-goat: built ${buildCount} Nix derivation(s)]`);
4727
+ out.push(`[token-goat: built ${countNoun(buildCount, "Nix derivation")}]`);
4743
4728
  }
4744
4729
  if (flakeUpdateCount) {
4745
- out.push(`[token-goat: collapsed ${flakeUpdateCount} flake lock update line(s)]`);
4730
+ out.push(`[token-goat: collapsed ${countNoun(flakeUpdateCount, "flake lock update line")}]`);
4746
4731
  }
4747
4732
  out.push(...kept);
4748
4733
  const notes = [];
4749
- maybeNote(notes, droppedNoise, `dropped ${droppedNoise} Nix scheduler/sandbox noise line(s)`);
4734
+ maybeNote(notes, droppedNoise, `dropped ${countNoun(droppedNoise, "Nix scheduler/sandbox noise line")}`);
4750
4735
  this.emitNotes(out, notes);
4751
4736
  return this.finalize(out);
4752
4737
  }
@@ -4801,19 +4786,19 @@ var WranglerFilter = class extends ToolFilter {
4801
4786
  const out = [];
4802
4787
  if (uploadCount) {
4803
4788
  out.push(
4804
- `[token-goat: ${uploadCount} asset upload line(s) collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`
4789
+ `[token-goat: ${countNoun(uploadCount, "asset upload line")} collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`
4805
4790
  );
4806
4791
  }
4807
4792
  if (skipCount) {
4808
- out.push(`[token-goat: ${skipCount} asset-skip line(s) collapsed]`);
4793
+ out.push(`[token-goat: ${countNoun(skipCount, "asset-skip line")} collapsed]`);
4809
4794
  }
4810
4795
  if (bulkCount) {
4811
- out.push(`[token-goat: ${bulkCount} bulk-operation progress line(s) collapsed]`);
4796
+ out.push(`[token-goat: ${countNoun(bulkCount, "bulk-operation progress line")} collapsed]`);
4812
4797
  }
4813
4798
  out.push(...kept);
4814
4799
  const notes = [];
4815
- maybeNote(notes, droppedBuild, `dropped ${droppedBuild} build-step noise line(s)`);
4816
- maybeNote(notes, droppedDev, `dropped ${droppedDev} dev-mode noise line(s)`);
4800
+ maybeNote(notes, droppedBuild, `dropped ${countNoun(droppedBuild, "build-step noise line")}`);
4801
+ maybeNote(notes, droppedDev, `dropped ${countNoun(droppedDev, "dev-mode noise line")}`);
4817
4802
  this.emitNotes(out, notes);
4818
4803
  return this.finalize(out);
4819
4804
  }
@@ -4870,16 +4855,16 @@ var HardhatFilter = class extends ToolFilter {
4870
4855
  const out = [];
4871
4856
  if (compilingCount) {
4872
4857
  out.push(
4873
- `[token-goat: collapsed ${compilingCount} Solidity compilation step line(s); disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
4858
+ `[token-goat: collapsed ${countNoun(compilingCount, "Solidity compilation step line")}; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
4874
4859
  );
4875
4860
  }
4876
4861
  if (solcTimingCount) {
4877
- out.push(`[token-goat: collapsed ${solcTimingCount} Solc per-version timing line(s)]`);
4862
+ out.push(`[token-goat: collapsed ${countNoun(solcTimingCount, "Solc per-version timing line")}]`);
4878
4863
  }
4879
4864
  out.push(...kept);
4880
4865
  const notes = [];
4881
- maybeNote(notes, passCount, `collapsed ${passCount} passing test line(s)`);
4882
- maybeNote(notes, txNoiseCount, `dropped ${txNoiseCount} transaction receipt noise line(s)`);
4866
+ maybeNote(notes, passCount, `collapsed ${countNoun(passCount, "passing test line")}`);
4867
+ maybeNote(notes, txNoiseCount, `dropped ${countNoun(txNoiseCount, "transaction receipt noise line")}`);
4883
4868
  this.emitNotes(out, notes);
4884
4869
  return this.finalize(out);
4885
4870
  }
@@ -4930,13 +4915,13 @@ var ServerlessFilter = class extends ToolFilter {
4930
4915
  const out = [];
4931
4916
  if (stepCount) {
4932
4917
  out.push(
4933
- `[token-goat: collapsed ${stepCount} Serverless deploy step line(s); disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
4918
+ `[token-goat: collapsed ${countNoun(stepCount, "Serverless deploy step line")}; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
4934
4919
  );
4935
4920
  }
4936
4921
  out.push(...kept);
4937
4922
  const notes = [];
4938
- maybeNote(notes, inProgressCount, `dropped ${inProgressCount} CF _IN_PROGRESS event line(s)`);
4939
- maybeNote(notes, dotCount, `dropped ${dotCount} polling dot line(s)`);
4923
+ maybeNote(notes, inProgressCount, `dropped ${countNoun(inProgressCount, "CF _IN_PROGRESS event line")}`);
4924
+ maybeNote(notes, dotCount, `dropped ${countNoun(dotCount, "polling dot line")}`);
4940
4925
  this.emitNotes(out, notes);
4941
4926
  return this.finalize(out);
4942
4927
  }
@@ -5003,15 +4988,15 @@ var FlyFilter = class extends ToolFilter {
5003
4988
  const out = [];
5004
4989
  if (buildStepCount) {
5005
4990
  out.push(
5006
- `[token-goat: ${buildStepCount} Docker build step line(s) collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
4991
+ `[token-goat: ${countNoun(buildStepCount, "Docker build step line")} collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
5007
4992
  );
5008
4993
  }
5009
4994
  if (machineWaitCount) {
5010
- out.push(`[token-goat: ${machineWaitCount} per-machine wait line(s) collapsed]`);
4995
+ out.push(`[token-goat: ${countNoun(machineWaitCount, "per-machine wait line")} collapsed]`);
5011
4996
  }
5012
4997
  out.push(...kept);
5013
4998
  const notes = [];
5014
- maybeNote(notes, droppedNoise, `dropped ${droppedNoise} DNS/polling noise line(s)`);
4999
+ maybeNote(notes, droppedNoise, `dropped ${countNoun(droppedNoise, "DNS/polling noise line")}`);
5015
5000
  this.emitNotes(out, notes);
5016
5001
  return this.finalize(out);
5017
5002
  }
@@ -5078,13 +5063,13 @@ var ForgeFilter = class extends ToolFilter {
5078
5063
  const out = [];
5079
5064
  if (compilingCount) {
5080
5065
  out.push(
5081
- `[token-goat: ${compilingCount} Solidity compilation step line(s) collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
5066
+ `[token-goat: ${countNoun(compilingCount, "Solidity compilation step line")} collapsed; disable via TOKEN_GOAT_BASH_COMPRESS for full output]`
5082
5067
  );
5083
5068
  }
5084
5069
  out.push(...kept);
5085
5070
  const notes = [];
5086
- maybeNote(notes, passCount, `collapsed ${passCount} passing test line(s)`);
5087
- maybeNote(notes, droppedGasSep, `dropped ${droppedGasSep} gas-report table separator row(s)`);
5071
+ maybeNote(notes, passCount, `collapsed ${countNoun(passCount, "passing test line")}`);
5072
+ maybeNote(notes, droppedGasSep, `dropped ${countNoun(droppedGasSep, "gas-report table separator row")}`);
5088
5073
  this.emitNotes(out, notes);
5089
5074
  return this.finalize(out);
5090
5075
  }
@@ -5132,7 +5117,7 @@ function collapseDiffBlocksToCap(outBlocks, maxLines, shape) {
5132
5117
  const hunkCount = lines.filter((ln) => shape.isHunkHeader(ln)).length;
5133
5118
  const added = lines.filter(isDiffAdd).length;
5134
5119
  const removed = lines.filter(isDiffRemove).length;
5135
- const summary = `[token-goat: ${hunkCount} hunk(s), +${added} -${removed} lines collapsed to fit the line cap]`;
5120
+ const summary = `[token-goat: ${countNoun(hunkCount, "hunk")}, +${added} -${removed} lines collapsed to fit the line cap]`;
5136
5121
  return { headerLines, summary, size: headerLines.length + 1 };
5137
5122
  };
5138
5123
  const collapsedSizes = outBlocks.map((block, i) => isFileBlock[i] ? collapsedFormOf(block).size : 0);
@@ -5243,16 +5228,16 @@ var GrepFilter = class extends ToolFilter {
5243
5228
  }
5244
5229
  const totalMatches = [...fileCounts.values()].reduce((a, b) => a + b, 0) + unattributed;
5245
5230
  const numFiles = fileCounts.size;
5246
- const outLines = ctx.inputTruncated ? [`grep: at least ${totalMatches} matches across ${numFiles} file(s) (counted over a truncated input; per-file counts below are lower bounds)`] : [`grep: ${totalMatches} matches across ${numFiles} file(s)`];
5231
+ const outLines = ctx.inputTruncated ? [`grep: at least ${countNoun(totalMatches, "match", "matches")} across ${countNoun(numFiles, "file")} (counted over a truncated input; per-file counts below are lower bounds)`] : [`grep: ${countNoun(totalMatches, "match", "matches")} across ${countNoun(numFiles, "file")}`];
5247
5232
  const sorted = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]);
5248
5233
  const shown = sorted.slice(0, _GREP_MAX_FILE_LINES);
5249
5234
  for (const [fname, count] of shown) {
5250
- outLines.push(` ${fname}: ${count} match(es)`);
5235
+ outLines.push(` ${fname}: ${countNoun(count, "match", "matches")}`);
5251
5236
  }
5252
5237
  if (sorted.length > _GREP_MAX_FILE_LINES) {
5253
5238
  const remaining = sorted.length - _GREP_MAX_FILE_LINES;
5254
5239
  outLines.push(
5255
- ` [token-goat: +${remaining} more file(s) elided; use a more specific pattern or --include=<glob> to narrow]`
5240
+ ` [token-goat: +${countNoun(remaining, "more file")} elided; use a more specific pattern or --include=<glob> to narrow]`
5256
5241
  );
5257
5242
  }
5258
5243
  if (unattributed) {
@@ -5285,10 +5270,7 @@ var RgFilter = class _RgFilter extends ToolFilter {
5285
5270
  }
5286
5271
  return false;
5287
5272
  }
5288
- // RgFilter only handles context-block output (-A/-B/-C/--context); a plain
5289
- // grep/rg with no context flags falls through to GrepFilter's per-file
5290
- // match-count summarizer, which produces dramatically smaller output.
5291
- // Both line regexes hard-code ripgrep's default field separators (`:` after a match's line number, `-` after a context line's). `--field-match-separator=-` makes a real match read `12-text`, which is exactly the shape this filter drops, so a search that overrides either separator is released to the generic path rather than compressed against an assumption it broke.
5273
+ // RgFilter only handles context-block output (-A/-B/-C/--context); a plain grep/rg with no context flags falls through to GrepFilter's per-file match-count summarizer, which produces dramatically smaller output. Both line regexes hard-code ripgrep's default field separators (`:` after a match's line number, `-` after a context line's). `--field-match-separator=-` makes a real match read `12-text`, which is exactly the shape this filter drops, so a search that overrides either separator is released to the generic path rather than compressed against an assumption it broke.
5292
5274
  static _hasCustomFieldSeparator(argv) {
5293
5275
  const flags = ["--field-match-separator", "--field-context-separator"];
5294
5276
  return argv.some((a) => flags.some((f) => a === f || a.startsWith(f + "=")));
@@ -5311,7 +5293,7 @@ var RgFilter = class _RgFilter extends ToolFilter {
5311
5293
  const firstDropped = scored[_RG_TOP_GROUPS]?.score;
5312
5294
  const tied = firstDropped !== void 0 && firstDropped === lastKept;
5313
5295
  return joined + `
5314
- [token-goat: ${suppressed} more match groups suppressed${tied ? ", tied on match count with the ones kept and separated only by filename order" : ", each with fewer matches than those kept"}: rerun with -l for filenames only]`;
5296
+ [token-goat: ${countNoun(suppressed, "more match group")} suppressed${tied ? ", tied on match count with the ones kept and separated only by filename order" : ", each with fewer matches than those kept"}: rerun with -l for filenames only]`;
5315
5297
  }
5316
5298
  // Same per-line clip GrepFilter applies: every branch below can return match lines verbatim, so the cap is applied once here rather than at each of the five return sites.
5317
5299
  compress(stdout, stderr, exitCode, argv) {
@@ -5344,8 +5326,8 @@ var RgFilter = class _RgFilter extends ToolFilter {
5344
5326
  };
5345
5327
  var _LS_PASSTHROUGH = 25;
5346
5328
  var _LS_MAX_ENTRIES = 10;
5347
- var _LS_HIDDEN_MARKER = "[token-goat: {n} more entries \u2014 use eza --tree or ls | grep PATTERN to filter]";
5348
- var _LS_HIDDEN_MARKER_EXT = "[token-goat: {n} more entries \u2014 by type: {ext_summary}]";
5329
+ var _LS_HIDDEN_MARKER = "[token-goat: {n} \u2014 use eza --tree or ls | grep PATTERN to filter]";
5330
+ var _LS_HIDDEN_MARKER_EXT = "[token-goat: {n} \u2014 by type: {ext_summary}]";
5349
5331
  function _lsExtFromLine(line) {
5350
5332
  const stripped = line.trimEnd();
5351
5333
  if (stripped.endsWith("/")) return null;
@@ -5409,7 +5391,7 @@ var LsFilter = class _LsFilter extends ToolFilter {
5409
5391
  const hiddenCount = entryIdx.length - keptEntryIdx.size;
5410
5392
  const fileEntries = entryIdx.slice(_LS_MAX_ENTRIES).map((i) => lines[i]).filter((l) => !_DIR_EXE_DIR_ENTRY_RE.test(l));
5411
5393
  const extPart = _lsExtSummary(fileEntries);
5412
- const hiddenMarker = extPart ? _LS_HIDDEN_MARKER_EXT.replace("{n}", String(hiddenCount)).replace("{ext_summary}", () => extPart) : _LS_HIDDEN_MARKER.replace("{n}", String(hiddenCount));
5394
+ const hiddenMarker = extPart ? _LS_HIDDEN_MARKER_EXT.replace("{n}", () => countNoun(hiddenCount, "more entry", "more entries")).replace("{ext_summary}", () => extPart) : _LS_HIDDEN_MARKER.replace("{n}", () => countNoun(hiddenCount, "more entry", "more entries"));
5413
5395
  const out = [];
5414
5396
  let hiddenMarkerEmitted = false;
5415
5397
  for (let i = 0; i < lines.length; i++) {
@@ -5448,9 +5430,9 @@ var LsFilter = class _LsFilter extends ToolFilter {
5448
5430
  const hidden = entries.length - _LS_MAX_ENTRIES;
5449
5431
  const extPart = _lsExtSummary(entries.slice(_LS_MAX_ENTRIES));
5450
5432
  if (extPart) {
5451
- out.push(_LS_HIDDEN_MARKER_EXT.replace("{n}", String(hidden)).replace("{ext_summary}", () => extPart));
5433
+ out.push(_LS_HIDDEN_MARKER_EXT.replace("{n}", () => countNoun(hidden, "more entry", "more entries")).replace("{ext_summary}", () => extPart));
5452
5434
  } else {
5453
- out.push(_LS_HIDDEN_MARKER.replace("{n}", String(hidden)));
5435
+ out.push(_LS_HIDDEN_MARKER.replace("{n}", () => countNoun(hidden, "more entry", "more entries")));
5454
5436
  }
5455
5437
  return out;
5456
5438
  }
@@ -5484,9 +5466,7 @@ var _SUMMARY_KEYWORDS = ["director", "file", "total"];
5484
5466
  var EzaFilter = class _EzaFilter extends ToolFilter {
5485
5467
  name = "eza";
5486
5468
  binaries = /* @__PURE__ */ new Set(["eza", "exa", "ls"]);
5487
- // Flags eza/exa support that plain GNU/BSD `ls` does not -- used to disambiguate the shared
5488
- // 'ls' binary claim below (a common `alias ls=eza` setup means the literal command text is
5489
- // "ls ...", but token-goat only ever sees that raw text, never the shell's alias resolution).
5469
+ // Flags eza/exa support that plain GNU/BSD `ls` does not -- used to disambiguate the shared 'ls' binary claim below (a common `alias ls=eza` setup means the literal command text is "ls ...", but token-goat only ever sees that raw text, never the shell's alias resolution).
5490
5470
  static _EZA_ONLY_FLAGS = /* @__PURE__ */ new Set([
5491
5471
  "--tree",
5492
5472
  "-T",
@@ -5497,14 +5477,7 @@ var EzaFilter = class _EzaFilter extends ToolFilter {
5497
5477
  "--git-repos-no-status",
5498
5478
  "--level"
5499
5479
  ]);
5500
- // LsFilter (registered before EzaFilter in SHELL_FILE_FILTERS) always wins a plain `ls`
5501
- // invocation since binary-name matching alone can't tell a real GNU/BSD `ls` from an
5502
- // `alias ls=eza` shell alias -- the literal command text is "ls ..." either way. Without this
5503
- // gate, EzaFilter's own 'ls' binary claim was permanently unreachable dead code: LsFilter's
5504
- // generic ls-format compressor (no awareness of eza's tree/column output) silently ran on
5505
- // every aliased `ls --tree`/`ls --icons`/etc. invocation instead. Mirrors RgFilter's own
5506
- // `_hasContextFlags` gate, which resolves the same kind of shared-binary ambiguity between
5507
- // itself and GrepFilter.
5480
+ // LsFilter (registered before EzaFilter in SHELL_FILE_FILTERS) always wins a plain `ls` invocation since binary-name matching alone can't tell a real GNU/BSD `ls` from an `alias ls=eza` shell alias -- the literal command text is "ls ..." either way. Without this gate, EzaFilter's own 'ls' binary claim was permanently unreachable dead code: LsFilter's generic ls-format compressor (no awareness of eza's tree/column output) silently ran on every aliased `ls --tree`/`ls --icons`/etc. invocation instead. Mirrors RgFilter's own `_hasContextFlags` gate, which resolves the same kind of shared-binary ambiguity between itself and GrepFilter.
5508
5481
  matches(argv) {
5509
5482
  if (!super.matches(argv)) return false;
5510
5483
  const first = argv[0];
@@ -5525,7 +5498,7 @@ var EzaFilter = class _EzaFilter extends ToolFilter {
5525
5498
  }
5526
5499
  _compressTree(nonEmpty) {
5527
5500
  if (nonEmpty.length <= 60) return nonEmpty.join("\n").trimEnd();
5528
- return headTailCompress(nonEmpty, 40, 10, "items").trimEnd();
5501
+ return headTailCompress(nonEmpty, 40, 10, "item").trimEnd();
5529
5502
  }
5530
5503
  _compressFlatListing(nonEmpty, _argv) {
5531
5504
  if (nonEmpty.length <= _EZA_PASSTHROUGH) return nonEmpty.join("\n").trimEnd();
@@ -5537,7 +5510,7 @@ var EzaFilter = class _EzaFilter extends ToolFilter {
5537
5510
  if (headerIdx > 0) kept.push(...nonEmpty.slice(0, headerIdx));
5538
5511
  const dataLines = nonEmpty.slice(headerIdx);
5539
5512
  if (dataLines.length > 30) {
5540
- const compressed = headTailCompress(dataLines, 25, 5, "entries");
5513
+ const compressed = headTailCompress(dataLines, 25, 5, "entry", "entries");
5541
5514
  kept.push(...compressed.split("\n"));
5542
5515
  } else {
5543
5516
  kept.push(...dataLines);
@@ -5614,7 +5587,7 @@ var FdFilter = class extends ToolFilter {
5614
5587
  const lines = text.split("\n");
5615
5588
  const nonEmpty = lines.filter((l) => l.trim());
5616
5589
  if (nonEmpty.length <= _FD_COMPRESS_THRESHOLD) return text.trimEnd();
5617
- return headTailCompress(nonEmpty, 35, 5, "paths");
5590
+ return headTailCompress(nonEmpty, 35, 5, "path");
5618
5591
  }
5619
5592
  };
5620
5593
  var WcFilter = class extends ToolFilter {
@@ -5649,7 +5622,7 @@ var BatFilter = class extends ToolFilter {
5649
5622
  const lines = _stripBatBorders(text.split("\n"));
5650
5623
  const nonEmpty = lines.filter((l) => l.trim());
5651
5624
  if (nonEmpty.length <= 50) return lines.join("\n").trimEnd();
5652
- return headTailCompress(nonEmpty, 40, 10, "lines").trimEnd();
5625
+ return headTailCompress(nonEmpty, 40, 10, "line").trimEnd();
5653
5626
  }
5654
5627
  };
5655
5628
  var _DELTA_SEP_CHARS = new Set("\u2500\u2501");
@@ -5668,7 +5641,7 @@ var DeltaFilter = class extends ToolFilter {
5668
5641
  const lines = _stripDeltaSeparators(text.split("\n"));
5669
5642
  const nonEmpty = lines.filter((l) => l.trim());
5670
5643
  if (nonEmpty.length <= 80) return lines.join("\n").trimEnd();
5671
- return headTailCompress(nonEmpty, 60, 20, "lines").trimEnd();
5644
+ return headTailCompress(nonEmpty, 60, 20, "line").trimEnd();
5672
5645
  }
5673
5646
  };
5674
5647
  var FzfFilter = class extends ToolFilter {
@@ -5680,7 +5653,7 @@ var FzfFilter = class extends ToolFilter {
5680
5653
  const lines = text.split("\n");
5681
5654
  const nonEmpty = lines.filter((l) => l.trim());
5682
5655
  if (nonEmpty.length <= 50) return text.trimEnd();
5683
- return headTailCompress(nonEmpty, 40, 10, "lines").trimEnd();
5656
+ return headTailCompress(nonEmpty, 40, 10, "line").trimEnd();
5684
5657
  }
5685
5658
  };
5686
5659
  var LazyGitFilter = class extends ToolFilter {
@@ -5705,7 +5678,7 @@ var JqFilter = class extends ToolFilter {
5705
5678
  const lines = text.split("\n");
5706
5679
  const nonEmpty = lines.filter((l) => l.trim());
5707
5680
  if (nonEmpty.length <= 200) return text.trimEnd();
5708
- return headTailCompress(nonEmpty, 150, 50, "lines").trimEnd();
5681
+ return headTailCompress(nonEmpty, 150, 50, "line").trimEnd();
5709
5682
  }
5710
5683
  };
5711
5684
  var YqFilter = class extends ToolFilter {
@@ -5717,7 +5690,7 @@ var YqFilter = class extends ToolFilter {
5717
5690
  const lines = text.split("\n");
5718
5691
  const nonEmpty = lines.filter((l) => l.trim());
5719
5692
  if (nonEmpty.length <= 150) return text.trimEnd();
5720
- return headTailCompress(nonEmpty, 100, 50, "lines").trimEnd();
5693
+ return headTailCompress(nonEmpty, 100, 50, "line").trimEnd();
5721
5694
  }
5722
5695
  };
5723
5696
  var _CURL_VERBOSE_META_RE = /^[*>](\s|$)/;
@@ -5915,7 +5888,7 @@ function _scoreAndCapHunks(hunkLines, maxHunks) {
5915
5888
  if (keepSet.has(i)) out.push(...actual[i] ?? []);
5916
5889
  }
5917
5890
  out.push(
5918
- `[... ${dropped.length} more hunks, avg density ${avg.toFixed(2)} \u2014 likely whitespace/formatting]`
5891
+ `[... ${countNoun(dropped.length, "more hunk")}, avg density ${avg.toFixed(2)} \u2014 likely whitespace/formatting]`
5919
5892
  );
5920
5893
  return out;
5921
5894
  }
@@ -6152,7 +6125,7 @@ var FileTypeFilter = class extends ToolFilter {
6152
6125
  const lines = merged.split(/\r?\n/);
6153
6126
  if (lines.length <= _FILE_BATCH_LIMIT) return merged;
6154
6127
  const remaining = lines.length - _FILE_BATCH_LIMIT;
6155
- const kept = [...lines.slice(0, _FILE_BATCH_LIMIT), `[token-goat: ${remaining} more file entries truncated]
6128
+ const kept = [...lines.slice(0, _FILE_BATCH_LIMIT), `[token-goat: ${countNoun(remaining, "more file entry", "more file entries")} truncated]
6156
6129
  `];
6157
6130
  return this.finalize(kept);
6158
6131
  }
@@ -6307,9 +6280,7 @@ var SHELL_FILE_FILTERS = [
6307
6280
  ffmpegFilter,
6308
6281
  // Diff tool (plain POSIX diff; git diff is handled by GitFilter)
6309
6282
  diffFilter,
6310
- // Directory listings — EzaFilter before LsFilter: EzaFilter's matches() gate falls through to
6311
- // LsFilter for a plain `ls` with no eza-only flag, but must run FIRST so it can actually claim
6312
- // an aliased `ls --tree`/`ls --icons`/etc. invocation (see EzaFilter.matches doc comment).
6283
+ // Directory listings — EzaFilter before LsFilter: EzaFilter's matches() gate falls through to LsFilter for a plain `ls` with no eza-only flag, but must run FIRST so it can actually claim an aliased `ls --tree`/`ls --icons`/etc. invocation (see EzaFilter.matches doc comment).
6313
6284
  ezaFilter,
6314
6285
  lsFilter,
6315
6286
  fdFilter,
@@ -6363,8 +6334,8 @@ var NodeFilter = class extends ToolFilter {
6363
6334
  kept.push(line);
6364
6335
  }
6365
6336
  const notes = [];
6366
- maybeNote(notes, collapsedInternal, `collapsed ${collapsedInternal} node: internal frame(s)`);
6367
- maybeNote(notes, collapsedModules, `collapsed ${collapsedModules} node_modules frame(s)`);
6337
+ maybeNote(notes, collapsedInternal, `collapsed ${countNoun(collapsedInternal, "node: internal frame")}`);
6338
+ maybeNote(notes, collapsedModules, `collapsed ${countNoun(collapsedModules, "node_modules frame")}`);
6368
6339
  this.emitNotes(kept, notes);
6369
6340
  return this.finalize(kept);
6370
6341
  }
@@ -6405,7 +6376,7 @@ var PythonFilter = class extends ToolFilter {
6405
6376
  for (const group of frameGroups) out.push(...group);
6406
6377
  } else {
6407
6378
  for (const group of frameGroups.slice(0, 2)) out.push(...group);
6408
- out.push(` ... [${frameGroups.length - 5} more frames elided by token-goat]`);
6379
+ out.push(` ... [${countNoun(frameGroups.length - 5, "more frame")} elided by token-goat]`);
6409
6380
  for (const group of frameGroups.slice(frameGroups.length - 3)) out.push(...group);
6410
6381
  }
6411
6382
  frameGroups = [];
@@ -6451,7 +6422,7 @@ var PythonFilter = class extends ToolFilter {
6451
6422
  const count = j - i;
6452
6423
  if (count >= 5) {
6453
6424
  out.push(line);
6454
- out.push(`[token-goat: previous line repeated ${count - 1} more time(s)]`);
6425
+ out.push(`[token-goat: previous line repeated ${countNoun(count - 1, "more time")}]`);
6455
6426
  } else {
6456
6427
  for (let k = i; k < j; k++) out.push(lines[k]);
6457
6428
  }
@@ -6479,7 +6450,7 @@ var PythonFilter = class extends ToolFilter {
6479
6450
  }
6480
6451
  if (totalSuppressed > 0) {
6481
6452
  out.push(
6482
- `[token-goat: ${totalSuppressed} repeated warning(s) suppressed; run without TOKEN_GOAT_BASH_COMPRESS for full list]`
6453
+ `[token-goat: ${countNoun(totalSuppressed, "repeated warning")} suppressed; run without TOKEN_GOAT_BASH_COMPRESS for full list]`
6483
6454
  );
6484
6455
  }
6485
6456
  return out;
@@ -6532,7 +6503,7 @@ var RubyFilter = class extends ToolFilter {
6532
6503
  kept.push(line);
6533
6504
  }
6534
6505
  const notes = [];
6535
- maybeNote(notes, dotCount, `collapsed ${dotCount} passing test dot(s)`);
6506
+ maybeNote(notes, dotCount, `collapsed ${countNoun(dotCount, "passing test dot")}`);
6536
6507
  if (summaryLine) kept.push(summaryLine);
6537
6508
  this.emitNotes(kept, notes);
6538
6509
  return this.finalize(kept);
@@ -6562,7 +6533,7 @@ var BunFilter = class extends ToolFilter {
6562
6533
  if (sub === "build") return this._compressBuild(lines);
6563
6534
  const nonEmpty = lines.filter((l) => l.trim());
6564
6535
  if (nonEmpty.length <= 80) return this.finalize(lines);
6565
- return headTailCompress(lines, 60, 20, "lines");
6536
+ return headTailCompress(lines, 60, 20, "line");
6566
6537
  }
6567
6538
  _compressInstall(lines) {
6568
6539
  const kept = [];
@@ -6579,7 +6550,7 @@ var BunFilter = class extends ToolFilter {
6579
6550
  kept.push(line);
6580
6551
  }
6581
6552
  const notes = [];
6582
- maybeNote(notes, downloadCount, `collapsed ${downloadCount} package download/install line(s)`);
6553
+ maybeNote(notes, downloadCount, `collapsed ${countNoun(downloadCount, "package download/install line")}`);
6583
6554
  this.emitNotes(kept, notes);
6584
6555
  return this.finalize(kept);
6585
6556
  }
@@ -6598,7 +6569,7 @@ var BunFilter = class extends ToolFilter {
6598
6569
  kept.push(line);
6599
6570
  }
6600
6571
  const notes = [];
6601
- maybeNote(notes, passCount, `collapsed ${passCount} passing test(s)`);
6572
+ maybeNote(notes, passCount, `collapsed ${countNoun(passCount, "passing test")}`);
6602
6573
  this.emitNotes(kept, notes);
6603
6574
  return this.finalize(kept);
6604
6575
  }
@@ -6624,7 +6595,7 @@ var BunFilter = class extends ToolFilter {
6624
6595
  const extra = assets.length - 10;
6625
6596
  return [
6626
6597
  ...assets.slice(0, 10),
6627
- `[token-goat: ${extra} more asset/chunk line(s) elided; run 'bun build' for full output]`
6598
+ `[token-goat: ${countNoun(extra, "more asset/chunk line")} elided; run 'bun build' for full output]`
6628
6599
  ];
6629
6600
  }
6630
6601
  };
@@ -6678,7 +6649,7 @@ var DenoFilter = class extends ToolFilter {
6678
6649
  kept.push(line);
6679
6650
  }
6680
6651
  const notes = [];
6681
- maybeNote(notes, passCount, `collapsed ${passCount} passing test(s)`);
6652
+ maybeNote(notes, passCount, `collapsed ${countNoun(passCount, "passing test")}`);
6682
6653
  this.emitNotes(kept, notes);
6683
6654
  return this.finalize(kept);
6684
6655
  }
@@ -6697,7 +6668,7 @@ var DenoFilter = class extends ToolFilter {
6697
6668
  kept.push(line);
6698
6669
  }
6699
6670
  const notes = [];
6700
- maybeNote(notes, downloadCount, `collapsed ${downloadCount} download line(s)`);
6671
+ maybeNote(notes, downloadCount, `collapsed ${countNoun(downloadCount, "download line")}`);
6701
6672
  this.emitNotes(kept, notes);
6702
6673
  return this.finalize(kept);
6703
6674
  }
@@ -6719,15 +6690,15 @@ var DenoFilter = class extends ToolFilter {
6719
6690
  kept.push(line);
6720
6691
  }
6721
6692
  const notes = [];
6722
- maybeNote(notes, progressCount, `collapsed ${progressCount} check-progress line(s)`);
6723
- maybeNote(notes, permWarnCount, `collapsed ${permWarnCount} permission warning(s)`);
6693
+ maybeNote(notes, progressCount, `collapsed ${countNoun(progressCount, "check-progress line")}`);
6694
+ maybeNote(notes, permWarnCount, `collapsed ${countNoun(permWarnCount, "permission warning")}`);
6724
6695
  this.emitNotes(kept, notes);
6725
6696
  return this.finalize(kept);
6726
6697
  }
6727
6698
  _compressGeneric(lines) {
6728
6699
  const nonEmpty = lines.filter((l) => l.trim());
6729
6700
  if (nonEmpty.length <= 30) return this.finalize(lines);
6730
- return headTailCompress(lines, 40, 20, "lines");
6701
+ return headTailCompress(lines, 40, 20, "line");
6731
6702
  }
6732
6703
  };
6733
6704
  var denoFilter = new DenoFilter();
@@ -6773,8 +6744,8 @@ var FlutterFilter = class extends ToolFilter {
6773
6744
  kept.push(line);
6774
6745
  }
6775
6746
  const notes = [];
6776
- maybeNote(notes, compilingCount, `collapsed ${compilingCount} Dart source compilation(s)`);
6777
- maybeNote(notes, fontAssetCount, `collapsed ${fontAssetCount} font asset line(s)`);
6747
+ maybeNote(notes, compilingCount, `collapsed ${countNoun(compilingCount, "Dart source compilation")}`);
6748
+ maybeNote(notes, fontAssetCount, `collapsed ${countNoun(fontAssetCount, "font asset line")}`);
6778
6749
  this.emitNotes(kept, notes);
6779
6750
  return this.finalize(kept);
6780
6751
  }
@@ -6793,7 +6764,7 @@ var FlutterFilter = class extends ToolFilter {
6793
6764
  kept.push(line);
6794
6765
  }
6795
6766
  const notes = [];
6796
- maybeNote(notes, progressCount, `collapsed ${progressCount} test-progress line(s)`);
6767
+ maybeNote(notes, progressCount, `collapsed ${countNoun(progressCount, "test-progress line")}`);
6797
6768
  this.emitNotes(kept, notes);
6798
6769
  return this.finalize(kept);
6799
6770
  }
@@ -6812,7 +6783,7 @@ var FlutterFilter = class extends ToolFilter {
6812
6783
  kept.push(line);
6813
6784
  }
6814
6785
  const notes = [];
6815
- maybeNote(notes, pkgCount, `collapsed ${pkgCount} package line(s)`);
6786
+ maybeNote(notes, pkgCount, `collapsed ${countNoun(pkgCount, "package line")}`);
6816
6787
  this.emitNotes(kept, notes);
6817
6788
  return this.finalize(kept);
6818
6789
  }
@@ -6855,7 +6826,7 @@ var DartFilter = class extends ToolFilter {
6855
6826
  kept.push(line);
6856
6827
  }
6857
6828
  const notes = [];
6858
- maybeNote(notes, analyzingCount, `collapsed ${analyzingCount} analysis-progress line(s)`);
6829
+ maybeNote(notes, analyzingCount, `collapsed ${countNoun(analyzingCount, "analysis-progress line")}`);
6859
6830
  this.emitNotes(kept, notes);
6860
6831
  return this.finalize(kept);
6861
6832
  }
@@ -6874,7 +6845,7 @@ var DartFilter = class extends ToolFilter {
6874
6845
  kept.push(line);
6875
6846
  }
6876
6847
  const notes = [];
6877
- maybeNote(notes, progressCount, `collapsed ${progressCount} test-progress line(s)`);
6848
+ maybeNote(notes, progressCount, `collapsed ${countNoun(progressCount, "test-progress line")}`);
6878
6849
  this.emitNotes(kept, notes);
6879
6850
  return this.finalize(kept);
6880
6851
  }
@@ -6898,8 +6869,8 @@ var DartFilter = class extends ToolFilter {
6898
6869
  kept.push(line);
6899
6870
  }
6900
6871
  const notes = [];
6901
- maybeNote(notes, pkgCount, `collapsed ${pkgCount} package line(s)`);
6902
- maybeNote(notes, downloadCount, `collapsed ${downloadCount} download line(s)`);
6872
+ maybeNote(notes, pkgCount, `collapsed ${countNoun(pkgCount, "package line")}`);
6873
+ maybeNote(notes, downloadCount, `collapsed ${countNoun(downloadCount, "download line")}`);
6903
6874
  this.emitNotes(kept, notes);
6904
6875
  return this.finalize(kept);
6905
6876
  }
@@ -6918,7 +6889,7 @@ var DartFilter = class extends ToolFilter {
6918
6889
  kept.push(line);
6919
6890
  }
6920
6891
  const notes = [];
6921
- maybeNote(notes, compileCount, `collapsed ${compileCount} compilation step(s)`);
6892
+ maybeNote(notes, compileCount, `collapsed ${countNoun(compileCount, "compilation step")}`);
6922
6893
  this.emitNotes(kept, notes);
6923
6894
  return this.finalize(kept);
6924
6895
  }
@@ -6959,7 +6930,7 @@ var SwiftFilter = class extends ToolFilter {
6959
6930
  kept.push(line);
6960
6931
  }
6961
6932
  const notes = [];
6962
- maybeNote(notes, compileCount, `collapsed ${compileCount} compile/link step(s)`);
6933
+ maybeNote(notes, compileCount, `collapsed ${countNoun(compileCount, "compile/link step")}`);
6963
6934
  this.emitNotes(kept, notes);
6964
6935
  return this.finalize(kept);
6965
6936
  }
@@ -6983,7 +6954,7 @@ var SwiftFilter = class extends ToolFilter {
6983
6954
  kept.push(line);
6984
6955
  }
6985
6956
  const notes = [];
6986
- maybeNote(notes, passCount + startCount, `collapsed ${passCount + startCount} test-pass/start line(s)`);
6957
+ maybeNote(notes, passCount + startCount, `collapsed ${countNoun(passCount + startCount, "test-pass/start line")}`);
6987
6958
  this.emitNotes(kept, notes);
6988
6959
  return this.finalize(kept);
6989
6960
  }
@@ -7019,8 +6990,8 @@ var XcodeFilter = class extends ToolFilter {
7019
6990
  kept.push(line);
7020
6991
  }
7021
6992
  const notes = [];
7022
- maybeNote(notes, compileCount, `collapsed ${compileCount} compile/link step(s)`);
7023
- maybeNote(notes, taskBodyCount, `collapsed ${taskBodyCount} task-body line(s)`);
6993
+ maybeNote(notes, compileCount, `collapsed ${countNoun(compileCount, "compile/link step")}`);
6994
+ maybeNote(notes, taskBodyCount, `collapsed ${countNoun(taskBodyCount, "task-body line")}`);
7024
6995
  this.emitNotes(kept, notes);
7025
6996
  return this.finalize(kept);
7026
6997
  }
@@ -7084,7 +7055,7 @@ var MixFilter = class extends ToolFilter {
7084
7055
  kept.push(line);
7085
7056
  }
7086
7057
  const notes = [];
7087
- maybeNote(notes, depCount, `collapsed ${depCount} dependency fetch line(s)`);
7058
+ maybeNote(notes, depCount, `collapsed ${countNoun(depCount, "dependency fetch line")}`);
7088
7059
  this.emitNotes(kept, notes);
7089
7060
  return this.finalize(kept);
7090
7061
  }
@@ -7103,7 +7074,7 @@ var MixFilter = class extends ToolFilter {
7103
7074
  kept.push(line);
7104
7075
  }
7105
7076
  const notes = [];
7106
- maybeNote(notes, compilingCount, `collapsed ${compilingCount} compilation batch(es)`);
7077
+ maybeNote(notes, compilingCount, `collapsed ${countNoun(compilingCount, "compilation batch", "compilation batches")}`);
7107
7078
  this.emitNotes(kept, notes);
7108
7079
  return this.finalize(kept);
7109
7080
  }
@@ -7124,12 +7095,11 @@ var MixFilter = class extends ToolFilter {
7124
7095
  kept.push(line);
7125
7096
  }
7126
7097
  const notes = [];
7127
- maybeNote(notes, dotCount, `collapsed ${dotCount} passing test dot(s)`);
7098
+ maybeNote(notes, dotCount, `collapsed ${countNoun(dotCount, "passing test dot")}`);
7128
7099
  this.emitNotes(kept, notes);
7129
7100
  return this.finalize(kept);
7130
7101
  }
7131
- // Deliberate passthrough: mix ecto.* output is already terse (no per-line noise pattern
7132
- // to collapse), so nothing is filtered here.
7102
+ // Deliberate passthrough: mix ecto.* output is already terse (no per-line noise pattern to collapse), so nothing is filtered here.
7133
7103
  _compressEcto(lines) {
7134
7104
  return this.finalize(lines);
7135
7105
  }
@@ -7187,13 +7157,13 @@ var ZigFilter = class extends ToolFilter {
7187
7157
  }
7188
7158
  const out = [...stepSample];
7189
7159
  if (stepCount > ZIG_STEP_SAMPLE) {
7190
- out.push(`[token-goat: +${stepCount - ZIG_STEP_SAMPLE} more build step(s)...]`);
7160
+ out.push(`[token-goat: +${countNoun(stepCount - ZIG_STEP_SAMPLE, "more build step")}...]`);
7191
7161
  }
7192
7162
  out.push(...kept);
7193
7163
  const notes = [];
7194
- maybeNote(notes, testPassCount, `collapsed ${testPassCount} passing test(s)`);
7195
- maybeNote(notes, fetchCount, `collapsed ${fetchCount} fetch line(s)`);
7196
- maybeNote(notes, infoNoiseCount, `dropped ${infoNoiseCount} info-noise line(s)`);
7164
+ maybeNote(notes, testPassCount, `collapsed ${countNoun(testPassCount, "passing test")}`);
7165
+ maybeNote(notes, fetchCount, `collapsed ${countNoun(fetchCount, "fetch line")}`);
7166
+ maybeNote(notes, infoNoiseCount, `dropped ${countNoun(infoNoiseCount, "info-noise line")}`);
7197
7167
  this.emitNotes(out, notes);
7198
7168
  return this.finalize(out);
7199
7169
  }
@@ -7249,8 +7219,8 @@ var RCmdFilter = class extends ToolFilter {
7249
7219
  kept.push(line);
7250
7220
  }
7251
7221
  const prepend = [];
7252
- if (okCount) prepend.push(`[token-goat: collapsed ${okCount} R CMD check-OK line(s)]`);
7253
- if (installSectionCount) prepend.push(`[token-goat: collapsed ${installSectionCount} package installation/loading line(s)]`);
7222
+ if (okCount) prepend.push(`[token-goat: collapsed ${countNoun(okCount, "R CMD check-OK line")}]`);
7223
+ if (installSectionCount) prepend.push(`[token-goat: collapsed ${countNoun(installSectionCount, "package installation/loading line")}]`);
7254
7224
  return this.finalize([...prepend, ...kept]);
7255
7225
  }
7256
7226
  };
@@ -7292,22 +7262,22 @@ var erlangFilter = makeLanguageFilter({
7292
7262
  {
7293
7263
  re: REBAR3_COMPILING_RE,
7294
7264
  position: "prepend",
7295
- note: (n) => `[token-goat: collapsed ${n} Erlang module compilation(s)]`
7265
+ note: (n) => `[token-goat: collapsed ${countNoun(n, "Erlang module compilation")}]`
7296
7266
  },
7297
7267
  {
7298
7268
  re: REBAR3_FETCH_RE,
7299
7269
  position: "prepend",
7300
- note: (n) => `[token-goat: collapsed ${n} dependency fetch/resolve line(s)]`
7270
+ note: (n) => `[token-goat: collapsed ${countNoun(n, "dependency fetch/resolve line")}]`
7301
7271
  },
7302
7272
  {
7303
7273
  re: REBAR3_STEP_NOISE_RE,
7304
7274
  position: "note",
7305
- note: (n) => `dropped ${n} rebar3 build-step line(s)`
7275
+ note: (n) => `dropped ${countNoun(n, "rebar3 build-step line")}`
7306
7276
  },
7307
7277
  {
7308
7278
  res: [REBAR3_EUNIT_PASS_RE, REBAR3_CT_PASS_RE],
7309
7279
  position: "note",
7310
- note: (n) => `collapsed ${n} test-pass line(s)`
7280
+ note: (n) => `collapsed ${countNoun(n, "test-pass line")}`
7311
7281
  }
7312
7282
  ]
7313
7283
  });
@@ -7335,22 +7305,22 @@ var crystalFilter = makeLanguageFilter({
7335
7305
  {
7336
7306
  re: CRYSTAL_COMPILING_RE,
7337
7307
  position: "prepend",
7338
- note: (n) => `[token-goat: collapsed ${n} Crystal compiling/linking step(s)]`
7308
+ note: (n) => `[token-goat: collapsed ${countNoun(n, "Crystal compiling/linking step")}]`
7339
7309
  },
7340
7310
  {
7341
7311
  re: CRYSTAL_SHARDS_PROGRESS_RE,
7342
7312
  position: "prepend",
7343
- note: (n) => `[token-goat: collapsed ${n} shards dependency operation(s)]`
7313
+ note: (n) => `[token-goat: collapsed ${countNoun(n, "shards dependency operation")}]`
7344
7314
  },
7345
7315
  {
7346
7316
  re: CRYSTAL_SPEC_PASS_RE,
7347
7317
  position: "note",
7348
- note: (n) => `collapsed ${n} spec-pass line(s)`
7318
+ note: (n) => `collapsed ${countNoun(n, "spec-pass line")}`
7349
7319
  },
7350
7320
  {
7351
7321
  re: CRYSTAL_DOT_PROGRESS_RE,
7352
7322
  position: "note",
7353
- note: (n) => `collapsed ${n} progress dot(s)`
7323
+ note: (n) => `collapsed ${countNoun(n, "progress dot")}`
7354
7324
  }
7355
7325
  ]
7356
7326
  });
@@ -7380,24 +7350,24 @@ var haskellFilter = makeLanguageFilter({
7380
7350
  {
7381
7351
  re: HASKELL_RESOLVING_RE,
7382
7352
  position: "prepend",
7383
- note: (n) => `[token-goat: collapsed ${n} dependency resolve/download line(s)]`
7353
+ note: (n) => `[token-goat: collapsed ${countNoun(n, "dependency resolve/download line")}]`
7384
7354
  },
7385
7355
  {
7386
7356
  re: HASKELL_COMPILING_RE,
7387
7357
  position: "prepend",
7388
- note: (n) => `[token-goat: collapsed ${n} module compilation(s)]`
7358
+ note: (n) => `[token-goat: collapsed ${countNoun(n, "module compilation")}]`
7389
7359
  },
7390
7360
  {
7391
7361
  re: HASKELL_LINKING_RE,
7392
7362
  position: "prepend",
7393
- note: (n) => `[token-goat: collapsed ${n} linking/building step(s)]`
7363
+ note: (n) => `[token-goat: collapsed ${countNoun(n, "linking/building step")}]`
7394
7364
  }
7395
7365
  ],
7396
7366
  dedupeRules: [
7397
7367
  {
7398
7368
  re: HASKELL_WARNING_RE,
7399
7369
  maxPerKey: 3,
7400
- note: (n) => `deduplicated ${n} repeated warning(s)`
7370
+ note: (n) => `deduplicated ${countNoun(n, "repeated warning")}`
7401
7371
  }
7402
7372
  ]
7403
7373
  });
@@ -7427,16 +7397,16 @@ var elmFilter = makeLanguageFilter({
7427
7397
  {
7428
7398
  re: ELM_DOWNLOADING_RE,
7429
7399
  position: "prepend",
7430
- note: (n) => `[token-goat: Downloaded ${n} Elm package(s); disable via TOKEN_GOAT_BASH_COMPRESS for full list]`
7400
+ note: (n) => `[token-goat: Downloaded ${countNoun(n, "Elm package")}; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`
7431
7401
  },
7432
7402
  {
7433
7403
  re: ELM_COMPILING_RE,
7434
7404
  position: "prepend",
7435
- note: (n) => `[token-goat: collapsed ${n} Elm source file compilation(s)]`
7405
+ note: (n) => `[token-goat: collapsed ${countNoun(n, "Elm source file compilation")}]`
7436
7406
  }
7437
7407
  ],
7438
7408
  dropRules: [ELM_DOT_PROGRESS_RE, ELM_DEPS_PROGRESS_RE],
7439
- droppedNoiseNote: (n) => `dropped ${n} dependency-progress line(s)`
7409
+ droppedNoiseNote: (n) => `dropped ${countNoun(n, "dependency-progress line")}`
7440
7410
  });
7441
7411
  var JULIA_PKG_RESOLVING_RE = (
7442
7412
  // eslint-disable-next-line no-control-regex
@@ -7471,17 +7441,17 @@ var juliaFilter = makeLanguageFilter({
7471
7441
  {
7472
7442
  res: [JULIA_PKG_DEP_LINE_RE, JULIA_PKG_STATUS_RE],
7473
7443
  position: "prepend",
7474
- note: (n) => `[token-goat: ${n} Julia package operation(s); disable via TOKEN_GOAT_BASH_COMPRESS for full list]`
7444
+ note: (n) => `[token-goat: ${countNoun(n, "Julia package operation")}; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`
7475
7445
  },
7476
7446
  {
7477
7447
  res: [JULIA_PKG_RESOLVING_RE, JULIA_PKG_INSTALLED_RE, JULIA_PKG_BUILDING_RE, JULIA_PRECOMPILE_RE],
7478
7448
  position: "prepend",
7479
- note: (n) => `[token-goat: collapsed ${n} Pkg progress banner(s)]`
7449
+ note: (n) => `[token-goat: collapsed ${countNoun(n, "Pkg progress banner")}]`
7480
7450
  },
7481
7451
  {
7482
7452
  re: JULIA_TEST_PASS_RE,
7483
7453
  position: "note",
7484
- note: (n) => `collapsed ${n} test-pass line(s)`
7454
+ note: (n) => `collapsed ${countNoun(n, "test-pass line")}`
7485
7455
  }
7486
7456
  ]
7487
7457
  });
@@ -7518,8 +7488,8 @@ var PowerShellErrorFilter = class extends ToolFilter {
7518
7488
  kept.push(line);
7519
7489
  }
7520
7490
  const notes = [];
7521
- if (noiseCount > 0) notes.push(`collapsed ${noiseCount} verbose/debug/install-progress line(s)`);
7522
- if (warnElided > 0) notes.push(`deduplicated ${warnElided} repeated WARNING(s)`);
7491
+ if (noiseCount > 0) notes.push(`collapsed ${countNoun(noiseCount, "verbose/debug/install-progress line")}`);
7492
+ if (warnElided > 0) notes.push(`deduplicated ${countNoun(warnElided, "repeated WARNING")}`);
7523
7493
  this.emitNotes(kept, notes);
7524
7494
  return this.finalize(kept);
7525
7495
  }
@@ -7528,7 +7498,7 @@ var PowerShellErrorFilter = class extends ToolFilter {
7528
7498
  return text.replace(PS_CNF_ERROR_RE, (match, headerCmd, categoryCmd) => {
7529
7499
  const cmd = (categoryCmd || headerCmd || "").trim();
7530
7500
  const elidedLines = match.replace(/\r?\n$/, "").split(/\r?\n/).length;
7531
- return `PowerShell CommandNotFoundException: '${cmd}' not found (elided ${elidedLines} lines of stack trace). If invoked from Bash/Git-Bash and the command used $_, bash pre-expands it before PowerShell sees it -- escape as \`$_\` (backtick) or single-quote the whole -Command string.`;
7501
+ return `PowerShell CommandNotFoundException: ${echoedValue(cmd)} not found (elided ${elidedLines} lines of stack trace). If invoked from Bash/Git-Bash and the command used $_, bash pre-expands it before PowerShell sees it -- escape as \`$_\` (backtick) or single-quote the whole -Command string.`;
7532
7502
  });
7533
7503
  }
7534
7504
  };
@@ -8254,7 +8224,7 @@ var SysPackageFilter = class extends ToolFilter {
8254
8224
  kept.push(line);
8255
8225
  }
8256
8226
  const out = [...progressSample];
8257
- if (progressExtra) out.push(`[token-goat: +${progressExtra} more brew progress lines collapsed]`);
8227
+ if (progressExtra) out.push(`[token-goat: +${countNoun(progressExtra, "more brew progress line")} collapsed]`);
8258
8228
  out.push(...kept);
8259
8229
  return this.finalize(out);
8260
8230
  }
@@ -8376,7 +8346,7 @@ var SassFilter = class _SassFilter extends ToolFilter {
8376
8346
  kept.push(line);
8377
8347
  }
8378
8348
  const out = [...writeSample];
8379
- if (writeExtra) out.push(`[token-goat: +${writeExtra} more compiled CSS files; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`);
8349
+ if (writeExtra) out.push(`[token-goat: +${countNoun(writeExtra, "more compiled CSS file")}; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`);
8380
8350
  out.push(...kept);
8381
8351
  const notes = [];
8382
8352
  maybeNote(notes, droppedMap, `dropped ${droppedMap} source-map write lines`);
@@ -8855,13 +8825,13 @@ var JsonArrayFilter = class extends ToolFilter {
8855
8825
  if (dupCounts.size) {
8856
8826
  for (const [ks, n] of dupCounts) {
8857
8827
  const keysRepr = ks.split(",").join(", ");
8858
- suffixLines.push(`[... ${n} duplicate objects with keys {${keysRepr}} omitted]`);
8828
+ suffixLines.push(`[... ${countNoun(n, "duplicate object")} with keys {${keysRepr}} omitted]`);
8859
8829
  }
8860
8830
  }
8861
8831
  if (kept.length > JSON_ARRAY_MAX_ITEMS) {
8862
8832
  const extra = kept.length - JSON_ARRAY_MAX_ITEMS;
8863
8833
  kept.splice(JSON_ARRAY_MAX_ITEMS);
8864
- suffixLines.push(`[... ${extra} more items not shown]`);
8834
+ suffixLines.push(`[... ${countNoun(extra, "more item")} not shown]`);
8865
8835
  changed = true;
8866
8836
  }
8867
8837
  if (!changed) return text;
@@ -9183,11 +9153,11 @@ var DockerComposeFilter = class extends ToolFilter {
9183
9153
  kept.push(line);
9184
9154
  }
9185
9155
  if (pullingCount > pullingKept) {
9186
- kept.push(`[token-goat: ${pullingCount - pullingKept} more Pulling lines elided]`);
9156
+ kept.push(`[token-goat: ${countNoun(pullingCount - pullingKept, "more Pulling line")} elided]`);
9187
9157
  }
9188
9158
  for (const [containerKey, count] of [...healthCounts.entries()].sort()) {
9189
9159
  if (count > 1) {
9190
- kept.push(`[token-goat: ${count - 1} more health-check wait lines for ${containerKey}]`);
9160
+ kept.push(`[token-goat: ${countNoun(count - 1, "more health-check wait line")} for ${containerKey}]`);
9191
9161
  }
9192
9162
  }
9193
9163
  const STREAM_THRESHOLD = 50;
@@ -9238,7 +9208,7 @@ function _compressKubectlEvents(text) {
9238
9208
  const elided = rows.length - _MAX_PER_REASON;
9239
9209
  totalElided += elided;
9240
9210
  kept.push(...rows.slice(-_MAX_PER_REASON));
9241
- kept.push(` [token-goat: ${elided} earlier '${reason}' events elided]`);
9211
+ kept.push(` [token-goat: ${elided} earlier ${echoedValue(reason)} events elided]`);
9242
9212
  }
9243
9213
  }
9244
9214
  if (totalElided) {
@@ -9306,7 +9276,7 @@ function _compressKubectlDescribe(text) {
9306
9276
  }
9307
9277
  kept.push(...entries.slice(0, 3));
9308
9278
  if (entries.length > 3) {
9309
- kept.push(" ".repeat(headerIndent + 2) + `[token-goat: ${entries.length - 3} more entries elided]`);
9279
+ kept.push(" ".repeat(headerIndent + 2) + `[token-goat: ${countNoun(entries.length - 3, "more entry", "more entries")} elided]`);
9310
9280
  }
9311
9281
  continue;
9312
9282
  }
@@ -9406,7 +9376,7 @@ var KubectlFilter = class extends ToolFilter {
9406
9376
  if (text.includes("\n")) {
9407
9377
  const nonEmpty = text.split("\n").filter((l) => l.trim());
9408
9378
  if (nonEmpty.length > 50) {
9409
- text = headTailCompress(nonEmpty, 30, 20, "log lines");
9379
+ text = headTailCompress(nonEmpty, 30, 20, "log line");
9410
9380
  } else {
9411
9381
  text = nonEmpty.join("\n");
9412
9382
  }
@@ -9415,7 +9385,7 @@ var KubectlFilter = class extends ToolFilter {
9415
9385
  } else if (subcommand === "diff") {
9416
9386
  const diffLines = text.split("\n");
9417
9387
  if (diffLines.length > 50) {
9418
- text = headTailCompress(diffLines, 35, 15, "diff lines");
9388
+ text = headTailCompress(diffLines, 35, 15, "diff line");
9419
9389
  }
9420
9390
  }
9421
9391
  if (stderr.trim()) {
@@ -9474,7 +9444,7 @@ function _collapseStackTraces(lines) {
9474
9444
  }
9475
9445
  out.push(...frames.slice(0, MAX_FRAMES).flat());
9476
9446
  if (frames.length > MAX_FRAMES) {
9477
- out.push(` ... ${frames.length - MAX_FRAMES} more frames`);
9447
+ out.push(` ... ${countNoun(frames.length - MAX_FRAMES, "more frame")}`);
9478
9448
  }
9479
9449
  i = j;
9480
9450
  } else {
@@ -9501,7 +9471,7 @@ function _dedupLogLinesWithPodPrefix(lines, keepFirstN = 3) {
9501
9471
  if (prevKey !== null && prevKey !== key) {
9502
9472
  const pendingOmit = omit.get(prevKey) ?? 0;
9503
9473
  if (pendingOmit > 0) {
9504
- out.push(`[token-goat: ${pendingOmit} more similar lines omitted]`);
9474
+ out.push(`[token-goat: ${countNoun(pendingOmit, "more similar line")} omitted]`);
9505
9475
  omit.set(prevKey, 0);
9506
9476
  }
9507
9477
  }
@@ -9514,7 +9484,7 @@ function _dedupLogLinesWithPodPrefix(lines, keepFirstN = 3) {
9514
9484
  const flushed = /* @__PURE__ */ new Set();
9515
9485
  for (const [key, count] of omit.entries()) {
9516
9486
  if (count > 0 && !flushed.has(key)) {
9517
- out.push(`[token-goat: ${count} more similar lines omitted]`);
9487
+ out.push(`[token-goat: ${countNoun(count, "more similar line")} omitted]`);
9518
9488
  flushed.add(key);
9519
9489
  }
9520
9490
  }
@@ -9582,7 +9552,7 @@ var KubectlLogsFilter = class extends ToolFilter {
9582
9552
  nonEmpty = _collapseJsonBlobs(nonEmpty, 5);
9583
9553
  let result;
9584
9554
  if (nonEmpty.length > 200) {
9585
- result = headTailCompress(nonEmpty, 40, 40, "log lines");
9555
+ result = headTailCompress(nonEmpty, 40, 40, "log line");
9586
9556
  } else {
9587
9557
  result = nonEmpty.join("\n");
9588
9558
  }
@@ -9639,7 +9609,7 @@ function _compressHelmTemplate(lines) {
9639
9609
  if (line.trim().startsWith("---")) sections.push(line);
9640
9610
  }
9641
9611
  if (sections.length === 0) {
9642
- return headTailCompress(lines, 10, 10, "template lines");
9612
+ return headTailCompress(lines, 10, 10, "template line");
9643
9613
  }
9644
9614
  sections.push(
9645
9615
  `[token-goat: helm template ${total} total lines; showing ${sections.length} document headers only]`
@@ -9750,10 +9720,7 @@ function _stripGitCrlfWarnings(text) {
9750
9720
  }
9751
9721
  var GitBaseFilter = class extends ToolFilter {
9752
9722
  binaries = /* @__PURE__ */ new Set(["git"]);
9753
- // Overrides ToolFilter.matches() to scan gitPositionalArgs() instead of the generic
9754
- // positionalArgs() -- git's subcommand-identifying tokens must skip the value of -m/--message
9755
- // and other value-taking flags, or a word inside a commit message could be mistaken for
9756
- // another filter's subcommand keyword.
9723
+ // Overrides ToolFilter.matches() to scan gitPositionalArgs() instead of the generic positionalArgs() -- git's subcommand-identifying tokens must skip the value of -m/--message and other value-taking flags, or a word inside a commit message could be mistaken for another filter's subcommand keyword.
9757
9724
  matches(argv) {
9758
9725
  if (argv.length === 0) return false;
9759
9726
  const first = argv[0];
@@ -9799,7 +9766,7 @@ function _summariseCommitHeader(lines) {
9799
9766
  const detailParts = [];
9800
9767
  if (author) detailParts.push(author);
9801
9768
  if (dateStr) detailParts.push(dateStr);
9802
- if (subject) detailParts.push(`"${subject}"`);
9769
+ if (subject) detailParts.push(`${echoedValue(subject)}`);
9803
9770
  if (detailParts.length) parts.push(" " + detailParts.join(" | "));
9804
9771
  return parts;
9805
9772
  }
@@ -9871,7 +9838,7 @@ function _capStatLinesInBlock(block, maxFiles) {
9871
9838
  if (statIdx < maxFiles) {
9872
9839
  newLines.push(ln);
9873
9840
  } else if (!replaced) {
9874
- newLines.push(`[token-goat: +${elided} more stat lines omitted]`);
9841
+ newLines.push(`[token-goat: +${countNoun(elided, "more stat line")} omitted]`);
9875
9842
  replaced = true;
9876
9843
  }
9877
9844
  statIdx++;
@@ -9909,7 +9876,7 @@ function _compressGitLogEnhanced(stdout, stderr, argv, inputTruncated = false, m
9909
9876
  let keptLines;
9910
9877
  if (blocks.length > ONELINE_CAP) {
9911
9878
  const elided = blocks.length - ONELINE_CAP;
9912
- const elidedNote = inputTruncated ? `[token-goat: at least ${elided} more commits (counted over a truncated input)]` : `[token-goat: +${elided} more commits]`;
9879
+ const elidedNote = inputTruncated ? `[token-goat: at least ${countNoun(elided, "more commit")} (counted over a truncated input)]` : `[token-goat: +${countNoun(elided, "more commit")}]`;
9913
9880
  keptLines = [...blocks.slice(0, ONELINE_CAP), elidedNote];
9914
9881
  } else {
9915
9882
  keptLines = blocks;
@@ -10010,7 +9977,7 @@ function _compressGitDiffStat(stdout, stderr, argv) {
10010
9977
  }
10011
9978
  const keptStat = [
10012
9979
  ...statLines.slice(0, HEAD_FILES),
10013
- ` [token-goat: +${elided} more files changed, +${adds} -${dels} lines]`
9980
+ ` [token-goat: +${countNoun(elided, "more file")} changed, +${adds} -${dels} lines]`
10014
9981
  ];
10015
9982
  out = [...otherLines, ...keptStat, ...summaryLines].join("\n");
10016
9983
  } else {
@@ -10106,7 +10073,7 @@ function _capHunksByDensity(hunks, maxHunksPerFile) {
10106
10073
  const kept = scored.filter((x) => keepSet.has(x.i)).map((x) => x.h);
10107
10074
  return {
10108
10075
  hunks: [hunks[0] ?? "", ...kept],
10109
- note: `[token-goat: ${dropped.length} more hunks, avg density ${avg.toFixed(2)} \u2014 likely whitespace/formatting]`
10076
+ note: `[token-goat: ${countNoun(dropped.length, "more hunk")}, avg density ${avg.toFixed(2)} \u2014 likely whitespace/formatting]`
10110
10077
  };
10111
10078
  }
10112
10079
  function _collapseDiffBlocksToCap(outBlocks, maxLines) {
@@ -10179,7 +10146,7 @@ function _compressGitDiffBody(stdout, stderr, maxHunksPerFile = 10, maxLines) {
10179
10146
  if (nTrimmed > 0) {
10180
10147
  compressedHunks.push(
10181
10148
  trimmedLines.join("\n") + `
10182
- [token-goat: ${nTrimmed} trailing context line(s) trimmed]`
10149
+ [token-goat: ${countNoun(nTrimmed, "trailing context line")} trimmed]`
10183
10150
  );
10184
10151
  } else {
10185
10152
  compressedHunks.push(hunk);
@@ -10315,7 +10282,7 @@ function _compressGitBlameAnnotated(lines, stderr) {
10315
10282
  out.push(runStartLine);
10316
10283
  if (runCount > 1) {
10317
10284
  const hashShort = currentHash ? currentHash.slice(0, 8) : "?";
10318
- out.push(`[token-goat: ${runCount - 1} more lines by ${currentAuthor} (${hashShort})]`);
10285
+ out.push(`[token-goat: ${countNoun(runCount - 1, "more line")} by ${currentAuthor} (${hashShort})]`);
10319
10286
  }
10320
10287
  }
10321
10288
  for (const line of lines) {
@@ -10358,7 +10325,7 @@ function _compressGitBlamePorcelain(lines, stderr) {
10358
10325
  out.push(...blockLines);
10359
10326
  if (runCount > 1) {
10360
10327
  const hashShort = currentHash ? currentHash.slice(0, 8) : "?";
10361
- out.push(`[token-goat: ${runCount - 1} more lines by ${currentAuthor} (${hashShort})]`);
10328
+ out.push(`[token-goat: ${countNoun(runCount - 1, "more line")} by ${currentAuthor} (${hashShort})]`);
10362
10329
  }
10363
10330
  }
10364
10331
  let i = 0;
@@ -10635,7 +10602,7 @@ function _compressGitPush(stdout, stderr) {
10635
10602
  const MAX_ERROR_LINES = 30;
10636
10603
  const flushCapMarker = () => {
10637
10604
  if (capReached) {
10638
- kept.push(`[token-goat: +${omittedCount} more error lines omitted]`);
10605
+ kept.push(`[token-goat: +${countNoun(omittedCount, "more error line")} omitted]`);
10639
10606
  }
10640
10607
  };
10641
10608
  const resetBlock = () => {
@@ -10688,7 +10655,7 @@ function _truncateListing(stdout, stderr, head = 100) {
10688
10655
  merged = stdout;
10689
10656
  } else {
10690
10657
  merged = lines.slice(0, head).join("\n") + `
10691
- [token-goat: +${lines.length - head} more lines elided]`;
10658
+ [token-goat: +${countNoun(lines.length - head, "more line")} elided]`;
10692
10659
  }
10693
10660
  if (stderr.trim()) merged += "\n---\n" + stderr.replace(/\s+$/, "");
10694
10661
  return merged;
@@ -10713,9 +10680,7 @@ function _compressGitRemote(stdout, stderr) {
10713
10680
  }
10714
10681
  var GitFilter = class extends GitBaseFilter {
10715
10682
  name = "git";
10716
- // No subcommands set — matches any git command as catch-all, except `git
10717
- // grep`, which is excluded here so dispatch falls through to GrepFilter
10718
- // (registered after GIT_FILTERS) and its per-file match-count summarizer.
10683
+ // No subcommands set — matches any git command as catch-all, except `git grep`, which is excluded here so dispatch falls through to GrepFilter (registered after GIT_FILTERS) and its per-file match-count summarizer.
10719
10684
  matches(argv) {
10720
10685
  if (!super.matches(argv)) return false;
10721
10686
  return gitPositionalArgs(argv.slice(1))[0] !== "grep";
@@ -10755,7 +10720,7 @@ function _emitEslintRules(perRule) {
10755
10720
  const out = [];
10756
10721
  for (const [rule, entries] of [...perRule.entries()].sort()) {
10757
10722
  out.push(...entries.slice(0, 3));
10758
- if (entries.length > 3) out.push(` [token-goat: +${entries.length - 3} more ${rule} violations]`);
10723
+ if (entries.length > 3) out.push(` [token-goat: +${countNoun(entries.length - 3, `more ${rule} violation`)}]`);
10759
10724
  }
10760
10725
  return out;
10761
10726
  }
@@ -11107,7 +11072,7 @@ var ESLintFilter = class extends ToolFilter {
11107
11072
  for (const [byRule, noun] of [[errorsByRule, "error"], [warningsByRule, "warning"]]) {
11108
11073
  for (const [rule, entries] of [...byRule.entries()].sort()) {
11109
11074
  fileOut.push(...entries.slice(0, 3));
11110
- if (entries.length > 3) fileOut.push(` [token-goat: +${entries.length - 3} more ${rule} ${noun}s]`);
11075
+ if (entries.length > 3) fileOut.push(` [token-goat: +${countNoun(entries.length - 3, `more ${rule} ${noun}`)}]`);
11111
11076
  }
11112
11077
  }
11113
11078
  fileOut.push(...issues.slice(lastBody));
@@ -11288,7 +11253,7 @@ var BiomeFilter = class extends ToolFilter {
11288
11253
  if (ruleCollapsed.size) {
11289
11254
  for (const [rule, cnt] of [...ruleCollapsed.entries()].sort()) {
11290
11255
  kept.push(
11291
- `[token-goat: +${cnt} more ${rule} diagnostic(s) elided; run \`biome check\` for full output]`
11256
+ `[token-goat: +${cnt} more ${rule} ${cnt === 1 ? "diagnostic" : "diagnostics"} elided; run \`biome check\` for full output]`
11292
11257
  );
11293
11258
  }
11294
11259
  }
@@ -11368,7 +11333,7 @@ var PrettierFilter = class _PrettierFilter extends ToolFilter {
11368
11333
  }
11369
11334
  const out = [...changedSample];
11370
11335
  if (changedExtra) {
11371
- out.push(`[token-goat: +${changedExtra} more formatted files; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`);
11336
+ out.push(`[token-goat: +${countNoun(changedExtra, "more formatted file")}; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`);
11372
11337
  }
11373
11338
  out.push(...kept);
11374
11339
  const notes = [];
@@ -11738,7 +11703,7 @@ var BlackIsortFilter = class _BlackIsortFilter extends ToolFilter {
11738
11703
  }
11739
11704
  const out = [...reformatSample];
11740
11705
  if (reformatExtra) {
11741
- out.push(`[token-goat: +${reformatExtra} more reformatted files; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`);
11706
+ out.push(`[token-goat: +${countNoun(reformatExtra, "more reformatted file")}; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`);
11742
11707
  }
11743
11708
  out.push(...kept);
11744
11709
  return this.finalize(out);
@@ -11767,7 +11732,7 @@ var BlackIsortFilter = class _BlackIsortFilter extends ToolFilter {
11767
11732
  }
11768
11733
  const out = [...fixSample];
11769
11734
  if (fixExtra) {
11770
- out.push(`[token-goat: +${fixExtra} more fixed files; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`);
11735
+ out.push(`[token-goat: +${countNoun(fixExtra, "more fixed file")}; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`);
11771
11736
  }
11772
11737
  out.push(...kept);
11773
11738
  return this.finalize(out);
@@ -11854,7 +11819,7 @@ var GolangciLintFilter = class _GolangciLintFilter extends ToolFilter {
11854
11819
  const lnt = mPh[2];
11855
11820
  const total = issueCounts.get(`${fp}\0${lnt}`) ?? _GolangciLintFilter._KEEP_FIRST_N + 1;
11856
11821
  const extra = total - _GolangciLintFilter._KEEP_FIRST_N;
11857
- final.push(`[token-goat: +${extra} more ${lnt} issues in ${fp} omitted]`);
11822
+ final.push(`[token-goat: +${countNoun(extra, `more ${lnt} issue`)} in ${fp} omitted]`);
11858
11823
  } else {
11859
11824
  final.push(line);
11860
11825
  }
@@ -11915,7 +11880,7 @@ var KtlintFilter = class _KtlintFilter extends ToolFilter {
11915
11880
  kept.push(line);
11916
11881
  } else {
11917
11882
  if (count === _KtlintFilter._KEEP_PER_RULE + 1) {
11918
- pendingPlaceholders.push({ index: kept.length, rule, suffix: "violations", indent: " " });
11883
+ pendingPlaceholders.push({ index: kept.length, rule, suffix: "violation", indent: " " });
11919
11884
  kept.push("");
11920
11885
  }
11921
11886
  deduplicated++;
@@ -11936,7 +11901,7 @@ var KtlintFilter = class _KtlintFilter extends ToolFilter {
11936
11901
  pendingPlaceholders.push({
11937
11902
  index: kept.length,
11938
11903
  rule,
11939
- suffix: severity === "warning" ? "warnings" : "violations",
11904
+ suffix: severity === "warning" ? "warning" : "violation",
11940
11905
  indent: ""
11941
11906
  });
11942
11907
  kept.push("");
@@ -11953,7 +11918,7 @@ var KtlintFilter = class _KtlintFilter extends ToolFilter {
11953
11918
  }
11954
11919
  for (const { index, rule, suffix, indent } of pendingPlaceholders) {
11955
11920
  const elided = (ruleCounts.get(rule) ?? 0) - _KtlintFilter._KEEP_PER_RULE;
11956
- kept[index] = `${indent}[token-goat: +${elided} more ${rule} ${suffix}; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`;
11921
+ kept[index] = `${indent}[token-goat: +${countNoun(elided, `more ${rule} ${suffix}`)}; disable via TOKEN_GOAT_BASH_COMPRESS for full list]`;
11957
11922
  }
11958
11923
  const notes = [];
11959
11924
  maybeNote(notes, deduplicated, `deduplicated ${deduplicated} repeated-rule violation lines`);
@@ -11977,7 +11942,7 @@ var swiftlintFilter = makeLinterFilter({
11977
11942
  return { severity: m[3].toLowerCase(), ruleId: m[5].toLowerCase() };
11978
11943
  },
11979
11944
  alwaysKeepSeverities: ["error", "serious"],
11980
- collapseNote: (ruleId, extra) => `[token-goat: +${extra} more ${ruleId} warning(s) elided]`
11945
+ collapseNote: (ruleId, extra) => `[token-goat: +${extra} more ${ruleId} ${extra === 1 ? "warning" : "warnings"} elided]`
11981
11946
  });
11982
11947
  var _PHPSTAN_SEP_RE = /^\s*-{3,}/;
11983
11948
  var _PHPSTAN_FILE_HEADER_RE = /^\s+Line\s+(\S.*\.php)\s*$/;
@@ -12009,7 +11974,7 @@ var PhpStanFilter = class extends ToolFilter {
12009
11974
  const msgs = fileMsgs.get(file);
12010
11975
  if (!msgs) return;
12011
11976
  const extraCount = [...msgs.values()].reduce((a, c) => a + Math.max(0, c - 3), 0);
12012
- if (extraCount) kept.push(` [token-goat: +${extraCount} more duplicate error(s) in ${file}]`);
11977
+ if (extraCount) kept.push(` [token-goat: +${countNoun(extraCount, "more duplicate error")} in ${file}]`);
12013
11978
  };
12014
11979
  for (const line of lines) {
12015
11980
  if (_PHPSTAN_INFO_RE.test(line)) {
@@ -12083,7 +12048,7 @@ var PhpStanFilter = class extends ToolFilter {
12083
12048
  maybeNote(notes, droppedProgress, `dropped ${droppedProgress} progress/info lines`);
12084
12049
  const collapsed = [...errorTypeCounts.entries()].filter(([, v]) => v > 3);
12085
12050
  for (const [etype, extra] of collapsed.sort()) {
12086
- notes.push(`collapsed +${extra - 3} more ${etype} occurrence(s)`);
12051
+ notes.push(`collapsed +${extra - 3} more ${etype} ${extra - 3 === 1 ? "occurrence" : "occurrences"}`);
12087
12052
  }
12088
12053
  this.emitNotes(kept, notes);
12089
12054
  return this.finalize(kept);
@@ -12660,16 +12625,14 @@ var PipFilter = class extends ToolFilter {
12660
12625
  kept.push(line);
12661
12626
  }
12662
12627
  const notes = [];
12663
- if (collects > 5) notes.push(`+${collects - 5} more 'Collecting' lines elided`);
12628
+ if (collects > 5) notes.push(`+${countNoun(collects - 5, "more 'Collecting' line")} elided`);
12664
12629
  maybeNote(notes, downloads, `dropped ${downloads} download/cache-hit lines`);
12665
12630
  maybeNote(notes, buildNoise, `dropped ${buildNoise} build-wheel/metadata lines`);
12666
12631
  maybeNote(notes, verboseDropped, `dropped ${verboseDropped} verbose debug/trace lines`);
12667
12632
  this.emitNotes(kept, notes);
12668
12633
  return this.finalize(kept);
12669
12634
  }
12670
- // `pip list`/`pip freeze` output has no install noise to strip (just one
12671
- // package per line) but can still run to hundreds of lines; cap it the same
12672
- // way UvFilter._compressFreezeList caps `uv pip list`/`uv pip freeze`.
12635
+ // `pip list`/`pip freeze` output has no install noise to strip (just one package per line) but can still run to hundreds of lines; cap it the same way UvFilter._compressFreezeList caps `uv pip list`/`uv pip freeze`.
12673
12636
  _compressFreezeList(text) {
12674
12637
  return compressFreezeList(text);
12675
12638
  }
@@ -12805,7 +12768,7 @@ var CondaFilter = class extends ToolFilter {
12805
12768
  if (pkgLines.length <= CONDA_LIST_THRESHOLD) return text.trimEnd();
12806
12769
  const shown = [...header, ...pkgLines.slice(0, CONDA_LIST_SHOW)];
12807
12770
  const remaining = pkgLines.length - CONDA_LIST_SHOW;
12808
- shown.push(`[token-goat: ${remaining} more packages elided; run conda list for full output]`);
12771
+ shown.push(`[token-goat: ${countNoun(remaining, "more package")} elided; run conda list for full output]`);
12809
12772
  return shown.join("\n");
12810
12773
  }
12811
12774
  _compressEnvExport(text) {
@@ -12831,7 +12794,7 @@ var CondaFilter = class extends ToolFilter {
12831
12794
  const result = [
12832
12795
  ...otherLines.slice(0, insertAt),
12833
12796
  ...depLines.slice(0, CONDA_LIST_SHOW),
12834
- ` # [token-goat: ${remaining} more dependencies elided]`,
12797
+ ` # [token-goat: ${countNoun(remaining, "more dependency", "more dependencies")} elided]`,
12835
12798
  ...otherLines.slice(insertAt)
12836
12799
  ];
12837
12800
  return result.join("\n");
@@ -13285,16 +13248,16 @@ var DepListFilter = class _DepListFilter extends ToolFilter {
13285
13248
  const nMore = lines.length - DEP_LIST_THRESHOLD;
13286
13249
  const shown = lines.slice(0, DEP_LIST_THRESHOLD);
13287
13250
  const hint = this._depCmdHint(argv);
13288
- const trailer = `...[${nMore} more packages \u2014 use '${hint}' to see full output]`;
13251
+ const trailer = `...[${countNoun(nMore, "more package")} \u2014 use ${hint} to see full output]`;
13289
13252
  return shown.join("\n") + "\n" + trailer;
13290
13253
  }
13291
13254
  _depCmdHint(argv) {
13292
13255
  if (argv.length === 0) return "the original command";
13293
13256
  const stem = pathStem(argv[0]).toLowerCase();
13294
13257
  const pos = positionalArgs(argv.slice(1));
13295
- if (stem === "uv" && pos.length >= 2) return `uv ${pos[0]} ${pos[1]}`;
13258
+ if (stem === "uv" && pos.length >= 2) return fencedCommand(`uv ${pos[0]} ${pos[1]}`);
13296
13259
  const subcmd = pos[0] ?? "";
13297
- return `${stem} ${subcmd}`.trim();
13260
+ return fencedCommand(`${stem} ${subcmd}`.trim());
13298
13261
  }
13299
13262
  };
13300
13263
  var PACKAGE_MANAGER_FILTERS = [
@@ -13528,25 +13491,12 @@ var jestFilter = makeNodeTestRunnerFilter({
13528
13491
  var vitestFilter = makeNodeTestRunnerFilter({
13529
13492
  name: "vitest",
13530
13493
  binaries: ["vitest"],
13531
- // File-level pass header carries a duration: `✓ src/x.test.ts (12ms)`. The ` > ` exclusion is
13532
- // not decoration: a verbose per-test line whose test NAME happens to end in something shaped
13533
- // like a duration (`... to the documented min (100ms) 1ms`) is otherwise claimed here, and gets
13534
- // counted as a passing FILE. Both branches drop the line, so nothing is lost -- but the reported
13535
- // count is wrong, and wrong in a way that depends on which reporter produced the run.
13494
+ // File-level pass header carries a duration: `✓ src/x.test.ts (12ms)`. The ` > ` exclusion is not decoration: a verbose per-test line whose test NAME happens to end in something shaped like a duration (`... to the documented min (100ms) 1ms`) is otherwise claimed here, and gets counted as a passing FILE. Both branches drop the line, so nothing is lost -- but the reported count is wrong, and wrong in a way that depends on which reporter produced the run.
13536
13495
  passFileRe: /^\s*✓(?!.*\s>\s)\s+\S.*\([\d.]+\s*[a-zA-Z]\w*\)/,
13537
13496
  failFileRe: /^\s*(?:×|FAIL|✗|✘)\s+\S/,
13538
- // Real vitest right-aligns its summary labels, so every one of these arrives indented:
13539
- // ` Tests 140 passed (140)`. Anchoring at `^` matched none of them. They survived only
13540
- // by falling through to the keep-verbatim branch, which is not the same rule -- a summary line
13541
- // reached while a fail block is open was swallowed into that block instead of ending it.
13497
+ // Real vitest right-aligns its summary labels, so every one of these arrives indented: ` Tests 140 passed (140)`. Anchoring at `^` matched none of them. They survived only by falling through to the keep-verbatim branch, which is not the same rule -- a summary line reached while a fail block is open was swallowed into that block instead of ending it.
13542
13498
  summaryRe: /^\s*(Test Files|Tests|Modules|Duration|Start at)[\s:]+\d/,
13543
- // Two shapes, because the reporter decides which one a passing test prints. The default
13544
- // reporter indents a per-test tick under its file header (` ✓ should pass`). `--reporter=
13545
- // verbose` prints one line per test at a single leading space, carrying the full
13546
- // `file > describe > name` path instead: ` ✓ tests/x.test.ts > safeJoin > joins normally 28ms`.
13547
- // Those lines match no rule at all today, so a verbose run compresses by nothing. The ` > `
13548
- // is what separates them from a file-level header, which passFileRe claims first and which
13549
- // never contains one.
13499
+ // Two shapes, because the reporter decides which one a passing test prints. The default reporter indents a per-test tick under its file header (` ✓ should pass`). `--reporter= verbose` prints one line per test at a single leading space, carrying the full `file > describe > name` path instead: ` ✓ tests/x.test.ts > safeJoin > joins normally 28ms`. Those lines match no rule at all today, so a verbose run compresses by nothing. The ` > ` is what separates them from a file-level header, which passFileRe claims first and which never contains one.
13550
13500
  testTickRe: /^\s{2,}✓\s|^\s*✓\s.*\s>\s/,
13551
13501
  outputHdrRe: /^\s*stdout\s*\|/,
13552
13502
  outputNoun: "stdout",