token-goat 2.9.6 → 2.9.9

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.
@@ -18,9 +18,10 @@ import {
18
18
  storeWebOutput,
19
19
  summarizeResidentContext,
20
20
  taskListPruneHint
21
- } from "./token-goat-chunk-7GJBID7S.mjs";
21
+ } from "./token-goat-chunk-QCHD2ENV.mjs";
22
22
  import {
23
23
  BASH_OUTPUT_SUBDIR,
24
+ GENERIC_SERVED_OUTPUT_KEY,
24
25
  OUTLINE_MAX_REPLACEMENT_RATIO,
25
26
  OUTLINE_MIN_HEADINGS,
26
27
  WEB_FETCH_KEY_SEP,
@@ -124,12 +125,12 @@ import {
124
125
  wasCliReadThisSession,
125
126
  wasFileReadThisSession,
126
127
  wasHintShown
127
- } from "./token-goat-chunk-3HJQR4OO.mjs";
128
+ } from "./token-goat-chunk-BYDDWQ2T.mjs";
128
129
  import {
129
130
  bashOutputCapBytes,
130
131
  canRunWrappedShell,
131
132
  deliveredOutputBytes
132
- } from "./token-goat-chunk-LXIC7MTW.mjs";
133
+ } from "./token-goat-chunk-OBDTBOQA.mjs";
133
134
  import {
134
135
  BODY_FIRST_TOOL_RESPONSE_KEYS,
135
136
  ENV_KEYS,
@@ -168,11 +169,13 @@ import {
168
169
  getToolName,
169
170
  globalDbPath,
170
171
  hasBareBackgroundOrNewline,
172
+ hasUnquotedOperator,
171
173
  isMcpErrorResponse,
172
174
  isRewriteWorthwhile,
173
175
  isUnderSystemTemp,
174
176
  loadConfig,
175
177
  makeDedupHintHandlers,
178
+ neutralizeSpokenMarkers,
176
179
  normalizePath,
177
180
  passOutput,
178
181
  recordStat,
@@ -191,7 +194,7 @@ import {
191
194
  stripAnsiEscapes,
192
195
  toDisplayPath,
193
196
  toKB
194
- } from "./token-goat-chunk-T2OE7MYM.mjs";
197
+ } from "./token-goat-chunk-KIEOFWLL.mjs";
195
198
  import {
196
199
  init_define_import_meta_env
197
200
  } from "./token-goat-chunk-A37V4PBF.mjs";
@@ -352,17 +355,66 @@ function foldGrepContentHandler(event) {
352
355
  return passOutput();
353
356
  }
354
357
  }
