@h-rig/runtime 0.0.6-alpha.3 → 0.0.6-alpha.31
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/task-ops.ts
|
|
3
|
-
import { appendFileSync, existsSync as
|
|
4
|
-
import { resolve as
|
|
3
|
+
import { appendFileSync, existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
4
|
+
import { resolve as resolve24 } from "path";
|
|
5
5
|
|
|
6
6
|
// packages/runtime/src/build-time-config.ts
|
|
7
7
|
function normalizeBuildConfig(value) {
|
|
@@ -651,6 +651,49 @@ function safeReadJson(path) {
|
|
|
651
651
|
}
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
+
// packages/runtime/src/control-plane/skill-materializer.ts
|
|
655
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
656
|
+
import { resolve as resolve6 } from "path";
|
|
657
|
+
import { loadSkill } from "@rig/skill-loader";
|
|
658
|
+
var MARKER_FILENAME = ".rig-plugin";
|
|
659
|
+
function skillDirName(id) {
|
|
660
|
+
return id.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
661
|
+
}
|
|
662
|
+
async function materializeSkills(projectRoot, entries) {
|
|
663
|
+
const skillsRoot = resolve6(projectRoot, ".pi", "skills");
|
|
664
|
+
if (existsSync4(skillsRoot)) {
|
|
665
|
+
for (const name of readdirSync(skillsRoot)) {
|
|
666
|
+
const dir = resolve6(skillsRoot, name);
|
|
667
|
+
if (existsSync4(resolve6(dir, MARKER_FILENAME))) {
|
|
668
|
+
rmSync(dir, { recursive: true, force: true });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const written = [];
|
|
673
|
+
for (const { pluginName, skill } of entries) {
|
|
674
|
+
const sourcePath = resolve6(projectRoot, skill.path);
|
|
675
|
+
if (!existsSync4(sourcePath)) {
|
|
676
|
+
console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${sourcePath} does not exist`);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
let body;
|
|
680
|
+
try {
|
|
681
|
+
await loadSkill(sourcePath);
|
|
682
|
+
body = readFileSync3(sourcePath, "utf-8");
|
|
683
|
+
} catch (err) {
|
|
684
|
+
console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${err instanceof Error ? err.message : err}`);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const dir = resolve6(skillsRoot, skillDirName(skill.id));
|
|
688
|
+
mkdirSync3(dir, { recursive: true });
|
|
689
|
+
writeFileSync3(resolve6(dir, "SKILL.md"), body, "utf-8");
|
|
690
|
+
writeFileSync3(resolve6(dir, MARKER_FILENAME), `${JSON.stringify({ plugin: pluginName, skillId: skill.id }, null, 2)}
|
|
691
|
+
`, "utf-8");
|
|
692
|
+
written.push({ id: skill.id, pluginName, directory: dir });
|
|
693
|
+
}
|
|
694
|
+
return written;
|
|
695
|
+
}
|
|
696
|
+
|
|
654
697
|
// packages/runtime/src/control-plane/plugin-host-context.ts
|
|
655
698
|
async function buildPluginHostContext(projectRoot) {
|
|
656
699
|
let config;
|
|
@@ -687,6 +730,17 @@ async function buildPluginHostContext(projectRoot) {
|
|
|
687
730
|
} catch (err) {
|
|
688
731
|
console.warn(`[plugin-host] hook materialization failed: ${err instanceof Error ? err.message : err}`);
|
|
689
732
|
}
|
|
733
|
+
try {
|
|
734
|
+
const skillEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.skills ?? []).map((skill) => ({
|
|
735
|
+
pluginName: plugin.name,
|
|
736
|
+
skill
|
|
737
|
+
})));
|
|
738
|
+
if (skillEntries.length > 0) {
|
|
739
|
+
await materializeSkills(projectRoot, skillEntries);
|
|
740
|
+
}
|
|
741
|
+
} catch (err) {
|
|
742
|
+
console.warn(`[plugin-host] skill materialization failed: ${err instanceof Error ? err.message : err}`);
|
|
743
|
+
}
|
|
690
744
|
return {
|
|
691
745
|
config,
|
|
692
746
|
pluginHost,
|
|
@@ -700,12 +754,12 @@ async function buildPluginHostContext(projectRoot) {
|
|
|
700
754
|
|
|
701
755
|
// packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
|
|
702
756
|
import { spawnSync } from "child_process";
|
|
703
|
-
import { existsSync as
|
|
704
|
-
import { basename as basename3, join as join2, resolve as
|
|
757
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync4 } from "fs";
|
|
758
|
+
import { basename as basename3, join as join2, resolve as resolve8 } from "path";
|
|
705
759
|
|
|
706
760
|
// packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
|
|
707
|
-
import { existsSync as
|
|
708
|
-
import { resolve as
|
|
761
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
762
|
+
import { resolve as resolve7 } from "path";
|
|
709
763
|
|
|
710
764
|
// packages/runtime/src/control-plane/tasks/task-record-reader.ts
|
|
711
765
|
async function findTaskById(reader, id) {
|
|
@@ -728,7 +782,7 @@ class LegacyTaskConfigReadError extends Error {
|
|
|
728
782
|
}
|
|
729
783
|
}
|
|
730
784
|
function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
|
|
731
|
-
const configPath = options.configPath ??
|
|
785
|
+
const configPath = options.configPath ?? resolve7(projectRoot, ".rig", "task-config.json");
|
|
732
786
|
const reader = {
|
|
733
787
|
async listTasks() {
|
|
734
788
|
return readLegacyTaskRecords(projectRoot, configPath);
|
|
@@ -739,8 +793,8 @@ function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
|
|
|
739
793
|
};
|
|
740
794
|
return reader;
|
|
741
795
|
}
|
|
742
|
-
function readLegacyTaskRecords(projectRoot, configPath =
|
|
743
|
-
if (!
|
|
796
|
+
function readLegacyTaskRecords(projectRoot, configPath = resolve7(projectRoot, ".rig", "task-config.json")) {
|
|
797
|
+
if (!existsSync5(configPath)) {
|
|
744
798
|
return [];
|
|
745
799
|
}
|
|
746
800
|
const rawConfig = readLegacyTaskConfigJson(projectRoot, configPath);
|
|
@@ -748,7 +802,7 @@ function readLegacyTaskRecords(projectRoot, configPath = resolve6(projectRoot, "
|
|
|
748
802
|
}
|
|
749
803
|
function readLegacyTaskConfigJson(projectRoot, configPath) {
|
|
750
804
|
try {
|
|
751
|
-
const parsed = JSON.parse(
|
|
805
|
+
const parsed = JSON.parse(readFileSync4(configPath, "utf8"));
|
|
752
806
|
if (isPlainRecord(parsed)) {
|
|
753
807
|
return parsed;
|
|
754
808
|
}
|
|
@@ -832,7 +886,7 @@ function isPlainRecord(candidate) {
|
|
|
832
886
|
var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
|
|
833
887
|
var FILE_TASK_PATTERN = /\.(task\.)?json$/;
|
|
834
888
|
function createSourceAwareTaskConfigRecordReader(projectRoot, options = {}) {
|
|
835
|
-
const configPath = options.configPath ??
|
|
889
|
+
const configPath = options.configPath ?? resolve8(projectRoot, ".rig", "task-config.json");
|
|
836
890
|
const legacy = createLegacyTaskConfigRecordReader(projectRoot, { configPath });
|
|
837
891
|
const spawnFn = options.spawn ?? spawnSync;
|
|
838
892
|
const ghBinary = options.ghBinary ?? "gh";
|
|
@@ -915,10 +969,10 @@ function readMaterializedTaskMetadata(entry) {
|
|
|
915
969
|
return metadata;
|
|
916
970
|
}
|
|
917
971
|
function readConfiguredFilesTaskSourcePath(projectRoot) {
|
|
918
|
-
const jsonPath =
|
|
919
|
-
if (
|
|
972
|
+
const jsonPath = resolve8(projectRoot, "rig.config.json");
|
|
973
|
+
if (existsSync6(jsonPath)) {
|
|
920
974
|
try {
|
|
921
|
-
const parsed = JSON.parse(
|
|
975
|
+
const parsed = JSON.parse(readFileSync5(jsonPath, "utf8"));
|
|
922
976
|
if (isPlainRecord2(parsed) && isPlainRecord2(parsed.taskSource)) {
|
|
923
977
|
const source = parsed.taskSource;
|
|
924
978
|
return source.kind === "files" && typeof source.path === "string" ? source.path : null;
|
|
@@ -927,12 +981,12 @@ function readConfiguredFilesTaskSourcePath(projectRoot) {
|
|
|
927
981
|
return null;
|
|
928
982
|
}
|
|
929
983
|
}
|
|
930
|
-
const tsPath =
|
|
931
|
-
if (!
|
|
984
|
+
const tsPath = resolve8(projectRoot, "rig.config.ts");
|
|
985
|
+
if (!existsSync6(tsPath)) {
|
|
932
986
|
return null;
|
|
933
987
|
}
|
|
934
988
|
try {
|
|
935
|
-
const source =
|
|
989
|
+
const source = readFileSync5(tsPath, "utf8");
|
|
936
990
|
const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
|
|
937
991
|
const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
|
|
938
992
|
if (kind !== "files") {
|
|
@@ -952,10 +1006,10 @@ function readRawTaskEntry(configPath, taskId) {
|
|
|
952
1006
|
return isPlainRecord2(entry) ? entry : null;
|
|
953
1007
|
}
|
|
954
1008
|
function readRawTaskConfig(configPath) {
|
|
955
|
-
if (!
|
|
1009
|
+
if (!existsSync6(configPath)) {
|
|
956
1010
|
return null;
|
|
957
1011
|
}
|
|
958
|
-
const parsed = JSON.parse(
|
|
1012
|
+
const parsed = JSON.parse(readFileSync5(configPath, "utf8"));
|
|
959
1013
|
return isPlainRecord2(parsed) ? parsed : null;
|
|
960
1014
|
}
|
|
961
1015
|
function stripLegacyTaskConfigMetadata2(raw) {
|
|
@@ -963,12 +1017,12 @@ function stripLegacyTaskConfigMetadata2(raw) {
|
|
|
963
1017
|
return tasks;
|
|
964
1018
|
}
|
|
965
1019
|
function listFileBackedTasks(projectRoot, sourcePath) {
|
|
966
|
-
const directory =
|
|
967
|
-
if (!
|
|
1020
|
+
const directory = resolve8(projectRoot, sourcePath);
|
|
1021
|
+
if (!existsSync6(directory)) {
|
|
968
1022
|
return [];
|
|
969
1023
|
}
|
|
970
1024
|
const tasks = [];
|
|
971
|
-
for (const name of
|
|
1025
|
+
for (const name of readdirSync2(directory)) {
|
|
972
1026
|
if (!FILE_TASK_PATTERN.test(name))
|
|
973
1027
|
continue;
|
|
974
1028
|
const inferredId = basename3(name).replace(FILE_TASK_PATTERN, "");
|
|
@@ -979,11 +1033,11 @@ function listFileBackedTasks(projectRoot, sourcePath) {
|
|
|
979
1033
|
return tasks;
|
|
980
1034
|
}
|
|
981
1035
|
function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
|
|
982
|
-
const file = findFileBackedTaskFile(
|
|
1036
|
+
const file = findFileBackedTaskFile(resolve8(projectRoot, sourcePath), taskId);
|
|
983
1037
|
if (!file) {
|
|
984
1038
|
return null;
|
|
985
1039
|
}
|
|
986
|
-
const raw = JSON.parse(
|
|
1040
|
+
const raw = JSON.parse(readFileSync5(file, "utf8"));
|
|
987
1041
|
if (!isPlainRecord2(raw)) {
|
|
988
1042
|
return null;
|
|
989
1043
|
}
|
|
@@ -996,17 +1050,17 @@ function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
|
|
|
996
1050
|
};
|
|
997
1051
|
}
|
|
998
1052
|
function findFileBackedTaskFile(directory, taskId) {
|
|
999
|
-
if (!
|
|
1053
|
+
if (!existsSync6(directory)) {
|
|
1000
1054
|
return null;
|
|
1001
1055
|
}
|
|
1002
|
-
for (const name of
|
|
1056
|
+
for (const name of readdirSync2(directory)) {
|
|
1003
1057
|
if (!FILE_TASK_PATTERN.test(name))
|
|
1004
1058
|
continue;
|
|
1005
1059
|
const file = join2(directory, name);
|
|
1006
1060
|
try {
|
|
1007
1061
|
if (!statSync(file).isFile())
|
|
1008
1062
|
continue;
|
|
1009
|
-
const raw = JSON.parse(
|
|
1063
|
+
const raw = JSON.parse(readFileSync5(file, "utf8"));
|
|
1010
1064
|
const inferredId = basename3(file).replace(FILE_TASK_PATTERN, "");
|
|
1011
1065
|
const id = isPlainRecord2(raw) && typeof raw.id === "string" ? raw.id : inferredId;
|
|
1012
1066
|
if (id === taskId) {
|
|
@@ -1089,8 +1143,8 @@ function githubStatusFor(issue) {
|
|
|
1089
1143
|
return "open";
|
|
1090
1144
|
}
|
|
1091
1145
|
function selectedGitHubEnv() {
|
|
1092
|
-
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim()
|
|
1093
|
-
return { GH_TOKEN: token, GITHUB_TOKEN: token };
|
|
1146
|
+
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
|
|
1147
|
+
return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
|
|
1094
1148
|
}
|
|
1095
1149
|
function ghSpawnOptions() {
|
|
1096
1150
|
return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
|
|
@@ -1166,8 +1220,8 @@ async function readConfiguredTaskSourceTask(projectRoot, taskId) {
|
|
|
1166
1220
|
}
|
|
1167
1221
|
|
|
1168
1222
|
// packages/runtime/src/control-plane/native/task-state.ts
|
|
1169
|
-
import { existsSync as
|
|
1170
|
-
import { basename as basename6, resolve as
|
|
1223
|
+
import { existsSync as existsSync14, readFileSync as readFileSync9, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
1224
|
+
import { basename as basename6, resolve as resolve16 } from "path";
|
|
1171
1225
|
|
|
1172
1226
|
// packages/runtime/src/control-plane/state-sync/types.ts
|
|
1173
1227
|
var SUPPORTED_TASK_STATE_SCHEMA_VERSION = 1;
|
|
@@ -1275,30 +1329,30 @@ function readTaskStateMetadataEnvelope(raw) {
|
|
|
1275
1329
|
};
|
|
1276
1330
|
}
|
|
1277
1331
|
// packages/runtime/src/control-plane/state-sync/read.ts
|
|
1278
|
-
import { existsSync as
|
|
1279
|
-
import { resolve as
|
|
1332
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
|
|
1333
|
+
import { resolve as resolve15 } from "path";
|
|
1280
1334
|
|
|
1281
1335
|
// packages/runtime/src/control-plane/native/git-native.ts
|
|
1282
|
-
import { chmodSync, copyFileSync, existsSync as
|
|
1336
|
+
import { chmodSync, copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
1283
1337
|
import { tmpdir as tmpdir3 } from "os";
|
|
1284
|
-
import { dirname as dirname5, isAbsolute, resolve as
|
|
1338
|
+
import { dirname as dirname5, isAbsolute, resolve as resolve9 } from "path";
|
|
1285
1339
|
import { createHash } from "crypto";
|
|
1286
1340
|
function isTextTreeCommitUpdate(update) {
|
|
1287
1341
|
return typeof update.content === "string";
|
|
1288
1342
|
}
|
|
1289
|
-
var sharedGitNativeOutputDir =
|
|
1290
|
-
var sharedGitNativeOutputPath =
|
|
1343
|
+
var sharedGitNativeOutputDir = resolve9(tmpdir3(), "rig-native");
|
|
1344
|
+
var sharedGitNativeOutputPath = resolve9(sharedGitNativeOutputDir, `rig-git-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
|
|
1291
1345
|
var trackerCommandUsageProbe = "usage: rig-git fetch-ref <repo-path> <remote> <branch>";
|
|
1292
1346
|
function temporaryGitBinaryOutputPath(outputPath) {
|
|
1293
1347
|
const suffix = process.platform === "win32" ? ".exe" : "";
|
|
1294
|
-
return
|
|
1348
|
+
return resolve9(dirname5(outputPath), `.rig-git-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}${suffix}`);
|
|
1295
1349
|
}
|
|
1296
1350
|
function publishGitBinary(tempOutputPath, outputPath) {
|
|
1297
1351
|
try {
|
|
1298
1352
|
renameSync(tempOutputPath, outputPath);
|
|
1299
1353
|
} catch (error) {
|
|
1300
|
-
if (process.platform === "win32" &&
|
|
1301
|
-
|
|
1354
|
+
if (process.platform === "win32" && existsSync7(outputPath)) {
|
|
1355
|
+
rmSync2(outputPath, { force: true });
|
|
1302
1356
|
renameSync(tempOutputPath, outputPath);
|
|
1303
1357
|
return;
|
|
1304
1358
|
}
|
|
@@ -1313,22 +1367,22 @@ function rigGitSourceCandidates() {
|
|
|
1313
1367
|
const cwd = process.cwd()?.trim() || "";
|
|
1314
1368
|
const projectRoot = process.env.PROJECT_RIG_ROOT?.trim() || "";
|
|
1315
1369
|
const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim() || "";
|
|
1316
|
-
const moduleRelativeSource =
|
|
1370
|
+
const moduleRelativeSource = resolve9(import.meta.dir, "../../../native/rig-git.zig");
|
|
1317
1371
|
return [...new Set([
|
|
1318
1372
|
process.env.RIG_NATIVE_GIT_SOURCE?.trim() || "",
|
|
1319
1373
|
moduleRelativeSource,
|
|
1320
|
-
projectRoot ?
|
|
1321
|
-
hostProjectRoot ?
|
|
1322
|
-
cwd ?
|
|
1323
|
-
execDir ?
|
|
1324
|
-
execDir ?
|
|
1374
|
+
projectRoot ? resolve9(projectRoot, "packages/runtime/native/rig-git.zig") : "",
|
|
1375
|
+
hostProjectRoot ? resolve9(hostProjectRoot, "packages/runtime/native/rig-git.zig") : "",
|
|
1376
|
+
cwd ? resolve9(cwd, "packages/runtime/native/rig-git.zig") : "",
|
|
1377
|
+
execDir ? resolve9(execDir, "..", "..", "packages/runtime/native/rig-git.zig") : "",
|
|
1378
|
+
execDir ? resolve9(execDir, "..", "native", "rig-git.zig") : ""
|
|
1325
1379
|
].filter(Boolean))];
|
|
1326
1380
|
}
|
|
1327
1381
|
function nativePackageBinaryCandidates(fromDir, fileName) {
|
|
1328
1382
|
const candidates = [];
|
|
1329
|
-
let cursor =
|
|
1383
|
+
let cursor = resolve9(fromDir);
|
|
1330
1384
|
for (let index = 0;index < 8; index += 1) {
|
|
1331
|
-
candidates.push(
|
|
1385
|
+
candidates.push(resolve9(cursor, "native", `${process.platform}-${process.arch}`, fileName), resolve9(cursor, "native", `${process.platform}-${process.arch}`, "bin", fileName), resolve9(cursor, "native", fileName), resolve9(cursor, "native", "bin", fileName));
|
|
1332
1386
|
const parent = dirname5(cursor);
|
|
1333
1387
|
if (parent === cursor)
|
|
1334
1388
|
break;
|
|
@@ -1343,15 +1397,15 @@ function rigGitBinaryCandidates() {
|
|
|
1343
1397
|
return [...new Set([
|
|
1344
1398
|
explicit,
|
|
1345
1399
|
...nativePackageBinaryCandidates(import.meta.dir, fileName),
|
|
1346
|
-
execDir ?
|
|
1347
|
-
execDir ?
|
|
1348
|
-
execDir ?
|
|
1400
|
+
execDir ? resolve9(execDir, fileName) : "",
|
|
1401
|
+
execDir ? resolve9(execDir, "..", fileName) : "",
|
|
1402
|
+
execDir ? resolve9(execDir, "..", "bin", fileName) : "",
|
|
1349
1403
|
sharedGitNativeOutputPath
|
|
1350
1404
|
].filter(Boolean))];
|
|
1351
1405
|
}
|
|
1352
1406
|
function resolveGitSourcePath() {
|
|
1353
1407
|
for (const candidate of rigGitSourceCandidates()) {
|
|
1354
|
-
if (candidate &&
|
|
1408
|
+
if (candidate && existsSync7(candidate)) {
|
|
1355
1409
|
return candidate;
|
|
1356
1410
|
}
|
|
1357
1411
|
}
|
|
@@ -1362,7 +1416,7 @@ function resolveGitBinaryPath() {
|
|
|
1362
1416
|
return null;
|
|
1363
1417
|
}
|
|
1364
1418
|
for (const candidate of rigGitBinaryCandidates()) {
|
|
1365
|
-
if (candidate &&
|
|
1419
|
+
if (candidate && existsSync7(candidate)) {
|
|
1366
1420
|
return candidate;
|
|
1367
1421
|
}
|
|
1368
1422
|
}
|
|
@@ -1392,18 +1446,18 @@ function nativeBuildManifestPath(outputPath) {
|
|
|
1392
1446
|
return `${outputPath}.build-manifest.json`;
|
|
1393
1447
|
}
|
|
1394
1448
|
function hasMatchingNativeBuildManifestSync(manifestPath, buildKey) {
|
|
1395
|
-
if (!
|
|
1449
|
+
if (!existsSync7(manifestPath)) {
|
|
1396
1450
|
return false;
|
|
1397
1451
|
}
|
|
1398
1452
|
try {
|
|
1399
|
-
const manifest = JSON.parse(
|
|
1453
|
+
const manifest = JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
1400
1454
|
return manifest.version === 1 && manifest.buildKey === buildKey;
|
|
1401
1455
|
} catch {
|
|
1402
1456
|
return false;
|
|
1403
1457
|
}
|
|
1404
1458
|
}
|
|
1405
1459
|
function sha256FileSync(path) {
|
|
1406
|
-
return createHash("sha256").update(
|
|
1460
|
+
return createHash("sha256").update(readFileSync6(path)).digest("hex");
|
|
1407
1461
|
}
|
|
1408
1462
|
function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath()) {
|
|
1409
1463
|
if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
|
|
@@ -1421,7 +1475,7 @@ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath())
|
|
|
1421
1475
|
if (!zigBinary) {
|
|
1422
1476
|
throw new Error("zig is required to build native Rig git tools.");
|
|
1423
1477
|
}
|
|
1424
|
-
|
|
1478
|
+
mkdirSync4(dirname5(outputPath), { recursive: true });
|
|
1425
1479
|
const sourceDigest = sha256FileSync(sourcePath);
|
|
1426
1480
|
const buildKey = JSON.stringify({
|
|
1427
1481
|
version: 1,
|
|
@@ -1432,7 +1486,7 @@ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath())
|
|
|
1432
1486
|
sourceDigest
|
|
1433
1487
|
});
|
|
1434
1488
|
const manifestPath = nativeBuildManifestPath(outputPath);
|
|
1435
|
-
const needsBuild = !
|
|
1489
|
+
const needsBuild = !existsSync7(outputPath) || !hasMatchingNativeBuildManifestSync(manifestPath, buildKey) || !binarySupportsTrackerCommandsSync(outputPath);
|
|
1436
1490
|
if (!needsBuild) {
|
|
1437
1491
|
chmodSync(outputPath, 493);
|
|
1438
1492
|
return outputPath;
|
|
@@ -1450,7 +1504,7 @@ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath())
|
|
|
1450
1504
|
stdout: "pipe",
|
|
1451
1505
|
stderr: "pipe"
|
|
1452
1506
|
});
|
|
1453
|
-
if (build.exitCode !== 0 || !
|
|
1507
|
+
if (build.exitCode !== 0 || !existsSync7(tempOutputPath)) {
|
|
1454
1508
|
const stderr = build.stderr.toString().trim();
|
|
1455
1509
|
const stdout = build.stdout.toString().trim();
|
|
1456
1510
|
const details = [stderr, stdout].filter(Boolean).join(`
|
|
@@ -1458,17 +1512,17 @@ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath())
|
|
|
1458
1512
|
throw new Error(`Failed to build native Rig git tools: ${details || `zig exited with code ${build.exitCode}`}`);
|
|
1459
1513
|
}
|
|
1460
1514
|
chmodSync(tempOutputPath, 493);
|
|
1461
|
-
if (
|
|
1462
|
-
|
|
1515
|
+
if (existsSync7(outputPath) && hasMatchingNativeBuildManifestSync(manifestPath, buildKey)) {
|
|
1516
|
+
rmSync2(tempOutputPath, { force: true });
|
|
1463
1517
|
chmodSync(outputPath, 493);
|
|
1464
1518
|
return outputPath;
|
|
1465
1519
|
}
|
|
1466
1520
|
publishGitBinary(tempOutputPath, outputPath);
|
|
1467
1521
|
if (!binarySupportsTrackerCommandsSync(outputPath)) {
|
|
1468
|
-
|
|
1522
|
+
rmSync2(outputPath, { force: true });
|
|
1469
1523
|
throw new Error("Failed to build native Rig git tools: tracker command probe failed");
|
|
1470
1524
|
}
|
|
1471
|
-
|
|
1525
|
+
writeFileSync5(manifestPath, `${JSON.stringify({ version: 1, buildKey }, null, 2)}
|
|
1472
1526
|
`, "utf8");
|
|
1473
1527
|
return outputPath;
|
|
1474
1528
|
}
|
|
@@ -1490,7 +1544,7 @@ function runGitNative(command, args) {
|
|
|
1490
1544
|
}
|
|
1491
1545
|
} else {
|
|
1492
1546
|
const explicitBinaryPath = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
|
|
1493
|
-
binaryPath = explicitBinaryPath &&
|
|
1547
|
+
binaryPath = explicitBinaryPath && existsSync7(explicitBinaryPath) ? explicitBinaryPath : !explicitBinaryPath ? resolveGitBinaryPath() : null;
|
|
1494
1548
|
if (!binaryPath) {
|
|
1495
1549
|
try {
|
|
1496
1550
|
binaryPath = ensureRigGitBinaryPathSync(preferredGitBinaryOutputPath());
|
|
@@ -1546,14 +1600,14 @@ function nativeFetchRef(repoPath, remote, branch) {
|
|
|
1546
1600
|
return requireGitNativeString("fetch-ref", [repoPath, remote, branch]);
|
|
1547
1601
|
}
|
|
1548
1602
|
function nativeReadBlobAtRef(repoPath, ref, path) {
|
|
1549
|
-
const requestDir =
|
|
1550
|
-
|
|
1551
|
-
const outputPath =
|
|
1603
|
+
const requestDir = resolve9(sharedGitNativeOutputDir, "reads", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
|
|
1604
|
+
mkdirSync4(requestDir, { recursive: true });
|
|
1605
|
+
const outputPath = resolve9(requestDir, "blob.txt");
|
|
1552
1606
|
try {
|
|
1553
1607
|
requireGitNative("read-blob-at-ref", [repoPath, ref, path, outputPath]);
|
|
1554
|
-
return
|
|
1608
|
+
return readFileSync6(outputPath, "utf8");
|
|
1555
1609
|
} finally {
|
|
1556
|
-
|
|
1610
|
+
rmSync2(requestDir, { recursive: true, force: true });
|
|
1557
1611
|
}
|
|
1558
1612
|
}
|
|
1559
1613
|
function serializeTreeCommitUpdates(updates) {
|
|
@@ -1572,16 +1626,16 @@ function buildTreeCommitUpdatesJson(updates) {
|
|
|
1572
1626
|
`;
|
|
1573
1627
|
}
|
|
1574
1628
|
function nativeWriteTreeCommit(repoPath, baseRef, updates, message) {
|
|
1575
|
-
const requestDir =
|
|
1576
|
-
|
|
1577
|
-
const messagePath =
|
|
1578
|
-
const updatesPath =
|
|
1629
|
+
const requestDir = resolve9(sharedGitNativeOutputDir, "requests", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
|
|
1630
|
+
mkdirSync4(requestDir, { recursive: true });
|
|
1631
|
+
const messagePath = resolve9(requestDir, "message.txt");
|
|
1632
|
+
const updatesPath = resolve9(requestDir, "updates.json");
|
|
1579
1633
|
try {
|
|
1580
|
-
|
|
1581
|
-
|
|
1634
|
+
writeFileSync5(messagePath, message, "utf8");
|
|
1635
|
+
writeFileSync5(updatesPath, buildTreeCommitUpdatesJson(updates), "utf8");
|
|
1582
1636
|
return requireGitNativeString("write-tree-commit", [repoPath, baseRef, messagePath, updatesPath]);
|
|
1583
1637
|
} finally {
|
|
1584
|
-
|
|
1638
|
+
rmSync2(requestDir, { recursive: true, force: true });
|
|
1585
1639
|
}
|
|
1586
1640
|
}
|
|
1587
1641
|
function nativePushRefWithLease(repoPath, localOid, remoteRef, expectedOldOid, remote = "origin") {
|
|
@@ -1596,35 +1650,35 @@ function nativePushRefWithLease(repoPath, localOid, remoteRef, expectedOldOid, r
|
|
|
1596
1650
|
|
|
1597
1651
|
// packages/runtime/src/control-plane/native/utils.ts
|
|
1598
1652
|
import { ptr as ptr2 } from "bun:ffi";
|
|
1599
|
-
import { existsSync as
|
|
1600
|
-
import { resolve as
|
|
1653
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
1654
|
+
import { resolve as resolve12 } from "path";
|
|
1601
1655
|
|
|
1602
1656
|
// packages/runtime/src/layout.ts
|
|
1603
|
-
import { existsSync as
|
|
1604
|
-
import { basename as basename4, dirname as dirname6, resolve as
|
|
1657
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1658
|
+
import { basename as basename4, dirname as dirname6, resolve as resolve10 } from "path";
|
|
1605
1659
|
var RIG_DEFINITION_DIRNAME = "rig";
|
|
1606
1660
|
var RIG_ARTIFACTS_DIRNAME = "artifacts";
|
|
1607
1661
|
function resolveMonorepoRoot(projectRoot) {
|
|
1608
|
-
const normalizedProjectRoot =
|
|
1662
|
+
const normalizedProjectRoot = resolve10(projectRoot);
|
|
1609
1663
|
const explicit = process.env.MONOREPO_ROOT?.trim();
|
|
1610
1664
|
if (explicit) {
|
|
1611
|
-
const explicitRoot =
|
|
1665
|
+
const explicitRoot = resolve10(explicit);
|
|
1612
1666
|
const explicitParent = dirname6(explicitRoot);
|
|
1613
1667
|
if (basename4(explicitParent) === ".worktrees") {
|
|
1614
1668
|
const owner = dirname6(explicitParent);
|
|
1615
|
-
const ownerHasGit =
|
|
1616
|
-
const ownerHasTaskConfig =
|
|
1617
|
-
const ownerHasRigConfig =
|
|
1669
|
+
const ownerHasGit = existsSync8(resolve10(owner, ".git"));
|
|
1670
|
+
const ownerHasTaskConfig = existsSync8(resolve10(owner, ".rig", "task-config.json"));
|
|
1671
|
+
const ownerHasRigConfig = existsSync8(resolve10(owner, "rig.config.ts"));
|
|
1618
1672
|
if (ownerHasGit && (ownerHasTaskConfig || ownerHasRigConfig)) {
|
|
1619
1673
|
return owner;
|
|
1620
1674
|
}
|
|
1621
1675
|
throw new Error(`MONOREPO_ROOT points to worktree ${explicitRoot}, but the owner checkout is incomplete at ${owner}.`);
|
|
1622
1676
|
}
|
|
1623
|
-
if (!
|
|
1677
|
+
if (!existsSync8(resolve10(explicitRoot, ".git"))) {
|
|
1624
1678
|
throw new Error(`MONOREPO_ROOT points to ${explicitRoot}, but no git checkout was found there.`);
|
|
1625
1679
|
}
|
|
1626
|
-
const hasTaskConfig =
|
|
1627
|
-
const hasRigConfig =
|
|
1680
|
+
const hasTaskConfig = existsSync8(resolve10(explicitRoot, ".rig", "task-config.json"));
|
|
1681
|
+
const hasRigConfig = existsSync8(resolve10(explicitRoot, "rig.config.ts"));
|
|
1628
1682
|
if (!hasTaskConfig && !hasRigConfig) {
|
|
1629
1683
|
throw new Error(`MONOREPO_ROOT points to ${explicitRoot}, but neither .rig/task-config.json nor rig.config.ts exists there.`);
|
|
1630
1684
|
}
|
|
@@ -1633,9 +1687,9 @@ function resolveMonorepoRoot(projectRoot) {
|
|
|
1633
1687
|
const projectParent = dirname6(normalizedProjectRoot);
|
|
1634
1688
|
if (basename4(projectParent) === ".worktrees") {
|
|
1635
1689
|
const worktreeOwner = dirname6(projectParent);
|
|
1636
|
-
const ownerHasGit =
|
|
1637
|
-
const ownerHasTaskConfig =
|
|
1638
|
-
const ownerHasRigConfig =
|
|
1690
|
+
const ownerHasGit = existsSync8(resolve10(worktreeOwner, ".git"));
|
|
1691
|
+
const ownerHasTaskConfig = existsSync8(resolve10(worktreeOwner, ".rig", "task-config.json"));
|
|
1692
|
+
const ownerHasRigConfig = existsSync8(resolve10(worktreeOwner, "rig.config.ts"));
|
|
1639
1693
|
if (ownerHasGit && (ownerHasTaskConfig || ownerHasRigConfig)) {
|
|
1640
1694
|
return worktreeOwner;
|
|
1641
1695
|
}
|
|
@@ -1643,28 +1697,28 @@ function resolveMonorepoRoot(projectRoot) {
|
|
|
1643
1697
|
return normalizedProjectRoot;
|
|
1644
1698
|
}
|
|
1645
1699
|
function resolveRuntimeWorkspaceLayout(workspaceDir) {
|
|
1646
|
-
const root =
|
|
1647
|
-
const rigRoot =
|
|
1648
|
-
const logsDir =
|
|
1649
|
-
const stateDir =
|
|
1650
|
-
const runtimeDir =
|
|
1651
|
-
const binDir =
|
|
1700
|
+
const root = resolve10(workspaceDir);
|
|
1701
|
+
const rigRoot = resolve10(root, ".rig");
|
|
1702
|
+
const logsDir = resolve10(rigRoot, "logs");
|
|
1703
|
+
const stateDir = resolve10(rigRoot, "state");
|
|
1704
|
+
const runtimeDir = resolve10(rigRoot, "runtime");
|
|
1705
|
+
const binDir = resolve10(rigRoot, "bin");
|
|
1652
1706
|
return {
|
|
1653
1707
|
workspaceDir: root,
|
|
1654
1708
|
rigRoot,
|
|
1655
1709
|
stateDir,
|
|
1656
1710
|
logsDir,
|
|
1657
|
-
artifactsRoot:
|
|
1711
|
+
artifactsRoot: resolve10(root, RIG_ARTIFACTS_DIRNAME),
|
|
1658
1712
|
runtimeDir,
|
|
1659
|
-
homeDir:
|
|
1660
|
-
tmpDir:
|
|
1661
|
-
cacheDir:
|
|
1662
|
-
sessionDir:
|
|
1713
|
+
homeDir: resolve10(rigRoot, "home"),
|
|
1714
|
+
tmpDir: resolve10(rigRoot, "tmp"),
|
|
1715
|
+
cacheDir: resolve10(rigRoot, "cache"),
|
|
1716
|
+
sessionDir: resolve10(rigRoot, "session"),
|
|
1663
1717
|
binDir,
|
|
1664
|
-
distDir:
|
|
1665
|
-
pluginBinDir:
|
|
1666
|
-
contextPath:
|
|
1667
|
-
controlPlaneEventsFile:
|
|
1718
|
+
distDir: resolve10(rigRoot, "dist"),
|
|
1719
|
+
pluginBinDir: resolve10(binDir, "plugins"),
|
|
1720
|
+
contextPath: resolve10(rigRoot, "runtime-context.json"),
|
|
1721
|
+
controlPlaneEventsFile: resolve10(logsDir, "control-plane.events.jsonl")
|
|
1668
1722
|
};
|
|
1669
1723
|
}
|
|
1670
1724
|
function resolveActiveRuntimeWorkspaceRoot(monorepoRoot) {
|
|
@@ -1672,14 +1726,14 @@ function resolveActiveRuntimeWorkspaceRoot(monorepoRoot) {
|
|
|
1672
1726
|
if (!explicit) {
|
|
1673
1727
|
throw new Error("No active runtime workspace. Set RIG_TASK_WORKSPACE or provision a task runtime first.");
|
|
1674
1728
|
}
|
|
1675
|
-
return
|
|
1729
|
+
return resolve10(explicit);
|
|
1676
1730
|
}
|
|
1677
1731
|
function resolveRigLayout(projectRoot) {
|
|
1678
1732
|
const monorepoRoot = resolveMonorepoRoot(projectRoot);
|
|
1679
|
-
const definitionRoot =
|
|
1733
|
+
const definitionRoot = resolve10(projectRoot, RIG_DEFINITION_DIRNAME);
|
|
1680
1734
|
const runtimeWorkspaceRoot = resolveActiveRuntimeWorkspaceRoot(monorepoRoot);
|
|
1681
1735
|
const runtimeLayout = resolveRuntimeWorkspaceLayout(runtimeWorkspaceRoot);
|
|
1682
|
-
const policyDir =
|
|
1736
|
+
const policyDir = resolve10(definitionRoot, "policy");
|
|
1683
1737
|
return {
|
|
1684
1738
|
projectRoot,
|
|
1685
1739
|
monorepoRoot,
|
|
@@ -1687,34 +1741,34 @@ function resolveRigLayout(projectRoot) {
|
|
|
1687
1741
|
runtimeWorkspaceRoot,
|
|
1688
1742
|
stateRoot: runtimeLayout.rigRoot,
|
|
1689
1743
|
artifactsRoot: runtimeLayout.artifactsRoot,
|
|
1690
|
-
configPath:
|
|
1691
|
-
taskConfigPath:
|
|
1744
|
+
configPath: resolve10(definitionRoot, "config.sh"),
|
|
1745
|
+
taskConfigPath: resolve10(runtimeWorkspaceRoot, ".rig", "task-config.json"),
|
|
1692
1746
|
policyDir,
|
|
1693
|
-
policyFile:
|
|
1694
|
-
pluginsDir:
|
|
1695
|
-
hooksDir:
|
|
1696
|
-
toolsDir:
|
|
1697
|
-
templatesDir:
|
|
1698
|
-
validationDir:
|
|
1747
|
+
policyFile: resolve10(policyDir, "policy.json"),
|
|
1748
|
+
pluginsDir: resolve10(definitionRoot, "plugins"),
|
|
1749
|
+
hooksDir: resolve10(definitionRoot, "hooks"),
|
|
1750
|
+
toolsDir: resolve10(definitionRoot, "tools"),
|
|
1751
|
+
templatesDir: resolve10(definitionRoot, "templates"),
|
|
1752
|
+
validationDir: resolve10(definitionRoot, "validation"),
|
|
1699
1753
|
stateDir: runtimeLayout.stateDir,
|
|
1700
1754
|
logsDir: runtimeLayout.logsDir,
|
|
1701
|
-
notificationsDir:
|
|
1755
|
+
notificationsDir: resolve10(definitionRoot, "notifications"),
|
|
1702
1756
|
runtimeDir: runtimeLayout.runtimeDir,
|
|
1703
1757
|
distDir: runtimeLayout.distDir,
|
|
1704
1758
|
binDir: runtimeLayout.binDir,
|
|
1705
1759
|
pluginBinDir: runtimeLayout.pluginBinDir,
|
|
1706
|
-
keybindingsPath:
|
|
1760
|
+
keybindingsPath: resolve10(definitionRoot, "keybindings.json"),
|
|
1707
1761
|
controlPlaneEventsFile: runtimeLayout.controlPlaneEventsFile
|
|
1708
1762
|
};
|
|
1709
1763
|
}
|
|
1710
1764
|
|
|
1711
1765
|
// packages/runtime/src/control-plane/native/runtime-native.ts
|
|
1712
1766
|
import { dlopen, ptr, suffix, toBuffer } from "bun:ffi";
|
|
1713
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
1767
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync5, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync2 } from "fs";
|
|
1714
1768
|
import { tmpdir as tmpdir4 } from "os";
|
|
1715
|
-
import { dirname as dirname7, resolve as
|
|
1716
|
-
var sharedNativeRuntimeOutputDir =
|
|
1717
|
-
var sharedNativeRuntimeOutputPath =
|
|
1769
|
+
import { dirname as dirname7, resolve as resolve11 } from "path";
|
|
1770
|
+
var sharedNativeRuntimeOutputDir = resolve11(tmpdir4(), "rig-native");
|
|
1771
|
+
var sharedNativeRuntimeOutputPath = resolve11(sharedNativeRuntimeOutputDir, `runtime-native-${process.platform}-${process.arch}.${suffix}`);
|
|
1718
1772
|
var colocatedNativeRuntimeFileName = `runtime-native.${suffix}`;
|
|
1719
1773
|
var nativeRuntimeLibrary = await loadNativeRuntimeLibrary();
|
|
1720
1774
|
function requireNativeRuntimeLibrary(feature) {
|
|
@@ -1727,14 +1781,14 @@ async function ensureNativeRuntimeLibraryPath(outputPath = sharedNativeRuntimeOu
|
|
|
1727
1781
|
if (await buildNativeRuntimeLibrary(outputPath, options)) {
|
|
1728
1782
|
return outputPath;
|
|
1729
1783
|
}
|
|
1730
|
-
return !options.force &&
|
|
1784
|
+
return !options.force && existsSync9(outputPath) ? outputPath : null;
|
|
1731
1785
|
}
|
|
1732
1786
|
async function loadNativeRuntimeLibrary() {
|
|
1733
1787
|
if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
|
|
1734
1788
|
return null;
|
|
1735
1789
|
}
|
|
1736
1790
|
for (const candidate of nativeRuntimeLibraryCandidates()) {
|
|
1737
|
-
if (!candidate || !
|
|
1791
|
+
if (!candidate || !existsSync9(candidate)) {
|
|
1738
1792
|
continue;
|
|
1739
1793
|
}
|
|
1740
1794
|
const loaded = tryDlopenNativeRuntimeLibrary(candidate);
|
|
@@ -1750,10 +1804,10 @@ async function loadNativeRuntimeLibrary() {
|
|
|
1750
1804
|
}
|
|
1751
1805
|
function nativePackageLibraryCandidates(fromDir, names) {
|
|
1752
1806
|
const candidates = [];
|
|
1753
|
-
let cursor =
|
|
1807
|
+
let cursor = resolve11(fromDir);
|
|
1754
1808
|
for (let index = 0;index < 8; index += 1) {
|
|
1755
1809
|
for (const name of names) {
|
|
1756
|
-
candidates.push(
|
|
1810
|
+
candidates.push(resolve11(cursor, "native", `${process.platform}-${process.arch}`, name), resolve11(cursor, "native", `${process.platform}-${process.arch}`, "lib", name), resolve11(cursor, "native", name), resolve11(cursor, "native", "lib", name));
|
|
1757
1811
|
}
|
|
1758
1812
|
const parent = dirname7(cursor);
|
|
1759
1813
|
if (parent === cursor)
|
|
@@ -1769,22 +1823,22 @@ function nativeRuntimeLibraryCandidates() {
|
|
|
1769
1823
|
return [...new Set([
|
|
1770
1824
|
explicit,
|
|
1771
1825
|
...nativePackageLibraryCandidates(import.meta.dir, [colocatedNativeRuntimeFileName, platformSpecific]),
|
|
1772
|
-
execDir ?
|
|
1773
|
-
execDir ?
|
|
1774
|
-
execDir ?
|
|
1775
|
-
execDir ?
|
|
1776
|
-
execDir ?
|
|
1777
|
-
execDir ?
|
|
1826
|
+
execDir ? resolve11(execDir, colocatedNativeRuntimeFileName) : "",
|
|
1827
|
+
execDir ? resolve11(execDir, platformSpecific) : "",
|
|
1828
|
+
execDir ? resolve11(execDir, "..", colocatedNativeRuntimeFileName) : "",
|
|
1829
|
+
execDir ? resolve11(execDir, "..", platformSpecific) : "",
|
|
1830
|
+
execDir ? resolve11(execDir, "lib", colocatedNativeRuntimeFileName) : "",
|
|
1831
|
+
execDir ? resolve11(execDir, "..", "lib", colocatedNativeRuntimeFileName) : "",
|
|
1778
1832
|
sharedNativeRuntimeOutputPath
|
|
1779
1833
|
].filter(Boolean))];
|
|
1780
1834
|
}
|
|
1781
1835
|
function resolveNativeRuntimeSourcePath() {
|
|
1782
1836
|
const explicit = process.env.RIG_NATIVE_RUNTIME_SOURCE?.trim();
|
|
1783
|
-
if (explicit &&
|
|
1837
|
+
if (explicit && existsSync9(explicit)) {
|
|
1784
1838
|
return explicit;
|
|
1785
1839
|
}
|
|
1786
|
-
const bundled =
|
|
1787
|
-
return
|
|
1840
|
+
const bundled = resolve11(import.meta.dir, "../../../native/snapshot.zig");
|
|
1841
|
+
return existsSync9(bundled) ? bundled : null;
|
|
1788
1842
|
}
|
|
1789
1843
|
async function buildNativeRuntimeLibrary(outputPath, options = {}) {
|
|
1790
1844
|
if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
|
|
@@ -1797,8 +1851,8 @@ async function buildNativeRuntimeLibrary(outputPath, options = {}) {
|
|
|
1797
1851
|
}
|
|
1798
1852
|
const tempOutputPath = `${outputPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
1799
1853
|
try {
|
|
1800
|
-
|
|
1801
|
-
const needsBuild = options.force === true || !
|
|
1854
|
+
mkdirSync5(dirname7(outputPath), { recursive: true });
|
|
1855
|
+
const needsBuild = options.force === true || !existsSync9(outputPath) || statSync2(sourcePath).mtimeMs > statSync2(outputPath).mtimeMs;
|
|
1802
1856
|
if (!needsBuild) {
|
|
1803
1857
|
return true;
|
|
1804
1858
|
}
|
|
@@ -1816,14 +1870,14 @@ async function buildNativeRuntimeLibrary(outputPath, options = {}) {
|
|
|
1816
1870
|
stderr: "pipe"
|
|
1817
1871
|
});
|
|
1818
1872
|
const exitCode = await build.exited;
|
|
1819
|
-
if (exitCode !== 0 || !
|
|
1820
|
-
|
|
1873
|
+
if (exitCode !== 0 || !existsSync9(tempOutputPath)) {
|
|
1874
|
+
rmSync3(tempOutputPath, { force: true });
|
|
1821
1875
|
return false;
|
|
1822
1876
|
}
|
|
1823
1877
|
renameSync2(tempOutputPath, outputPath);
|
|
1824
1878
|
return true;
|
|
1825
1879
|
} catch {
|
|
1826
|
-
|
|
1880
|
+
rmSync3(tempOutputPath, { force: true });
|
|
1827
1881
|
return false;
|
|
1828
1882
|
}
|
|
1829
1883
|
}
|
|
@@ -1947,11 +2001,11 @@ async function runCaptureAsync(command, cwd, env, timeoutMs) {
|
|
|
1947
2001
|
return { exitCode, stdout, stderr };
|
|
1948
2002
|
}
|
|
1949
2003
|
function readJsonFile(path, fallback) {
|
|
1950
|
-
if (!
|
|
2004
|
+
if (!existsSync10(path)) {
|
|
1951
2005
|
return fallback;
|
|
1952
2006
|
}
|
|
1953
2007
|
try {
|
|
1954
|
-
return JSON.parse(
|
|
2008
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
1955
2009
|
} catch {
|
|
1956
2010
|
return fallback;
|
|
1957
2011
|
}
|
|
@@ -1965,31 +2019,31 @@ function unique(values) {
|
|
|
1965
2019
|
function resolveHarnessPaths(projectRoot) {
|
|
1966
2020
|
const hasRuntimeWorkspace = Boolean(process.env.RIG_TASK_WORKSPACE?.trim());
|
|
1967
2021
|
const monorepoRoot = resolveMonorepoRoot2(projectRoot);
|
|
1968
|
-
const harnessRoot =
|
|
1969
|
-
const stateRoot =
|
|
2022
|
+
const harnessRoot = resolve12(projectRoot, "rig");
|
|
2023
|
+
const stateRoot = resolve12(projectRoot, ".rig");
|
|
1970
2024
|
const layout = hasRuntimeWorkspace ? resolveRigLayout(projectRoot) : null;
|
|
1971
|
-
const stateDir = layout?.stateDir ??
|
|
1972
|
-
const logsDir = layout?.logsDir ??
|
|
1973
|
-
const artifactsDir = layout?.artifactsRoot ??
|
|
1974
|
-
const taskConfigPath = layout?.taskConfigPath ??
|
|
1975
|
-
const binDir = layout?.binDir ??
|
|
2025
|
+
const stateDir = layout?.stateDir ?? resolve12(stateRoot, "state");
|
|
2026
|
+
const logsDir = layout?.logsDir ?? resolve12(stateRoot, "logs");
|
|
2027
|
+
const artifactsDir = layout?.artifactsRoot ?? resolve12(monorepoRoot, "artifacts");
|
|
2028
|
+
const taskConfigPath = layout?.taskConfigPath ?? resolve12(monorepoRoot, ".rig", "task-config.json");
|
|
2029
|
+
const binDir = layout?.binDir ?? resolve12(stateRoot, "bin");
|
|
1976
2030
|
return {
|
|
1977
2031
|
harnessRoot,
|
|
1978
2032
|
stateDir: process.env.RIG_STATE_DIR || stateDir,
|
|
1979
2033
|
artifactsDir,
|
|
1980
2034
|
logsDir: process.env.RIG_LOGS_DIR || logsDir,
|
|
1981
2035
|
binDir,
|
|
1982
|
-
hooksDir:
|
|
1983
|
-
validationDir:
|
|
2036
|
+
hooksDir: resolve12(harnessRoot, "hooks"),
|
|
2037
|
+
validationDir: resolve12(harnessRoot, "validation"),
|
|
1984
2038
|
taskConfigPath,
|
|
1985
|
-
sessionPath: process.env.RIG_SESSION_FILE ||
|
|
2039
|
+
sessionPath: process.env.RIG_SESSION_FILE || resolve12(stateRoot, "session", "session.json"),
|
|
1986
2040
|
monorepoRoot,
|
|
1987
|
-
tsApiTestsDir: process.env.TS_API_TESTS_DIR ||
|
|
1988
|
-
taskRepoCommitsPath:
|
|
1989
|
-
baseRepoPinsPath:
|
|
1990
|
-
failedApproachesPath:
|
|
1991
|
-
agentProfilePath:
|
|
1992
|
-
reviewProfilePath:
|
|
2041
|
+
tsApiTestsDir: process.env.TS_API_TESTS_DIR || resolve12(monorepoRoot, "TSAPITests"),
|
|
2042
|
+
taskRepoCommitsPath: resolve12(stateDir, "task-repo-commits.json"),
|
|
2043
|
+
baseRepoPinsPath: resolve12(stateDir, "base-repo-pins.json"),
|
|
2044
|
+
failedApproachesPath: resolve12(stateDir, "failed_approaches.md"),
|
|
2045
|
+
agentProfilePath: resolve12(stateDir, "agent-profile.json"),
|
|
2046
|
+
reviewProfilePath: resolve12(stateDir, "review-profile.json")
|
|
1993
2047
|
};
|
|
1994
2048
|
}
|
|
1995
2049
|
function normalizeRelativeScopePath(inputPath) {
|
|
@@ -2082,34 +2136,34 @@ function createNativeScopeMatcher() {
|
|
|
2082
2136
|
}
|
|
2083
2137
|
|
|
2084
2138
|
// packages/runtime/src/control-plane/state-sync/repo.ts
|
|
2085
|
-
import { existsSync as
|
|
2086
|
-
import { resolve as
|
|
2139
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2140
|
+
import { resolve as resolve14 } from "path";
|
|
2087
2141
|
|
|
2088
2142
|
// packages/runtime/src/control-plane/repos/layout.ts
|
|
2089
|
-
import { existsSync as
|
|
2090
|
-
import { basename as basename5, dirname as dirname8, join as join3, resolve as
|
|
2143
|
+
import { existsSync as existsSync11 } from "fs";
|
|
2144
|
+
import { basename as basename5, dirname as dirname8, join as join3, resolve as resolve13 } from "path";
|
|
2091
2145
|
function resolveRepoStateDir(projectRoot) {
|
|
2092
|
-
const normalizedProjectRoot =
|
|
2146
|
+
const normalizedProjectRoot = resolve13(projectRoot);
|
|
2093
2147
|
const projectParent = dirname8(normalizedProjectRoot);
|
|
2094
2148
|
if (basename5(projectParent) === ".worktrees") {
|
|
2095
2149
|
const ownerRoot = dirname8(projectParent);
|
|
2096
|
-
const ownerHasRepoMarkers =
|
|
2150
|
+
const ownerHasRepoMarkers = existsSync11(resolve13(ownerRoot, ".git")) || existsSync11(resolve13(ownerRoot, ".rig", "state"));
|
|
2097
2151
|
if (ownerHasRepoMarkers) {
|
|
2098
|
-
return
|
|
2152
|
+
return resolve13(ownerRoot, ".rig", "state");
|
|
2099
2153
|
}
|
|
2100
2154
|
}
|
|
2101
|
-
return
|
|
2155
|
+
return resolve13(projectRoot, ".rig", "state");
|
|
2102
2156
|
}
|
|
2103
2157
|
function resolveManagedRepoLayout(projectRoot, repoId) {
|
|
2104
|
-
const normalizedProjectRoot =
|
|
2158
|
+
const normalizedProjectRoot = resolve13(projectRoot);
|
|
2105
2159
|
const entry = getManagedRepoEntry(repoId);
|
|
2106
2160
|
const stateDir = resolveRepoStateDir(normalizedProjectRoot);
|
|
2107
2161
|
const metadataRelativePath = join3("repos", entry.id);
|
|
2108
|
-
const metadataRoot =
|
|
2162
|
+
const metadataRoot = resolve13(stateDir, metadataRelativePath);
|
|
2109
2163
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
2110
|
-
const runsInsideTaskWorktree = runtimeWorkspace &&
|
|
2164
|
+
const runsInsideTaskWorktree = runtimeWorkspace && resolve13(runtimeWorkspace) === normalizedProjectRoot || basename5(dirname8(normalizedProjectRoot)) === ".worktrees";
|
|
2111
2165
|
const isPrimaryManagedRepo = listManagedRepoEntries()[0]?.id === repoId;
|
|
2112
|
-
const checkoutRoot = isPrimaryManagedRepo && runsInsideTaskWorktree ? resolveMonorepoRoot(normalizedProjectRoot) : entry.checkoutEnvVar && process.env[entry.checkoutEnvVar]?.trim() ?
|
|
2166
|
+
const checkoutRoot = isPrimaryManagedRepo && runsInsideTaskWorktree ? resolveMonorepoRoot(normalizedProjectRoot) : entry.checkoutEnvVar && process.env[entry.checkoutEnvVar]?.trim() ? resolve13(process.env[entry.checkoutEnvVar].trim()) : resolve13(normalizedProjectRoot, entry.alias);
|
|
2113
2167
|
return {
|
|
2114
2168
|
projectRoot: normalizedProjectRoot,
|
|
2115
2169
|
repoId: entry.id,
|
|
@@ -2117,12 +2171,12 @@ function resolveManagedRepoLayout(projectRoot, repoId) {
|
|
|
2117
2171
|
defaultBranch: entry.defaultBranch,
|
|
2118
2172
|
remoteUrl: entry.remoteEnvVar && process.env[entry.remoteEnvVar]?.trim() ? process.env[entry.remoteEnvVar].trim() : entry.defaultRemoteUrl,
|
|
2119
2173
|
checkoutRoot,
|
|
2120
|
-
worktreesRoot:
|
|
2174
|
+
worktreesRoot: resolve13(checkoutRoot, ".worktrees"),
|
|
2121
2175
|
stateDir,
|
|
2122
2176
|
metadataRoot,
|
|
2123
2177
|
metadataRelativePath,
|
|
2124
|
-
mirrorRoot:
|
|
2125
|
-
mirrorStatePath:
|
|
2178
|
+
mirrorRoot: resolve13(metadataRoot, "mirror.git"),
|
|
2179
|
+
mirrorStatePath: resolve13(metadataRoot, "mirror-state.json"),
|
|
2126
2180
|
mirrorStateRelativePath: join3(metadataRelativePath, "mirror-state.json")
|
|
2127
2181
|
};
|
|
2128
2182
|
}
|
|
@@ -2140,7 +2194,7 @@ function resolveTrackerRepoPath(projectRoot) {
|
|
|
2140
2194
|
const monorepoRoot = resolveMonorepoRoot2(projectRoot);
|
|
2141
2195
|
try {
|
|
2142
2196
|
const layout = resolveMonorepoRepoLayout(projectRoot);
|
|
2143
|
-
if (
|
|
2197
|
+
if (existsSync12(resolve14(layout.mirrorRoot, "HEAD"))) {
|
|
2144
2198
|
return layout.mirrorRoot;
|
|
2145
2199
|
}
|
|
2146
2200
|
} catch {}
|
|
@@ -2151,8 +2205,8 @@ function resolveTrackerRepoPath(projectRoot) {
|
|
|
2151
2205
|
var DEFAULT_READ_DEPS = {
|
|
2152
2206
|
fetchRef: nativeFetchRef,
|
|
2153
2207
|
readBlobAtRef: nativeReadBlobAtRef,
|
|
2154
|
-
exists:
|
|
2155
|
-
readFile: (path) =>
|
|
2208
|
+
exists: existsSync13,
|
|
2209
|
+
readFile: (path) => readFileSync8(path, "utf8")
|
|
2156
2210
|
};
|
|
2157
2211
|
function parseIssueStatus(rawStatus) {
|
|
2158
2212
|
const normalized = normalizeTaskLifecycleStatus(rawStatus);
|
|
@@ -2233,12 +2287,12 @@ function shouldPreferLocalTrackerState(options) {
|
|
|
2233
2287
|
if (runtimeContextPath) {
|
|
2234
2288
|
return true;
|
|
2235
2289
|
}
|
|
2236
|
-
return
|
|
2290
|
+
return existsSync13(resolve15(runtimeWorkspace, ".rig", "runtime-context.json"));
|
|
2237
2291
|
}
|
|
2238
2292
|
function readLocalTrackerState(projectRoot, deps) {
|
|
2239
2293
|
const monorepoRoot = resolveMonorepoRoot2(projectRoot);
|
|
2240
|
-
const issuesPath =
|
|
2241
|
-
const taskStatePath =
|
|
2294
|
+
const issuesPath = resolve15(monorepoRoot, ".beads", "issues.jsonl");
|
|
2295
|
+
const taskStatePath = resolve15(monorepoRoot, ".beads", "task-state.json");
|
|
2242
2296
|
return projectSyncedTrackerSnapshot({
|
|
2243
2297
|
source: "local",
|
|
2244
2298
|
issuesBaseOid: null,
|
|
@@ -2552,7 +2606,7 @@ function readValidationDescriptions(projectRoot) {
|
|
|
2552
2606
|
return readValidationDescriptionMap(raw);
|
|
2553
2607
|
}
|
|
2554
2608
|
function readSourceValidationDescriptions(projectRoot) {
|
|
2555
|
-
const rootRaw = readJsonFile(
|
|
2609
|
+
const rootRaw = readJsonFile(resolve16(projectRoot, "rig", "task-config.json"), {});
|
|
2556
2610
|
const sourcePath = findSourceTaskConfigPath(projectRoot);
|
|
2557
2611
|
const sourceRaw = sourcePath ? readJsonFile(sourcePath, {}) : {};
|
|
2558
2612
|
const rootDescriptions = readValidationDescriptionMap(rootRaw);
|
|
@@ -2628,15 +2682,15 @@ function readValidationDescriptionsFromMeta(meta) {
|
|
|
2628
2682
|
return meta.validation_descriptions;
|
|
2629
2683
|
}
|
|
2630
2684
|
function readLocalSourceTaskStateEnvelope(projectRoot) {
|
|
2631
|
-
const taskStatePath =
|
|
2685
|
+
const taskStatePath = resolve16(resolveMonorepoRoot2(projectRoot), ".beads", "task-state.json");
|
|
2632
2686
|
return readTaskStateMetadataEnvelope(readJsonFile(taskStatePath, {}));
|
|
2633
2687
|
}
|
|
2634
2688
|
function readLocalSourceTaskLifecycleStatus(projectRoot, taskId) {
|
|
2635
|
-
const issuesPath =
|
|
2636
|
-
if (!
|
|
2689
|
+
const issuesPath = resolve16(resolveMonorepoRoot2(projectRoot), ".beads", "issues.jsonl");
|
|
2690
|
+
if (!existsSync14(issuesPath)) {
|
|
2637
2691
|
return null;
|
|
2638
2692
|
}
|
|
2639
|
-
for (const line of
|
|
2693
|
+
for (const line of readFileSync9(issuesPath, "utf8").split(/\r?\n/)) {
|
|
2640
2694
|
const trimmed = line.trim();
|
|
2641
2695
|
if (!trimmed) {
|
|
2642
2696
|
continue;
|
|
@@ -2677,25 +2731,25 @@ function lookupTask(projectRoot, input) {
|
|
|
2677
2731
|
function artifactDirForId(projectRoot, id) {
|
|
2678
2732
|
const workspaceDir = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
2679
2733
|
if (workspaceDir) {
|
|
2680
|
-
const worktreeArtifacts =
|
|
2681
|
-
if (
|
|
2734
|
+
const worktreeArtifacts = resolve16(workspaceDir, "artifacts", id);
|
|
2735
|
+
if (existsSync14(worktreeArtifacts) || existsSync14(resolve16(workspaceDir, "artifacts"))) {
|
|
2682
2736
|
return worktreeArtifacts;
|
|
2683
2737
|
}
|
|
2684
2738
|
}
|
|
2685
2739
|
try {
|
|
2686
2740
|
const paths = resolveHarnessPaths(projectRoot);
|
|
2687
|
-
return
|
|
2741
|
+
return resolve16(paths.artifactsDir, id);
|
|
2688
2742
|
} catch {
|
|
2689
|
-
return
|
|
2743
|
+
return resolve16(resolveMonorepoRoot2(projectRoot), "artifacts", id);
|
|
2690
2744
|
}
|
|
2691
2745
|
}
|
|
2692
2746
|
function resolveTaskConfigPath(projectRoot) {
|
|
2693
2747
|
const paths = resolveHarnessPaths(projectRoot);
|
|
2694
|
-
if (
|
|
2748
|
+
if (existsSync14(paths.taskConfigPath)) {
|
|
2695
2749
|
return paths.taskConfigPath;
|
|
2696
2750
|
}
|
|
2697
2751
|
for (const candidate of sourceTaskConfigCandidates(projectRoot)) {
|
|
2698
|
-
if (
|
|
2752
|
+
if (existsSync14(candidate)) {
|
|
2699
2753
|
return candidate;
|
|
2700
2754
|
}
|
|
2701
2755
|
}
|
|
@@ -2703,7 +2757,7 @@ function resolveTaskConfigPath(projectRoot) {
|
|
|
2703
2757
|
}
|
|
2704
2758
|
function findSourceTaskConfigPath(projectRoot) {
|
|
2705
2759
|
for (const candidate of sourceTaskConfigCandidates(projectRoot)) {
|
|
2706
|
-
if (
|
|
2760
|
+
if (existsSync14(candidate)) {
|
|
2707
2761
|
return candidate;
|
|
2708
2762
|
}
|
|
2709
2763
|
}
|
|
@@ -2716,7 +2770,7 @@ function readAndSyncSourceTaskConfig(projectRoot) {
|
|
|
2716
2770
|
const synced = synchronizeTaskConfigWithTracker(projectRoot, raw);
|
|
2717
2771
|
if (sourcePath && synced.updated) {
|
|
2718
2772
|
try {
|
|
2719
|
-
|
|
2773
|
+
writeFileSync6(sourcePath, `${JSON.stringify(synced.config, null, 2)}
|
|
2720
2774
|
`, "utf-8");
|
|
2721
2775
|
} catch {}
|
|
2722
2776
|
}
|
|
@@ -2768,12 +2822,12 @@ function shouldRefreshAutoSyncedTaskConfigEntry(entry) {
|
|
|
2768
2822
|
return !candidate.role;
|
|
2769
2823
|
}
|
|
2770
2824
|
function readSourceIssueRecords(projectRoot) {
|
|
2771
|
-
const issuesPath =
|
|
2772
|
-
if (!
|
|
2825
|
+
const issuesPath = resolve16(resolveMonorepoRoot2(projectRoot), ".beads", "issues.jsonl");
|
|
2826
|
+
if (!existsSync14(issuesPath)) {
|
|
2773
2827
|
return [];
|
|
2774
2828
|
}
|
|
2775
2829
|
const records = [];
|
|
2776
|
-
for (const line of
|
|
2830
|
+
for (const line of readFileSync9(issuesPath, "utf-8").split(/\r?\n/)) {
|
|
2777
2831
|
const trimmed = line.trim();
|
|
2778
2832
|
if (!trimmed) {
|
|
2779
2833
|
continue;
|
|
@@ -2829,19 +2883,19 @@ function readConfiguredFileTaskConfig(projectRoot) {
|
|
|
2829
2883
|
if (!sourcePath) {
|
|
2830
2884
|
return {};
|
|
2831
2885
|
}
|
|
2832
|
-
const directory =
|
|
2833
|
-
if (!
|
|
2886
|
+
const directory = resolve16(projectRoot, sourcePath);
|
|
2887
|
+
if (!existsSync14(directory)) {
|
|
2834
2888
|
return {};
|
|
2835
2889
|
}
|
|
2836
2890
|
const config = {};
|
|
2837
|
-
for (const name of
|
|
2891
|
+
for (const name of readdirSync3(directory)) {
|
|
2838
2892
|
if (!FILE_TASK_PATTERN2.test(name))
|
|
2839
2893
|
continue;
|
|
2840
|
-
const file =
|
|
2894
|
+
const file = resolve16(directory, name);
|
|
2841
2895
|
try {
|
|
2842
2896
|
if (!statSync3(file).isFile())
|
|
2843
2897
|
continue;
|
|
2844
|
-
const raw = JSON.parse(
|
|
2898
|
+
const raw = JSON.parse(readFileSync9(file, "utf8"));
|
|
2845
2899
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
2846
2900
|
continue;
|
|
2847
2901
|
const record = raw;
|
|
@@ -2883,10 +2937,10 @@ function firstStringList2(...candidates) {
|
|
|
2883
2937
|
return [];
|
|
2884
2938
|
}
|
|
2885
2939
|
function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
2886
|
-
const jsonPath =
|
|
2887
|
-
if (
|
|
2940
|
+
const jsonPath = resolve16(projectRoot, "rig.config.json");
|
|
2941
|
+
if (existsSync14(jsonPath)) {
|
|
2888
2942
|
try {
|
|
2889
|
-
const parsed = JSON.parse(
|
|
2943
|
+
const parsed = JSON.parse(readFileSync9(jsonPath, "utf8"));
|
|
2890
2944
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2891
2945
|
const taskSource = parsed.taskSource;
|
|
2892
2946
|
if (taskSource && typeof taskSource === "object" && !Array.isArray(taskSource)) {
|
|
@@ -2898,12 +2952,12 @@ function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
|
2898
2952
|
return null;
|
|
2899
2953
|
}
|
|
2900
2954
|
}
|
|
2901
|
-
const tsPath =
|
|
2902
|
-
if (!
|
|
2955
|
+
const tsPath = resolve16(projectRoot, "rig.config.ts");
|
|
2956
|
+
if (!existsSync14(tsPath)) {
|
|
2903
2957
|
return null;
|
|
2904
2958
|
}
|
|
2905
2959
|
try {
|
|
2906
|
-
const source =
|
|
2960
|
+
const source = readFileSync9(tsPath, "utf8");
|
|
2907
2961
|
const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
|
|
2908
2962
|
const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
|
|
2909
2963
|
if (kind !== "files") {
|
|
@@ -2917,23 +2971,23 @@ function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
|
2917
2971
|
function sourceTaskConfigCandidates(projectRoot) {
|
|
2918
2972
|
const runtimeContext = loadRuntimeContextFromEnv();
|
|
2919
2973
|
return [
|
|
2920
|
-
runtimeContext?.monorepoMainRoot ?
|
|
2921
|
-
process.env.MONOREPO_MAIN_ROOT?.trim() ?
|
|
2922
|
-
|
|
2974
|
+
runtimeContext?.monorepoMainRoot ? resolve16(runtimeContext.monorepoMainRoot, ".rig", "task-config.json") : "",
|
|
2975
|
+
process.env.MONOREPO_MAIN_ROOT?.trim() ? resolve16(process.env.MONOREPO_MAIN_ROOT.trim(), ".rig", "task-config.json") : "",
|
|
2976
|
+
resolve16(resolveMonorepoRoot2(projectRoot), ".rig", "task-config.json")
|
|
2923
2977
|
].filter(Boolean);
|
|
2924
2978
|
}
|
|
2925
2979
|
|
|
2926
2980
|
// packages/runtime/src/control-plane/native/validator.ts
|
|
2927
|
-
import { existsSync as
|
|
2928
|
-
import { resolve as
|
|
2981
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
2982
|
+
import { resolve as resolve21 } from "path";
|
|
2929
2983
|
|
|
2930
2984
|
// packages/runtime/src/control-plane/native/validator-binaries.ts
|
|
2931
|
-
import { existsSync as
|
|
2932
|
-
import { dirname as dirname10, resolve as
|
|
2985
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync7, rmSync as rmSync5, statSync as statSync4 } from "fs";
|
|
2986
|
+
import { dirname as dirname10, resolve as resolve20 } from "path";
|
|
2933
2987
|
|
|
2934
2988
|
// packages/runtime/src/binary-run.ts
|
|
2935
|
-
import { chmodSync as chmodSync2, cpSync, existsSync as
|
|
2936
|
-
import { basename as basename7, dirname as dirname9, resolve as
|
|
2989
|
+
import { chmodSync as chmodSync2, cpSync, existsSync as existsSync15, mkdirSync as mkdirSync6, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
2990
|
+
import { basename as basename7, dirname as dirname9, resolve as resolve17 } from "path";
|
|
2937
2991
|
import { fileURLToPath } from "url";
|
|
2938
2992
|
import { drainMicrotasks, gcAndSweep } from "bun:jsc";
|
|
2939
2993
|
var runtimeBinaryBuildQueue = Promise.resolve();
|
|
@@ -2959,9 +3013,9 @@ async function buildRuntimeBinary(options) {
|
|
|
2959
3013
|
});
|
|
2960
3014
|
}
|
|
2961
3015
|
async function buildRuntimeBinaryInProcess(options, manifest) {
|
|
2962
|
-
const tempBuildDir =
|
|
2963
|
-
const tempOutputPath =
|
|
2964
|
-
|
|
3016
|
+
const tempBuildDir = resolve17(dirname9(options.outputPath), `.bun-build-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
3017
|
+
const tempOutputPath = resolve17(tempBuildDir, basename7(options.outputPath));
|
|
3018
|
+
mkdirSync6(tempBuildDir, { recursive: true });
|
|
2965
3019
|
await withTemporaryEnv({
|
|
2966
3020
|
...options.env,
|
|
2967
3021
|
...options.define ? { RIG_BUILD_CONFIG_JSON: JSON.stringify(options.define) } : {}
|
|
@@ -2986,7 +3040,7 @@ async function buildRuntimeBinaryInProcess(options, manifest) {
|
|
|
2986
3040
|
`);
|
|
2987
3041
|
throw new Error(`Failed to build ${options.entrypoint}: ${details || "Bun.build() returned errors"}`);
|
|
2988
3042
|
}
|
|
2989
|
-
if (!
|
|
3043
|
+
if (!existsSync15(tempOutputPath)) {
|
|
2990
3044
|
const emitted = buildResult.outputs.map((output) => output.path).join(", ") || "(none)";
|
|
2991
3045
|
throw new Error(`Failed to build ${options.entrypoint}: Bun.build() did not emit ${tempOutputPath}. Emitted: ${emitted}`);
|
|
2992
3046
|
}
|
|
@@ -3001,7 +3055,7 @@ async function buildRuntimeBinaryInProcess(options, manifest) {
|
|
|
3001
3055
|
});
|
|
3002
3056
|
}
|
|
3003
3057
|
})).finally(() => {
|
|
3004
|
-
|
|
3058
|
+
rmSync4(tempBuildDir, { recursive: true, force: true });
|
|
3005
3059
|
});
|
|
3006
3060
|
}
|
|
3007
3061
|
function runBestEffortBuildGc() {
|
|
@@ -3018,8 +3072,8 @@ function runtimeBinaryCacheManifestPath(outputPath) {
|
|
|
3018
3072
|
function resolveRuntimeBinaryBuildOptions(options) {
|
|
3019
3073
|
return {
|
|
3020
3074
|
...options,
|
|
3021
|
-
entrypoint:
|
|
3022
|
-
outputPath:
|
|
3075
|
+
entrypoint: resolve17(options.cwd, options.sourcePath),
|
|
3076
|
+
outputPath: resolve17(options.outputPath)
|
|
3023
3077
|
};
|
|
3024
3078
|
}
|
|
3025
3079
|
function shouldUseRuntimeBinaryBuildWorker() {
|
|
@@ -3033,7 +3087,7 @@ function shouldUseRuntimeBinaryBuildWorker() {
|
|
|
3033
3087
|
}
|
|
3034
3088
|
async function buildRuntimeBinaryViaWorker(options) {
|
|
3035
3089
|
const workerSourcePath = resolveRuntimeBinaryBuildWorkerSourcePath(options);
|
|
3036
|
-
if (!workerSourcePath || !
|
|
3090
|
+
if (!workerSourcePath || !existsSync15(workerSourcePath)) {
|
|
3037
3091
|
await buildRuntimeBinaryInProcess(options, {
|
|
3038
3092
|
manifestPath: runtimeBinaryCacheManifestPath(options.outputPath),
|
|
3039
3093
|
buildKey: createRuntimeBinaryBuildKey({
|
|
@@ -3064,13 +3118,13 @@ async function buildRuntimeBinaryViaWorker(options) {
|
|
|
3064
3118
|
new Response(build.stdout).text(),
|
|
3065
3119
|
new Response(build.stderr).text()
|
|
3066
3120
|
]);
|
|
3067
|
-
|
|
3121
|
+
rmSync4(payloadPath, { force: true });
|
|
3068
3122
|
if (exitCode !== 0) {
|
|
3069
3123
|
throw new Error(`Failed to build ${options.entrypoint}: ${(stderr || stdout || `worker exited ${exitCode}`).trim()}`);
|
|
3070
3124
|
}
|
|
3071
3125
|
}
|
|
3072
3126
|
function createRuntimeBinaryBuildWorkerPayloadPath(outputPath) {
|
|
3073
|
-
return
|
|
3127
|
+
return resolve17(dirname9(outputPath), `.bun-build-worker-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
|
3074
3128
|
}
|
|
3075
3129
|
function resolveRuntimeBinaryBuildWorkerSourcePath(options) {
|
|
3076
3130
|
const envRoots = [
|
|
@@ -3079,13 +3133,13 @@ function resolveRuntimeBinaryBuildWorkerSourcePath(options) {
|
|
|
3079
3133
|
process.env.PROJECT_RIG_ROOT?.trim()
|
|
3080
3134
|
].filter(Boolean);
|
|
3081
3135
|
for (const root of envRoots) {
|
|
3082
|
-
const candidate =
|
|
3083
|
-
if (
|
|
3136
|
+
const candidate = resolve17(root, "packages/runtime/src/binary-build-worker.ts");
|
|
3137
|
+
if (existsSync15(candidate)) {
|
|
3084
3138
|
return candidate;
|
|
3085
3139
|
}
|
|
3086
3140
|
}
|
|
3087
|
-
const localCandidate =
|
|
3088
|
-
return
|
|
3141
|
+
const localCandidate = resolve17(import.meta.dir, "binary-build-worker.ts");
|
|
3142
|
+
return existsSync15(localCandidate) ? localCandidate : null;
|
|
3089
3143
|
}
|
|
3090
3144
|
function resolveRuntimeBinaryBuildWorkerInvocation() {
|
|
3091
3145
|
const bunPath = Bun.which("bun");
|
|
@@ -3121,7 +3175,7 @@ function createRuntimeBinaryBuildKey(input) {
|
|
|
3121
3175
|
});
|
|
3122
3176
|
}
|
|
3123
3177
|
async function isRuntimeBinaryBuildFresh(input) {
|
|
3124
|
-
if (!
|
|
3178
|
+
if (!existsSync15(input.outputPath) || !existsSync15(input.manifestPath)) {
|
|
3125
3179
|
return false;
|
|
3126
3180
|
}
|
|
3127
3181
|
let manifest = null;
|
|
@@ -3134,7 +3188,7 @@ async function isRuntimeBinaryBuildFresh(input) {
|
|
|
3134
3188
|
return false;
|
|
3135
3189
|
}
|
|
3136
3190
|
for (const [filePath, expectedDigest] of Object.entries(manifest.inputs || {})) {
|
|
3137
|
-
if (!
|
|
3191
|
+
if (!existsSync15(filePath)) {
|
|
3138
3192
|
return false;
|
|
3139
3193
|
}
|
|
3140
3194
|
if (await sha256File(filePath) !== expectedDigest) {
|
|
@@ -3147,7 +3201,7 @@ async function writeRuntimeBinaryCacheManifest(input) {
|
|
|
3147
3201
|
const inputs = {};
|
|
3148
3202
|
for (const inputPath of Object.keys(input.metafile?.inputs || {}).sort()) {
|
|
3149
3203
|
const normalized = normalizeBuildInputPath(input.cwd, inputPath);
|
|
3150
|
-
if (!normalized || !
|
|
3204
|
+
if (!normalized || !existsSync15(normalized)) {
|
|
3151
3205
|
continue;
|
|
3152
3206
|
}
|
|
3153
3207
|
inputs[normalized] = await sha256File(normalized);
|
|
@@ -3170,7 +3224,7 @@ function normalizeBuildInputPath(cwd, inputPath) {
|
|
|
3170
3224
|
if (inputPath.startsWith("<")) {
|
|
3171
3225
|
return null;
|
|
3172
3226
|
}
|
|
3173
|
-
return
|
|
3227
|
+
return resolve17(cwd, inputPath);
|
|
3174
3228
|
}
|
|
3175
3229
|
async function sha256File(path) {
|
|
3176
3230
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
@@ -3186,8 +3240,8 @@ function sortRecord(value) {
|
|
|
3186
3240
|
async function runSerializedRuntimeBinaryBuild(action) {
|
|
3187
3241
|
const previous = runtimeBinaryBuildQueue;
|
|
3188
3242
|
let release;
|
|
3189
|
-
runtimeBinaryBuildQueue = new Promise((
|
|
3190
|
-
release =
|
|
3243
|
+
runtimeBinaryBuildQueue = new Promise((resolve18) => {
|
|
3244
|
+
release = resolve18;
|
|
3191
3245
|
});
|
|
3192
3246
|
await previous;
|
|
3193
3247
|
try {
|
|
@@ -3232,11 +3286,11 @@ async function withTemporaryCwd(cwd, action) {
|
|
|
3232
3286
|
}
|
|
3233
3287
|
|
|
3234
3288
|
// packages/runtime/src/control-plane/runtime/provisioning-env.ts
|
|
3235
|
-
import { delimiter, resolve as
|
|
3289
|
+
import { delimiter, resolve as resolve19 } from "path";
|
|
3236
3290
|
|
|
3237
3291
|
// packages/runtime/src/control-plane/runtime/runtime-paths.ts
|
|
3238
|
-
import { existsSync as
|
|
3239
|
-
import { resolve as
|
|
3292
|
+
import { existsSync as existsSync16, readdirSync as readdirSync4, realpathSync } from "fs";
|
|
3293
|
+
import { resolve as resolve18 } from "path";
|
|
3240
3294
|
|
|
3241
3295
|
// packages/runtime/src/control-plane/runtime/sandbox/utils.ts
|
|
3242
3296
|
function uniq(values) {
|
|
@@ -3254,7 +3308,7 @@ function resolveBunBinaryPath() {
|
|
|
3254
3308
|
}
|
|
3255
3309
|
const home = process.env.HOME?.trim();
|
|
3256
3310
|
const fallbackCandidates = [
|
|
3257
|
-
home ?
|
|
3311
|
+
home ? resolve18(home, ".bun/bin/bun") : "",
|
|
3258
3312
|
"/opt/homebrew/bin/bun",
|
|
3259
3313
|
"/usr/local/bin/bun",
|
|
3260
3314
|
"/usr/bin/bun"
|
|
@@ -3282,8 +3336,8 @@ function resolveClaudeBinaryPath() {
|
|
|
3282
3336
|
}
|
|
3283
3337
|
const home = process.env.HOME?.trim();
|
|
3284
3338
|
const fallbackCandidates = [
|
|
3285
|
-
home ?
|
|
3286
|
-
home ?
|
|
3339
|
+
home ? resolve18(home, ".local/bin/claude") : "",
|
|
3340
|
+
home ? resolve18(home, ".local/share/claude/local/claude") : "",
|
|
3287
3341
|
"/opt/homebrew/bin/claude",
|
|
3288
3342
|
"/usr/local/bin/claude",
|
|
3289
3343
|
"/usr/bin/claude"
|
|
@@ -3297,51 +3351,51 @@ function resolveClaudeBinaryPath() {
|
|
|
3297
3351
|
throw new Error("claude not found in PATH");
|
|
3298
3352
|
}
|
|
3299
3353
|
function resolveBunInstallDir(bunBinaryPath = resolveBunBinaryPath()) {
|
|
3300
|
-
return
|
|
3354
|
+
return resolve18(bunBinaryPath, "../..");
|
|
3301
3355
|
}
|
|
3302
3356
|
function resolveClaudeInstallDir() {
|
|
3303
3357
|
const realPath = resolveClaudeBinaryPath();
|
|
3304
|
-
return
|
|
3358
|
+
return resolve18(realPath, "..");
|
|
3305
3359
|
}
|
|
3306
3360
|
function resolveNodeInstallDir() {
|
|
3307
3361
|
const preferredNode = resolvePreferredNodeBinary();
|
|
3308
3362
|
if (!preferredNode)
|
|
3309
3363
|
return null;
|
|
3310
3364
|
const explicitNode = process.env.RIG_NODE_BIN?.trim();
|
|
3311
|
-
if (explicitNode &&
|
|
3312
|
-
return preferredNode.endsWith("/bin/node") ?
|
|
3365
|
+
if (explicitNode && resolve18(explicitNode) === resolve18(preferredNode)) {
|
|
3366
|
+
return preferredNode.endsWith("/bin/node") ? resolve18(preferredNode, "../..") : resolve18(preferredNode, "..");
|
|
3313
3367
|
}
|
|
3314
3368
|
try {
|
|
3315
3369
|
const realPath = realpathSync(preferredNode);
|
|
3316
3370
|
if (realPath.endsWith("/bin/node")) {
|
|
3317
|
-
return
|
|
3371
|
+
return resolve18(realPath, "../..");
|
|
3318
3372
|
}
|
|
3319
|
-
return
|
|
3373
|
+
return resolve18(realPath, "..");
|
|
3320
3374
|
} catch {
|
|
3321
|
-
return
|
|
3375
|
+
return resolve18(preferredNode, "..");
|
|
3322
3376
|
}
|
|
3323
3377
|
}
|
|
3324
3378
|
function resolvePreferredNodeBinary() {
|
|
3325
3379
|
const candidates = [];
|
|
3326
3380
|
const envNode = process.env.RIG_NODE_BIN?.trim();
|
|
3327
3381
|
if (envNode) {
|
|
3328
|
-
const explicit =
|
|
3329
|
-
if (
|
|
3382
|
+
const explicit = resolve18(envNode);
|
|
3383
|
+
if (existsSync16(explicit)) {
|
|
3330
3384
|
return explicit;
|
|
3331
3385
|
}
|
|
3332
3386
|
}
|
|
3333
3387
|
const nvmBin = process.env.NVM_BIN?.trim();
|
|
3334
3388
|
if (nvmBin) {
|
|
3335
|
-
candidates.push(
|
|
3389
|
+
candidates.push(resolve18(nvmBin, "node"));
|
|
3336
3390
|
}
|
|
3337
3391
|
const home = process.env.HOME?.trim();
|
|
3338
3392
|
if (home) {
|
|
3339
|
-
const nvmVersionsDir =
|
|
3340
|
-
if (
|
|
3393
|
+
const nvmVersionsDir = resolve18(home, ".nvm/versions/node");
|
|
3394
|
+
if (existsSync16(nvmVersionsDir)) {
|
|
3341
3395
|
try {
|
|
3342
|
-
const versionDirs =
|
|
3396
|
+
const versionDirs = readdirSync4(nvmVersionsDir).map((entry) => entry.trim()).filter((entry) => /^v\d+\.\d+\.\d+$/.test(entry)).sort((a, b) => Bun.semver.order(b.replace(/^v/, ""), a.replace(/^v/, "")));
|
|
3343
3397
|
for (const versionDir of versionDirs) {
|
|
3344
|
-
candidates.push(
|
|
3398
|
+
candidates.push(resolve18(nvmVersionsDir, versionDir, "bin/node"));
|
|
3345
3399
|
}
|
|
3346
3400
|
} catch {}
|
|
3347
3401
|
}
|
|
@@ -3350,8 +3404,8 @@ function resolvePreferredNodeBinary() {
|
|
|
3350
3404
|
if (whichNode) {
|
|
3351
3405
|
candidates.push(whichNode);
|
|
3352
3406
|
}
|
|
3353
|
-
const deduped = uniq(candidates.map((candidate) =>
|
|
3354
|
-
const existing = deduped.filter((candidate) =>
|
|
3407
|
+
const deduped = uniq(candidates.map((candidate) => resolve18(candidate)));
|
|
3408
|
+
const existing = deduped.filter((candidate) => existsSync16(candidate));
|
|
3355
3409
|
if (existing.length === 0) {
|
|
3356
3410
|
return null;
|
|
3357
3411
|
}
|
|
@@ -3365,7 +3419,7 @@ function resolvePreferredNodeBinary() {
|
|
|
3365
3419
|
return existing[0] ?? null;
|
|
3366
3420
|
}
|
|
3367
3421
|
function inferNodeMajor(nodeBinaryPath) {
|
|
3368
|
-
const normalized =
|
|
3422
|
+
const normalized = resolve18(nodeBinaryPath).replace(/\\/g, "/");
|
|
3369
3423
|
const match = normalized.match(/(?:^|\/)(?:node-)?v?(\d+)\.\d+\.\d+(?:\/|$)/);
|
|
3370
3424
|
if (!match) {
|
|
3371
3425
|
return null;
|
|
@@ -3377,8 +3431,8 @@ function normalizeExecutablePath(candidate) {
|
|
|
3377
3431
|
if (!candidate) {
|
|
3378
3432
|
return "";
|
|
3379
3433
|
}
|
|
3380
|
-
const normalized =
|
|
3381
|
-
if (!
|
|
3434
|
+
const normalized = resolve18(candidate);
|
|
3435
|
+
if (!existsSync16(normalized)) {
|
|
3382
3436
|
return "";
|
|
3383
3437
|
}
|
|
3384
3438
|
try {
|
|
@@ -3388,7 +3442,7 @@ function normalizeExecutablePath(candidate) {
|
|
|
3388
3442
|
}
|
|
3389
3443
|
}
|
|
3390
3444
|
function looksLikeRuntimeGateway(candidate) {
|
|
3391
|
-
const normalized =
|
|
3445
|
+
const normalized = resolve18(candidate).replace(/\\/g, "/");
|
|
3392
3446
|
return normalized.includes("/.rig/bin/") || normalized.endsWith("/rig-shell") || normalized.endsWith("/rig-agent");
|
|
3393
3447
|
}
|
|
3394
3448
|
|
|
@@ -3409,7 +3463,7 @@ function runtimeProvisioningEnv(baseEnv = process.env) {
|
|
|
3409
3463
|
try {
|
|
3410
3464
|
return resolveClaudeInstallDir();
|
|
3411
3465
|
} catch {
|
|
3412
|
-
return
|
|
3466
|
+
return resolve19(claudeBinary, "..");
|
|
3413
3467
|
}
|
|
3414
3468
|
})() : "";
|
|
3415
3469
|
const nodeDir = resolveNodeInstallDir();
|
|
@@ -3419,8 +3473,8 @@ function runtimeProvisioningEnv(baseEnv = process.env) {
|
|
|
3419
3473
|
`${bunDir}/bin`,
|
|
3420
3474
|
claudeDir,
|
|
3421
3475
|
nodeDir ? `${nodeDir}/bin` : "",
|
|
3422
|
-
realHome ?
|
|
3423
|
-
realHome ?
|
|
3476
|
+
realHome ? resolve19(realHome, ".local/bin") : "",
|
|
3477
|
+
realHome ? resolve19(realHome, ".cargo/bin") : "",
|
|
3424
3478
|
...inheritedPath,
|
|
3425
3479
|
"/usr/local/bin",
|
|
3426
3480
|
"/usr/local/sbin",
|
|
@@ -3451,9 +3505,9 @@ function runtimeProvisioningEnv(baseEnv = process.env) {
|
|
|
3451
3505
|
// packages/runtime/src/control-plane/native/validator-binaries.ts
|
|
3452
3506
|
function resolveValidatorBinaryPath(projectRoot, binaryName, runtimeContext) {
|
|
3453
3507
|
if (runtimeContext) {
|
|
3454
|
-
return
|
|
3508
|
+
return resolve20(runtimeContext.binDir, "validators", binaryName);
|
|
3455
3509
|
}
|
|
3456
|
-
return
|
|
3510
|
+
return resolve20(resolveHarnessPaths(projectRoot).binDir, "validators", binaryName);
|
|
3457
3511
|
}
|
|
3458
3512
|
async function ensureValidatorBinary(projectRoot, checkId, runtimeContext) {
|
|
3459
3513
|
const match = checkId.match(/^([a-z][\w-]*):([a-z][\w-]*)$/);
|
|
@@ -3468,19 +3522,19 @@ async function ensureValidatorBinary(projectRoot, checkId, runtimeContext) {
|
|
|
3468
3522
|
const binaryName = `${category}-${check}`;
|
|
3469
3523
|
const binaryPath = resolveValidatorBinaryPath(projectRoot, binaryName, runtimeContext);
|
|
3470
3524
|
const hostProjectRoot = runtimeContext?.hostProjectRoot?.trim() || projectRoot;
|
|
3471
|
-
const sourcePath =
|
|
3472
|
-
if (!
|
|
3525
|
+
const sourcePath = resolve20(hostProjectRoot, "packages/runtime/src/control-plane/validators", category, `${check}.ts`);
|
|
3526
|
+
if (!existsSync17(sourcePath)) {
|
|
3473
3527
|
return null;
|
|
3474
3528
|
}
|
|
3475
3529
|
const sourceMtime = statSync4(sourcePath).mtimeMs;
|
|
3476
|
-
const binaryExists =
|
|
3530
|
+
const binaryExists = existsSync17(binaryPath);
|
|
3477
3531
|
const binaryMtime = binaryExists ? statSync4(binaryPath).mtimeMs : 0;
|
|
3478
3532
|
if (!binaryExists || sourceMtime > binaryMtime) {
|
|
3479
3533
|
if (binaryExists) {
|
|
3480
|
-
|
|
3481
|
-
|
|
3534
|
+
rmSync5(binaryPath, { force: true });
|
|
3535
|
+
rmSync5(`${binaryPath}.build-manifest.json`, { force: true });
|
|
3482
3536
|
}
|
|
3483
|
-
|
|
3537
|
+
mkdirSync7(dirname10(binaryPath), { recursive: true });
|
|
3484
3538
|
await buildRuntimeBinary({
|
|
3485
3539
|
sourcePath: `packages/runtime/src/control-plane/validators/${category}/${check}.ts`,
|
|
3486
3540
|
outputPath: binaryPath,
|
|
@@ -3489,7 +3543,7 @@ async function ensureValidatorBinary(projectRoot, checkId, runtimeContext) {
|
|
|
3489
3543
|
env: runtimeProvisioningEnv()
|
|
3490
3544
|
});
|
|
3491
3545
|
}
|
|
3492
|
-
return
|
|
3546
|
+
return existsSync17(binaryPath) ? binaryPath : null;
|
|
3493
3547
|
}
|
|
3494
3548
|
|
|
3495
3549
|
// packages/runtime/src/control-plane/native/validator.ts
|
|
@@ -3526,20 +3580,20 @@ async function readTaskSourceValidation(projectRoot, taskId) {
|
|
|
3526
3580
|
function resolveValidationPaths(projectRoot, taskId, runtimeContext) {
|
|
3527
3581
|
if (runtimeContext) {
|
|
3528
3582
|
return {
|
|
3529
|
-
taskLogDir:
|
|
3530
|
-
artifactDir:
|
|
3583
|
+
taskLogDir: resolve21(runtimeContext.logsDir, taskId),
|
|
3584
|
+
artifactDir: resolve21(runtimeContext.workspaceDir, "artifacts", taskId)
|
|
3531
3585
|
};
|
|
3532
3586
|
}
|
|
3533
3587
|
const paths = resolveHarnessPaths(projectRoot);
|
|
3534
3588
|
return {
|
|
3535
|
-
taskLogDir:
|
|
3536
|
-
artifactDir:
|
|
3589
|
+
taskLogDir: resolve21(paths.logsDir, taskId),
|
|
3590
|
+
artifactDir: resolve21(paths.artifactsDir, taskId)
|
|
3537
3591
|
};
|
|
3538
3592
|
}
|
|
3539
3593
|
async function runValidatorBinary(projectRoot, taskId, checkId, runtimeContext) {
|
|
3540
3594
|
const binaryName = checkId.replace(":", "-");
|
|
3541
3595
|
const binaryPath = await ensureValidatorBinary(projectRoot, checkId, runtimeContext) ?? resolveValidatorBinaryPath(projectRoot, binaryName, runtimeContext);
|
|
3542
|
-
if (!
|
|
3596
|
+
if (!existsSync18(binaryPath)) {
|
|
3543
3597
|
return {
|
|
3544
3598
|
result: {
|
|
3545
3599
|
id: checkId,
|
|
@@ -3550,7 +3604,7 @@ async function runValidatorBinary(projectRoot, taskId, checkId, runtimeContext)
|
|
|
3550
3604
|
};
|
|
3551
3605
|
}
|
|
3552
3606
|
const validatorCwd = runtimeContext?.workspaceDir || resolveMonorepoRoot(projectRoot);
|
|
3553
|
-
const runtimeShellPath = runtimeContext ?
|
|
3607
|
+
const runtimeShellPath = runtimeContext ? resolve21(runtimeContext.binDir, "rig-shell") : "";
|
|
3554
3608
|
const monorepoMainRoot = runtimeContext?.monorepoMainRoot || process.env.MONOREPO_MAIN_ROOT?.trim() || resolveMonorepoRoot(projectRoot);
|
|
3555
3609
|
const validatorEnv = {
|
|
3556
3610
|
PROJECT_RIG_ROOT: runtimeContext?.hostProjectRoot || projectRoot,
|
|
@@ -3565,7 +3619,7 @@ async function runValidatorBinary(projectRoot, taskId, checkId, runtimeContext)
|
|
|
3565
3619
|
validatorEnv.RIG_LOGS_DIR = runtimeContext.logsDir;
|
|
3566
3620
|
validatorEnv.RIG_RUNTIME_BIN_DIR = runtimeContext.binDir;
|
|
3567
3621
|
}
|
|
3568
|
-
const { exitCode, stdout, stderr } = await runCaptureAsync(runtimeShellPath &&
|
|
3622
|
+
const { exitCode, stdout, stderr } = await runCaptureAsync(runtimeShellPath && existsSync18(runtimeShellPath) ? [runtimeShellPath, "run-binary", binaryPath] : [binaryPath], validatorCwd, validatorEnv);
|
|
3569
3623
|
try {
|
|
3570
3624
|
const result = JSON.parse(stdout.trim());
|
|
3571
3625
|
return { result, exitCode };
|
|
@@ -3605,8 +3659,8 @@ async function validateTask(projectRoot, taskId, runtimeContext, registry, optio
|
|
|
3605
3659
|
const configuredValidation = stringArray(taskConfig[taskId]?.validation);
|
|
3606
3660
|
const commands = resolvedContext?.validation?.length ? resolvedContext.validation : configuredValidation.length > 0 ? configuredValidation : sourceValidation.validation;
|
|
3607
3661
|
const { taskLogDir, artifactDir } = resolveValidationPaths(projectRoot, taskId, resolvedContext);
|
|
3608
|
-
|
|
3609
|
-
|
|
3662
|
+
mkdirSync8(taskLogDir, { recursive: true });
|
|
3663
|
+
mkdirSync8(artifactDir, { recursive: true });
|
|
3610
3664
|
if (commands.length === 0) {
|
|
3611
3665
|
const skipped = {
|
|
3612
3666
|
status: "skipped",
|
|
@@ -3615,7 +3669,7 @@ async function validateTask(projectRoot, taskId, runtimeContext, registry, optio
|
|
|
3615
3669
|
failed: 0,
|
|
3616
3670
|
categories: []
|
|
3617
3671
|
};
|
|
3618
|
-
|
|
3672
|
+
writeFileSync8(resolve21(artifactDir, "validation-summary.json"), `${JSON.stringify(skipped, null, 2)}
|
|
3619
3673
|
`, "utf-8");
|
|
3620
3674
|
return skipped;
|
|
3621
3675
|
}
|
|
@@ -3650,18 +3704,18 @@ async function validateTask(projectRoot, taskId, runtimeContext, registry, optio
|
|
|
3650
3704
|
exit_code: 2,
|
|
3651
3705
|
duration_seconds: 0
|
|
3652
3706
|
});
|
|
3653
|
-
const logFile2 =
|
|
3654
|
-
|
|
3655
|
-
|
|
3707
|
+
const logFile2 = resolve21(taskLogDir, `invalid-entry-validation.log`);
|
|
3708
|
+
mkdirSync8(taskLogDir, { recursive: true });
|
|
3709
|
+
writeFileSync8(logFile2, `=== ${nowIso()} :: ${cmd} ===
|
|
3656
3710
|
Invalid validation entry: not a check-ID. All entries must use format "category:check-name".
|
|
3657
3711
|
`, "utf-8");
|
|
3658
3712
|
continue;
|
|
3659
3713
|
}
|
|
3660
3714
|
const { result, exitCode } = await dispatchValidator(cmd, effectiveRegistry, validatorCtx, (id) => runValidatorBinary(projectRoot, taskId, id, resolvedContext));
|
|
3661
3715
|
const durationSeconds = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
3662
|
-
const logFile =
|
|
3663
|
-
|
|
3664
|
-
|
|
3716
|
+
const logFile = resolve21(taskLogDir, `${cmd.replace(":", "-")}-validation.log`);
|
|
3717
|
+
mkdirSync8(taskLogDir, { recursive: true });
|
|
3718
|
+
writeFileSync8(logFile, `=== ${nowIso()} :: ${cmd} ===
|
|
3665
3719
|
${JSON.stringify(result, null, 2)}
|
|
3666
3720
|
`, "utf-8");
|
|
3667
3721
|
if (result.passed) {
|
|
@@ -3683,15 +3737,15 @@ ${JSON.stringify(result, null, 2)}
|
|
|
3683
3737
|
failed,
|
|
3684
3738
|
categories
|
|
3685
3739
|
};
|
|
3686
|
-
|
|
3687
|
-
|
|
3740
|
+
mkdirSync8(artifactDir, { recursive: true });
|
|
3741
|
+
writeFileSync8(resolve21(artifactDir, "validation-summary.json"), `${JSON.stringify(summary, null, 2)}
|
|
3688
3742
|
`, "utf-8");
|
|
3689
3743
|
return summary;
|
|
3690
3744
|
}
|
|
3691
3745
|
|
|
3692
3746
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3693
|
-
import { existsSync as
|
|
3694
|
-
import { resolve as
|
|
3747
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
3748
|
+
import { resolve as resolve23 } from "path";
|
|
3695
3749
|
|
|
3696
3750
|
// packages/runtime/src/control-plane/runtime/baked-secrets.ts
|
|
3697
3751
|
var BAKED_RUNTIME_SECRETS = {
|
|
@@ -3737,8 +3791,1253 @@ function resolveRuntimeSecrets(env, baked = BAKED_RUNTIME_SECRETS) {
|
|
|
3737
3791
|
}
|
|
3738
3792
|
|
|
3739
3793
|
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
3740
|
-
import { existsSync as
|
|
3741
|
-
import { dirname as dirname11, isAbsolute as isAbsolute2, resolve as
|
|
3794
|
+
import { existsSync as existsSync19, lstatSync, mkdirSync as mkdirSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
|
|
3795
|
+
import { dirname as dirname11, isAbsolute as isAbsolute2, resolve as resolve22 } from "path";
|
|
3796
|
+
|
|
3797
|
+
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
3798
|
+
function parseJsonObject(value) {
|
|
3799
|
+
if (!value?.trim())
|
|
3800
|
+
return { value: {}, error: "empty JSON output" };
|
|
3801
|
+
try {
|
|
3802
|
+
const parsed = JSON.parse(value);
|
|
3803
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
|
|
3804
|
+
} catch (error) {
|
|
3805
|
+
return { value: {}, error: error instanceof Error ? error.message : String(error) };
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
function flattenPaginatedArray(value) {
|
|
3809
|
+
if (!Array.isArray(value))
|
|
3810
|
+
return null;
|
|
3811
|
+
if (value.every((entry) => Array.isArray(entry))) {
|
|
3812
|
+
return value.flatMap((entry) => entry);
|
|
3813
|
+
}
|
|
3814
|
+
return value;
|
|
3815
|
+
}
|
|
3816
|
+
function parseConcatenatedJsonValues(value) {
|
|
3817
|
+
const text = value.trim();
|
|
3818
|
+
const docs = [];
|
|
3819
|
+
let start = null;
|
|
3820
|
+
let depth = 0;
|
|
3821
|
+
let inString = false;
|
|
3822
|
+
let escape = false;
|
|
3823
|
+
for (let index = 0;index < text.length; index += 1) {
|
|
3824
|
+
const char = text[index];
|
|
3825
|
+
if (start === null) {
|
|
3826
|
+
if (/\s/.test(char))
|
|
3827
|
+
continue;
|
|
3828
|
+
start = index;
|
|
3829
|
+
}
|
|
3830
|
+
if (inString) {
|
|
3831
|
+
if (escape) {
|
|
3832
|
+
escape = false;
|
|
3833
|
+
} else if (char === "\\") {
|
|
3834
|
+
escape = true;
|
|
3835
|
+
} else if (char === '"') {
|
|
3836
|
+
inString = false;
|
|
3837
|
+
}
|
|
3838
|
+
continue;
|
|
3839
|
+
}
|
|
3840
|
+
if (char === '"') {
|
|
3841
|
+
inString = true;
|
|
3842
|
+
continue;
|
|
3843
|
+
}
|
|
3844
|
+
if (char === "{" || char === "[") {
|
|
3845
|
+
depth += 1;
|
|
3846
|
+
continue;
|
|
3847
|
+
}
|
|
3848
|
+
if (char === "}" || char === "]") {
|
|
3849
|
+
depth -= 1;
|
|
3850
|
+
if (depth < 0)
|
|
3851
|
+
return { value: docs, error: "unexpected JSON close delimiter" };
|
|
3852
|
+
if (depth === 0 && start !== null) {
|
|
3853
|
+
const segment = text.slice(start, index + 1);
|
|
3854
|
+
try {
|
|
3855
|
+
docs.push(JSON.parse(segment));
|
|
3856
|
+
} catch (error) {
|
|
3857
|
+
return { value: docs, error: error instanceof Error ? error.message : String(error) };
|
|
3858
|
+
}
|
|
3859
|
+
start = null;
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
if (inString || depth !== 0 || start !== null)
|
|
3864
|
+
return { value: docs, error: "incomplete JSON stream" };
|
|
3865
|
+
return { value: docs };
|
|
3866
|
+
}
|
|
3867
|
+
function parseJsonArray(value) {
|
|
3868
|
+
if (!value?.trim())
|
|
3869
|
+
return { value: [], error: "empty JSON output" };
|
|
3870
|
+
try {
|
|
3871
|
+
const parsed = JSON.parse(value);
|
|
3872
|
+
const flattened = flattenPaginatedArray(parsed);
|
|
3873
|
+
return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
3874
|
+
} catch (error) {
|
|
3875
|
+
const streamed = parseConcatenatedJsonValues(value);
|
|
3876
|
+
if (streamed.error)
|
|
3877
|
+
return { value: [], error: error instanceof Error ? error.message : String(error) };
|
|
3878
|
+
const flattened = streamed.value.flatMap((entry) => flattenPaginatedArray(entry) ?? []);
|
|
3879
|
+
return flattened.length > 0 || streamed.value.length === 0 ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
function parseGithubPrUrl(prUrl) {
|
|
3883
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
|
|
3884
|
+
if (!match)
|
|
3885
|
+
return null;
|
|
3886
|
+
const prNumber = Number.parseInt(match[3], 10);
|
|
3887
|
+
if (!Number.isFinite(prNumber))
|
|
3888
|
+
return null;
|
|
3889
|
+
return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
|
|
3890
|
+
}
|
|
3891
|
+
function checkName(check) {
|
|
3892
|
+
return String(check.name ?? check.context ?? "").trim();
|
|
3893
|
+
}
|
|
3894
|
+
function checkState(check) {
|
|
3895
|
+
return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
|
|
3896
|
+
}
|
|
3897
|
+
function isGreptileLabel(value) {
|
|
3898
|
+
return String(value ?? "").toLowerCase().includes("greptile");
|
|
3899
|
+
}
|
|
3900
|
+
function isGreptileGithubLogin(value) {
|
|
3901
|
+
const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
|
|
3902
|
+
return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
|
|
3903
|
+
}
|
|
3904
|
+
function isPassingCheck(check) {
|
|
3905
|
+
const state = checkState(check);
|
|
3906
|
+
return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
|
|
3907
|
+
}
|
|
3908
|
+
function isPendingCheck(check) {
|
|
3909
|
+
const state = checkState(check);
|
|
3910
|
+
return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
|
|
3911
|
+
}
|
|
3912
|
+
function isFailingCheck(check) {
|
|
3913
|
+
const state = checkState(check);
|
|
3914
|
+
return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
|
|
3915
|
+
}
|
|
3916
|
+
function wildcardToRegExp(pattern) {
|
|
3917
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3918
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
3919
|
+
}
|
|
3920
|
+
function isAllowedFailure(name, allowedFailures) {
|
|
3921
|
+
return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
|
|
3922
|
+
}
|
|
3923
|
+
function greptileScorePatterns() {
|
|
3924
|
+
return [
|
|
3925
|
+
/\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
|
|
3926
|
+
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
|
|
3927
|
+
/\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
|
|
3928
|
+
];
|
|
3929
|
+
}
|
|
3930
|
+
function parseGreptileScores(input) {
|
|
3931
|
+
const text = stripHtml(input);
|
|
3932
|
+
const seen = new Set;
|
|
3933
|
+
const scores = [];
|
|
3934
|
+
for (const pattern of greptileScorePatterns()) {
|
|
3935
|
+
for (const match of text.matchAll(pattern)) {
|
|
3936
|
+
const value = Number.parseInt(match[1] || "", 10);
|
|
3937
|
+
const scale = Number.parseInt(match[2] || "", 10);
|
|
3938
|
+
if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
|
|
3939
|
+
continue;
|
|
3940
|
+
const raw = match[0] || `${value}/${scale}`;
|
|
3941
|
+
const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
|
|
3942
|
+
if (seen.has(key))
|
|
3943
|
+
continue;
|
|
3944
|
+
seen.add(key);
|
|
3945
|
+
scores.push({ value, scale, raw });
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
return scores;
|
|
3949
|
+
}
|
|
3950
|
+
function parseGreptileScore(input) {
|
|
3951
|
+
return parseGreptileScores(input)[0] ?? null;
|
|
3952
|
+
}
|
|
3953
|
+
function stripHtml(input) {
|
|
3954
|
+
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
3955
|
+
|
|
3956
|
+
`).trim();
|
|
3957
|
+
}
|
|
3958
|
+
function containsBlockerText(input) {
|
|
3959
|
+
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3960
|
+
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);
|
|
3961
|
+
}
|
|
3962
|
+
function isStrictFiveOfFive(score) {
|
|
3963
|
+
return score.value === 5 && score.scale === 5;
|
|
3964
|
+
}
|
|
3965
|
+
function containsConflictingScoreText(input) {
|
|
3966
|
+
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
3967
|
+
}
|
|
3968
|
+
function extractGreptileCommentBlock(input) {
|
|
3969
|
+
const match = input.match(/<!--\s*greptile_comment\s*-->([\s\S]*?)<!--\s*\/greptile_comment\s*-->/i);
|
|
3970
|
+
return match?.[1]?.trim() ?? null;
|
|
3971
|
+
}
|
|
3972
|
+
function extractGreptileBodyReviewedSha(input) {
|
|
3973
|
+
const block = extractGreptileCommentBlock(input);
|
|
3974
|
+
if (!block)
|
|
3975
|
+
return null;
|
|
3976
|
+
const commitLink = block.match(/\/commit\/([0-9a-f]{40})(?:\b|[^0-9a-f])/i);
|
|
3977
|
+
return commitLink?.[1]?.toLowerCase() ?? null;
|
|
3978
|
+
}
|
|
3979
|
+
function isoAtOrAfter(value, floor) {
|
|
3980
|
+
if (!value || !floor)
|
|
3981
|
+
return false;
|
|
3982
|
+
const valueMs = Date.parse(value);
|
|
3983
|
+
const floorMs = Date.parse(floor);
|
|
3984
|
+
return Number.isFinite(valueMs) && Number.isFinite(floorMs) && valueMs >= floorMs;
|
|
3985
|
+
}
|
|
3986
|
+
function greptileStatusVerdict(status) {
|
|
3987
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
3988
|
+
if (!normalized)
|
|
3989
|
+
return null;
|
|
3990
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
3991
|
+
return "approved";
|
|
3992
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
3993
|
+
return "rejected";
|
|
3994
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
3995
|
+
return "skipped";
|
|
3996
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
3997
|
+
return "failed";
|
|
3998
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
3999
|
+
return "pending";
|
|
4000
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
4001
|
+
return "completed";
|
|
4002
|
+
return null;
|
|
4003
|
+
}
|
|
4004
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
4005
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
4006
|
+
}
|
|
4007
|
+
function greptileRequestTimeoutMs(env) {
|
|
4008
|
+
const fallback = 30000;
|
|
4009
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
4010
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
4011
|
+
}
|
|
4012
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
4013
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4014
|
+
return null;
|
|
4015
|
+
const record = entry;
|
|
4016
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
4017
|
+
if (!id)
|
|
4018
|
+
return null;
|
|
4019
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
4020
|
+
return {
|
|
4021
|
+
id,
|
|
4022
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
4023
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
4024
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
4025
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
4026
|
+
};
|
|
4027
|
+
}
|
|
4028
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
4029
|
+
const seen = new Set;
|
|
4030
|
+
const unique2 = [];
|
|
4031
|
+
for (const review of reviews) {
|
|
4032
|
+
if (seen.has(review.id))
|
|
4033
|
+
continue;
|
|
4034
|
+
seen.add(review.id);
|
|
4035
|
+
unique2.push(review);
|
|
4036
|
+
}
|
|
4037
|
+
return unique2;
|
|
4038
|
+
}
|
|
4039
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
4040
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
4041
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
4042
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
4043
|
+
const latest = sorted.slice(0, 1);
|
|
4044
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
4045
|
+
}
|
|
4046
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
4047
|
+
const selected = details ?? review;
|
|
4048
|
+
return {
|
|
4049
|
+
id: selected.id || review.id,
|
|
4050
|
+
body: selected.body ?? review.body ?? null,
|
|
4051
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
4052
|
+
status: selected.status ?? review.status ?? null
|
|
4053
|
+
};
|
|
4054
|
+
}
|
|
4055
|
+
async function callGreptileMcpToolForGate(input) {
|
|
4056
|
+
const controller = new AbortController;
|
|
4057
|
+
const timeoutId = setTimeout(() => {
|
|
4058
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
4059
|
+
}, input.timeoutMs);
|
|
4060
|
+
let response;
|
|
4061
|
+
try {
|
|
4062
|
+
response = await input.fetchFn(input.apiBase, {
|
|
4063
|
+
method: "POST",
|
|
4064
|
+
headers: {
|
|
4065
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
4066
|
+
"Content-Type": "application/json"
|
|
4067
|
+
},
|
|
4068
|
+
body: JSON.stringify({
|
|
4069
|
+
jsonrpc: "2.0",
|
|
4070
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
4071
|
+
method: "tools/call",
|
|
4072
|
+
params: { name: input.name, arguments: input.args }
|
|
4073
|
+
}),
|
|
4074
|
+
signal: controller.signal
|
|
4075
|
+
});
|
|
4076
|
+
} catch (error) {
|
|
4077
|
+
if (controller.signal.aborted) {
|
|
4078
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
4079
|
+
}
|
|
4080
|
+
throw error;
|
|
4081
|
+
} finally {
|
|
4082
|
+
clearTimeout(timeoutId);
|
|
4083
|
+
}
|
|
4084
|
+
const raw = await response.text();
|
|
4085
|
+
if (!response.ok) {
|
|
4086
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
4087
|
+
}
|
|
4088
|
+
let envelope;
|
|
4089
|
+
try {
|
|
4090
|
+
envelope = JSON.parse(raw);
|
|
4091
|
+
} catch {
|
|
4092
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
4093
|
+
}
|
|
4094
|
+
if (envelope.error?.message) {
|
|
4095
|
+
throw new Error(envelope.error.message);
|
|
4096
|
+
}
|
|
4097
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
4098
|
+
`).trim();
|
|
4099
|
+
if (!text) {
|
|
4100
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
4101
|
+
}
|
|
4102
|
+
return text;
|
|
4103
|
+
}
|
|
4104
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
4105
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
4106
|
+
try {
|
|
4107
|
+
return JSON.parse(text);
|
|
4108
|
+
} catch {
|
|
4109
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
4113
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
4114
|
+
return { signals: [], errors: [] };
|
|
4115
|
+
}
|
|
4116
|
+
const env = input.options?.env ?? process.env;
|
|
4117
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
4118
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
4119
|
+
if (!apiKey) {
|
|
4120
|
+
return { signals: [], errors: [] };
|
|
4121
|
+
}
|
|
4122
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
4123
|
+
if (typeof fetchFn !== "function") {
|
|
4124
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
4125
|
+
}
|
|
4126
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
4127
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
4128
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
4129
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
4130
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
4131
|
+
try {
|
|
4132
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
4133
|
+
apiBase,
|
|
4134
|
+
apiKey,
|
|
4135
|
+
name: "list_code_reviews",
|
|
4136
|
+
args: {
|
|
4137
|
+
name: repository,
|
|
4138
|
+
remote,
|
|
4139
|
+
defaultBranch,
|
|
4140
|
+
prNumber: input.prNumber,
|
|
4141
|
+
limit: 20
|
|
4142
|
+
},
|
|
4143
|
+
timeoutMs,
|
|
4144
|
+
fetchFn
|
|
4145
|
+
});
|
|
4146
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
4147
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
4148
|
+
const signals = [];
|
|
4149
|
+
for (const review of selectedReviews) {
|
|
4150
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
4151
|
+
apiBase,
|
|
4152
|
+
apiKey,
|
|
4153
|
+
name: "get_code_review",
|
|
4154
|
+
args: { codeReviewId: review.id },
|
|
4155
|
+
timeoutMs,
|
|
4156
|
+
fetchFn
|
|
4157
|
+
});
|
|
4158
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
4159
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
4160
|
+
}
|
|
4161
|
+
return { signals, errors: [] };
|
|
4162
|
+
} catch (error) {
|
|
4163
|
+
return {
|
|
4164
|
+
signals: [],
|
|
4165
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
4166
|
+
};
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
function firstString(record, keys) {
|
|
4170
|
+
for (const key of keys) {
|
|
4171
|
+
const value = record[key];
|
|
4172
|
+
if (typeof value === "string")
|
|
4173
|
+
return value;
|
|
4174
|
+
}
|
|
4175
|
+
return "";
|
|
4176
|
+
}
|
|
4177
|
+
function arrayField(record, key) {
|
|
4178
|
+
const value = record[key];
|
|
4179
|
+
return Array.isArray(value) ? value : [];
|
|
4180
|
+
}
|
|
4181
|
+
async function runJsonArray(command, args, cwd) {
|
|
4182
|
+
const result = await command(args, { cwd });
|
|
4183
|
+
const label = `gh ${args.join(" ")}`;
|
|
4184
|
+
if (result.exitCode !== 0) {
|
|
4185
|
+
return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
4186
|
+
}
|
|
4187
|
+
const parsed = parseJsonArray(result.stdout);
|
|
4188
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
4189
|
+
}
|
|
4190
|
+
async function runJsonObject(command, args, cwd) {
|
|
4191
|
+
const result = await command(args, { cwd });
|
|
4192
|
+
const label = `gh ${args.join(" ")}`;
|
|
4193
|
+
if (result.exitCode !== 0) {
|
|
4194
|
+
return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
4195
|
+
}
|
|
4196
|
+
const parsed = parseJsonObject(result.stdout);
|
|
4197
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
4198
|
+
}
|
|
4199
|
+
function normalizeStatusCheck(entry) {
|
|
4200
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4201
|
+
return null;
|
|
4202
|
+
const record = entry;
|
|
4203
|
+
const name = firstString(record, ["name", "context"]);
|
|
4204
|
+
if (!name.trim())
|
|
4205
|
+
return null;
|
|
4206
|
+
const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
|
|
4207
|
+
const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
|
|
4208
|
+
return {
|
|
4209
|
+
__typename: typeof record.__typename === "string" ? record.__typename : null,
|
|
4210
|
+
name,
|
|
4211
|
+
context: typeof record.context === "string" ? record.context : null,
|
|
4212
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
4213
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
4214
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
4215
|
+
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,
|
|
4216
|
+
link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
|
|
4217
|
+
headSha: typeof record.headSha === "string" ? record.headSha : null,
|
|
4218
|
+
head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
|
|
4219
|
+
output: output ? {
|
|
4220
|
+
title: typeof output.title === "string" ? output.title : null,
|
|
4221
|
+
summary: typeof output.summary === "string" ? output.summary : null,
|
|
4222
|
+
text: typeof output.text === "string" ? output.text : null
|
|
4223
|
+
} : null,
|
|
4224
|
+
app: app ? {
|
|
4225
|
+
slug: typeof app.slug === "string" ? app.slug : null,
|
|
4226
|
+
name: typeof app.name === "string" ? app.name : null,
|
|
4227
|
+
owner: app.owner && typeof app.owner === "object" ? app.owner : null
|
|
4228
|
+
} : null
|
|
4229
|
+
};
|
|
4230
|
+
}
|
|
4231
|
+
function normalizeReview(entry) {
|
|
4232
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4233
|
+
return null;
|
|
4234
|
+
const record = entry;
|
|
4235
|
+
return {
|
|
4236
|
+
id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
|
|
4237
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
4238
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
4239
|
+
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,
|
|
4240
|
+
html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
|
|
4241
|
+
author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
4244
|
+
function normalizeReviewComment(entry) {
|
|
4245
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4246
|
+
return null;
|
|
4247
|
+
const record = entry;
|
|
4248
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
4249
|
+
const path = typeof record.path === "string" ? record.path : null;
|
|
4250
|
+
if (!body && !path)
|
|
4251
|
+
return null;
|
|
4252
|
+
return {
|
|
4253
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
4254
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
4255
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
4256
|
+
body,
|
|
4257
|
+
path,
|
|
4258
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
4259
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
4260
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
|
|
4261
|
+
original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
|
|
4262
|
+
};
|
|
4263
|
+
}
|
|
4264
|
+
function normalizeIssueComment(entry) {
|
|
4265
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4266
|
+
return null;
|
|
4267
|
+
const record = entry;
|
|
4268
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
4269
|
+
if (!body)
|
|
4270
|
+
return null;
|
|
4271
|
+
return {
|
|
4272
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
4273
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
4274
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
4275
|
+
body,
|
|
4276
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
4277
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
4278
|
+
created_at: typeof record.created_at === "string" ? record.created_at : null
|
|
4279
|
+
};
|
|
4280
|
+
}
|
|
4281
|
+
function normalizeReviewThread(entry) {
|
|
4282
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4283
|
+
return null;
|
|
4284
|
+
const record = entry;
|
|
4285
|
+
return {
|
|
4286
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
4287
|
+
isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
|
|
4288
|
+
isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
|
|
4289
|
+
comments: record.comments && typeof record.comments === "object" ? record.comments : null
|
|
4290
|
+
};
|
|
4291
|
+
}
|
|
4292
|
+
function relevantIssueComment(comment) {
|
|
4293
|
+
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
4294
|
+
const body = comment.body ?? "";
|
|
4295
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
4296
|
+
}
|
|
4297
|
+
function latestThreadComment(thread) {
|
|
4298
|
+
const nodes = thread.comments?.nodes ?? [];
|
|
4299
|
+
return nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
4300
|
+
}
|
|
4301
|
+
function unresolvedThreadSummaries(threads) {
|
|
4302
|
+
return threads.flatMap((thread) => {
|
|
4303
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4304
|
+
return [];
|
|
4305
|
+
const latest = latestThreadComment(thread);
|
|
4306
|
+
if (!latest)
|
|
4307
|
+
return ["Unresolved review thread"];
|
|
4308
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4309
|
+
return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4310
|
+
});
|
|
4311
|
+
}
|
|
4312
|
+
function collectBodies(evidence) {
|
|
4313
|
+
return [
|
|
4314
|
+
evidence.title ?? "",
|
|
4315
|
+
evidence.body,
|
|
4316
|
+
...evidence.reviews.map((review) => review.body ?? ""),
|
|
4317
|
+
...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
|
|
4318
|
+
...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
|
|
4319
|
+
...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
|
|
4320
|
+
...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
|
|
4321
|
+
].filter((body) => body.trim().length > 0);
|
|
4322
|
+
}
|
|
4323
|
+
function bodyExcerpt(body) {
|
|
4324
|
+
const text = stripHtml(body).replace(/\s+/g, " ").trim();
|
|
4325
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
4326
|
+
}
|
|
4327
|
+
function makeGreptileSignal(input) {
|
|
4328
|
+
const scores = parseGreptileScores(input.body);
|
|
4329
|
+
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
4330
|
+
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
4331
|
+
const verdict = input.verdict ?? null;
|
|
4332
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
4333
|
+
const explicitApproval = input.explicitApproval ?? false;
|
|
4334
|
+
return {
|
|
4335
|
+
source: input.source,
|
|
4336
|
+
trusted: input.trusted,
|
|
4337
|
+
authorLogin: input.authorLogin ?? null,
|
|
4338
|
+
reviewedSha,
|
|
4339
|
+
current,
|
|
4340
|
+
stale: current === false,
|
|
4341
|
+
score: scores[0] ?? null,
|
|
4342
|
+
scores,
|
|
4343
|
+
explicitApproval,
|
|
4344
|
+
verdict,
|
|
4345
|
+
blocker,
|
|
4346
|
+
actionable: input.actionable ?? blocker,
|
|
4347
|
+
bodyExcerpt: bodyExcerpt(input.body),
|
|
4348
|
+
body: input.body,
|
|
4349
|
+
allScores: scores
|
|
4350
|
+
};
|
|
4351
|
+
}
|
|
4352
|
+
function reviewAuthorLogin(review) {
|
|
4353
|
+
return review.author?.login ?? null;
|
|
4354
|
+
}
|
|
4355
|
+
function commentAuthorLogin(comment) {
|
|
4356
|
+
return comment.user?.login ?? comment.author?.login ?? null;
|
|
4357
|
+
}
|
|
4358
|
+
function collectGreptileSignals(evidence) {
|
|
4359
|
+
const signals = [];
|
|
4360
|
+
const greptileBodyReviewedSha = extractGreptileBodyReviewedSha(evidence.body);
|
|
4361
|
+
const trustedGreptileBody = Boolean(greptileBodyReviewedSha && isGreptileGithubLogin(evidence.bodyEditorLogin) && isoAtOrAfter(evidence.bodyLastEditedAt, evidence.headCommittedDate));
|
|
4362
|
+
const contextSources = [
|
|
4363
|
+
{ source: "pr-title", body: evidence.title ?? "" },
|
|
4364
|
+
{
|
|
4365
|
+
source: "pr-body",
|
|
4366
|
+
body: evidence.body,
|
|
4367
|
+
trusted: trustedGreptileBody,
|
|
4368
|
+
authorLogin: trustedGreptileBody ? evidence.bodyEditorLogin ?? null : null,
|
|
4369
|
+
reviewedSha: greptileBodyReviewedSha,
|
|
4370
|
+
verdict: trustedGreptileBody ? "completed" : null
|
|
4371
|
+
}
|
|
4372
|
+
];
|
|
4373
|
+
for (const context of contextSources) {
|
|
4374
|
+
if (!context.body.trim())
|
|
4375
|
+
continue;
|
|
4376
|
+
const contextBlocker = containsBlockerText(context.body);
|
|
4377
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
4378
|
+
continue;
|
|
4379
|
+
signals.push(makeGreptileSignal({
|
|
4380
|
+
source: context.source,
|
|
4381
|
+
body: context.body,
|
|
4382
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4383
|
+
trusted: context.trusted === true,
|
|
4384
|
+
authorLogin: context.authorLogin,
|
|
4385
|
+
reviewedSha: context.reviewedSha,
|
|
4386
|
+
verdict: context.verdict,
|
|
4387
|
+
blocker: contextBlocker,
|
|
4388
|
+
actionable: contextBlocker
|
|
4389
|
+
}));
|
|
4390
|
+
}
|
|
4391
|
+
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
4392
|
+
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
4393
|
+
|
|
4394
|
+
`) || "Status: UNKNOWN";
|
|
4395
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
4396
|
+
signals.push(makeGreptileSignal({
|
|
4397
|
+
source: "api",
|
|
4398
|
+
body,
|
|
4399
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4400
|
+
trusted: true,
|
|
4401
|
+
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
4402
|
+
explicitApproval: verdict === "approved",
|
|
4403
|
+
verdict
|
|
4404
|
+
}));
|
|
4405
|
+
}
|
|
4406
|
+
for (const review of evidence.reviews) {
|
|
4407
|
+
const login = reviewAuthorLogin(review);
|
|
4408
|
+
if (!isGreptileGithubLogin(login))
|
|
4409
|
+
continue;
|
|
4410
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4411
|
+
const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
|
|
4412
|
+
|
|
4413
|
+
`);
|
|
4414
|
+
if (!body.trim())
|
|
4415
|
+
continue;
|
|
4416
|
+
const dismissed = state === "DISMISSED";
|
|
4417
|
+
signals.push(makeGreptileSignal({
|
|
4418
|
+
source: "github-review",
|
|
4419
|
+
body,
|
|
4420
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4421
|
+
trusted: !dismissed,
|
|
4422
|
+
authorLogin: login,
|
|
4423
|
+
reviewedSha: review.commit_id ?? null,
|
|
4424
|
+
explicitApproval: undefined,
|
|
4425
|
+
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
4426
|
+
}));
|
|
4427
|
+
}
|
|
4428
|
+
for (const comment of evidence.relevantIssueComments) {
|
|
4429
|
+
const login = commentAuthorLogin(comment);
|
|
4430
|
+
const body = comment.body ?? "";
|
|
4431
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4432
|
+
continue;
|
|
4433
|
+
signals.push(makeGreptileSignal({
|
|
4434
|
+
source: "issue-comment",
|
|
4435
|
+
body,
|
|
4436
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4437
|
+
trusted: true,
|
|
4438
|
+
authorLogin: login
|
|
4439
|
+
}));
|
|
4440
|
+
}
|
|
4441
|
+
for (const thread of evidence.reviewThreads) {
|
|
4442
|
+
if (thread.isOutdated === true || thread.isResolved === true)
|
|
4443
|
+
continue;
|
|
4444
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
4445
|
+
const login = comment.author?.login ?? null;
|
|
4446
|
+
const body = comment.body ?? "";
|
|
4447
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4448
|
+
continue;
|
|
4449
|
+
signals.push(makeGreptileSignal({
|
|
4450
|
+
source: "review-thread",
|
|
4451
|
+
body,
|
|
4452
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4453
|
+
trusted: true,
|
|
4454
|
+
authorLogin: login
|
|
4455
|
+
}));
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
for (const check of evidence.checks) {
|
|
4459
|
+
if (!isGreptileLabel(checkName(check)))
|
|
4460
|
+
continue;
|
|
4461
|
+
const reviewedSha = check.headSha ?? check.head_sha ?? null;
|
|
4462
|
+
const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
|
|
4463
|
+
const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
|
|
4464
|
+
|
|
4465
|
+
`);
|
|
4466
|
+
signals.push(makeGreptileSignal({
|
|
4467
|
+
source: "github-check",
|
|
4468
|
+
body,
|
|
4469
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4470
|
+
trusted: false,
|
|
4471
|
+
reviewedSha,
|
|
4472
|
+
explicitApproval: false,
|
|
4473
|
+
blocker: isFailingCheck(check),
|
|
4474
|
+
actionable: isFailingCheck(check)
|
|
4475
|
+
}));
|
|
4476
|
+
}
|
|
4477
|
+
return signals;
|
|
4478
|
+
}
|
|
4479
|
+
function unresolvedGreptileThreadSummaries(threads) {
|
|
4480
|
+
return threads.flatMap((thread) => {
|
|
4481
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4482
|
+
return [];
|
|
4483
|
+
const comments = thread.comments?.nodes ?? [];
|
|
4484
|
+
if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
|
|
4485
|
+
return [];
|
|
4486
|
+
const latest = latestThreadComment(thread);
|
|
4487
|
+
if (!latest)
|
|
4488
|
+
return ["Unresolved Greptile review thread"];
|
|
4489
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4490
|
+
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4493
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
4494
|
+
return [];
|
|
4495
|
+
}
|
|
4496
|
+
function issueLevelBlockerSummaries(comments) {
|
|
4497
|
+
return comments.flatMap((comment) => {
|
|
4498
|
+
const body = comment.body?.trim() ?? "";
|
|
4499
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4500
|
+
return [];
|
|
4501
|
+
const login = commentAuthorLogin(comment) ?? "unknown";
|
|
4502
|
+
const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
|
|
4503
|
+
return [`${author}: ${body}`];
|
|
4504
|
+
});
|
|
4505
|
+
}
|
|
4506
|
+
function reviewBodyBlockerSummaries(reviews) {
|
|
4507
|
+
return reviews.flatMap((review) => {
|
|
4508
|
+
const login = reviewAuthorLogin(review) ?? "unknown";
|
|
4509
|
+
if (isGreptileGithubLogin(login))
|
|
4510
|
+
return [];
|
|
4511
|
+
const body = review.body?.trim() ?? "";
|
|
4512
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4513
|
+
return [];
|
|
4514
|
+
const state = review.state ? ` (${review.state})` : "";
|
|
4515
|
+
return [`PR review summary by ${login}${state}: ${body}`];
|
|
4516
|
+
});
|
|
4517
|
+
}
|
|
4518
|
+
function signalLabel(signal) {
|
|
4519
|
+
const source = signal.source.replace(/-/g, " ");
|
|
4520
|
+
const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
|
|
4521
|
+
const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
|
|
4522
|
+
return `${source}${author}${sha}`;
|
|
4523
|
+
}
|
|
4524
|
+
function deriveGreptileEvidence(input) {
|
|
4525
|
+
const rawBodies = collectBodies(input);
|
|
4526
|
+
const signals = collectGreptileSignals(input);
|
|
4527
|
+
const trustedSignals = signals.filter((signal) => signal.trusted);
|
|
4528
|
+
const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4529
|
+
const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4530
|
+
const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
|
|
4531
|
+
const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
|
|
4532
|
+
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4533
|
+
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4534
|
+
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4535
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
4536
|
+
const signalCanApproveByScore = (signal) => {
|
|
4537
|
+
if (signal.source === "api")
|
|
4538
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
4539
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
4540
|
+
};
|
|
4541
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4542
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
4543
|
+
const approvedByScore = !!approvingScoreEntry;
|
|
4544
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
4545
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
4546
|
+
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4547
|
+
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4548
|
+
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4549
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4550
|
+
const staleBlockingSignals = [];
|
|
4551
|
+
const blockers = [
|
|
4552
|
+
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
4553
|
+
...reviewBodyBlockerSummaries(input.reviews),
|
|
4554
|
+
...issueLevelBlockerSummaries(input.relevantIssueComments),
|
|
4555
|
+
...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
|
|
4556
|
+
...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
|
|
4557
|
+
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4558
|
+
];
|
|
4559
|
+
const unresolvedComments = [
|
|
4560
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
4561
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
4562
|
+
];
|
|
4563
|
+
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4564
|
+
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
4565
|
+
const completedGreptileCheck = greptileChecks.some((check) => {
|
|
4566
|
+
const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
|
|
4567
|
+
return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
|
|
4568
|
+
});
|
|
4569
|
+
const completedGreptileReview = greptileReviews.some((review) => {
|
|
4570
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4571
|
+
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4572
|
+
return completedState && review.commit_id === input.currentHeadSha;
|
|
4573
|
+
});
|
|
4574
|
+
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"));
|
|
4575
|
+
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4576
|
+
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4577
|
+
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4578
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
4579
|
+
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4580
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
4581
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
4582
|
+
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";
|
|
4583
|
+
return {
|
|
4584
|
+
source,
|
|
4585
|
+
currentHeadSha: input.currentHeadSha,
|
|
4586
|
+
reviewedSha,
|
|
4587
|
+
fresh,
|
|
4588
|
+
completed,
|
|
4589
|
+
approved,
|
|
4590
|
+
score,
|
|
4591
|
+
explicitApproval: approvedByExplicitMapping,
|
|
4592
|
+
blockers,
|
|
4593
|
+
unresolvedComments,
|
|
4594
|
+
rawBodies,
|
|
4595
|
+
signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
|
|
4596
|
+
mapping
|
|
4597
|
+
};
|
|
4598
|
+
}
|
|
4599
|
+
function isGreptileCheckDetail(check) {
|
|
4600
|
+
return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
|
|
4601
|
+
}
|
|
4602
|
+
async function collectGreptileCheckDetails(input) {
|
|
4603
|
+
const checkRunsRead = await runJsonObject(input.command, [
|
|
4604
|
+
"api",
|
|
4605
|
+
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
4606
|
+
"-F",
|
|
4607
|
+
"per_page=100"
|
|
4608
|
+
], input.projectRoot);
|
|
4609
|
+
const checkRuns = arrayField(checkRunsRead.value, "check_runs").map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
|
|
4610
|
+
return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
|
|
4611
|
+
}
|
|
4612
|
+
async function collectPullRequestProvenance(input) {
|
|
4613
|
+
const response = await runJsonObject(input.command, [
|
|
4614
|
+
"api",
|
|
4615
|
+
"graphql",
|
|
4616
|
+
"-F",
|
|
4617
|
+
`owner=${input.owner}`,
|
|
4618
|
+
"-F",
|
|
4619
|
+
`name=${input.name}`,
|
|
4620
|
+
"-F",
|
|
4621
|
+
`prNumber=${input.prNumber}`,
|
|
4622
|
+
"-f",
|
|
4623
|
+
"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 } } } } } }"
|
|
4624
|
+
], input.projectRoot);
|
|
4625
|
+
if (response.error)
|
|
4626
|
+
return { value: {}, error: response.error };
|
|
4627
|
+
const data = response.value.data;
|
|
4628
|
+
const repository = data?.repository;
|
|
4629
|
+
const pullRequest = repository?.pullRequest;
|
|
4630
|
+
if (!pullRequest)
|
|
4631
|
+
return { value: {}, error: "GitHub pullRequest provenance response did not include a pullRequest object" };
|
|
4632
|
+
const editor = pullRequest.editor;
|
|
4633
|
+
const commits = pullRequest.commits;
|
|
4634
|
+
const nodes = Array.isArray(commits?.nodes) ? commits.nodes : [];
|
|
4635
|
+
const latestCommitNode = nodes[nodes.length - 1];
|
|
4636
|
+
const latestCommit = latestCommitNode?.commit;
|
|
4637
|
+
return {
|
|
4638
|
+
value: {
|
|
4639
|
+
bodyEditorLogin: typeof editor?.login === "string" ? editor.login : null,
|
|
4640
|
+
bodyLastEditedAt: typeof pullRequest.lastEditedAt === "string" ? pullRequest.lastEditedAt : null,
|
|
4641
|
+
headCommittedDate: typeof latestCommit?.committedDate === "string" ? latestCommit.committedDate : null
|
|
4642
|
+
}
|
|
4643
|
+
};
|
|
4644
|
+
}
|
|
4645
|
+
async function collectReviewThreads(input) {
|
|
4646
|
+
const reviewThreads = [];
|
|
4647
|
+
let afterCursor = null;
|
|
4648
|
+
for (let page = 0;page < 100; page += 1) {
|
|
4649
|
+
const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
|
|
4650
|
+
const threadsResponse = await runJsonObject(input.command, [
|
|
4651
|
+
"api",
|
|
4652
|
+
"graphql",
|
|
4653
|
+
"-F",
|
|
4654
|
+
`owner=${input.owner}`,
|
|
4655
|
+
"-F",
|
|
4656
|
+
`name=${input.name}`,
|
|
4657
|
+
"-F",
|
|
4658
|
+
`prNumber=${input.prNumber}`,
|
|
4659
|
+
"-f",
|
|
4660
|
+
`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 } } } } }`
|
|
4661
|
+
], input.projectRoot);
|
|
4662
|
+
if (threadsResponse.error) {
|
|
4663
|
+
return { value: reviewThreads, error: threadsResponse.error };
|
|
4664
|
+
}
|
|
4665
|
+
const data = threadsResponse.value.data;
|
|
4666
|
+
const repository = data?.repository;
|
|
4667
|
+
const pullRequest = repository?.pullRequest;
|
|
4668
|
+
const threads = pullRequest?.reviewThreads;
|
|
4669
|
+
const nodes = threads?.nodes;
|
|
4670
|
+
if (!Array.isArray(nodes)) {
|
|
4671
|
+
return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
|
|
4672
|
+
}
|
|
4673
|
+
const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
|
|
4674
|
+
reviewThreads.push(...normalized);
|
|
4675
|
+
const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
|
|
4676
|
+
if (truncatedCommentThread) {
|
|
4677
|
+
return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
|
|
4678
|
+
}
|
|
4679
|
+
const pageInfo = threads?.pageInfo;
|
|
4680
|
+
if (!pageInfo) {
|
|
4681
|
+
if (nodes.length >= 100) {
|
|
4682
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
|
|
4683
|
+
}
|
|
4684
|
+
return { value: reviewThreads };
|
|
4685
|
+
}
|
|
4686
|
+
if (pageInfo.hasNextPage !== true) {
|
|
4687
|
+
return { value: reviewThreads };
|
|
4688
|
+
}
|
|
4689
|
+
if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
|
|
4690
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
|
|
4691
|
+
}
|
|
4692
|
+
afterCursor = pageInfo.endCursor;
|
|
4693
|
+
}
|
|
4694
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
|
|
4695
|
+
}
|
|
4696
|
+
async function collectPrReviewEvidence(input) {
|
|
4697
|
+
const parsed = parseGithubPrUrl(input.prUrl);
|
|
4698
|
+
if (!parsed) {
|
|
4699
|
+
throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
|
|
4700
|
+
}
|
|
4701
|
+
const readErrors = [];
|
|
4702
|
+
const viewRead = await runJsonObject(input.command, [
|
|
4703
|
+
"pr",
|
|
4704
|
+
"view",
|
|
4705
|
+
input.prUrl,
|
|
4706
|
+
"--json",
|
|
4707
|
+
"title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
|
|
4708
|
+
], input.projectRoot);
|
|
4709
|
+
if (viewRead.error)
|
|
4710
|
+
readErrors.push(viewRead.error);
|
|
4711
|
+
const view = viewRead.value;
|
|
4712
|
+
if (!Array.isArray(view.statusCheckRollup)) {
|
|
4713
|
+
readErrors.push("gh pr view did not return required statusCheckRollup array");
|
|
4714
|
+
}
|
|
4715
|
+
if (!Array.isArray(view.reviews)) {
|
|
4716
|
+
readErrors.push("gh pr view did not return required reviews array");
|
|
4717
|
+
}
|
|
4718
|
+
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4719
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
4720
|
+
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4721
|
+
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4722
|
+
const provenanceRead = await collectPullRequestProvenance({
|
|
4723
|
+
command: input.command,
|
|
4724
|
+
projectRoot: input.projectRoot,
|
|
4725
|
+
owner: parsed.owner,
|
|
4726
|
+
name: parsed.repo,
|
|
4727
|
+
prNumber: parsed.prNumber
|
|
4728
|
+
});
|
|
4729
|
+
const provenance = provenanceRead.value;
|
|
4730
|
+
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
|
|
4731
|
+
if (reviewCommentsRead.error)
|
|
4732
|
+
readErrors.push(reviewCommentsRead.error);
|
|
4733
|
+
const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
|
|
4734
|
+
const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
|
|
4735
|
+
if (issueCommentsRead.error)
|
|
4736
|
+
readErrors.push(issueCommentsRead.error);
|
|
4737
|
+
const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
|
|
4738
|
+
const reviewThreadsRead = await collectReviewThreads({
|
|
4739
|
+
command: input.command,
|
|
4740
|
+
projectRoot: input.projectRoot,
|
|
4741
|
+
owner: parsed.owner,
|
|
4742
|
+
name: parsed.repo,
|
|
4743
|
+
prNumber: parsed.prNumber
|
|
4744
|
+
});
|
|
4745
|
+
if (reviewThreadsRead.error)
|
|
4746
|
+
readErrors.push(reviewThreadsRead.error);
|
|
4747
|
+
const reviewThreads = reviewThreadsRead.value;
|
|
4748
|
+
const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
|
|
4749
|
+
let greptileCheckDetails = [];
|
|
4750
|
+
if (headSha && greptileRollupChecks.length > 0) {
|
|
4751
|
+
const checkDetailsRead = await collectGreptileCheckDetails({
|
|
4752
|
+
command: input.command,
|
|
4753
|
+
projectRoot: input.projectRoot,
|
|
4754
|
+
repoName: parsed.repoName,
|
|
4755
|
+
headSha
|
|
4756
|
+
});
|
|
4757
|
+
greptileCheckDetails = checkDetailsRead.value;
|
|
4758
|
+
}
|
|
4759
|
+
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
4760
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
4761
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
4762
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
4763
|
+
options: input.greptileApi,
|
|
4764
|
+
repoName: parsed.repoName,
|
|
4765
|
+
prNumber: parsed.prNumber,
|
|
4766
|
+
headSha,
|
|
4767
|
+
baseRefName
|
|
4768
|
+
});
|
|
4769
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
4770
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
4771
|
+
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})` : ""}`);
|
|
4772
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4773
|
+
const evidenceBase = {
|
|
4774
|
+
title: firstString(view, ["title"]),
|
|
4775
|
+
body: firstString(view, ["body"]),
|
|
4776
|
+
bodyEditorLogin: provenance.bodyEditorLogin ?? null,
|
|
4777
|
+
bodyLastEditedAt: provenance.bodyLastEditedAt ?? null,
|
|
4778
|
+
headCommittedDate: provenance.headCommittedDate ?? null,
|
|
4779
|
+
reviews,
|
|
4780
|
+
changedFileReviewComments: reviewComments,
|
|
4781
|
+
relevantIssueComments: issueComments,
|
|
4782
|
+
reviewThreads,
|
|
4783
|
+
checks: checksWithGreptileDetails,
|
|
4784
|
+
currentHeadSha: headSha,
|
|
4785
|
+
apiSignals
|
|
4786
|
+
};
|
|
4787
|
+
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4788
|
+
return {
|
|
4789
|
+
prUrl: input.prUrl,
|
|
4790
|
+
prNumber: parsed.prNumber,
|
|
4791
|
+
repoName: parsed.repoName,
|
|
4792
|
+
title: evidenceBase.title,
|
|
4793
|
+
body: evidenceBase.body,
|
|
4794
|
+
bodyEditorLogin: evidenceBase.bodyEditorLogin,
|
|
4795
|
+
bodyLastEditedAt: evidenceBase.bodyLastEditedAt,
|
|
4796
|
+
headCommittedDate: evidenceBase.headCommittedDate,
|
|
4797
|
+
headSha,
|
|
4798
|
+
headRefName: firstString(view, ["headRefName"]),
|
|
4799
|
+
baseRefName,
|
|
4800
|
+
state: firstString(view, ["state"]),
|
|
4801
|
+
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4802
|
+
mergeable: firstString(view, ["mergeable"]),
|
|
4803
|
+
mergeStateStatus: firstString(view, ["mergeStateStatus"]),
|
|
4804
|
+
reviewDecision: firstString(view, ["reviewDecision"]),
|
|
4805
|
+
reviews,
|
|
4806
|
+
reviewThreads,
|
|
4807
|
+
changedFileReviewComments: reviewComments,
|
|
4808
|
+
relevantIssueComments: issueComments,
|
|
4809
|
+
statusCheckRollup: checksWithGreptileDetails,
|
|
4810
|
+
checkFailures,
|
|
4811
|
+
pendingChecks,
|
|
4812
|
+
readErrors,
|
|
4813
|
+
greptile
|
|
4814
|
+
};
|
|
4815
|
+
}
|
|
4816
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
4817
|
+
const normalized = value.trim();
|
|
4818
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
4819
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
4820
|
+
}
|
|
4821
|
+
function evaluateEvidence(evidence) {
|
|
4822
|
+
const reasonDetails = [];
|
|
4823
|
+
const warnings = [];
|
|
4824
|
+
const seen = new Set;
|
|
4825
|
+
const addReason = (reason) => {
|
|
4826
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
4827
|
+
const key = `${capped.code}:${capped.message}`;
|
|
4828
|
+
if (seen.has(key))
|
|
4829
|
+
return;
|
|
4830
|
+
seen.add(key);
|
|
4831
|
+
reasonDetails.push(capped);
|
|
4832
|
+
};
|
|
4833
|
+
const greptile = evidence.greptile;
|
|
4834
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4835
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
4836
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4837
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4838
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
4839
|
+
for (const error of evidence.readErrors) {
|
|
4840
|
+
addReason({
|
|
4841
|
+
code: "read_error",
|
|
4842
|
+
reasonClass: "reject",
|
|
4843
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
4844
|
+
suggestedAction: "needs_attention",
|
|
4845
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
4846
|
+
headSha: evidence.headSha || null
|
|
4847
|
+
});
|
|
4848
|
+
}
|
|
4849
|
+
if (!evidence.headSha) {
|
|
4850
|
+
addReason({
|
|
4851
|
+
code: "missing_head_sha",
|
|
4852
|
+
reasonClass: "reject",
|
|
4853
|
+
surface: "github",
|
|
4854
|
+
suggestedAction: "needs_attention",
|
|
4855
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
4856
|
+
headSha: null
|
|
4857
|
+
});
|
|
4858
|
+
}
|
|
4859
|
+
for (const failure of evidence.checkFailures) {
|
|
4860
|
+
addReason({
|
|
4861
|
+
code: "ci_failed",
|
|
4862
|
+
reasonClass: "reject",
|
|
4863
|
+
surface: "ci",
|
|
4864
|
+
suggestedAction: "fix",
|
|
4865
|
+
message: failure,
|
|
4866
|
+
headSha: evidence.headSha || null
|
|
4867
|
+
});
|
|
4868
|
+
}
|
|
4869
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
4870
|
+
addReason({
|
|
4871
|
+
code: "check_pending",
|
|
4872
|
+
reasonClass: "pending",
|
|
4873
|
+
surface: "ci",
|
|
4874
|
+
suggestedAction: "wait",
|
|
4875
|
+
message: pendingCheck,
|
|
4876
|
+
headSha: evidence.headSha || null
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4880
|
+
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4881
|
+
addReason({
|
|
4882
|
+
code: "review_decision_blocking",
|
|
4883
|
+
reasonClass: "reject",
|
|
4884
|
+
surface: "review",
|
|
4885
|
+
suggestedAction: "fix",
|
|
4886
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
4887
|
+
headSha: evidence.headSha || null
|
|
4888
|
+
});
|
|
4889
|
+
}
|
|
4890
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
4891
|
+
addReason({
|
|
4892
|
+
code: "review_thread_unresolved",
|
|
4893
|
+
reasonClass: "reject",
|
|
4894
|
+
surface: "review",
|
|
4895
|
+
suggestedAction: "fix",
|
|
4896
|
+
message: thread,
|
|
4897
|
+
headSha: evidence.headSha || null
|
|
4898
|
+
});
|
|
4899
|
+
}
|
|
4900
|
+
if (greptile.mapping === "missing") {
|
|
4901
|
+
addReason({
|
|
4902
|
+
code: "greptile_missing",
|
|
4903
|
+
reasonClass: "pending",
|
|
4904
|
+
surface: "greptile",
|
|
4905
|
+
suggestedAction: "wait",
|
|
4906
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
4907
|
+
headSha: evidence.headSha || null
|
|
4908
|
+
});
|
|
4909
|
+
}
|
|
4910
|
+
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4911
|
+
addReason({
|
|
4912
|
+
code: "greptile_stale",
|
|
4913
|
+
reasonClass: "pending",
|
|
4914
|
+
surface: "greptile",
|
|
4915
|
+
suggestedAction: "wait",
|
|
4916
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
4917
|
+
headSha: evidence.headSha || null,
|
|
4918
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
4919
|
+
});
|
|
4920
|
+
}
|
|
4921
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
4922
|
+
addReason({
|
|
4923
|
+
code: "greptile_pending",
|
|
4924
|
+
reasonClass: "pending",
|
|
4925
|
+
surface: "greptile",
|
|
4926
|
+
suggestedAction: "wait",
|
|
4927
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
4928
|
+
headSha: evidence.headSha || null,
|
|
4929
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4930
|
+
});
|
|
4931
|
+
}
|
|
4932
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
4933
|
+
addReason({
|
|
4934
|
+
code: "greptile_api_status_unknown",
|
|
4935
|
+
reasonClass: "reject",
|
|
4936
|
+
surface: "greptile",
|
|
4937
|
+
suggestedAction: "needs_attention",
|
|
4938
|
+
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}` : "."}`,
|
|
4939
|
+
headSha: evidence.headSha || null,
|
|
4940
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4941
|
+
});
|
|
4942
|
+
}
|
|
4943
|
+
if (!greptile.completed) {
|
|
4944
|
+
addReason({
|
|
4945
|
+
code: "greptile_pending",
|
|
4946
|
+
reasonClass: "pending",
|
|
4947
|
+
surface: "greptile",
|
|
4948
|
+
suggestedAction: "wait",
|
|
4949
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
4950
|
+
headSha: evidence.headSha || null,
|
|
4951
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4952
|
+
});
|
|
4953
|
+
}
|
|
4954
|
+
if (!greptile.fresh) {
|
|
4955
|
+
addReason({
|
|
4956
|
+
code: "greptile_not_current_head",
|
|
4957
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4958
|
+
surface: "greptile",
|
|
4959
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4960
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
4961
|
+
headSha: evidence.headSha || null,
|
|
4962
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4963
|
+
});
|
|
4964
|
+
}
|
|
4965
|
+
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4966
|
+
addReason({
|
|
4967
|
+
code: "greptile_score_not_5",
|
|
4968
|
+
reasonClass: "reject",
|
|
4969
|
+
surface: "greptile",
|
|
4970
|
+
suggestedAction: "fix",
|
|
4971
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
4972
|
+
headSha: evidence.headSha || null,
|
|
4973
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4974
|
+
});
|
|
4975
|
+
}
|
|
4976
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
4977
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
4978
|
+
addReason({
|
|
4979
|
+
code: "greptile_score_missing",
|
|
4980
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4981
|
+
surface: "greptile",
|
|
4982
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4983
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
4984
|
+
headSha: evidence.headSha || null,
|
|
4985
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4986
|
+
});
|
|
4987
|
+
}
|
|
4988
|
+
if (greptile.mapping === "unproven") {
|
|
4989
|
+
addReason({
|
|
4990
|
+
code: "greptile_mapping_unproven",
|
|
4991
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4992
|
+
surface: "greptile",
|
|
4993
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4994
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
4995
|
+
headSha: evidence.headSha || null,
|
|
4996
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4997
|
+
});
|
|
4998
|
+
}
|
|
4999
|
+
for (const blocker of greptile.blockers) {
|
|
5000
|
+
addReason({
|
|
5001
|
+
code: "greptile_blocker_text",
|
|
5002
|
+
reasonClass: "reject",
|
|
5003
|
+
surface: "greptile",
|
|
5004
|
+
suggestedAction: "fix",
|
|
5005
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
5006
|
+
headSha: evidence.headSha || null,
|
|
5007
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5008
|
+
});
|
|
5009
|
+
}
|
|
5010
|
+
for (const comment of greptile.unresolvedComments) {
|
|
5011
|
+
addReason({
|
|
5012
|
+
code: "greptile_unresolved_comment",
|
|
5013
|
+
reasonClass: "reject",
|
|
5014
|
+
surface: "greptile",
|
|
5015
|
+
suggestedAction: "fix",
|
|
5016
|
+
message: comment,
|
|
5017
|
+
headSha: evidence.headSha || null,
|
|
5018
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5019
|
+
});
|
|
5020
|
+
}
|
|
5021
|
+
if (!greptile.approved)
|
|
5022
|
+
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
5023
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
5024
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
5025
|
+
}
|
|
5026
|
+
function evaluateStrictPrMergeGate(evidence) {
|
|
5027
|
+
const evaluated = evaluateEvidence(evidence);
|
|
5028
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
5029
|
+
return {
|
|
5030
|
+
approved,
|
|
5031
|
+
pending: evaluated.pending,
|
|
5032
|
+
reasons: evaluated.reasons,
|
|
5033
|
+
reasonDetails: evaluated.reasonDetails,
|
|
5034
|
+
warnings: evaluated.warnings,
|
|
5035
|
+
actionableFeedback: evaluated.reasons,
|
|
5036
|
+
evidence
|
|
5037
|
+
};
|
|
5038
|
+
}
|
|
5039
|
+
|
|
5040
|
+
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
3742
5041
|
var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
3743
5042
|
"changed-files.txt",
|
|
3744
5043
|
"contract-changes.md",
|
|
@@ -3750,12 +5049,12 @@ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
|
3750
5049
|
"validation-summary.json"
|
|
3751
5050
|
]);
|
|
3752
5051
|
function readPrMetadata(projectRoot, taskId) {
|
|
3753
|
-
const path =
|
|
3754
|
-
if (!
|
|
5052
|
+
const path = resolve22(artifactDirForId(projectRoot, taskId), "pr-state.json");
|
|
5053
|
+
if (!existsSync19(path)) {
|
|
3755
5054
|
return [];
|
|
3756
5055
|
}
|
|
3757
5056
|
try {
|
|
3758
|
-
const parsed = JSON.parse(
|
|
5057
|
+
const parsed = JSON.parse(readFileSync10(path, "utf-8"));
|
|
3759
5058
|
if (!parsed || typeof parsed !== "object") {
|
|
3760
5059
|
return [];
|
|
3761
5060
|
}
|
|
@@ -3781,11 +5080,11 @@ async function verifyTask(options) {
|
|
|
3781
5080
|
const taskId = options.taskId;
|
|
3782
5081
|
const normalizedTaskId = lookupTask(options.projectRoot, taskId);
|
|
3783
5082
|
const artifactDir = artifactDirForId(options.projectRoot, taskId);
|
|
3784
|
-
|
|
3785
|
-
const validationSummaryPath =
|
|
3786
|
-
const reviewFeedbackPath =
|
|
3787
|
-
const reviewStatePath =
|
|
3788
|
-
const greptileRawPath =
|
|
5083
|
+
mkdirSync10(artifactDir, { recursive: true });
|
|
5084
|
+
const validationSummaryPath = resolve23(artifactDir, "validation-summary.json");
|
|
5085
|
+
const reviewFeedbackPath = resolve23(artifactDir, "review-feedback.md");
|
|
5086
|
+
const reviewStatePath = resolve23(artifactDir, "review-state.json");
|
|
5087
|
+
const greptileRawPath = resolve23(artifactDir, "review-greptile-raw.json");
|
|
3789
5088
|
const prStates = readPrMetadata(options.projectRoot, taskId);
|
|
3790
5089
|
const prState = prStates[0] || null;
|
|
3791
5090
|
const localReasons = [];
|
|
@@ -3797,7 +5096,7 @@ async function verifyTask(options) {
|
|
|
3797
5096
|
if (!normalizedTaskId && !await hasConfiguredSourceTask(options.projectRoot, taskId)) {
|
|
3798
5097
|
localReasons.push(`[Task Config] Unknown task id '${taskId}' in task-config or configured task source.`);
|
|
3799
5098
|
}
|
|
3800
|
-
if (!
|
|
5099
|
+
if (!existsSync20(validationSummaryPath)) {
|
|
3801
5100
|
localReasons.push(`[Artifact Quality] validation-summary.json not found at ${validationSummaryPath}.`);
|
|
3802
5101
|
} else {
|
|
3803
5102
|
const summary = await parseValidationSummary(validationSummaryPath);
|
|
@@ -3806,13 +5105,13 @@ async function verifyTask(options) {
|
|
|
3806
5105
|
}
|
|
3807
5106
|
}
|
|
3808
5107
|
for (const file of ["task-result.json", "decision-log.md", "next-actions.md", "changed-files.txt"]) {
|
|
3809
|
-
const requiredPath =
|
|
3810
|
-
if (!
|
|
5108
|
+
const requiredPath = resolve23(artifactDir, file);
|
|
5109
|
+
if (!existsSync20(requiredPath)) {
|
|
3811
5110
|
localReasons.push(`[Artifact Quality] Missing required artifact file: ${requiredPath}`);
|
|
3812
5111
|
}
|
|
3813
5112
|
}
|
|
3814
|
-
const taskResultPath =
|
|
3815
|
-
if (
|
|
5113
|
+
const taskResultPath = resolve23(artifactDir, "task-result.json");
|
|
5114
|
+
if (existsSync20(taskResultPath)) {
|
|
3816
5115
|
const taskResult = await readJsonFile2(taskResultPath);
|
|
3817
5116
|
const artifactStatus = typeof taskResult?.status === "string" ? taskResult.status.trim().toLowerCase() : "";
|
|
3818
5117
|
if (artifactStatus === "partial") {
|
|
@@ -3825,8 +5124,8 @@ async function verifyTask(options) {
|
|
|
3825
5124
|
localReasons.push("[Artifact Quality] task-result.json next actions indicate remaining implementation scope.");
|
|
3826
5125
|
}
|
|
3827
5126
|
}
|
|
3828
|
-
const nextActionsPath =
|
|
3829
|
-
if (
|
|
5127
|
+
const nextActionsPath = resolve23(artifactDir, "next-actions.md");
|
|
5128
|
+
if (existsSync20(nextActionsPath)) {
|
|
3830
5129
|
const nextActionsContent = await Bun.file(nextActionsPath).text();
|
|
3831
5130
|
if (nextActionsContent.includes("TODO: Replace this scaffold") || nextActionsContent.includes("bd-<downstream-task-id>")) {
|
|
3832
5131
|
localReasons.push("[Artifact Quality] next-actions.md still contains scaffold placeholder text. Replace with real recommendations.");
|
|
@@ -3839,12 +5138,6 @@ async function verifyTask(options) {
|
|
|
3839
5138
|
if (sourceCloseoutIssueId) {
|
|
3840
5139
|
localReasons.push(...evaluateGithubSourceIssuePrCloseout(options.projectRoot, prStates, sourceCloseoutIssueId));
|
|
3841
5140
|
}
|
|
3842
|
-
const pluginResults = await options.plugins.runValidators(taskId);
|
|
3843
|
-
for (const result of pluginResults) {
|
|
3844
|
-
if (!result.passed) {
|
|
3845
|
-
localReasons.push(`[Plugin Validator] ${result.id}: ${result.summary}`);
|
|
3846
|
-
}
|
|
3847
|
-
}
|
|
3848
5141
|
const reviewMode = await loadReviewMode(paths.reviewProfilePath, process.env.AI_REVIEW_MODE || "advisory");
|
|
3849
5142
|
const reviewProvider = await loadReviewProvider(paths.reviewProfilePath, process.env.AI_REVIEW_PROVIDER || "greptile");
|
|
3850
5143
|
if (!options.skipAiReview && localReasons.length === 0 && reviewProvider === "greptile" && reviewMode !== "off") {
|
|
@@ -3863,7 +5156,7 @@ async function verifyTask(options) {
|
|
|
3863
5156
|
aiReasons.push(`[AI Review] Required mode needs a completed Greptile approval; current verdict is ${ai.verdict}.`);
|
|
3864
5157
|
}
|
|
3865
5158
|
if (persistArtifacts && ai.rawResponse) {
|
|
3866
|
-
|
|
5159
|
+
writeFileSync10(greptileRawPath, `${ai.rawResponse}
|
|
3867
5160
|
`, "utf-8");
|
|
3868
5161
|
}
|
|
3869
5162
|
} else if (!options.skipAiReview && reviewMode === "off") {
|
|
@@ -4202,7 +5495,7 @@ function isAcceptedValidationSummary(summary) {
|
|
|
4202
5495
|
return summary.status === "skipped" && summary.total === 0 && summary.failed === 0;
|
|
4203
5496
|
}
|
|
4204
5497
|
async function loadReviewMode(reviewProfilePath, fallback) {
|
|
4205
|
-
const parsed =
|
|
5498
|
+
const parsed = existsSync20(reviewProfilePath) ? await readJsonFile2(reviewProfilePath) : null;
|
|
4206
5499
|
const mode = parsed?.mode;
|
|
4207
5500
|
if (mode === "off" || mode === "advisory" || mode === "required") {
|
|
4208
5501
|
return mode;
|
|
@@ -4213,7 +5506,7 @@ async function loadReviewMode(reviewProfilePath, fallback) {
|
|
|
4213
5506
|
return "advisory";
|
|
4214
5507
|
}
|
|
4215
5508
|
async function loadReviewProvider(reviewProfilePath, fallback) {
|
|
4216
|
-
const parsed =
|
|
5509
|
+
const parsed = existsSync20(reviewProfilePath) ? await readJsonFile2(reviewProfilePath) : null;
|
|
4217
5510
|
const provider = parsed?.provider;
|
|
4218
5511
|
if (typeof provider === "string" && provider.trim().length > 0) {
|
|
4219
5512
|
return provider;
|
|
@@ -4372,7 +5665,7 @@ function writeFeedbackFile(options) {
|
|
|
4372
5665
|
if (options.aiRawFeedback) {
|
|
4373
5666
|
lines.push("## Raw Reviewer Feedback", "", "```text", options.aiRawFeedback, "```", "");
|
|
4374
5667
|
}
|
|
4375
|
-
|
|
5668
|
+
writeFileSync10(options.output, `${lines.join(`
|
|
4376
5669
|
`)}
|
|
4377
5670
|
`, "utf-8");
|
|
4378
5671
|
}
|
|
@@ -4389,7 +5682,7 @@ function writeReviewStateFile(options) {
|
|
|
4389
5682
|
ai_warnings: options.aiWarnings,
|
|
4390
5683
|
updated_at: nowIso()
|
|
4391
5684
|
};
|
|
4392
|
-
|
|
5685
|
+
writeFileSync10(options.output, `${JSON.stringify(payload, null, 2)}
|
|
4393
5686
|
`, "utf-8");
|
|
4394
5687
|
}
|
|
4395
5688
|
async function runGreptileReviewForPr(options) {
|
|
@@ -4571,7 +5864,8 @@ async function runGreptileReviewForPr(options) {
|
|
|
4571
5864
|
}
|
|
4572
5865
|
};
|
|
4573
5866
|
}
|
|
4574
|
-
|
|
5867
|
+
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
5868
|
+
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)) {
|
|
4575
5869
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
4576
5870
|
return {
|
|
4577
5871
|
verdict: "REJECT",
|
|
@@ -4587,44 +5881,79 @@ async function runGreptileReviewForPr(options) {
|
|
|
4587
5881
|
}
|
|
4588
5882
|
};
|
|
4589
5883
|
}
|
|
4590
|
-
if (score) {
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
5884
|
+
if (score?.scale === 5 && score.value < 5) {
|
|
5885
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
|
|
5886
|
+
return {
|
|
5887
|
+
verdict: "REJECT",
|
|
5888
|
+
feedback,
|
|
5889
|
+
reasons,
|
|
5890
|
+
warnings,
|
|
5891
|
+
rawPayload: {
|
|
5892
|
+
pr: options.prState,
|
|
5893
|
+
codeReviews: reviewsPayload,
|
|
5894
|
+
selectedReview,
|
|
5895
|
+
reviewDetails,
|
|
5896
|
+
comments: commentsPayload,
|
|
5897
|
+
score
|
|
5898
|
+
}
|
|
5899
|
+
};
|
|
5900
|
+
}
|
|
5901
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5902
|
+
let strictGate = null;
|
|
5903
|
+
try {
|
|
5904
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5905
|
+
projectRoot: options.projectRoot,
|
|
5906
|
+
taskId: options.taskId,
|
|
5907
|
+
prUrl,
|
|
5908
|
+
apiSignals: [{
|
|
5909
|
+
id: selectedReview.id,
|
|
5910
|
+
body: reviewBody,
|
|
5911
|
+
reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
|
|
5912
|
+
status: selectedReview.status
|
|
5913
|
+
}]
|
|
5914
|
+
});
|
|
5915
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5916
|
+
} catch (error) {
|
|
5917
|
+
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5918
|
+
return {
|
|
5919
|
+
verdict: "REJECT",
|
|
5920
|
+
feedback,
|
|
5921
|
+
reasons,
|
|
5922
|
+
warnings,
|
|
5923
|
+
rawPayload: {
|
|
5924
|
+
pr: options.prState,
|
|
5925
|
+
codeReviews: reviewsPayload,
|
|
5926
|
+
selectedReview,
|
|
5927
|
+
reviewDetails,
|
|
5928
|
+
comments: commentsPayload,
|
|
5929
|
+
score
|
|
5930
|
+
}
|
|
5931
|
+
};
|
|
5932
|
+
}
|
|
5933
|
+
if (!strictGate.approved) {
|
|
5934
|
+
return {
|
|
5935
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
5936
|
+
feedback,
|
|
5937
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5938
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
5939
|
+
rawPayload: {
|
|
5940
|
+
pr: options.prState,
|
|
5941
|
+
codeReviews: reviewsPayload,
|
|
5942
|
+
selectedReview,
|
|
5943
|
+
reviewDetails,
|
|
5944
|
+
comments: commentsPayload,
|
|
5945
|
+
score,
|
|
5946
|
+
strictGate: {
|
|
5947
|
+
approved: strictGate.approved,
|
|
5948
|
+
pending: strictGate.pending,
|
|
5949
|
+
reasons: strictGate.reasons,
|
|
5950
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5951
|
+
warnings: strictGate.warnings,
|
|
5952
|
+
greptile: strictGate.evidence.greptile,
|
|
5953
|
+
readErrors: strictGate.evidence.readErrors
|
|
4622
5954
|
}
|
|
4623
|
-
}
|
|
4624
|
-
}
|
|
4625
|
-
if (score.scale === 5 && score.value < 5) {
|
|
4626
|
-
warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
|
|
4627
|
-
}
|
|
5955
|
+
}
|
|
5956
|
+
};
|
|
4628
5957
|
}
|
|
4629
5958
|
return {
|
|
4630
5959
|
verdict: "APPROVE",
|
|
@@ -4636,7 +5965,16 @@ async function runGreptileReviewForPr(options) {
|
|
|
4636
5965
|
codeReviews: reviewsPayload,
|
|
4637
5966
|
selectedReview,
|
|
4638
5967
|
reviewDetails,
|
|
4639
|
-
comments: commentsPayload
|
|
5968
|
+
comments: commentsPayload,
|
|
5969
|
+
strictGate: {
|
|
5970
|
+
approved: strictGate.approved,
|
|
5971
|
+
pending: strictGate.pending,
|
|
5972
|
+
reasons: strictGate.reasons,
|
|
5973
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5974
|
+
warnings: strictGate.warnings,
|
|
5975
|
+
greptile: strictGate.evidence.greptile,
|
|
5976
|
+
readErrors: strictGate.evidence.readErrors
|
|
5977
|
+
}
|
|
4640
5978
|
}
|
|
4641
5979
|
};
|
|
4642
5980
|
}
|
|
@@ -4660,7 +5998,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4660
5998
|
let threads = [];
|
|
4661
5999
|
let actionableThreads = [];
|
|
4662
6000
|
let checkRollup = [];
|
|
4663
|
-
let
|
|
6001
|
+
let checkState2 = { pending: false, completed: false };
|
|
4664
6002
|
for (let attempt = 0;; attempt += 1) {
|
|
4665
6003
|
reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
|
|
4666
6004
|
selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
|
|
@@ -4669,15 +6007,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4669
6007
|
threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
|
|
4670
6008
|
actionableThreads = filterActionableGithubGreptileThreads(threads);
|
|
4671
6009
|
checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
4672
|
-
|
|
4673
|
-
const
|
|
6010
|
+
checkState2 = classifyGithubGreptileCheckState(checkRollup);
|
|
6011
|
+
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4674
6012
|
if (!shouldContinueGithubGreptileFallbackPolling({
|
|
4675
6013
|
attempt,
|
|
4676
6014
|
pollAttempts: options.pollAttempts,
|
|
4677
|
-
checkState,
|
|
6015
|
+
checkState: checkState2,
|
|
4678
6016
|
fallbackReview,
|
|
4679
6017
|
selectedReview,
|
|
4680
|
-
approvedViaReviewedAncestor
|
|
6018
|
+
approvedViaReviewedAncestor
|
|
4681
6019
|
})) {
|
|
4682
6020
|
break;
|
|
4683
6021
|
}
|
|
@@ -4705,7 +6043,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4705
6043
|
].filter(Boolean).join(`
|
|
4706
6044
|
`);
|
|
4707
6045
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
4708
|
-
if (
|
|
6046
|
+
if (checkState2.pending) {
|
|
4709
6047
|
return {
|
|
4710
6048
|
verdict: "SKIP",
|
|
4711
6049
|
feedback,
|
|
@@ -4716,34 +6054,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4716
6054
|
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4717
6055
|
};
|
|
4718
6056
|
}
|
|
4719
|
-
const
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
};
|
|
4730
|
-
}
|
|
4731
|
-
return {
|
|
4732
|
-
verdict: "SKIP",
|
|
4733
|
-
feedback,
|
|
4734
|
-
reasons: [
|
|
4735
|
-
`[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
|
|
4736
|
-
],
|
|
4737
|
-
warnings,
|
|
4738
|
-
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4739
|
-
};
|
|
4740
|
-
}
|
|
4741
|
-
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4742
|
-
if (actionableThreads.length > 0) {
|
|
6057
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
6058
|
+
let strictGate;
|
|
6059
|
+
try {
|
|
6060
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
6061
|
+
projectRoot: options.projectRoot,
|
|
6062
|
+
taskId: options.taskId,
|
|
6063
|
+
prUrl
|
|
6064
|
+
});
|
|
6065
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
6066
|
+
} catch (error) {
|
|
4743
6067
|
return {
|
|
4744
6068
|
verdict: "REJECT",
|
|
4745
6069
|
feedback,
|
|
4746
|
-
reasons:
|
|
6070
|
+
reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
|
|
4747
6071
|
warnings,
|
|
4748
6072
|
rawPayload: {
|
|
4749
6073
|
pr: options.prState,
|
|
@@ -4756,44 +6080,31 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4756
6080
|
}
|
|
4757
6081
|
};
|
|
4758
6082
|
}
|
|
4759
|
-
if (!
|
|
4760
|
-
if (approvedViaCompletedCheck) {
|
|
4761
|
-
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.`);
|
|
4762
|
-
return {
|
|
4763
|
-
verdict: "APPROVE",
|
|
4764
|
-
feedback,
|
|
4765
|
-
reasons: [],
|
|
4766
|
-
warnings,
|
|
4767
|
-
rawPayload: {
|
|
4768
|
-
pr: options.prState,
|
|
4769
|
-
selectedReview: fallbackReview,
|
|
4770
|
-
reviews,
|
|
4771
|
-
threads,
|
|
4772
|
-
checkRollup,
|
|
4773
|
-
...buildGithubGreptileFallbackRawPayload(options)
|
|
4774
|
-
}
|
|
4775
|
-
};
|
|
4776
|
-
}
|
|
6083
|
+
if (!strictGate.approved) {
|
|
4777
6084
|
return {
|
|
4778
|
-
verdict: "SKIP",
|
|
6085
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
4779
6086
|
feedback,
|
|
4780
|
-
reasons: [
|
|
4781
|
-
|
|
4782
|
-
],
|
|
4783
|
-
warnings,
|
|
6087
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
6088
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
4784
6089
|
rawPayload: {
|
|
4785
6090
|
pr: options.prState,
|
|
4786
6091
|
selectedReview: fallbackReview,
|
|
4787
6092
|
reviews,
|
|
4788
6093
|
threads,
|
|
4789
6094
|
checkRollup,
|
|
6095
|
+
actionableThreads,
|
|
6096
|
+
strictGate: {
|
|
6097
|
+
approved: strictGate.approved,
|
|
6098
|
+
pending: strictGate.pending,
|
|
6099
|
+
reasons: strictGate.reasons,
|
|
6100
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6101
|
+
warnings: strictGate.warnings,
|
|
6102
|
+
greptile: strictGate.evidence.greptile
|
|
6103
|
+
},
|
|
4790
6104
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4791
6105
|
}
|
|
4792
6106
|
};
|
|
4793
6107
|
}
|
|
4794
|
-
if (approvedViaReviewedAncestor) {
|
|
4795
|
-
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.`);
|
|
4796
|
-
}
|
|
4797
6108
|
return {
|
|
4798
6109
|
verdict: "APPROVE",
|
|
4799
6110
|
feedback,
|
|
@@ -4805,6 +6116,14 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4805
6116
|
reviews,
|
|
4806
6117
|
threads,
|
|
4807
6118
|
checkRollup,
|
|
6119
|
+
strictGate: {
|
|
6120
|
+
approved: strictGate.approved,
|
|
6121
|
+
pending: strictGate.pending,
|
|
6122
|
+
reasons: strictGate.reasons,
|
|
6123
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6124
|
+
warnings: strictGate.warnings,
|
|
6125
|
+
greptile: strictGate.evidence.greptile
|
|
6126
|
+
},
|
|
4808
6127
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4809
6128
|
}
|
|
4810
6129
|
};
|
|
@@ -4917,19 +6236,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
4917
6236
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
4918
6237
|
return true;
|
|
4919
6238
|
}
|
|
4920
|
-
return
|
|
6239
|
+
return false;
|
|
4921
6240
|
}
|
|
4922
6241
|
function shouldContinueGreptileMcpPolling(options) {
|
|
4923
6242
|
if (options.githubCheckState.completed) {
|
|
4924
6243
|
return false;
|
|
4925
6244
|
}
|
|
6245
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6246
|
+
return false;
|
|
6247
|
+
}
|
|
4926
6248
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
4927
6249
|
return true;
|
|
4928
6250
|
}
|
|
4929
|
-
return
|
|
6251
|
+
return true;
|
|
4930
6252
|
}
|
|
4931
6253
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
4932
6254
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
6255
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6256
|
+
return false;
|
|
6257
|
+
}
|
|
4933
6258
|
if (waitingForVisiblePendingReview) {
|
|
4934
6259
|
return true;
|
|
4935
6260
|
}
|
|
@@ -4990,6 +6315,20 @@ function runGhJson(projectRoot, args) {
|
|
|
4990
6315
|
throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
|
|
4991
6316
|
}
|
|
4992
6317
|
}
|
|
6318
|
+
async function collectStrictPrEvidenceForVerifier(input) {
|
|
6319
|
+
return collectPrReviewEvidence({
|
|
6320
|
+
projectRoot: input.projectRoot,
|
|
6321
|
+
prUrl: input.prUrl,
|
|
6322
|
+
taskId: input.taskId,
|
|
6323
|
+
runId: "verifier",
|
|
6324
|
+
cycle: 0,
|
|
6325
|
+
apiSignals: input.apiSignals ?? [],
|
|
6326
|
+
command: async (args, options) => {
|
|
6327
|
+
const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
6328
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
6329
|
+
}
|
|
6330
|
+
});
|
|
6331
|
+
}
|
|
4993
6332
|
function deriveRepoName(projectRoot, prState) {
|
|
4994
6333
|
const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
|
|
4995
6334
|
if (fromUrl?.[1]) {
|
|
@@ -5004,8 +6343,9 @@ function resolvePrHeadSha(projectRoot, prState) {
|
|
|
5004
6343
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
5005
6344
|
return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
5006
6345
|
}
|
|
5007
|
-
function
|
|
5008
|
-
|
|
6346
|
+
function isGreptileGithubLogin2(login) {
|
|
6347
|
+
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
6348
|
+
return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
|
|
5009
6349
|
}
|
|
5010
6350
|
function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
|
|
5011
6351
|
const matching = sortGithubGreptileReviews(reviews);
|
|
@@ -5022,7 +6362,7 @@ function pickLatestGithubGreptileReview(reviews) {
|
|
|
5022
6362
|
return sortGithubGreptileReviews(reviews)[0] || null;
|
|
5023
6363
|
}
|
|
5024
6364
|
function sortGithubGreptileReviews(reviews) {
|
|
5025
|
-
return reviews.filter((review) =>
|
|
6365
|
+
return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
|
|
5026
6366
|
}
|
|
5027
6367
|
function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
5028
6368
|
const response = runGhJson(projectRoot, [
|
|
@@ -5095,32 +6435,6 @@ function classifyGithubGreptileCheckState(checks) {
|
|
|
5095
6435
|
}
|
|
5096
6436
|
return { pending: false, completed: false };
|
|
5097
6437
|
}
|
|
5098
|
-
function isGithubGreptileCheckApproved(checks) {
|
|
5099
|
-
const greptileChecks = checks.filter((check) => {
|
|
5100
|
-
const label = (check.name || check.context || "").toLowerCase();
|
|
5101
|
-
return label.includes("greptile");
|
|
5102
|
-
});
|
|
5103
|
-
if (greptileChecks.length === 0) {
|
|
5104
|
-
return false;
|
|
5105
|
-
}
|
|
5106
|
-
for (const check of greptileChecks) {
|
|
5107
|
-
if ((check.__typename || "") === "CheckRun") {
|
|
5108
|
-
if ((check.status || "").toUpperCase() !== "COMPLETED") {
|
|
5109
|
-
return false;
|
|
5110
|
-
}
|
|
5111
|
-
const conclusion = (check.conclusion || "").toUpperCase();
|
|
5112
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
|
|
5113
|
-
return false;
|
|
5114
|
-
}
|
|
5115
|
-
continue;
|
|
5116
|
-
}
|
|
5117
|
-
const state = (check.state || "").toUpperCase();
|
|
5118
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
|
|
5119
|
-
return false;
|
|
5120
|
-
}
|
|
5121
|
-
}
|
|
5122
|
-
return true;
|
|
5123
|
-
}
|
|
5124
6438
|
function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
|
|
5125
6439
|
const [owner, name] = repoName.split("/");
|
|
5126
6440
|
if (!owner || !name) {
|
|
@@ -5146,7 +6460,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
5146
6460
|
return [];
|
|
5147
6461
|
}
|
|
5148
6462
|
const comments = thread.comments?.nodes || [];
|
|
5149
|
-
const latestGreptileComment = [...comments].reverse().find((comment) =>
|
|
6463
|
+
const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
|
|
5150
6464
|
if (!latestGreptileComment?.path?.trim()) {
|
|
5151
6465
|
return [];
|
|
5152
6466
|
}
|
|
@@ -5155,7 +6469,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
5155
6469
|
}
|
|
5156
6470
|
function resolvePrRepoRoot(projectRoot, prState) {
|
|
5157
6471
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
5158
|
-
if (prState.target === "monorepo" && runtimeWorkspace &&
|
|
6472
|
+
if (prState.target === "monorepo" && runtimeWorkspace && existsSync20(resolve23(runtimeWorkspace, ".git"))) {
|
|
5159
6473
|
return runtimeWorkspace;
|
|
5160
6474
|
}
|
|
5161
6475
|
const paths = resolveHarnessPaths(projectRoot);
|
|
@@ -5168,11 +6482,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
5168
6482
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
5169
6483
|
return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
5170
6484
|
}
|
|
5171
|
-
function stripHtml(input) {
|
|
5172
|
-
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
5173
|
-
|
|
5174
|
-
`).trim();
|
|
5175
|
-
}
|
|
5176
6485
|
function summarizeComment(input) {
|
|
5177
6486
|
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
5178
6487
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
@@ -5181,31 +6490,14 @@ function asGreptileInfrastructureWarning(reason) {
|
|
|
5181
6490
|
return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
|
|
5182
6491
|
}
|
|
5183
6492
|
function isAiReviewApproved(input) {
|
|
6493
|
+
if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
|
|
6494
|
+
return false;
|
|
6495
|
+
}
|
|
5184
6496
|
if (input.reviewMode !== "required") {
|
|
5185
6497
|
return true;
|
|
5186
6498
|
}
|
|
5187
6499
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
5188
6500
|
}
|
|
5189
|
-
function parseGreptileScore(input) {
|
|
5190
|
-
const text = stripHtml(input);
|
|
5191
|
-
const patterns = [
|
|
5192
|
-
/confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5193
|
-
/\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5194
|
-
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
|
|
5195
|
-
];
|
|
5196
|
-
for (const pattern of patterns) {
|
|
5197
|
-
const match = pattern.exec(text);
|
|
5198
|
-
if (!match) {
|
|
5199
|
-
continue;
|
|
5200
|
-
}
|
|
5201
|
-
const value = Number.parseInt(match[1] || "", 10);
|
|
5202
|
-
const scale = Number.parseInt(match[2] || "", 10);
|
|
5203
|
-
if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
|
|
5204
|
-
return { value, scale };
|
|
5205
|
-
}
|
|
5206
|
-
}
|
|
5207
|
-
return null;
|
|
5208
|
-
}
|
|
5209
6501
|
|
|
5210
6502
|
// packages/runtime/src/control-plane/provider/runtime-instructions.ts
|
|
5211
6503
|
var CLAUDE_ROUTER_TOOL_NAMES = [
|
|
@@ -5557,16 +6849,16 @@ async function taskDeps(projectRoot, taskId) {
|
|
|
5557
6849
|
for (const dep of deps) {
|
|
5558
6850
|
const artifactDir = artifactDirForId(projectRoot, dep);
|
|
5559
6851
|
console.log(`=== ${dep} ===`);
|
|
5560
|
-
if (!
|
|
6852
|
+
if (!existsSync21(artifactDir)) {
|
|
5561
6853
|
console.log(` (no artifacts yet)
|
|
5562
6854
|
`);
|
|
5563
6855
|
continue;
|
|
5564
6856
|
}
|
|
5565
|
-
printArtifactSection(
|
|
5566
|
-
printArtifactSection(
|
|
5567
|
-
const changedFiles =
|
|
5568
|
-
if (
|
|
5569
|
-
const lines =
|
|
6857
|
+
printArtifactSection(resolve24(artifactDir, "decision-log.md"), "--- Decisions ---");
|
|
6858
|
+
printArtifactSection(resolve24(artifactDir, "next-actions.md"), "--- Next Actions (for you) ---");
|
|
6859
|
+
const changedFiles = resolve24(artifactDir, "changed-files.txt");
|
|
6860
|
+
if (existsSync21(changedFiles)) {
|
|
6861
|
+
const lines = readFileSync11(changedFiles, "utf-8").split(/\r?\n/).filter(Boolean);
|
|
5570
6862
|
console.log(`--- Changed Files (${lines.length}) ---`);
|
|
5571
6863
|
for (const line of lines) {
|
|
5572
6864
|
console.log(line);
|
|
@@ -5611,12 +6903,12 @@ function taskRecord(projectRoot, type, text, taskId) {
|
|
|
5611
6903
|
throw new Error("No active task.");
|
|
5612
6904
|
}
|
|
5613
6905
|
const paths = resolveHarnessPaths(projectRoot);
|
|
5614
|
-
|
|
6906
|
+
mkdirSync11(paths.stateDir, { recursive: true });
|
|
5615
6907
|
if (type === "decision") {
|
|
5616
|
-
const artifactDir =
|
|
5617
|
-
|
|
6908
|
+
const artifactDir = resolve24(paths.artifactsDir, activeTask);
|
|
6909
|
+
mkdirSync11(artifactDir, { recursive: true });
|
|
5618
6910
|
const timestamp = nowIso();
|
|
5619
|
-
appendFileSync(
|
|
6911
|
+
appendFileSync(resolve24(artifactDir, "decision-log.md"), `
|
|
5620
6912
|
### ${timestamp}
|
|
5621
6913
|
|
|
5622
6914
|
${text}
|
|
@@ -5626,15 +6918,15 @@ ${text}
|
|
|
5626
6918
|
return;
|
|
5627
6919
|
}
|
|
5628
6920
|
const failedPath = paths.failedApproachesPath;
|
|
5629
|
-
if (!
|
|
5630
|
-
|
|
6921
|
+
if (!existsSync21(failedPath)) {
|
|
6922
|
+
writeFileSync11(failedPath, `# Failed Approaches Log
|
|
5631
6923
|
|
|
5632
6924
|
This file records approaches that did not work.
|
|
5633
6925
|
|
|
5634
6926
|
`, "utf-8");
|
|
5635
6927
|
}
|
|
5636
|
-
const content =
|
|
5637
|
-
const attempts = (content.match(new RegExp(`^## ${
|
|
6928
|
+
const content = readFileSync11(failedPath, "utf-8");
|
|
6929
|
+
const attempts = (content.match(new RegExp(`^## ${escapeRegExp(activeTask)}\\b`, "gm")) || []).length + 1;
|
|
5638
6930
|
appendFileSync(failedPath, `
|
|
5639
6931
|
## ${activeTask} - Attempt ${attempts} (${nowIso()})
|
|
5640
6932
|
|
|
@@ -5650,40 +6942,40 @@ function taskArtifacts(projectRoot, taskId) {
|
|
|
5650
6942
|
throw new Error("No active task.");
|
|
5651
6943
|
}
|
|
5652
6944
|
const paths = resolveHarnessPaths(projectRoot);
|
|
5653
|
-
const artifactDir =
|
|
5654
|
-
|
|
6945
|
+
const artifactDir = resolve24(paths.artifactsDir, activeTask);
|
|
6946
|
+
mkdirSync11(artifactDir, { recursive: true });
|
|
5655
6947
|
const changed = changedFilesForTask(projectRoot, activeTask, true);
|
|
5656
|
-
|
|
6948
|
+
writeFileSync11(resolve24(artifactDir, "changed-files.txt"), `${changed.join(`
|
|
5657
6949
|
`)}
|
|
5658
6950
|
`, "utf-8");
|
|
5659
6951
|
console.log(`changed-files.txt: ${changed.length} files`);
|
|
5660
|
-
const taskResultPath =
|
|
5661
|
-
if (!
|
|
6952
|
+
const taskResultPath = resolve24(artifactDir, "task-result.json");
|
|
6953
|
+
if (!existsSync21(taskResultPath)) {
|
|
5662
6954
|
const template = {
|
|
5663
6955
|
task_id: activeTask,
|
|
5664
6956
|
status: "completed",
|
|
5665
6957
|
summary: "TODO: Write a one-line summary of what you did",
|
|
5666
6958
|
completed_at: nowIso()
|
|
5667
6959
|
};
|
|
5668
|
-
|
|
6960
|
+
writeFileSync11(taskResultPath, `${JSON.stringify(template, null, 2)}
|
|
5669
6961
|
`, "utf-8");
|
|
5670
6962
|
console.log("task-result.json: created (update the summary!)");
|
|
5671
6963
|
} else {
|
|
5672
6964
|
console.log("task-result.json: already exists");
|
|
5673
6965
|
}
|
|
5674
|
-
const decisionLogPath =
|
|
5675
|
-
if (!
|
|
6966
|
+
const decisionLogPath = resolve24(artifactDir, "decision-log.md");
|
|
6967
|
+
if (!existsSync21(decisionLogPath)) {
|
|
5676
6968
|
const content = `# Decision Log: ${activeTask}
|
|
5677
6969
|
|
|
5678
6970
|
Record key decisions here using: rig-agent record decision "..."
|
|
5679
6971
|
`;
|
|
5680
|
-
|
|
6972
|
+
writeFileSync11(decisionLogPath, content, "utf-8");
|
|
5681
6973
|
console.log("decision-log.md: created (record your decisions!)");
|
|
5682
6974
|
} else {
|
|
5683
6975
|
console.log("decision-log.md: already exists");
|
|
5684
6976
|
}
|
|
5685
|
-
const nextActionsPath =
|
|
5686
|
-
if (!
|
|
6977
|
+
const nextActionsPath = resolve24(artifactDir, "next-actions.md");
|
|
6978
|
+
if (!existsSync21(nextActionsPath)) {
|
|
5687
6979
|
const content = [
|
|
5688
6980
|
`# Next Actions: ${activeTask}`,
|
|
5689
6981
|
"",
|
|
@@ -5700,13 +6992,13 @@ Record key decisions here using: rig-agent record decision "..."
|
|
|
5700
6992
|
""
|
|
5701
6993
|
].join(`
|
|
5702
6994
|
`);
|
|
5703
|
-
|
|
6995
|
+
writeFileSync11(nextActionsPath, content, "utf-8");
|
|
5704
6996
|
console.log("next-actions.md: created (add recommendations for downstream tasks!)");
|
|
5705
6997
|
} else {
|
|
5706
6998
|
console.log("next-actions.md: already exists");
|
|
5707
6999
|
}
|
|
5708
|
-
const validationSummaryPath =
|
|
5709
|
-
if (
|
|
7000
|
+
const validationSummaryPath = resolve24(artifactDir, "validation-summary.json");
|
|
7001
|
+
if (existsSync21(validationSummaryPath)) {
|
|
5710
7002
|
console.log("validation-summary.json: already exists");
|
|
5711
7003
|
} else {
|
|
5712
7004
|
console.log("validation-summary.json: not yet created (run: rig-agent validate)");
|
|
@@ -5720,8 +7012,8 @@ function taskArtifactDir(projectRoot, taskId) {
|
|
|
5720
7012
|
throw new Error("No active task.");
|
|
5721
7013
|
}
|
|
5722
7014
|
const paths = resolveHarnessPaths(projectRoot);
|
|
5723
|
-
const artifactDir =
|
|
5724
|
-
|
|
7015
|
+
const artifactDir = resolve24(paths.artifactsDir, activeTask);
|
|
7016
|
+
mkdirSync11(artifactDir, { recursive: true });
|
|
5725
7017
|
return artifactDir;
|
|
5726
7018
|
}
|
|
5727
7019
|
function taskArtifactWrite(projectRoot, filename, content, taskId) {
|
|
@@ -5730,10 +7022,10 @@ function taskArtifactWrite(projectRoot, filename, content, taskId) {
|
|
|
5730
7022
|
throw new Error("No active task.");
|
|
5731
7023
|
}
|
|
5732
7024
|
const paths = resolveHarnessPaths(projectRoot);
|
|
5733
|
-
const artifactDir =
|
|
5734
|
-
|
|
5735
|
-
const targetPath =
|
|
5736
|
-
|
|
7025
|
+
const artifactDir = resolve24(paths.artifactsDir, activeTask);
|
|
7026
|
+
mkdirSync11(artifactDir, { recursive: true });
|
|
7027
|
+
const targetPath = resolve24(artifactDir, filename);
|
|
7028
|
+
writeFileSync11(targetPath, content, "utf-8");
|
|
5737
7029
|
console.log(`Wrote: ${targetPath}`);
|
|
5738
7030
|
}
|
|
5739
7031
|
function taskArtifactRead(projectRoot, filename, options) {
|
|
@@ -5742,11 +7034,11 @@ function taskArtifactRead(projectRoot, filename, options) {
|
|
|
5742
7034
|
throw new Error("No active task.");
|
|
5743
7035
|
}
|
|
5744
7036
|
const maxBytes = options?.maxBytes ?? 64 * 1024;
|
|
5745
|
-
const targetPath =
|
|
5746
|
-
if (!
|
|
7037
|
+
const targetPath = resolve24(taskArtifactDir(projectRoot, activeTask), filename);
|
|
7038
|
+
if (!existsSync21(targetPath)) {
|
|
5747
7039
|
throw new Error(`Artifact not found: ${targetPath}`);
|
|
5748
7040
|
}
|
|
5749
|
-
const buffer =
|
|
7041
|
+
const buffer = readFileSync11(targetPath);
|
|
5750
7042
|
const preview = buffer.subarray(0, maxBytes);
|
|
5751
7043
|
return {
|
|
5752
7044
|
path: targetPath,
|
|
@@ -5791,12 +7083,12 @@ async function taskValidate(projectRoot, taskId, validatorRegistry) {
|
|
|
5791
7083
|
console.log(`Validation passed: ${summary.passed}/${summary.total}`);
|
|
5792
7084
|
return true;
|
|
5793
7085
|
}
|
|
5794
|
-
async function taskVerify(projectRoot,
|
|
7086
|
+
async function taskVerify(projectRoot, taskId) {
|
|
5795
7087
|
const activeTask = taskId || currentTaskId(projectRoot);
|
|
5796
7088
|
if (!activeTask) {
|
|
5797
7089
|
throw new Error("No active task.");
|
|
5798
7090
|
}
|
|
5799
|
-
const outcome = await verifyTask({ projectRoot, taskId: activeTask
|
|
7091
|
+
const outcome = await verifyTask({ projectRoot, taskId: activeTask });
|
|
5800
7092
|
if (!outcome.approved) {
|
|
5801
7093
|
console.log("REJECT:");
|
|
5802
7094
|
for (const reason of outcome.localReasons) {
|
|
@@ -5896,7 +7188,7 @@ function collectTaskChangedFiles(projectRoot, taskId, includeCommitted) {
|
|
|
5896
7188
|
[projectRoot, ""],
|
|
5897
7189
|
[monorepoRepoRoot, ""]
|
|
5898
7190
|
]) {
|
|
5899
|
-
if (!
|
|
7191
|
+
if (!existsSync21(resolve24(repo, ".git"))) {
|
|
5900
7192
|
continue;
|
|
5901
7193
|
}
|
|
5902
7194
|
if (includeCommitted && repo === monorepoRepoRoot) {
|
|
@@ -5934,8 +7226,8 @@ function filterTaskChangedFiles(projectRoot, taskId, files, scoped) {
|
|
|
5934
7226
|
}
|
|
5935
7227
|
function resolveTaskMonorepoRoot(projectRoot) {
|
|
5936
7228
|
const runtimeWorkspace = loadRuntimeContextFromEnv()?.workspaceDir || process.env.RIG_TASK_WORKSPACE?.trim();
|
|
5937
|
-
if (runtimeWorkspace &&
|
|
5938
|
-
return
|
|
7229
|
+
if (runtimeWorkspace && existsSync21(resolve24(runtimeWorkspace, ".git"))) {
|
|
7230
|
+
return resolve24(runtimeWorkspace);
|
|
5939
7231
|
}
|
|
5940
7232
|
return resolveHarnessPaths(projectRoot).monorepoRoot;
|
|
5941
7233
|
}
|
|
@@ -5963,7 +7255,7 @@ function resolveRuntimeInitialHeadCommit(projectRoot, repo) {
|
|
|
5963
7255
|
const runtimeContext = loadRuntimeContextFromEnv();
|
|
5964
7256
|
if (runtimeContext?.initialHeadCommits?.monorepo?.trim()) {
|
|
5965
7257
|
const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
|
|
5966
|
-
if (
|
|
7258
|
+
if (resolve24(monorepoRoot) === resolve24(repo)) {
|
|
5967
7259
|
return runtimeContext.initialHeadCommits.monorepo.trim();
|
|
5968
7260
|
}
|
|
5969
7261
|
}
|
|
@@ -5973,7 +7265,7 @@ function resolveMonorepoBaseCommit(projectRoot, repo) {
|
|
|
5973
7265
|
const runtimeContext = loadRuntimeContextFromEnv();
|
|
5974
7266
|
if (runtimeContext?.monorepoBaseCommit?.trim()) {
|
|
5975
7267
|
const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
|
|
5976
|
-
if (
|
|
7268
|
+
if (resolve24(monorepoRoot) === resolve24(repo)) {
|
|
5977
7269
|
return runtimeContext.monorepoBaseCommit.trim();
|
|
5978
7270
|
}
|
|
5979
7271
|
}
|
|
@@ -6007,7 +7299,7 @@ function resolveRuntimeDirtyBaseline(projectRoot, repo) {
|
|
|
6007
7299
|
return new Set;
|
|
6008
7300
|
}
|
|
6009
7301
|
const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
|
|
6010
|
-
const selected =
|
|
7302
|
+
const selected = resolve24(repo) === resolve24(monorepoRoot) ? dirtyFiles.monorepo : resolve24(repo) === resolve24(projectRoot) ? dirtyFiles.project : undefined;
|
|
6011
7303
|
return new Set((selected || []).map((file) => normalizeChangedFilePath(file)).filter(Boolean));
|
|
6012
7304
|
}
|
|
6013
7305
|
function normalizeChangedFilePath(file) {
|
|
@@ -6107,12 +7399,12 @@ function printIndented(text) {
|
|
|
6107
7399
|
}
|
|
6108
7400
|
}
|
|
6109
7401
|
function readLocalBeadsTasks(projectRoot) {
|
|
6110
|
-
const issuesPath =
|
|
6111
|
-
if (!
|
|
7402
|
+
const issuesPath = resolve24(resolveMonorepoRoot2(projectRoot), ".beads/issues.jsonl");
|
|
7403
|
+
if (!existsSync21(issuesPath)) {
|
|
6112
7404
|
return [];
|
|
6113
7405
|
}
|
|
6114
7406
|
const tasks = [];
|
|
6115
|
-
for (const line of
|
|
7407
|
+
for (const line of readFileSync11(issuesPath, "utf-8").split(/\r?\n/)) {
|
|
6116
7408
|
const trimmed = line.trim();
|
|
6117
7409
|
if (!trimmed) {
|
|
6118
7410
|
continue;
|
|
@@ -6283,14 +7575,14 @@ function taskDependencies(projectRoot, taskId, tracker) {
|
|
|
6283
7575
|
return [...ids].sort();
|
|
6284
7576
|
}
|
|
6285
7577
|
function printArtifactSection(path, header) {
|
|
6286
|
-
if (!
|
|
7578
|
+
if (!existsSync21(path)) {
|
|
6287
7579
|
return;
|
|
6288
7580
|
}
|
|
6289
7581
|
console.log(header);
|
|
6290
|
-
process.stdout.write(
|
|
7582
|
+
process.stdout.write(readFileSync11(path, "utf-8"));
|
|
6291
7583
|
console.log("");
|
|
6292
7584
|
}
|
|
6293
|
-
function
|
|
7585
|
+
function escapeRegExp(value) {
|
|
6294
7586
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6295
7587
|
}
|
|
6296
7588
|
function changedFilesForTask(projectRoot, taskId, scoped) {
|