evrex-mcp 0.8.1 → 0.9.0

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.
package/dist/import.js CHANGED
@@ -184,14 +184,34 @@ function commitMeta(repoPath, sha) {
184
184
  sha
185
185
  ]).trim();
186
186
  const parts = line.split(FIELD_SEP);
187
- const message = git(repoPath, ["show", "-s", "--format=%B", sha]).replace(/\n+$/, "");
187
+ const body = git(repoPath, ["show", "-s", "--notes", `--format=%B${FIELD_SEP}%N`, sha]);
188
+ const sep2 = body.indexOf(FIELD_SEP);
189
+ let message = (sep2 === -1 ? body : body.slice(0, sep2)).replace(/\n+$/, "");
190
+ const note = sep2 === -1 ? "" : body.slice(sep2 + FIELD_SEP.length);
191
+ const noted = /^Evrex-Session:\s*(\S+)\s*$/m.exec(note);
192
+ if (noted && !/^Evrex-Session:/m.test(message)) {
193
+ message = `${message}
194
+
195
+ ${noted[0].trim()}`;
196
+ }
188
197
  return {
189
198
  sha: parts[0] ?? sha,
190
199
  author: parts[1] ?? "",
191
200
  authorEmail: parts[2] ?? "",
192
201
  ts: parts[3] ?? (/* @__PURE__ */ new Date()).toISOString(),
193
202
  parents: (parts[4] ?? "").split(" ").filter(Boolean),
194
- message
203
+ message,
204
+ // The note's session, exposed on its own as well as folded above. When
205
+ // the message ALREADY carries a trailer, the fold declines (never rewrite
206
+ // what was stamped at commit time) — but the parse must still prefer the
207
+ // note: it exists only because an operator pressed "verify" on a commit
208
+ // whose message trailer names a session no ingest can resolve. Commit
209
+ // 843dabb3 is the standing example — its at-commit-time trailer names a
210
+ // session with no transcript anywhere, so the folding guard alone left it
211
+ // permanently un-verifiable: every promote re-ingested the dangling id,
212
+ // bestLink demoted it below the resolvable match, and the button offered
213
+ // the same commit again.
214
+ notedSession: noted?.[1] ?? null
195
215
  };
196
216
  }
