diffowl 0.1.0 → 0.2.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/README.md +3 -20
- package/dist/cli.js +291 -194
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,19 @@ import { existsSync } from "fs";
|
|
|
12
12
|
import { join, dirname } from "path";
|
|
13
13
|
import { parse, stringify } from "yaml";
|
|
14
14
|
import { z, ZodError } from "zod";
|
|
15
|
+
var ReviewConfidenceSchema = z.enum(["low", "medium", "high"]);
|
|
16
|
+
var ReviewContextDepthSchema = z.enum(["shallow", "default"]);
|
|
17
|
+
var ReasoningEffortSchema = z.enum([
|
|
18
|
+
"auto",
|
|
19
|
+
"none",
|
|
20
|
+
"minimal",
|
|
21
|
+
"low",
|
|
22
|
+
"medium",
|
|
23
|
+
"high",
|
|
24
|
+
"max",
|
|
25
|
+
"xhigh"
|
|
26
|
+
]);
|
|
27
|
+
var ModelSchema = z.string().trim().min(1, "model must not be empty").regex(/^[^/\s]+\/\S+$/, "model must use provider/model format");
|
|
15
28
|
var DEFAULT_CONFIG = {
|
|
16
29
|
model: "opencode-go/big-pickle",
|
|
17
30
|
server: {
|
|
@@ -19,17 +32,17 @@ var DEFAULT_CONFIG = {
|
|
|
19
32
|
auto_start: true
|
|
20
33
|
},
|
|
21
34
|
context: {
|
|
22
|
-
depth:
|
|
35
|
+
depth: ReviewContextDepthSchema.enum.default
|
|
23
36
|
},
|
|
24
37
|
reasoning: {
|
|
25
|
-
effort:
|
|
38
|
+
effort: ReasoningEffortSchema.enum.auto
|
|
26
39
|
},
|
|
27
40
|
retention: {
|
|
28
41
|
hook_log_kb: 1024
|
|
29
42
|
},
|
|
30
43
|
timeout: 300,
|
|
31
44
|
// 5 minutes
|
|
32
|
-
min_confidence:
|
|
45
|
+
min_confidence: ReviewConfidenceSchema.enum.medium,
|
|
33
46
|
include: ["**/*"],
|
|
34
47
|
exclude: [
|
|
35
48
|
"**/*.test.*",
|
|
@@ -45,19 +58,6 @@ var DEFAULT_CONFIG = {
|
|
|
45
58
|
verbose: false
|
|
46
59
|
};
|
|
47
60
|
var CONFIG_FILENAME = ".diffowl.yml";
|
|
48
|
-
var ReviewConfidenceSchema = z.enum(["low", "medium", "high"]);
|
|
49
|
-
var ReviewContextDepthSchema = z.enum(["shallow", "default"]);
|
|
50
|
-
var ReasoningEffortSchema = z.enum([
|
|
51
|
-
"auto",
|
|
52
|
-
"none",
|
|
53
|
-
"minimal",
|
|
54
|
-
"low",
|
|
55
|
-
"medium",
|
|
56
|
-
"high",
|
|
57
|
-
"max",
|
|
58
|
-
"xhigh"
|
|
59
|
-
]);
|
|
60
|
-
var ModelSchema = z.string().trim().min(1, "model must not be empty").regex(/^[^/\s]+\/\S+$/, "model must use provider/model format");
|
|
61
61
|
var stringArraySchema = z.array(z.string().trim().min(1));
|
|
62
62
|
var DiffOwlConfigSchema = z.object({
|
|
63
63
|
model: ModelSchema.default(DEFAULT_CONFIG.model),
|
|
@@ -114,7 +114,7 @@ function findConfigPath() {
|
|
|
114
114
|
async function loadConfig() {
|
|
115
115
|
const configPath = findConfigPath();
|
|
116
116
|
if (!existsSync(configPath)) {
|
|
117
|
-
return {
|
|
117
|
+
return parseConfigInput({});
|
|
118
118
|
}
|
|
119
119
|
try {
|
|
120
120
|
const raw = await readFile(configPath, "utf-8");
|
|
@@ -366,8 +366,8 @@ Required review passes:
|
|
|
366
366
|
- Performance and boundedness: Look for unbounded scans, large-file/diff cliffs, slow hook behavior, and expensive operations in common paths.
|
|
367
367
|
- Data filtering/loss: Look for data silently dropped, hidden, duplicated, parsed with a fallback, or reported inconsistently.
|
|
368
368
|
`;
|
|
369
|
-
function buildReviewPrompt(
|
|
370
|
-
const modeInstruction =
|
|
369
|
+
function buildReviewPrompt(target, customRules, include, exclude, localContext, depth = "default") {
|
|
370
|
+
const modeInstruction = target.kind === "staged" ? "Review the currently staged changes." : target.kind === "commit" ? "Review the selected commit." : "Review the last commit.";
|
|
371
371
|
let prompt = `${modeInstruction}
|
|
372
372
|
|
|
373
373
|
DiffOwl has already collected the diff and likely-relevant local context below. Use this context first.
|
|
@@ -425,7 +425,7 @@ var ReviewSeveritySchema = z2.preprocess(
|
|
|
425
425
|
);
|
|
426
426
|
var ReviewConfidenceSchema2 = z2.preprocess(
|
|
427
427
|
(value) => typeof value === "string" ? value.toLowerCase() : value,
|
|
428
|
-
|
|
428
|
+
ReviewConfidenceSchema
|
|
429
429
|
).catch("low");
|
|
430
430
|
var ReviewFindingLineSchema = z2.preprocess(
|
|
431
431
|
(value) => typeof value === "string" ? Number(value) : value,
|
|
@@ -553,19 +553,20 @@ function createReviewSettlementCoordinator(options) {
|
|
|
553
553
|
let reconciliationRunning = false;
|
|
554
554
|
let timeoutRequested = false;
|
|
555
555
|
let lastReconciliationError;
|
|
556
|
-
const settle = (outcome
|
|
556
|
+
const settle = (outcome) => {
|
|
557
557
|
if (settled) return;
|
|
558
558
|
settled = true;
|
|
559
559
|
clearTimeout(safetyTimeout);
|
|
560
560
|
clearInterval(reconciliationInterval);
|
|
561
561
|
options.onAbort();
|
|
562
|
-
if (outcome === "resolve") {
|
|
563
|
-
options.resolve(
|
|
562
|
+
if (outcome.kind === "resolve") {
|
|
563
|
+
options.resolve(outcome.text);
|
|
564
564
|
} else {
|
|
565
|
-
options.reject(
|
|
565
|
+
options.reject(outcome.error);
|
|
566
566
|
}
|
|
567
567
|
};
|
|
568
568
|
const acceptText = (text) => {
|
|
569
|
+
if (settled) return false;
|
|
569
570
|
if (text.length > fullResponse.length) {
|
|
570
571
|
fullResponse = text;
|
|
571
572
|
options.onText?.(fullResponse);
|
|
@@ -576,7 +577,7 @@ function createReviewSettlementCoordinator(options) {
|
|
|
576
577
|
if (lengthDelta > 500 || endsWithBrace) {
|
|
577
578
|
lastCheckedLength = fullResponse.length;
|
|
578
579
|
if (looksLikeCompleteStructuredReview(fullResponse)) {
|
|
579
|
-
settle("resolve", fullResponse);
|
|
580
|
+
settle({ kind: "resolve", text: fullResponse });
|
|
580
581
|
return true;
|
|
581
582
|
}
|
|
582
583
|
}
|
|
@@ -587,27 +588,30 @@ function createReviewSettlementCoordinator(options) {
|
|
|
587
588
|
reconciliationRunning = true;
|
|
588
589
|
try {
|
|
589
590
|
const result = await options.reconcile();
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
591
|
+
switch (result.kind) {
|
|
592
|
+
case "review-error":
|
|
593
|
+
settle({ kind: "reject", error: result.error });
|
|
594
|
+
return;
|
|
595
|
+
case "transport-error":
|
|
596
|
+
lastReconciliationError = result.error;
|
|
597
|
+
break;
|
|
598
|
+
case "text":
|
|
599
|
+
lastReconciliationError = void 0;
|
|
600
|
+
if (acceptText(result.text)) return;
|
|
601
|
+
break;
|
|
602
|
+
case "empty":
|
|
603
|
+
lastReconciliationError = void 0;
|
|
604
|
+
break;
|
|
601
605
|
}
|
|
602
606
|
if (isTimeout || timeoutRequested) {
|
|
603
607
|
const suffix = lastReconciliationError ? ` Last session reconciliation error: ${lastReconciliationError.message}` : "";
|
|
604
|
-
settle(
|
|
605
|
-
"reject",
|
|
606
|
-
new Error(
|
|
608
|
+
settle({
|
|
609
|
+
kind: "reject",
|
|
610
|
+
error: new Error(
|
|
607
611
|
`Review timed out.${suffix}`,
|
|
608
612
|
lastReconciliationError ? { cause: lastReconciliationError } : void 0
|
|
609
613
|
)
|
|
610
|
-
);
|
|
614
|
+
});
|
|
611
615
|
}
|
|
612
616
|
} finally {
|
|
613
617
|
reconciliationRunning = false;
|
|
@@ -625,14 +629,14 @@ function createReviewSettlementCoordinator(options) {
|
|
|
625
629
|
acceptText,
|
|
626
630
|
finish: () => {
|
|
627
631
|
if (settled || acceptText(fullResponse)) return;
|
|
628
|
-
settle(
|
|
629
|
-
"reject",
|
|
630
|
-
new Error("OpenCode event stream ended before a complete review was received.")
|
|
631
|
-
);
|
|
632
|
+
settle({
|
|
633
|
+
kind: "reject",
|
|
634
|
+
error: new Error("OpenCode event stream ended before a complete review was received.")
|
|
635
|
+
});
|
|
632
636
|
},
|
|
633
637
|
isSettled: () => settled,
|
|
634
|
-
reject: (error) => settle("reject", error),
|
|
635
|
-
resolve: (text) => settle("resolve", text)
|
|
638
|
+
reject: (error) => settle({ kind: "reject", error }),
|
|
639
|
+
resolve: (text) => settle({ kind: "resolve", text })
|
|
636
640
|
};
|
|
637
641
|
}
|
|
638
642
|
|
|
@@ -706,7 +710,7 @@ async function replyWithAvailableEndpoint(client, permission, response) {
|
|
|
706
710
|
});
|
|
707
711
|
}
|
|
708
712
|
}
|
|
709
|
-
function extractPermissionRequest(payload,
|
|
713
|
+
function extractPermissionRequest(payload, expectedSessionId) {
|
|
710
714
|
if (!payload || typeof payload !== "object") {
|
|
711
715
|
return void 0;
|
|
712
716
|
}
|
|
@@ -715,7 +719,8 @@ function extractPermissionRequest(payload, sessionId) {
|
|
|
715
719
|
return void 0;
|
|
716
720
|
}
|
|
717
721
|
const properties = event.properties;
|
|
718
|
-
|
|
722
|
+
const sessionId = properties["sessionID"];
|
|
723
|
+
if (typeof sessionId !== "string" || expectedSessionId !== void 0 && sessionId !== expectedSessionId) {
|
|
719
724
|
return void 0;
|
|
720
725
|
}
|
|
721
726
|
if (event.type === "permission.updated") {
|
|
@@ -832,14 +837,96 @@ function listAvailableModels(payload) {
|
|
|
832
837
|
}
|
|
833
838
|
|
|
834
839
|
// src/opencode/client.ts
|
|
835
|
-
function
|
|
840
|
+
function normalizeOpenCodeEvent(event, expectedSessionId) {
|
|
836
841
|
if (!event || typeof event !== "object") return void 0;
|
|
837
842
|
const payload = event.payload;
|
|
838
843
|
if (!payload || typeof payload !== "object") return void 0;
|
|
839
|
-
|
|
844
|
+
const raw = payload;
|
|
845
|
+
if (typeof raw.type !== "string" || !raw.properties || typeof raw.properties !== "object") {
|
|
846
|
+
return void 0;
|
|
847
|
+
}
|
|
848
|
+
const permission = extractPermissionRequest(payload, expectedSessionId);
|
|
849
|
+
if (permission) return { type: "permission", request: permission };
|
|
850
|
+
const properties = raw.properties;
|
|
851
|
+
const sessionId = properties["sessionID"];
|
|
852
|
+
if (expectedSessionId !== void 0 && typeof sessionId === "string" && sessionId !== expectedSessionId) {
|
|
853
|
+
return void 0;
|
|
854
|
+
}
|
|
855
|
+
if (raw.type === "session.error" && typeof sessionId === "string") {
|
|
856
|
+
return {
|
|
857
|
+
type: "session-error",
|
|
858
|
+
sessionId,
|
|
859
|
+
error: new Error(`OpenCode session failed: ${describeSessionError(properties["error"])}`)
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
if (raw.type === "message.part.updated") {
|
|
863
|
+
return normalizeMessagePart(properties["part"], expectedSessionId);
|
|
864
|
+
}
|
|
865
|
+
if (raw.type === "message.updated") {
|
|
866
|
+
return normalizeAssistantMessage(properties["info"], expectedSessionId);
|
|
867
|
+
}
|
|
868
|
+
if (raw.type === "session.status" && typeof sessionId === "string") {
|
|
869
|
+
const status = properties["status"];
|
|
870
|
+
if (!status || typeof status !== "object") return void 0;
|
|
871
|
+
const statusType = status.type;
|
|
872
|
+
if (typeof statusType !== "string") return void 0;
|
|
873
|
+
const message = status.message;
|
|
874
|
+
return {
|
|
875
|
+
type: "session-status",
|
|
876
|
+
sessionId,
|
|
877
|
+
status: statusType,
|
|
878
|
+
...typeof message === "string" ? { message } : {}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
if (raw.type === "session.idle" && typeof sessionId === "string") {
|
|
882
|
+
return { type: "session-idle", sessionId };
|
|
883
|
+
}
|
|
884
|
+
return void 0;
|
|
885
|
+
}
|
|
886
|
+
function normalizeMessagePart(part, expectedSessionId) {
|
|
887
|
+
if (!part || typeof part !== "object") return void 0;
|
|
888
|
+
const value = part;
|
|
889
|
+
const sessionId = value["sessionID"];
|
|
890
|
+
if (typeof sessionId !== "string" || expectedSessionId !== void 0 && sessionId !== expectedSessionId) {
|
|
891
|
+
return void 0;
|
|
892
|
+
}
|
|
893
|
+
if (value["type"] === "tool" && typeof value["tool"] === "string") {
|
|
894
|
+
const state = value["state"] && typeof value["state"] === "object" ? value["state"] : void 0;
|
|
895
|
+
const status = typeof state?.["status"] === "string" ? state["status"] : "unknown";
|
|
896
|
+
const title = typeof state?.["title"] === "string" ? state["title"] : value["tool"];
|
|
897
|
+
return {
|
|
898
|
+
type: "tool-part",
|
|
899
|
+
sessionId,
|
|
900
|
+
tool: value["tool"],
|
|
901
|
+
status,
|
|
902
|
+
title
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
if (value["type"] === "text" && typeof value["messageID"] === "string" && typeof value["text"] === "string" && value["text"] !== "") {
|
|
906
|
+
return {
|
|
907
|
+
type: "text-part",
|
|
908
|
+
sessionId,
|
|
909
|
+
messageId: value["messageID"],
|
|
910
|
+
text: value["text"]
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
return void 0;
|
|
914
|
+
}
|
|
915
|
+
function normalizeAssistantMessage(info, expectedSessionId) {
|
|
916
|
+
if (!info || typeof info !== "object") return void 0;
|
|
917
|
+
const value = info;
|
|
918
|
+
if (value["role"] !== "assistant" || typeof value["sessionID"] !== "string" || typeof value["id"] !== "string" || expectedSessionId !== void 0 && value["sessionID"] !== expectedSessionId) {
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
type: "assistant-message",
|
|
923
|
+
sessionId: value["sessionID"],
|
|
924
|
+
messageId: value["id"],
|
|
925
|
+
...value["error"] ? { error: new Error(describeSessionError(value["error"]) || "Review failed") } : {}
|
|
926
|
+
};
|
|
840
927
|
}
|
|
841
928
|
async function runReview(options) {
|
|
842
|
-
const {
|
|
929
|
+
const { target, config, localContext, depth, onProgress } = options;
|
|
843
930
|
const port = config.server.port;
|
|
844
931
|
const directoryOptions = opencodeDirectoryOptions();
|
|
845
932
|
const timings = [];
|
|
@@ -869,7 +956,7 @@ async function runReview(options) {
|
|
|
869
956
|
recordTiming(timings, onProgress, "tool-policy", "OpenCode tool policy", toolPolicyStart);
|
|
870
957
|
const promptStart = performance.now();
|
|
871
958
|
const prompt = buildReviewPrompt(
|
|
872
|
-
|
|
959
|
+
target,
|
|
873
960
|
config.rules,
|
|
874
961
|
config.include,
|
|
875
962
|
config.exclude,
|
|
@@ -920,69 +1007,59 @@ async function runReview(options) {
|
|
|
920
1007
|
try {
|
|
921
1008
|
for await (const event of sseResult.stream) {
|
|
922
1009
|
if (settlement.isSettled()) break;
|
|
923
|
-
const
|
|
924
|
-
if (!
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
if (payload.type === "message.part.updated" && payload.properties?.part?.sessionID === sessionId) {
|
|
942
|
-
const part = payload.properties.part;
|
|
943
|
-
if (part.type === "tool" && typeof part.tool === "string") {
|
|
944
|
-
const state = typeof part.state?.status === "string" ? part.state.status : "unknown";
|
|
945
|
-
const title = typeof part.state?.title === "string" ? part.state.title : part.tool;
|
|
1010
|
+
const normalized = normalizeOpenCodeEvent(event, sessionId);
|
|
1011
|
+
if (!normalized) continue;
|
|
1012
|
+
switch (normalized.type) {
|
|
1013
|
+
case "permission":
|
|
1014
|
+
void replyToPermissionRequest(client, normalized.request, onProgress).catch(
|
|
1015
|
+
(err) => {
|
|
1016
|
+
onProgress?.({
|
|
1017
|
+
type: "session",
|
|
1018
|
+
message: `OpenCode permission reply failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1019
|
+
sessionId
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
);
|
|
1023
|
+
break;
|
|
1024
|
+
case "session-error":
|
|
1025
|
+
settlement.reject(normalized.error);
|
|
1026
|
+
break;
|
|
1027
|
+
case "tool-part":
|
|
946
1028
|
onProgress?.({
|
|
947
1029
|
type: "tool",
|
|
948
|
-
message: `${title} (${
|
|
949
|
-
tool:
|
|
950
|
-
status:
|
|
1030
|
+
message: `${normalized.title} (${normalized.status})`,
|
|
1031
|
+
tool: normalized.tool,
|
|
1032
|
+
status: normalized.status
|
|
951
1033
|
});
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
textPartsByMessageId.set(
|
|
955
|
-
if (assistantMessageIds.has(
|
|
1034
|
+
break;
|
|
1035
|
+
case "text-part":
|
|
1036
|
+
textPartsByMessageId.set(normalized.messageId, normalized.text);
|
|
1037
|
+
if (assistantMessageIds.has(normalized.messageId) && settlement.acceptText(normalized.text)) {
|
|
956
1038
|
break;
|
|
957
1039
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
if (msg?.role === "assistant" && typeof msg.id === "string") {
|
|
963
|
-
assistantMessageIds.add(msg.id);
|
|
964
|
-
const text = textPartsByMessageId.get(msg.id);
|
|
1040
|
+
break;
|
|
1041
|
+
case "assistant-message": {
|
|
1042
|
+
assistantMessageIds.add(normalized.messageId);
|
|
1043
|
+
const text = textPartsByMessageId.get(normalized.messageId);
|
|
965
1044
|
if (text && settlement.acceptText(text)) {
|
|
966
1045
|
break;
|
|
967
1046
|
}
|
|
968
|
-
if (
|
|
969
|
-
|
|
970
|
-
settlement.reject(new Error(message));
|
|
971
|
-
break;
|
|
1047
|
+
if (normalized.error) {
|
|
1048
|
+
settlement.reject(normalized.error);
|
|
972
1049
|
}
|
|
1050
|
+
break;
|
|
973
1051
|
}
|
|
1052
|
+
case "session-status":
|
|
1053
|
+
const message = normalized.status === "retry" ? `OpenCode retrying: ${normalized.message ?? "unknown error"}` : `OpenCode session ${normalized.status}.`;
|
|
1054
|
+
onProgress?.({ type: "session", message, sessionId });
|
|
1055
|
+
break;
|
|
1056
|
+
case "session-idle":
|
|
1057
|
+
if (fullResponse.length === 0) break;
|
|
1058
|
+
onProgress?.({ type: "idle", message: "OpenCode session is idle." });
|
|
1059
|
+
settlement.finish();
|
|
1060
|
+
break;
|
|
974
1061
|
}
|
|
975
|
-
if (
|
|
976
|
-
const status = payload.properties.status;
|
|
977
|
-
if (!status || typeof status.type !== "string") continue;
|
|
978
|
-
const message = status.type === "retry" ? `OpenCode retrying: ${typeof status.message === "string" ? status.message : "unknown error"}` : `OpenCode session ${status.type}.`;
|
|
979
|
-
onProgress?.({ type: "session", message, sessionId });
|
|
980
|
-
}
|
|
981
|
-
if (payload.type === "session.idle" && payload.properties?.sessionID === sessionId && fullResponse.length > 0) {
|
|
982
|
-
onProgress?.({ type: "idle", message: "OpenCode session is idle." });
|
|
983
|
-
settlement.finish();
|
|
984
|
-
break;
|
|
985
|
-
}
|
|
1062
|
+
if (settlement.isSettled()) break;
|
|
986
1063
|
}
|
|
987
1064
|
if (!settlement.isSettled()) {
|
|
988
1065
|
settlement.finish();
|
|
@@ -1031,18 +1108,10 @@ async function runReview(options) {
|
|
|
1031
1108
|
sessionId
|
|
1032
1109
|
};
|
|
1033
1110
|
}
|
|
1034
|
-
function extractSessionError(payload, sessionId) {
|
|
1035
|
-
if (!payload || typeof payload !== "object") return void 0;
|
|
1036
|
-
const event = payload;
|
|
1037
|
-
if (event.type !== "session.error" || event.properties?.sessionID !== sessionId) {
|
|
1038
|
-
return void 0;
|
|
1039
|
-
}
|
|
1040
|
-
return new Error(`OpenCode session failed: ${describeSessionError(event.properties.error)}`);
|
|
1041
|
-
}
|
|
1042
1111
|
function extractSessionMessageResult(response) {
|
|
1043
|
-
if (!response || typeof response !== "object") return
|
|
1112
|
+
if (!response || typeof response !== "object") return { kind: "empty" };
|
|
1044
1113
|
const data = response.data;
|
|
1045
|
-
if (!Array.isArray(data)) return
|
|
1114
|
+
if (!Array.isArray(data)) return { kind: "empty" };
|
|
1046
1115
|
for (let index = data.length - 1; index >= 0; index--) {
|
|
1047
1116
|
const message = data[index];
|
|
1048
1117
|
if (!message || typeof message !== "object") continue;
|
|
@@ -1052,7 +1121,10 @@ function extractSessionMessageResult(response) {
|
|
|
1052
1121
|
}
|
|
1053
1122
|
const error = info.error;
|
|
1054
1123
|
if (error) {
|
|
1055
|
-
return {
|
|
1124
|
+
return {
|
|
1125
|
+
kind: "review-error",
|
|
1126
|
+
error: new Error(`OpenCode session failed: ${describeSessionError(error)}`)
|
|
1127
|
+
};
|
|
1056
1128
|
}
|
|
1057
1129
|
const parts = message.parts;
|
|
1058
1130
|
if (!Array.isArray(parts)) continue;
|
|
@@ -1061,9 +1133,9 @@ function extractSessionMessageResult(response) {
|
|
|
1061
1133
|
part && typeof part === "object" && part.type === "text" && typeof part.text === "string"
|
|
1062
1134
|
)
|
|
1063
1135
|
).map((part) => part.text).join("");
|
|
1064
|
-
if (text) return { text };
|
|
1136
|
+
if (text) return { kind: "text", text };
|
|
1065
1137
|
}
|
|
1066
|
-
return
|
|
1138
|
+
return { kind: "empty" };
|
|
1067
1139
|
}
|
|
1068
1140
|
async function reconcileSessionMessages(client, directoryOptions, sessionId) {
|
|
1069
1141
|
try {
|
|
@@ -1080,7 +1152,8 @@ async function reconcileSessionMessages(client, directoryOptions, sessionId) {
|
|
|
1080
1152
|
return extractSessionMessageResult(response);
|
|
1081
1153
|
} catch (error) {
|
|
1082
1154
|
return {
|
|
1083
|
-
|
|
1155
|
+
kind: "transport-error",
|
|
1156
|
+
error: error instanceof Error ? error : new Error(`Session reconciliation failed: ${String(error)}`)
|
|
1084
1157
|
};
|
|
1085
1158
|
}
|
|
1086
1159
|
}
|
|
@@ -1298,7 +1371,7 @@ async function installHook() {
|
|
|
1298
1371
|
const command = await resolveHookCommand();
|
|
1299
1372
|
if (existsSync3(hookPath)) {
|
|
1300
1373
|
const existing = await readFile4(hookPath, "utf-8");
|
|
1301
|
-
const base = existing.includes(HOOK_MARKER)
|
|
1374
|
+
const base = existing.includes(HOOK_MARKER) ? removeManagedSection(existing) : existing.trimEnd();
|
|
1302
1375
|
const hookSection = generateManagedSection(command);
|
|
1303
1376
|
const updated = base && !isOnlyShebangs(base) ? `${base}
|
|
1304
1377
|
|
|
@@ -1315,7 +1388,7 @@ async function uninstallHook() {
|
|
|
1315
1388
|
const hookPath = join3(hooksDir, "post-commit");
|
|
1316
1389
|
if (!existsSync3(hookPath)) return false;
|
|
1317
1390
|
const content = await readFile4(hookPath, "utf-8");
|
|
1318
|
-
if (!content.includes(HOOK_MARKER)
|
|
1391
|
+
if (!content.includes(HOOK_MARKER)) return false;
|
|
1319
1392
|
const cleaned = removeManagedSection(content);
|
|
1320
1393
|
if (isOnlyShebangs(cleaned) || cleaned === "") {
|
|
1321
1394
|
await unlink2(hookPath);
|
|
@@ -1579,8 +1652,12 @@ function isHookReviewLockActive(lockFile) {
|
|
|
1579
1652
|
try {
|
|
1580
1653
|
const pid = Number.parseInt(readFileSync(lockFile, "utf-8"), 10);
|
|
1581
1654
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1582
|
-
|
|
1583
|
-
|
|
1655
|
+
try {
|
|
1656
|
+
process.kill(pid, 0);
|
|
1657
|
+
return true;
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
return err.code === "EPERM";
|
|
1660
|
+
}
|
|
1584
1661
|
} catch {
|
|
1585
1662
|
return false;
|
|
1586
1663
|
}
|
|
@@ -1675,7 +1752,6 @@ async function resolveCommand(command) {
|
|
|
1675
1752
|
function removeManagedSection(content) {
|
|
1676
1753
|
let lines = content.split("\n");
|
|
1677
1754
|
lines = removeSectionByMarkers(lines, "# diffowl-managed", "# end-diffowl");
|
|
1678
|
-
lines = removeSectionByMarkers(lines, "# commitdog-managed", "# end-commitdog");
|
|
1679
1755
|
return lines.join("\n").trim();
|
|
1680
1756
|
}
|
|
1681
1757
|
function removeSectionByMarkers(lines, startMarker, endMarker) {
|
|
@@ -1821,12 +1897,13 @@ async function hasCommits() {
|
|
|
1821
1897
|
}
|
|
1822
1898
|
}
|
|
1823
1899
|
function parseDiff(raw, diagnostics = []) {
|
|
1824
|
-
const
|
|
1900
|
+
const drafts = [];
|
|
1825
1901
|
const lines = raw.split(/\r?\n/).map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
1826
1902
|
for (const line of lines) {
|
|
1827
1903
|
const gitDiffPaths = parseGitDiffLine(line);
|
|
1828
1904
|
if (gitDiffPaths) {
|
|
1829
|
-
|
|
1905
|
+
drafts.push({
|
|
1906
|
+
sourcePath: gitDiffPaths.pathA,
|
|
1830
1907
|
path: gitDiffPaths.pathB,
|
|
1831
1908
|
status: "modified",
|
|
1832
1909
|
additions: 0,
|
|
@@ -1836,7 +1913,8 @@ function parseDiff(raw, diagnostics = []) {
|
|
|
1836
1913
|
}
|
|
1837
1914
|
const combinedPath = parseCombinedDiffLine(line);
|
|
1838
1915
|
if (combinedPath) {
|
|
1839
|
-
|
|
1916
|
+
drafts.push({
|
|
1917
|
+
sourcePath: combinedPath,
|
|
1840
1918
|
path: combinedPath,
|
|
1841
1919
|
status: "modified",
|
|
1842
1920
|
additions: 0,
|
|
@@ -1844,11 +1922,10 @@ function parseDiff(raw, diagnostics = []) {
|
|
|
1844
1922
|
});
|
|
1845
1923
|
continue;
|
|
1846
1924
|
}
|
|
1847
|
-
const lastFile =
|
|
1925
|
+
const lastFile = drafts[drafts.length - 1];
|
|
1848
1926
|
if (lastFile) {
|
|
1849
1927
|
if (line.startsWith("rename to ")) {
|
|
1850
|
-
|
|
1851
|
-
lastFile.path = target;
|
|
1928
|
+
lastFile.path = unescapePath(line.slice("rename to ".length));
|
|
1852
1929
|
lastFile.status = "renamed";
|
|
1853
1930
|
continue;
|
|
1854
1931
|
}
|
|
@@ -1867,9 +1944,20 @@ function parseDiff(raw, diagnostics = []) {
|
|
|
1867
1944
|
}
|
|
1868
1945
|
}
|
|
1869
1946
|
}
|
|
1870
|
-
const
|
|
1947
|
+
const files = drafts.map(finalizeDiffFile);
|
|
1948
|
+
const summary = files.map((file) => {
|
|
1949
|
+
const path = file.status === "renamed" ? `${file.oldPath} -> ${file.path}` : file.path;
|
|
1950
|
+
return `${statusSymbol(file.status)} ${path} (+${file.additions}/-${file.deletions})`;
|
|
1951
|
+
}).join("\n");
|
|
1871
1952
|
return { files, raw, summary, ...diagnostics.length > 0 ? { diagnostics } : {} };
|
|
1872
1953
|
}
|
|
1954
|
+
function finalizeDiffFile(draft) {
|
|
1955
|
+
const { sourcePath, path, status, additions, deletions } = draft;
|
|
1956
|
+
if (status === "renamed") {
|
|
1957
|
+
return { oldPath: sourcePath, path, status, additions, deletions };
|
|
1958
|
+
}
|
|
1959
|
+
return { path, status, additions, deletions };
|
|
1960
|
+
}
|
|
1873
1961
|
function isMaxBufferError(err) {
|
|
1874
1962
|
return err !== null && typeof err === "object" && err.isMaxBuffer === true && "stdout" in err && typeof err.stdout === "string";
|
|
1875
1963
|
}
|
|
@@ -2347,7 +2435,7 @@ function renderReviewContext(context, options = {}) {
|
|
|
2347
2435
|
const lines = [];
|
|
2348
2436
|
lines.push("## Local Review Context");
|
|
2349
2437
|
lines.push("");
|
|
2350
|
-
lines.push(`Mode: ${context.
|
|
2438
|
+
lines.push(`Mode: ${context.target.kind}`);
|
|
2351
2439
|
lines.push(`Review depth: ${depth}`);
|
|
2352
2440
|
lines.push("");
|
|
2353
2441
|
lines.push("### Changed Files");
|
|
@@ -2413,22 +2501,30 @@ function renderReviewContext(context, options = {}) {
|
|
|
2413
2501
|
lines.push("");
|
|
2414
2502
|
}
|
|
2415
2503
|
}
|
|
2416
|
-
if (fileContext.content
|
|
2417
|
-
lines.push(
|
|
2418
|
-
fence(
|
|
2419
|
-
shallow ? truncateText2(fileContext.content, MAX_QUICK_FILE_CHARS).text : fileContext.content,
|
|
2420
|
-
languageForPath(fileContext.file.path)
|
|
2421
|
-
)
|
|
2422
|
-
);
|
|
2423
|
-
if (fileContext.truncated) {
|
|
2424
|
-
lines.push("_File content truncated._");
|
|
2425
|
-
}
|
|
2426
|
-
} else if (fileContext.content && fileContext.astSymbols.length === 0) {
|
|
2427
|
-
lines.push("_Full file content omitted because the diff already shows the changed hunks._");
|
|
2428
|
-
} else if (fileContext.content) {
|
|
2429
|
-
lines.push("_Full file content omitted because changed AST symbols are shown._");
|
|
2504
|
+
if (fileContext.content.status === "skipped") {
|
|
2505
|
+
lines.push(`_File content skipped: ${fileContext.content.reason}._`);
|
|
2430
2506
|
} else {
|
|
2431
|
-
|
|
2507
|
+
switch (fileContext.content.render) {
|
|
2508
|
+
case "full":
|
|
2509
|
+
lines.push(
|
|
2510
|
+
fence(
|
|
2511
|
+
shallow ? truncateText2(fileContext.content.text, MAX_QUICK_FILE_CHARS).text : fileContext.content.text,
|
|
2512
|
+
languageForPath(fileContext.file.path)
|
|
2513
|
+
)
|
|
2514
|
+
);
|
|
2515
|
+
if (fileContext.content.truncated) {
|
|
2516
|
+
lines.push("_File content truncated._");
|
|
2517
|
+
}
|
|
2518
|
+
break;
|
|
2519
|
+
case "diff-only":
|
|
2520
|
+
lines.push(
|
|
2521
|
+
"_Full file content omitted because the diff already shows the changed hunks._"
|
|
2522
|
+
);
|
|
2523
|
+
break;
|
|
2524
|
+
case "ast-symbols":
|
|
2525
|
+
lines.push("_Full file content omitted because changed AST symbols are shown._");
|
|
2526
|
+
break;
|
|
2527
|
+
}
|
|
2432
2528
|
}
|
|
2433
2529
|
lines.push("");
|
|
2434
2530
|
}
|
|
@@ -2536,8 +2632,18 @@ var MAX_INLINE_FILE_LINES = 80;
|
|
|
2536
2632
|
var MAX_CONTEXT_FILE_BYTES = 512 * 1024;
|
|
2537
2633
|
var MIN_CHANGED_RATIO_FOR_INLINE_CONTENT = 0.4;
|
|
2538
2634
|
var LOCKFILE_EXCLUDES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb"];
|
|
2539
|
-
async function
|
|
2540
|
-
|
|
2635
|
+
async function loadReviewDiff(target) {
|
|
2636
|
+
switch (target.kind) {
|
|
2637
|
+
case "staged":
|
|
2638
|
+
return getStagedDiff();
|
|
2639
|
+
case "commit":
|
|
2640
|
+
return getCommitDiff(target.ref);
|
|
2641
|
+
case "last-commit":
|
|
2642
|
+
return getLastCommitDiff();
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
async function buildReviewContextFromDiff(snapshot, config, depth = config.context.depth) {
|
|
2646
|
+
const { target, diff: diffResult } = snapshot;
|
|
2541
2647
|
const reviewableFiles = diffResult.files.filter((file) => shouldReviewFile(file.path, config));
|
|
2542
2648
|
const skippedFiles = diffResult.files.filter((file) => !shouldReviewFile(file.path, config));
|
|
2543
2649
|
const changedLines = getChangedLinesByFile(diffResult.raw);
|
|
@@ -2553,7 +2659,7 @@ async function buildReviewContext(mode, config, depth = config.context.depth, di
|
|
|
2553
2659
|
const relatedFiles = depth === "shallow" ? [] : await buildRelatedFileContexts(reviewableFiles);
|
|
2554
2660
|
const references = depth === "shallow" ? [] : await buildReferenceContexts(changedFiles, skippedFiles, diagnostics);
|
|
2555
2661
|
return {
|
|
2556
|
-
|
|
2662
|
+
target,
|
|
2557
2663
|
depth,
|
|
2558
2664
|
diff: diffResult,
|
|
2559
2665
|
changedFiles,
|
|
@@ -2563,15 +2669,6 @@ async function buildReviewContext(mode, config, depth = config.context.depth, di
|
|
|
2563
2669
|
diagnostics
|
|
2564
2670
|
};
|
|
2565
2671
|
}
|
|
2566
|
-
async function loadDiffForMode(mode) {
|
|
2567
|
-
if (mode === "staged") {
|
|
2568
|
-
return getStagedDiff();
|
|
2569
|
-
}
|
|
2570
|
-
if (mode === "commit") {
|
|
2571
|
-
throw new Error("Commit review context requires an explicit diff.");
|
|
2572
|
-
}
|
|
2573
|
-
return getLastCommitDiff();
|
|
2574
|
-
}
|
|
2575
2672
|
async function buildChangedFileContext(file, changedLines) {
|
|
2576
2673
|
if (file.status === "deleted") {
|
|
2577
2674
|
return {
|
|
@@ -2581,15 +2678,13 @@ async function buildChangedFileContext(file, changedLines) {
|
|
|
2581
2678
|
symbols: [],
|
|
2582
2679
|
changedLines,
|
|
2583
2680
|
astSymbols: [],
|
|
2584
|
-
|
|
2585
|
-
shouldRenderContent: false,
|
|
2586
|
-
skippedReason: "deleted file"
|
|
2681
|
+
content: { status: "skipped", reason: "deleted file" }
|
|
2587
2682
|
},
|
|
2588
2683
|
diagnostics: []
|
|
2589
2684
|
};
|
|
2590
2685
|
}
|
|
2591
2686
|
const contentResult = await readTextFile(file.path, MAX_FILE_CHARS);
|
|
2592
|
-
if (
|
|
2687
|
+
if (contentResult.status === "skipped") {
|
|
2593
2688
|
return {
|
|
2594
2689
|
fileContext: {
|
|
2595
2690
|
file,
|
|
@@ -2597,9 +2692,7 @@ async function buildChangedFileContext(file, changedLines) {
|
|
|
2597
2692
|
symbols: [],
|
|
2598
2693
|
changedLines,
|
|
2599
2694
|
astSymbols: [],
|
|
2600
|
-
|
|
2601
|
-
shouldRenderContent: false,
|
|
2602
|
-
skippedReason: contentResult.reason
|
|
2695
|
+
content: { status: "skipped", reason: contentResult.reason }
|
|
2603
2696
|
},
|
|
2604
2697
|
diagnostics: []
|
|
2605
2698
|
};
|
|
@@ -2615,9 +2708,12 @@ async function buildChangedFileContext(file, changedLines) {
|
|
|
2615
2708
|
),
|
|
2616
2709
|
changedLines,
|
|
2617
2710
|
astSymbols: astResult.symbols,
|
|
2618
|
-
content:
|
|
2619
|
-
|
|
2620
|
-
|
|
2711
|
+
content: {
|
|
2712
|
+
status: "loaded",
|
|
2713
|
+
text: contentResult.content,
|
|
2714
|
+
truncated: contentResult.truncated,
|
|
2715
|
+
render: astResult.symbols.length > 0 ? "ast-symbols" : shouldRenderFullFileContent(file, contentResult.content) ? "full" : "diff-only"
|
|
2716
|
+
}
|
|
2621
2717
|
},
|
|
2622
2718
|
diagnostics: astResult.diagnostics ?? []
|
|
2623
2719
|
};
|
|
@@ -2631,7 +2727,7 @@ async function buildRelatedFileContexts(files) {
|
|
|
2631
2727
|
if (seen.has(candidate) || !existsSync4(candidate)) continue;
|
|
2632
2728
|
seen.add(candidate);
|
|
2633
2729
|
const result = await readTextFile(candidate, MAX_RELATED_FILE_CHARS);
|
|
2634
|
-
if (
|
|
2730
|
+
if (result.status === "skipped") continue;
|
|
2635
2731
|
related.push({
|
|
2636
2732
|
path: candidate,
|
|
2637
2733
|
reason: `Likely test file for ${file.path}`,
|
|
@@ -2654,23 +2750,23 @@ async function readTextFile(path, maxChars) {
|
|
|
2654
2750
|
try {
|
|
2655
2751
|
const info = await stat2(path);
|
|
2656
2752
|
if (!info.isFile()) {
|
|
2657
|
-
return {
|
|
2753
|
+
return { status: "skipped", reason: "not a regular file" };
|
|
2658
2754
|
}
|
|
2659
2755
|
if (info.size > MAX_CONTEXT_FILE_BYTES) {
|
|
2660
2756
|
return {
|
|
2661
|
-
|
|
2757
|
+
status: "skipped",
|
|
2662
2758
|
reason: `file too large for context (${formatBytes2(info.size)} > ${formatBytes2(MAX_CONTEXT_FILE_BYTES)})`
|
|
2663
2759
|
};
|
|
2664
2760
|
}
|
|
2665
2761
|
const raw = await readFile6(path, "utf-8");
|
|
2666
2762
|
if (raw.includes("\0")) {
|
|
2667
|
-
return {
|
|
2763
|
+
return { status: "skipped", reason: "binary file" };
|
|
2668
2764
|
}
|
|
2669
2765
|
const result = truncateText3(raw, maxChars);
|
|
2670
|
-
return { content: result.text, truncated: result.truncated };
|
|
2766
|
+
return { status: "loaded", content: result.text, truncated: result.truncated };
|
|
2671
2767
|
} catch (err) {
|
|
2672
2768
|
return {
|
|
2673
|
-
|
|
2769
|
+
status: "skipped",
|
|
2674
2770
|
reason: err instanceof Error ? err.message : String(err)
|
|
2675
2771
|
};
|
|
2676
2772
|
}
|
|
@@ -3033,7 +3129,7 @@ import { execa as execa5 } from "execa";
|
|
|
3033
3129
|
// package.json
|
|
3034
3130
|
var package_default = {
|
|
3035
3131
|
name: "diffowl",
|
|
3036
|
-
version: "0.
|
|
3132
|
+
version: "0.2.0",
|
|
3037
3133
|
description: "Local AI code review agent powered by OpenCode",
|
|
3038
3134
|
keywords: [
|
|
3039
3135
|
"ai",
|
|
@@ -3052,7 +3148,7 @@ var package_default = {
|
|
|
3052
3148
|
url: "git+https://github.com/gutierrezje/diffowl.git"
|
|
3053
3149
|
},
|
|
3054
3150
|
bin: {
|
|
3055
|
-
diffowl: "
|
|
3151
|
+
diffowl: "dist/cli.js"
|
|
3056
3152
|
},
|
|
3057
3153
|
files: [
|
|
3058
3154
|
"dist"
|
|
@@ -3147,11 +3243,11 @@ program.command("review", { isDefault: true }).description("Review the last comm
|
|
|
3147
3243
|
console.error(chalk2.red("Cannot use --staged and --commit together"));
|
|
3148
3244
|
process.exit(1);
|
|
3149
3245
|
}
|
|
3150
|
-
const
|
|
3246
|
+
const target = options.staged ? { kind: "staged" } : options.commit ? { kind: "commit", ref: String(options.commit) } : { kind: "last-commit" };
|
|
3151
3247
|
const depth = resolveReviewDepth(options.depth, config);
|
|
3152
3248
|
config.reasoning.effort = resolveReasoningEffort(options.reasoning, config);
|
|
3153
3249
|
const verbose = Boolean(config.verbose || options.verbose);
|
|
3154
|
-
if (
|
|
3250
|
+
if (target.kind !== "staged") {
|
|
3155
3251
|
const hasCommitsStart = performance.now();
|
|
3156
3252
|
const commitsExist = await hasCommits();
|
|
3157
3253
|
recordCliTiming(timings, "git-commit-check", "Git commit check", hasCommitsStart);
|
|
@@ -3166,17 +3262,6 @@ program.command("review", { isDefault: true }).description("Review the last comm
|
|
|
3166
3262
|
console.log(chalk2.yellow(`\u26A0 ${formatHookFailure(hookFailure)}`));
|
|
3167
3263
|
console.log();
|
|
3168
3264
|
}
|
|
3169
|
-
const diff = mode === "staged" ? await getStagedDiff() : mode === "commit" ? await getCommitDiff(String(options.commit)) : await getLastCommitDiff();
|
|
3170
|
-
if (config.skip_doc_only && isDocOnlyDiff(diff)) {
|
|
3171
|
-
console.warn(chalk2.yellow("Documentation-only changes detected. Skipping review."));
|
|
3172
|
-
const skipContent = buildDocOnlySkipMarkdown(diff);
|
|
3173
|
-
const reportPath = await writeMarkdownReport(skipContent);
|
|
3174
|
-
console.log(chalk2.dim(`Report saved: ${reportPath}`));
|
|
3175
|
-
if (options.hook) {
|
|
3176
|
-
await writeHookStatus(0, hookCommit);
|
|
3177
|
-
}
|
|
3178
|
-
process.exit(0);
|
|
3179
|
-
}
|
|
3180
3265
|
const spinner = ora({
|
|
3181
3266
|
text: "Building local review context...",
|
|
3182
3267
|
color: "cyan",
|
|
@@ -3199,14 +3284,26 @@ program.command("review", { isDefault: true }).description("Review the last comm
|
|
|
3199
3284
|
process.exit(146);
|
|
3200
3285
|
});
|
|
3201
3286
|
try {
|
|
3202
|
-
const
|
|
3203
|
-
|
|
3204
|
-
recordCliTiming(timings, "context-build", "Local review context build", contextStart);
|
|
3205
|
-
if (mode === "staged" && reviewContext.diff.files.length === 0) {
|
|
3287
|
+
const diff = await loadReviewDiff(target);
|
|
3288
|
+
if (target.kind === "staged" && diff.files.length === 0) {
|
|
3206
3289
|
spinner.stop();
|
|
3207
3290
|
console.log(chalk2.yellow("No staged changes to review"));
|
|
3208
3291
|
process.exit(0);
|
|
3209
3292
|
}
|
|
3293
|
+
if (config.skip_doc_only && isDocOnlyDiff(diff)) {
|
|
3294
|
+
spinner.stop();
|
|
3295
|
+
console.warn(chalk2.yellow("Documentation-only changes detected. Skipping review."));
|
|
3296
|
+
const skipContent = buildDocOnlySkipMarkdown(diff);
|
|
3297
|
+
const reportPath2 = await writeMarkdownReport(skipContent);
|
|
3298
|
+
console.log(chalk2.dim(`Report saved: ${reportPath2}`));
|
|
3299
|
+
if (options.hook) {
|
|
3300
|
+
await writeHookStatus(0, hookCommit);
|
|
3301
|
+
}
|
|
3302
|
+
process.exit(0);
|
|
3303
|
+
}
|
|
3304
|
+
const contextStart = performance.now();
|
|
3305
|
+
const reviewContext = await buildReviewContextFromDiff({ target, diff }, config, depth);
|
|
3306
|
+
recordCliTiming(timings, "context-build", "Local review context build", contextStart);
|
|
3210
3307
|
const contextRenderStart = performance.now();
|
|
3211
3308
|
const localContext = renderReviewContext(reviewContext, { depth });
|
|
3212
3309
|
recordCliTiming(timings, "context-render", "Local review context render", contextRenderStart);
|
|
@@ -3225,7 +3322,7 @@ program.command("review", { isDefault: true }).description("Review the last comm
|
|
|
3225
3322
|
spinner.text = "Reviewing changes...";
|
|
3226
3323
|
const reviewStart = performance.now();
|
|
3227
3324
|
const reviewResult = await runReview({
|
|
3228
|
-
|
|
3325
|
+
target,
|
|
3229
3326
|
config,
|
|
3230
3327
|
localContext,
|
|
3231
3328
|
depth,
|