evrex-mcp 0.8.1 → 0.8.2
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/account.js +1 -1
- package/dist/capture.js +228 -25
- package/dist/continuity.js +1 -1
- package/dist/hook.js +5 -2
- package/dist/import.js +228 -25
- package/dist/index.js +24 -1
- package/dist/pretool.js +1 -1
- package/dist/tickets.js +1 -1
- package/package.json +1 -1
package/dist/account.js
CHANGED
|
@@ -49,7 +49,7 @@ var evrexApi = {
|
|
|
49
49
|
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
50
50
|
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
51
51
|
// which wants ranked hits fast, not a synthesized paragraph.
|
|
52
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
52
|
+
search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
|
|
53
53
|
// Everything that happened in a repo, newest first, bounded by days — the
|
|
54
54
|
// same query the desktop Timeline screen makes. Sessions and commits
|
|
55
55
|
// interleaved, each with the handle evrex_expand takes.
|
package/dist/capture.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
|
|
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
|
-
|
|
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 =
|
|
435
|
+
PARSER_VERSION = 7;
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -890,6 +1004,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
890
1004
|
const subagent = agentId ? { agentId, agentType: meta?.agentType ?? null, description: meta?.description ?? null } : null;
|
|
891
1005
|
if (agentId && !aiTitle && meta?.description) aiTitle = meta.description;
|
|
892
1006
|
const sortedTs = turns.map((t) => t.ts).sort();
|
|
1007
|
+
const commitEvidence = collectCommitEvidence(lines);
|
|
893
1008
|
const rawContent = capped ? "" : lines.map((line) => {
|
|
894
1009
|
try {
|
|
895
1010
|
return JSON.stringify(redactJsonValue(JSON.parse(line)).value);
|
|
@@ -913,7 +1028,9 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
913
1028
|
// filled in by the caller — see getGitUserName in git-history.ts
|
|
914
1029
|
sourceFile: filePath,
|
|
915
1030
|
redactionCount: totalRedactions,
|
|
916
|
-
committedSubjects:
|
|
1031
|
+
committedSubjects: commitEvidence.subjects,
|
|
1032
|
+
committedShas: commitEvidence.shas,
|
|
1033
|
+
mergedPrNumbers: commitEvidence.prNumbers,
|
|
917
1034
|
branch,
|
|
918
1035
|
parentSessionId,
|
|
919
1036
|
subagent,
|
|
@@ -922,10 +1039,14 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
922
1039
|
turns
|
|
923
1040
|
};
|
|
924
1041
|
}
|
|
925
|
-
function
|
|
1042
|
+
function collectCommitEvidence(lines) {
|
|
926
1043
|
const subjects = /* @__PURE__ */ new Set();
|
|
1044
|
+
const shas = /* @__PURE__ */ new Set();
|
|
1045
|
+
const prNumbers = /* @__PURE__ */ new Set();
|
|
1046
|
+
const commitCallIds = /* @__PURE__ */ new Set();
|
|
1047
|
+
const mergeCallIds = /* @__PURE__ */ new Map();
|
|
927
1048
|
for (const line of lines) {
|
|
928
|
-
if (!line.includes("git commit")) continue;
|
|
1049
|
+
if (!line.includes("git commit") && !line.includes("/merge") && !line.includes("tool_use_id")) continue;
|
|
929
1050
|
let record;
|
|
930
1051
|
try {
|
|
931
1052
|
record = JSON.parse(line);
|
|
@@ -935,13 +1056,40 @@ function collectCommittedSubjects(lines) {
|
|
|
935
1056
|
const content = record?.message?.content;
|
|
936
1057
|
if (!Array.isArray(content)) continue;
|
|
937
1058
|
for (const block of content) {
|
|
938
|
-
if (block?.type
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1059
|
+
if (block?.type === "tool_use") {
|
|
1060
|
+
const command = block?.input?.command;
|
|
1061
|
+
if (typeof command !== "string") continue;
|
|
1062
|
+
const found = committedSubjects(command);
|
|
1063
|
+
for (const subject of found) subjects.add(subject);
|
|
1064
|
+
if (/\bgit\s+(?:-C\s+\S+\s+)?commit\b/.test(command) && typeof block?.id === "string") {
|
|
1065
|
+
commitCallIds.add(block.id);
|
|
1066
|
+
}
|
|
1067
|
+
if ((/\/pulls\/\d+\/merge\b/.test(command) || /\bgh\s+pr\s+merge\b/.test(command)) && typeof block?.id === "string") {
|
|
1068
|
+
mergeCallIds.set(block.id, mergedPrFromCommand(command));
|
|
1069
|
+
}
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
if (block?.type === "tool_result" && mergeCallIds.has(block?.tool_use_id)) {
|
|
1073
|
+
const raw = block?.content;
|
|
1074
|
+
const text = typeof raw === "string" ? raw : Array.isArray(raw) ? raw.map(
|
|
1075
|
+
(x) => typeof x?.text === "string" ? x.text : ""
|
|
1076
|
+
).join(" ") : "";
|
|
1077
|
+
for (const sha of mergeResponseShas(text)) shas.add(sha);
|
|
1078
|
+
if (mergeSucceeded(text)) {
|
|
1079
|
+
const pr = mergeCallIds.get(block?.tool_use_id);
|
|
1080
|
+
if (typeof pr === "number") prNumbers.add(pr);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (block?.type === "tool_result" && commitCallIds.has(block?.tool_use_id)) {
|
|
1084
|
+
const raw = block?.content;
|
|
1085
|
+
const text = typeof raw === "string" ? raw : Array.isArray(raw) ? raw.map(
|
|
1086
|
+
(x) => typeof x?.text === "string" ? x.text : ""
|
|
1087
|
+
).join(" ") : "";
|
|
1088
|
+
for (const sha of commitShaOutputs(text)) shas.add(sha);
|
|
1089
|
+
}
|
|
942
1090
|
}
|
|
943
1091
|
}
|
|
944
|
-
return [...subjects];
|
|
1092
|
+
return { subjects: [...subjects], shas: [...shas], prNumbers: [...prNumbers] };
|
|
945
1093
|
}
|
|
946
1094
|
function parseAllSessions(repoPath, repoId, cursors = {}) {
|
|
947
1095
|
const id = repoId ?? deriveRepoId(repoPath);
|
|
@@ -1907,16 +2055,58 @@ function addUsage(into, next) {
|
|
|
1907
2055
|
model: into.model ?? next.model
|
|
1908
2056
|
};
|
|
1909
2057
|
}
|
|
1910
|
-
function
|
|
2058
|
+
function commitEvidenceFromToolCalls(lines) {
|
|
1911
2059
|
const subjects = /* @__PURE__ */ new Set();
|
|
2060
|
+
const shas = /* @__PURE__ */ new Set();
|
|
2061
|
+
const prNumbers = /* @__PURE__ */ new Set();
|
|
2062
|
+
const commitCallIds = /* @__PURE__ */ new Set();
|
|
2063
|
+
const mergeCallIds = /* @__PURE__ */ new Map();
|
|
1912
2064
|
for (const line of lines) {
|
|
1913
2065
|
if (line.type !== "response_item") continue;
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2066
|
+
const kind = payloadType(line);
|
|
2067
|
+
if (kind === "custom_tool_call") {
|
|
2068
|
+
const input = asString(line.payload?.input);
|
|
2069
|
+
if (!input) continue;
|
|
2070
|
+
const cmd = execCmdFrom(input) ?? input;
|
|
2071
|
+
const callId = asString(line.payload?.call_id);
|
|
2072
|
+
for (const subject of committedSubjects(cmd)) subjects.add(subject);
|
|
2073
|
+
if (/\bgit\s+(?:-C\s+\S+\s+)?commit\b/.test(cmd) && callId) {
|
|
2074
|
+
commitCallIds.add(callId);
|
|
2075
|
+
}
|
|
2076
|
+
if ((/\/pulls\/\d+\/merge\b/.test(cmd) || /\bgh\s+pr\s+merge\b/.test(cmd)) && callId) {
|
|
2077
|
+
mergeCallIds.set(callId, mergedPrFromCommand(cmd));
|
|
2078
|
+
}
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
if (kind === "custom_tool_call_output") {
|
|
2082
|
+
const callId = asString(line.payload?.call_id);
|
|
2083
|
+
if (!callId) continue;
|
|
2084
|
+
const raw = line.payload?.output;
|
|
2085
|
+
const text = Array.isArray(raw) ? raw.map(
|
|
2086
|
+
(x) => typeof x?.text === "string" ? x.text : ""
|
|
2087
|
+
).join(" ") : asString(raw) ?? "";
|
|
2088
|
+
if (commitCallIds.has(callId)) {
|
|
2089
|
+
for (const sha of commitShaOutputs(text)) shas.add(sha);
|
|
2090
|
+
}
|
|
2091
|
+
if (mergeCallIds.has(callId)) {
|
|
2092
|
+
for (const sha of mergeResponseShas(text)) shas.add(sha);
|
|
2093
|
+
if (mergeSucceeded(text)) {
|
|
2094
|
+
const pr = mergeCallIds.get(callId);
|
|
2095
|
+
if (typeof pr === "number") prNumbers.add(pr);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
return { subjects: [...subjects], shas: [...shas], prNumbers: [...prNumbers] };
|
|
2101
|
+
}
|
|
2102
|
+
function execCmdFrom(input) {
|
|
2103
|
+
const m = /cmd\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(input);
|
|
2104
|
+
if (!m) return null;
|
|
2105
|
+
try {
|
|
2106
|
+
return JSON.parse(`"${m[1]}"`);
|
|
2107
|
+
} catch {
|
|
2108
|
+
return null;
|
|
1918
2109
|
}
|
|
1919
|
-
return [...subjects];
|
|
1920
2110
|
}
|
|
1921
2111
|
function filesFromToolCalls(lines) {
|
|
1922
2112
|
const byPath = /* @__PURE__ */ new Map();
|
|
@@ -1975,20 +2165,28 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
1975
2165
|
const { content: rawContent, count: rawRedactions } = redactRollout(raw);
|
|
1976
2166
|
redactionCount += rawRedactions;
|
|
1977
2167
|
const firstUser = events.find((e) => e.role === "user");
|
|
2168
|
+
const codexCommitEvidence = commitEvidenceFromToolCalls(lines);
|
|
1978
2169
|
return {
|
|
1979
2170
|
id: sessionId,
|
|
1980
2171
|
agentKind: "codex",
|
|
1981
2172
|
branch: null,
|
|
1982
2173
|
parentSessionId: null,
|
|
1983
2174
|
subagent: null,
|
|
1984
|
-
//
|
|
1985
|
-
//
|
|
1986
|
-
|
|
2175
|
+
// The caller's derivation first: it reads the checkout's remote NOW,
|
|
2176
|
+
// which is the identity every other ingest path agrees on. The remote
|
|
2177
|
+
// recorded in session_meta is a historical fact — after the repo's
|
|
2178
|
+
// GitHub transfer, month-old rollouts kept re-minting the dead
|
|
2179
|
+
// jeffcheema id on every re-index, resurrecting a "third evrex-app"
|
|
2180
|
+
// as fast as migrations folded it. Meta only serves a rollout whose
|
|
2181
|
+
// checkout is gone, where a stale identity beats none.
|
|
2182
|
+
repoId: repoId.startsWith("path:") ? repoIdFromMeta(meta) ?? repoId : repoId,
|
|
1987
2183
|
cwd,
|
|
1988
2184
|
startedAt: asString(meta.timestamp) ?? events[0]?.ts ?? null,
|
|
1989
2185
|
endedAt: events[events.length - 1]?.ts ?? null,
|
|
1990
2186
|
turnCount: turns.length,
|
|
1991
|
-
committedSubjects:
|
|
2187
|
+
committedSubjects: codexCommitEvidence.subjects,
|
|
2188
|
+
committedShas: codexCommitEvidence.shas,
|
|
2189
|
+
mergedPrNumbers: codexCommitEvidence.prNumbers,
|
|
1992
2190
|
aiTitle: firstUser ? firstUser.text.slice(0, 120).trim() : null,
|
|
1993
2191
|
author: null,
|
|
1994
2192
|
sourceFile: filePath,
|
|
@@ -2962,6 +3160,7 @@ __export(src_exports, {
|
|
|
2962
3160
|
changedSessions: () => changedSessions,
|
|
2963
3161
|
codexSessionsDir: () => codexSessionsDir,
|
|
2964
3162
|
collectRepoData: () => collectRepoData,
|
|
3163
|
+
commitShaOutputs: () => commitShaOutputs,
|
|
2965
3164
|
commitsOlderThan: () => commitsOlderThan,
|
|
2966
3165
|
committedSubjects: () => committedSubjects,
|
|
2967
3166
|
copilotSessionsDir: () => copilotSessionsDir,
|
|
@@ -2996,6 +3195,9 @@ __export(src_exports, {
|
|
|
2996
3195
|
loadComposers: () => loadComposers,
|
|
2997
3196
|
matchCommitToSession: () => matchCommitToSession,
|
|
2998
3197
|
measureLineSurvival: () => measureLineSurvival,
|
|
3198
|
+
mergeResponseShas: () => mergeResponseShas,
|
|
3199
|
+
mergeSucceeded: () => mergeSucceeded,
|
|
3200
|
+
mergedPrFromCommand: () => mergedPrFromCommand,
|
|
2999
3201
|
normalizeRepoRemote: () => normalizeRepoRemote,
|
|
3000
3202
|
opencodeDbPath: () => opencodeDbPath,
|
|
3001
3203
|
originOf: () => originOf,
|
|
@@ -3005,6 +3207,7 @@ __export(src_exports, {
|
|
|
3005
3207
|
parseAllOpenCodeSessions: () => parseAllOpenCodeSessions,
|
|
3006
3208
|
parseAllSessions: () => parseAllSessions,
|
|
3007
3209
|
parseCodexSessionFile: () => parseCodexSessionFile,
|
|
3210
|
+
parseCommitsBySha: () => parseCommitsBySha,
|
|
3008
3211
|
parseCopilotSessionDir: () => parseCopilotSessionDir,
|
|
3009
3212
|
parseCopilotWorkspace: () => parseCopilotWorkspace,
|
|
3010
3213
|
parseCursorComposer: () => parseCursorComposer,
|
|
@@ -3171,7 +3374,7 @@ var init_client = __esm({
|
|
|
3171
3374
|
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
3172
3375
|
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
3173
3376
|
// which wants ranked hits fast, not a synthesized paragraph.
|
|
3174
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
3377
|
+
search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
|
|
3175
3378
|
// Everything that happened in a repo, newest first, bounded by days — the
|
|
3176
3379
|
// same query the desktop Timeline screen makes. Sessions and commits
|
|
3177
3380
|
// interleaved, each with the handle evrex_expand takes.
|
package/dist/continuity.js
CHANGED
|
@@ -51,7 +51,7 @@ var evrexApi = {
|
|
|
51
51
|
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
52
52
|
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
53
53
|
// which wants ranked hits fast, not a synthesized paragraph.
|
|
54
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
54
|
+
search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
|
|
55
55
|
// Everything that happened in a repo, newest first, bounded by days — the
|
|
56
56
|
// same query the desktop Timeline screen makes. Sessions and commits
|
|
57
57
|
// interleaved, each with the handle evrex_expand takes.
|
package/dist/hook.js
CHANGED
|
@@ -49,7 +49,7 @@ var evrexApi = {
|
|
|
49
49
|
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
50
50
|
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
51
51
|
// which wants ranked hits fast, not a synthesized paragraph.
|
|
52
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
52
|
+
search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
|
|
53
53
|
// Everything that happened in a repo, newest first, bounded by days — the
|
|
54
54
|
// same query the desktop Timeline screen makes. Sessions and commits
|
|
55
55
|
// interleaved, each with the handle evrex_expand takes.
|
|
@@ -245,7 +245,10 @@ async function main() {
|
|
|
245
245
|
if (!repoId) return;
|
|
246
246
|
const remaining = DEADLINE_MS - (Date.now() - started);
|
|
247
247
|
if (remaining < 500) return;
|
|
248
|
-
const result = await withDeadline(
|
|
248
|
+
const result = await withDeadline(
|
|
249
|
+
evrexApi.search(repoId, prompt, void 0, false),
|
|
250
|
+
remaining
|
|
251
|
+
);
|
|
249
252
|
if (!result?.evidence?.length) return;
|
|
250
253
|
const sessionId = input.session_id ?? "";
|
|
251
254
|
const path = statePath();
|
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
|
|
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
|
-
|
|
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 =
|
|
435
|
+
PARSER_VERSION = 7;
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -890,6 +1004,7 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
890
1004
|
const subagent = agentId ? { agentId, agentType: meta?.agentType ?? null, description: meta?.description ?? null } : null;
|
|
891
1005
|
if (agentId && !aiTitle && meta?.description) aiTitle = meta.description;
|
|
892
1006
|
const sortedTs = turns.map((t) => t.ts).sort();
|
|
1007
|
+
const commitEvidence = collectCommitEvidence(lines);
|
|
893
1008
|
const rawContent = capped ? "" : lines.map((line) => {
|
|
894
1009
|
try {
|
|
895
1010
|
return JSON.stringify(redactJsonValue(JSON.parse(line)).value);
|
|
@@ -913,7 +1028,9 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
913
1028
|
// filled in by the caller — see getGitUserName in git-history.ts
|
|
914
1029
|
sourceFile: filePath,
|
|
915
1030
|
redactionCount: totalRedactions,
|
|
916
|
-
committedSubjects:
|
|
1031
|
+
committedSubjects: commitEvidence.subjects,
|
|
1032
|
+
committedShas: commitEvidence.shas,
|
|
1033
|
+
mergedPrNumbers: commitEvidence.prNumbers,
|
|
917
1034
|
branch,
|
|
918
1035
|
parentSessionId,
|
|
919
1036
|
subagent,
|
|
@@ -922,10 +1039,14 @@ function parseSessionFile(filePath, repoPath, repoId = deriveRepoId(repoPath), a
|
|
|
922
1039
|
turns
|
|
923
1040
|
};
|
|
924
1041
|
}
|
|
925
|
-
function
|
|
1042
|
+
function collectCommitEvidence(lines) {
|
|
926
1043
|
const subjects = /* @__PURE__ */ new Set();
|
|
1044
|
+
const shas = /* @__PURE__ */ new Set();
|
|
1045
|
+
const prNumbers = /* @__PURE__ */ new Set();
|
|
1046
|
+
const commitCallIds = /* @__PURE__ */ new Set();
|
|
1047
|
+
const mergeCallIds = /* @__PURE__ */ new Map();
|
|
927
1048
|
for (const line of lines) {
|
|
928
|
-
if (!line.includes("git commit")) continue;
|
|
1049
|
+
if (!line.includes("git commit") && !line.includes("/merge") && !line.includes("tool_use_id")) continue;
|
|
929
1050
|
let record;
|
|
930
1051
|
try {
|
|
931
1052
|
record = JSON.parse(line);
|
|
@@ -935,13 +1056,40 @@ function collectCommittedSubjects(lines) {
|
|
|
935
1056
|
const content = record?.message?.content;
|
|
936
1057
|
if (!Array.isArray(content)) continue;
|
|
937
1058
|
for (const block of content) {
|
|
938
|
-
if (block?.type
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1059
|
+
if (block?.type === "tool_use") {
|
|
1060
|
+
const command = block?.input?.command;
|
|
1061
|
+
if (typeof command !== "string") continue;
|
|
1062
|
+
const found = committedSubjects(command);
|
|
1063
|
+
for (const subject of found) subjects.add(subject);
|
|
1064
|
+
if (/\bgit\s+(?:-C\s+\S+\s+)?commit\b/.test(command) && typeof block?.id === "string") {
|
|
1065
|
+
commitCallIds.add(block.id);
|
|
1066
|
+
}
|
|
1067
|
+
if ((/\/pulls\/\d+\/merge\b/.test(command) || /\bgh\s+pr\s+merge\b/.test(command)) && typeof block?.id === "string") {
|
|
1068
|
+
mergeCallIds.set(block.id, mergedPrFromCommand(command));
|
|
1069
|
+
}
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
if (block?.type === "tool_result" && mergeCallIds.has(block?.tool_use_id)) {
|
|
1073
|
+
const raw = block?.content;
|
|
1074
|
+
const text = typeof raw === "string" ? raw : Array.isArray(raw) ? raw.map(
|
|
1075
|
+
(x) => typeof x?.text === "string" ? x.text : ""
|
|
1076
|
+
).join(" ") : "";
|
|
1077
|
+
for (const sha of mergeResponseShas(text)) shas.add(sha);
|
|
1078
|
+
if (mergeSucceeded(text)) {
|
|
1079
|
+
const pr = mergeCallIds.get(block?.tool_use_id);
|
|
1080
|
+
if (typeof pr === "number") prNumbers.add(pr);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (block?.type === "tool_result" && commitCallIds.has(block?.tool_use_id)) {
|
|
1084
|
+
const raw = block?.content;
|
|
1085
|
+
const text = typeof raw === "string" ? raw : Array.isArray(raw) ? raw.map(
|
|
1086
|
+
(x) => typeof x?.text === "string" ? x.text : ""
|
|
1087
|
+
).join(" ") : "";
|
|
1088
|
+
for (const sha of commitShaOutputs(text)) shas.add(sha);
|
|
1089
|
+
}
|
|
942
1090
|
}
|
|
943
1091
|
}
|
|
944
|
-
return [...subjects];
|
|
1092
|
+
return { subjects: [...subjects], shas: [...shas], prNumbers: [...prNumbers] };
|
|
945
1093
|
}
|
|
946
1094
|
function parseAllSessions(repoPath, repoId, cursors = {}) {
|
|
947
1095
|
const id = repoId ?? deriveRepoId(repoPath);
|
|
@@ -1907,16 +2055,58 @@ function addUsage(into, next) {
|
|
|
1907
2055
|
model: into.model ?? next.model
|
|
1908
2056
|
};
|
|
1909
2057
|
}
|
|
1910
|
-
function
|
|
2058
|
+
function commitEvidenceFromToolCalls(lines) {
|
|
1911
2059
|
const subjects = /* @__PURE__ */ new Set();
|
|
2060
|
+
const shas = /* @__PURE__ */ new Set();
|
|
2061
|
+
const prNumbers = /* @__PURE__ */ new Set();
|
|
2062
|
+
const commitCallIds = /* @__PURE__ */ new Set();
|
|
2063
|
+
const mergeCallIds = /* @__PURE__ */ new Map();
|
|
1912
2064
|
for (const line of lines) {
|
|
1913
2065
|
if (line.type !== "response_item") continue;
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2066
|
+
const kind = payloadType(line);
|
|
2067
|
+
if (kind === "custom_tool_call") {
|
|
2068
|
+
const input = asString(line.payload?.input);
|
|
2069
|
+
if (!input) continue;
|
|
2070
|
+
const cmd = execCmdFrom(input) ?? input;
|
|
2071
|
+
const callId = asString(line.payload?.call_id);
|
|
2072
|
+
for (const subject of committedSubjects(cmd)) subjects.add(subject);
|
|
2073
|
+
if (/\bgit\s+(?:-C\s+\S+\s+)?commit\b/.test(cmd) && callId) {
|
|
2074
|
+
commitCallIds.add(callId);
|
|
2075
|
+
}
|
|
2076
|
+
if ((/\/pulls\/\d+\/merge\b/.test(cmd) || /\bgh\s+pr\s+merge\b/.test(cmd)) && callId) {
|
|
2077
|
+
mergeCallIds.set(callId, mergedPrFromCommand(cmd));
|
|
2078
|
+
}
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
if (kind === "custom_tool_call_output") {
|
|
2082
|
+
const callId = asString(line.payload?.call_id);
|
|
2083
|
+
if (!callId) continue;
|
|
2084
|
+
const raw = line.payload?.output;
|
|
2085
|
+
const text = Array.isArray(raw) ? raw.map(
|
|
2086
|
+
(x) => typeof x?.text === "string" ? x.text : ""
|
|
2087
|
+
).join(" ") : asString(raw) ?? "";
|
|
2088
|
+
if (commitCallIds.has(callId)) {
|
|
2089
|
+
for (const sha of commitShaOutputs(text)) shas.add(sha);
|
|
2090
|
+
}
|
|
2091
|
+
if (mergeCallIds.has(callId)) {
|
|
2092
|
+
for (const sha of mergeResponseShas(text)) shas.add(sha);
|
|
2093
|
+
if (mergeSucceeded(text)) {
|
|
2094
|
+
const pr = mergeCallIds.get(callId);
|
|
2095
|
+
if (typeof pr === "number") prNumbers.add(pr);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
return { subjects: [...subjects], shas: [...shas], prNumbers: [...prNumbers] };
|
|
2101
|
+
}
|
|
2102
|
+
function execCmdFrom(input) {
|
|
2103
|
+
const m = /cmd\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(input);
|
|
2104
|
+
if (!m) return null;
|
|
2105
|
+
try {
|
|
2106
|
+
return JSON.parse(`"${m[1]}"`);
|
|
2107
|
+
} catch {
|
|
2108
|
+
return null;
|
|
1918
2109
|
}
|
|
1919
|
-
return [...subjects];
|
|
1920
2110
|
}
|
|
1921
2111
|
function filesFromToolCalls(lines) {
|
|
1922
2112
|
const byPath = /* @__PURE__ */ new Map();
|
|
@@ -1975,20 +2165,28 @@ function parseCodexSessionFile(filePath, repoPath, repoId) {
|
|
|
1975
2165
|
const { content: rawContent, count: rawRedactions } = redactRollout(raw);
|
|
1976
2166
|
redactionCount += rawRedactions;
|
|
1977
2167
|
const firstUser = events.find((e) => e.role === "user");
|
|
2168
|
+
const codexCommitEvidence = commitEvidenceFromToolCalls(lines);
|
|
1978
2169
|
return {
|
|
1979
2170
|
id: sessionId,
|
|
1980
2171
|
agentKind: "codex",
|
|
1981
2172
|
branch: null,
|
|
1982
2173
|
parentSessionId: null,
|
|
1983
2174
|
subagent: null,
|
|
1984
|
-
//
|
|
1985
|
-
//
|
|
1986
|
-
|
|
2175
|
+
// The caller's derivation first: it reads the checkout's remote NOW,
|
|
2176
|
+
// which is the identity every other ingest path agrees on. The remote
|
|
2177
|
+
// recorded in session_meta is a historical fact — after the repo's
|
|
2178
|
+
// GitHub transfer, month-old rollouts kept re-minting the dead
|
|
2179
|
+
// jeffcheema id on every re-index, resurrecting a "third evrex-app"
|
|
2180
|
+
// as fast as migrations folded it. Meta only serves a rollout whose
|
|
2181
|
+
// checkout is gone, where a stale identity beats none.
|
|
2182
|
+
repoId: repoId.startsWith("path:") ? repoIdFromMeta(meta) ?? repoId : repoId,
|
|
1987
2183
|
cwd,
|
|
1988
2184
|
startedAt: asString(meta.timestamp) ?? events[0]?.ts ?? null,
|
|
1989
2185
|
endedAt: events[events.length - 1]?.ts ?? null,
|
|
1990
2186
|
turnCount: turns.length,
|
|
1991
|
-
committedSubjects:
|
|
2187
|
+
committedSubjects: codexCommitEvidence.subjects,
|
|
2188
|
+
committedShas: codexCommitEvidence.shas,
|
|
2189
|
+
mergedPrNumbers: codexCommitEvidence.prNumbers,
|
|
1992
2190
|
aiTitle: firstUser ? firstUser.text.slice(0, 120).trim() : null,
|
|
1993
2191
|
author: null,
|
|
1994
2192
|
sourceFile: filePath,
|
|
@@ -2962,6 +3160,7 @@ __export(src_exports, {
|
|
|
2962
3160
|
changedSessions: () => changedSessions,
|
|
2963
3161
|
codexSessionsDir: () => codexSessionsDir,
|
|
2964
3162
|
collectRepoData: () => collectRepoData,
|
|
3163
|
+
commitShaOutputs: () => commitShaOutputs,
|
|
2965
3164
|
commitsOlderThan: () => commitsOlderThan,
|
|
2966
3165
|
committedSubjects: () => committedSubjects,
|
|
2967
3166
|
copilotSessionsDir: () => copilotSessionsDir,
|
|
@@ -2996,6 +3195,9 @@ __export(src_exports, {
|
|
|
2996
3195
|
loadComposers: () => loadComposers,
|
|
2997
3196
|
matchCommitToSession: () => matchCommitToSession,
|
|
2998
3197
|
measureLineSurvival: () => measureLineSurvival,
|
|
3198
|
+
mergeResponseShas: () => mergeResponseShas,
|
|
3199
|
+
mergeSucceeded: () => mergeSucceeded,
|
|
3200
|
+
mergedPrFromCommand: () => mergedPrFromCommand,
|
|
2999
3201
|
normalizeRepoRemote: () => normalizeRepoRemote,
|
|
3000
3202
|
opencodeDbPath: () => opencodeDbPath,
|
|
3001
3203
|
originOf: () => originOf,
|
|
@@ -3005,6 +3207,7 @@ __export(src_exports, {
|
|
|
3005
3207
|
parseAllOpenCodeSessions: () => parseAllOpenCodeSessions,
|
|
3006
3208
|
parseAllSessions: () => parseAllSessions,
|
|
3007
3209
|
parseCodexSessionFile: () => parseCodexSessionFile,
|
|
3210
|
+
parseCommitsBySha: () => parseCommitsBySha,
|
|
3008
3211
|
parseCopilotSessionDir: () => parseCopilotSessionDir,
|
|
3009
3212
|
parseCopilotWorkspace: () => parseCopilotWorkspace,
|
|
3010
3213
|
parseCursorComposer: () => parseCursorComposer,
|
|
@@ -3171,7 +3374,7 @@ var init_client = __esm({
|
|
|
3171
3374
|
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
3172
3375
|
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
3173
3376
|
// which wants ranked hits fast, not a synthesized paragraph.
|
|
3174
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
3377
|
+
search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
|
|
3175
3378
|
// Everything that happened in a repo, newest first, bounded by days — the
|
|
3176
3379
|
// same query the desktop Timeline screen makes. Sessions and commits
|
|
3177
3380
|
// interleaved, each with the handle evrex_expand takes.
|
package/dist/index.js
CHANGED
|
@@ -63,7 +63,7 @@ var init_client = __esm({
|
|
|
63
63
|
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
64
64
|
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
65
65
|
// which wants ranked hits fast, not a synthesized paragraph.
|
|
66
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
66
|
+
search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
|
|
67
67
|
// Everything that happened in a repo, newest first, bounded by days — the
|
|
68
68
|
// same query the desktop Timeline screen makes. Sessions and commits
|
|
69
69
|
// interleaved, each with the handle evrex_expand takes.
|
|
@@ -525,6 +525,29 @@ async function synthesizeAnswer(question, evidence, client2, options = {}) {
|
|
|
525
525
|
};
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
+
// ../../packages/llm-core/src/retrieval-boost.ts
|
|
529
|
+
var CORPUS = `The corpus is an engineering-memory index built from a team's coding-agent conversations and git history, retrieved with Postgres full-text OR-matching plus BM25 (exact word stems match; meanings do not). Searchable documents:
|
|
530
|
+
- Conversation turns: prose by the person and the agent, plus tool-call lines like "[tool_call: Edit] apps/backend/src/query/query.service.ts" \u2014 so file paths, function names, error strings and identifiers appear verbatim.
|
|
531
|
+
- Extracted insights: short titled records of decisions made, approaches REJECTED (with the reason), constraints, and procedures.
|
|
532
|
+
- Commit messages: conventional-commit subjects and explanatory bodies.`;
|
|
533
|
+
var EXPAND_SYSTEM = `You expand a search query into terms that will match an engineering record.
|
|
534
|
+
|
|
535
|
+
${CORPUS}
|
|
536
|
+
|
|
537
|
+
Given a person's question, produce the search terms most likely to appear VERBATIM in that corpus but absent from the question itself: the identifiers an engineer would have typed (function, constant, env var, file names), the error text the failure would have printed, and the words the same idea takes in commit-message register ("fix the flaky test" \u2192 "flaky", "deflake", "retry", "timeout"). Prefer specific over general \u2014 "RLS" and "row level security" over "database permissions". Never invent identifiers you are not confident exist in this kind of codebase; a wrong specific term matches nothing and costs nothing, but fill the list with your best evidence-seeking guesses.
|
|
538
|
+
|
|
539
|
+
Respond with JSON only: {"terms": ["...", ...]} \u2014 3 to 8 terms, each 1 to 4 words, no duplicates of the question's own words.`;
|
|
540
|
+
var RERANK_SYSTEM = `You order search results for an engineer's question about their own project's history.
|
|
541
|
+
|
|
542
|
+
${CORPUS}
|
|
543
|
+
|
|
544
|
+
You are given the question and a numbered list of candidate results that lexical retrieval fetched. Order the candidate ids best-first by how much each would actually help ANSWER the question:
|
|
545
|
+
- Recorded reasoning outranks mechanical record: a decision, a rejected approach with its reason, a constraint, or an explanation beats a log line or a file listing that merely shares words with the question.
|
|
546
|
+
- A candidate about the question's subject beats one that only quotes its vocabulary; watch for results that repeat the question's words while being about something else entirely.
|
|
547
|
+
- Prefer the origin of an idea over a later mention of it; among near-duplicates prefer the more complete one; use the date only to break ties.
|
|
548
|
+
|
|
549
|
+
Respond with JSON only: {"ranked": ["id", ...]} \u2014 every listed id at most once, ids from the list only, best first. Include every id you consider relevant; omit only what would waste the reader's attention.`;
|
|
550
|
+
|
|
528
551
|
// ../../packages/llm-core/src/cli-client.ts
|
|
529
552
|
import { execFile } from "node:child_process";
|
|
530
553
|
var TIMEOUT_MS = 12e4;
|
package/dist/pretool.js
CHANGED
|
@@ -49,7 +49,7 @@ var evrexApi = {
|
|
|
49
49
|
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
50
50
|
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
51
51
|
// which wants ranked hits fast, not a synthesized paragraph.
|
|
52
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
52
|
+
search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
|
|
53
53
|
// Everything that happened in a repo, newest first, bounded by days — the
|
|
54
54
|
// same query the desktop Timeline screen makes. Sessions and commits
|
|
55
55
|
// interleaved, each with the handle evrex_expand takes.
|
package/dist/tickets.js
CHANGED
|
@@ -63,7 +63,7 @@ var init_client = __esm({
|
|
|
63
63
|
// Evidence-only retrieval (BM25 + embedding), no LLM synthesis call — see
|
|
64
64
|
// apps/backend/src/query/query.service.ts#search. Used by evrex_search,
|
|
65
65
|
// which wants ranked hits fast, not a synthesized paragraph.
|
|
66
|
-
search: (repoPath, text, filePaths) => post("/search", { repoPath, text, filePaths }),
|
|
66
|
+
search: (repoPath, text, filePaths, boost) => post("/search", { repoPath, text, filePaths, boost }),
|
|
67
67
|
// Everything that happened in a repo, newest first, bounded by days — the
|
|
68
68
|
// same query the desktop Timeline screen makes. Sessions and commits
|
|
69
69
|
// interleaved, each with the handle evrex_expand takes.
|
package/package.json
CHANGED