358
+ var DOC_EXT_RE = /\.(?:md|mdx|rst|txt)$/i;
359
+ var SOURCE_EXT_RE = /\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|css|scss|sass|less)$/i;
360
+ var STRUCTURAL_DOC_PATTERN_RE = /^(?:\^)?#+\s*/;
361
+ var STRUCTURAL_SOURCE_PATTERN_RE = /^(?:\^|\s)*(?:def|class|function|async\s+def|async\s+function|export\s+(?:default\s+)?(?:class|function|interface|type|const|enum)|func|fn|struct|interface|impl|type)\b/i;
362
+ function extractGrepStructuralSearch(toolInput) {
363
+ const pattern = typeof toolInput["pattern"] === "string" ? toolInput["pattern"].trim() : "";
364
+ if (!pattern) return null;
365
+ let rawPath = null;
366
+ if (typeof toolInput["path"] === "string" && toolInput["path"].trim() !== "") {
367
+ rawPath = toolInput["path"].trim();
368
+ } else if (typeof toolInput["paths"] === "string" && toolInput["paths"].trim() !== "") {
369
+ rawPath = toolInput["paths"].trim();
370
+ } else if (Array.isArray(toolInput["paths"]) && toolInput["paths"].length === 1 && typeof toolInput["paths"][0] === "string") {
371
+ rawPath = toolInput["paths"][0].trim();
372
+ }
373
+ if (!rawPath) return null;
374
+ if (/[*?[{]/.test(rawPath)) return null;
375
+ if (DOC_EXT_RE.test(rawPath) && STRUCTURAL_DOC_PATTERN_RE.test(pattern)) {
376
+ return { filePath: rawPath, isDoc: true, isSource: false };
377
+ }
378
+ if (SOURCE_EXT_RE.test(rawPath) && STRUCTURAL_SOURCE_PATTERN_RE.test(pattern)) {
379
+ return { filePath: rawPath, isDoc: false, isSource: true };
380
+ }
381
+ return null;
382
+ }
383
+ function preGrepHandler(event) {
384
+ try {
385
+ if (getToolName(event) !== "Grep") return passOutput();
386
+ const toolInput = getToolInput(event);
387
+ const structSearch = extractGrepStructuralSearch(toolInput);
388
+ if (structSearch !== null) {
389
+ recordStat("session_hint", 0, 0);
390
+ const { isDoc } = structSearch;
391
+ const filePath = displaySafePath(structSearch.filePath);
392
+ const hint = isDoc ? 'Scanning a document for headings loads large match output. Use `token-goat section "' + filePath + '::SectionHeading"` to read one section or `token-goat outline "' + filePath + '"` to see the document outline.' : 'Scanning a source file for symbols loads large match output. Use `token-goat skeleton "' + filePath + '"` to see the file structure or `token-goat read "' + filePath + '::SymbolName"` to inspect a specific symbol.';
393
+ return contextOutput(hint);
394
+ }
395
+ return preGrepDedupHandler(event);
396
+ } catch {
397
+ return passOutput();
398
+ }
399
+ }
355
400
  function postGrepHandler(event) {
356
401
  const dedupResult = dedupPostHandler(event);
357
402
  const foldResult = foldGrepContentHandler(event);
358
403
  if (foldResult.hookType === "rewriteOutput") return foldResult;
359
404
  return dedupResult;
360
405
  }
361
- registerHook("pre_tool_use", preGrepDedupHandler, { toolName: "Grep" });
406
+ registerHook("pre_tool_use", preGrepHandler, { toolName: "Grep" });
362
407
  registerHook("post_tool_use", postGrepHandler, { toolName: "Grep" });
363
408
 
364
409
  // src/hooks_glob.ts
365
410
  init_define_import_meta_env();
411
+ function isBroadCatchAllGlob(pattern, pathArg) {
412
+ const p = pattern.trim();
413
+ const isBroad = p === "*" || p === "**/*" || p === "**" || p === "*.*" || p === "**/*.*" || p === "**/*.**";
414
+ if (!isBroad) return false;
415
+ const target = (pathArg ?? "").trim().replace(/[\\/]+$/, "");
416
+ return target === "" || target === "." || target === "./" || target === ".\\" || !target.includes("/") && !target.includes("\\");
417
+ }
366
418
  function globSignature(toolInput) {
367
419
  const pattern = toolInput["pattern"];
368
420
  if (typeof pattern !== "string" || pattern === "") return null;
@@ -377,7 +429,24 @@ var { post: postGlobHandler, pre: preGlobDedupHandler } = makeDedupHintHandlers(
377
429
  minMatchesConfigKey: "glob_dedup_min_matches",
378
430
  statName: "glob_dedup_hint"
379
431
  });
380
- registerHook("pre_tool_use", preGlobDedupHandler, { toolName: "Glob" });
432
+ function preGlobHandler(event) {
433
+ try {
434
+ if (getToolName(event) !== "Glob") return passOutput();
435
+ const toolInput = getToolInput(event);
436
+ const pattern = typeof toolInput["pattern"] === "string" ? toolInput["pattern"] : "";
437
+ const pathArg = typeof toolInput["path"] === "string" ? toolInput["path"] : typeof toolInput["paths"] === "string" ? toolInput["paths"] : Array.isArray(toolInput["paths"]) && toolInput["paths"].length === 1 && typeof toolInput["paths"][0] === "string" ? toolInput["paths"][0] : void 0;
438
+ if (pattern && isBroadCatchAllGlob(pattern, pathArg)) {
439
+ recordStat("session_hint", 0, 0);
440
+ return contextOutput(
441
+ 'Broad recursive glob pattern "' + displaySafeText(pattern) + '" traverses entire directory trees and may dump thousands of paths into context. Use `token-goat map --compact` to inspect project directory structure efficiently, or narrow the glob path.'
442
+ );
443
+ }
444
+ return preGlobDedupHandler(event);
445
+ } catch {
446
+ return passOutput();
447
+ }
448
+ }
449
+ registerHook("pre_tool_use", preGlobHandler, { toolName: "Glob" });
381
450
  registerHook("post_tool_use", postGlobHandler, { toolName: "Glob" });
382
451
 
383
452
  // src/hooks_edit.ts
@@ -567,8 +636,8 @@ function buildManifest(sessionId, cwd) {
567
636
  "### Web URLs fetched",
568
637
  webFetches.map(([key, cacheId]) => {
569
638
  const [url = key, prompt = ""] = key.split(WEB_FETCH_KEY_SEP);
570
- const promptSuffix = prompt ? `, prompt: ${JSON.stringify(prompt)}` : "";
571
- return `- ${url} (cacheId: ${cacheId}${promptSuffix})`;
639
+ const promptSuffix = prompt ? `, prompt: ${neutralizeSpokenMarkers(JSON.stringify(prompt))}` : "";
640
+ return `- ${displaySafeText(url)} (cacheId: ${cacheId}${promptSuffix})`;
572
641
  }),
573
642
  MAX_ROWS
574
643
  );
@@ -673,15 +742,20 @@ function buildMemEpochSection() {
673
742
  ];
674
743
  }
675
744
  var MANIFEST_PREAMBLE = "When summarizing this session, keep the file paths and symbol names below exactly as written -- they are the handles the next turn needs to resume work. Do not paraphrase them into prose.";
745
+ var BUDGET_ESCALATION_MARKER = "TG-BUDGET-ESCALATION:";
746
+ function summaryBudgetDirective(budgetChars) {
747
+ if (budgetChars <= 0) return "";
748
+ return ` Aim for at most ${budgetChars} characters. Prefer a shorter summary that keeps every path, symbol, command and decision over a longer one that reproduces tool output verbatim: cite the recall id (\`token-goat bash-output <id> --full\`) rather than quoting the output again. If this session genuinely cannot be summarized within that target without losing state the next turn needs, exceed it and open the summary with a single line reading \`${BUDGET_ESCALATION_MARKER} <one-sentence reason>\`.`;
749
+ }
676
750
  function preCompactHandler(event) {
677
- const out = loadConfig().compact_assist.enabled ? contextOutput(`${MANIFEST_PREAMBLE}
751
+ const out = loadConfig().compact_assist.enabled ? contextOutput(`${MANIFEST_PREAMBLE}${summaryBudgetDirective(loadConfig().compact_assist.summary_budget_chars)}
678
752
 
679
753
  ${buildManifest(event.sessionId, getCwd(event))}`) : passOutput();
680
754
  markCompacted();
681
755
  return out;
682
756
  }
683
757
  registerHook("pre_compact", preCompactHandler);
684
- var MANIFEST_SURVIVAL_SAMPLE = 12;
758
+ var MANIFEST_SURVIVAL_SAMPLE = 64;
685
759
  function manifestPathSample(sessionId) {
686
760
  const ownFiles = [...getSessionFiles().values()];
687
761
  const siblingFiles = sessionId !== void 0 ? listSiblingSessionStates(sessionId).flatMap((s) => s.files) : [];
@@ -701,12 +775,15 @@ function postCompactHandler(event) {
701
775
  const haystack = foldPath(summary);
702
776
  const survived = sample.filter((p) => haystack.includes(foldPath(p))).length;
703
777
  const trigger = typeof event.raw["trigger"] === "string" ? event.raw["trigger"] : "unknown";
778
+ const budget = loadConfig().compact_assist.summary_budget_chars;
779
+ const over = budget > 0 && summary.length > budget ? 1 : 0;
780
+ const escalated = summary.includes(BUDGET_ESCALATION_MARKER) ? 1 : 0;
704
781
  recordStat(
705
782
  "compact_summary",
706
783
  0,
707
784
  0,
708
785
  void 0,
709
- `trigger=${trigger} bytes=${bytes} est_tokens=${savedTokensFromBytes(bytes)} manifest_paths=${survived}/${sample.length}`
786
+ `trigger=${trigger} bytes=${bytes} est_tokens=${savedTokensFromBytes(bytes)} manifest_paths=${survived}/${sample.length} budget=${budget} over=${over} escalated=${escalated}`
710
787
  );
711
788
  return passOutput();
712
789
  }
@@ -721,7 +798,7 @@ init_define_import_meta_env();
721
798
  var TRACKED_SKILL = "token-goat";
722
799
  var MAX_COMMANDS_SHOWN = 8;
723
800
  async function currentCommandNames() {
724
- const { buildProgram } = await import("./token-goat-chunk-3ZAMYHQG.mjs");
801
+ const { buildProgram } = await import("./token-goat-chunk-DRH4CVGF.mjs");
725
802
  return flattenCommandNames(buildCommandManifest(buildProgram()));
726
803
  }
727
804
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -7321,6 +7398,16 @@ function resolveSkillContext(event) {
7321
7398
  }
7322
7399
  return { skillName };
7323
7400
  }
7401
+ function planHeadingTree(body, bodyBytes, skillName) {
7402
+ const headings = extractMarkdownHeadings(body);
7403
+ if (headings.length < OUTLINE_MIN_HEADINGS) return null;
7404
+ const { sectionsList } = formatHeadingTreeParts(headings, skillName);
7405
+ const treeBytes = Buffer.byteLength(sectionsList, "utf-8");
7406
+ if (treeBytes > bodyBytes * OUTLINE_MAX_REPLACEMENT_RATIO) return null;
7407
+ const totalHeadings = extractMarkdownHeadings(body, Number.MAX_SAFE_INTEGER).length;
7408
+ const phrase = totalHeadings > headings.length ? "its heading tree shows " + headings.length + " of " + totalHeadings + " headings below instead of the full body; the remaining sections are reachable only through `token-goat skill-body " + skillName + "`." : "its heading tree (" + headings.length + " headings) is inlined below instead of the full body.";
7409
+ return { sectionsList, treeBytes, phrase };
7410
+ }
7324
7411
  async function preSkillHandler(event) {
7325
7412
  try {
7326
7413
  const ctx = resolveSkillContext(event);
@@ -7334,7 +7421,7 @@ async function preSkillHandler(event) {
7334
7421
  if (await hasSessionOutput(event.sessionId, skillName)) {
7335
7422
  const cachedBytes = await sessionOutputBodyBytes(event.sessionId, skillName);
7336
7423
  const denyCredit = cachedBytes !== null ? Math.min(cachedBytes, PER_FILE_COUNTERFACTUAL_CEILING) : 0;
7337
- recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit));
7424
+ recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit), void 0, "skill-reload-deny");
7338
7425
  return denyOutput(
7339
7426
  "Skill `" + skillName + "` was already loaded this session and is cached. Use `token-goat skill-section " + skillName + " '<heading>'` to recall a section, `token-goat skill-body " + skillName + " --compact` to recall the compact slice, or `token-goat skill-body " + skillName + "` for the full body instead of re-loading it."
7340
7427
  );
@@ -7344,35 +7431,33 @@ async function preSkillHandler(event) {
7344
7431
  try {
7345
7432
  const body = await readFile(sourcePath, "utf-8");
7346
7433
  const bodyBytes = Buffer.byteLength(body, "utf-8");
7347
- const compact = bodyBytes > OVERSIZED_FIRST_LOAD_THRESHOLD_BYTES ? extractCompactFromMarker(body) : null;
7348
- if (compact !== null) {
7349
- const compactBytes = Buffer.byteLength(compact, "utf-8");
7350
- if (compactBytes * 2 <= bodyBytes && compactBytes <= COMPACT_INLINE_MAX_BYTES) {
7351
- const savedBytes = bodyBytes - compactBytes;
7352
- recordStat("skill_compact_inlined", savedBytes, savedTokensFromBytes(savedBytes));
7353
- return denyOutput(
7354
- "Skill `" + skillName + "` is large (" + bodyBytes + " bytes); its compact slice (" + compactBytes + " bytes) is inlined below instead of the full body. For a specific section, run `token-goat skill-section " + skillName + " '<heading>'`, or `token-goat skill-body " + skillName + "` if you need the full body.\n\n" + compact
7355
- );
7356
- }
7357
- recordStat("skill_oversized_first_load");
7358
- return denyOutput(
7359
- "Skill `" + skillName + "` is large (" + bodyBytes + " bytes) and has a compact slice available. Use `token-goat skill-section " + skillName + " '<heading>'` to load a specific section, `token-goat skill-body " + skillName + " --compact` to load the compact slice, or `token-goat skill-body " + skillName + "` for the full body."
7360
- );
7361
- } else if (bodyBytes > OVERSIZED_FIRST_LOAD_THRESHOLD_BYTES) {
7362
- const headings = extractMarkdownHeadings(body);
7363
- if (headings.length >= OUTLINE_MIN_HEADINGS) {
7364
- const { sectionsList } = formatHeadingTreeParts(headings, skillName);
7365
- const treeBytes = Buffer.byteLength(sectionsList, "utf-8");
7366
- if (treeBytes <= bodyBytes * OUTLINE_MAX_REPLACEMENT_RATIO) {
7367
- const savedBytes = bodyBytes - treeBytes;
7368
- recordStat("skill_heading_tree_inlined", savedBytes, savedTokensFromBytes(savedBytes));
7369
- const totalHeadings = extractMarkdownHeadings(body, Number.MAX_SAFE_INTEGER).length;
7370
- const headingCountPhrase = totalHeadings > headings.length ? "its heading tree shows " + headings.length + " of " + totalHeadings + " headings below instead of the full body; the remaining sections are reachable only through `token-goat skill-body " + skillName + "`." : "its heading tree (" + headings.length + " headings) is inlined below instead of the full body.";
7434
+ if (bodyBytes > OVERSIZED_FIRST_LOAD_THRESHOLD_BYTES) {
7435
+ const compact = extractCompactFromMarker(body);
7436
+ if (compact !== null) {
7437
+ const compactBytes = Buffer.byteLength(compact, "utf-8");
7438
+ if (compactBytes * 2 <= bodyBytes && compactBytes <= COMPACT_INLINE_MAX_BYTES) {
7439
+ const savedBytes = bodyBytes - compactBytes;
7440
+ recordStat("skill_compact_inlined", savedBytes, savedTokensFromBytes(savedBytes));
7371
7441
  return denyOutput(
7372
- "Skill `" + skillName + "` is large (" + bodyBytes + " bytes) with no compact slice; " + headingCountPhrase + " Use `token-goat skill-section " + skillName + " '<heading>'` to load a specific section, or `token-goat skill-body " + skillName + "` for the full body.\n\n" + fenceUntrustedFileContent(sectionsList)
7442
+ "Skill `" + skillName + "` is large (" + bodyBytes + " bytes); its compact slice (" + compactBytes + " bytes) is inlined below instead of the full body. For a specific section, run `token-goat skill-section " + skillName + " '<heading>'`, or `token-goat skill-body " + skillName + "` if you need the full body.\n\n" + compact
7373
7443
  );
7374
7444
  }
7375
7445
  }
7446
+ const tree = planHeadingTree(body, bodyBytes, skillName);
7447
+ if (tree !== null) {
7448
+ const replaced = compact !== null ? Buffer.byteLength(compact, "utf-8") : bodyBytes;
7449
+ const savedBytes = Math.max(0, replaced - tree.treeBytes);
7450
+ recordStat("skill_heading_tree_inlined", savedBytes, savedTokensFromBytes(savedBytes), void 0, "skill=" + skillName + " marker=" + (compact !== null ? "unusable" : "none"));
7451
+ return denyOutput(
7452
+ "Skill `" + skillName + "` is large (" + bodyBytes + " bytes)" + (compact !== null ? ", and its compact slice is too large to inline; " : " with no compact slice; ") + tree.phrase + " Use `token-goat skill-section " + skillName + " '<heading>'` to load a specific section" + (compact !== null ? ", `token-goat skill-body " + skillName + " --compact` to load the compact slice" : "") + ", or `token-goat skill-body " + skillName + "` for the full body.\n\n" + fenceUntrustedFileContent(tree.sectionsList)
7453
+ );
7454
+ }
7455
+ if (compact !== null) {
7456
+ recordStat("skill_oversized_first_load");
7457
+ return denyOutput(
7458
+ "Skill `" + skillName + "` is large (" + bodyBytes + " bytes) and has a compact slice available. Use `token-goat skill-section " + skillName + " '<heading>'` to load a specific section, `token-goat skill-body " + skillName + " --compact` to load the compact slice, or `token-goat skill-body " + skillName + "` for the full body."
7459
+ );
7460
+ }
7376
7461
  }
7377
7462
  } catch {
7378
7463
  }
@@ -7743,6 +7828,7 @@ function extractCatFilesMulti(cmd) {
7743
7828
  }
7744
7829
  var POWERSHELL_WRAP_RE = /^(?:powershell|pwsh)(?:\.exe)?(?:\s+-[a-zA-Z]+(?:\s+\S+)?)*\s+(?:-Command|-c|-EncodedCommand)\s+(?:"([^"]*)"|'([^']*)')\s*$/i;
7745
7830
  var PS_GETCONTENT_INNER_RE = /^(?:Get-Content|gc|cat|type)(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+?))(?:\s+-[a-zA-Z].*)?\s*$/i;
7831
+ var PS_FILE_METHOD_RE = /\[(?:System\.)?IO\.File\]::(?:ReadAllText|ReadAllLines|ReadAllBytes|ReadLines|OpenText)\(\s*['"]([^'"]+)['"]/i;
7746
7832
  var PS_TEMP_READ_FLOOD_BYTES = 16 * 1024;
7747
7833
  function isLargeFileOnDisk(filePath, floor) {
7748
7834
  try {
@@ -7765,6 +7851,24 @@ function extractPowerShellWrappedGetContent(cmd) {
7765
7851
  if (isTempPath(filePath) && !isLargeFileOnDisk(filePath, PS_TEMP_READ_FLOOD_BYTES)) return null;
7766
7852
  return { filePath, ...flags };
7767
7853
  }
7854
+ function extractPowerShellFileMethodRead(cmd) {
7855
+ let inner = cmd.trim();
7856
+ const w = POWERSHELL_WRAP_RE.exec(inner);
7857
+ if (w) {
7858
+ inner = (w[1] ?? w[2] ?? "").trim();
7859
+ }
7860
+ const m = PS_FILE_METHOD_RE.exec(inner);
7861
+ if (!m?.[1]) return null;
7862
+ const filePath = m[1];
7863
+ if (isOrchestratorStateFile(filePath)) return null;
7864
+ if (isTempPath(filePath) && !isLargeFileOnDisk(filePath, PS_TEMP_READ_FLOOD_BYTES)) return null;
7865
+ const flags = classifyFileExtensions(filePath);
7866
+ if (flags === null) {
7867
+ const { isDoc, isConfig, isSql } = classifyDocConfig(filePath);
7868
+ return { filePath, isDoc, isConfig, isSql };
7869
+ }
7870
+ return { filePath, isDoc: flags.isDoc, isConfig: flags.isConfig, isSql: flags.isSql };
7871
+ }
7768
7872
  function extractRgSymbolSearch(cmd) {
7769
7873
  if (!/^(?:rg|grep)\s+/.test(cmd)) return null;
7770
7874
  if (!/-n\b/.test(cmd)) return null;
@@ -7918,57 +8022,75 @@ function pythonOpenPathsAreAllLiteral(text) {
7918
8022
  const calls = pythonOpenCalls(text);
7919
8023
  return calls.length > 0 && calls.every((call) => call.pathLiteral !== null);
7920
8024
  }
8025
+ var KNOWN_PYTHON_EXT_STR = "java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|css|scss|sass|less|md|mdx|rst|txt|json|yaml|yml|toml|xml|html|htm|conf|cfg|ini|properties|sql|ps1|psm1|env";
8026
+ var OPEN_EXT = new RegExp(`\\.(?:${KNOWN_PYTHON_EXT_STR})$`, "i");
7921
8027
  function extractPythonFileRead(cmd) {
7922
- if (!/^python3?\b/.test(cmd)) return null;
7923
- if (pythonOpenWritesAFile(cmd) || pythonWritesThroughFileObject(cmd)) return null;
7924
- const outputOpen = /open\s*\(\s*r?['"]([^'"]+\.output)['"]/i.exec(cmd);
8028
+ let inner = cmd.trim();
8029
+ const w = POWERSHELL_WRAP_RE.exec(inner);
8030
+ if (w) {
8031
+ inner = (w[1] ?? w[2] ?? "").trim();
8032
+ }
8033
+ const psHereMatch = /^@'([\s\S]*?)'@\s*\|\s*(?:python3?|py)(?:\.exe)?(?:\s+-\S*|\s+-)?\s*$/i.exec(inner) ?? /^@"([\s\S]*?)"@\s*\|\s*(?:python3?|py)(?:\.exe)?(?:\s+-\S*|\s+-)?\s*$/i.exec(inner);
8034
+ let pythonBody = null;
8035
+ if (psHereMatch) {
8036
+ pythonBody = (psHereMatch[1] ?? "").trim();
8037
+ } else if (/^(?:python3?|py)(?:\.exe)?\b/i.test(inner)) {
8038
+ pythonBody = inner;
8039
+ }
8040
+ if (pythonBody === null) return null;
8041
+ if (pythonOpenWritesAFile(pythonBody) || pythonWritesThroughFileObject(pythonBody)) return null;
8042
+ const outputOpen = /open\s*\(\s*r?['"]([^'"]+\.output)['"]/i.exec(pythonBody);
7925
8043
  if (outputOpen?.[1]) {
7926
8044
  const filePath = outputOpen[1];
7927
8045
  if (isOrchestratorStateFile(filePath)) return null;
7928
- return { filePath, isDoc: false, isOutputFile: true };
8046
+ return { filePath, isDoc: false, isConfig: false, isEnv: false, isSql: false, isOutputFile: true };
7929
8047
  }
7930
- const OPEN_EXT = /\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties|ps1|psm1)/i;
7931
- const heredocMatch = /^python3?\s+-\s+<<\s*'?(\w+)'?\s*\n([\s\S]*?)\n\1\s*$/.exec(cmd);
8048
+ const classifyResult = (filePath) => {
8049
+ const flags = classifyFileExtensions(filePath);
8050
+ if (flags !== null) {
8051
+ return { filePath, isDoc: flags.isDoc, isConfig: flags.isConfig, isEnv: flags.isEnv, isSql: flags.isSql, isOutputFile: false };
8052
+ }
8053
+ const { isDoc, isConfig, isSql } = classifyDocConfig(filePath);
8054
+ const isEnv = /\.env(\.\w+)?$/i.test(filePath);
8055
+ return { filePath, isDoc, isConfig, isEnv, isSql, isOutputFile: false };
8056
+ };
8057
+ const heredocMatch = /^python3?\s+-\s+<<\s*'?(\w+)'?\s*\n([\s\S]*?)\n\1\s*$/.exec(pythonBody);
7932
8058
  if (heredocMatch) {
7933
8059
  const body = heredocMatch[2] ?? "";
7934
8060
  if (pythonOpenWritesAFile(body) || pythonWritesThroughFileObject(body)) return null;
7935
- const heredocOpen = /open\s*\(\s*r?['"]([^'"]+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties))['"]/i.exec(body);
8061
+ const heredocOpen = new RegExp(`open\\s*\\(\\s*r?['"]([^'"]+\\.(?:${KNOWN_PYTHON_EXT_STR}))['"]`, "i").exec(body);
7936
8062
  if (heredocOpen?.[1]) {
7937
8063
  const filePath = heredocOpen[1];
7938
8064
  if (isOrchestratorStateFile(filePath)) return null;
7939
- const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7940
- return { filePath, isDoc, isOutputFile: false };
8065
+ return classifyResult(filePath);
7941
8066
  }
7942
8067
  if (/open\s*\(/.test(body) && !pythonOpenPathsAreAllLiteral(body)) {
7943
- const literal2 = /['"]([^'"]+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties))['"]/i.exec(body);
8068
+ const literal2 = new RegExp(`['"]([^'"]+\\.(?:${KNOWN_PYTHON_EXT_STR}))['"]`, "i").exec(body);
7944
8069
  if (literal2?.[1]) {
7945
8070
  const filePath = literal2[1];
7946
8071
  if (isOrchestratorStateFile(filePath)) return null;
7947
8072
  if (OPEN_EXT.test(filePath)) {
7948
- const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7949
- return { filePath, isDoc, isOutputFile: false };
8073
+ return classifyResult(filePath);
7950
8074
  }
7951
8075
  }
7952
8076
  }
7953
8077
  return null;
7954
8078
  }
7955
- const direct = /open\(['"]([^'"]+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties))['"]/i.exec(cmd);
8079
+ const direct = new RegExp(`open\\s*\\(\\s*r?['"]([^'"]+\\.(?:${KNOWN_PYTHON_EXT_STR}))['"]`, "i").exec(pythonBody);
7956
8080
  if (direct) {
7957
8081
  const filePath = direct[1] ?? "";
7958
8082
  if (!filePath) return null;
7959
8083
  if (isOrchestratorStateFile(filePath)) return null;
7960
- const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7961
- return { filePath, isDoc, isOutputFile: false };
8084
+ return classifyResult(filePath);
7962
8085
  }
7963
- if (/open\s*\(/.test(cmd) && !pythonOpenPathsAreAllLiteral(cmd)) {
7964
- const literal2 = /['"]([^'"]+\.(?:java|py|ts|tsx|js|jsx|go|rb|rs|cpp|cc|cxx|c|h|hpp|kt|swift|cs|php|scala|clj|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties))['"]/i.exec(cmd);
8086
+ if (/open\s*\(/.test(pythonBody) && !pythonOpenPathsAreAllLiteral(pythonBody)) {
8087
+ const literal2 = new RegExp(`['"]([^'"]+\\.(?:${KNOWN_PYTHON_EXT_STR}))['"]`, "i").exec(pythonBody);
7965
8088
  if (literal2) {
7966
8089
  const filePath = literal2[1] ?? "";
7967
8090
  if (filePath) {
7968
8091
  if (isOrchestratorStateFile(filePath)) return null;
7969
8092
  if (OPEN_EXT.test(filePath)) {
7970
- const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7971
- return { filePath, isDoc, isOutputFile: false };
8093
+ return classifyResult(filePath);
7972
8094
  }
7973
8095
  }
7974
8096
  }
@@ -8291,7 +8413,7 @@ function extractToolResultsFile(cmd) {
8291
8413
  return null;
8292
8414
  }
8293
8415
  function extractDirectoryListing(cmd) {
8294
- return /^eza\s+.*--long\s+\S+/.test(cmd) || /^eza\s+.*--tree/.test(cmd) || /^tree(\s|$)/.test(cmd) || /^ls\s+(?:\S+\s+)*-[a-zA-Z]*R[a-zA-Z]*(?:\s|$)/.test(cmd) || /^ls\s+(?:-[la]+\s+)?(\S+)\s*[|]\s*head/.test(cmd) || /^ls\s+(?:-[la]+\s+)?(\S+)\s*[|]\s*grep/.test(cmd) || /^ls\s+(?:-[la]+\s+)?(\S+)\s*[|]\s*wc/.test(cmd);
8416
+ return /^eza\s+.*--long\s+\S+/.test(cmd) || /^eza\s+.*--tree/.test(cmd) || /^tree(\s|$)/.test(cmd) || /^ls\s+(?:\S+\s+)*-[a-zA-Z]*R[a-zA-Z]*(?:\s|$)/.test(cmd) || /^ls\s+(?:-[la]+\s+)?(\S+)\s*[|]\s*head/.test(cmd) || /^ls\s+(?:-[la]+\s+)?(\S+)\s*[|]\s*grep/.test(cmd) || /^ls\s+(?:-[la]+\s+)?(\S+)\s*[|]\s*wc/.test(cmd) || /^(?:Get-ChildItem|gci|dir)\b.*(?:-Recurse|-r\b|\/s\b)/i.test(cmd);
8295
8417
  }
8296
8418
  function extractForLoopWcL(cmd) {
8297
8419
  return /^for\s+\w+\s+in\s+.*;\s*do\s+wc\s+-l/.test(cmd);
@@ -8606,6 +8728,73 @@ function maybeCompressRewrite(event, rawCmd, cmd) {
8606
8728
  const wrapped = `token-goat compress -f ${filterName} --timeout ${cfg.timeout_seconds} -c ${shellQuoteSingle(rawCmd)}`;
8607
8729
  return { hookType: "rewriteInput", updatedInput: { ...event.toolInput, command: wrapped } };
8608
8730
  }
8731
+ var PIPELINE_PASSTHROUGH_HEADS = /* @__PURE__ */ new Set(["head", "tail", "cat", "tee", "less", "more"]);
8732
+ var CI_BUILD_TEST_FILTER_NAMES = /* @__PURE__ */ new Set([
8733
+ "generic-ci",
8734
+ "jest",
8735
+ "vitest",
8736
+ "pytest",
8737
+ "go_test",
8738
+ "cargo_test",
8739
+ "cargo",
8740
+ "go",
8741
+ "make",
8742
+ "cmake",
8743
+ "gradle",
8744
+ "maven",
8745
+ "dotnet",
8746
+ "turbo",
8747
+ "nx",
8748
+ "lerna",
8749
+ "webpack",
8750
+ "eslint",
8751
+ "ruff",
8752
+ "clippy",
8753
+ "flake8",
8754
+ "mypy",
8755
+ "prettier",
8756
+ "tsc"
8757
+ ]);
8758
+ function isCiBuildTestSegment(cleaned, cwd) {
8759
+ if (isTestRunnerCommand(cleaned) || isBuildCommand(cleaned) || isTscCommand(cleaned)) return true;
8760
+ if (/^\s*(npm|pnpm|yarn|bun)\s+(run\s+)?(test|build|lint|typecheck|check|ci|guards)\b/i.test(cleaned)) return true;
8761
+ if (/^\s*(cargo|go|dotnet|make|gradle|mvn|pytest|vitest|jest|eslint|ruff)\b/i.test(cleaned)) return true;
8762
+ const detected = detectFromCommand(cleaned, cwd ?? void 0);
8763
+ if (detected !== null && CI_BUILD_TEST_FILTER_NAMES.has(detected.filter.name)) return true;
8764
+ return false;
8765
+ }
8766
+ function pipelineShapeFilter(cmd, cwd) {
8767
+ if (hasUnquotedOperator(cmd, ["&&", "||", ";"])) {
8768
+ const forSplit2 = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
8769
+ if (hasBareBackgroundOrNewline(forSplit2)) return null;
8770
+ const segments2 = splitShellSegments(forSplit2);
8771
+ if (segments2.length >= 2) {
8772
+ let recognizedCiCount = 0;
8773
+ for (const segment of segments2) {
8774
+ const cleaned = stripOutputPipeline(segment.trim());
8775
+ if (cleaned.length === 0) continue;
8776
+ if (isCiBuildTestSegment(cleaned, cwd)) {
8777
+ recognizedCiCount++;
8778
+ }
8779
+ }
8780
+ if (recognizedCiCount > 0) {
8781
+ return filterByName("generic-ci");
8782
+ }
8783
+ }
8784
+ return null;
8785
+ }
8786
+ const forSplit = cmd.replace(/\s2>(?:&1|\/dev\/null)/g, "");
8787
+ if (hasBareBackgroundOrNewline(forSplit)) return null;
8788
+ const segments = splitShellSegments(forSplit);
8789
+ if (segments.length < 2) return null;
8790
+ for (const segment of segments.slice(1)) {
8791
+ const head = safeShlexSplit(segment)?.[0];
8792
+ if (head === void 0) return null;
8793
+ if (!PIPELINE_PASSTHROUGH_HEADS.has(head.replace(/^.*[/\\]/, ""))) return null;
8794
+ }
8795
+ const detected = detectFromCommand(stripOutputPipeline(cmd), cwd ?? void 0);
8796
+ return detected === null ? null : detected.filter;
8797
+ }
8609
8798
  function pureFileReadPath(cmd) {
8610
8799
  const single = extractCatFile(cmd)?.filePath ?? extractHeadFile(cmd)?.filePath ?? extractTailFile(cmd)?.filePath ?? extractLineRangeRead(cmd)?.filePath;
8611
8800
  if (single !== void 0) return single;
@@ -8629,10 +8818,10 @@ function deliveredLineNumbers(cmd, lineCount) {
8629
8818
  if (singleFileCompoundReadPath(cmd) !== null) return Array.from({ length: lineCount }, () => null);
8630
8819
  return null;
8631
8820
  }
8632
- function elideServedShellLines(cmd, output, priorIds) {
8821
+ function elideServedShellLines(cmd, output, priorIds, unknownLineNumbers = false) {
8633
8822
  if (priorIds.length === 0) return null;
8634
8823
  const lines = output.split("\n");
8635
- const numbers = deliveredLineNumbers(cmd, lines.length);
8824
+ const numbers = unknownLineNumbers ? Array.from({ length: lines.length }, () => null) : deliveredLineNumbers(cmd, lines.length);
8636
8825
  if (numbers === null) return null;
8637
8826
  const rows = lines.map((text, i) => ({ no: numbers[i] ?? null, text, raw: text }));
8638
8827
  const bodies = [];
@@ -8750,6 +8939,26 @@ async function maybeCollapseIdenticalRead(cmd, rawCmd, output, exitCode, cwd, ca
8750
8939
  if (!isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(pointer, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) return null;
8751
8940
  return emitRewrite(pointer, identical ? "identical file re-read collapsed" : "already-served file lines collapsed", { kind: identical ? "bash_compress:identical-reread" : "bash_compress:contained-reread", originalBytes: deliveredOutputBytes(originalBytes) });
8752
8941
  }
8942
+ async function maybeElideServedGenericOutput(cmd, output, exitCode, cwd, cacheMinBytes) {
8943
+ if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
8944
+ if (!loadConfig().bash_compress.elide_served_shell_output) return null;
8945
+ if (exitCode !== null && exitCode !== 0) return null;
8946
+ if (redactSecrets(output).count > 0) return null;
8947
+ const originalBytes = Buffer.byteLength(output, "utf-8");
8948
+ if (originalBytes < cacheMinBytes) return null;
8949
+ const priorIds = getFileServedOutputs(GENERIC_SERVED_OUTPUT_KEY).filter((id) => getBashOutput(id)?.command !== cmd);
8950
+ let rewrittenText = null;
8951
+ if (priorIds.length > 0) {
8952
+ const elided = elideServedShellLines(cmd, output, priorIds, true);
8953
+ if (elided !== null && isRewriteWorthwhile({ originalBytes, rewrittenBytes: Buffer.byteLength(elided, "utf-8"), noticeBytes: 0, minNetSavingsBytes: resolveMinNetSavingsBytes() })) {
8954
+ rewrittenText = elided;
8955
+ }
8956
+ }
8957
+ const storedId = await storeBashOutput(cmd, rewrittenText ?? output, exitCode ?? 0, cwd);
8958
+ recordFileServedOutput(GENERIC_SERVED_OUTPUT_KEY, storedId);
8959
+ if (rewrittenText === null) return null;
8960
+ return emitRewrite(rewrittenText, "already-served shell output collapsed", { kind: "bash_compress:generic-served-elision", originalBytes: deliveredOutputBytes(originalBytes) });
8961
+ }
8753
8962
  async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes) {
8754
8963
  if (process.env["TOKEN_GOAT_BASH_COMPRESS"] === "0") return null;
8755
8964
  if (isCompressibleSingleCommand(cmd)) return null;
@@ -8762,7 +8971,8 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
8762
8971
  }
8763
8972
  if (!cfg.enabled || cfg.disabled_filters.includes("generic")) return null;
8764
8973
  if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return null;
8765
- const filter3 = filterByName("generic");
8974
+ const shaped = pipelineShapeFilter(cmd, cwd);
8975
+ const filter3 = shaped !== null && !cfg.disabled_filters.includes(shaped.name) ? shaped : filterByName("generic");
8766
8976
  if (filter3 === null) return null;
8767
8977
  const compressed = compressOutput(filter3, output, "", exitCode ?? 0, [], {
8768
8978
  maxLines: cfg.max_lines,
@@ -8784,7 +8994,7 @@ async function maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinB
8784
8994
  return null;
8785
8995
  }
8786
8996
  await storeBashOutput(cmd, output, exitCode ?? 0, cwd);
8787
- return emitRewrite(body, "bash", { kind: "bash_compress:generic", originalBytes: deliveredOutputBytes(compressed.originalBytes) });
8997
+ return emitRewrite(body, "bash", { kind: `bash_compress:${filter3.name}`, originalBytes: deliveredOutputBytes(compressed.originalBytes) });
8788
8998
  }
8789
8999
  function maybeStripAnsiOnly(output) {
8790
9000
  if (!output.includes("\x1B")) return null;
@@ -9079,7 +9289,7 @@ function preBashHandlerInner(event) {
9079
9289
  recordStat("session_hint", 0, 0);
9080
9290
  const hints = [];
9081
9291
  for (const { filePath, ranges, tool } of sedReads) {
9082
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9292
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9083
9293
  const sedDedupKey = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
9084
9294
  const overlapHints = [];
9085
9295
  const freshRanges = [];
@@ -9100,7 +9310,7 @@ function preBashHandlerInner(event) {
9100
9310
  const catJsonPipe = extractCatJsonPipe(cmd);
9101
9311
  if (catJsonPipe !== null) {
9102
9312
  const { filePath } = catJsonPipe;
9103
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9313
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9104
9314
  recordStat("session_hint", 0, 0);
9105
9315
  return contextOutput(
9106
9316
  '`cat | jq` loads the whole file. Use `token-goat config-get "' + hintPath + '" KEY_NAME` or `token-goat section "' + hintPath + '::sectionName"` to slice one value.'
@@ -9109,7 +9319,7 @@ function preBashHandlerInner(event) {
9109
9319
  const catResult = extractCatFile(cmd);
9110
9320
  if (catResult !== null) {
9111
9321
  const { filePath, isDoc, isEnv, isConfig, isSql, cmd0, advisoryOnly } = catResult;
9112
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9322
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9113
9323
  recordStat("session_hint", 0, 0);
9114
9324
  if (isSql) {
9115
9325
  return contextOutput(
@@ -9124,7 +9334,7 @@ function preBashHandlerInner(event) {
9124
9334
  recordStat("session_hint", 0, 0);
9125
9335
  const cmd0 = catMulti[0].cmd0;
9126
9336
  const perPath = catMulti.map(({ filePath, isDoc, isEnv, isConfig, isSql }) => {
9127
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9337
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9128
9338
  const how = isSql ? 'token-goat section "' + hintPath + '::table_name"' : isEnv || isConfig ? 'token-goat config-get "' + hintPath + '" KEY_NAME' : isDoc ? 'token-goat section "' + hintPath + '::SectionHeading"' : 'token-goat read "' + hintPath + '::SymbolName"';
9129
9339
  return " " + hintPath + " -> `" + how + "`";
9130
9340
  });
@@ -9134,7 +9344,7 @@ function preBashHandlerInner(event) {
9134
9344
  const psGetContentResult = extractPowerShellWrappedGetContent(cmd);
9135
9345
  if (psGetContentResult !== null) {
9136
9346
  const { filePath, isDoc, isEnv, isConfig, isSql } = psGetContentResult;
9137
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9347
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9138
9348
  recordStat("session_hint", 0, 0);
9139
9349
  const lead = "`Get-Content` via a `powershell -Command` wrapper bypasses read hooks and loads the entire file into context. ";
9140
9350
  if (isSql) {
@@ -9146,7 +9356,7 @@ function preBashHandlerInner(event) {
9146
9356
  const wslCatResult = extractWslCatFile(cmd);
9147
9357
  if (wslCatResult !== null) {
9148
9358
  const { filePath, isDoc, isEnv, isConfig, isSql } = wslCatResult;
9149
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9359
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9150
9360
  recordStat("session_hint", 0, 0);
9151
9361
  if (isSql) {
9152
9362
  return contextOutput(
@@ -9158,8 +9368,8 @@ function preBashHandlerInner(event) {
9158
9368
  }
9159
9369
  const pyRead = extractPythonFileRead(cmd);
9160
9370
  if (pyRead !== null) {
9161
- const { filePath, isDoc, isOutputFile } = pyRead;
9162
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9371
+ const { filePath, isDoc, isConfig, isEnv, isSql, isOutputFile } = pyRead;
9372
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9163
9373
  recordStat("session_hint", 0, 0);
9164
9374
  if (isOutputFile) {
9165
9375
  if (taskOutputIsJsonlTranscript(filePath)) {
@@ -9170,41 +9380,46 @@ function preBashHandlerInner(event) {
9170
9380
  "This `.output` file is a background command's stdout. Use `token-goat bash-output --file \"" + hintPath + '"` to narrow it with `--grep PATTERN`, `--tail N` or `--head N`, instead of reading the whole file.'
9171
9381
  );
9172
9382
  }
9173
- const hint = isDoc ? 'Use `token-goat section "' + hintPath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + hintPath + '::SymbolName"` to extract a specific symbol.';
9383
+ if (isSql) {
9384
+ return contextOutput(
9385
+ 'Python `open()` file reads bypass read hooks. Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.'
9386
+ );
9387
+ }
9388
+ const hint = surgicalHintFor(hintPath, isEnv, isConfig, isDoc);
9174
9389
  return cdStripped ? contextOutput("Python `open()` file reads bypass read hooks. " + hint) : denyOutput("Python `open()` file reads bypass read hooks. " + hint);
9175
9390
  }
9176
9391
  const tailResult = extractTailFile(cmd);
9177
9392
  if (tailResult !== null) {
9178
9393
  const { filePath, isDoc, isConfig, isSql } = tailResult;
9179
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9394
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9180
9395
  recordStat("session_hint", 0, 0);
9181
9396
  return contextOutput("`tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql));
9182
9397
  }
9183
9398
  const headResult = extractHeadFile(cmd);
9184
9399
  if (headResult !== null) {
9185
9400
  const { filePath, isDoc, isConfig, isSql, n } = headResult;
9186
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9401
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9187
9402
  recordStat("session_hint", 0, 0);
9188
9403
  return contextOutput(leadingLinesHint("`head` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql }, preHookCwd));
9189
9404
  }
9190
9405
  const gcTailResult = extractGetContentTail(cmd);
9191
9406
  if (gcTailResult !== null) {
9192
9407
  const { filePath, isDoc, isConfig, isSql } = gcTailResult;
9193
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9408
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9194
9409
  recordStat("session_hint", 0, 0);
9195
9410
  return contextOutput("`Get-Content -Tail` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql));
9196
9411
  }
9197
9412
  const gcSelectResult = extractGetContentSelectFirst(cmd);
9198
9413
  if (gcSelectResult !== null) {
9199
9414
  const { filePath, isDoc, isConfig, isSql, n } = gcSelectResult;
9200
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9415
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9201
9416
  recordStat("session_hint", 0, 0);
9202
9417
  return contextOutput(leadingLinesHint("`Select-Object -First` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql }, preHookCwd));
9203
9418
  }
9204
9419
  const nodeRead = extractNodeFileRead(cmd);
9205
9420
  if (nodeRead !== null) {
9206
9421
  const { filePath, isDoc, isConfig, isSql } = nodeRead;
9207
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9422
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9208
9423
  const lead = "Node.js `fs.readFileSync()` bypasses read hooks. ";
9209
9424
  if (isSql) {
9210
9425
  recordStat("session_hint", 0, 0);
@@ -9214,6 +9429,19 @@ function preBashHandlerInner(event) {
9214
9429
  recordStat("session_hint", 0, 0);
9215
9430
  return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9216
9431
  }
9432
+ const psMethodRead = extractPowerShellFileMethodRead(cmd);
9433
+ if (psMethodRead !== null) {
9434
+ const { filePath, isDoc, isConfig, isSql } = psMethodRead;
9435
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9436
+ const lead = "PowerShell `[IO.File]::ReadAllText()` bypasses read hooks. ";
9437
+ if (isSql) {
9438
+ recordStat("session_hint", 0, 0);
9439
+ return contextOutput(lead + 'Use `token-goat section "' + hintPath + '::table_name"` to pull one CREATE TABLE / CREATE TYPE block.');
9440
+ }
9441
+ const hint = isDoc ? 'Use `token-goat section "' + hintPath + '::SectionHeading"` to read one section.' : isConfig ? 'Use `token-goat config-get "' + hintPath + '" KEY_NAME` or `token-goat section "' + hintPath + '::sectionName"` to read a specific value.' : 'Use `token-goat read "' + hintPath + '::SymbolName"` to extract a specific symbol.';
9442
+ recordStat("session_hint", 0, 0);
9443
+ return cdStripped ? contextOutput(lead + hint) : denyOutput(lead + hint);
9444
+ }
9217
9445
  if (extractGrepPipeChain(cmd)) {
9218
9446
  recordStat("session_hint", 0, 0);
9219
9447
  return contextOutput(
@@ -9223,7 +9451,7 @@ function preBashHandlerInner(event) {
9223
9451
  const mdHeadingGrep = extractMarkdownHeadingGrep(cmd);
9224
9452
  if (mdHeadingGrep !== null) {
9225
9453
  const { filePath } = mdHeadingGrep;
9226
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9454
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9227
9455
  recordStat("session_hint", 0, 0);
9228
9456
  return contextOutput(
9229
9457
  'Use `token-goat outline "' + hintPath + '"` to get all headings with line ranges \u2014 then `token-goat section "' + hintPath + '::Heading"` to read one section.'
@@ -9240,7 +9468,7 @@ function preBashHandlerInner(event) {
9240
9468
  const rgStructural = extractRgStructuralSearch(cmd);
9241
9469
  if (rgStructural !== null) {
9242
9470
  const { filePath } = rgStructural;
9243
- const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
9471
+ const hintPath = displaySafePath(cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath);
9244
9472
  recordStat("session_hint", 0, 0);
9245
9473
  return contextOutput(
9246
9474
  'Searching for code definitions with `rg`/`grep` is slower than surgical reads. Use `token-goat skeleton "' + hintPath + '"` to see all symbols with line numbers, or `token-goat outline "' + hintPath + '"` for symbols with docstrings and line ranges.'
@@ -9444,6 +9672,21 @@ function recordBashFileReadsForSessionCache(cmd, cwd) {
9444
9672
  recordFileRead(resolve(wslCat.filePath));
9445
9673
  return;
9446
9674
  }
9675
+ const nodeRead = extractNodeFileRead(cmd);
9676
+ if (nodeRead !== null) {
9677
+ recordFileRead(resolve(nodeRead.filePath));
9678
+ return;
9679
+ }
9680
+ const psMethod = extractPowerShellFileMethodRead(cmd);
9681
+ if (psMethod !== null) {
9682
+ recordFileRead(resolve(psMethod.filePath));
9683
+ return;
9684
+ }
9685
+ const pyRead = extractPythonFileRead(cmd);
9686
+ if (pyRead !== null && !pyRead.isOutputFile) {
9687
+ recordFileRead(resolve(pyRead.filePath));
9688
+ return;
9689
+ }
9447
9690
  }
9448
9691
  async function postBashHandler(event) {
9449
9692
  try {
@@ -9539,6 +9782,10 @@ async function postBashHandler(event) {
9539
9782
  if (identical !== null) return identical;
9540
9783
  const compound = await maybeCompressCompoundOutput(cmd, output, exitCode, cwd, cacheMinBytes);
9541
9784
  if (compound !== null) return compound;
9785
+ if (!isFileRead && extractLineRangeReadsCompound(cmd) === null) {
9786
+ const genericElision = await maybeElideServedGenericOutput(cmd, output, exitCode, cwd, cacheMinBytes);
9787
+ if (genericElision !== null) return genericElision;
9788
+ }
9542
9789
  return maybeStripAnsiOnly(output) ?? passOutput();
9543
9790
  }
9544
9791
  if (Buffer.byteLength(output, "utf-8") < cacheMinBytes) return passOutput();
@@ -9747,8 +9994,8 @@ registerHook("post_tool_use", postTaskOutputHandler, { toolName: "TaskOutput" })
9747
9994
 
9748
9995
  // src/hooks_tool_failure.ts
9749
9996
  init_define_import_meta_env();
9750
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
9751
- import { dirname as dirname2 } from "node:path";
9997
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync as statSync4, writeFileSync as writeFileSync2 } from "node:fs";
9998
+ import { dirname as dirname2, relative } from "node:path";
9752
9999
  var FAILURE_SUFFIX = ".tool-failures.json";
9753
10000
  var MAX_TRACKED_FAILURES = 64;
9754
10001
  var SIGNATURE_ERROR_CHARS = 200;
@@ -9795,9 +10042,74 @@ function writeLedger(target, ledger) {
9795
10042
  }
9796
10043
  }
9797
10044
  function repeatFailureNotice(toolName) {
9798
- const tool = toolName === void 0 || toolName === "" ? "This tool" : toolName;
10045
+ const tool = toolName === void 0 || toolName === "" ? "This tool" : displaySafeText(toolName);
9799
10046
  return `[token-goat] ${tool} just failed with the same error as an earlier call this session. Retrying it unchanged will fail the same way -- change the arguments, the tool, or the approach.`;
9800
10047
  }
10048
+ var MAX_EDIT_DIAGNOSE_BYTES = 10 * 1024 * 1024;
10049
+ function diagnoseEditFailure(event, errorText) {
10050
+ const toolName = getToolName(event);
10051
+ if (!toolName || !/^(edit|str_replace_editor)$/i.test(toolName)) {
10052
+ return null;
10053
+ }
10054
+ const isMultiple = /multiple matches|not unique|found \d+ matches|matches \d+ times|more than one match/i.test(errorText);
10055
+ const isNotFound = /no match|not found|could not find|zero matches|string to replace.*not found/i.test(errorText);
10056
+ if (!isMultiple && !isNotFound) {
10057
+ return null;
10058
+ }
10059
+ const filePath = getFilePath(event) ?? (typeof event.toolInput["path"] === "string" ? event.toolInput["path"] : void 0);
10060
+ if (!filePath) return null;
10061
+ const oldString = typeof event.toolInput["old_string"] === "string" ? event.toolInput["old_string"] : typeof event.toolInput["old_str"] === "string" ? event.toolInput["old_str"] : typeof event.toolInput["target"] === "string" ? event.toolInput["target"] : void 0;
10062
+ if (oldString === void 0 || oldString === "") return null;
10063
+ const absPath = normalizePath(filePath);
10064
+ if (!existsSync2(absPath)) return null;
10065
+ try {
10066
+ const stat = statSync4(absPath);
10067
+ if (stat.size > MAX_EDIT_DIAGNOSE_BYTES) return null;
10068
+ const fileContent = readFileSync3(absPath, "utf8");
10069
+ const relDisplay = displaySafeText(relative(process.cwd(), absPath).replace(/\\/g, "/") || absPath);
10070
+ if (isMultiple) {
10071
+ const matchLines = [];
10072
+ let pos = 0;
10073
+ while (pos < fileContent.length) {
10074
+ const idx = fileContent.indexOf(oldString, pos);
10075
+ if (idx === -1) break;
10076
+ const line = fileContent.slice(0, idx).split("\n").length;
10077
+ matchLines.push(line);
10078
+ pos = idx + Math.max(1, oldString.length);
10079
+ }
10080
+ if (matchLines.length > 1) {
10081
+ const lineList = matchLines.slice(0, 5).join(", ");
10082
+ const overflow = matchLines.length > 5 ? ` (+${matchLines.length - 5} more)` : "";
10083
+ return `[token-goat] Edit failed: string matched ${matchLines.length} times in ${relDisplay} on lines ${lineList}${overflow}. Include 2-3 lines of surrounding context to make old_str unique.`;
10084
+ }
10085
+ }
10086
+ if (isNotFound) {
10087
+ const normOld = oldString.replace(/\r\n/g, "\n");
10088
+ const normFile = fileContent.replace(/\r\n/g, "\n");
10089
+ if (normFile.includes(normOld)) {
10090
+ return `[token-goat] Edit failed: string not found in ${relDisplay}, but matches with normalized line endings. Check CRLF vs LF line endings or whitespace.`;
10091
+ }
10092
+ const firstLine = oldString.split(/\r?\n/)[0]?.trim() ?? "";
10093
+ if (firstLine.length >= 10) {
10094
+ const fileLines = fileContent.split("\n");
10095
+ const similarLines = [];
10096
+ for (let i = 0; i < fileLines.length; i++) {
10097
+ const lineText = fileLines[i];
10098
+ if (lineText !== void 0 && lineText.includes(firstLine)) {
10099
+ similarLines.push(i + 1);
10100
+ if (similarLines.length >= 3) break;
10101
+ }
10102
+ }
10103
+ if (similarLines.length > 0) {
10104
+ return `[token-goat] Edit failed: string not found in ${relDisplay}. A similar line was found on line ${similarLines.join(", ")} \u2014 view that range to copy exact text.`;
10105
+ }
10106
+ }
10107
+ return `[token-goat] Edit failed: string not found in ${relDisplay}. View the target lines to copy the exact current content and indentation.`;
10108
+ }
10109
+ } catch {
10110
+ }
10111
+ return null;
10112
+ }
9801
10113
  function postToolUseFailureHandler(event) {
9802
10114
  try {
9803
10115
  if (!event.sessionId) return passOutput();
@@ -9809,6 +10121,7 @@ function postToolUseFailureHandler(event) {
9809
10121
  const signature = failureSignature(toolName, errorText);
9810
10122
  const ledger = readLedger(target);
9811
10123
  const priorState = ledger.seen[signature];
10124
+ const editDiagnostic = diagnoseEditFailure(event, errorText);
9812
10125
  if (priorState === void 0) {
9813
10126
  ledger.seen[signature] = false;
9814
10127
  ledger.order.push(signature);
@@ -9817,11 +10130,19 @@ function postToolUseFailureHandler(event) {
9817
10130
  if (evicted !== void 0) delete ledger.seen[evicted];
9818
10131
  }
9819
10132
  writeLedger(target, ledger);
10133
+ if (editDiagnostic !== null) {
10134
+ ledger.seen[signature] = true;
10135
+ writeLedger(target, ledger);
10136
+ return contextOutput(editDiagnostic);
10137
+ }
9820
10138
  return passOutput();
9821
10139
  }
9822
10140
  if (priorState) return passOutput();
9823
10141
  ledger.seen[signature] = true;
9824
10142
  writeLedger(target, ledger);
10143
+ if (editDiagnostic !== null) {
10144
+ return contextOutput(editDiagnostic);
10145
+ }
9825
10146
  return contextOutput(repeatFailureNotice(toolName));
9826
10147
  } catch {
9827
10148
  return passOutput();
@@ -10723,13 +11044,16 @@ var SPAWN_RESTRICT_HINT_KEY = "agent-spawn-restrict-hint";
10723
11044
  var SPAWN_RESTRICT_MAX_NAMES = 3;
10724
11045
  var ROSTER_WALK_MAX_DEPTH = 4;
10725
11046
  var ROSTER_WALK_MAX_FILES = 400;
11047
+ var AGENT_NAME_RE = /^[A-Za-z0-9._:-]{1,64}$/;
10726
11048
  function parseAgentDefinition(text, fallbackName) {
10727
11049
  const fmMatch = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
10728
11050
  if (!fmMatch) return null;
10729
11051
  const fm = fmMatch[1];
10730
11052
  const nameMatch = /^name:(.*)$/m.exec(fm);
10731
11053
  const rawName = nameMatch ? nameMatch[1].trim().replace(/^["']|["']$/g, "") : "";
10732
- const name2 = rawName !== "" ? rawName : fallbackName;
11054
+ const resolved = AGENT_NAME_RE.test(rawName) ? rawName : fallbackName;
11055
+ if (!AGENT_NAME_RE.test(resolved)) return null;
11056
+ const name2 = resolved;
10733
11057
  const toolsMatch = /^tools:(.*)$/m.exec(fm);
10734
11058
  if (!toolsMatch) return { name: name2, restricted: false };
10735
11059
  const inline = toolsMatch[1].trim();
@@ -10743,7 +11067,7 @@ function parseAgentDefinition(text, fallbackName) {
10743
11067
  return { name: name2, restricted: false };
10744
11068
  }
10745
11069
  function findRestrictedAgentNames(roots) {
10746
- const scanRoots = roots ?? [path2.join(os.homedir(), ".claude", "agents")];
11070
+ const scanRoots = roots ?? [path2.join(os.homedir(), ".claude", "agents"), path2.join(process.cwd(), ".claude", "agents")];
10747
11071
  const names = /* @__PURE__ */ new Set();
10748
11072
  const visited = /* @__PURE__ */ new Set();
10749
11073
  let filesSeen = 0;
@@ -10803,7 +11127,7 @@ function buildUnrestrictedSpawnAdvisory(toolInput) {
10803
11127
  if (names.length === 0) return "";
10804
11128
  markHintShown(SPAWN_RESTRICT_HINT_KEY);
10805
11129
  recordStat("session_hint", 0, 0, void 0, "agent-spawn-restrict");
10806
- const shown = names.slice(0, SPAWN_RESTRICT_MAX_NAMES).join(", ");
11130
+ const shown = neutralizeSpokenMarkers(names.slice(0, SPAWN_RESTRICT_MAX_NAMES).join(", "));
10807
11131
  const more = names.length > SPAWN_RESTRICT_MAX_NAMES ? ` and ${names.length - SPAWN_RESTRICT_MAX_NAMES} more` : "";
10808
11132
  return `[token-goat] This spawn ran as general-purpose (the default when subagent_type is omitted), which is unrestricted: its lane starts by paying for every tool and MCP schema on the machine. Tools-restricted agent definitions exist here: ${shown}${more}. A future spawn that fits one of them can pass that name as subagent_type to start with a much smaller prefix. Advisory only: this spawn has already run, and this notice saved nothing.`;
10809
11133
  } catch {