197
217
  function parseTrailers(repoPath, message) {
@@ -312,6 +332,9 @@ function commitFiles(repoPath, sha) {
312
332
  }
313
333
  function parseGitLog(repoPath, repoId, known) {
314
334
  const shas = listShas(repoPath).filter((sha) => !known?.has(sha));
335
+ return parseCommitsBySha(repoPath, shas, repoId);
336
+ }
337
+ function parseCommitsBySha(repoPath, shas, repoId) {
315
338
  const id = repoId ?? deriveRepoId(repoPath);
316
339
  return shas.map((sha) => {
317
340
  const meta = commitMeta(repoPath, sha);
@@ -326,7 +349,9 @@ function parseGitLog(repoPath, repoId, known) {
326
349
  message: meta.message,
327
350
  parents: meta.parents,
328
351
  branch: commitBranch(repoPath, sha),
329
- evrexSessionTrailer: trailerValue(trailers, EVREX_SESSION_TRAILER_KEY),
352
+ // A note is the operator's explicit later correction, so where both
353
+ // exist it names the session — see commitMeta's notedSession.
354
+ evrexSessionTrailer: meta.notedSession ?? trailerValue(trailers, EVREX_SESSION_TRAILER_KEY),
330
355
  origin: originOf(trailers),
331
356
  agentTrailers: agentTrailersOf(trailers),
332
357
  statedInsights: statedInsightsOf(trailers),
@@ -407,7 +432,7 @@ var PARSER_VERSION;
407
432
  var init_incremental = __esm({
408
433
  "../../packages/ingest-core/src/incremental.ts"() {
409
434
  "use strict";
410
- PARSER_VERSION = 4;
435
+ PARSER_VERSION = 9;
411
436
  }
412
437
  });
413
438
 
@@ -522,11 +547,54 @@ var init_redact = __esm({
522
547
 
523
548
  // ../../packages/ingest-core/src/subject-linking.ts
524
549
  function matchCommitToSession(commit, sessions) {
550
+ const saw = sessions.filter(
551
+ (s) => (s.committedShas ?? []).some(
552
+ (short) => short.length >= 7 && commit.sha.startsWith(short)
553
+ )
554
+ );
555
+ if (saw.length > 0) {
556
+ const oneLineage2 = saw.every((s) => s.startedAt === saw[0].startedAt);
557
+ if (!oneLineage2) {
558
+ return { sha: commit.sha, sessionId: null, reason: "ambiguous" };
559
+ }
560
+ const original2 = [...saw].sort((a, b) => a.endedAt - b.endedAt)[0];
561
+ return {
562
+ sha: commit.sha,
563
+ sessionId: original2.sessionId,
564
+ evidence: "saw-the-sha",
565
+ confidence: SHA_CONFIDENCE
566
+ };
567
+ }
525
568
  const subject = commit.subject.trim();
569
+ const prMatch = /\(#(\d+)\)$/.exec(subject) ?? /^Merge pull request #(\d+)\b/.exec(subject);
570
+ if (prMatch) {
571
+ const pr = Number(prMatch[1]);
572
+ const mergedIt = sessions.filter(
573
+ (s) => (s.mergedPrNumbers ?? []).includes(pr)
574
+ );
575
+ if (mergedIt.length > 0) {
576
+ const oneLineage2 = mergedIt.every(
577
+ (s) => s.startedAt === mergedIt[0].startedAt
578
+ );
579
+ if (!oneLineage2) {
580
+ return { sha: commit.sha, sessionId: null, reason: "ambiguous" };
581
+ }
582
+ const original2 = [...mergedIt].sort((a, b) => a.endedAt - b.endedAt)[0];
583
+ return {
584
+ sha: commit.sha,
585
+ sessionId: original2.sessionId,
586
+ evidence: "merged-the-pr",
587
+ confidence: SHA_CONFIDENCE
588
+ };
589
+ }
590
+ }
526
591
  if (subject.length === 0) {
527
592
  return { sha: commit.sha, sessionId: null, reason: "absent" };
528
593
  }
529
- const ran = sessions.filter((s) => s.committedSubjects.includes(subject));
594
+ const unsquashed = subject.replace(/\s+\(#\d+\)$/, "");
595
+ const ran = sessions.filter(
596
+ (s) => s.committedSubjects.includes(subject) || unsquashed !== subject && s.committedSubjects.includes(unsquashed)
597
+ );
530
598
  if (ran.length === 0) {
531
599
  return { sha: commit.sha, sessionId: null, reason: "absent" };
532
600
  }
@@ -580,15 +648,61 @@ function committedSubjects(command) {
580
648
  if (inline) {
581
649
  const subject = inline[2].split("\n")[0].trim();
582
650
  if (subject) out.push(subject);
651
+ continue;
652
+ }
653
+ const marginal = /^[^\n]*?-am\s*(['"])([\s\S]*?)\1/.exec(rest) ?? /^[^\n]*?--message[= ]\s*(['"])([\s\S]*?)\1/.exec(rest) ?? /^[^\n]*?-m(['"])([\s\S]*?)\1/.exec(rest);
654
+ if (marginal) {
655
+ const subject = marginal[2].split("\n")[0].trim();
656
+ if (subject) out.push(subject);
657
+ continue;
658
+ }
659
+ const fromFile = /^[^\n]*?(?:-F|--file)[= ]\s*(\S+)/.exec(rest);
660
+ if (fromFile) {
661
+ const file = fromFile[1].replace(/^["']|["']$/g, "");
662
+ const escaped = file.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
663
+ const wrote = new RegExp(
664
+ `(?:cat\\s*>>?|tee\\s+(?:-a\\s+)?)\\s*["']?${escaped}["']?[^\\n]*<<-?\\s*(['"]?)(\\w+)\\1[^\\n]*\\r?\\n([^\\n]*)`
665
+ ).exec(command.slice(0, found.index));
666
+ if (wrote) {
667
+ const subject = wrote[3].trim();
668
+ if (subject) out.push(subject);
669
+ }
583
670
  }
584
671
  }
585
672
  return out;
586
673
  }
587
- var INFERRED_CONFIDENCE;
674
+ function mergeResponseShas(output) {
675
+ const out = /* @__PURE__ */ new Set();
676
+ const line = /"sha"\s*:\s*"([0-9a-f]{40})"/g;
677
+ for (let m = line.exec(output); m !== null; m = line.exec(output)) {
678
+ out.add(m[1]);
679
+ }
680
+ return [...out];
681
+ }
682
+ function commitShaOutputs(output) {
683
+ const out = /* @__PURE__ */ new Set();
684
+ const line = /\[[\w./+-]+ (?:\(root-commit\) )?([0-9a-f]{7,12})\]/g;
685
+ for (let m = line.exec(output); m !== null; m = line.exec(output)) {
686
+ out.add(m[1]);
687
+ }
688
+ return [...out];
689
+ }
690
+ function mergedPrFromCommand(command) {
691
+ const api = /\/pulls\/(\d+)\/merge\b/.exec(command);
692
+ if (api) return Number(api[1]);
693
+ const gh = /\bgh\s+pr\s+merge\s+(?:.*?\/pull\/(\d+)\b|(\d+)\b)/.exec(command);
694
+ if (gh) return Number(gh[1] ?? gh[2]);
695
+ return null;
696
+ }
697
+ function mergeSucceeded(output) {
698
+ return /"merged"\s*:\s*true/.test(output) || /(Squashed and |Rebased and )?[Mm]erged pull request #\d+/.test(output);
699
+ }
700
+ var INFERRED_CONFIDENCE, SHA_CONFIDENCE;
588
701
  var init_subject_linking = __esm({
589
702
  "../../packages/ingest-core/src/subject-linking.ts"() {
590
703
  "use strict";
591
704
  INFERRED_CONFIDENCE = 0.9;
705
+ SHA_CONFIDENCE = 0.95;
592
706
  }
593
707
  });
594
708
 
@@ -663,14 +777,30 @@ function findSessionFiles(repoPath) {
663
777
  function editFromXcodeInput(input) {
664
778
  const filePath = input.filePath;
665
779
  if (typeof filePath !== "string" || !filePath) return null;
666
- const oldLines = typeof input.oldString === "string" ? input.oldString.split("\n") : [];
667
- const newLines = typeof input.newString === "string" ? input.newString.split("\n") : [];
780
+ return editFromStrings(filePath, textOf(input.oldString), textOf(input.newString));
781
+ }
782
+ function textOf(value) {
783
+ return typeof value === "string" ? value : "";
784
+ }
785
+ function editFromStrings(path, before, after) {
786
+ const oldLines = before ? before.split("\n") : [];
787
+ const newLines = after ? after.split("\n") : [];
668
788
  const oldSet = new Set(oldLines);
669
789
  const newSet = new Set(newLines);
670
790
  const added = meaningfulLines(newLines.filter((l) => !oldSet.has(l)));
671
791
  const removed = meaningfulLines(oldLines.filter((l) => !newSet.has(l)));
672
792
  if (added.length === 0 && removed.length === 0) return null;
673
- return { path: filePath, added, removed };
793
+ return { path, added, removed };
794
+ }
795
+ function editFromClaudeInput(name, input) {
796
+ const path = input.file_path;
797
+ if (typeof path !== "string" || !path) return null;
798
+ if (name === "Write") return editFromStrings(path, "", textOf(input.content));
799
+ if (name === "Edit") return editFromStrings(path, textOf(input.old_string), textOf(input.new_string));
800
+ if (name !== "MultiEdit" || !Array.isArray(input.edits)) return null;
801
+ const parts = input.edits.map((e) => editFromStrings(path, textOf(e?.old_string), textOf(e?.new_string))).filter((e) => e !== null);
802
+ if (parts.length === 0) return null;
803
+ return { path, added: parts.flatMap((p) => p.added), removed: parts.flatMap((p) => p.removed) };
674
804
  }
675
805
  function extractPathsFromText(text) {
676
806
  const matches = text.match(PATH_TOKEN_RE) ?? [];
@@ -687,6 +817,7 @@ function extractFromAssistantContent(content) {
687
817
  const textParts = [];
688
818
  const filesTouched = [];
689
819
  const editedLines = [];
820
+ const inputEdits = /* @__PURE__ */ new Map();
690
821
  for (const block of content) {
691
822
  if (block.type === "text" && block.text) {
692
823
  textParts.push(block.text);
@@ -707,6 +838,10 @@ function extractFromAssistantContent(content) {
707
838
  } else if (FILE_PATH_TOOLS.has(name) && typeof input.file_path === "string") {
708
839
  textParts.push(`[tool_call: ${name}] ${input.file_path}`);
709
840
  filesTouched.push({ path: input.file_path, source: "tool_path", tool: name });
841
+ if (CLAUDE_EDIT_TOOLS.has(name) && block.id) {
842
+ const edit = editFromClaudeInput(name, input);
843
+ if (edit) inputEdits.set(block.id, edit);
844
+ }
710
845
  } else if (name === "Bash" && typeof input.command === "string") {
711
846
  const desc = typeof input.description === "string" ? input.description : input.command;
712
847
  textParts.push(`[tool_call: Bash] ${desc}`);
@@ -718,7 +853,7 @@ function extractFromAssistantContent(content) {
718
853
  }
719
854
  }
720
855
  }
721
- return { text: textParts.join("\n"), filesTouched, editedLines };
856
+ return { text: textParts.join("\n"), filesTouched, editedLines, inputEdits };
722
857
  }
723
858
  function extractFromUserContent(content) {
724
859
  if (typeof content === "string") {
@@ -800,14 +935,23 @@ function readSubagentMeta(transcriptPath) {
800
935
  const metaPath = transcriptPath.replace(/\.jsonl$/, ".meta.json");
801
936
  try {
802
937
  const raw = JSON.parse(readFileSync(metaPath, "utf-8"));
938
+ const text = (key) => typeof raw[key] === "string" && raw[key] ? raw[key] : null;
803
939
  return {
804
- agentType: typeof raw.agentType === "string" ? raw.agentType : null,
805
- description: typeof raw.description === "string" ? raw.description : null
940
+ agentType: text("agentType"),
941
+ description: text("description"),
942
+ parentAgentId: text("parentAgentId"),
943
+ // The upload's DTO takes an integer >= 0; one malformed sidecar must not
944
+ // 400 the whole batch it travels in.
945
+ spawnDepth: Number.isInteger(raw.spawnDepth) && raw.spawnDepth >= 0 ? raw.spawnDepth : null,
946
+ toolUseId: text("toolUseId")
806
947
  };
807
948
  } catch {
808
949
  return null;
809
950
  }
810
951
  }
952
+ function subagentSessionId(rootSessionId, agentId) {
953
+ return deriveUuid(`evrex-subagent\0${rootSessionId}\0${agentId}`);
954
+ }
811
955
  function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), agentKind = "claude-code") {
812
956
  const { text: raw, capped } = readTranscript(filePath);
813
957
  const lines = raw.split("\n").filter((l) => l.trim().length > 0);
@@ -820,6 +964,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
820
964
  let branch = null;
821
965
  let agentId = null;
822
966
  const seenIds = /* @__PURE__ */ new Set();
967
+ const callEdits = /* @__PURE__ */ new Map();
823
968
  for (const line of lines) {
824
969
  let record;
825
970
  try {
@@ -852,6 +997,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
852
997
  text = extracted.text;
853
998
  filesTouched = extracted.filesTouched;
854
999
  editedLines = extracted.editedLines;
1000
+ for (const [toolUseId, edit] of extracted.inputEdits) callEdits.set(toolUseId, edit);
855
1001
  }
856
1002
  } else {
857
1003
  const extracted = extractFromUserContent(record.message?.content);
@@ -861,6 +1007,13 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
861
1007
  isToolError = extracted.isToolError ?? false;
862
1008
  const edited = extractEditedLines(record.toolUseResult);
863
1009
  if (edited) editedLines = [edited];
1010
+ const content = record.message?.content;
1011
+ for (const block of Array.isArray(content) ? content : []) {
1012
+ if (block.type !== "tool_result" || !block.tool_use_id) continue;
1013
+ const fromCall = callEdits.get(block.tool_use_id);
1014
+ callEdits.delete(block.tool_use_id);
1015
+ if (fromCall && !edited && block.is_error !== true) editedLines.push(fromCall);
1016
+ }
864
1017
  }
865
1018
  const redacted = redactSecrets(text);
866
1019
  totalRedactions += redacted.count;
@@ -881,15 +1034,24 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
881
1034
  });
882
1035
  }
883
1036
  if (!sessionId || turns.length === 0) return null;
884
- const parentSessionId = agentId ? sessionId : null;
885
- if (agentId) {
886
- sessionId = deriveUuid(`evrex-subagent\0${parentSessionId}\0${agentId}`);
1037
+ const rootSessionId = agentId ? sessionId : null;
1038
+ const meta = agentId ? readSubagentMeta(filePath) : null;
1039
+ const parentSessionId = agentId && rootSessionId ? meta?.parentAgentId ? subagentSessionId(rootSessionId, meta.parentAgentId) : rootSessionId : null;
1040
+ if (agentId && rootSessionId) {
1041
+ sessionId = subagentSessionId(rootSessionId, agentId);
887
1042
  for (const t of turns) t.sessionId = sessionId;
888
1043
  }
889
- const meta = agentId ? readSubagentMeta(filePath) : null;
890
- const subagent = agentId ? { agentId, agentType: meta?.agentType ?? null, description: meta?.description ?? null } : null;
1044
+ const subagent = agentId ? {
1045
+ agentId,
1046
+ agentType: meta?.agentType ?? null,
1047
+ description: meta?.description ?? null,
1048
+ rootSessionId,
1049
+ spawnDepth: meta?.spawnDepth ?? null,
1050
+ toolUseId: meta?.toolUseId ?? null
1051
+ } : null;
891
1052
  if (agentId && !aiTitle && meta?.description) aiTitle = meta.description;
892
1053
  const sortedTs = turns.map((t) => t.ts).sort();
1054
+ const commitEvidence = collectCommitEvidence(lines);
893
1055
  const rawContent = capped ? "" : lines.map((line) => {
894
1056
  try {
895
1057
  return JSON.stringify(redactJsonValue(JSON.parse(line)).value);
@@ -913,7 +1075,9 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
913
1075
  // filled in by the caller — see getGitUserName in git-history.ts
914
1076
  sourceFile: filePath,
915
1077
  redactionCount: totalRedactions,
916
- committedSubjects: collectCommittedSubjects(lines),
1078
+ committedSubjects: commitEvidence.subjects,
1079
+ committedShas: commitEvidence.shas,
1080
+ mergedPrNumbers: commitEvidence.prNumbers,
917
1081
  branch,
918
1082
  parentSessionId,
919
1083
  subagent,
@@ -922,10 +1086,14 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
922
1086
  turns
923
1087
  };
924
1088
  }
925
- function collectCommittedSubjects(lines) {
1089
+ function collectCommitEvidence(lines) {
926
1090
  const subjects = /* @__PURE__ */ new Set();
1091
+ const shas = /* @__PURE__ */ new Set();
1092
+ const prNumbers = /* @__PURE__ */ new Set();
1093
+ const commitCallIds = /* @__PURE__ */ new Set();
1094
+ const mergeCallIds = /* @__PURE__ */ new Map();
927
1095
  for (const line of lines) {
928
- if (!line.includes("git commit")) continue;
1096
+ if (!line.includes("git commit") && !line.includes("/merge") && !line.includes("tool_use_id")) continue;
929
1097
  let record;
930
1098
  try {
931
1099
  record = JSON.parse(line);
@@ -935,13 +1103,40 @@ function collectCommittedSubjects(lines) {
935
1103
  const content = record?.message?.content;
936
1104
  if (!Array.isArray(content)) continue;
937
1105
  for (const block of content) {
938
- if (block?.type !== "tool_use") continue;
939
- const command = block?.input?.command;
940
- if (typeof command !== "string") continue;
941
- for (const subject of committedSubjects(command)) subjects.add(subject);
1106
+ if (block?.type === "tool_use") {
1107
+ const command = block?.input?.command;
1108
+ if (typeof command !== "string") continue;
1109
+ const found = committedSubjects(command);
1110
+ for (const subject of found) subjects.add(subject);
1111
+ if (/\bgit\s+(?:-C\s+\S+\s+)?commit\b/.test(command) && typeof block?.id === "string") {
1112
+ commitCallIds.add(block.id);
1113
+ }
1114
+ if ((/\/pulls\/\d+\/merge\b/.test(command) || /\bgh\s+pr\s+merge\b/.test(command)) && typeof block?.id === "string") {
1115
+ mergeCallIds.set(block.id, mergedPrFromCommand(command));
1116
+ }
1117
+ continue;
1118
+ }
1119
+ if (block?.type === "tool_result" && mergeCallIds.has(block?.tool_use_id)) {
1120
+ const raw = block?.content;
1121
+ const text = typeof raw === "string" ? raw : Array.isArray(raw) ? raw.map(
1122
+ (x) => typeof x?.text === "string" ? x.text : ""
1123
+ ).join(" ") : "";
1124
+ for (const sha of mergeResponseShas(text)) shas.add(sha);
1125
+ if (mergeSucceeded(text)) {
1126
+ const pr = mergeCallIds.get(block?.tool_use_id);
1127
+ if (typeof pr === "number") prNumbers.add(pr);
1128
+ }
1129
+ }
1130
+ if (block?.type === "tool_result" && commitCallIds.has(block?.tool_use_id)) {
1131
+ const raw = block?.content;
1132
+ const text = typeof raw === "string" ? raw : Array.isArray(raw) ? raw.map(
1133
+ (x) => typeof x?.text === "string" ? x.text : ""
1134
+ ).join(" ") : "";
1135
+ for (const sha of commitShaOutputs(text)) shas.add(sha);
1136
+ }
942
1137
  }
943
1138
  }
944
- return [...subjects];
1139
+ return { subjects: [...subjects], shas: [...shas], prNumbers: [...prNumbers] };
945
1140
  }
946
1141
  function parseAllSessions(repoPath, repoId, cursors = {}) {
947
1142
  const id = repoId ?? deriveRepoId(repoPath);
@@ -961,7 +1156,7 @@ function parseAllSessions(repoPath, repoId, cursors = {}) {
961
1156
  }
962
1157
  return { sessions, cursors: next, skipped };
963
1158
  }
964
- var MIN_MEANINGFUL_LINE_LENGTH, MAX_TURN_TEXT_LENGTH, FILE_PATH_TOOLS, XCODE_EDIT_TOOL, XCODE_PATH_TOOLS, PATH_TOKEN_RE, SYNTHETIC_CONTENT_RE, MAX_TRANSCRIPT_BYTES;
1159
+ var MIN_MEANINGFUL_LINE_LENGTH, MAX_TURN_TEXT_LENGTH, FILE_PATH_TOOLS, XCODE_EDIT_TOOL, XCODE_PATH_TOOLS, CLAUDE_EDIT_TOOLS, PATH_TOKEN_RE, SYNTHETIC_CONTENT_RE, MAX_TRANSCRIPT_BYTES;
965
1160
  var init_claude_sessions = __esm({
966
1161
  "../../packages/ingest-core/src/claude-sessions.ts"() {
967
1162
  "use strict";
@@ -973,9 +1168,10 @@ var init_claude_sessions = __esm({
973
1168
  init_types();
974
1169
  MIN_MEANINGFUL_LINE_LENGTH = 6;
975
1170
  MAX_TURN_TEXT_LENGTH = 4e3;
976
- FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "NotebookEdit"]);
1171
+ FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
977
1172
  XCODE_EDIT_TOOL = /XcodeUpdate$/;
978
1173
  XCODE_PATH_TOOLS = /Xcode(Read|Grep|Glob|RefreshCodeIssuesInFile)$/;
1174
+ CLAUDE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
979
1175
  PATH_TOKEN_RE = /(?:[.~]?\/)?(?:[\w-]+\/)+[\w.-]+/g;
980
1176
  SYNTHETIC_CONTENT_RE = /^\s*(<task-notification>|<system-reminder>|\[SYSTEM NOTIFICATION)/;
981
1177
  MAX_TRANSCRIPT_BYTES = 256 * 1024 * 1024;
@@ -1907,16 +2103,58 @@ function addUsage(into, next) {
1907
2103
  model: into.model ?? next.model
1908
2104
  };
1909
2105
  }
1910
- function committedSubjectsFromToolCalls(lines) {
2106
+ function commitEvidenceFromToolCalls(lines) {
1911
2107
  const subjects = /* @__PURE__ */ new Set();
2108
+ const shas = /* @__PURE__ */ new Set();
2109
+ const prNumbers = /* @__PURE__ */ new Set();
2110
+ const commitCallIds = /* @__PURE__ */ new Set();
2111
+ const mergeCallIds = /* @__PURE__ */ new Map();
1912
2112
  for (const line of lines) {
1913
2113
  if (line.type !== "response_item") continue;
1914
- if (payloadType(line) !== "custom_tool_call") continue;
1915
- const input = asString(line.payload?.input);
1916
- if (!input) continue;
1917
- for (const subject of committedSubjects(input)) subjects.add(subject);
2114
+ const kind = payloadType(line);
2115
+ if (kind === "custom_tool_call") {
2116
+ const input = asString(line.payload?.input);
2117
+ if (!input) continue;
2118
+ const cmd = execCmdFrom(input) ?? input;
2119
+ const callId = asString(line.payload?.call_id);
2120
+ for (const subject of committedSubjects(cmd)) subjects.add(subject);
2121
+ if (/\bgit\s+(?:-C\s+\S+\s+)?commit\b/.test(cmd) && callId) {
2122
+ commitCallIds.add(callId);
2123
+ }
2124
+ if ((/\/pulls\/\d+\/merge\b/.test(cmd) || /\bgh\s+pr\s+merge\b/.test(cmd)) && callId) {
2125
+ mergeCallIds.set(callId, mergedPrFromCommand(cmd));
2126
+ }
2127
+ continue;
2128
+ }
2129
+ if (kind === "custom_tool_call_output") {
2130
+ const callId = asString(line.payload?.call_id);
2131
+ if (!callId) continue;
2132
+ const raw = line.payload?.output;
2133
+ const text = Array.isArray(raw) ? raw.map(
2134
+ (x) => typeof x?.text === "string" ? x.text : ""
2135
+ ).join(" ") : asString(raw) ?? "";
2136
+ if (commitCallIds.has(callId)) {
2137
+ for (const sha of commitShaOutputs(text)) shas.add(sha);
2138
+ }
2139
+ if (mergeCallIds.has(callId)) {
2140
+ for (const sha of mergeResponseShas(text)) shas.add(sha);
2141
+ if (mergeSucceeded(text)) {
2142
+ const pr = mergeCallIds.get(callId);
2143
+ if (typeof pr === "number") prNumbers.add(pr);
2144
+ }
2145
+ }
2146
+ }
2147
+ }
2148
+ return { subjects: [...subjects], shas: [...shas], prNumbers: [...prNumbers] };
2149
+ }
2150
+ function execCmdFrom(input) {
2151
+ const m = /cmd\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(input);
2152
+ if (!m) return null;
2153
+ try {
2154
+ return JSON.parse(`"${m[1]}"`);
2155
+ } catch {
2156
+ return null;
1918
2157
  }
1919
- return [...subjects];
1920
2158
  }
1921
2159
  function filesFromToolCalls(lines) {
1922
2160
  const byPath = /* @__PURE__ */ new Map();
@@ -1975,20 +2213,28 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
1975
2213
  const { content: rawContent, count: rawRedactions } = redactRollout(raw);
1976
2214
  redactionCount += rawRedactions;
1977
2215
  const firstUser = events.find((e) => e.role === "user");
2216
+ const codexCommitEvidence = commitEvidenceFromToolCalls(lines);
1978
2217
  return {
1979
2218
  id: sessionId,
1980
2219
  agentKind: "codex",
1981
2220
  branch: null,
1982
2221
  parentSessionId: null,
1983
2222
  subagent: null,
1984
- // Codex records the remote itself, so identity survives a moved or deleted
1985
- // checkout. Falls back to the caller's derivation when it is absent.
1986
- repoId: repoIdFromMeta(meta) ?? repoId,
2223
+ // The caller's derivation first: it reads the checkout's remote NOW,
2224
+ // which is the identity every other ingest path agrees on. The remote
2225
+ // recorded in session_meta is a historical fact — after the repo's
2226
+ // GitHub transfer, month-old rollouts kept re-minting the dead
2227
+ // jeffcheema id on every re-index, resurrecting a "third evrex-app"
2228
+ // as fast as migrations folded it. Meta only serves a rollout whose
2229
+ // checkout is gone, where a stale identity beats none.
2230
+ repoId: repoId.startsWith("path:") ? repoIdFromMeta(meta) ?? repoId : repoId,
1987
2231
  cwd,
1988
2232
  startedAt: asString(meta.timestamp) ?? events[0]?.ts ?? null,
1989
2233
  endedAt: events[events.length - 1]?.ts ?? null,
1990
2234
  turnCount: turns.length,
1991
- committedSubjects: committedSubjectsFromToolCalls(lines),
2235
+ committedSubjects: codexCommitEvidence.subjects,
2236
+ committedShas: codexCommitEvidence.shas,
2237
+ mergedPrNumbers: codexCommitEvidence.prNumbers,
1992
2238
  aiTitle: firstUser ? firstUser.text.slice(0, 120).trim() : null,
1993
2239
  author: null,
1994
2240
  sourceFile: filePath,
@@ -2504,7 +2750,7 @@ var init_trace_target = __esm({
2504
2750
 
2505
2751
  // ../../packages/ingest-core/src/gemini-sessions.ts
2506
2752
  import { readFileSync as readFileSync4 } from "node:fs";
2507
- function textOf(content) {
2753
+ function textOf2(content) {
2508
2754
  if (typeof content === "string") return content;
2509
2755
  if (!Array.isArray(content)) return "";
2510
2756
  return content.map(
@@ -2538,7 +2784,7 @@ function parseGeminiSessionFile(filePath, repoPath, repoId) {
2538
2784
  messages.forEach((record, index) => {
2539
2785
  const role = record.type === "user" ? "user" : record.type === "gemini" || record.type === "model" ? "assistant" : null;
2540
2786
  if (!role) return;
2541
- const text = textOf(record.content ?? record.displayContent);
2787
+ const text = textOf2(record.content ?? record.displayContent);
2542
2788
  if (!text.trim()) return;
2543
2789
  const { text: safe, count } = redactSecrets(text);
2544
2790
  redactionCount += count;
@@ -2962,6 +3208,7 @@ __export(src_exports, {
2962
3208
  changedSessions: () => changedSessions,
2963
3209
  codexSessionsDir: () => codexSessionsDir,
2964
3210
  collectRepoData: () => collectRepoData,
3211
+ commitShaOutputs: () => commitShaOutputs,
2965
3212
  commitsOlderThan: () => commitsOlderThan,
2966
3213
  committedSubjects: () => committedSubjects,
2967
3214
  copilotSessionsDir: () => copilotSessionsDir,
@@ -2996,6 +3243,9 @@ __export(src_exports, {
2996
3243
  loadComposers: () => loadComposers,
2997
3244
  matchCommitToSession: () => matchCommitToSession,
2998
3245
  measureLineSurvival: () => measureLineSurvival,
3246
+ mergeResponseShas: () => mergeResponseShas,
3247
+ mergeSucceeded: () => mergeSucceeded,
3248
+ mergedPrFromCommand: () => mergedPrFromCommand,
2999
3249
  normalizeRepoRemote: () => normalizeRepoRemote,
3000
3250
  opencodeDbPath: () => opencodeDbPath,
3001
3251
  originOf: () => originOf,
@@ -3005,6 +3255,7 @@ __export(src_exports, {
3005
3255
  parseAllOpenCodeSessions: () => parseAllOpenCodeSessions,
3006
3256
  parseAllSessions: () => parseAllSessions,
3007
3257
  parseCodexSessionFile: () => parseCodexSessionFile,
3258
+ parseCommitsBySha: () => parseCommitsBySha,
3008
3259
  parseCopilotSessionDir: () => parseCopilotSessionDir,
3009
3260
  parseCopilotWorkspace: () => parseCopilotWorkspace,
3010
3261
  parseCursorComposer: () => parseCursorComposer,
@@ -3171,7 +3422,7 @@ var init_client = __esm({
3171
3422
  // Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
3172
3423
  // apps/backend/src/query/query.service.ts#search. Used by evrex_search,
3173
3424
  // which wants ranked hits fast, not a synthesized paragraph.
3174
- search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
3425
+ search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
3175
3426
  // Everything that happened in a repo, newest first, bounded by days — the
3176
3427
  // same query the desktop Timeline screen makes. Sessions and commits
3177
3428
  // interleaved, each with the handle evrex_expand takes.