token-goat 2.6.36 → 2.8.1

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 (34) hide show
  1. package/README.md +36 -8
  2. package/SECURITY.md +46 -17
  3. package/dist/{token-goat-chunk-UFOVM7ZN.mjs → token-goat-chunk-2G6RAB4G.mjs} +1391 -378
  4. package/dist/token-goat-chunk-324QOJYZ.mjs +91 -0
  5. package/dist/token-goat-chunk-5CVKO3DA.mjs +185 -0
  6. package/dist/{token-goat-chunk-65BKISIS.mjs → token-goat-chunk-73MF6YWW.mjs} +4 -4
  7. package/dist/{token-goat-chunk-MGOUYAA2.mjs → token-goat-chunk-7JDXDERZ.mjs} +1 -1
  8. package/dist/{token-goat-hook-chunk-BDR6C6IE.mjs → token-goat-chunk-AOBZUFNJ.mjs} +318 -51
  9. package/dist/{token-goat-chunk-V465YKOR.mjs → token-goat-chunk-ELDJRLHZ.mjs} +492 -28
  10. package/dist/{token-goat-chunk-CNDOJ3ZP.mjs → token-goat-chunk-I6TOUPLP.mjs} +2 -2
  11. package/dist/token-goat-chunk-R4SR7MQY.mjs +486 -0
  12. package/dist/{token-goat-chunk-IYTVE6KN.mjs → token-goat-chunk-RMDQFTQD.mjs} +226 -150
  13. package/dist/{token-goat-chunk-KYFJC37X.mjs → token-goat-chunk-VBXBLGTO.mjs} +792 -144
  14. package/dist/{token-goat-chunk-DG53MVNJ.mjs → token-goat-chunk-WN5T5EW5.mjs} +212 -212
  15. package/dist/{token-goat-chunk-VYMGEVZS.mjs → token-goat-chunk-XEDQH5DA.mjs} +317 -11
  16. package/dist/{token-goat-chunk-LN6OUHTV.mjs → token-goat-chunk-Y2SYNH3S.mjs} +5 -5
  17. package/dist/token-goat-chunk-YUGNM3KL.mjs +23 -0
  18. package/dist/token-goat-hook.mjs +7 -7
  19. package/dist/token-goat.core.mjs +5 -5
  20. package/package.json +12 -8
  21. package/dist/token-goat-chunk-FRTBMRP7.mjs +0 -10048
  22. package/dist/token-goat-hook-chunk-3QYSN4QV.mjs +0 -14764
  23. package/dist/token-goat-hook-chunk-3ZDBWJDF.mjs +0 -13659
  24. package/dist/token-goat-hook-chunk-5UH54CW6.mjs +0 -912
  25. package/dist/token-goat-hook-chunk-6ODM3MP7.mjs +0 -706
  26. package/dist/token-goat-hook-chunk-A77A26A7.mjs +0 -18184
  27. package/dist/token-goat-hook-chunk-BUOCULAM.mjs +0 -29
  28. package/dist/token-goat-hook-chunk-C6GIABOX.mjs +0 -15971
  29. package/dist/token-goat-hook-chunk-E257IGSN.mjs +0 -153
  30. package/dist/token-goat-hook-chunk-MW5HPEGD.mjs +0 -10411
  31. package/dist/token-goat-hook-chunk-QSCYNJ2B.mjs +0 -23
  32. package/dist/token-goat-hook-chunk-RFRLWOQH.mjs +0 -11
  33. package/dist/token-goat-hook-chunk-XUMIVYEN.mjs +0 -109
  34. package/dist/token-goat-hook-chunk-Y2WHX2P3.mjs +0 -6463
@@ -1,19 +1,9 @@
1
1
  import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
