@h-rig/runtime 0.0.6-alpha.3 → 0.0.6-alpha.30
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/bin/rig-agent-dispatch.js +1165 -785
- package/dist/bin/rig-agent.js +458 -389
- package/dist/src/control-plane/agent-wrapper.js +1191 -504
- package/dist/src/control-plane/authority-files.js +12 -6
- package/dist/src/control-plane/harness-main.js +2186 -1786
- package/dist/src/control-plane/hooks/completion-verification.js +2084 -1019
- package/dist/src/control-plane/hooks/inject-context.js +193 -139
- package/dist/src/control-plane/hooks/submodule-branch.js +603 -545
- package/dist/src/control-plane/hooks/task-runtime-start.js +603 -545
- package/dist/src/control-plane/materialize-task-config.js +64 -8
- package/dist/src/control-plane/native/git-ops.js +90 -64
- package/dist/src/control-plane/native/harness-cli.js +1989 -682
- package/dist/src/control-plane/native/pr-automation.js +1657 -54
- package/dist/src/control-plane/native/pr-review-gate.js +1455 -0
- package/dist/src/control-plane/native/repo-ops.js +3 -0
- package/dist/src/control-plane/native/run-ops.js +39 -13
- package/dist/src/control-plane/native/task-ops.js +1819 -527
- package/dist/src/control-plane/native/validator.js +163 -109
- package/dist/src/control-plane/native/verifier.js +1616 -323
- package/dist/src/control-plane/native/workspace-ops.js +12 -6
- package/dist/src/control-plane/pi-sessiond/bin.js +793 -0
- package/dist/src/control-plane/pi-sessiond/client.js +41 -0
- package/dist/src/control-plane/pi-sessiond/event-hub.js +59 -0
- package/dist/src/control-plane/pi-sessiond/extension-ui-context.js +198 -0
- package/dist/src/control-plane/pi-sessiond/launcher.js +173 -0
- package/dist/src/control-plane/pi-sessiond/server.js +802 -0
- package/dist/src/control-plane/pi-sessiond/session-service.js +540 -0
- package/dist/src/control-plane/pi-sessiond/types.js +1 -0
- package/dist/src/control-plane/plugin-host-context.js +54 -0
- package/dist/src/control-plane/runtime/image/fingerprint-sidecar.js +3 -0
- package/dist/src/control-plane/runtime/image/index.js +3 -0
- package/dist/src/control-plane/runtime/image-fingerprint-sidecar.js +3 -0
- package/dist/src/control-plane/runtime/image.js +3 -0
- package/dist/src/control-plane/runtime/index.js +517 -722
- package/dist/src/control-plane/runtime/isolation/home.js +28 -6
- package/dist/src/control-plane/runtime/isolation/index.js +541 -461
- package/dist/src/control-plane/runtime/isolation/runner.js +28 -6
- package/dist/src/control-plane/runtime/isolation/shared.js +9 -6
- package/dist/src/control-plane/runtime/isolation.js +541 -461
- package/dist/src/control-plane/runtime/plugin-mode.js +3 -27
- package/dist/src/control-plane/runtime/queue.js +458 -385
- package/dist/src/control-plane/runtime/snapshot/task-run.js +3 -0
- package/dist/src/control-plane/runtime/task-run-snapshot.js +3 -0
- package/dist/src/control-plane/skill-materializer.js +46 -0
- package/dist/src/control-plane/tasks/source-aware-task-config-source.js +14 -2
- package/dist/src/control-plane/tasks/source-lifecycle.js +86 -32
- package/dist/src/index.js +27 -298
- package/dist/src/layout.js +12 -7
- package/dist/src/local-server.js +20 -14
- package/native/darwin-arm64/rig-git +0 -0
- package/native/darwin-arm64/rig-git.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-shell +0 -0
- package/native/darwin-arm64/rig-shell.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-tools +0 -0
- package/native/darwin-arm64/rig-tools.build-manifest.json +1 -1
- package/native/darwin-arm64/runtime-native.dylib +0 -0
- package/package.json +8 -6
- package/dist/src/control-plane/runtime/plugins.js +0 -1131
- package/dist/src/plugins.js +0 -329
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3
|
-
import { existsSync as
|
|
4
|
-
import { resolve as
|
|
3
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
4
|
+
import { resolve as resolve14 } from "path";
|
|
5
5
|
|
|
6
6
|
// packages/runtime/src/control-plane/runtime/baked-secrets.ts
|
|
7
7
|
var BAKED_RUNTIME_SECRETS = {
|
|
@@ -47,8 +47,8 @@ function resolveRuntimeSecrets(env, baked = BAKED_RUNTIME_SECRETS) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
50
|
-
import { existsSync as
|
|
51
|
-
import { dirname as dirname8, isAbsolute as isAbsolute2, resolve as
|
|
50
|
+
import { existsSync as existsSync11, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
51
|
+
import { dirname as dirname8, isAbsolute as isAbsolute2, resolve as resolve13 } from "path";
|
|
52
52
|
|
|
53
53
|
// packages/runtime/src/control-plane/runtime/context.ts
|
|
54
54
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -539,6 +539,49 @@ function safeReadJson(path) {
|
|
|
539
539
|
}
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
+
// packages/runtime/src/control-plane/skill-materializer.ts
|
|
543
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
544
|
+
import { resolve as resolve5 } from "path";
|
|
545
|
+
import { loadSkill } from "@rig/skill-loader";
|
|
546
|
+
var MARKER_FILENAME = ".rig-plugin";
|
|
547
|
+
function skillDirName(id) {
|
|
548
|
+
return id.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
549
|
+
}
|
|
550
|
+
async function materializeSkills(projectRoot, entries) {
|
|
551
|
+
const skillsRoot = resolve5(projectRoot, ".pi", "skills");
|
|
552
|
+
if (existsSync4(skillsRoot)) {
|
|
553
|
+
for (const name of readdirSync(skillsRoot)) {
|
|
554
|
+
const dir = resolve5(skillsRoot, name);
|
|
555
|
+
if (existsSync4(resolve5(dir, MARKER_FILENAME))) {
|
|
556
|
+
rmSync(dir, { recursive: true, force: true });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const written = [];
|
|
561
|
+
for (const { pluginName, skill } of entries) {
|
|
562
|
+
const sourcePath = resolve5(projectRoot, skill.path);
|
|
563
|
+
if (!existsSync4(sourcePath)) {
|
|
564
|
+
console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${sourcePath} does not exist`);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
let body;
|
|
568
|
+
try {
|
|
569
|
+
await loadSkill(sourcePath);
|
|
570
|
+
body = readFileSync3(sourcePath, "utf-8");
|
|
571
|
+
} catch (err) {
|
|
572
|
+
console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${err instanceof Error ? err.message : err}`);
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
const dir = resolve5(skillsRoot, skillDirName(skill.id));
|
|
576
|
+
mkdirSync3(dir, { recursive: true });
|
|
577
|
+
writeFileSync3(resolve5(dir, "SKILL.md"), body, "utf-8");
|
|
578
|
+
writeFileSync3(resolve5(dir, MARKER_FILENAME), `${JSON.stringify({ plugin: pluginName, skillId: skill.id }, null, 2)}
|
|
579
|
+
`, "utf-8");
|
|
580
|
+
written.push({ id: skill.id, pluginName, directory: dir });
|
|
581
|
+
}
|
|
582
|
+
return written;
|
|
583
|
+
}
|
|
584
|
+
|
|
542
585
|
// packages/runtime/src/control-plane/plugin-host-context.ts
|
|
543
586
|
async function buildPluginHostContext(projectRoot) {
|
|
544
587
|
let config;
|
|
@@ -575,6 +618,17 @@ async function buildPluginHostContext(projectRoot) {
|
|
|
575
618
|
} catch (err) {
|
|
576
619
|
console.warn(`[plugin-host] hook materialization failed: ${err instanceof Error ? err.message : err}`);
|
|
577
620
|
}
|
|
621
|
+
try {
|
|
622
|
+
const skillEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.skills ?? []).map((skill) => ({
|
|
623
|
+
pluginName: plugin.name,
|
|
624
|
+
skill
|
|
625
|
+
})));
|
|
626
|
+
if (skillEntries.length > 0) {
|
|
627
|
+
await materializeSkills(projectRoot, skillEntries);
|
|
628
|
+
}
|
|
629
|
+
} catch (err) {
|
|
630
|
+
console.warn(`[plugin-host] skill materialization failed: ${err instanceof Error ? err.message : err}`);
|
|
631
|
+
}
|
|
578
632
|
return {
|
|
579
633
|
config,
|
|
580
634
|
pluginHost,
|
|
@@ -588,12 +642,12 @@ async function buildPluginHostContext(projectRoot) {
|
|
|
588
642
|
|
|
589
643
|
// packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
|
|
590
644
|
import { spawnSync } from "child_process";
|
|
591
|
-
import { existsSync as
|
|
592
|
-
import { basename as basename3, join as join2, resolve as
|
|
645
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync4 } from "fs";
|
|
646
|
+
import { basename as basename3, join as join2, resolve as resolve7 } from "path";
|
|
593
647
|
|
|
594
648
|
// packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
|
|
595
|
-
import { existsSync as
|
|
596
|
-
import { resolve as
|
|
649
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
650
|
+
import { resolve as resolve6 } from "path";
|
|
597
651
|
|
|
598
652
|
// packages/runtime/src/control-plane/tasks/task-record-reader.ts
|
|
599
653
|
async function findTaskById(reader, id) {
|
|
@@ -616,7 +670,7 @@ class LegacyTaskConfigReadError extends Error {
|
|
|
616
670
|
}
|
|
617
671
|
}
|
|
618
672
|
function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
|
|
619
|
-
const configPath = options.configPath ??
|
|
673
|
+
const configPath = options.configPath ?? resolve6(projectRoot, ".rig", "task-config.json");
|
|
620
674
|
const reader = {
|
|
621
675
|
async listTasks() {
|
|
622
676
|
return readLegacyTaskRecords(projectRoot, configPath);
|
|
@@ -627,8 +681,8 @@ function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
|
|
|
627
681
|
};
|
|
628
682
|
return reader;
|
|
629
683
|
}
|
|
630
|
-
function readLegacyTaskRecords(projectRoot, configPath =
|
|
631
|
-
if (!
|
|
684
|
+
function readLegacyTaskRecords(projectRoot, configPath = resolve6(projectRoot, ".rig", "task-config.json")) {
|
|
685
|
+
if (!existsSync5(configPath)) {
|
|
632
686
|
return [];
|
|
633
687
|
}
|
|
634
688
|
const rawConfig = readLegacyTaskConfigJson(projectRoot, configPath);
|
|
@@ -636,7 +690,7 @@ function readLegacyTaskRecords(projectRoot, configPath = resolve5(projectRoot, "
|
|
|
636
690
|
}
|
|
637
691
|
function readLegacyTaskConfigJson(projectRoot, configPath) {
|
|
638
692
|
try {
|
|
639
|
-
const parsed = JSON.parse(
|
|
693
|
+
const parsed = JSON.parse(readFileSync4(configPath, "utf8"));
|
|
640
694
|
if (isPlainRecord(parsed)) {
|
|
641
695
|
return parsed;
|
|
642
696
|
}
|
|
@@ -720,7 +774,7 @@ function isPlainRecord(candidate) {
|
|
|
720
774
|
var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
|
|
721
775
|
var FILE_TASK_PATTERN = /\.(task\.)?json$/;
|
|
722
776
|
function createSourceAwareTaskConfigRecordReader(projectRoot, options = {}) {
|
|
723
|
-
const configPath = options.configPath ??
|
|
777
|
+
const configPath = options.configPath ?? resolve7(projectRoot, ".rig", "task-config.json");
|
|
724
778
|
const legacy = createLegacyTaskConfigRecordReader(projectRoot, { configPath });
|
|
725
779
|
const spawnFn = options.spawn ?? spawnSync;
|
|
726
780
|
const ghBinary = options.ghBinary ?? "gh";
|
|
@@ -803,10 +857,10 @@ function readMaterializedTaskMetadata(entry) {
|
|
|
803
857
|
return metadata;
|
|
804
858
|
}
|
|
805
859
|
function readConfiguredFilesTaskSourcePath(projectRoot) {
|
|
806
|
-
const jsonPath =
|
|
807
|
-
if (
|
|
860
|
+
const jsonPath = resolve7(projectRoot, "rig.config.json");
|
|
861
|
+
if (existsSync6(jsonPath)) {
|
|
808
862
|
try {
|
|
809
|
-
const parsed = JSON.parse(
|
|
863
|
+
const parsed = JSON.parse(readFileSync5(jsonPath, "utf8"));
|
|
810
864
|
if (isPlainRecord2(parsed) && isPlainRecord2(parsed.taskSource)) {
|
|
811
865
|
const source = parsed.taskSource;
|
|
812
866
|
return source.kind === "files" && typeof source.path === "string" ? source.path : null;
|
|
@@ -815,12 +869,12 @@ function readConfiguredFilesTaskSourcePath(projectRoot) {
|
|
|
815
869
|
return null;
|
|
816
870
|
}
|
|
817
871
|
}
|
|
818
|
-
const tsPath =
|
|
819
|
-
if (!
|
|
872
|
+
const tsPath = resolve7(projectRoot, "rig.config.ts");
|
|
873
|
+
if (!existsSync6(tsPath)) {
|
|
820
874
|
return null;
|
|
821
875
|
}
|
|
822
876
|
try {
|
|
823
|
-
const source =
|
|
877
|
+
const source = readFileSync5(tsPath, "utf8");
|
|
824
878
|
const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
|
|
825
879
|
const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
|
|
826
880
|
if (kind !== "files") {
|
|
@@ -840,10 +894,10 @@ function readRawTaskEntry(configPath, taskId) {
|
|
|
840
894
|
return isPlainRecord2(entry) ? entry : null;
|
|
841
895
|
}
|
|
842
896
|
function readRawTaskConfig(configPath) {
|
|
843
|
-
if (!
|
|
897
|
+
if (!existsSync6(configPath)) {
|
|
844
898
|
return null;
|
|
845
899
|
}
|
|
846
|
-
const parsed = JSON.parse(
|
|
900
|
+
const parsed = JSON.parse(readFileSync5(configPath, "utf8"));
|
|
847
901
|
return isPlainRecord2(parsed) ? parsed : null;
|
|
848
902
|
}
|
|
849
903
|
function stripLegacyTaskConfigMetadata2(raw) {
|
|
@@ -851,12 +905,12 @@ function stripLegacyTaskConfigMetadata2(raw) {
|
|
|
851
905
|
return tasks;
|
|
852
906
|
}
|
|
853
907
|
function listFileBackedTasks(projectRoot, sourcePath) {
|
|
854
|
-
const directory =
|
|
855
|
-
if (!
|
|
908
|
+
const directory = resolve7(projectRoot, sourcePath);
|
|
909
|
+
if (!existsSync6(directory)) {
|
|
856
910
|
return [];
|
|
857
911
|
}
|
|
858
912
|
const tasks = [];
|
|
859
|
-
for (const name of
|
|
913
|
+
for (const name of readdirSync2(directory)) {
|
|
860
914
|
if (!FILE_TASK_PATTERN.test(name))
|
|
861
915
|
continue;
|
|
862
916
|
const inferredId = basename3(name).replace(FILE_TASK_PATTERN, "");
|
|
@@ -867,11 +921,11 @@ function listFileBackedTasks(projectRoot, sourcePath) {
|
|
|
867
921
|
return tasks;
|
|
868
922
|
}
|
|
869
923
|
function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
|
|
870
|
-
const file = findFileBackedTaskFile(
|
|
924
|
+
const file = findFileBackedTaskFile(resolve7(projectRoot, sourcePath), taskId);
|
|
871
925
|
if (!file) {
|
|
872
926
|
return null;
|
|
873
927
|
}
|
|
874
|
-
const raw = JSON.parse(
|
|
928
|
+
const raw = JSON.parse(readFileSync5(file, "utf8"));
|
|
875
929
|
if (!isPlainRecord2(raw)) {
|
|
876
930
|
return null;
|
|
877
931
|
}
|
|
@@ -884,17 +938,17 @@ function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
|
|
|
884
938
|
};
|
|
885
939
|
}
|
|
886
940
|
function findFileBackedTaskFile(directory, taskId) {
|
|
887
|
-
if (!
|
|
941
|
+
if (!existsSync6(directory)) {
|
|
888
942
|
return null;
|
|
889
943
|
}
|
|
890
|
-
for (const name of
|
|
944
|
+
for (const name of readdirSync2(directory)) {
|
|
891
945
|
if (!FILE_TASK_PATTERN.test(name))
|
|
892
946
|
continue;
|
|
893
947
|
const file = join2(directory, name);
|
|
894
948
|
try {
|
|
895
949
|
if (!statSync(file).isFile())
|
|
896
950
|
continue;
|
|
897
|
-
const raw = JSON.parse(
|
|
951
|
+
const raw = JSON.parse(readFileSync5(file, "utf8"));
|
|
898
952
|
const inferredId = basename3(file).replace(FILE_TASK_PATTERN, "");
|
|
899
953
|
const id = isPlainRecord2(raw) && typeof raw.id === "string" ? raw.id : inferredId;
|
|
900
954
|
if (id === taskId) {
|
|
@@ -977,8 +1031,8 @@ function githubStatusFor(issue) {
|
|
|
977
1031
|
return "open";
|
|
978
1032
|
}
|
|
979
1033
|
function selectedGitHubEnv() {
|
|
980
|
-
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim()
|
|
981
|
-
return { GH_TOKEN: token, GITHUB_TOKEN: token };
|
|
1034
|
+
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
|
|
1035
|
+
return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
|
|
982
1036
|
}
|
|
983
1037
|
function ghSpawnOptions() {
|
|
984
1038
|
return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
|
|
@@ -1054,8 +1108,8 @@ async function readConfiguredTaskSourceTask(projectRoot, taskId) {
|
|
|
1054
1108
|
}
|
|
1055
1109
|
|
|
1056
1110
|
// packages/runtime/src/control-plane/native/task-state.ts
|
|
1057
|
-
import { existsSync as
|
|
1058
|
-
import { basename as basename5, resolve as
|
|
1111
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
1112
|
+
import { basename as basename5, resolve as resolve12 } from "path";
|
|
1059
1113
|
|
|
1060
1114
|
// packages/runtime/src/control-plane/state-sync/types.ts
|
|
1061
1115
|
var CANONICAL_TASK_LIFECYCLE_STATUSES = new Set([
|
|
@@ -1071,40 +1125,40 @@ var CANONICAL_TASK_LIFECYCLE_STATUSES = new Set([
|
|
|
1071
1125
|
]);
|
|
1072
1126
|
// packages/runtime/src/control-plane/native/git-native.ts
|
|
1073
1127
|
import { tmpdir as tmpdir3 } from "os";
|
|
1074
|
-
import { dirname as dirname5, isAbsolute, resolve as
|
|
1075
|
-
var sharedGitNativeOutputDir =
|
|
1076
|
-
var sharedGitNativeOutputPath =
|
|
1128
|
+
import { dirname as dirname5, isAbsolute, resolve as resolve8 } from "path";
|
|
1129
|
+
var sharedGitNativeOutputDir = resolve8(tmpdir3(), "rig-native");
|
|
1130
|
+
var sharedGitNativeOutputPath = resolve8(sharedGitNativeOutputDir, `rig-git-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
|
|
1077
1131
|
|
|
1078
1132
|
// packages/runtime/src/control-plane/native/utils.ts
|
|
1079
|
-
import { existsSync as
|
|
1080
|
-
import { resolve as
|
|
1133
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
|
|
1134
|
+
import { resolve as resolve11 } from "path";
|
|
1081
1135
|
|
|
1082
1136
|
// packages/runtime/src/layout.ts
|
|
1083
|
-
import { existsSync as
|
|
1084
|
-
import { basename as basename4, dirname as dirname6, resolve as
|
|
1137
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1138
|
+
import { basename as basename4, dirname as dirname6, resolve as resolve9 } from "path";
|
|
1085
1139
|
var RIG_DEFINITION_DIRNAME = "rig";
|
|
1086
1140
|
var RIG_ARTIFACTS_DIRNAME = "artifacts";
|
|
1087
1141
|
function resolveMonorepoRoot(projectRoot) {
|
|
1088
|
-
const normalizedProjectRoot =
|
|
1142
|
+
const normalizedProjectRoot = resolve9(projectRoot);
|
|
1089
1143
|
const explicit = process.env.MONOREPO_ROOT?.trim();
|
|
1090
1144
|
if (explicit) {
|
|
1091
|
-
const explicitRoot =
|
|
1145
|
+
const explicitRoot = resolve9(explicit);
|
|
1092
1146
|
const explicitParent = dirname6(explicitRoot);
|
|
1093
1147
|
if (basename4(explicitParent) === ".worktrees") {
|
|
1094
1148
|
const owner = dirname6(explicitParent);
|
|
1095
|
-
const ownerHasGit =
|
|
1096
|
-
const ownerHasTaskConfig =
|
|
1097
|
-
const ownerHasRigConfig =
|
|
1149
|
+
const ownerHasGit = existsSync7(resolve9(owner, ".git"));
|
|
1150
|
+
const ownerHasTaskConfig = existsSync7(resolve9(owner, ".rig", "task-config.json"));
|
|
1151
|
+
const ownerHasRigConfig = existsSync7(resolve9(owner, "rig.config.ts"));
|
|
1098
1152
|
if (ownerHasGit && (ownerHasTaskConfig || ownerHasRigConfig)) {
|
|
1099
1153
|
return owner;
|
|
1100
1154
|
}
|
|
1101
1155
|
throw new Error(`MONOREPO_ROOT points to worktree ${explicitRoot}, but the owner checkout is incomplete at ${owner}.`);
|
|
1102
1156
|
}
|
|
1103
|
-
if (!
|
|
1157
|
+
if (!existsSync7(resolve9(explicitRoot, ".git"))) {
|
|
1104
1158
|
throw new Error(`MONOREPO_ROOT points to ${explicitRoot}, but no git checkout was found there.`);
|
|
1105
1159
|
}
|
|
1106
|
-
const hasTaskConfig =
|
|
1107
|
-
const hasRigConfig =
|
|
1160
|
+
const hasTaskConfig = existsSync7(resolve9(explicitRoot, ".rig", "task-config.json"));
|
|
1161
|
+
const hasRigConfig = existsSync7(resolve9(explicitRoot, "rig.config.ts"));
|
|
1108
1162
|
if (!hasTaskConfig && !hasRigConfig) {
|
|
1109
1163
|
throw new Error(`MONOREPO_ROOT points to ${explicitRoot}, but neither .rig/task-config.json nor rig.config.ts exists there.`);
|
|
1110
1164
|
}
|
|
@@ -1113,9 +1167,9 @@ function resolveMonorepoRoot(projectRoot) {
|
|
|
1113
1167
|
const projectParent = dirname6(normalizedProjectRoot);
|
|
1114
1168
|
if (basename4(projectParent) === ".worktrees") {
|
|
1115
1169
|
const worktreeOwner = dirname6(projectParent);
|
|
1116
|
-
const ownerHasGit =
|
|
1117
|
-
const ownerHasTaskConfig =
|
|
1118
|
-
const ownerHasRigConfig =
|
|
1170
|
+
const ownerHasGit = existsSync7(resolve9(worktreeOwner, ".git"));
|
|
1171
|
+
const ownerHasTaskConfig = existsSync7(resolve9(worktreeOwner, ".rig", "task-config.json"));
|
|
1172
|
+
const ownerHasRigConfig = existsSync7(resolve9(worktreeOwner, "rig.config.ts"));
|
|
1119
1173
|
if (ownerHasGit && (ownerHasTaskConfig || ownerHasRigConfig)) {
|
|
1120
1174
|
return worktreeOwner;
|
|
1121
1175
|
}
|
|
@@ -1123,28 +1177,28 @@ function resolveMonorepoRoot(projectRoot) {
|
|
|
1123
1177
|
return normalizedProjectRoot;
|
|
1124
1178
|
}
|
|
1125
1179
|
function resolveRuntimeWorkspaceLayout(workspaceDir) {
|
|
1126
|
-
const root =
|
|
1127
|
-
const rigRoot =
|
|
1128
|
-
const logsDir =
|
|
1129
|
-
const stateDir =
|
|
1130
|
-
const runtimeDir =
|
|
1131
|
-
const binDir =
|
|
1180
|
+
const root = resolve9(workspaceDir);
|
|
1181
|
+
const rigRoot = resolve9(root, ".rig");
|
|
1182
|
+
const logsDir = resolve9(rigRoot, "logs");
|
|
1183
|
+
const stateDir = resolve9(rigRoot, "state");
|
|
1184
|
+
const runtimeDir = resolve9(rigRoot, "runtime");
|
|
1185
|
+
const binDir = resolve9(rigRoot, "bin");
|
|
1132
1186
|
return {
|
|
1133
1187
|
workspaceDir: root,
|
|
1134
1188
|
rigRoot,
|
|
1135
1189
|
stateDir,
|
|
1136
1190
|
logsDir,
|
|
1137
|
-
artifactsRoot:
|
|
1191
|
+
artifactsRoot: resolve9(root, RIG_ARTIFACTS_DIRNAME),
|
|
1138
1192
|
runtimeDir,
|
|
1139
|
-
homeDir:
|
|
1140
|
-
tmpDir:
|
|
1141
|
-
cacheDir:
|
|
1142
|
-
sessionDir:
|
|
1193
|
+
homeDir: resolve9(rigRoot, "home"),
|
|
1194
|
+
tmpDir: resolve9(rigRoot, "tmp"),
|
|
1195
|
+
cacheDir: resolve9(rigRoot, "cache"),
|
|
1196
|
+
sessionDir: resolve9(rigRoot, "session"),
|
|
1143
1197
|
binDir,
|
|
1144
|
-
distDir:
|
|
1145
|
-
pluginBinDir:
|
|
1146
|
-
contextPath:
|
|
1147
|
-
controlPlaneEventsFile:
|
|
1198
|
+
distDir: resolve9(rigRoot, "dist"),
|
|
1199
|
+
pluginBinDir: resolve9(binDir, "plugins"),
|
|
1200
|
+
contextPath: resolve9(rigRoot, "runtime-context.json"),
|
|
1201
|
+
controlPlaneEventsFile: resolve9(logsDir, "control-plane.events.jsonl")
|
|
1148
1202
|
};
|
|
1149
1203
|
}
|
|
1150
1204
|
function resolveActiveRuntimeWorkspaceRoot(monorepoRoot) {
|
|
@@ -1152,14 +1206,14 @@ function resolveActiveRuntimeWorkspaceRoot(monorepoRoot) {
|
|
|
1152
1206
|
if (!explicit) {
|
|
1153
1207
|
throw new Error("No active runtime workspace. Set RIG_TASK_WORKSPACE or provision a task runtime first.");
|
|
1154
1208
|
}
|
|
1155
|
-
return
|
|
1209
|
+
return resolve9(explicit);
|
|
1156
1210
|
}
|
|
1157
1211
|
function resolveRigLayout(projectRoot) {
|
|
1158
1212
|
const monorepoRoot = resolveMonorepoRoot(projectRoot);
|
|
1159
|
-
const definitionRoot =
|
|
1213
|
+
const definitionRoot = resolve9(projectRoot, RIG_DEFINITION_DIRNAME);
|
|
1160
1214
|
const runtimeWorkspaceRoot = resolveActiveRuntimeWorkspaceRoot(monorepoRoot);
|
|
1161
1215
|
const runtimeLayout = resolveRuntimeWorkspaceLayout(runtimeWorkspaceRoot);
|
|
1162
|
-
const policyDir =
|
|
1216
|
+
const policyDir = resolve9(definitionRoot, "policy");
|
|
1163
1217
|
return {
|
|
1164
1218
|
projectRoot,
|
|
1165
1219
|
monorepoRoot,
|
|
@@ -1167,48 +1221,48 @@ function resolveRigLayout(projectRoot) {
|
|
|
1167
1221
|
runtimeWorkspaceRoot,
|
|
1168
1222
|
stateRoot: runtimeLayout.rigRoot,
|
|
1169
1223
|
artifactsRoot: runtimeLayout.artifactsRoot,
|
|
1170
|
-
configPath:
|
|
1171
|
-
taskConfigPath:
|
|
1224
|
+
configPath: resolve9(definitionRoot, "config.sh"),
|
|
1225
|
+
taskConfigPath: resolve9(runtimeWorkspaceRoot, ".rig", "task-config.json"),
|
|
1172
1226
|
policyDir,
|
|
1173
|
-
policyFile:
|
|
1174
|
-
pluginsDir:
|
|
1175
|
-
hooksDir:
|
|
1176
|
-
toolsDir:
|
|
1177
|
-
templatesDir:
|
|
1178
|
-
validationDir:
|
|
1227
|
+
policyFile: resolve9(policyDir, "policy.json"),
|
|
1228
|
+
pluginsDir: resolve9(definitionRoot, "plugins"),
|
|
1229
|
+
hooksDir: resolve9(definitionRoot, "hooks"),
|
|
1230
|
+
toolsDir: resolve9(definitionRoot, "tools"),
|
|
1231
|
+
templatesDir: resolve9(definitionRoot, "templates"),
|
|
1232
|
+
validationDir: resolve9(definitionRoot, "validation"),
|
|
1179
1233
|
stateDir: runtimeLayout.stateDir,
|
|
1180
1234
|
logsDir: runtimeLayout.logsDir,
|
|
1181
|
-
notificationsDir:
|
|
1235
|
+
notificationsDir: resolve9(definitionRoot, "notifications"),
|
|
1182
1236
|
runtimeDir: runtimeLayout.runtimeDir,
|
|
1183
1237
|
distDir: runtimeLayout.distDir,
|
|
1184
1238
|
binDir: runtimeLayout.binDir,
|
|
1185
1239
|
pluginBinDir: runtimeLayout.pluginBinDir,
|
|
1186
|
-
keybindingsPath:
|
|
1240
|
+
keybindingsPath: resolve9(definitionRoot, "keybindings.json"),
|
|
1187
1241
|
controlPlaneEventsFile: runtimeLayout.controlPlaneEventsFile
|
|
1188
1242
|
};
|
|
1189
1243
|
}
|
|
1190
1244
|
|
|
1191
1245
|
// packages/runtime/src/control-plane/native/runtime-native.ts
|
|
1192
1246
|
import { dlopen, ptr, suffix, toBuffer } from "bun:ffi";
|
|
1193
|
-
import { copyFileSync, existsSync as
|
|
1247
|
+
import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync4, renameSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
1194
1248
|
import { tmpdir as tmpdir4 } from "os";
|
|
1195
|
-
import { dirname as dirname7, resolve as
|
|
1196
|
-
var sharedNativeRuntimeOutputDir =
|
|
1197
|
-
var sharedNativeRuntimeOutputPath =
|
|
1249
|
+
import { dirname as dirname7, resolve as resolve10 } from "path";
|
|
1250
|
+
var sharedNativeRuntimeOutputDir = resolve10(tmpdir4(), "rig-native");
|
|
1251
|
+
var sharedNativeRuntimeOutputPath = resolve10(sharedNativeRuntimeOutputDir, `runtime-native-${process.platform}-${process.arch}.${suffix}`);
|
|
1198
1252
|
var colocatedNativeRuntimeFileName = `runtime-native.${suffix}`;
|
|
1199
1253
|
var nativeRuntimeLibrary = await loadNativeRuntimeLibrary();
|
|
1200
1254
|
async function ensureNativeRuntimeLibraryPath(outputPath = sharedNativeRuntimeOutputPath, options = {}) {
|
|
1201
1255
|
if (await buildNativeRuntimeLibrary(outputPath, options)) {
|
|
1202
1256
|
return outputPath;
|
|
1203
1257
|
}
|
|
1204
|
-
return !options.force &&
|
|
1258
|
+
return !options.force && existsSync8(outputPath) ? outputPath : null;
|
|
1205
1259
|
}
|
|
1206
1260
|
async function loadNativeRuntimeLibrary() {
|
|
1207
1261
|
if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
|
|
1208
1262
|
return null;
|
|
1209
1263
|
}
|
|
1210
1264
|
for (const candidate of nativeRuntimeLibraryCandidates()) {
|
|
1211
|
-
if (!candidate || !
|
|
1265
|
+
if (!candidate || !existsSync8(candidate)) {
|
|
1212
1266
|
continue;
|
|
1213
1267
|
}
|
|
1214
1268
|
const loaded = tryDlopenNativeRuntimeLibrary(candidate);
|
|
@@ -1224,10 +1278,10 @@ async function loadNativeRuntimeLibrary() {
|
|
|
1224
1278
|
}
|
|
1225
1279
|
function nativePackageLibraryCandidates(fromDir, names) {
|
|
1226
1280
|
const candidates = [];
|
|
1227
|
-
let cursor =
|
|
1281
|
+
let cursor = resolve10(fromDir);
|
|
1228
1282
|
for (let index = 0;index < 8; index += 1) {
|
|
1229
1283
|
for (const name of names) {
|
|
1230
|
-
candidates.push(
|
|
1284
|
+
candidates.push(resolve10(cursor, "native", `${process.platform}-${process.arch}`, name), resolve10(cursor, "native", `${process.platform}-${process.arch}`, "lib", name), resolve10(cursor, "native", name), resolve10(cursor, "native", "lib", name));
|
|
1231
1285
|
}
|
|
1232
1286
|
const parent = dirname7(cursor);
|
|
1233
1287
|
if (parent === cursor)
|
|
@@ -1243,22 +1297,22 @@ function nativeRuntimeLibraryCandidates() {
|
|
|
1243
1297
|
return [...new Set([
|
|
1244
1298
|
explicit,
|
|
1245
1299
|
...nativePackageLibraryCandidates(import.meta.dir, [colocatedNativeRuntimeFileName, platformSpecific]),
|
|
1246
|
-
execDir ?
|
|
1247
|
-
execDir ?
|
|
1248
|
-
execDir ?
|
|
1249
|
-
execDir ?
|
|
1250
|
-
execDir ?
|
|
1251
|
-
execDir ?
|
|
1300
|
+
execDir ? resolve10(execDir, colocatedNativeRuntimeFileName) : "",
|
|
1301
|
+
execDir ? resolve10(execDir, platformSpecific) : "",
|
|
1302
|
+
execDir ? resolve10(execDir, "..", colocatedNativeRuntimeFileName) : "",
|
|
1303
|
+
execDir ? resolve10(execDir, "..", platformSpecific) : "",
|
|
1304
|
+
execDir ? resolve10(execDir, "lib", colocatedNativeRuntimeFileName) : "",
|
|
1305
|
+
execDir ? resolve10(execDir, "..", "lib", colocatedNativeRuntimeFileName) : "",
|
|
1252
1306
|
sharedNativeRuntimeOutputPath
|
|
1253
1307
|
].filter(Boolean))];
|
|
1254
1308
|
}
|
|
1255
1309
|
function resolveNativeRuntimeSourcePath() {
|
|
1256
1310
|
const explicit = process.env.RIG_NATIVE_RUNTIME_SOURCE?.trim();
|
|
1257
|
-
if (explicit &&
|
|
1311
|
+
if (explicit && existsSync8(explicit)) {
|
|
1258
1312
|
return explicit;
|
|
1259
1313
|
}
|
|
1260
|
-
const bundled =
|
|
1261
|
-
return
|
|
1314
|
+
const bundled = resolve10(import.meta.dir, "../../../native/snapshot.zig");
|
|
1315
|
+
return existsSync8(bundled) ? bundled : null;
|
|
1262
1316
|
}
|
|
1263
1317
|
async function buildNativeRuntimeLibrary(outputPath, options = {}) {
|
|
1264
1318
|
if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
|
|
@@ -1271,8 +1325,8 @@ async function buildNativeRuntimeLibrary(outputPath, options = {}) {
|
|
|
1271
1325
|
}
|
|
1272
1326
|
const tempOutputPath = `${outputPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
1273
1327
|
try {
|
|
1274
|
-
|
|
1275
|
-
const needsBuild = options.force === true || !
|
|
1328
|
+
mkdirSync4(dirname7(outputPath), { recursive: true });
|
|
1329
|
+
const needsBuild = options.force === true || !existsSync8(outputPath) || statSync2(sourcePath).mtimeMs > statSync2(outputPath).mtimeMs;
|
|
1276
1330
|
if (!needsBuild) {
|
|
1277
1331
|
return true;
|
|
1278
1332
|
}
|
|
@@ -1290,14 +1344,14 @@ async function buildNativeRuntimeLibrary(outputPath, options = {}) {
|
|
|
1290
1344
|
stderr: "pipe"
|
|
1291
1345
|
});
|
|
1292
1346
|
const exitCode = await build.exited;
|
|
1293
|
-
if (exitCode !== 0 || !
|
|
1294
|
-
|
|
1347
|
+
if (exitCode !== 0 || !existsSync8(tempOutputPath)) {
|
|
1348
|
+
rmSync2(tempOutputPath, { force: true });
|
|
1295
1349
|
return false;
|
|
1296
1350
|
}
|
|
1297
1351
|
renameSync(tempOutputPath, outputPath);
|
|
1298
1352
|
return true;
|
|
1299
1353
|
} catch {
|
|
1300
|
-
|
|
1354
|
+
rmSync2(tempOutputPath, { force: true });
|
|
1301
1355
|
return false;
|
|
1302
1356
|
}
|
|
1303
1357
|
}
|
|
@@ -1377,11 +1431,11 @@ function runCapture(command, cwd, env) {
|
|
|
1377
1431
|
};
|
|
1378
1432
|
}
|
|
1379
1433
|
function readJsonFile(path, fallback) {
|
|
1380
|
-
if (!
|
|
1434
|
+
if (!existsSync9(path)) {
|
|
1381
1435
|
return fallback;
|
|
1382
1436
|
}
|
|
1383
1437
|
try {
|
|
1384
|
-
return JSON.parse(
|
|
1438
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
1385
1439
|
} catch {
|
|
1386
1440
|
return fallback;
|
|
1387
1441
|
}
|
|
@@ -1392,31 +1446,31 @@ function nowIso() {
|
|
|
1392
1446
|
function resolveHarnessPaths(projectRoot) {
|
|
1393
1447
|
const hasRuntimeWorkspace = Boolean(process.env.RIG_TASK_WORKSPACE?.trim());
|
|
1394
1448
|
const monorepoRoot = resolveMonorepoRoot2(projectRoot);
|
|
1395
|
-
const harnessRoot =
|
|
1396
|
-
const stateRoot =
|
|
1449
|
+
const harnessRoot = resolve11(projectRoot, "rig");
|
|
1450
|
+
const stateRoot = resolve11(projectRoot, ".rig");
|
|
1397
1451
|
const layout = hasRuntimeWorkspace ? resolveRigLayout(projectRoot) : null;
|
|
1398
|
-
const stateDir = layout?.stateDir ??
|
|
1399
|
-
const logsDir = layout?.logsDir ??
|
|
1400
|
-
const artifactsDir = layout?.artifactsRoot ??
|
|
1401
|
-
const taskConfigPath = layout?.taskConfigPath ??
|
|
1402
|
-
const binDir = layout?.binDir ??
|
|
1452
|
+
const stateDir = layout?.stateDir ?? resolve11(stateRoot, "state");
|
|
1453
|
+
const logsDir = layout?.logsDir ?? resolve11(stateRoot, "logs");
|
|
1454
|
+
const artifactsDir = layout?.artifactsRoot ?? resolve11(monorepoRoot, "artifacts");
|
|
1455
|
+
const taskConfigPath = layout?.taskConfigPath ?? resolve11(monorepoRoot, ".rig", "task-config.json");
|
|
1456
|
+
const binDir = layout?.binDir ?? resolve11(stateRoot, "bin");
|
|
1403
1457
|
return {
|
|
1404
1458
|
harnessRoot,
|
|
1405
1459
|
stateDir: process.env.RIG_STATE_DIR || stateDir,
|
|
1406
1460
|
artifactsDir,
|
|
1407
1461
|
logsDir: process.env.RIG_LOGS_DIR || logsDir,
|
|
1408
1462
|
binDir,
|
|
1409
|
-
hooksDir:
|
|
1410
|
-
validationDir:
|
|
1463
|
+
hooksDir: resolve11(harnessRoot, "hooks"),
|
|
1464
|
+
validationDir: resolve11(harnessRoot, "validation"),
|
|
1411
1465
|
taskConfigPath,
|
|
1412
|
-
sessionPath: process.env.RIG_SESSION_FILE ||
|
|
1466
|
+
sessionPath: process.env.RIG_SESSION_FILE || resolve11(stateRoot, "session", "session.json"),
|
|
1413
1467
|
monorepoRoot,
|
|
1414
|
-
tsApiTestsDir: process.env.TS_API_TESTS_DIR ||
|
|
1415
|
-
taskRepoCommitsPath:
|
|
1416
|
-
baseRepoPinsPath:
|
|
1417
|
-
failedApproachesPath:
|
|
1418
|
-
agentProfilePath:
|
|
1419
|
-
reviewProfilePath:
|
|
1468
|
+
tsApiTestsDir: process.env.TS_API_TESTS_DIR || resolve11(monorepoRoot, "TSAPITests"),
|
|
1469
|
+
taskRepoCommitsPath: resolve11(stateDir, "task-repo-commits.json"),
|
|
1470
|
+
baseRepoPinsPath: resolve11(stateDir, "base-repo-pins.json"),
|
|
1471
|
+
failedApproachesPath: resolve11(stateDir, "failed_approaches.md"),
|
|
1472
|
+
agentProfilePath: resolve11(stateDir, "agent-profile.json"),
|
|
1473
|
+
reviewProfilePath: resolve11(stateDir, "review-profile.json")
|
|
1420
1474
|
};
|
|
1421
1475
|
}
|
|
1422
1476
|
// packages/runtime/src/control-plane/state-sync/reconcile.ts
|
|
@@ -1471,25 +1525,25 @@ function lookupTask(projectRoot, input) {
|
|
|
1471
1525
|
function artifactDirForId(projectRoot, id) {
|
|
1472
1526
|
const workspaceDir = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
1473
1527
|
if (workspaceDir) {
|
|
1474
|
-
const worktreeArtifacts =
|
|
1475
|
-
if (
|
|
1528
|
+
const worktreeArtifacts = resolve12(workspaceDir, "artifacts", id);
|
|
1529
|
+
if (existsSync10(worktreeArtifacts) || existsSync10(resolve12(workspaceDir, "artifacts"))) {
|
|
1476
1530
|
return worktreeArtifacts;
|
|
1477
1531
|
}
|
|
1478
1532
|
}
|
|
1479
1533
|
try {
|
|
1480
1534
|
const paths = resolveHarnessPaths(projectRoot);
|
|
1481
|
-
return
|
|
1535
|
+
return resolve12(paths.artifactsDir, id);
|
|
1482
1536
|
} catch {
|
|
1483
|
-
return
|
|
1537
|
+
return resolve12(resolveMonorepoRoot2(projectRoot), "artifacts", id);
|
|
1484
1538
|
}
|
|
1485
1539
|
}
|
|
1486
1540
|
function resolveTaskConfigPath(projectRoot) {
|
|
1487
1541
|
const paths = resolveHarnessPaths(projectRoot);
|
|
1488
|
-
if (
|
|
1542
|
+
if (existsSync10(paths.taskConfigPath)) {
|
|
1489
1543
|
return paths.taskConfigPath;
|
|
1490
1544
|
}
|
|
1491
1545
|
for (const candidate of sourceTaskConfigCandidates(projectRoot)) {
|
|
1492
|
-
if (
|
|
1546
|
+
if (existsSync10(candidate)) {
|
|
1493
1547
|
return candidate;
|
|
1494
1548
|
}
|
|
1495
1549
|
}
|
|
@@ -1497,7 +1551,7 @@ function resolveTaskConfigPath(projectRoot) {
|
|
|
1497
1551
|
}
|
|
1498
1552
|
function findSourceTaskConfigPath(projectRoot) {
|
|
1499
1553
|
for (const candidate of sourceTaskConfigCandidates(projectRoot)) {
|
|
1500
|
-
if (
|
|
1554
|
+
if (existsSync10(candidate)) {
|
|
1501
1555
|
return candidate;
|
|
1502
1556
|
}
|
|
1503
1557
|
}
|
|
@@ -1510,7 +1564,7 @@ function readAndSyncSourceTaskConfig(projectRoot) {
|
|
|
1510
1564
|
const synced = synchronizeTaskConfigWithTracker(projectRoot, raw);
|
|
1511
1565
|
if (sourcePath && synced.updated) {
|
|
1512
1566
|
try {
|
|
1513
|
-
|
|
1567
|
+
writeFileSync5(sourcePath, `${JSON.stringify(synced.config, null, 2)}
|
|
1514
1568
|
`, "utf-8");
|
|
1515
1569
|
} catch {}
|
|
1516
1570
|
}
|
|
@@ -1562,12 +1616,12 @@ function shouldRefreshAutoSyncedTaskConfigEntry(entry) {
|
|
|
1562
1616
|
return !candidate.role;
|
|
1563
1617
|
}
|
|
1564
1618
|
function readSourceIssueRecords(projectRoot) {
|
|
1565
|
-
const issuesPath =
|
|
1566
|
-
if (!
|
|
1619
|
+
const issuesPath = resolve12(resolveMonorepoRoot2(projectRoot), ".beads", "issues.jsonl");
|
|
1620
|
+
if (!existsSync10(issuesPath)) {
|
|
1567
1621
|
return [];
|
|
1568
1622
|
}
|
|
1569
1623
|
const records = [];
|
|
1570
|
-
for (const line of
|
|
1624
|
+
for (const line of readFileSync7(issuesPath, "utf-8").split(/\r?\n/)) {
|
|
1571
1625
|
const trimmed = line.trim();
|
|
1572
1626
|
if (!trimmed) {
|
|
1573
1627
|
continue;
|
|
@@ -1623,19 +1677,19 @@ function readConfiguredFileTaskConfig(projectRoot) {
|
|
|
1623
1677
|
if (!sourcePath) {
|
|
1624
1678
|
return {};
|
|
1625
1679
|
}
|
|
1626
|
-
const directory =
|
|
1627
|
-
if (!
|
|
1680
|
+
const directory = resolve12(projectRoot, sourcePath);
|
|
1681
|
+
if (!existsSync10(directory)) {
|
|
1628
1682
|
return {};
|
|
1629
1683
|
}
|
|
1630
1684
|
const config = {};
|
|
1631
|
-
for (const name of
|
|
1685
|
+
for (const name of readdirSync3(directory)) {
|
|
1632
1686
|
if (!FILE_TASK_PATTERN2.test(name))
|
|
1633
1687
|
continue;
|
|
1634
|
-
const file =
|
|
1688
|
+
const file = resolve12(directory, name);
|
|
1635
1689
|
try {
|
|
1636
1690
|
if (!statSync3(file).isFile())
|
|
1637
1691
|
continue;
|
|
1638
|
-
const raw = JSON.parse(
|
|
1692
|
+
const raw = JSON.parse(readFileSync7(file, "utf8"));
|
|
1639
1693
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
1640
1694
|
continue;
|
|
1641
1695
|
const record = raw;
|
|
@@ -1677,10 +1731,10 @@ function firstStringList2(...candidates) {
|
|
|
1677
1731
|
return [];
|
|
1678
1732
|
}
|
|
1679
1733
|
function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
1680
|
-
const jsonPath =
|
|
1681
|
-
if (
|
|
1734
|
+
const jsonPath = resolve12(projectRoot, "rig.config.json");
|
|
1735
|
+
if (existsSync10(jsonPath)) {
|
|
1682
1736
|
try {
|
|
1683
|
-
const parsed = JSON.parse(
|
|
1737
|
+
const parsed = JSON.parse(readFileSync7(jsonPath, "utf8"));
|
|
1684
1738
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1685
1739
|
const taskSource = parsed.taskSource;
|
|
1686
1740
|
if (taskSource && typeof taskSource === "object" && !Array.isArray(taskSource)) {
|
|
@@ -1692,12 +1746,12 @@ function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
|
1692
1746
|
return null;
|
|
1693
1747
|
}
|
|
1694
1748
|
}
|
|
1695
|
-
const tsPath =
|
|
1696
|
-
if (!
|
|
1749
|
+
const tsPath = resolve12(projectRoot, "rig.config.ts");
|
|
1750
|
+
if (!existsSync10(tsPath)) {
|
|
1697
1751
|
return null;
|
|
1698
1752
|
}
|
|
1699
1753
|
try {
|
|
1700
|
-
const source =
|
|
1754
|
+
const source = readFileSync7(tsPath, "utf8");
|
|
1701
1755
|
const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
|
|
1702
1756
|
const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
|
|
1703
1757
|
if (kind !== "files") {
|
|
@@ -1711,9 +1765,9 @@ function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
|
1711
1765
|
function sourceTaskConfigCandidates(projectRoot) {
|
|
1712
1766
|
const runtimeContext = loadRuntimeContextFromEnv();
|
|
1713
1767
|
return [
|
|
1714
|
-
runtimeContext?.monorepoMainRoot ?
|
|
1715
|
-
process.env.MONOREPO_MAIN_ROOT?.trim() ?
|
|
1716
|
-
|
|
1768
|
+
runtimeContext?.monorepoMainRoot ? resolve12(runtimeContext.monorepoMainRoot, ".rig", "task-config.json") : "",
|
|
1769
|
+
process.env.MONOREPO_MAIN_ROOT?.trim() ? resolve12(process.env.MONOREPO_MAIN_ROOT.trim(), ".rig", "task-config.json") : "",
|
|
1770
|
+
resolve12(resolveMonorepoRoot2(projectRoot), ".rig", "task-config.json")
|
|
1717
1771
|
].filter(Boolean);
|
|
1718
1772
|
}
|
|
1719
1773
|
|
|
@@ -1756,6 +1810,1249 @@ var GENERATED_TASK_ARTIFACT_FILES = new Set([
|
|
|
1756
1810
|
"git-state.txt"
|
|
1757
1811
|
]);
|
|
1758
1812
|
|
|
1813
|
+
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
1814
|
+
function parseJsonObject(value) {
|
|
1815
|
+
if (!value?.trim())
|
|
1816
|
+
return { value: {}, error: "empty JSON output" };
|
|
1817
|
+
try {
|
|
1818
|
+
const parsed = JSON.parse(value);
|
|
1819
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
return { value: {}, error: error instanceof Error ? error.message : String(error) };
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
function flattenPaginatedArray(value) {
|
|
1825
|
+
if (!Array.isArray(value))
|
|
1826
|
+
return null;
|
|
1827
|
+
if (value.every((entry) => Array.isArray(entry))) {
|
|
1828
|
+
return value.flatMap((entry) => entry);
|
|
1829
|
+
}
|
|
1830
|
+
return value;
|
|
1831
|
+
}
|
|
1832
|
+
function parseConcatenatedJsonValues(value) {
|
|
1833
|
+
const text = value.trim();
|
|
1834
|
+
const docs = [];
|
|
1835
|
+
let start = null;
|
|
1836
|
+
let depth = 0;
|
|
1837
|
+
let inString = false;
|
|
1838
|
+
let escape = false;
|
|
1839
|
+
for (let index = 0;index < text.length; index += 1) {
|
|
1840
|
+
const char = text[index];
|
|
1841
|
+
if (start === null) {
|
|
1842
|
+
if (/\s/.test(char))
|
|
1843
|
+
continue;
|
|
1844
|
+
start = index;
|
|
1845
|
+
}
|
|
1846
|
+
if (inString) {
|
|
1847
|
+
if (escape) {
|
|
1848
|
+
escape = false;
|
|
1849
|
+
} else if (char === "\\") {
|
|
1850
|
+
escape = true;
|
|
1851
|
+
} else if (char === '"') {
|
|
1852
|
+
inString = false;
|
|
1853
|
+
}
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
if (char === '"') {
|
|
1857
|
+
inString = true;
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
if (char === "{" || char === "[") {
|
|
1861
|
+
depth += 1;
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
if (char === "}" || char === "]") {
|
|
1865
|
+
depth -= 1;
|
|
1866
|
+
if (depth < 0)
|
|
1867
|
+
return { value: docs, error: "unexpected JSON close delimiter" };
|
|
1868
|
+
if (depth === 0 && start !== null) {
|
|
1869
|
+
const segment = text.slice(start, index + 1);
|
|
1870
|
+
try {
|
|
1871
|
+
docs.push(JSON.parse(segment));
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
return { value: docs, error: error instanceof Error ? error.message : String(error) };
|
|
1874
|
+
}
|
|
1875
|
+
start = null;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
if (inString || depth !== 0 || start !== null)
|
|
1880
|
+
return { value: docs, error: "incomplete JSON stream" };
|
|
1881
|
+
return { value: docs };
|
|
1882
|
+
}
|
|
1883
|
+
function parseJsonArray(value) {
|
|
1884
|
+
if (!value?.trim())
|
|
1885
|
+
return { value: [], error: "empty JSON output" };
|
|
1886
|
+
try {
|
|
1887
|
+
const parsed = JSON.parse(value);
|
|
1888
|
+
const flattened = flattenPaginatedArray(parsed);
|
|
1889
|
+
return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
const streamed = parseConcatenatedJsonValues(value);
|
|
1892
|
+
if (streamed.error)
|
|
1893
|
+
return { value: [], error: error instanceof Error ? error.message : String(error) };
|
|
1894
|
+
const flattened = streamed.value.flatMap((entry) => flattenPaginatedArray(entry) ?? []);
|
|
1895
|
+
return flattened.length > 0 || streamed.value.length === 0 ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
function parseGithubPrUrl(prUrl) {
|
|
1899
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
|
|
1900
|
+
if (!match)
|
|
1901
|
+
return null;
|
|
1902
|
+
const prNumber = Number.parseInt(match[3], 10);
|
|
1903
|
+
if (!Number.isFinite(prNumber))
|
|
1904
|
+
return null;
|
|
1905
|
+
return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
|
|
1906
|
+
}
|
|
1907
|
+
function checkName(check) {
|
|
1908
|
+
return String(check.name ?? check.context ?? "").trim();
|
|
1909
|
+
}
|
|
1910
|
+
function checkState(check) {
|
|
1911
|
+
return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
|
|
1912
|
+
}
|
|
1913
|
+
function isGreptileLabel(value) {
|
|
1914
|
+
return String(value ?? "").toLowerCase().includes("greptile");
|
|
1915
|
+
}
|
|
1916
|
+
function isGreptileGithubLogin(value) {
|
|
1917
|
+
const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
|
|
1918
|
+
return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
|
|
1919
|
+
}
|
|
1920
|
+
function isPassingCheck(check) {
|
|
1921
|
+
const state = checkState(check);
|
|
1922
|
+
return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
|
|
1923
|
+
}
|
|
1924
|
+
function isPendingCheck(check) {
|
|
1925
|
+
const state = checkState(check);
|
|
1926
|
+
return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
|
|
1927
|
+
}
|
|
1928
|
+
function isFailingCheck(check) {
|
|
1929
|
+
const state = checkState(check);
|
|
1930
|
+
return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
|
|
1931
|
+
}
|
|
1932
|
+
function wildcardToRegExp(pattern) {
|
|
1933
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1934
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
1935
|
+
}
|
|
1936
|
+
function isAllowedFailure(name, allowedFailures) {
|
|
1937
|
+
return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
|
|
1938
|
+
}
|
|
1939
|
+
function greptileScorePatterns() {
|
|
1940
|
+
return [
|
|
1941
|
+
/\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
|
|
1942
|
+
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
|
|
1943
|
+
/\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
|
|
1944
|
+
];
|
|
1945
|
+
}
|
|
1946
|
+
function parseGreptileScores(input) {
|
|
1947
|
+
const text = stripHtml(input);
|
|
1948
|
+
const seen = new Set;
|
|
1949
|
+
const scores = [];
|
|
1950
|
+
for (const pattern of greptileScorePatterns()) {
|
|
1951
|
+
for (const match of text.matchAll(pattern)) {
|
|
1952
|
+
const value = Number.parseInt(match[1] || "", 10);
|
|
1953
|
+
const scale = Number.parseInt(match[2] || "", 10);
|
|
1954
|
+
if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
|
|
1955
|
+
continue;
|
|
1956
|
+
const raw = match[0] || `${value}/${scale}`;
|
|
1957
|
+
const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
|
|
1958
|
+
if (seen.has(key))
|
|
1959
|
+
continue;
|
|
1960
|
+
seen.add(key);
|
|
1961
|
+
scores.push({ value, scale, raw });
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
return scores;
|
|
1965
|
+
}
|
|
1966
|
+
function parseGreptileScore(input) {
|
|
1967
|
+
return parseGreptileScores(input)[0] ?? null;
|
|
1968
|
+
}
|
|
1969
|
+
function stripHtml(input) {
|
|
1970
|
+
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
1971
|
+
|
|
1972
|
+
`).trim();
|
|
1973
|
+
}
|
|
1974
|
+
function containsBlockerText(input) {
|
|
1975
|
+
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
1976
|
+
return /not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/i.test(text);
|
|
1977
|
+
}
|
|
1978
|
+
function isStrictFiveOfFive(score) {
|
|
1979
|
+
return score.value === 5 && score.scale === 5;
|
|
1980
|
+
}
|
|
1981
|
+
function containsConflictingScoreText(input) {
|
|
1982
|
+
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
1983
|
+
}
|
|
1984
|
+
function extractGreptileCommentBlock(input) {
|
|
1985
|
+
const match = input.match(/<!--\s*greptile_comment\s*-->([\s\S]*?)<!--\s*\/greptile_comment\s*-->/i);
|
|
1986
|
+
return match?.[1]?.trim() ?? null;
|
|
1987
|
+
}
|
|
1988
|
+
function extractGreptileBodyReviewedSha(input) {
|
|
1989
|
+
const block = extractGreptileCommentBlock(input);
|
|
1990
|
+
if (!block)
|
|
1991
|
+
return null;
|
|
1992
|
+
const commitLink = block.match(/\/commit\/([0-9a-f]{40})(?:\b|[^0-9a-f])/i);
|
|
1993
|
+
return commitLink?.[1]?.toLowerCase() ?? null;
|
|
1994
|
+
}
|
|
1995
|
+
function isoAtOrAfter(value, floor) {
|
|
1996
|
+
if (!value || !floor)
|
|
1997
|
+
return false;
|
|
1998
|
+
const valueMs = Date.parse(value);
|
|
1999
|
+
const floorMs = Date.parse(floor);
|
|
2000
|
+
return Number.isFinite(valueMs) && Number.isFinite(floorMs) && valueMs >= floorMs;
|
|
2001
|
+
}
|
|
2002
|
+
function greptileStatusVerdict(status) {
|
|
2003
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
2004
|
+
if (!normalized)
|
|
2005
|
+
return null;
|
|
2006
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
2007
|
+
return "approved";
|
|
2008
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
2009
|
+
return "rejected";
|
|
2010
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
2011
|
+
return "skipped";
|
|
2012
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
2013
|
+
return "failed";
|
|
2014
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
2015
|
+
return "pending";
|
|
2016
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
2017
|
+
return "completed";
|
|
2018
|
+
return null;
|
|
2019
|
+
}
|
|
2020
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
2021
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
2022
|
+
}
|
|
2023
|
+
function greptileRequestTimeoutMs(env) {
|
|
2024
|
+
const fallback = 30000;
|
|
2025
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
2026
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
2027
|
+
}
|
|
2028
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
2029
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
2030
|
+
return null;
|
|
2031
|
+
const record = entry;
|
|
2032
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
2033
|
+
if (!id)
|
|
2034
|
+
return null;
|
|
2035
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
2036
|
+
return {
|
|
2037
|
+
id,
|
|
2038
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
2039
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
2040
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
2041
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
2045
|
+
const seen = new Set;
|
|
2046
|
+
const unique2 = [];
|
|
2047
|
+
for (const review of reviews) {
|
|
2048
|
+
if (seen.has(review.id))
|
|
2049
|
+
continue;
|
|
2050
|
+
seen.add(review.id);
|
|
2051
|
+
unique2.push(review);
|
|
2052
|
+
}
|
|
2053
|
+
return unique2;
|
|
2054
|
+
}
|
|
2055
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
2056
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
2057
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
2058
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
2059
|
+
const latest = sorted.slice(0, 1);
|
|
2060
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
2061
|
+
}
|
|
2062
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
2063
|
+
const selected = details ?? review;
|
|
2064
|
+
return {
|
|
2065
|
+
id: selected.id || review.id,
|
|
2066
|
+
body: selected.body ?? review.body ?? null,
|
|
2067
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
2068
|
+
status: selected.status ?? review.status ?? null
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
async function callGreptileMcpToolForGate(input) {
|
|
2072
|
+
const controller = new AbortController;
|
|
2073
|
+
const timeoutId = setTimeout(() => {
|
|
2074
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
2075
|
+
}, input.timeoutMs);
|
|
2076
|
+
let response;
|
|
2077
|
+
try {
|
|
2078
|
+
response = await input.fetchFn(input.apiBase, {
|
|
2079
|
+
method: "POST",
|
|
2080
|
+
headers: {
|
|
2081
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
2082
|
+
"Content-Type": "application/json"
|
|
2083
|
+
},
|
|
2084
|
+
body: JSON.stringify({
|
|
2085
|
+
jsonrpc: "2.0",
|
|
2086
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
2087
|
+
method: "tools/call",
|
|
2088
|
+
params: { name: input.name, arguments: input.args }
|
|
2089
|
+
}),
|
|
2090
|
+
signal: controller.signal
|
|
2091
|
+
});
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
if (controller.signal.aborted) {
|
|
2094
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
2095
|
+
}
|
|
2096
|
+
throw error;
|
|
2097
|
+
} finally {
|
|
2098
|
+
clearTimeout(timeoutId);
|
|
2099
|
+
}
|
|
2100
|
+
const raw = await response.text();
|
|
2101
|
+
if (!response.ok) {
|
|
2102
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
2103
|
+
}
|
|
2104
|
+
let envelope;
|
|
2105
|
+
try {
|
|
2106
|
+
envelope = JSON.parse(raw);
|
|
2107
|
+
} catch {
|
|
2108
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
2109
|
+
}
|
|
2110
|
+
if (envelope.error?.message) {
|
|
2111
|
+
throw new Error(envelope.error.message);
|
|
2112
|
+
}
|
|
2113
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
2114
|
+
`).trim();
|
|
2115
|
+
if (!text) {
|
|
2116
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
2117
|
+
}
|
|
2118
|
+
return text;
|
|
2119
|
+
}
|
|
2120
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
2121
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
2122
|
+
try {
|
|
2123
|
+
return JSON.parse(text);
|
|
2124
|
+
} catch {
|
|
2125
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
2129
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
2130
|
+
return { signals: [], errors: [] };
|
|
2131
|
+
}
|
|
2132
|
+
const env = input.options?.env ?? process.env;
|
|
2133
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
2134
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
2135
|
+
if (!apiKey) {
|
|
2136
|
+
return { signals: [], errors: [] };
|
|
2137
|
+
}
|
|
2138
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
2139
|
+
if (typeof fetchFn !== "function") {
|
|
2140
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
2141
|
+
}
|
|
2142
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
2143
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
2144
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
2145
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
2146
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
2147
|
+
try {
|
|
2148
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
2149
|
+
apiBase,
|
|
2150
|
+
apiKey,
|
|
2151
|
+
name: "list_code_reviews",
|
|
2152
|
+
args: {
|
|
2153
|
+
name: repository,
|
|
2154
|
+
remote,
|
|
2155
|
+
defaultBranch,
|
|
2156
|
+
prNumber: input.prNumber,
|
|
2157
|
+
limit: 20
|
|
2158
|
+
},
|
|
2159
|
+
timeoutMs,
|
|
2160
|
+
fetchFn
|
|
2161
|
+
});
|
|
2162
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
2163
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
2164
|
+
const signals = [];
|
|
2165
|
+
for (const review of selectedReviews) {
|
|
2166
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
2167
|
+
apiBase,
|
|
2168
|
+
apiKey,
|
|
2169
|
+
name: "get_code_review",
|
|
2170
|
+
args: { codeReviewId: review.id },
|
|
2171
|
+
timeoutMs,
|
|
2172
|
+
fetchFn
|
|
2173
|
+
});
|
|
2174
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
2175
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
2176
|
+
}
|
|
2177
|
+
return { signals, errors: [] };
|
|
2178
|
+
} catch (error) {
|
|
2179
|
+
return {
|
|
2180
|
+
signals: [],
|
|
2181
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
function firstString(record, keys) {
|
|
2186
|
+
for (const key of keys) {
|
|
2187
|
+
const value = record[key];
|
|
2188
|
+
if (typeof value === "string")
|
|
2189
|
+
return value;
|
|
2190
|
+
}
|
|
2191
|
+
return "";
|
|
2192
|
+
}
|
|
2193
|
+
function arrayField(record, key) {
|
|
2194
|
+
const value = record[key];
|
|
2195
|
+
return Array.isArray(value) ? value : [];
|
|
2196
|
+
}
|
|
2197
|
+
async function runJsonArray(command, args, cwd) {
|
|
2198
|
+
const result = await command(args, { cwd });
|
|
2199
|
+
const label = `gh ${args.join(" ")}`;
|
|
2200
|
+
if (result.exitCode !== 0) {
|
|
2201
|
+
return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
2202
|
+
}
|
|
2203
|
+
const parsed = parseJsonArray(result.stdout);
|
|
2204
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
2205
|
+
}
|
|
2206
|
+
async function runJsonObject(command, args, cwd) {
|
|
2207
|
+
const result = await command(args, { cwd });
|
|
2208
|
+
const label = `gh ${args.join(" ")}`;
|
|
2209
|
+
if (result.exitCode !== 0) {
|
|
2210
|
+
return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
2211
|
+
}
|
|
2212
|
+
const parsed = parseJsonObject(result.stdout);
|
|
2213
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
2214
|
+
}
|
|
2215
|
+
function normalizeStatusCheck(entry) {
|
|
2216
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
2217
|
+
return null;
|
|
2218
|
+
const record = entry;
|
|
2219
|
+
const name = firstString(record, ["name", "context"]);
|
|
2220
|
+
if (!name.trim())
|
|
2221
|
+
return null;
|
|
2222
|
+
const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
|
|
2223
|
+
const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
|
|
2224
|
+
return {
|
|
2225
|
+
__typename: typeof record.__typename === "string" ? record.__typename : null,
|
|
2226
|
+
name,
|
|
2227
|
+
context: typeof record.context === "string" ? record.context : null,
|
|
2228
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
2229
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
2230
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
2231
|
+
detailsUrl: typeof record.detailsUrl === "string" ? record.detailsUrl : typeof record.details_url === "string" ? record.details_url : typeof record.html_url === "string" ? record.html_url : typeof record.link === "string" ? record.link : null,
|
|
2232
|
+
link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
|
|
2233
|
+
headSha: typeof record.headSha === "string" ? record.headSha : null,
|
|
2234
|
+
head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
|
|
2235
|
+
output: output ? {
|
|
2236
|
+
title: typeof output.title === "string" ? output.title : null,
|
|
2237
|
+
summary: typeof output.summary === "string" ? output.summary : null,
|
|
2238
|
+
text: typeof output.text === "string" ? output.text : null
|
|
2239
|
+
} : null,
|
|
2240
|
+
app: app ? {
|
|
2241
|
+
slug: typeof app.slug === "string" ? app.slug : null,
|
|
2242
|
+
name: typeof app.name === "string" ? app.name : null,
|
|
2243
|
+
owner: app.owner && typeof app.owner === "object" ? app.owner : null
|
|
2244
|
+
} : null
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
function normalizeReview(entry) {
|
|
2248
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
2249
|
+
return null;
|
|
2250
|
+
const record = entry;
|
|
2251
|
+
return {
|
|
2252
|
+
id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
|
|
2253
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
2254
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
2255
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : typeof record.commitId === "string" ? record.commitId : record.commit && typeof record.commit === "object" && typeof record.commit.oid === "string" ? record.commit.oid : null,
|
|
2256
|
+
html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
|
|
2257
|
+
author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
function normalizeReviewComment(entry) {
|
|
2261
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
2262
|
+
return null;
|
|
2263
|
+
const record = entry;
|
|
2264
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
2265
|
+
const path = typeof record.path === "string" ? record.path : null;
|
|
2266
|
+
if (!body && !path)
|
|
2267
|
+
return null;
|
|
2268
|
+
return {
|
|
2269
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
2270
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
2271
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
2272
|
+
body,
|
|
2273
|
+
path,
|
|
2274
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
2275
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
2276
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
|
|
2277
|
+
original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
function normalizeIssueComment(entry) {
|
|
2281
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
2282
|
+
return null;
|
|
2283
|
+
const record = entry;
|
|
2284
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
2285
|
+
if (!body)
|
|
2286
|
+
return null;
|
|
2287
|
+
return {
|
|
2288
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
2289
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
2290
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
2291
|
+
body,
|
|
2292
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
2293
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
2294
|
+
created_at: typeof record.created_at === "string" ? record.created_at : null
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
function normalizeReviewThread(entry) {
|
|
2298
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
2299
|
+
return null;
|
|
2300
|
+
const record = entry;
|
|
2301
|
+
return {
|
|
2302
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
2303
|
+
isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
|
|
2304
|
+
isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
|
|
2305
|
+
comments: record.comments && typeof record.comments === "object" ? record.comments : null
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
function relevantIssueComment(comment) {
|
|
2309
|
+
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
2310
|
+
const body = comment.body ?? "";
|
|
2311
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
2312
|
+
}
|
|
2313
|
+
function latestThreadComment(thread) {
|
|
2314
|
+
const nodes = thread.comments?.nodes ?? [];
|
|
2315
|
+
return nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
2316
|
+
}
|
|
2317
|
+
function unresolvedThreadSummaries(threads) {
|
|
2318
|
+
return threads.flatMap((thread) => {
|
|
2319
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
2320
|
+
return [];
|
|
2321
|
+
const latest = latestThreadComment(thread);
|
|
2322
|
+
if (!latest)
|
|
2323
|
+
return ["Unresolved review thread"];
|
|
2324
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
2325
|
+
return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
function collectBodies(evidence) {
|
|
2329
|
+
return [
|
|
2330
|
+
evidence.title ?? "",
|
|
2331
|
+
evidence.body,
|
|
2332
|
+
...evidence.reviews.map((review) => review.body ?? ""),
|
|
2333
|
+
...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
|
|
2334
|
+
...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
|
|
2335
|
+
...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
|
|
2336
|
+
...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
|
|
2337
|
+
].filter((body) => body.trim().length > 0);
|
|
2338
|
+
}
|
|
2339
|
+
function bodyExcerpt(body) {
|
|
2340
|
+
const text = stripHtml(body).replace(/\s+/g, " ").trim();
|
|
2341
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
2342
|
+
}
|
|
2343
|
+
function makeGreptileSignal(input) {
|
|
2344
|
+
const scores = parseGreptileScores(input.body);
|
|
2345
|
+
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
2346
|
+
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
2347
|
+
const verdict = input.verdict ?? null;
|
|
2348
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
2349
|
+
const explicitApproval = input.explicitApproval ?? false;
|
|
2350
|
+
return {
|
|
2351
|
+
source: input.source,
|
|
2352
|
+
trusted: input.trusted,
|
|
2353
|
+
authorLogin: input.authorLogin ?? null,
|
|
2354
|
+
reviewedSha,
|
|
2355
|
+
current,
|
|
2356
|
+
stale: current === false,
|
|
2357
|
+
score: scores[0] ?? null,
|
|
2358
|
+
scores,
|
|
2359
|
+
explicitApproval,
|
|
2360
|
+
verdict,
|
|
2361
|
+
blocker,
|
|
2362
|
+
actionable: input.actionable ?? blocker,
|
|
2363
|
+
bodyExcerpt: bodyExcerpt(input.body),
|
|
2364
|
+
body: input.body,
|
|
2365
|
+
allScores: scores
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
function reviewAuthorLogin(review) {
|
|
2369
|
+
return review.author?.login ?? null;
|
|
2370
|
+
}
|
|
2371
|
+
function commentAuthorLogin(comment) {
|
|
2372
|
+
return comment.user?.login ?? comment.author?.login ?? null;
|
|
2373
|
+
}
|
|
2374
|
+
function collectGreptileSignals(evidence) {
|
|
2375
|
+
const signals = [];
|
|
2376
|
+
const greptileBodyReviewedSha = extractGreptileBodyReviewedSha(evidence.body);
|
|
2377
|
+
const trustedGreptileBody = Boolean(greptileBodyReviewedSha && isGreptileGithubLogin(evidence.bodyEditorLogin) && isoAtOrAfter(evidence.bodyLastEditedAt, evidence.headCommittedDate));
|
|
2378
|
+
const contextSources = [
|
|
2379
|
+
{ source: "pr-title", body: evidence.title ?? "" },
|
|
2380
|
+
{
|
|
2381
|
+
source: "pr-body",
|
|
2382
|
+
body: evidence.body,
|
|
2383
|
+
trusted: trustedGreptileBody,
|
|
2384
|
+
authorLogin: trustedGreptileBody ? evidence.bodyEditorLogin ?? null : null,
|
|
2385
|
+
reviewedSha: greptileBodyReviewedSha,
|
|
2386
|
+
verdict: trustedGreptileBody ? "completed" : null
|
|
2387
|
+
}
|
|
2388
|
+
];
|
|
2389
|
+
for (const context of contextSources) {
|
|
2390
|
+
if (!context.body.trim())
|
|
2391
|
+
continue;
|
|
2392
|
+
const contextBlocker = containsBlockerText(context.body);
|
|
2393
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
2394
|
+
continue;
|
|
2395
|
+
signals.push(makeGreptileSignal({
|
|
2396
|
+
source: context.source,
|
|
2397
|
+
body: context.body,
|
|
2398
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2399
|
+
trusted: context.trusted === true,
|
|
2400
|
+
authorLogin: context.authorLogin,
|
|
2401
|
+
reviewedSha: context.reviewedSha,
|
|
2402
|
+
verdict: context.verdict,
|
|
2403
|
+
blocker: contextBlocker,
|
|
2404
|
+
actionable: contextBlocker
|
|
2405
|
+
}));
|
|
2406
|
+
}
|
|
2407
|
+
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
2408
|
+
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
2409
|
+
|
|
2410
|
+
`) || "Status: UNKNOWN";
|
|
2411
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
2412
|
+
signals.push(makeGreptileSignal({
|
|
2413
|
+
source: "api",
|
|
2414
|
+
body,
|
|
2415
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2416
|
+
trusted: true,
|
|
2417
|
+
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
2418
|
+
explicitApproval: verdict === "approved",
|
|
2419
|
+
verdict
|
|
2420
|
+
}));
|
|
2421
|
+
}
|
|
2422
|
+
for (const review of evidence.reviews) {
|
|
2423
|
+
const login = reviewAuthorLogin(review);
|
|
2424
|
+
if (!isGreptileGithubLogin(login))
|
|
2425
|
+
continue;
|
|
2426
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
2427
|
+
const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
|
|
2428
|
+
|
|
2429
|
+
`);
|
|
2430
|
+
if (!body.trim())
|
|
2431
|
+
continue;
|
|
2432
|
+
const dismissed = state === "DISMISSED";
|
|
2433
|
+
signals.push(makeGreptileSignal({
|
|
2434
|
+
source: "github-review",
|
|
2435
|
+
body,
|
|
2436
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2437
|
+
trusted: !dismissed,
|
|
2438
|
+
authorLogin: login,
|
|
2439
|
+
reviewedSha: review.commit_id ?? null,
|
|
2440
|
+
explicitApproval: undefined,
|
|
2441
|
+
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
2442
|
+
}));
|
|
2443
|
+
}
|
|
2444
|
+
for (const comment of evidence.relevantIssueComments) {
|
|
2445
|
+
const login = commentAuthorLogin(comment);
|
|
2446
|
+
const body = comment.body ?? "";
|
|
2447
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
2448
|
+
continue;
|
|
2449
|
+
signals.push(makeGreptileSignal({
|
|
2450
|
+
source: "issue-comment",
|
|
2451
|
+
body,
|
|
2452
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2453
|
+
trusted: true,
|
|
2454
|
+
authorLogin: login
|
|
2455
|
+
}));
|
|
2456
|
+
}
|
|
2457
|
+
for (const thread of evidence.reviewThreads) {
|
|
2458
|
+
if (thread.isOutdated === true || thread.isResolved === true)
|
|
2459
|
+
continue;
|
|
2460
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
2461
|
+
const login = comment.author?.login ?? null;
|
|
2462
|
+
const body = comment.body ?? "";
|
|
2463
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
2464
|
+
continue;
|
|
2465
|
+
signals.push(makeGreptileSignal({
|
|
2466
|
+
source: "review-thread",
|
|
2467
|
+
body,
|
|
2468
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2469
|
+
trusted: true,
|
|
2470
|
+
authorLogin: login
|
|
2471
|
+
}));
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
for (const check of evidence.checks) {
|
|
2475
|
+
if (!isGreptileLabel(checkName(check)))
|
|
2476
|
+
continue;
|
|
2477
|
+
const reviewedSha = check.headSha ?? check.head_sha ?? null;
|
|
2478
|
+
const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
|
|
2479
|
+
const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
|
|
2480
|
+
|
|
2481
|
+
`);
|
|
2482
|
+
signals.push(makeGreptileSignal({
|
|
2483
|
+
source: "github-check",
|
|
2484
|
+
body,
|
|
2485
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
2486
|
+
trusted: false,
|
|
2487
|
+
reviewedSha,
|
|
2488
|
+
explicitApproval: false,
|
|
2489
|
+
blocker: isFailingCheck(check),
|
|
2490
|
+
actionable: isFailingCheck(check)
|
|
2491
|
+
}));
|
|
2492
|
+
}
|
|
2493
|
+
return signals;
|
|
2494
|
+
}
|
|
2495
|
+
function unresolvedGreptileThreadSummaries(threads) {
|
|
2496
|
+
return threads.flatMap((thread) => {
|
|
2497
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
2498
|
+
return [];
|
|
2499
|
+
const comments = thread.comments?.nodes ?? [];
|
|
2500
|
+
if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
|
|
2501
|
+
return [];
|
|
2502
|
+
const latest = latestThreadComment(thread);
|
|
2503
|
+
if (!latest)
|
|
2504
|
+
return ["Unresolved Greptile review thread"];
|
|
2505
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
2506
|
+
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
2510
|
+
return [];
|
|
2511
|
+
}
|
|
2512
|
+
function issueLevelBlockerSummaries(comments) {
|
|
2513
|
+
return comments.flatMap((comment) => {
|
|
2514
|
+
const body = comment.body?.trim() ?? "";
|
|
2515
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
2516
|
+
return [];
|
|
2517
|
+
const login = commentAuthorLogin(comment) ?? "unknown";
|
|
2518
|
+
const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
|
|
2519
|
+
return [`${author}: ${body}`];
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
function reviewBodyBlockerSummaries(reviews) {
|
|
2523
|
+
return reviews.flatMap((review) => {
|
|
2524
|
+
const login = reviewAuthorLogin(review) ?? "unknown";
|
|
2525
|
+
if (isGreptileGithubLogin(login))
|
|
2526
|
+
return [];
|
|
2527
|
+
const body = review.body?.trim() ?? "";
|
|
2528
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
2529
|
+
return [];
|
|
2530
|
+
const state = review.state ? ` (${review.state})` : "";
|
|
2531
|
+
return [`PR review summary by ${login}${state}: ${body}`];
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
function signalLabel(signal) {
|
|
2535
|
+
const source = signal.source.replace(/-/g, " ");
|
|
2536
|
+
const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
|
|
2537
|
+
const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
|
|
2538
|
+
return `${source}${author}${sha}`;
|
|
2539
|
+
}
|
|
2540
|
+
function deriveGreptileEvidence(input) {
|
|
2541
|
+
const rawBodies = collectBodies(input);
|
|
2542
|
+
const signals = collectGreptileSignals(input);
|
|
2543
|
+
const trustedSignals = signals.filter((signal) => signal.trusted);
|
|
2544
|
+
const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
2545
|
+
const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
2546
|
+
const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
|
|
2547
|
+
const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
|
|
2548
|
+
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
2549
|
+
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
2550
|
+
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
2551
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
2552
|
+
const signalCanApproveByScore = (signal) => {
|
|
2553
|
+
if (signal.source === "api")
|
|
2554
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
2555
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
2556
|
+
};
|
|
2557
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
2558
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
2559
|
+
const approvedByScore = !!approvingScoreEntry;
|
|
2560
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
2561
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
2562
|
+
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
2563
|
+
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
2564
|
+
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
2565
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
2566
|
+
const staleBlockingSignals = [];
|
|
2567
|
+
const blockers = [
|
|
2568
|
+
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
2569
|
+
...reviewBodyBlockerSummaries(input.reviews),
|
|
2570
|
+
...issueLevelBlockerSummaries(input.relevantIssueComments),
|
|
2571
|
+
...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
|
|
2572
|
+
...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
|
|
2573
|
+
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
2574
|
+
];
|
|
2575
|
+
const unresolvedComments = [
|
|
2576
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
2577
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
2578
|
+
];
|
|
2579
|
+
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
2580
|
+
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
2581
|
+
const completedGreptileCheck = greptileChecks.some((check) => {
|
|
2582
|
+
const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
|
|
2583
|
+
return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
|
|
2584
|
+
});
|
|
2585
|
+
const completedGreptileReview = greptileReviews.some((review) => {
|
|
2586
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
2587
|
+
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
2588
|
+
return completedState && review.commit_id === input.currentHeadSha;
|
|
2589
|
+
});
|
|
2590
|
+
const completedGreptileApi = trustedSignals.some((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && (signal.verdict === "approved" || signal.verdict === "rejected" || signal.verdict === "skipped" || signal.verdict === "failed" || signal.verdict === "completed"));
|
|
2591
|
+
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
2592
|
+
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
2593
|
+
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
2594
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
2595
|
+
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
2596
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
2597
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
2598
|
+
const source = approvingSignal?.source === "api" ? "api" : approvingSignal?.source === "github-review" ? "github-review" : approvingSignal?.source === "pr-body" || approvingSignal?.source === "pr-title" ? "pr-body" : approvingSignal?.source === "changed-file-comment" || approvingSignal?.source === "issue-comment" || approvingSignal?.source === "review-thread" ? "github-comment" : greptileReviews.length > 0 && greptileChecks.length > 0 ? "combined" : greptileReviews.length > 0 ? "github-review" : greptileChecks.length > 0 ? "github-check" : signals.some((signal) => signal.source === "pr-body" || signal.source === "pr-title") ? "pr-body" : "missing";
|
|
2599
|
+
return {
|
|
2600
|
+
source,
|
|
2601
|
+
currentHeadSha: input.currentHeadSha,
|
|
2602
|
+
reviewedSha,
|
|
2603
|
+
fresh,
|
|
2604
|
+
completed,
|
|
2605
|
+
approved,
|
|
2606
|
+
score,
|
|
2607
|
+
explicitApproval: approvedByExplicitMapping,
|
|
2608
|
+
blockers,
|
|
2609
|
+
unresolvedComments,
|
|
2610
|
+
rawBodies,
|
|
2611
|
+
signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
|
|
2612
|
+
mapping
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
function isGreptileCheckDetail(check) {
|
|
2616
|
+
return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
|
|
2617
|
+
}
|
|
2618
|
+
async function collectGreptileCheckDetails(input) {
|
|
2619
|
+
const checkRunsRead = await runJsonObject(input.command, [
|
|
2620
|
+
"api",
|
|
2621
|
+
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
2622
|
+
"-F",
|
|
2623
|
+
"per_page=100"
|
|
2624
|
+
], input.projectRoot);
|
|
2625
|
+
const checkRuns = arrayField(checkRunsRead.value, "check_runs").map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
|
|
2626
|
+
return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
|
|
2627
|
+
}
|
|
2628
|
+
async function collectPullRequestProvenance(input) {
|
|
2629
|
+
const response = await runJsonObject(input.command, [
|
|
2630
|
+
"api",
|
|
2631
|
+
"graphql",
|
|
2632
|
+
"-F",
|
|
2633
|
+
`owner=${input.owner}`,
|
|
2634
|
+
"-F",
|
|
2635
|
+
`name=${input.name}`,
|
|
2636
|
+
"-F",
|
|
2637
|
+
`prNumber=${input.prNumber}`,
|
|
2638
|
+
"-f",
|
|
2639
|
+
"query=query($owner: String!, $name: String!, $prNumber: Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$prNumber) { lastEditedAt editor { login } commits(last: 1) { nodes { commit { oid committedDate } } } } } }"
|
|
2640
|
+
], input.projectRoot);
|
|
2641
|
+
if (response.error)
|
|
2642
|
+
return { value: {}, error: response.error };
|
|
2643
|
+
const data = response.value.data;
|
|
2644
|
+
const repository = data?.repository;
|
|
2645
|
+
const pullRequest = repository?.pullRequest;
|
|
2646
|
+
if (!pullRequest)
|
|
2647
|
+
return { value: {}, error: "GitHub pullRequest provenance response did not include a pullRequest object" };
|
|
2648
|
+
const editor = pullRequest.editor;
|
|
2649
|
+
const commits = pullRequest.commits;
|
|
2650
|
+
const nodes = Array.isArray(commits?.nodes) ? commits.nodes : [];
|
|
2651
|
+
const latestCommitNode = nodes[nodes.length - 1];
|
|
2652
|
+
const latestCommit = latestCommitNode?.commit;
|
|
2653
|
+
return {
|
|
2654
|
+
value: {
|
|
2655
|
+
bodyEditorLogin: typeof editor?.login === "string" ? editor.login : null,
|
|
2656
|
+
bodyLastEditedAt: typeof pullRequest.lastEditedAt === "string" ? pullRequest.lastEditedAt : null,
|
|
2657
|
+
headCommittedDate: typeof latestCommit?.committedDate === "string" ? latestCommit.committedDate : null
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
async function collectReviewThreads(input) {
|
|
2662
|
+
const reviewThreads = [];
|
|
2663
|
+
let afterCursor = null;
|
|
2664
|
+
for (let page = 0;page < 100; page += 1) {
|
|
2665
|
+
const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
|
|
2666
|
+
const threadsResponse = await runJsonObject(input.command, [
|
|
2667
|
+
"api",
|
|
2668
|
+
"graphql",
|
|
2669
|
+
"-F",
|
|
2670
|
+
`owner=${input.owner}`,
|
|
2671
|
+
"-F",
|
|
2672
|
+
`name=${input.name}`,
|
|
2673
|
+
"-F",
|
|
2674
|
+
`prNumber=${input.prNumber}`,
|
|
2675
|
+
"-f",
|
|
2676
|
+
`query=query($owner: String!, $name: String!, $prNumber: Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$prNumber) { reviewThreads(first: 100, after: ${afterLiteral}) { nodes { id isResolved isOutdated comments(first: 100) { nodes { author { login } body path url createdAt } pageInfo { hasNextPage endCursor } } } pageInfo { hasNextPage endCursor } } } } }`
|
|
2677
|
+
], input.projectRoot);
|
|
2678
|
+
if (threadsResponse.error) {
|
|
2679
|
+
return { value: reviewThreads, error: threadsResponse.error };
|
|
2680
|
+
}
|
|
2681
|
+
const data = threadsResponse.value.data;
|
|
2682
|
+
const repository = data?.repository;
|
|
2683
|
+
const pullRequest = repository?.pullRequest;
|
|
2684
|
+
const threads = pullRequest?.reviewThreads;
|
|
2685
|
+
const nodes = threads?.nodes;
|
|
2686
|
+
if (!Array.isArray(nodes)) {
|
|
2687
|
+
return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
|
|
2688
|
+
}
|
|
2689
|
+
const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
|
|
2690
|
+
reviewThreads.push(...normalized);
|
|
2691
|
+
const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
|
|
2692
|
+
if (truncatedCommentThread) {
|
|
2693
|
+
return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
|
|
2694
|
+
}
|
|
2695
|
+
const pageInfo = threads?.pageInfo;
|
|
2696
|
+
if (!pageInfo) {
|
|
2697
|
+
if (nodes.length >= 100) {
|
|
2698
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
|
|
2699
|
+
}
|
|
2700
|
+
return { value: reviewThreads };
|
|
2701
|
+
}
|
|
2702
|
+
if (pageInfo.hasNextPage !== true) {
|
|
2703
|
+
return { value: reviewThreads };
|
|
2704
|
+
}
|
|
2705
|
+
if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
|
|
2706
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
|
|
2707
|
+
}
|
|
2708
|
+
afterCursor = pageInfo.endCursor;
|
|
2709
|
+
}
|
|
2710
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
|
|
2711
|
+
}
|
|
2712
|
+
async function collectPrReviewEvidence(input) {
|
|
2713
|
+
const parsed = parseGithubPrUrl(input.prUrl);
|
|
2714
|
+
if (!parsed) {
|
|
2715
|
+
throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
|
|
2716
|
+
}
|
|
2717
|
+
const readErrors = [];
|
|
2718
|
+
const viewRead = await runJsonObject(input.command, [
|
|
2719
|
+
"pr",
|
|
2720
|
+
"view",
|
|
2721
|
+
input.prUrl,
|
|
2722
|
+
"--json",
|
|
2723
|
+
"title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
|
|
2724
|
+
], input.projectRoot);
|
|
2725
|
+
if (viewRead.error)
|
|
2726
|
+
readErrors.push(viewRead.error);
|
|
2727
|
+
const view = viewRead.value;
|
|
2728
|
+
if (!Array.isArray(view.statusCheckRollup)) {
|
|
2729
|
+
readErrors.push("gh pr view did not return required statusCheckRollup array");
|
|
2730
|
+
}
|
|
2731
|
+
if (!Array.isArray(view.reviews)) {
|
|
2732
|
+
readErrors.push("gh pr view did not return required reviews array");
|
|
2733
|
+
}
|
|
2734
|
+
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
2735
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
2736
|
+
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
2737
|
+
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
2738
|
+
const provenanceRead = await collectPullRequestProvenance({
|
|
2739
|
+
command: input.command,
|
|
2740
|
+
projectRoot: input.projectRoot,
|
|
2741
|
+
owner: parsed.owner,
|
|
2742
|
+
name: parsed.repo,
|
|
2743
|
+
prNumber: parsed.prNumber
|
|
2744
|
+
});
|
|
2745
|
+
const provenance = provenanceRead.value;
|
|
2746
|
+
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
|
|
2747
|
+
if (reviewCommentsRead.error)
|
|
2748
|
+
readErrors.push(reviewCommentsRead.error);
|
|
2749
|
+
const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
|
|
2750
|
+
const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
|
|
2751
|
+
if (issueCommentsRead.error)
|
|
2752
|
+
readErrors.push(issueCommentsRead.error);
|
|
2753
|
+
const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
|
|
2754
|
+
const reviewThreadsRead = await collectReviewThreads({
|
|
2755
|
+
command: input.command,
|
|
2756
|
+
projectRoot: input.projectRoot,
|
|
2757
|
+
owner: parsed.owner,
|
|
2758
|
+
name: parsed.repo,
|
|
2759
|
+
prNumber: parsed.prNumber
|
|
2760
|
+
});
|
|
2761
|
+
if (reviewThreadsRead.error)
|
|
2762
|
+
readErrors.push(reviewThreadsRead.error);
|
|
2763
|
+
const reviewThreads = reviewThreadsRead.value;
|
|
2764
|
+
const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
|
|
2765
|
+
let greptileCheckDetails = [];
|
|
2766
|
+
if (headSha && greptileRollupChecks.length > 0) {
|
|
2767
|
+
const checkDetailsRead = await collectGreptileCheckDetails({
|
|
2768
|
+
command: input.command,
|
|
2769
|
+
projectRoot: input.projectRoot,
|
|
2770
|
+
repoName: parsed.repoName,
|
|
2771
|
+
headSha
|
|
2772
|
+
});
|
|
2773
|
+
greptileCheckDetails = checkDetailsRead.value;
|
|
2774
|
+
}
|
|
2775
|
+
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
2776
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
2777
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
2778
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
2779
|
+
options: input.greptileApi,
|
|
2780
|
+
repoName: parsed.repoName,
|
|
2781
|
+
prNumber: parsed.prNumber,
|
|
2782
|
+
headSha,
|
|
2783
|
+
baseRefName
|
|
2784
|
+
});
|
|
2785
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
2786
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
2787
|
+
const checkFailures = statusCheckRollup.filter((check) => !isGreptileLabel(checkName(check)) && isFailingCheck(check) && !isAllowedFailure(checkName(check), input.allowedFailures ?? [])).map((check) => `Check failed: ${checkName(check)}${check.detailsUrl || check.link ? ` (${check.detailsUrl ?? check.link})` : ""}`);
|
|
2788
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
2789
|
+
const evidenceBase = {
|
|
2790
|
+
title: firstString(view, ["title"]),
|
|
2791
|
+
body: firstString(view, ["body"]),
|
|
2792
|
+
bodyEditorLogin: provenance.bodyEditorLogin ?? null,
|
|
2793
|
+
bodyLastEditedAt: provenance.bodyLastEditedAt ?? null,
|
|
2794
|
+
headCommittedDate: provenance.headCommittedDate ?? null,
|
|
2795
|
+
reviews,
|
|
2796
|
+
changedFileReviewComments: reviewComments,
|
|
2797
|
+
relevantIssueComments: issueComments,
|
|
2798
|
+
reviewThreads,
|
|
2799
|
+
checks: checksWithGreptileDetails,
|
|
2800
|
+
currentHeadSha: headSha,
|
|
2801
|
+
apiSignals
|
|
2802
|
+
};
|
|
2803
|
+
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
2804
|
+
return {
|
|
2805
|
+
prUrl: input.prUrl,
|
|
2806
|
+
prNumber: parsed.prNumber,
|
|
2807
|
+
repoName: parsed.repoName,
|
|
2808
|
+
title: evidenceBase.title,
|
|
2809
|
+
body: evidenceBase.body,
|
|
2810
|
+
bodyEditorLogin: evidenceBase.bodyEditorLogin,
|
|
2811
|
+
bodyLastEditedAt: evidenceBase.bodyLastEditedAt,
|
|
2812
|
+
headCommittedDate: evidenceBase.headCommittedDate,
|
|
2813
|
+
headSha,
|
|
2814
|
+
headRefName: firstString(view, ["headRefName"]),
|
|
2815
|
+
baseRefName,
|
|
2816
|
+
state: firstString(view, ["state"]),
|
|
2817
|
+
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
2818
|
+
mergeable: firstString(view, ["mergeable"]),
|
|
2819
|
+
mergeStateStatus: firstString(view, ["mergeStateStatus"]),
|
|
2820
|
+
reviewDecision: firstString(view, ["reviewDecision"]),
|
|
2821
|
+
reviews,
|
|
2822
|
+
reviewThreads,
|
|
2823
|
+
changedFileReviewComments: reviewComments,
|
|
2824
|
+
relevantIssueComments: issueComments,
|
|
2825
|
+
statusCheckRollup: checksWithGreptileDetails,
|
|
2826
|
+
checkFailures,
|
|
2827
|
+
pendingChecks,
|
|
2828
|
+
readErrors,
|
|
2829
|
+
greptile
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
2833
|
+
const normalized = value.trim();
|
|
2834
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
2835
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
2836
|
+
}
|
|
2837
|
+
function evaluateEvidence(evidence) {
|
|
2838
|
+
const reasonDetails = [];
|
|
2839
|
+
const warnings = [];
|
|
2840
|
+
const seen = new Set;
|
|
2841
|
+
const addReason = (reason) => {
|
|
2842
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
2843
|
+
const key = `${capped.code}:${capped.message}`;
|
|
2844
|
+
if (seen.has(key))
|
|
2845
|
+
return;
|
|
2846
|
+
seen.add(key);
|
|
2847
|
+
reasonDetails.push(capped);
|
|
2848
|
+
};
|
|
2849
|
+
const greptile = evidence.greptile;
|
|
2850
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
2851
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
2852
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
2853
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
2854
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
2855
|
+
for (const error of evidence.readErrors) {
|
|
2856
|
+
addReason({
|
|
2857
|
+
code: "read_error",
|
|
2858
|
+
reasonClass: "reject",
|
|
2859
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
2860
|
+
suggestedAction: "needs_attention",
|
|
2861
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
2862
|
+
headSha: evidence.headSha || null
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
if (!evidence.headSha) {
|
|
2866
|
+
addReason({
|
|
2867
|
+
code: "missing_head_sha",
|
|
2868
|
+
reasonClass: "reject",
|
|
2869
|
+
surface: "github",
|
|
2870
|
+
suggestedAction: "needs_attention",
|
|
2871
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
2872
|
+
headSha: null
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
for (const failure of evidence.checkFailures) {
|
|
2876
|
+
addReason({
|
|
2877
|
+
code: "ci_failed",
|
|
2878
|
+
reasonClass: "reject",
|
|
2879
|
+
surface: "ci",
|
|
2880
|
+
suggestedAction: "fix",
|
|
2881
|
+
message: failure,
|
|
2882
|
+
headSha: evidence.headSha || null
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
2886
|
+
addReason({
|
|
2887
|
+
code: "check_pending",
|
|
2888
|
+
reasonClass: "pending",
|
|
2889
|
+
surface: "ci",
|
|
2890
|
+
suggestedAction: "wait",
|
|
2891
|
+
message: pendingCheck,
|
|
2892
|
+
headSha: evidence.headSha || null
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
2896
|
+
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
2897
|
+
addReason({
|
|
2898
|
+
code: "review_decision_blocking",
|
|
2899
|
+
reasonClass: "reject",
|
|
2900
|
+
surface: "review",
|
|
2901
|
+
suggestedAction: "fix",
|
|
2902
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
2903
|
+
headSha: evidence.headSha || null
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
2906
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
2907
|
+
addReason({
|
|
2908
|
+
code: "review_thread_unresolved",
|
|
2909
|
+
reasonClass: "reject",
|
|
2910
|
+
surface: "review",
|
|
2911
|
+
suggestedAction: "fix",
|
|
2912
|
+
message: thread,
|
|
2913
|
+
headSha: evidence.headSha || null
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
if (greptile.mapping === "missing") {
|
|
2917
|
+
addReason({
|
|
2918
|
+
code: "greptile_missing",
|
|
2919
|
+
reasonClass: "pending",
|
|
2920
|
+
surface: "greptile",
|
|
2921
|
+
suggestedAction: "wait",
|
|
2922
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
2923
|
+
headSha: evidence.headSha || null
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
2927
|
+
addReason({
|
|
2928
|
+
code: "greptile_stale",
|
|
2929
|
+
reasonClass: "pending",
|
|
2930
|
+
surface: "greptile",
|
|
2931
|
+
suggestedAction: "wait",
|
|
2932
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
2933
|
+
headSha: evidence.headSha || null,
|
|
2934
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
2938
|
+
addReason({
|
|
2939
|
+
code: "greptile_pending",
|
|
2940
|
+
reasonClass: "pending",
|
|
2941
|
+
surface: "greptile",
|
|
2942
|
+
suggestedAction: "wait",
|
|
2943
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
2944
|
+
headSha: evidence.headSha || null,
|
|
2945
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
2949
|
+
addReason({
|
|
2950
|
+
code: "greptile_api_status_unknown",
|
|
2951
|
+
reasonClass: "reject",
|
|
2952
|
+
surface: "greptile",
|
|
2953
|
+
suggestedAction: "needs_attention",
|
|
2954
|
+
message: `Greptile API/MCP review status is unknown; merge requires a known terminal APPROVED/COMPLETED 5/5 result or a known conservative status${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
2955
|
+
headSha: evidence.headSha || null,
|
|
2956
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
2957
|
+
});
|
|
2958
|
+
}
|
|
2959
|
+
if (!greptile.completed) {
|
|
2960
|
+
addReason({
|
|
2961
|
+
code: "greptile_pending",
|
|
2962
|
+
reasonClass: "pending",
|
|
2963
|
+
surface: "greptile",
|
|
2964
|
+
suggestedAction: "wait",
|
|
2965
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
2966
|
+
headSha: evidence.headSha || null,
|
|
2967
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
2968
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
if (!greptile.fresh) {
|
|
2971
|
+
addReason({
|
|
2972
|
+
code: "greptile_not_current_head",
|
|
2973
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
2974
|
+
surface: "greptile",
|
|
2975
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
2976
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
2977
|
+
headSha: evidence.headSha || null,
|
|
2978
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
2982
|
+
addReason({
|
|
2983
|
+
code: "greptile_score_not_5",
|
|
2984
|
+
reasonClass: "reject",
|
|
2985
|
+
surface: "greptile",
|
|
2986
|
+
suggestedAction: "fix",
|
|
2987
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
2988
|
+
headSha: evidence.headSha || null,
|
|
2989
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
2992
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
2993
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
2994
|
+
addReason({
|
|
2995
|
+
code: "greptile_score_missing",
|
|
2996
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
2997
|
+
surface: "greptile",
|
|
2998
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
2999
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
3000
|
+
headSha: evidence.headSha || null,
|
|
3001
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
if (greptile.mapping === "unproven") {
|
|
3005
|
+
addReason({
|
|
3006
|
+
code: "greptile_mapping_unproven",
|
|
3007
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
3008
|
+
surface: "greptile",
|
|
3009
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
3010
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
3011
|
+
headSha: evidence.headSha || null,
|
|
3012
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
3013
|
+
});
|
|
3014
|
+
}
|
|
3015
|
+
for (const blocker of greptile.blockers) {
|
|
3016
|
+
addReason({
|
|
3017
|
+
code: "greptile_blocker_text",
|
|
3018
|
+
reasonClass: "reject",
|
|
3019
|
+
surface: "greptile",
|
|
3020
|
+
suggestedAction: "fix",
|
|
3021
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
3022
|
+
headSha: evidence.headSha || null,
|
|
3023
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
for (const comment of greptile.unresolvedComments) {
|
|
3027
|
+
addReason({
|
|
3028
|
+
code: "greptile_unresolved_comment",
|
|
3029
|
+
reasonClass: "reject",
|
|
3030
|
+
surface: "greptile",
|
|
3031
|
+
suggestedAction: "fix",
|
|
3032
|
+
message: comment,
|
|
3033
|
+
headSha: evidence.headSha || null,
|
|
3034
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
if (!greptile.approved)
|
|
3038
|
+
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
3039
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
3040
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
3041
|
+
}
|
|
3042
|
+
function evaluateStrictPrMergeGate(evidence) {
|
|
3043
|
+
const evaluated = evaluateEvidence(evidence);
|
|
3044
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
3045
|
+
return {
|
|
3046
|
+
approved,
|
|
3047
|
+
pending: evaluated.pending,
|
|
3048
|
+
reasons: evaluated.reasons,
|
|
3049
|
+
reasonDetails: evaluated.reasonDetails,
|
|
3050
|
+
warnings: evaluated.warnings,
|
|
3051
|
+
actionableFeedback: evaluated.reasons,
|
|
3052
|
+
evidence
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
|
|
1759
3056
|
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
1760
3057
|
var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
1761
3058
|
"changed-files.txt",
|
|
@@ -1768,12 +3065,12 @@ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
|
1768
3065
|
"validation-summary.json"
|
|
1769
3066
|
]);
|
|
1770
3067
|
function readPrMetadata(projectRoot, taskId) {
|
|
1771
|
-
const path =
|
|
1772
|
-
if (!
|
|
3068
|
+
const path = resolve13(artifactDirForId(projectRoot, taskId), "pr-state.json");
|
|
3069
|
+
if (!existsSync11(path)) {
|
|
1773
3070
|
return [];
|
|
1774
3071
|
}
|
|
1775
3072
|
try {
|
|
1776
|
-
const parsed = JSON.parse(
|
|
3073
|
+
const parsed = JSON.parse(readFileSync8(path, "utf-8"));
|
|
1777
3074
|
if (!parsed || typeof parsed !== "object") {
|
|
1778
3075
|
return [];
|
|
1779
3076
|
}
|
|
@@ -1799,11 +3096,11 @@ async function verifyTask(options) {
|
|
|
1799
3096
|
const taskId = options.taskId;
|
|
1800
3097
|
const normalizedTaskId = lookupTask(options.projectRoot, taskId);
|
|
1801
3098
|
const artifactDir = artifactDirForId(options.projectRoot, taskId);
|
|
1802
|
-
|
|
1803
|
-
const validationSummaryPath =
|
|
1804
|
-
const reviewFeedbackPath =
|
|
1805
|
-
const reviewStatePath =
|
|
1806
|
-
const greptileRawPath =
|
|
3099
|
+
mkdirSync6(artifactDir, { recursive: true });
|
|
3100
|
+
const validationSummaryPath = resolve14(artifactDir, "validation-summary.json");
|
|
3101
|
+
const reviewFeedbackPath = resolve14(artifactDir, "review-feedback.md");
|
|
3102
|
+
const reviewStatePath = resolve14(artifactDir, "review-state.json");
|
|
3103
|
+
const greptileRawPath = resolve14(artifactDir, "review-greptile-raw.json");
|
|
1807
3104
|
const prStates = readPrMetadata(options.projectRoot, taskId);
|
|
1808
3105
|
const prState = prStates[0] || null;
|
|
1809
3106
|
const localReasons = [];
|
|
@@ -1815,7 +3112,7 @@ async function verifyTask(options) {
|
|
|
1815
3112
|
if (!normalizedTaskId && !await hasConfiguredSourceTask(options.projectRoot, taskId)) {
|
|
1816
3113
|
localReasons.push(`[Task Config] Unknown task id '${taskId}' in task-config or configured task source.`);
|
|
1817
3114
|
}
|
|
1818
|
-
if (!
|
|
3115
|
+
if (!existsSync12(validationSummaryPath)) {
|
|
1819
3116
|
localReasons.push(`[Artifact Quality] validation-summary.json not found at ${validationSummaryPath}.`);
|
|
1820
3117
|
} else {
|
|
1821
3118
|
const summary = await parseValidationSummary(validationSummaryPath);
|
|
@@ -1824,13 +3121,13 @@ async function verifyTask(options) {
|
|
|
1824
3121
|
}
|
|
1825
3122
|
}
|
|
1826
3123
|
for (const file of ["task-result.json", "decision-log.md", "next-actions.md", "changed-files.txt"]) {
|
|
1827
|
-
const requiredPath =
|
|
1828
|
-
if (!
|
|
3124
|
+
const requiredPath = resolve14(artifactDir, file);
|
|
3125
|
+
if (!existsSync12(requiredPath)) {
|
|
1829
3126
|
localReasons.push(`[Artifact Quality] Missing required artifact file: ${requiredPath}`);
|
|
1830
3127
|
}
|
|
1831
3128
|
}
|
|
1832
|
-
const taskResultPath =
|
|
1833
|
-
if (
|
|
3129
|
+
const taskResultPath = resolve14(artifactDir, "task-result.json");
|
|
3130
|
+
if (existsSync12(taskResultPath)) {
|
|
1834
3131
|
const taskResult = await readJsonFile2(taskResultPath);
|
|
1835
3132
|
const artifactStatus = typeof taskResult?.status === "string" ? taskResult.status.trim().toLowerCase() : "";
|
|
1836
3133
|
if (artifactStatus === "partial") {
|
|
@@ -1843,8 +3140,8 @@ async function verifyTask(options) {
|
|
|
1843
3140
|
localReasons.push("[Artifact Quality] task-result.json next actions indicate remaining implementation scope.");
|
|
1844
3141
|
}
|
|
1845
3142
|
}
|
|
1846
|
-
const nextActionsPath =
|
|
1847
|
-
if (
|
|
3143
|
+
const nextActionsPath = resolve14(artifactDir, "next-actions.md");
|
|
3144
|
+
if (existsSync12(nextActionsPath)) {
|
|
1848
3145
|
const nextActionsContent = await Bun.file(nextActionsPath).text();
|
|
1849
3146
|
if (nextActionsContent.includes("TODO: Replace this scaffold") || nextActionsContent.includes("bd-<downstream-task-id>")) {
|
|
1850
3147
|
localReasons.push("[Artifact Quality] next-actions.md still contains scaffold placeholder text. Replace with real recommendations.");
|
|
@@ -1857,12 +3154,6 @@ async function verifyTask(options) {
|
|
|
1857
3154
|
if (sourceCloseoutIssueId) {
|
|
1858
3155
|
localReasons.push(...evaluateGithubSourceIssuePrCloseout(options.projectRoot, prStates, sourceCloseoutIssueId));
|
|
1859
3156
|
}
|
|
1860
|
-
const pluginResults = await options.plugins.runValidators(taskId);
|
|
1861
|
-
for (const result of pluginResults) {
|
|
1862
|
-
if (!result.passed) {
|
|
1863
|
-
localReasons.push(`[Plugin Validator] ${result.id}: ${result.summary}`);
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
3157
|
const reviewMode = await loadReviewMode(paths.reviewProfilePath, process.env.AI_REVIEW_MODE || "advisory");
|
|
1867
3158
|
const reviewProvider = await loadReviewProvider(paths.reviewProfilePath, process.env.AI_REVIEW_PROVIDER || "greptile");
|
|
1868
3159
|
if (!options.skipAiReview && localReasons.length === 0 && reviewProvider === "greptile" && reviewMode !== "off") {
|
|
@@ -1881,7 +3172,7 @@ async function verifyTask(options) {
|
|
|
1881
3172
|
aiReasons.push(`[AI Review] Required mode needs a completed Greptile approval; current verdict is ${ai.verdict}.`);
|
|
1882
3173
|
}
|
|
1883
3174
|
if (persistArtifacts && ai.rawResponse) {
|
|
1884
|
-
|
|
3175
|
+
writeFileSync7(greptileRawPath, `${ai.rawResponse}
|
|
1885
3176
|
`, "utf-8");
|
|
1886
3177
|
}
|
|
1887
3178
|
} else if (!options.skipAiReview && reviewMode === "off") {
|
|
@@ -2220,7 +3511,7 @@ function isAcceptedValidationSummary(summary) {
|
|
|
2220
3511
|
return summary.status === "skipped" && summary.total === 0 && summary.failed === 0;
|
|
2221
3512
|
}
|
|
2222
3513
|
async function loadReviewMode(reviewProfilePath, fallback) {
|
|
2223
|
-
const parsed =
|
|
3514
|
+
const parsed = existsSync12(reviewProfilePath) ? await readJsonFile2(reviewProfilePath) : null;
|
|
2224
3515
|
const mode = parsed?.mode;
|
|
2225
3516
|
if (mode === "off" || mode === "advisory" || mode === "required") {
|
|
2226
3517
|
return mode;
|
|
@@ -2231,7 +3522,7 @@ async function loadReviewMode(reviewProfilePath, fallback) {
|
|
|
2231
3522
|
return "advisory";
|
|
2232
3523
|
}
|
|
2233
3524
|
async function loadReviewProvider(reviewProfilePath, fallback) {
|
|
2234
|
-
const parsed =
|
|
3525
|
+
const parsed = existsSync12(reviewProfilePath) ? await readJsonFile2(reviewProfilePath) : null;
|
|
2235
3526
|
const provider = parsed?.provider;
|
|
2236
3527
|
if (typeof provider === "string" && provider.trim().length > 0) {
|
|
2237
3528
|
return provider;
|
|
@@ -2390,7 +3681,7 @@ function writeFeedbackFile(options) {
|
|
|
2390
3681
|
if (options.aiRawFeedback) {
|
|
2391
3682
|
lines.push("## Raw Reviewer Feedback", "", "```text", options.aiRawFeedback, "```", "");
|
|
2392
3683
|
}
|
|
2393
|
-
|
|
3684
|
+
writeFileSync7(options.output, `${lines.join(`
|
|
2394
3685
|
`)}
|
|
2395
3686
|
`, "utf-8");
|
|
2396
3687
|
}
|
|
@@ -2407,7 +3698,7 @@ function writeReviewStateFile(options) {
|
|
|
2407
3698
|
ai_warnings: options.aiWarnings,
|
|
2408
3699
|
updated_at: nowIso()
|
|
2409
3700
|
};
|
|
2410
|
-
|
|
3701
|
+
writeFileSync7(options.output, `${JSON.stringify(payload, null, 2)}
|
|
2411
3702
|
`, "utf-8");
|
|
2412
3703
|
}
|
|
2413
3704
|
async function runGreptileReviewForPr(options) {
|
|
@@ -2589,7 +3880,8 @@ async function runGreptileReviewForPr(options) {
|
|
|
2589
3880
|
}
|
|
2590
3881
|
};
|
|
2591
3882
|
}
|
|
2592
|
-
|
|
3883
|
+
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3884
|
+
if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/i.test(blockerScanBody)) {
|
|
2593
3885
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
2594
3886
|
return {
|
|
2595
3887
|
verdict: "REJECT",
|
|
@@ -2605,44 +3897,79 @@ async function runGreptileReviewForPr(options) {
|
|
|
2605
3897
|
}
|
|
2606
3898
|
};
|
|
2607
3899
|
}
|
|
2608
|
-
if (score) {
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
3900
|
+
if (score?.scale === 5 && score.value < 5) {
|
|
3901
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
|
|
3902
|
+
return {
|
|
3903
|
+
verdict: "REJECT",
|
|
3904
|
+
feedback,
|
|
3905
|
+
reasons,
|
|
3906
|
+
warnings,
|
|
3907
|
+
rawPayload: {
|
|
3908
|
+
pr: options.prState,
|
|
3909
|
+
codeReviews: reviewsPayload,
|
|
3910
|
+
selectedReview,
|
|
3911
|
+
reviewDetails,
|
|
3912
|
+
comments: commentsPayload,
|
|
3913
|
+
score
|
|
3914
|
+
}
|
|
3915
|
+
};
|
|
3916
|
+
}
|
|
3917
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
3918
|
+
let strictGate = null;
|
|
3919
|
+
try {
|
|
3920
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
3921
|
+
projectRoot: options.projectRoot,
|
|
3922
|
+
taskId: options.taskId,
|
|
3923
|
+
prUrl,
|
|
3924
|
+
apiSignals: [{
|
|
3925
|
+
id: selectedReview.id,
|
|
3926
|
+
body: reviewBody,
|
|
3927
|
+
reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
|
|
3928
|
+
status: selectedReview.status
|
|
3929
|
+
}]
|
|
3930
|
+
});
|
|
3931
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
3932
|
+
} catch (error) {
|
|
3933
|
+
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3934
|
+
return {
|
|
3935
|
+
verdict: "REJECT",
|
|
3936
|
+
feedback,
|
|
3937
|
+
reasons,
|
|
3938
|
+
warnings,
|
|
3939
|
+
rawPayload: {
|
|
3940
|
+
pr: options.prState,
|
|
3941
|
+
codeReviews: reviewsPayload,
|
|
3942
|
+
selectedReview,
|
|
3943
|
+
reviewDetails,
|
|
3944
|
+
comments: commentsPayload,
|
|
3945
|
+
score
|
|
3946
|
+
}
|
|
3947
|
+
};
|
|
3948
|
+
}
|
|
3949
|
+
if (!strictGate.approved) {
|
|
3950
|
+
return {
|
|
3951
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
3952
|
+
feedback,
|
|
3953
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
3954
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
3955
|
+
rawPayload: {
|
|
3956
|
+
pr: options.prState,
|
|
3957
|
+
codeReviews: reviewsPayload,
|
|
3958
|
+
selectedReview,
|
|
3959
|
+
reviewDetails,
|
|
3960
|
+
comments: commentsPayload,
|
|
3961
|
+
score,
|
|
3962
|
+
strictGate: {
|
|
3963
|
+
approved: strictGate.approved,
|
|
3964
|
+
pending: strictGate.pending,
|
|
3965
|
+
reasons: strictGate.reasons,
|
|
3966
|
+
reasonDetails: strictGate.reasonDetails,
|
|
3967
|
+
warnings: strictGate.warnings,
|
|
3968
|
+
greptile: strictGate.evidence.greptile,
|
|
3969
|
+
readErrors: strictGate.evidence.readErrors
|
|
2640
3970
|
}
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
if (score.scale === 5 && score.value < 5) {
|
|
2644
|
-
warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
|
|
2645
|
-
}
|
|
3971
|
+
}
|
|
3972
|
+
};
|
|
2646
3973
|
}
|
|
2647
3974
|
return {
|
|
2648
3975
|
verdict: "APPROVE",
|
|
@@ -2654,7 +3981,16 @@ async function runGreptileReviewForPr(options) {
|
|
|
2654
3981
|
codeReviews: reviewsPayload,
|
|
2655
3982
|
selectedReview,
|
|
2656
3983
|
reviewDetails,
|
|
2657
|
-
comments: commentsPayload
|
|
3984
|
+
comments: commentsPayload,
|
|
3985
|
+
strictGate: {
|
|
3986
|
+
approved: strictGate.approved,
|
|
3987
|
+
pending: strictGate.pending,
|
|
3988
|
+
reasons: strictGate.reasons,
|
|
3989
|
+
reasonDetails: strictGate.reasonDetails,
|
|
3990
|
+
warnings: strictGate.warnings,
|
|
3991
|
+
greptile: strictGate.evidence.greptile,
|
|
3992
|
+
readErrors: strictGate.evidence.readErrors
|
|
3993
|
+
}
|
|
2658
3994
|
}
|
|
2659
3995
|
};
|
|
2660
3996
|
}
|
|
@@ -2678,7 +4014,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
2678
4014
|
let threads = [];
|
|
2679
4015
|
let actionableThreads = [];
|
|
2680
4016
|
let checkRollup = [];
|
|
2681
|
-
let
|
|
4017
|
+
let checkState2 = { pending: false, completed: false };
|
|
2682
4018
|
for (let attempt = 0;; attempt += 1) {
|
|
2683
4019
|
reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
|
|
2684
4020
|
selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
|
|
@@ -2687,15 +4023,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
2687
4023
|
threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
|
|
2688
4024
|
actionableThreads = filterActionableGithubGreptileThreads(threads);
|
|
2689
4025
|
checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
2690
|
-
|
|
2691
|
-
const
|
|
4026
|
+
checkState2 = classifyGithubGreptileCheckState(checkRollup);
|
|
4027
|
+
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
2692
4028
|
if (!shouldContinueGithubGreptileFallbackPolling({
|
|
2693
4029
|
attempt,
|
|
2694
4030
|
pollAttempts: options.pollAttempts,
|
|
2695
|
-
checkState,
|
|
4031
|
+
checkState: checkState2,
|
|
2696
4032
|
fallbackReview,
|
|
2697
4033
|
selectedReview,
|
|
2698
|
-
approvedViaReviewedAncestor
|
|
4034
|
+
approvedViaReviewedAncestor
|
|
2699
4035
|
})) {
|
|
2700
4036
|
break;
|
|
2701
4037
|
}
|
|
@@ -2723,7 +4059,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
2723
4059
|
].filter(Boolean).join(`
|
|
2724
4060
|
`);
|
|
2725
4061
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
2726
|
-
if (
|
|
4062
|
+
if (checkState2.pending) {
|
|
2727
4063
|
return {
|
|
2728
4064
|
verdict: "SKIP",
|
|
2729
4065
|
feedback,
|
|
@@ -2734,34 +4070,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
2734
4070
|
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
2735
4071
|
};
|
|
2736
4072
|
}
|
|
2737
|
-
const
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
};
|
|
2748
|
-
}
|
|
2749
|
-
return {
|
|
2750
|
-
verdict: "SKIP",
|
|
2751
|
-
feedback,
|
|
2752
|
-
reasons: [
|
|
2753
|
-
`[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
|
|
2754
|
-
],
|
|
2755
|
-
warnings,
|
|
2756
|
-
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
2757
|
-
};
|
|
2758
|
-
}
|
|
2759
|
-
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
2760
|
-
if (actionableThreads.length > 0) {
|
|
4073
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
4074
|
+
let strictGate;
|
|
4075
|
+
try {
|
|
4076
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
4077
|
+
projectRoot: options.projectRoot,
|
|
4078
|
+
taskId: options.taskId,
|
|
4079
|
+
prUrl
|
|
4080
|
+
});
|
|
4081
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
4082
|
+
} catch (error) {
|
|
2761
4083
|
return {
|
|
2762
4084
|
verdict: "REJECT",
|
|
2763
4085
|
feedback,
|
|
2764
|
-
reasons:
|
|
4086
|
+
reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
|
|
2765
4087
|
warnings,
|
|
2766
4088
|
rawPayload: {
|
|
2767
4089
|
pr: options.prState,
|
|
@@ -2774,44 +4096,31 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
2774
4096
|
}
|
|
2775
4097
|
};
|
|
2776
4098
|
}
|
|
2777
|
-
if (!
|
|
2778
|
-
if (approvedViaCompletedCheck) {
|
|
2779
|
-
warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no fresh Greptile GitHub review on the current head, but the Greptile check completed successfully and all Greptile threads are resolved.`);
|
|
2780
|
-
return {
|
|
2781
|
-
verdict: "APPROVE",
|
|
2782
|
-
feedback,
|
|
2783
|
-
reasons: [],
|
|
2784
|
-
warnings,
|
|
2785
|
-
rawPayload: {
|
|
2786
|
-
pr: options.prState,
|
|
2787
|
-
selectedReview: fallbackReview,
|
|
2788
|
-
reviews,
|
|
2789
|
-
threads,
|
|
2790
|
-
checkRollup,
|
|
2791
|
-
...buildGithubGreptileFallbackRawPayload(options)
|
|
2792
|
-
}
|
|
2793
|
-
};
|
|
2794
|
-
}
|
|
4099
|
+
if (!strictGate.approved) {
|
|
2795
4100
|
return {
|
|
2796
|
-
verdict: "SKIP",
|
|
4101
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
2797
4102
|
feedback,
|
|
2798
|
-
reasons: [
|
|
2799
|
-
|
|
2800
|
-
],
|
|
2801
|
-
warnings,
|
|
4103
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
4104
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
2802
4105
|
rawPayload: {
|
|
2803
4106
|
pr: options.prState,
|
|
2804
4107
|
selectedReview: fallbackReview,
|
|
2805
4108
|
reviews,
|
|
2806
4109
|
threads,
|
|
2807
4110
|
checkRollup,
|
|
4111
|
+
actionableThreads,
|
|
4112
|
+
strictGate: {
|
|
4113
|
+
approved: strictGate.approved,
|
|
4114
|
+
pending: strictGate.pending,
|
|
4115
|
+
reasons: strictGate.reasons,
|
|
4116
|
+
reasonDetails: strictGate.reasonDetails,
|
|
4117
|
+
warnings: strictGate.warnings,
|
|
4118
|
+
greptile: strictGate.evidence.greptile
|
|
4119
|
+
},
|
|
2808
4120
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
2809
4121
|
}
|
|
2810
4122
|
};
|
|
2811
4123
|
}
|
|
2812
|
-
if (approvedViaReviewedAncestor) {
|
|
2813
|
-
warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no fresh Greptile review on the current head, but the latest reviewed commit is an ancestor and all Greptile threads are resolved.`);
|
|
2814
|
-
}
|
|
2815
4124
|
return {
|
|
2816
4125
|
verdict: "APPROVE",
|
|
2817
4126
|
feedback,
|
|
@@ -2823,6 +4132,14 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
2823
4132
|
reviews,
|
|
2824
4133
|
threads,
|
|
2825
4134
|
checkRollup,
|
|
4135
|
+
strictGate: {
|
|
4136
|
+
approved: strictGate.approved,
|
|
4137
|
+
pending: strictGate.pending,
|
|
4138
|
+
reasons: strictGate.reasons,
|
|
4139
|
+
reasonDetails: strictGate.reasonDetails,
|
|
4140
|
+
warnings: strictGate.warnings,
|
|
4141
|
+
greptile: strictGate.evidence.greptile
|
|
4142
|
+
},
|
|
2826
4143
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
2827
4144
|
}
|
|
2828
4145
|
};
|
|
@@ -2935,19 +4252,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
2935
4252
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
2936
4253
|
return true;
|
|
2937
4254
|
}
|
|
2938
|
-
return
|
|
4255
|
+
return false;
|
|
2939
4256
|
}
|
|
2940
4257
|
function shouldContinueGreptileMcpPolling(options) {
|
|
2941
4258
|
if (options.githubCheckState.completed) {
|
|
2942
4259
|
return false;
|
|
2943
4260
|
}
|
|
4261
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
4262
|
+
return false;
|
|
4263
|
+
}
|
|
2944
4264
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
2945
4265
|
return true;
|
|
2946
4266
|
}
|
|
2947
|
-
return
|
|
4267
|
+
return true;
|
|
2948
4268
|
}
|
|
2949
4269
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
2950
4270
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
4271
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
4272
|
+
return false;
|
|
4273
|
+
}
|
|
2951
4274
|
if (waitingForVisiblePendingReview) {
|
|
2952
4275
|
return true;
|
|
2953
4276
|
}
|
|
@@ -3008,6 +4331,20 @@ function runGhJson(projectRoot, args) {
|
|
|
3008
4331
|
throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
|
|
3009
4332
|
}
|
|
3010
4333
|
}
|
|
4334
|
+
async function collectStrictPrEvidenceForVerifier(input) {
|
|
4335
|
+
return collectPrReviewEvidence({
|
|
4336
|
+
projectRoot: input.projectRoot,
|
|
4337
|
+
prUrl: input.prUrl,
|
|
4338
|
+
taskId: input.taskId,
|
|
4339
|
+
runId: "verifier",
|
|
4340
|
+
cycle: 0,
|
|
4341
|
+
apiSignals: input.apiSignals ?? [],
|
|
4342
|
+
command: async (args, options) => {
|
|
4343
|
+
const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
4344
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
4345
|
+
}
|
|
4346
|
+
});
|
|
4347
|
+
}
|
|
3011
4348
|
function deriveRepoName(projectRoot, prState) {
|
|
3012
4349
|
const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
|
|
3013
4350
|
if (fromUrl?.[1]) {
|
|
@@ -3022,8 +4359,9 @@ function resolvePrHeadSha(projectRoot, prState) {
|
|
|
3022
4359
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
3023
4360
|
return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
3024
4361
|
}
|
|
3025
|
-
function
|
|
3026
|
-
|
|
4362
|
+
function isGreptileGithubLogin2(login) {
|
|
4363
|
+
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
4364
|
+
return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
|
|
3027
4365
|
}
|
|
3028
4366
|
function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
|
|
3029
4367
|
const matching = sortGithubGreptileReviews(reviews);
|
|
@@ -3040,7 +4378,7 @@ function pickLatestGithubGreptileReview(reviews) {
|
|
|
3040
4378
|
return sortGithubGreptileReviews(reviews)[0] || null;
|
|
3041
4379
|
}
|
|
3042
4380
|
function sortGithubGreptileReviews(reviews) {
|
|
3043
|
-
return reviews.filter((review) =>
|
|
4381
|
+
return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
|
|
3044
4382
|
}
|
|
3045
4383
|
function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
3046
4384
|
const response = runGhJson(projectRoot, [
|
|
@@ -3113,31 +4451,8 @@ function classifyGithubGreptileCheckState(checks) {
|
|
|
3113
4451
|
}
|
|
3114
4452
|
return { pending: false, completed: false };
|
|
3115
4453
|
}
|
|
3116
|
-
function isGithubGreptileCheckApproved(
|
|
3117
|
-
|
|
3118
|
-
const label = (check.name || check.context || "").toLowerCase();
|
|
3119
|
-
return label.includes("greptile");
|
|
3120
|
-
});
|
|
3121
|
-
if (greptileChecks.length === 0) {
|
|
3122
|
-
return false;
|
|
3123
|
-
}
|
|
3124
|
-
for (const check of greptileChecks) {
|
|
3125
|
-
if ((check.__typename || "") === "CheckRun") {
|
|
3126
|
-
if ((check.status || "").toUpperCase() !== "COMPLETED") {
|
|
3127
|
-
return false;
|
|
3128
|
-
}
|
|
3129
|
-
const conclusion = (check.conclusion || "").toUpperCase();
|
|
3130
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
|
|
3131
|
-
return false;
|
|
3132
|
-
}
|
|
3133
|
-
continue;
|
|
3134
|
-
}
|
|
3135
|
-
const state = (check.state || "").toUpperCase();
|
|
3136
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
|
|
3137
|
-
return false;
|
|
3138
|
-
}
|
|
3139
|
-
}
|
|
3140
|
-
return true;
|
|
4454
|
+
function isGithubGreptileCheckApproved(_checks) {
|
|
4455
|
+
return false;
|
|
3141
4456
|
}
|
|
3142
4457
|
function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
|
|
3143
4458
|
const [owner, name] = repoName.split("/");
|
|
@@ -3164,7 +4479,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
3164
4479
|
return [];
|
|
3165
4480
|
}
|
|
3166
4481
|
const comments = thread.comments?.nodes || [];
|
|
3167
|
-
const latestGreptileComment = [...comments].reverse().find((comment) =>
|
|
4482
|
+
const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
|
|
3168
4483
|
if (!latestGreptileComment?.path?.trim()) {
|
|
3169
4484
|
return [];
|
|
3170
4485
|
}
|
|
@@ -3173,7 +4488,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
3173
4488
|
}
|
|
3174
4489
|
function resolvePrRepoRoot(projectRoot, prState) {
|
|
3175
4490
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
3176
|
-
if (prState.target === "monorepo" && runtimeWorkspace &&
|
|
4491
|
+
if (prState.target === "monorepo" && runtimeWorkspace && existsSync12(resolve14(runtimeWorkspace, ".git"))) {
|
|
3177
4492
|
return runtimeWorkspace;
|
|
3178
4493
|
}
|
|
3179
4494
|
const paths = resolveHarnessPaths(projectRoot);
|
|
@@ -3186,11 +4501,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
3186
4501
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
3187
4502
|
return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
3188
4503
|
}
|
|
3189
|
-
function stripHtml(input) {
|
|
3190
|
-
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
3191
|
-
|
|
3192
|
-
`).trim();
|
|
3193
|
-
}
|
|
3194
4504
|
function summarizeComment(input) {
|
|
3195
4505
|
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
3196
4506
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
@@ -3199,31 +4509,14 @@ function asGreptileInfrastructureWarning(reason) {
|
|
|
3199
4509
|
return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
|
|
3200
4510
|
}
|
|
3201
4511
|
function isAiReviewApproved(input) {
|
|
4512
|
+
if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
|
|
4513
|
+
return false;
|
|
4514
|
+
}
|
|
3202
4515
|
if (input.reviewMode !== "required") {
|
|
3203
4516
|
return true;
|
|
3204
4517
|
}
|
|
3205
4518
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
3206
4519
|
}
|
|
3207
|
-
function parseGreptileScore(input) {
|
|
3208
|
-
const text = stripHtml(input);
|
|
3209
|
-
const patterns = [
|
|
3210
|
-
/confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
3211
|
-
/\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
3212
|
-
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
|
|
3213
|
-
];
|
|
3214
|
-
for (const pattern of patterns) {
|
|
3215
|
-
const match = pattern.exec(text);
|
|
3216
|
-
if (!match) {
|
|
3217
|
-
continue;
|
|
3218
|
-
}
|
|
3219
|
-
const value = Number.parseInt(match[1] || "", 10);
|
|
3220
|
-
const scale = Number.parseInt(match[2] || "", 10);
|
|
3221
|
-
if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
|
|
3222
|
-
return { value, scale };
|
|
3223
|
-
}
|
|
3224
|
-
}
|
|
3225
|
-
return null;
|
|
3226
|
-
}
|
|
3227
4520
|
var __testOnly = {
|
|
3228
4521
|
asGreptileInfrastructureWarning,
|
|
3229
4522
|
callGreptileMcpToolWithTimeout,
|