@basou/core 0.35.0 → 0.36.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/index.d.ts +159 -6
- package/dist/index.js +430 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/event.schema.json +21 -0
- package/schemas/session-import.schema.json +21 -0
package/dist/index.js
CHANGED
|
@@ -701,6 +701,32 @@ function codexRolloutToImportPayload(records, options) {
|
|
|
701
701
|
continue;
|
|
702
702
|
}
|
|
703
703
|
if (readString4(record.type) !== "response_item") continue;
|
|
704
|
+
if (readString4(payload2.type) === "custom_tool_call") {
|
|
705
|
+
if (readString4(payload2.name) !== SCRIPT_TOOL_NAME) continue;
|
|
706
|
+
const scan = scanScript(readString4(payload2.input));
|
|
707
|
+
if (scan.commands.length === 0) continue;
|
|
708
|
+
const output2 = readCallId(payload2.call_id, outputsByCallId);
|
|
709
|
+
const durationMs = scan.toolCallCount === 1 ? parseWallTimeMs(output2) : 0;
|
|
710
|
+
const scriptTsMs = Date.parse(ts);
|
|
711
|
+
if (Number.isFinite(scriptTsMs)) engagementTsMs.push(scriptTsMs);
|
|
712
|
+
for (const command2 of scan.commands) {
|
|
713
|
+
derived.push(
|
|
714
|
+
// The per-command exit code is absent from this format (the script
|
|
715
|
+
// would have to print it), so it stays null — "unknown", not "0".
|
|
716
|
+
commandExecutedEvent2(
|
|
717
|
+
ts,
|
|
718
|
+
placeholderSessionId,
|
|
719
|
+
command2.cmd,
|
|
720
|
+
command2.workdir ?? workingDir ?? ".",
|
|
721
|
+
{
|
|
722
|
+
exitCode: null,
|
|
723
|
+
durationMs
|
|
724
|
+
}
|
|
725
|
+
)
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
704
730
|
if (readString4(payload2.type) !== "function_call") continue;
|
|
705
731
|
if (readString4(payload2.name) !== "exec_command") continue;
|
|
706
732
|
const command = readExecCommand(payload2.arguments);
|
|
@@ -840,6 +866,250 @@ function readCallId(value, outputs) {
|
|
|
840
866
|
const callId = readString4(value);
|
|
841
867
|
return callId !== void 0 ? outputs.get(callId) : void 0;
|
|
842
868
|
}
|
|
869
|
+
var SCRIPT_TOOL_NAME = "exec";
|
|
870
|
+
var TOOL_CALL_PREFIX = "tools.";
|
|
871
|
+
var EXEC_TOOL = "exec_command";
|
|
872
|
+
function scanScript(script) {
|
|
873
|
+
const scan = { commands: [], toolCallCount: 0 };
|
|
874
|
+
if (script === void 0) return scan;
|
|
875
|
+
try {
|
|
876
|
+
collectScriptCalls(script, scan);
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
return scan;
|
|
880
|
+
}
|
|
881
|
+
function collectScriptCalls(script, scan) {
|
|
882
|
+
let i = 0;
|
|
883
|
+
while (i < script.length) {
|
|
884
|
+
const skipped = skipNonCode(script, i);
|
|
885
|
+
if (skipped !== i) {
|
|
886
|
+
if (skipped === -1) return;
|
|
887
|
+
i = skipped;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (!script.startsWith(TOOL_CALL_PREFIX, i) || isIdentifierChar(script[i - 1])) {
|
|
891
|
+
i++;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
const nameStart = i + TOOL_CALL_PREFIX.length;
|
|
895
|
+
const nameEnd = readIdentifierEnd(script, nameStart);
|
|
896
|
+
const open = skipWhitespaceAndComments(script, nameEnd);
|
|
897
|
+
if (nameEnd === nameStart || open === -1 || script[open] !== "(") {
|
|
898
|
+
i = nameStart;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
scan.toolCallCount++;
|
|
902
|
+
const argsStart = skipWhitespaceAndComments(script, open + 1);
|
|
903
|
+
i = open + 1;
|
|
904
|
+
if (argsStart === -1 || script[argsStart] !== "{") continue;
|
|
905
|
+
const argsEnd = findObjectEnd(script, argsStart);
|
|
906
|
+
if (argsEnd === -1) return;
|
|
907
|
+
i = argsStart + 1;
|
|
908
|
+
if (script.slice(nameStart, nameEnd) !== EXEC_TOOL) continue;
|
|
909
|
+
const command = readExecArguments(script.slice(argsStart, argsEnd + 1));
|
|
910
|
+
if (command !== void 0) scan.commands.push(command);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function isIdentifierChar(ch) {
|
|
914
|
+
return ch !== void 0 && /[A-Za-z0-9_$]/.test(ch);
|
|
915
|
+
}
|
|
916
|
+
function skipNonCode(script, at) {
|
|
917
|
+
const ch = script[at];
|
|
918
|
+
if (ch === '"' || ch === "'" || ch === "`") return skipStringLiteral(script, at);
|
|
919
|
+
if (ch !== "/") return at;
|
|
920
|
+
const next = script[at + 1];
|
|
921
|
+
if (next === "/") {
|
|
922
|
+
const eol = script.indexOf("\n", at + 2);
|
|
923
|
+
return eol === -1 ? script.length : eol + 1;
|
|
924
|
+
}
|
|
925
|
+
if (next === "*") {
|
|
926
|
+
const end = script.indexOf("*/", at + 2);
|
|
927
|
+
return end === -1 ? -1 : end + 2;
|
|
928
|
+
}
|
|
929
|
+
return at;
|
|
930
|
+
}
|
|
931
|
+
function skipStringLiteral(script, at) {
|
|
932
|
+
const quote = script[at];
|
|
933
|
+
for (let i = at + 1; i < script.length; i++) {
|
|
934
|
+
const ch = script[i];
|
|
935
|
+
if (ch === "\\") {
|
|
936
|
+
i++;
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
if (ch === quote) return i + 1;
|
|
940
|
+
if (quote === "`" && ch === "$" && script[i + 1] === "{") {
|
|
941
|
+
const end = findObjectEnd(script, i + 1);
|
|
942
|
+
if (end === -1) return -1;
|
|
943
|
+
i = end;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return -1;
|
|
947
|
+
}
|
|
948
|
+
function skipWhitespaceAndComments(script, at) {
|
|
949
|
+
let i = at;
|
|
950
|
+
while (i < script.length) {
|
|
951
|
+
if (/\s/.test(script[i] ?? "")) {
|
|
952
|
+
i++;
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (script[i] !== "/") return i;
|
|
956
|
+
const skipped = skipNonCode(script, i);
|
|
957
|
+
if (skipped === -1) return -1;
|
|
958
|
+
if (skipped === i) return i;
|
|
959
|
+
i = skipped;
|
|
960
|
+
}
|
|
961
|
+
return i;
|
|
962
|
+
}
|
|
963
|
+
function readIdentifierEnd(script, start) {
|
|
964
|
+
let i = start;
|
|
965
|
+
while (i < script.length && /[A-Za-z0-9_$]/.test(script[i] ?? "")) i++;
|
|
966
|
+
return i;
|
|
967
|
+
}
|
|
968
|
+
function findObjectEnd(script, start) {
|
|
969
|
+
const closers = { "{": "}", "(": ")", "[": "]" };
|
|
970
|
+
const stack = [];
|
|
971
|
+
let i = start;
|
|
972
|
+
while (i < script.length) {
|
|
973
|
+
const skipped = skipNonCode(script, i);
|
|
974
|
+
if (skipped === -1) return -1;
|
|
975
|
+
if (skipped !== i) {
|
|
976
|
+
i = skipped;
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
const ch = script[i] ?? "";
|
|
980
|
+
const closer = closers[ch];
|
|
981
|
+
if (closer !== void 0) {
|
|
982
|
+
stack.push(closer);
|
|
983
|
+
} else if (ch === "}" || ch === ")" || ch === "]") {
|
|
984
|
+
if (stack.pop() !== ch) return -1;
|
|
985
|
+
if (stack.length === 0) return i;
|
|
986
|
+
}
|
|
987
|
+
i++;
|
|
988
|
+
}
|
|
989
|
+
return -1;
|
|
990
|
+
}
|
|
991
|
+
function readExecArguments(literal) {
|
|
992
|
+
const values = /* @__PURE__ */ new Map();
|
|
993
|
+
let i = 1;
|
|
994
|
+
while (i < literal.length) {
|
|
995
|
+
const at = skipWhitespaceAndComments(literal, i);
|
|
996
|
+
if (at === -1) return void 0;
|
|
997
|
+
const ch = literal[at];
|
|
998
|
+
if (ch === "}" || ch === void 0) break;
|
|
999
|
+
if (ch === ",") {
|
|
1000
|
+
i = at + 1;
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
if (literal.startsWith("...", at)) return void 0;
|
|
1004
|
+
let key;
|
|
1005
|
+
let afterKey;
|
|
1006
|
+
if (ch === '"' || ch === "'") {
|
|
1007
|
+
const end = skipStringLiteral(literal, at);
|
|
1008
|
+
if (end === -1) return void 0;
|
|
1009
|
+
key = literal.slice(at + 1, end - 1);
|
|
1010
|
+
afterKey = end;
|
|
1011
|
+
} else {
|
|
1012
|
+
afterKey = readIdentifierEnd(literal, at);
|
|
1013
|
+
if (afterKey === at) return void 0;
|
|
1014
|
+
key = literal.slice(at, afterKey);
|
|
1015
|
+
}
|
|
1016
|
+
const colon = skipWhitespaceAndComments(literal, afterKey);
|
|
1017
|
+
if (colon === -1) return void 0;
|
|
1018
|
+
if (literal[colon] !== ":") {
|
|
1019
|
+
if (key === "cmd" || key === "workdir") return void 0;
|
|
1020
|
+
i = colon;
|
|
1021
|
+
const next2 = skipToPropertyEnd(literal, colon);
|
|
1022
|
+
if (next2 === -1) return void 0;
|
|
1023
|
+
i = next2;
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const valueAt = skipWhitespaceAndComments(literal, colon + 1);
|
|
1027
|
+
if (valueAt === -1) return void 0;
|
|
1028
|
+
const valueChar = literal[valueAt];
|
|
1029
|
+
let value;
|
|
1030
|
+
let afterValue = valueAt;
|
|
1031
|
+
if (valueChar === '"' || valueChar === "'" || valueChar === "`") {
|
|
1032
|
+
const end = skipStringLiteral(literal, valueAt);
|
|
1033
|
+
if (end === -1) return void 0;
|
|
1034
|
+
const decoded = unescapeScriptString(literal.slice(valueAt, end));
|
|
1035
|
+
afterValue = end;
|
|
1036
|
+
const after = skipWhitespaceAndComments(literal, end);
|
|
1037
|
+
if (after === -1) return void 0;
|
|
1038
|
+
const terminator = literal[after];
|
|
1039
|
+
if (terminator === "," || terminator === "}") {
|
|
1040
|
+
value = decoded.length > 0 ? decoded : void 0;
|
|
1041
|
+
afterValue = after;
|
|
1042
|
+
} else if (key === "cmd" || key === "workdir") {
|
|
1043
|
+
return void 0;
|
|
1044
|
+
}
|
|
1045
|
+
} else if (key === "cmd" || key === "workdir") {
|
|
1046
|
+
return void 0;
|
|
1047
|
+
}
|
|
1048
|
+
if (key === "cmd" || key === "workdir") {
|
|
1049
|
+
if (values.has(key)) return void 0;
|
|
1050
|
+
values.set(key, value);
|
|
1051
|
+
}
|
|
1052
|
+
const next = skipToPropertyEnd(literal, afterValue);
|
|
1053
|
+
if (next === -1) return void 0;
|
|
1054
|
+
i = next;
|
|
1055
|
+
}
|
|
1056
|
+
const cmd = values.get("cmd");
|
|
1057
|
+
if (cmd === void 0) return void 0;
|
|
1058
|
+
return { cmd, workdir: values.get("workdir") };
|
|
1059
|
+
}
|
|
1060
|
+
function skipToPropertyEnd(literal, at) {
|
|
1061
|
+
let i = at;
|
|
1062
|
+
while (i < literal.length) {
|
|
1063
|
+
const skipped = skipNonCode(literal, i);
|
|
1064
|
+
if (skipped === -1) return -1;
|
|
1065
|
+
if (skipped !== i) {
|
|
1066
|
+
i = skipped;
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
const ch = literal[i];
|
|
1070
|
+
if (ch === "," || ch === "}") return i;
|
|
1071
|
+
if (ch === "{" || ch === "(" || ch === "[") {
|
|
1072
|
+
const end = findObjectEnd(literal, i);
|
|
1073
|
+
if (end === -1) return -1;
|
|
1074
|
+
i = end + 1;
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
i++;
|
|
1078
|
+
}
|
|
1079
|
+
return -1;
|
|
1080
|
+
}
|
|
1081
|
+
var SCRIPT_STRING_ESCAPES = {
|
|
1082
|
+
n: "\n",
|
|
1083
|
+
r: "\r",
|
|
1084
|
+
t: " ",
|
|
1085
|
+
b: "\b",
|
|
1086
|
+
f: "\f",
|
|
1087
|
+
v: "\v",
|
|
1088
|
+
"0": "\0"
|
|
1089
|
+
};
|
|
1090
|
+
function unescapeScriptString(quoted) {
|
|
1091
|
+
return quoted.slice(1, -1).replace(
|
|
1092
|
+
/\\(u\{[0-9a-fA-F]{1,6}\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|\r\n|[\s\S])/g,
|
|
1093
|
+
(match, sequence) => {
|
|
1094
|
+
if (sequence.length <= 2) {
|
|
1095
|
+
if (sequence === "\n" || sequence === "\r" || sequence === "\r\n") return "";
|
|
1096
|
+
if (sequence === "u" || sequence === "x") return match;
|
|
1097
|
+
return SCRIPT_STRING_ESCAPES[sequence] ?? sequence;
|
|
1098
|
+
}
|
|
1099
|
+
const hex = sequence.startsWith("u{") ? sequence.slice(2, -1) : sequence.slice(1);
|
|
1100
|
+
return codePointOrLiteral(hex, match);
|
|
1101
|
+
}
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
function codePointOrLiteral(hex, literal) {
|
|
1105
|
+
const code = Number.parseInt(hex, 16);
|
|
1106
|
+
if (!Number.isInteger(code) || code < 0 || code > 1114111) return literal;
|
|
1107
|
+
try {
|
|
1108
|
+
return String.fromCodePoint(code);
|
|
1109
|
+
} catch {
|
|
1110
|
+
return literal;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
843
1113
|
function turnIntervalFromComplete(endTs, payload, startMsByTurnId) {
|
|
844
1114
|
const endMs = Date.parse(endTs);
|
|
845
1115
|
if (!Number.isFinite(endMs)) return void 0;
|
|
@@ -871,7 +1141,7 @@ function parseExitCode(output) {
|
|
|
871
1141
|
}
|
|
872
1142
|
function parseWallTimeMs(output) {
|
|
873
1143
|
if (output === void 0) return 0;
|
|
874
|
-
const match = output.match(/Wall time
|
|
1144
|
+
const match = output.match(/Wall time:?\s*([\d.]+)\s*seconds/);
|
|
875
1145
|
if (match?.[1] === void 0) return 0;
|
|
876
1146
|
const seconds = Number.parseFloat(match[1]);
|
|
877
1147
|
return Number.isFinite(seconds) ? Math.round(seconds * 1e3) : 0;
|
|
@@ -882,13 +1152,20 @@ function indexOutputs(records) {
|
|
|
882
1152
|
if (readString4(record.type) !== "response_item") continue;
|
|
883
1153
|
const payload = isObject4(record.payload) ? record.payload : void 0;
|
|
884
1154
|
if (payload === void 0) continue;
|
|
885
|
-
|
|
1155
|
+
const payloadType = readString4(payload.type);
|
|
1156
|
+
const output = payloadType === "function_call_output" ? readString4(payload.output) : payloadType === "custom_tool_call_output" ? readOutputParts(payload.output) : void 0;
|
|
886
1157
|
const callId = readString4(payload.call_id);
|
|
887
|
-
const output = readString4(payload.output);
|
|
888
1158
|
if (callId !== void 0 && output !== void 0) byId.set(callId, output);
|
|
889
1159
|
}
|
|
890
1160
|
return byId;
|
|
891
1161
|
}
|
|
1162
|
+
function readOutputParts(value) {
|
|
1163
|
+
const asString = readString4(value);
|
|
1164
|
+
if (asString !== void 0) return asString;
|
|
1165
|
+
if (!Array.isArray(value)) return void 0;
|
|
1166
|
+
const texts = value.map((part) => isObject4(part) ? readString4(part.text) : void 0).filter((text) => text !== void 0);
|
|
1167
|
+
return texts.length > 0 ? texts.join("\n") : void 0;
|
|
1168
|
+
}
|
|
892
1169
|
|
|
893
1170
|
// src/approval/approval-store.ts
|
|
894
1171
|
import { readdir } from "fs/promises";
|
|
@@ -1257,6 +1534,9 @@ var ReviewRecordedEventSchema = BaseEventSchema.extend({
|
|
|
1257
1534
|
type: z3.literal("review_recorded"),
|
|
1258
1535
|
reviewer: z3.string().min(1),
|
|
1259
1536
|
target: z3.string().min(1),
|
|
1537
|
+
repos: z3.array(z3.string().min(1)).optional(),
|
|
1538
|
+
repos_resolved: z3.array(z3.string().min(1)).optional(),
|
|
1539
|
+
commits: z3.array(z3.string().min(1)).optional(),
|
|
1260
1540
|
verdict: z3.enum(["pass", "needs-attention", "fail"]).optional(),
|
|
1261
1541
|
findings: z3.array(ReviewFindingSchema).optional(),
|
|
1262
1542
|
blocked: z3.array(ReviewBlockedSchema).optional()
|
|
@@ -7262,6 +7542,29 @@ function normalizeRepoPath(p) {
|
|
|
7262
7542
|
if (/-workspace$/.test(seg) || seg.includes("$")) return null;
|
|
7263
7543
|
return s;
|
|
7264
7544
|
}
|
|
7545
|
+
function recordRepoKey(p) {
|
|
7546
|
+
return resolveRepoRoot(p);
|
|
7547
|
+
}
|
|
7548
|
+
function resolveRepoRoot(p) {
|
|
7549
|
+
return classifyRepoPath(p).resolved;
|
|
7550
|
+
}
|
|
7551
|
+
function classifyRepoPath(p) {
|
|
7552
|
+
let s = stripQuotes((p ?? "").trim()).replace(/\/+$/, "");
|
|
7553
|
+
if (s.startsWith("~/")) s = homedir2() + s.slice(1);
|
|
7554
|
+
if (s.length === 0 || !isAbsolute2(s)) return { resolved: null, problem: "relative" };
|
|
7555
|
+
const real = resolveRealpath(s);
|
|
7556
|
+
if (real === null) return { resolved: null, problem: "absent" };
|
|
7557
|
+
if (!isRepoRoot(real)) return { resolved: null, problem: "not_a_repo_root" };
|
|
7558
|
+
return { resolved: real, problem: null };
|
|
7559
|
+
}
|
|
7560
|
+
function findUnbindableRepos(repos) {
|
|
7561
|
+
const out = [];
|
|
7562
|
+
repos.forEach((repo, index) => {
|
|
7563
|
+
const { problem } = classifyRepoPath(repo);
|
|
7564
|
+
if (problem !== null) out.push({ repo, index, problem });
|
|
7565
|
+
});
|
|
7566
|
+
return out;
|
|
7567
|
+
}
|
|
7265
7568
|
function normalizeRepoKey(p) {
|
|
7266
7569
|
const full = normalizeRepoPath(p);
|
|
7267
7570
|
return full === null ? null : basename3(full);
|
|
@@ -7283,10 +7586,16 @@ function inspectCommand(args) {
|
|
|
7283
7586
|
}
|
|
7284
7587
|
return { files: [...files], examinedDiff };
|
|
7285
7588
|
}
|
|
7286
|
-
function
|
|
7589
|
+
function commandRepoWithProvenance(args, cwd) {
|
|
7590
|
+
const raw = commandRepoPath(args, cwd);
|
|
7591
|
+
return { key: normalizeRepoPath(raw), resolved: resolveRepoRoot(raw) !== null };
|
|
7592
|
+
}
|
|
7593
|
+
function commandRepoPath(args, cwd) {
|
|
7287
7594
|
const cd = args.join(" ").match(/\bcd\s+("[^"]+"|'[^']+'|[^\s&]+)\s*&&/);
|
|
7288
|
-
|
|
7289
|
-
|
|
7595
|
+
return cd?.[1] ?? cwd;
|
|
7596
|
+
}
|
|
7597
|
+
function commandRepo(args, cwd) {
|
|
7598
|
+
return normalizeRepoPath(commandRepoPath(args, cwd));
|
|
7290
7599
|
}
|
|
7291
7600
|
function commandFailed(exitCode) {
|
|
7292
7601
|
return exitCode !== null && exitCode !== 0;
|
|
@@ -7308,6 +7617,9 @@ async function findReviewGaps(input) {
|
|
|
7308
7617
|
if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
|
|
7309
7618
|
const entries = await loadSessionEntries(input.paths, loadOpts);
|
|
7310
7619
|
const reviews = [];
|
|
7620
|
+
const selfReports = [];
|
|
7621
|
+
let noRepos = 0;
|
|
7622
|
+
let unresolvableRepo = 0;
|
|
7311
7623
|
const workUnits = /* @__PURE__ */ new Map();
|
|
7312
7624
|
const unknownCommits = /* @__PURE__ */ new Map();
|
|
7313
7625
|
for (const entry of entries) {
|
|
@@ -7319,6 +7631,28 @@ async function findReviewGaps(input) {
|
|
|
7319
7631
|
for await (const ev of replayEvents(sessionDir, {
|
|
7320
7632
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
7321
7633
|
})) {
|
|
7634
|
+
if (ev.type === "review_recorded") {
|
|
7635
|
+
const recordedAt = Date.parse(ev.occurred_at);
|
|
7636
|
+
const named = ev.repos_resolved !== void 0 && ev.repos_resolved.length > 0 ? ev.repos_resolved : ev.repos ?? [];
|
|
7637
|
+
const keys = named.map((r) => recordRepoKey(r));
|
|
7638
|
+
const repos2 = new Set(keys.filter((r) => r !== null));
|
|
7639
|
+
if (keys.some((k) => k === null) || repos2.size === 0 || Number.isNaN(recordedAt)) {
|
|
7640
|
+
if (named.length === 0) noRepos++;
|
|
7641
|
+
else unresolvableRepo++;
|
|
7642
|
+
continue;
|
|
7643
|
+
}
|
|
7644
|
+
selfReports.push({
|
|
7645
|
+
sessionId: entry.sessionId,
|
|
7646
|
+
eventId: ev.id,
|
|
7647
|
+
reviewer: ev.reviewer,
|
|
7648
|
+
target: ev.target,
|
|
7649
|
+
recordedAt: ev.occurred_at,
|
|
7650
|
+
commits: ev.commits ?? [],
|
|
7651
|
+
at: recordedAt,
|
|
7652
|
+
repos: repos2
|
|
7653
|
+
});
|
|
7654
|
+
continue;
|
|
7655
|
+
}
|
|
7322
7656
|
if (ev.type !== "command_executed") continue;
|
|
7323
7657
|
if (commandFailed(ev.exit_code)) continue;
|
|
7324
7658
|
const at = Date.parse(ev.occurred_at);
|
|
@@ -7334,7 +7668,7 @@ async function findReviewGaps(input) {
|
|
|
7334
7668
|
continue;
|
|
7335
7669
|
}
|
|
7336
7670
|
if (!ev.args.join(" ").includes("git commit")) continue;
|
|
7337
|
-
const repo =
|
|
7671
|
+
const { key: repo, resolved: keyResolved } = commandRepoWithProvenance(ev.args, ev.cwd);
|
|
7338
7672
|
if (repo === null || Number.isNaN(at)) {
|
|
7339
7673
|
const list2 = unknownCommits.get(entry.sessionId) ?? [];
|
|
7340
7674
|
list2.push(Number.isNaN(at) ? null : at);
|
|
@@ -7343,7 +7677,7 @@ async function findReviewGaps(input) {
|
|
|
7343
7677
|
}
|
|
7344
7678
|
const byRepo = workUnits.get(entry.sessionId) ?? /* @__PURE__ */ new Map();
|
|
7345
7679
|
const list = byRepo.get(repo) ?? [];
|
|
7346
|
-
list.push({ repo, at, files: commitFiles(ev.args) });
|
|
7680
|
+
list.push({ repo, at, files: commitFiles(ev.args), keyResolved });
|
|
7347
7681
|
byRepo.set(repo, list);
|
|
7348
7682
|
workUnits.set(entry.sessionId, byRepo);
|
|
7349
7683
|
}
|
|
@@ -7358,19 +7692,33 @@ async function findReviewGaps(input) {
|
|
|
7358
7692
|
const windowMs = windowHours * 3600 * 1e3;
|
|
7359
7693
|
const units = [];
|
|
7360
7694
|
let newestCommit = null;
|
|
7695
|
+
const attachedSelfReports = /* @__PURE__ */ new Set();
|
|
7696
|
+
const refusedForUnit = /* @__PURE__ */ new Set();
|
|
7697
|
+
let refusedPairings = 0;
|
|
7361
7698
|
for (const [sessionId, byRepo] of workUnits) {
|
|
7362
7699
|
for (const [repoPath, commits] of byRepo) {
|
|
7363
7700
|
const label = basename3(repoPath);
|
|
7364
|
-
if (scope !== null && !scope.includes(label)) continue;
|
|
7365
7701
|
const times = commits.map((c) => c.at).sort((a, b) => a - b);
|
|
7366
7702
|
const first = times[0] ?? null;
|
|
7367
7703
|
const last = times[times.length - 1] ?? null;
|
|
7704
|
+
const earliest = first ?? last ?? 0;
|
|
7705
|
+
const latest = last ?? first ?? 0;
|
|
7706
|
+
const unitRepoIsHere = commits.every((c) => c.keyResolved);
|
|
7707
|
+
const inWindow = selfReports.filter(
|
|
7708
|
+
(r) => r.repos.has(repoPath) && r.at >= earliest - windowMs && r.at <= latest + windowMs
|
|
7709
|
+
);
|
|
7710
|
+
const selfBound = unitRepoIsHere ? inWindow : [];
|
|
7711
|
+
if (!unitRepoIsHere) {
|
|
7712
|
+
refusedPairings += inWindow.length;
|
|
7713
|
+
for (const r of inWindow) refusedForUnit.add(r.eventId);
|
|
7714
|
+
}
|
|
7715
|
+
for (const r of selfBound) attachedSelfReports.add(r.eventId);
|
|
7716
|
+
if (scope !== null && !scope.includes(label)) continue;
|
|
7368
7717
|
if (last !== null) newestCommit = newestCommit === null ? last : Math.max(newestCommit, last);
|
|
7369
7718
|
const changedFiles = new Set(commits.flatMap((c) => c.files));
|
|
7370
|
-
const before = first ?? last ?? 0;
|
|
7371
7719
|
const nearby = reviews.filter((r) => {
|
|
7372
7720
|
if (!r.repos.has(repoPath) || r.endedAt === null) return false;
|
|
7373
|
-
return r.endedAt <=
|
|
7721
|
+
return r.endedAt <= earliest && r.endedAt >= earliest - windowMs;
|
|
7374
7722
|
});
|
|
7375
7723
|
const bound = nearby.filter((r) => {
|
|
7376
7724
|
const touched = r.repos.get(repoPath);
|
|
@@ -7388,6 +7736,9 @@ async function findReviewGaps(input) {
|
|
|
7388
7736
|
firstCommitAt: first === null ? null : new Date(first).toISOString(),
|
|
7389
7737
|
lastCommitAt: last === null ? null : new Date(last).toISOString(),
|
|
7390
7738
|
verdict,
|
|
7739
|
+
// Attached after the verdict is computed, and deliberately not an input
|
|
7740
|
+
// to it: a record must never move a unit out of `gaps`.
|
|
7741
|
+
selfReports: selfBound.map((r) => toSelfReportedReview(r, r.at > earliest)),
|
|
7391
7742
|
reviews: cited.map((r) => ({
|
|
7392
7743
|
sessionId: r.sessionId,
|
|
7393
7744
|
examinedDiff: r.repos.get(repoPath)?.examinedDiff ?? false,
|
|
@@ -7397,34 +7748,41 @@ async function findReviewGaps(input) {
|
|
|
7397
7748
|
});
|
|
7398
7749
|
}
|
|
7399
7750
|
}
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
units.push({
|
|
7407
|
-
repo: "(unknown)",
|
|
7408
|
-
sessionId,
|
|
7409
|
-
commitCount: times.length,
|
|
7410
|
-
firstCommitAt: first === null ? null : new Date(first).toISOString(),
|
|
7411
|
-
lastCommitAt: last === null ? null : new Date(last).toISOString(),
|
|
7412
|
-
verdict: "unknown",
|
|
7413
|
-
reviews: []
|
|
7414
|
-
});
|
|
7751
|
+
for (const [sessionId, times] of unknownCommits) {
|
|
7752
|
+
const valid = times.filter((t) => t !== null).sort((a, b) => a - b);
|
|
7753
|
+
const first = valid[0] ?? null;
|
|
7754
|
+
const last = valid[valid.length - 1] ?? null;
|
|
7755
|
+
if (last !== null && scope === null) {
|
|
7756
|
+
newestCommit = newestCommit === null ? last : Math.max(newestCommit, last);
|
|
7415
7757
|
}
|
|
7758
|
+
units.push({
|
|
7759
|
+
repo: "(unknown)",
|
|
7760
|
+
sessionId,
|
|
7761
|
+
commitCount: times.length,
|
|
7762
|
+
firstCommitAt: first === null ? null : new Date(first).toISOString(),
|
|
7763
|
+
lastCommitAt: last === null ? null : new Date(last).toISOString(),
|
|
7764
|
+
verdict: "unknown",
|
|
7765
|
+
reviews: [],
|
|
7766
|
+
// No repo key, so nothing a record's `repos` could bind to.
|
|
7767
|
+
selfReports: []
|
|
7768
|
+
});
|
|
7416
7769
|
}
|
|
7770
|
+
const missed = selfReports.filter((r) => !attachedSelfReports.has(r.eventId));
|
|
7771
|
+
const unverifiableUnit = missed.filter((r) => refusedForUnit.has(r.eventId)).length;
|
|
7772
|
+
const noMatchingUnit = missed.length - unverifiableUnit;
|
|
7417
7773
|
const recentFirst = (a, b) => (Date.parse(b.lastCommitAt ?? "") || 0) - (Date.parse(a.lastCommitAt ?? "") || 0);
|
|
7418
|
-
const
|
|
7774
|
+
const talliedUnits = scope === null ? units : units.filter((u) => u.verdict !== "unknown");
|
|
7775
|
+
const repoKeys = [...new Set(talliedUnits.map((u) => u.repo))].sort();
|
|
7419
7776
|
const repos = repoKeys.map((repo) => {
|
|
7420
|
-
const us =
|
|
7777
|
+
const us = talliedUnits.filter((u) => u.repo === repo);
|
|
7421
7778
|
return {
|
|
7422
7779
|
repo,
|
|
7423
7780
|
units: us.length,
|
|
7424
7781
|
omissionUnits: us.filter((u) => u.verdict === "omission").length,
|
|
7425
7782
|
nearUnboundUnits: us.filter((u) => u.verdict === "near_unbound").length,
|
|
7426
7783
|
candidateUnits: us.filter((u) => u.verdict === "candidate").length,
|
|
7427
|
-
unknownUnits: us.filter((u) => u.verdict === "unknown").length
|
|
7784
|
+
unknownUnits: us.filter((u) => u.verdict === "unknown").length,
|
|
7785
|
+
selfReportedGapUnits: us.filter((u) => isGap(u) && u.selfReports.length > 0).length
|
|
7428
7786
|
};
|
|
7429
7787
|
});
|
|
7430
7788
|
return {
|
|
@@ -7432,12 +7790,34 @@ async function findReviewGaps(input) {
|
|
|
7432
7790
|
windowHours,
|
|
7433
7791
|
scope,
|
|
7434
7792
|
repos,
|
|
7435
|
-
gaps: units.filter(
|
|
7793
|
+
gaps: units.filter(isGap).sort(recentFirst),
|
|
7436
7794
|
candidates: units.filter((u) => u.verdict === "candidate").sort(recentFirst),
|
|
7437
7795
|
unknowns: units.filter((u) => u.verdict === "unknown").sort(recentFirst),
|
|
7796
|
+
unattachedSelfReports: {
|
|
7797
|
+
total: noRepos + unresolvableRepo + noMatchingUnit + unverifiableUnit,
|
|
7798
|
+
noRepos,
|
|
7799
|
+
unresolvableRepo,
|
|
7800
|
+
noMatchingUnit,
|
|
7801
|
+
unverifiableUnit
|
|
7802
|
+
},
|
|
7803
|
+
refusedPairings,
|
|
7438
7804
|
newestCommitAt: newestCommit === null ? null : new Date(newestCommit).toISOString()
|
|
7439
7805
|
};
|
|
7440
7806
|
}
|
|
7807
|
+
function isGap(u) {
|
|
7808
|
+
return u.verdict === "omission" || u.verdict === "near_unbound";
|
|
7809
|
+
}
|
|
7810
|
+
function toSelfReportedReview(r, recordedAfterCommit) {
|
|
7811
|
+
return {
|
|
7812
|
+
sessionId: r.sessionId,
|
|
7813
|
+
eventId: r.eventId,
|
|
7814
|
+
reviewer: r.reviewer,
|
|
7815
|
+
target: r.target,
|
|
7816
|
+
recordedAt: r.recordedAt,
|
|
7817
|
+
commits: r.commits,
|
|
7818
|
+
recordedAfterCommit
|
|
7819
|
+
};
|
|
7820
|
+
}
|
|
7441
7821
|
|
|
7442
7822
|
// src/review/review-record.ts
|
|
7443
7823
|
var VALID_VERDICTS = /* @__PURE__ */ new Set(["pass", "needs-attention", "fail"]);
|
|
@@ -7446,6 +7826,8 @@ var VALID_BLOCK_REASONS = /* @__PURE__ */ new Set(["spec-deviation", "design-rev
|
|
|
7446
7826
|
var ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
7447
7827
|
"reviewer",
|
|
7448
7828
|
"target",
|
|
7829
|
+
"repos",
|
|
7830
|
+
"commits",
|
|
7449
7831
|
"verdict",
|
|
7450
7832
|
"findings",
|
|
7451
7833
|
"blocked"
|
|
@@ -7476,13 +7858,19 @@ function parseReviewRecordInput(raw) {
|
|
|
7476
7858
|
for (const key of Object.keys(obj)) {
|
|
7477
7859
|
if (!ALLOWED_KEYS.has(key)) {
|
|
7478
7860
|
throw new Error(
|
|
7479
|
-
`Unknown field '${key}'. Allowed: reviewer, target, verdict, findings, blocked.`
|
|
7861
|
+
`Unknown field '${key}'. Allowed: reviewer, target, repos, commits, verdict, findings, blocked.`
|
|
7480
7862
|
);
|
|
7481
7863
|
}
|
|
7482
7864
|
}
|
|
7483
7865
|
const reviewer = requireNonEmptyString(obj.reviewer, "reviewer");
|
|
7484
7866
|
const target = requireNonEmptyString(obj.target, "target");
|
|
7485
7867
|
const out = { reviewer, target };
|
|
7868
|
+
if (obj.repos !== void 0) {
|
|
7869
|
+
out.repos = parseStringArray(obj.repos, "repos");
|
|
7870
|
+
}
|
|
7871
|
+
if (obj.commits !== void 0) {
|
|
7872
|
+
out.commits = parseStringArray(obj.commits, "commits");
|
|
7873
|
+
}
|
|
7486
7874
|
if (obj.verdict !== void 0) {
|
|
7487
7875
|
if (typeof obj.verdict !== "string" || !VALID_VERDICTS.has(obj.verdict)) {
|
|
7488
7876
|
throw new Error(`verdict must be one of pass, needs-attention, fail, got '${obj.verdict}'.`);
|
|
@@ -7497,6 +7885,12 @@ function parseReviewRecordInput(raw) {
|
|
|
7497
7885
|
}
|
|
7498
7886
|
return out;
|
|
7499
7887
|
}
|
|
7888
|
+
function parseStringArray(value, field) {
|
|
7889
|
+
if (!Array.isArray(value)) {
|
|
7890
|
+
throw new Error(`${field} must be an array of strings.`);
|
|
7891
|
+
}
|
|
7892
|
+
return value.map((item, i) => requireNonEmptyString(item, `${field}[${i}]`));
|
|
7893
|
+
}
|
|
7500
7894
|
function parseFindings(value) {
|
|
7501
7895
|
if (!Array.isArray(value)) {
|
|
7502
7896
|
throw new Error("findings must be an array of objects.");
|
|
@@ -7579,6 +7973,9 @@ function buildReviewRecordedEvent(input) {
|
|
|
7579
7973
|
type: "review_recorded",
|
|
7580
7974
|
reviewer: review.reviewer,
|
|
7581
7975
|
target: review.target,
|
|
7976
|
+
...review.repos !== void 0 ? { repos: review.repos } : {},
|
|
7977
|
+
...input.reposResolved !== void 0 && input.reposResolved.length > 0 ? { repos_resolved: input.reposResolved } : {},
|
|
7978
|
+
...review.commits !== void 0 ? { commits: review.commits } : {},
|
|
7582
7979
|
...review.verdict !== void 0 ? { verdict: review.verdict } : {},
|
|
7583
7980
|
...review.findings !== void 0 ? { findings: review.findings } : {},
|
|
7584
7981
|
...review.blocked !== void 0 ? { blocked: review.blocked } : {}
|
|
@@ -8589,6 +8986,7 @@ export {
|
|
|
8589
8986
|
findBasouStopHookCommand,
|
|
8590
8987
|
findErrorCode,
|
|
8591
8988
|
findReviewGaps,
|
|
8989
|
+
findUnbindableRepos,
|
|
8592
8990
|
formatDurationMs,
|
|
8593
8991
|
genesisHash,
|
|
8594
8992
|
getDiff,
|
|
@@ -8652,6 +9050,7 @@ export {
|
|
|
8652
9050
|
resolveClaudeCodeCommand,
|
|
8653
9051
|
resolveCodexCommand,
|
|
8654
9052
|
resolveRepoContentLanguage,
|
|
9053
|
+
resolveRepoRoot,
|
|
8655
9054
|
resolveRepositoryRoot,
|
|
8656
9055
|
resolveSessionId,
|
|
8657
9056
|
resolveTaskId,
|