- import {
4
- canRunWrappedShell,
5
- compressOutput,
6
- dedupeConsecutive,
7
- detectFromCommand,
8
- filterByName,
9
- hasBareBackgroundOrNewline,
10
- isRewriteWorthwhile,
11
- resolveMinNetSavingsBytes,
12
- shlexSplit
13
- } from "./token-goat-hook-chunk-3ZDBWJDF.mjs";
14
3
  import {
15
4
  BASH_OUTPUT_SUBDIR,
16
5
  HOOK_EVENTS,
6
+ accumulateResidentLines,
17
7
  buildCommandManifest,
18
8
  checkSymbolBodySize,
19
9
  commandHash,
@@ -23,12 +13,17 @@ import {
23
13
  indexRecallEntry,
24
14
  isBashEntryStale,
25
15
  isScopedGitStatusOrDiffStatCommand,
16
+ lineMayCarryResidentSignal,
26
17
  normalizePayload,
27
18
  readStdinJson,
19
+ readTranscriptTail,
20
+ repeatedSkillBodyHint,
28
21
  storeBashOutput,
29
22
  storeWebOutput,
30
- summarizeOutputDelta
31
- } from "./token-goat-hook-chunk-5UH54CW6.mjs";
23
+ summarizeOutputDelta,
24
+ summarizeResidentContext,
25
+ taskListPruneHint
26
+ } from "./token-goat-chunk-XEDQH5DA.mjs";
32
27
  import {
33
28
  BODY_FIRST_TOOL_RESPONSE_KEYS,
34
29
  OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
@@ -49,6 +44,7 @@ import {
49
44
  enqueueDirtyPathSafe,
50
45
  ensureWorkerAlive,
51
46
  estimateTokens,
47
+ estimateTokensFromLength,
52
48
  extractCompactFromMarker,
53
49
  extractToolResponseField,
54
50
  extractToolResultText,
@@ -72,7 +68,9 @@ import {
72
68
  getSessionWebFetches,
73
69
  getToolInput,
74
70
  getToolName,
71
+ hasSeenImage,
75
72
  hasSessionOutput,
73
+ imageQualifiesForShrink,
76
74
  incrementSkillHit,
77
75
  installedSkillPath,
78
76
  isBuildCommand,
@@ -102,6 +100,7 @@ import {
102
100
  recordKnownRootThrottled,
103
101
  recordOutstandingAgentSpawn,
104
102
  recordScheduledPrompt,
103
+ recordSeenImage,
105
104
  recordSymbolRead,
106
105
  recordWebFetch,
107
106
  registerHook,
@@ -110,6 +109,7 @@ import {
110
109
  saveSessionState,
111
110
  scanForInjectionPatterns,
112
111
  serializeOutput,
112
+ sessionSidecarPath,
113
113
  setLastTabContext,
114
114
  shrinkImage,
115
115
  storeBlob,
@@ -118,7 +118,18 @@ import {
118
118
  wasCliReadThisSession,
119
119
  wasFileReadThisSession,
120
120
  wasHintShown
121
- } from "./token-goat-hook-chunk-A77A26A7.mjs";
121
+ } from "./token-goat-chunk-2G6RAB4G.mjs";
122
+ import {
123
+ canRunWrappedShell,
124
+ compressOutput,
125
+ dedupeConsecutive,
126
+ detectFromCommand,
127
+ filterByName,
128
+ hasBareBackgroundOrNewline,
129
+ isRewriteWorthwhile,
130
+ resolveMinNetSavingsBytes,
131
+ shlexSplit
132
+ } from "./token-goat-chunk-7JDXDERZ.mjs";
122
133
  import {
123
134
  VERSION,
124
135
  detectHarness,
@@ -127,6 +138,7 @@ import {
127
138
  extractErrorMessage,
128
139
  foldPath,
129
140
  getDb,
141
+ getHarnessName,
130
142
  globalDbPath,
131
143
  isUnderSystemTemp,
132
144
  loadConfig,
@@ -137,7 +149,7 @@ import {
137
149
  runGit,
138
150
  shortFingerprint,
139
151
  toKB
140
- } from "./token-goat-hook-chunk-Y2WHX2P3.mjs";
152
+ } from "./token-goat-chunk-ELDJRLHZ.mjs";
141
153
 
142
154
  // src/hooks_grep.ts
143
155
  function grepIntInput(toolInput, key) {
@@ -561,12 +573,45 @@ function buildMemEpochSection() {
561
573
  `mem epoch: ${stdout} (no live TGMEM block is tracked in this session -- only the epoch counter is folded in; see buildMemEpochSection's doc comment)`
562
574
  ];
563
575
  }
576
+ 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.";
564
577
  function preCompactHandler(event) {
565
- const out = loadConfig().compact_assist.enabled ? contextOutput(buildManifest(event.sessionId, getCwd(event))) : passOutput();
578
+ const out = loadConfig().compact_assist.enabled ? contextOutput(`${MANIFEST_PREAMBLE}
579
+
580
+ ${buildManifest(event.sessionId, getCwd(event))}`) : passOutput();
566
581
  markCompacted();
567
582
  return out;
568
583
  }
569
584
  registerHook("pre_compact", preCompactHandler);
585
+ var MANIFEST_SURVIVAL_SAMPLE = 12;
586
+ function manifestPathSample(sessionId) {
587
+ const ownFiles = [...getSessionFiles().values()];
588
+ const siblingFiles = sessionId !== void 0 ? listSiblingSessionStates(sessionId).flatMap((s) => s.files) : [];
589
+ const files = siblingFiles.length > 0 ? mergeManifestFiles(ownFiles, siblingFiles) : ownFiles;
590
+ const seen = /* @__PURE__ */ new Set();
591
+ for (const entry of files) {
592
+ if (entry.path) seen.add(entry.path);
593
+ if (seen.size >= MANIFEST_SURVIVAL_SAMPLE) break;
594
+ }
595
+ return [...seen];
596
+ }
597
+ function postCompactHandler(event) {
598
+ const raw = event.raw["compact_summary"];
599
+ const summary = typeof raw === "string" ? raw : "";
600
+ const bytes = Buffer.byteLength(summary, "utf-8");
601
+ const sample = manifestPathSample(event.sessionId);
602
+ const haystack = foldPath(summary);
603
+ const survived = sample.filter((p) => haystack.includes(foldPath(p))).length;
604
+ const trigger = typeof event.raw["trigger"] === "string" ? event.raw["trigger"] : "unknown";
605
+ recordStat(
606
+ "compact_summary",
607
+ 0,
608
+ 0,
609
+ void 0,
610
+ `trigger=${trigger} bytes=${bytes} est_tokens=${estimateTokensFromLength(summary.length)} manifest_paths=${survived}/${sample.length}`
611
+ );
612
+ return passOutput();
613
+ }
614
+ registerHook("post_compact", postCompactHandler);
570
615
 
571
616
  // src/hooks_session.ts
572
617
  import crypto from "node:crypto";
@@ -575,7 +620,7 @@ import crypto from "node:crypto";
575
620
  var TRACKED_SKILL = "token-goat";
576
621
  var MAX_COMMANDS_SHOWN = 8;
577
622
  async function currentCommandNames() {
578
- const { buildProgram } = await import("./token-goat-hook-chunk-C6GIABOX.mjs");
623
+ const { buildProgram } = await import("./token-goat-chunk-Y2SYNH3S.mjs");
579
624
  return flattenCommandNames(buildCommandManifest(buildProgram()));
580
625
  }
581
626
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -627,7 +672,59 @@ async function checkSkillVersionDrift(sessionId) {
627
672
  }
628
673
  }
629
674
 
675
+ // src/pending_context.ts
676
+ import { readFileSync as readFileSync2, rmSync, writeFileSync, mkdirSync } from "node:fs";
677
+ import { dirname } from "node:path";
678
+ var PENDING_SUFFIX = ".pending-context.txt";
679
+ var MAX_PENDING_CONTEXT_BYTES = 4096;
680
+ function queuePendingContext(sessionId, text) {
681
+ const trimmed = text.trim();
682
+ if (trimmed === "") return;
683
+ const target = sessionSidecarPath(sessionId, PENDING_SUFFIX);
684
+ if (target === null) return;
685
+ try {
686
+ const existing = readPending(target);
687
+ const merged = existing === null ? trimmed : `${existing}
688
+ ${trimmed}`;
689
+ const capped = merged.length <= MAX_PENDING_CONTEXT_BYTES ? merged : merged.slice(merged.length - MAX_PENDING_CONTEXT_BYTES);
690
+ mkdirSync(dirname(target), { recursive: true });
691
+ writeFileSync(target, capped, "utf8");
692
+ } catch {
693
+ }
694
+ }
695
+ function drainPendingContext(sessionId) {
696
+ const target = sessionSidecarPath(sessionId, PENDING_SUFFIX);
697
+ if (target === null) return null;
698
+ const text = readPending(target);
699
+ try {
700
+ rmSync(target, { force: true });
701
+ } catch {
702
+ }
703
+ return text;
704
+ }
705
+ function readPending(target) {
706
+ try {
707
+ const raw = readFileSync2(target, "utf8").trim();
708
+ return raw === "" ? null : raw;
709
+ } catch {
710
+ return null;
711
+ }
712
+ }
713
+
630
714
  // src/hooks_session.ts
715
+ var PROMPT_SUBMIT_CONTEXT_DROPPED = /* @__PURE__ */ new Set([]);
716
+ function dropsPromptSubmitContext() {
717
+ return PROMPT_SUBMIT_CONTEXT_DROPPED.has(getHarnessName());
718
+ }
719
+ function pendingContextHandler(event) {
720
+ try {
721
+ if (!event.sessionId || !dropsPromptSubmitContext()) return passOutput();
722
+ const pending = drainPendingContext(event.sessionId);
723
+ return pending === null ? passOutput() : contextOutput(pending);
724
+ } catch {
725
+ return passOutput();
726
+ }
727
+ }
631
728
  var EMBEDDED_SKILL_CONTEXT_RE = /^\s*<skill-context\b/i;
632
729
  var CONTINUATION_PROMPT_RE = /^(?:continue|resume|next|go on)$/i;
633
730
  var SCHEDULED_PROMPT_RE = /^\[Scheduled prompt #(\d+)\]/i;
@@ -635,6 +732,35 @@ var SCHEDULED_PROMPT_PRESSURE_THRESHOLDS = /* @__PURE__ */ new Set([25, 100, 250
635
732
  function promptFingerprint(prompt) {
636
733
  return crypto.createHash("sha256").update(prompt).digest("hex").slice(0, 16);
637
734
  }
735
+ function residentContextHints(event) {
736
+ const transcriptPath = event.raw["transcript_path"];
737
+ if (typeof transcriptPath !== "string" || transcriptPath === "") return [];
738
+ const taskKey = `resident-task-list:${event.sessionId}`;
739
+ const skillKey = `resident-skill-body:${event.sessionId}`;
740
+ const wantTask = !wasHintShown(taskKey);
741
+ const wantSkill = !wasHintShown(skillKey);
742
+ if (!wantTask && !wantSkill) return [];
743
+ const lines = readTranscriptTail(transcriptPath).filter(lineMayCarryResidentSignal);
744
+ if (lines.length === 0) return [];
745
+ const summary = summarizeResidentContext(accumulateResidentLines(lines));
746
+ const hints = [];
747
+ if (wantTask) {
748
+ const hint = taskListPruneHint(summary.latestTaskList);
749
+ if (hint !== null) {
750
+ markHintShown(taskKey);
751
+ hints.push(hint);
752
+ }
753
+ }
754
+ if (wantSkill) {
755
+ const hint = repeatedSkillBodyHint(summary.repeatedSkillBodies);
756
+ if (hint !== null) {
757
+ markHintShown(skillKey);
758
+ hints.push(hint);
759
+ }
760
+ }
761
+ if (hints.length > 0) recordStat("session_hint", 0, 0);
762
+ return hints;
763
+ }
638
764
  async function userPromptSubmitHandler(event) {
639
765
  try {
640
766
  const rawPrompt = event.raw["prompt"] || "";
@@ -685,7 +811,8 @@ async function userPromptSubmitHandler(event) {
685
811
  }
686
812
  }
687
813
  const driftNudge = await checkSkillVersionDrift(event.sessionId);
688
- if (parts.length === 0 && !driftNudge) {
814
+ const residentHints = residentContextHints(event);
815
+ if (parts.length === 0 && !driftNudge && residentHints.length === 0) {
689
816
  return passOutput();
690
817
  }
691
818
  const lines = [];
@@ -695,7 +822,15 @@ async function userPromptSubmitHandler(event) {
695
822
  if (driftNudge) {
696
823
  lines.push(driftNudge);
697
824
  }
698
- return contextOutput(lines.join("\n"));
825
+ for (const hint of residentHints) {
826
+ lines.push(hint);
827
+ }
828
+ const text = lines.join("\n");
829
+ if (dropsPromptSubmitContext()) {
830
+ queuePendingContext(event.sessionId, text);
831
+ return passOutput();
832
+ }
833
+ return contextOutput(text);
699
834
  } catch {
700
835
  return passOutput();
701
836
  }
@@ -736,6 +871,7 @@ function subagentStopHandler(event) {
736
871
  }
737
872
  registerHook("user_prompt_submit", userPromptSubmitHandler);
738
873
  registerHook("subagent_stop", subagentStopHandler);
874
+ registerHook("post_tool_use", pendingContextHandler, { advisory: true, followsMatcher: true });
739
875
 
740
876
  // src/hooks_session_start.ts
741
877
  var GENERIC_REMINDER = 'token-goat: prefer surgical reads over the Read/Grep tools on this codebase; shell commands like `rg`, `grep`, `fd`, `sed`, `cat`, `find`, and `ls` are just commands, not tool names -- `token-goat symbol <name>`, `token-goat read "file::symbol"`, `token-goat section "file::Heading"`, `token-goat semantic "description"`, `token-goat outline <file>`. Run `token-goat index .` if this project is not indexed yet.';
@@ -7049,7 +7185,7 @@ registerHook("pre_tool_use", preSkillHandler, { toolName: "Skill" });
7049
7185
  registerHook("post_tool_use", postSkillHandler, { toolName: "Skill" });
7050
7186
 
7051
7187
  // src/hooks_bash.ts
7052
- import { statSync as statSync3, existsSync, readFileSync as readFileSync2 } from "node:fs";
7188
+ import { statSync as statSync3, existsSync, openSync, readSync, closeSync } from "node:fs";
7053
7189
  function stripCdPrefix(cmd) {
7054
7190
  const stripped = cmd.replace(/^(?:cd\s+(?:"[^"]*"|'[^']*'|\S+)\s*&&\s*)+/, "");
7055
7191
  return stripped.trim() || cmd;
@@ -7563,7 +7699,7 @@ function extractPythonFileRead(cmd) {
7563
7699
  if (outputOpen?.[1]) {
7564
7700
  const filePath = outputOpen[1];
7565
7701
  if (isOrchestratorStateFile(filePath)) return null;
7566
- return { filePath, isDoc: false, isTranscript: true };
7702
+ return { filePath, isDoc: false, isOutputFile: true };
7567
7703
  }
7568
7704
  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;
7569
7705
  const heredocMatch = /^python3?\s+-\s+<<\s*'?(\w+)'?\s*\n([\s\S]*?)\n\1\s*$/.exec(cmd);
@@ -7575,7 +7711,7 @@ function extractPythonFileRead(cmd) {
7575
7711
  const filePath = heredocOpen[1];
7576
7712
  if (isOrchestratorStateFile(filePath)) return null;
7577
7713
  const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7578
- return { filePath, isDoc, isTranscript: false };
7714
+ return { filePath, isDoc, isOutputFile: false };
7579
7715
  }
7580
7716
  if (/open\s*\(/.test(body) && !pythonOpenPathsAreAllLiteral(body)) {
7581
7717
  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);
@@ -7584,7 +7720,7 @@ function extractPythonFileRead(cmd) {
7584
7720
  if (isOrchestratorStateFile(filePath)) return null;
7585
7721
  if (OPEN_EXT.test(filePath)) {
7586
7722
  const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7587
- return { filePath, isDoc, isTranscript: false };
7723
+ return { filePath, isDoc, isOutputFile: false };
7588
7724
  }
7589
7725
  }
7590
7726
  }
@@ -7596,7 +7732,7 @@ function extractPythonFileRead(cmd) {
7596
7732
  if (!filePath) return null;
7597
7733
  if (isOrchestratorStateFile(filePath)) return null;
7598
7734
  const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7599
- return { filePath, isDoc, isTranscript: false };
7735
+ return { filePath, isDoc, isOutputFile: false };
7600
7736
  }
7601
7737
  if (/open\s*\(/.test(cmd) && !pythonOpenPathsAreAllLiteral(cmd)) {
7602
7738
  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);
@@ -7606,7 +7742,7 @@ function extractPythonFileRead(cmd) {
7606
7742
  if (isOrchestratorStateFile(filePath)) return null;
7607
7743
  if (OPEN_EXT.test(filePath)) {
7608
7744
  const isDoc = /\.(?:md|mdx|rst|txt)$/i.test(filePath);
7609
- return { filePath, isDoc, isTranscript: false };
7745
+ return { filePath, isDoc, isOutputFile: false };
7610
7746
  }
7611
7747
  }
7612
7748
  }
@@ -7715,6 +7851,12 @@ function sedRangeHint(filePath, ranges, tool) {
7715
7851
  }
7716
7852
  return prefix + "Use " + allReads + " to read exactly those lines.";
7717
7853
  }
7854
+ function leadingLinesHint(lead, hintPath, start, end, flags, preHookCwd) {
7855
+ const key = resolveIndexPath(hintPath, preHookCwd ?? process.cwd());
7856
+ const prior = findRangeOverlap(getFileLineRanges(key), start, end);
7857
+ if (prior !== null) return sedOverlapHint(hintPath, prior, start, end);
7858
+ return lead + surgicalHintForConfigDoc(hintPath, flags.isConfig, flags.isDoc, flags.isSql);
7859
+ }
7718
7860
  function findRangeOverlap(prior, start, end) {
7719
7861
  let best = null;
7720
7862
  let bestOverlap = 0;
@@ -7740,9 +7882,9 @@ function sedOverlapHint(filePath, prior, start, end) {
7740
7882
  }
7741
7883
  function extractNodeFileRead(cmd) {
7742
7884
  if (!/^node\s+-e/.test(cmd)) return null;
7743
- const readSync = /readFileSync\(['"]([^'"]+\.(?:ts|tsx|js|jsx|py|go|java|rs|rb|cs|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties|sql))['"]/i.exec(cmd);
7744
- if (readSync?.[1]) {
7745
- const filePath = readSync[1];
7885
+ const readSync2 = /readFileSync\(['"]([^'"]+\.(?:ts|tsx|js|jsx|py|go|java|rs|rb|cs|md|mdx|rst|txt|json|yaml|yml|toml|xml|conf|cfg|ini|properties|sql))['"]/i.exec(cmd);
7886
+ if (readSync2?.[1]) {
7887
+ const filePath = readSync2[1];
7746
7888
  if (isOrchestratorStateFile(filePath)) return null;
7747
7889
  if (isTempPath(filePath)) return null;
7748
7890
  const { isDoc, isConfig, isSql } = classifyDocConfig(filePath);
@@ -7802,6 +7944,24 @@ function extractGetContentSelectFirst(cmd) {
7802
7944
  const { isDoc, isConfig, isSql } = classifyDocConfig(filePath);
7803
7945
  return { filePath, isDoc, isConfig, isSql, n };
7804
7946
  }
7947
+ function taskOutputIsJsonlTranscript(outPath) {
7948
+ let fd = null;
7949
+ try {
7950
+ fd = openSync(normalizePath(outPath), "r");
7951
+ const buf = Buffer.alloc(64);
7952
+ const read = readSync(fd, buf, 0, buf.length, 0);
7953
+ return buf.subarray(0, read).toString("utf-8").trim().startsWith("{");
7954
+ } catch {
7955
+ return false;
7956
+ } finally {
7957
+ if (fd !== null) {
7958
+ try {
7959
+ closeSync(fd);
7960
+ } catch {
7961
+ }
7962
+ }
7963
+ }
7964
+ }
7805
7965
  function extractTasksOutput(cmd) {
7806
7966
  const taskOutputRe = /[/\\]tasks[/\\]([a-z0-9]+)\.output$/;
7807
7967
  const catM = /^cat(?:\s+(?:-[a-zA-Z]+|--[a-zA-Z-]+))*\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/.exec(cmd);
@@ -8390,17 +8550,11 @@ function preBashHandlerInner(event) {
8390
8550
  if (taskOutput !== null) {
8391
8551
  const { id, path: outPath, n } = taskOutput;
8392
8552
  recordStat("session_hint", 0, 0);
8393
- try {
8394
- const firstByte = readFileSync2(normalizePath(outPath), { encoding: "utf8", flag: "r" }).slice(0, 1);
8395
- const trimmed = firstByte.trim();
8396
- if (trimmed !== "{") {
8397
- } else {
8398
- const tail = n ?? 50;
8399
- return denyOutput(
8400
- "Task output " + id + ' is a JSONL agent transcript on disk. Use `token-goat bash-output --file "' + outPath + '" --transcript` to read the assistant text, then narrow with `--grep PATTERN` or `--tail ' + tail + '`, or read a specific line range (the only way to reach the MIDDLE of a large artifact) with `token-goat read "' + outPath + '@START-END"`, instead of reading the whole file.'
8401
- );
8402
- }
8403
- } catch {
8553
+ if (taskOutputIsJsonlTranscript(outPath)) {
8554
+ const tail = n ?? 50;
8555
+ return denyOutput(
8556
+ "Task output " + id + ' is a JSONL agent transcript on disk. Use `token-goat bash-output --file "' + outPath + '" --transcript` to read the assistant text, then narrow with `--grep PATTERN` or `--tail ' + tail + '`, or read a specific line range (the only way to reach the MIDDLE of a large artifact) with `token-goat read "' + outPath + '@START-END"`, instead of reading the whole file.'
8557
+ );
8404
8558
  }
8405
8559
  }
8406
8560
  const toolResults = extractToolResultsFile(cmd);
@@ -8519,12 +8673,17 @@ function preBashHandlerInner(event) {
8519
8673
  }
8520
8674
  const pyRead = extractPythonFileRead(cmd);
8521
8675
  if (pyRead !== null) {
8522
- const { filePath, isDoc, isTranscript } = pyRead;
8676
+ const { filePath, isDoc, isOutputFile } = pyRead;
8523
8677
  const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
8524
8678
  recordStat("session_hint", 0, 0);
8525
- if (isTranscript) {
8526
- const tHint = '`.output` files are JSONL agent transcripts. Use `token-goat bash-output --file "' + hintPath + '" --transcript` to read the assistant text, then narrow with `--grep PATTERN` or `--tail N`, instead of hand-parsing the JSONL.';
8527
- return cdStripped ? contextOutput(tHint) : denyOutput(tHint);
8679
+ if (isOutputFile) {
8680
+ if (taskOutputIsJsonlTranscript(filePath)) {
8681
+ const tHint = 'This `.output` file is a JSONL agent transcript. Use `token-goat bash-output --file "' + hintPath + '" --transcript` to read the assistant text, then narrow with `--grep PATTERN` or `--tail N`, instead of hand-parsing the JSONL.';
8682
+ return cdStripped ? contextOutput(tHint) : denyOutput(tHint);
8683
+ }
8684
+ return contextOutput(
8685
+ "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.'
8686
+ );
8528
8687
  }
8529
8688
  const hint = isDoc ? 'Use `token-goat section "' + hintPath + '::SectionHeading"` to read one section.' : 'Use `token-goat read "' + hintPath + '::SymbolName"` to extract a specific symbol.';
8530
8689
  return cdStripped ? contextOutput("Python `open()` file reads bypass read hooks. " + hint) : denyOutput("Python `open()` file reads bypass read hooks. " + hint);
@@ -8538,10 +8697,10 @@ function preBashHandlerInner(event) {
8538
8697
  }
8539
8698
  const headResult = extractHeadFile(cmd);
8540
8699
  if (headResult !== null) {
8541
- const { filePath, isDoc, isConfig, isSql } = headResult;
8700
+ const { filePath, isDoc, isConfig, isSql, n } = headResult;
8542
8701
  const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
8543
8702
  recordStat("session_hint", 0, 0);
8544
- return contextOutput("`head` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql));
8703
+ return contextOutput(leadingLinesHint("`head` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql }, preHookCwd));
8545
8704
  }
8546
8705
  const gcTailResult = extractGetContentTail(cmd);
8547
8706
  if (gcTailResult !== null) {
@@ -8552,10 +8711,10 @@ function preBashHandlerInner(event) {
8552
8711
  }
8553
8712
  const gcSelectResult = extractGetContentSelectFirst(cmd);
8554
8713
  if (gcSelectResult !== null) {
8555
- const { filePath, isDoc, isConfig, isSql } = gcSelectResult;
8714
+ const { filePath, isDoc, isConfig, isSql, n } = gcSelectResult;
8556
8715
  const hintPath = cdStripped ? resolveCdHintPath(rawCmd, filePath, hintCwd) : filePath;
8557
8716
  recordStat("session_hint", 0, 0);
8558
- return contextOutput("`Select-Object -First` bypasses read hooks. " + surgicalHintForConfigDoc(hintPath, isConfig, isDoc, isSql));
8717
+ return contextOutput(leadingLinesHint("`Select-Object -First` bypasses read hooks. ", hintPath, 1, n, { isConfig, isDoc, isSql }, preHookCwd));
8559
8718
  }
8560
8719
  const nodeRead = extractNodeFileRead(cmd);
8561
8720
  if (nodeRead !== null) {
@@ -9092,6 +9251,89 @@ function postTaskOutputHandler(event) {
9092
9251
  }
9093
9252
  registerHook("post_tool_use", postTaskOutputHandler, { toolName: "TaskOutput" });
9094
9253
 
9254
+ // src/hooks_tool_failure.ts
9255
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
9256
+ import { dirname as dirname2 } from "node:path";
9257
+ var FAILURE_SUFFIX = ".tool-failures.json";
9258
+ var MAX_TRACKED_FAILURES = 64;
9259
+ var SIGNATURE_ERROR_CHARS = 200;
9260
+ function failureSignature(toolName, errorText) {
9261
+ const tool = toolName === void 0 || toolName === "" ? "unknown" : toolName;
9262
+ const normalized = errorText.replace(/\s+/g, " ").trim().slice(0, SIGNATURE_ERROR_CHARS);
9263
+ return `${tool} :: ${normalized}`;
9264
+ }
9265
+ function extractFailureText(raw) {
9266
+ for (const key of ["error", "errorMessage", "tool_error", "toolError", "message", "reason"]) {
9267
+ const value = raw[key];
9268
+ if (typeof value === "string" && value.trim() !== "") return value;
9269
+ }
9270
+ const response = raw["tool_response"] ?? raw["toolResponse"];
9271
+ if (typeof response === "string" && response.trim() !== "") return response;
9272
+ if (response !== null && typeof response === "object") {
9273
+ const nested = response["error"];
9274
+ if (typeof nested === "string" && nested.trim() !== "") return nested;
9275
+ }
9276
+ return "";
9277
+ }
9278
+ function ledgerPath(sessionId) {
9279
+ return sessionSidecarPath(sessionId, FAILURE_SUFFIX);
9280
+ }
9281
+ function readLedger(target) {
9282
+ try {
9283
+ const parsed = JSON.parse(readFileSync3(target, "utf8"));
9284
+ if (parsed !== null && typeof parsed === "object") {
9285
+ const seen = parsed.seen;
9286
+ const order = parsed.order;
9287
+ if (seen !== null && typeof seen === "object" && Array.isArray(order)) {
9288
+ return { seen, order: order.filter((k) => typeof k === "string") };
9289
+ }
9290
+ }
9291
+ } catch {
9292
+ }
9293
+ return { seen: {}, order: [] };
9294
+ }
9295
+ function writeLedger(target, ledger) {
9296
+ try {
9297
+ mkdirSync2(dirname2(target), { recursive: true });
9298
+ writeFileSync2(target, JSON.stringify(ledger), "utf8");
9299
+ } catch {
9300
+ }
9301
+ }
9302
+ function repeatFailureNotice(toolName) {
9303
+ const tool = toolName === void 0 || toolName === "" ? "This tool" : toolName;
9304
+ 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.`;
9305
+ }
9306
+ function postToolUseFailureHandler(event) {
9307
+ try {
9308
+ if (!event.sessionId) return passOutput();
9309
+ const errorText = extractFailureText(event.raw);
9310
+ if (errorText === "") return passOutput();
9311
+ const target = ledgerPath(event.sessionId);
9312
+ if (target === null) return passOutput();
9313
+ const toolName = getToolName(event);
9314
+ const signature = failureSignature(toolName, errorText);
9315
+ const ledger = readLedger(target);
9316
+ const priorState = ledger.seen[signature];
9317
+ if (priorState === void 0) {
9318
+ ledger.seen[signature] = false;
9319
+ ledger.order.push(signature);
9320
+ while (ledger.order.length > MAX_TRACKED_FAILURES) {
9321
+ const evicted = ledger.order.shift();
9322
+ if (evicted !== void 0) delete ledger.seen[evicted];
9323
+ }
9324
+ writeLedger(target, ledger);
9325
+ return passOutput();
9326
+ }
9327
+ if (priorState) return passOutput();
9328
+ ledger.seen[signature] = true;
9329
+ writeLedger(target, ledger);
9330
+ return contextOutput(repeatFailureNotice(toolName));
9331
+ } catch {
9332
+ return passOutput();
9333
+ }
9334
+ }
9335
+ registerHook("post_tool_use_failure", postToolUseFailureHandler);
9336
+
9095
9337
  // src/hooks_exitplanmode.ts
9096
9338
  var PLAN_OMIT_POINTER = "[token-goat: plan body omitted -- identical to this call's own tool_input, already saved to the plans file]";
9097
9339
  var APPROVED_PLAN_MARKER = "## Approved Plan:";
@@ -9646,6 +9888,7 @@ function preScreenshotHandler(event) {
9646
9888
  registerHook("pre_tool_use", preScreenshotHandler, { toolPattern: "^mcp__" });
9647
9889
 
9648
9890
  // src/hooks_browser_image.ts
9891
+ import { createHash } from "node:crypto";
9649
9892
  function getResponseContentBlocks(raw) {
9650
9893
  const tr = raw["tool_response"];
9651
9894
  if (!tr || typeof tr !== "object") return null;
@@ -9653,6 +9896,10 @@ function getResponseContentBlocks(raw) {
9653
9896
  return Array.isArray(content) ? content : null;
9654
9897
  }
9655
9898
  var TAB_CONTEXT_RE = /^\n*Tab Context:/;
9899
+ var SCREENSHOT_REPEAT_NOTICE = "[token-goat: screenshot identical to one already shown this session; not re-sent. Nothing on the page has changed since.]";
9900
+ function screenshotFingerprint(data) {
9901
+ return createHash("sha256").update(data).digest("hex").slice(0, 32);
9902
+ }
9656
9903
  async function shrinkImageBlock(block) {
9657
9904
  const source = block.source;
9658
9905
  const mediaType = typeof source?.media_type === "string" ? source.media_type : "image/png";
@@ -9665,7 +9912,25 @@ async function shrinkImageBlock(block) {
9665
9912
  } catch {
9666
9913
  return { text: originalDataUrl, changed: false, savedBytes: 0 };
9667
9914
  }
9668
- const result = await shrinkImage(buffer);
9915
+ const fingerprint = screenshotFingerprint(data);
9916
+ const alreadyShown = hasSeenImage(fingerprint);
9917
+ recordSeenImage(fingerprint);
9918
+ if (alreadyShown) {
9919
+ const worthwhile = isRewriteWorthwhile({
9920
+ originalBytes: originalDataUrl.length,
9921
+ rewrittenBytes: 0,
9922
+ noticeBytes: Buffer.byteLength(SCREENSHOT_REPEAT_NOTICE, "utf-8"),
9923
+ minNetSavingsBytes: resolveMinNetSavingsBytes()
9924
+ });
9925
+ if (worthwhile) {
9926
+ return {
9927
+ text: SCREENSHOT_REPEAT_NOTICE,
9928
+ changed: true,
9929
+ savedBytes: originalDataUrl.length - SCREENSHOT_REPEAT_NOTICE.length
9930
+ };
9931
+ }
9932
+ }
9933
+ const result = await imageQualifiesForShrink(buffer) ? await shrinkImage(buffer, { sizeThresholdBytes: 0 }) : null;
9669
9934
  if (result === null) return { text: originalDataUrl, changed: false, savedBytes: 0 };
9670
9935
  const saved = result.originalBytes - result.shrunkBytes;
9671
9936
  const { summary, dataUrl } = formatShrinkSummary(result, "an inline browser screenshot");
@@ -9725,7 +9990,7 @@ async function postBrowserImageHandler(event) {
9725
9990
  registerHook("post_tool_use", postBrowserImageHandler, { toolPattern: "^mcp__" });
9726
9991
 
9727
9992
  // src/hooks_agent_spawn.ts
9728
- import { createHash } from "node:crypto";
9993
+ import { createHash as createHash2 } from "node:crypto";
9729
9994
  var BRIEFING_TARGET_TOKENS = 550;
9730
9995
  function buildSubagentBriefing() {
9731
9996
  try {
@@ -9870,7 +10135,7 @@ function dedupeFencedBlocks(collapsed, original, recallHint) {
9870
10135
  const collapsedBlocks = findFencedBlockLines(collapsedLines);
9871
10136
  if (collapsedBlocks.length !== originalBlocks.length) return collapsed;
9872
10137
  const bodyHashes = originalBlocks.map(
9873
- ({ fenceStart, closerIndex }) => createHash("sha256").update(originalLines.slice(fenceStart + 1, closerIndex).join("\n")).digest("hex")
10138
+ ({ fenceStart, closerIndex }) => createHash2("sha256").update(originalLines.slice(fenceStart + 1, closerIndex).join("\n")).digest("hex")
9874
10139
  );
9875
10140
  const firstOccurrenceOf = /* @__PURE__ */ new Map();
9876
10141
  const out = [];
@@ -9989,6 +10254,7 @@ function harnessForNormalization() {
9989
10254
  if (detected === "gemini") return "gemini";
9990
10255
  if (detected === "grok") return "grok";
9991
10256
  if (detected === "kimi") return "kimi";
10257
+ if (detected === "copilot_cli") return "copilot_cli";
9992
10258
  return "claude";
9993
10259
  }
9994
10260
  async function relayInProcess(eventName, rawPayload) {
@@ -9996,6 +10262,7 @@ async function relayInProcess(eventName, rawPayload) {
9996
10262
  if (!isHookEventName(eventName)) {
9997
10263
  return "{}";
9998
10264
  }
10265
+ const harness = detectHarness();
9999
10266
  const payload = eventName === "pre_tool_use" || eventName === "post_tool_use" ? normalizePayload(rawPayload, harnessForNormalization()) : rawPayload;
10000
10267
  const event = buildEvent(eventName, payload);
10001
10268
  if (!process.env["CLAUDE_CODE_SESSION_ID"] && event.sessionId) {
@@ -10011,7 +10278,7 @@ async function relayInProcess(eventName, rawPayload) {
10011
10278
  saveSessionState(stateKey);
10012
10279
  } catch {
10013
10280
  }
10014
- return serializeOutput(output, event.eventName);
10281
+ return serializeOutput(output, event.eventName, harness);
10015
10282
  } catch {
10016
10283
  return "{}";
10017
10284
  }