@h-rig/runtime 0.0.6-alpha.3 → 0.0.6-alpha.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rig-agent-dispatch.js +1165 -785
- package/dist/bin/rig-agent.js +458 -389
- package/dist/src/control-plane/agent-wrapper.js +1191 -504
- package/dist/src/control-plane/authority-files.js +12 -6
- package/dist/src/control-plane/harness-main.js +2186 -1786
- package/dist/src/control-plane/hooks/completion-verification.js +2084 -1019
- package/dist/src/control-plane/hooks/inject-context.js +193 -139
- package/dist/src/control-plane/hooks/submodule-branch.js +603 -545
- package/dist/src/control-plane/hooks/task-runtime-start.js +603 -545
- package/dist/src/control-plane/materialize-task-config.js +64 -8
- package/dist/src/control-plane/native/git-ops.js +90 -64
- package/dist/src/control-plane/native/harness-cli.js +1989 -682
- package/dist/src/control-plane/native/pr-automation.js +1657 -54
- package/dist/src/control-plane/native/pr-review-gate.js +1455 -0
- package/dist/src/control-plane/native/repo-ops.js +3 -0
- package/dist/src/control-plane/native/run-ops.js +39 -13
- package/dist/src/control-plane/native/task-ops.js +1819 -527
- package/dist/src/control-plane/native/validator.js +163 -109
- package/dist/src/control-plane/native/verifier.js +1616 -323
- package/dist/src/control-plane/native/workspace-ops.js +12 -6
- package/dist/src/control-plane/pi-sessiond/bin.js +793 -0
- package/dist/src/control-plane/pi-sessiond/client.js +41 -0
- package/dist/src/control-plane/pi-sessiond/event-hub.js +59 -0
- package/dist/src/control-plane/pi-sessiond/extension-ui-context.js +198 -0
- package/dist/src/control-plane/pi-sessiond/launcher.js +173 -0
- package/dist/src/control-plane/pi-sessiond/server.js +802 -0
- package/dist/src/control-plane/pi-sessiond/session-service.js +540 -0
- package/dist/src/control-plane/pi-sessiond/types.js +1 -0
- package/dist/src/control-plane/plugin-host-context.js +54 -0
- package/dist/src/control-plane/runtime/image/fingerprint-sidecar.js +3 -0
- package/dist/src/control-plane/runtime/image/index.js +3 -0
- package/dist/src/control-plane/runtime/image-fingerprint-sidecar.js +3 -0
- package/dist/src/control-plane/runtime/image.js +3 -0
- package/dist/src/control-plane/runtime/index.js +517 -722
- package/dist/src/control-plane/runtime/isolation/home.js +28 -6
- package/dist/src/control-plane/runtime/isolation/index.js +541 -461
- package/dist/src/control-plane/runtime/isolation/runner.js +28 -6
- package/dist/src/control-plane/runtime/isolation/shared.js +9 -6
- package/dist/src/control-plane/runtime/isolation.js +541 -461
- package/dist/src/control-plane/runtime/plugin-mode.js +3 -27
- package/dist/src/control-plane/runtime/queue.js +458 -385
- package/dist/src/control-plane/runtime/snapshot/task-run.js +3 -0
- package/dist/src/control-plane/runtime/task-run-snapshot.js +3 -0
- package/dist/src/control-plane/skill-materializer.js +46 -0
- package/dist/src/control-plane/tasks/source-aware-task-config-source.js +14 -2
- package/dist/src/control-plane/tasks/source-lifecycle.js +86 -32
- package/dist/src/index.js +27 -298
- package/dist/src/layout.js +12 -7
- package/dist/src/local-server.js +20 -14
- package/native/darwin-arm64/rig-git +0 -0
- package/native/darwin-arm64/rig-git.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-shell +0 -0
- package/native/darwin-arm64/rig-shell.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-tools +0 -0
- package/native/darwin-arm64/rig-tools.build-manifest.json +1 -1
- package/native/darwin-arm64/runtime-native.dylib +0 -0
- package/package.json +8 -6
- package/dist/src/control-plane/runtime/plugins.js +0 -1131
- package/dist/src/plugins.js +0 -329
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/runtime/src/control-plane/native/harness-cli.ts
|
|
3
|
-
import { existsSync as
|
|
4
|
-
import { resolve as
|
|
3
|
+
import { existsSync as existsSync29 } from "fs";
|
|
4
|
+
import { resolve as resolve30 } from "path";
|
|
5
5
|
|
|
6
6
|
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
7
|
-
import { existsSync as
|
|
8
|
-
import { dirname as dirname11, isAbsolute as isAbsolute2, resolve as
|
|
7
|
+
import { existsSync as existsSync22, lstatSync, mkdirSync as mkdirSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
|
|
8
|
+
import { dirname as dirname11, isAbsolute as isAbsolute2, resolve as resolve25 } from "path";
|
|
9
9
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10
10
|
|
|
11
11
|
// packages/runtime/src/control-plane/runtime/baked-secrets.ts
|
|
@@ -274,8 +274,8 @@ function isAgentRuntimeContextPath(path) {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
// packages/runtime/src/control-plane/native/task-ops.ts
|
|
277
|
-
import { appendFileSync, existsSync as
|
|
278
|
-
import { resolve as
|
|
277
|
+
import { appendFileSync, existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
|
|
278
|
+
import { resolve as resolve24 } from "path";
|
|
279
279
|
|
|
280
280
|
// packages/runtime/src/build-time-config.ts
|
|
281
281
|
function normalizeBuildConfig(value) {
|
|
@@ -760,6 +760,49 @@ function safeReadJson(path) {
|
|
|
760
760
|
}
|
|
761
761
|
}
|
|
762
762
|
|
|
763
|
+
// packages/runtime/src/control-plane/skill-materializer.ts
|
|
764
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
765
|
+
import { resolve as resolve7 } from "path";
|
|
766
|
+
import { loadSkill } from "@rig/skill-loader";
|
|
767
|
+
var MARKER_FILENAME = ".rig-plugin";
|
|
768
|
+
function skillDirName(id) {
|
|
769
|
+
return id.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
770
|
+
}
|
|
771
|
+
async function materializeSkills(projectRoot, entries) {
|
|
772
|
+
const skillsRoot = resolve7(projectRoot, ".pi", "skills");
|
|
773
|
+
if (existsSync5(skillsRoot)) {
|
|
774
|
+
for (const name of readdirSync(skillsRoot)) {
|
|
775
|
+
const dir = resolve7(skillsRoot, name);
|
|
776
|
+
if (existsSync5(resolve7(dir, MARKER_FILENAME))) {
|
|
777
|
+
rmSync(dir, { recursive: true, force: true });
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const written = [];
|
|
782
|
+
for (const { pluginName, skill } of entries) {
|
|
783
|
+
const sourcePath = resolve7(projectRoot, skill.path);
|
|
784
|
+
if (!existsSync5(sourcePath)) {
|
|
785
|
+
console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${sourcePath} does not exist`);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
let body;
|
|
789
|
+
try {
|
|
790
|
+
await loadSkill(sourcePath);
|
|
791
|
+
body = readFileSync4(sourcePath, "utf-8");
|
|
792
|
+
} catch (err) {
|
|
793
|
+
console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${err instanceof Error ? err.message : err}`);
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
const dir = resolve7(skillsRoot, skillDirName(skill.id));
|
|
797
|
+
mkdirSync3(dir, { recursive: true });
|
|
798
|
+
writeFileSync3(resolve7(dir, "SKILL.md"), body, "utf-8");
|
|
799
|
+
writeFileSync3(resolve7(dir, MARKER_FILENAME), `${JSON.stringify({ plugin: pluginName, skillId: skill.id }, null, 2)}
|
|
800
|
+
`, "utf-8");
|
|
801
|
+
written.push({ id: skill.id, pluginName, directory: dir });
|
|
802
|
+
}
|
|
803
|
+
return written;
|
|
804
|
+
}
|
|
805
|
+
|
|
763
806
|
// packages/runtime/src/control-plane/plugin-host-context.ts
|
|
764
807
|
async function buildPluginHostContext(projectRoot) {
|
|
765
808
|
let config;
|
|
@@ -796,6 +839,17 @@ async function buildPluginHostContext(projectRoot) {
|
|
|
796
839
|
} catch (err) {
|
|
797
840
|
console.warn(`[plugin-host] hook materialization failed: ${err instanceof Error ? err.message : err}`);
|
|
798
841
|
}
|
|
842
|
+
try {
|
|
843
|
+
const skillEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.skills ?? []).map((skill) => ({
|
|
844
|
+
pluginName: plugin.name,
|
|
845
|
+
skill
|
|
846
|
+
})));
|
|
847
|
+
if (skillEntries.length > 0) {
|
|
848
|
+
await materializeSkills(projectRoot, skillEntries);
|
|
849
|
+
}
|
|
850
|
+
} catch (err) {
|
|
851
|
+
console.warn(`[plugin-host] skill materialization failed: ${err instanceof Error ? err.message : err}`);
|
|
852
|
+
}
|
|
799
853
|
return {
|
|
800
854
|
config,
|
|
801
855
|
pluginHost,
|
|
@@ -809,12 +863,12 @@ async function buildPluginHostContext(projectRoot) {
|
|
|
809
863
|
|
|
810
864
|
// packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
|
|
811
865
|
import { spawnSync } from "child_process";
|
|
812
|
-
import { existsSync as
|
|
813
|
-
import { basename as basename3, join as join2, resolve as
|
|
866
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync4 } from "fs";
|
|
867
|
+
import { basename as basename3, join as join2, resolve as resolve9 } from "path";
|
|
814
868
|
|
|
815
869
|
// packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
|
|
816
|
-
import { existsSync as
|
|
817
|
-
import { resolve as
|
|
870
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
871
|
+
import { resolve as resolve8 } from "path";
|
|
818
872
|
|
|
819
873
|
// packages/runtime/src/control-plane/tasks/task-record-reader.ts
|
|
820
874
|
async function findTaskById(reader, id) {
|
|
@@ -837,7 +891,7 @@ class LegacyTaskConfigReadError extends Error {
|
|
|
837
891
|
}
|
|
838
892
|
}
|
|
839
893
|
function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
|
|
840
|
-
const configPath = options.configPath ??
|
|
894
|
+
const configPath = options.configPath ?? resolve8(projectRoot, ".rig", "task-config.json");
|
|
841
895
|
const reader = {
|
|
842
896
|
async listTasks() {
|
|
843
897
|
return readLegacyTaskRecords(projectRoot, configPath);
|
|
@@ -848,8 +902,8 @@ function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
|
|
|
848
902
|
};
|
|
849
903
|
return reader;
|
|
850
904
|
}
|
|
851
|
-
function readLegacyTaskRecords(projectRoot, configPath =
|
|
852
|
-
if (!
|
|
905
|
+
function readLegacyTaskRecords(projectRoot, configPath = resolve8(projectRoot, ".rig", "task-config.json")) {
|
|
906
|
+
if (!existsSync6(configPath)) {
|
|
853
907
|
return [];
|
|
854
908
|
}
|
|
855
909
|
const rawConfig = readLegacyTaskConfigJson(projectRoot, configPath);
|
|
@@ -857,7 +911,7 @@ function readLegacyTaskRecords(projectRoot, configPath = resolve7(projectRoot, "
|
|
|
857
911
|
}
|
|
858
912
|
function readLegacyTaskConfigJson(projectRoot, configPath) {
|
|
859
913
|
try {
|
|
860
|
-
const parsed = JSON.parse(
|
|
914
|
+
const parsed = JSON.parse(readFileSync5(configPath, "utf8"));
|
|
861
915
|
if (isPlainRecord(parsed)) {
|
|
862
916
|
return parsed;
|
|
863
917
|
}
|
|
@@ -941,7 +995,7 @@ function isPlainRecord(candidate) {
|
|
|
941
995
|
var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
|
|
942
996
|
var FILE_TASK_PATTERN = /\.(task\.)?json$/;
|
|
943
997
|
function createSourceAwareTaskConfigRecordReader(projectRoot, options = {}) {
|
|
944
|
-
const configPath = options.configPath ??
|
|
998
|
+
const configPath = options.configPath ?? resolve9(projectRoot, ".rig", "task-config.json");
|
|
945
999
|
const legacy = createLegacyTaskConfigRecordReader(projectRoot, { configPath });
|
|
946
1000
|
const spawnFn = options.spawn ?? spawnSync;
|
|
947
1001
|
const ghBinary = options.ghBinary ?? "gh";
|
|
@@ -1024,10 +1078,10 @@ function readMaterializedTaskMetadata(entry) {
|
|
|
1024
1078
|
return metadata;
|
|
1025
1079
|
}
|
|
1026
1080
|
function readConfiguredFilesTaskSourcePath(projectRoot) {
|
|
1027
|
-
const jsonPath =
|
|
1028
|
-
if (
|
|
1081
|
+
const jsonPath = resolve9(projectRoot, "rig.config.json");
|
|
1082
|
+
if (existsSync7(jsonPath)) {
|
|
1029
1083
|
try {
|
|
1030
|
-
const parsed = JSON.parse(
|
|
1084
|
+
const parsed = JSON.parse(readFileSync6(jsonPath, "utf8"));
|
|
1031
1085
|
if (isPlainRecord2(parsed) && isPlainRecord2(parsed.taskSource)) {
|
|
1032
1086
|
const source = parsed.taskSource;
|
|
1033
1087
|
return source.kind === "files" && typeof source.path === "string" ? source.path : null;
|
|
@@ -1036,12 +1090,12 @@ function readConfiguredFilesTaskSourcePath(projectRoot) {
|
|
|
1036
1090
|
return null;
|
|
1037
1091
|
}
|
|
1038
1092
|
}
|
|
1039
|
-
const tsPath =
|
|
1040
|
-
if (!
|
|
1093
|
+
const tsPath = resolve9(projectRoot, "rig.config.ts");
|
|
1094
|
+
if (!existsSync7(tsPath)) {
|
|
1041
1095
|
return null;
|
|
1042
1096
|
}
|
|
1043
1097
|
try {
|
|
1044
|
-
const source =
|
|
1098
|
+
const source = readFileSync6(tsPath, "utf8");
|
|
1045
1099
|
const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
|
|
1046
1100
|
const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
|
|
1047
1101
|
if (kind !== "files") {
|
|
@@ -1061,10 +1115,10 @@ function readRawTaskEntry(configPath, taskId) {
|
|
|
1061
1115
|
return isPlainRecord2(entry) ? entry : null;
|
|
1062
1116
|
}
|
|
1063
1117
|
function readRawTaskConfig(configPath) {
|
|
1064
|
-
if (!
|
|
1118
|
+
if (!existsSync7(configPath)) {
|
|
1065
1119
|
return null;
|
|
1066
1120
|
}
|
|
1067
|
-
const parsed = JSON.parse(
|
|
1121
|
+
const parsed = JSON.parse(readFileSync6(configPath, "utf8"));
|
|
1068
1122
|
return isPlainRecord2(parsed) ? parsed : null;
|
|
1069
1123
|
}
|
|
1070
1124
|
function stripLegacyTaskConfigMetadata2(raw) {
|
|
@@ -1072,12 +1126,12 @@ function stripLegacyTaskConfigMetadata2(raw) {
|
|
|
1072
1126
|
return tasks;
|
|
1073
1127
|
}
|
|
1074
1128
|
function listFileBackedTasks(projectRoot, sourcePath) {
|
|
1075
|
-
const directory =
|
|
1076
|
-
if (!
|
|
1129
|
+
const directory = resolve9(projectRoot, sourcePath);
|
|
1130
|
+
if (!existsSync7(directory)) {
|
|
1077
1131
|
return [];
|
|
1078
1132
|
}
|
|
1079
1133
|
const tasks = [];
|
|
1080
|
-
for (const name of
|
|
1134
|
+
for (const name of readdirSync2(directory)) {
|
|
1081
1135
|
if (!FILE_TASK_PATTERN.test(name))
|
|
1082
1136
|
continue;
|
|
1083
1137
|
const inferredId = basename3(name).replace(FILE_TASK_PATTERN, "");
|
|
@@ -1088,11 +1142,11 @@ function listFileBackedTasks(projectRoot, sourcePath) {
|
|
|
1088
1142
|
return tasks;
|
|
1089
1143
|
}
|
|
1090
1144
|
function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
|
|
1091
|
-
const file = findFileBackedTaskFile(
|
|
1145
|
+
const file = findFileBackedTaskFile(resolve9(projectRoot, sourcePath), taskId);
|
|
1092
1146
|
if (!file) {
|
|
1093
1147
|
return null;
|
|
1094
1148
|
}
|
|
1095
|
-
const raw = JSON.parse(
|
|
1149
|
+
const raw = JSON.parse(readFileSync6(file, "utf8"));
|
|
1096
1150
|
if (!isPlainRecord2(raw)) {
|
|
1097
1151
|
return null;
|
|
1098
1152
|
}
|
|
@@ -1105,17 +1159,17 @@ function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
|
|
|
1105
1159
|
};
|
|
1106
1160
|
}
|
|
1107
1161
|
function findFileBackedTaskFile(directory, taskId) {
|
|
1108
|
-
if (!
|
|
1162
|
+
if (!existsSync7(directory)) {
|
|
1109
1163
|
return null;
|
|
1110
1164
|
}
|
|
1111
|
-
for (const name of
|
|
1165
|
+
for (const name of readdirSync2(directory)) {
|
|
1112
1166
|
if (!FILE_TASK_PATTERN.test(name))
|
|
1113
1167
|
continue;
|
|
1114
1168
|
const file = join2(directory, name);
|
|
1115
1169
|
try {
|
|
1116
1170
|
if (!statSync(file).isFile())
|
|
1117
1171
|
continue;
|
|
1118
|
-
const raw = JSON.parse(
|
|
1172
|
+
const raw = JSON.parse(readFileSync6(file, "utf8"));
|
|
1119
1173
|
const inferredId = basename3(file).replace(FILE_TASK_PATTERN, "");
|
|
1120
1174
|
const id = isPlainRecord2(raw) && typeof raw.id === "string" ? raw.id : inferredId;
|
|
1121
1175
|
if (id === taskId) {
|
|
@@ -1198,8 +1252,8 @@ function githubStatusFor(issue) {
|
|
|
1198
1252
|
return "open";
|
|
1199
1253
|
}
|
|
1200
1254
|
function selectedGitHubEnv() {
|
|
1201
|
-
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim()
|
|
1202
|
-
return { GH_TOKEN: token, GITHUB_TOKEN: token };
|
|
1255
|
+
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
|
|
1256
|
+
return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
|
|
1203
1257
|
}
|
|
1204
1258
|
function ghSpawnOptions() {
|
|
1205
1259
|
return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
|
|
@@ -1275,8 +1329,8 @@ async function readConfiguredTaskSourceTask(projectRoot, taskId) {
|
|
|
1275
1329
|
}
|
|
1276
1330
|
|
|
1277
1331
|
// packages/runtime/src/control-plane/native/task-state.ts
|
|
1278
|
-
import { existsSync as
|
|
1279
|
-
import { basename as basename6, resolve as
|
|
1332
|
+
import { existsSync as existsSync15, readFileSync as readFileSync10, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
1333
|
+
import { basename as basename6, resolve as resolve17 } from "path";
|
|
1280
1334
|
|
|
1281
1335
|
// packages/runtime/src/control-plane/state-sync/types.ts
|
|
1282
1336
|
var SUPPORTED_TASK_STATE_SCHEMA_VERSION = 1;
|
|
@@ -1384,30 +1438,30 @@ function readTaskStateMetadataEnvelope(raw) {
|
|
|
1384
1438
|
};
|
|
1385
1439
|
}
|
|
1386
1440
|
// packages/runtime/src/control-plane/state-sync/read.ts
|
|
1387
|
-
import { existsSync as
|
|
1388
|
-
import { resolve as
|
|
1441
|
+
import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
|
|
1442
|
+
import { resolve as resolve16 } from "path";
|
|
1389
1443
|
|
|
1390
1444
|
// packages/runtime/src/control-plane/native/git-native.ts
|
|
1391
|
-
import { chmodSync, copyFileSync, existsSync as
|
|
1445
|
+
import { chmodSync, copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
1392
1446
|
import { tmpdir as tmpdir3 } from "os";
|
|
1393
|
-
import { dirname as dirname5, isAbsolute, resolve as
|
|
1447
|
+
import { dirname as dirname5, isAbsolute, resolve as resolve10 } from "path";
|
|
1394
1448
|
import { createHash } from "crypto";
|
|
1395
1449
|
function isTextTreeCommitUpdate(update) {
|
|
1396
1450
|
return typeof update.content === "string";
|
|
1397
1451
|
}
|
|
1398
|
-
var sharedGitNativeOutputDir =
|
|
1399
|
-
var sharedGitNativeOutputPath =
|
|
1452
|
+
var sharedGitNativeOutputDir = resolve10(tmpdir3(), "rig-native");
|
|
1453
|
+
var sharedGitNativeOutputPath = resolve10(sharedGitNativeOutputDir, `rig-git-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
|
|
1400
1454
|
var trackerCommandUsageProbe = "usage: rig-git fetch-ref <repo-path> <remote> <branch>";
|
|
1401
1455
|
function temporaryGitBinaryOutputPath(outputPath) {
|
|
1402
1456
|
const suffix = process.platform === "win32" ? ".exe" : "";
|
|
1403
|
-
return
|
|
1457
|
+
return resolve10(dirname5(outputPath), `.rig-git-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}${suffix}`);
|
|
1404
1458
|
}
|
|
1405
1459
|
function publishGitBinary(tempOutputPath, outputPath) {
|
|
1406
1460
|
try {
|
|
1407
1461
|
renameSync(tempOutputPath, outputPath);
|
|
1408
1462
|
} catch (error) {
|
|
1409
|
-
if (process.platform === "win32" &&
|
|
1410
|
-
|
|
1463
|
+
if (process.platform === "win32" && existsSync8(outputPath)) {
|
|
1464
|
+
rmSync2(outputPath, { force: true });
|
|
1411
1465
|
renameSync(tempOutputPath, outputPath);
|
|
1412
1466
|
return;
|
|
1413
1467
|
}
|
|
@@ -1422,22 +1476,22 @@ function rigGitSourceCandidates() {
|
|
|
1422
1476
|
const cwd = process.cwd()?.trim() || "";
|
|
1423
1477
|
const projectRoot = process.env.PROJECT_RIG_ROOT?.trim() || "";
|
|
1424
1478
|
const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim() || "";
|
|
1425
|
-
const moduleRelativeSource =
|
|
1479
|
+
const moduleRelativeSource = resolve10(import.meta.dir, "../../../native/rig-git.zig");
|
|
1426
1480
|
return [...new Set([
|
|
1427
1481
|
process.env.RIG_NATIVE_GIT_SOURCE?.trim() || "",
|
|
1428
1482
|
moduleRelativeSource,
|
|
1429
|
-
projectRoot ?
|
|
1430
|
-
hostProjectRoot ?
|
|
1431
|
-
cwd ?
|
|
1432
|
-
execDir ?
|
|
1433
|
-
execDir ?
|
|
1483
|
+
projectRoot ? resolve10(projectRoot, "packages/runtime/native/rig-git.zig") : "",
|
|
1484
|
+
hostProjectRoot ? resolve10(hostProjectRoot, "packages/runtime/native/rig-git.zig") : "",
|
|
1485
|
+
cwd ? resolve10(cwd, "packages/runtime/native/rig-git.zig") : "",
|
|
1486
|
+
execDir ? resolve10(execDir, "..", "..", "packages/runtime/native/rig-git.zig") : "",
|
|
1487
|
+
execDir ? resolve10(execDir, "..", "native", "rig-git.zig") : ""
|
|
1434
1488
|
].filter(Boolean))];
|
|
1435
1489
|
}
|
|
1436
1490
|
function nativePackageBinaryCandidates(fromDir, fileName) {
|
|
1437
1491
|
const candidates = [];
|
|
1438
|
-
let cursor =
|
|
1492
|
+
let cursor = resolve10(fromDir);
|
|
1439
1493
|
for (let index = 0;index < 8; index += 1) {
|
|
1440
|
-
candidates.push(
|
|
1494
|
+
candidates.push(resolve10(cursor, "native", `${process.platform}-${process.arch}`, fileName), resolve10(cursor, "native", `${process.platform}-${process.arch}`, "bin", fileName), resolve10(cursor, "native", fileName), resolve10(cursor, "native", "bin", fileName));
|
|
1441
1495
|
const parent = dirname5(cursor);
|
|
1442
1496
|
if (parent === cursor)
|
|
1443
1497
|
break;
|
|
@@ -1452,15 +1506,15 @@ function rigGitBinaryCandidates() {
|
|
|
1452
1506
|
return [...new Set([
|
|
1453
1507
|
explicit,
|
|
1454
1508
|
...nativePackageBinaryCandidates(import.meta.dir, fileName),
|
|
1455
|
-
execDir ?
|
|
1456
|
-
execDir ?
|
|
1457
|
-
execDir ?
|
|
1509
|
+
execDir ? resolve10(execDir, fileName) : "",
|
|
1510
|
+
execDir ? resolve10(execDir, "..", fileName) : "",
|
|
1511
|
+
execDir ? resolve10(execDir, "..", "bin", fileName) : "",
|
|
1458
1512
|
sharedGitNativeOutputPath
|
|
1459
1513
|
].filter(Boolean))];
|
|
1460
1514
|
}
|
|
1461
1515
|
function resolveGitSourcePath() {
|
|
1462
1516
|
for (const candidate of rigGitSourceCandidates()) {
|
|
1463
|
-
if (candidate &&
|
|
1517
|
+
if (candidate && existsSync8(candidate)) {
|
|
1464
1518
|
return candidate;
|
|
1465
1519
|
}
|
|
1466
1520
|
}
|
|
@@ -1471,7 +1525,7 @@ function resolveGitBinaryPath() {
|
|
|
1471
1525
|
return null;
|
|
1472
1526
|
}
|
|
1473
1527
|
for (const candidate of rigGitBinaryCandidates()) {
|
|
1474
|
-
if (candidate &&
|
|
1528
|
+
if (candidate && existsSync8(candidate)) {
|
|
1475
1529
|
return candidate;
|
|
1476
1530
|
}
|
|
1477
1531
|
}
|
|
@@ -1501,18 +1555,18 @@ function nativeBuildManifestPath(outputPath) {
|
|
|
1501
1555
|
return `${outputPath}.build-manifest.json`;
|
|
1502
1556
|
}
|
|
1503
1557
|
function hasMatchingNativeBuildManifestSync(manifestPath, buildKey) {
|
|
1504
|
-
if (!
|
|
1558
|
+
if (!existsSync8(manifestPath)) {
|
|
1505
1559
|
return false;
|
|
1506
1560
|
}
|
|
1507
1561
|
try {
|
|
1508
|
-
const manifest = JSON.parse(
|
|
1562
|
+
const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
|
|
1509
1563
|
return manifest.version === 1 && manifest.buildKey === buildKey;
|
|
1510
1564
|
} catch {
|
|
1511
1565
|
return false;
|
|
1512
1566
|
}
|
|
1513
1567
|
}
|
|
1514
1568
|
function sha256FileSync(path) {
|
|
1515
|
-
return createHash("sha256").update(
|
|
1569
|
+
return createHash("sha256").update(readFileSync7(path)).digest("hex");
|
|
1516
1570
|
}
|
|
1517
1571
|
function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath()) {
|
|
1518
1572
|
if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
|
|
@@ -1530,7 +1584,7 @@ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath())
|
|
|
1530
1584
|
if (!zigBinary) {
|
|
1531
1585
|
throw new Error("zig is required to build native Rig git tools.");
|
|
1532
1586
|
}
|
|
1533
|
-
|
|
1587
|
+
mkdirSync4(dirname5(outputPath), { recursive: true });
|
|
1534
1588
|
const sourceDigest = sha256FileSync(sourcePath);
|
|
1535
1589
|
const buildKey = JSON.stringify({
|
|
1536
1590
|
version: 1,
|
|
@@ -1541,7 +1595,7 @@ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath())
|
|
|
1541
1595
|
sourceDigest
|
|
1542
1596
|
});
|
|
1543
1597
|
const manifestPath = nativeBuildManifestPath(outputPath);
|
|
1544
|
-
const needsBuild = !
|
|
1598
|
+
const needsBuild = !existsSync8(outputPath) || !hasMatchingNativeBuildManifestSync(manifestPath, buildKey) || !binarySupportsTrackerCommandsSync(outputPath);
|
|
1545
1599
|
if (!needsBuild) {
|
|
1546
1600
|
chmodSync(outputPath, 493);
|
|
1547
1601
|
return outputPath;
|
|
@@ -1559,7 +1613,7 @@ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath())
|
|
|
1559
1613
|
stdout: "pipe",
|
|
1560
1614
|
stderr: "pipe"
|
|
1561
1615
|
});
|
|
1562
|
-
if (build.exitCode !== 0 || !
|
|
1616
|
+
if (build.exitCode !== 0 || !existsSync8(tempOutputPath)) {
|
|
1563
1617
|
const stderr = build.stderr.toString().trim();
|
|
1564
1618
|
const stdout = build.stdout.toString().trim();
|
|
1565
1619
|
const details = [stderr, stdout].filter(Boolean).join(`
|
|
@@ -1567,17 +1621,17 @@ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath())
|
|
|
1567
1621
|
throw new Error(`Failed to build native Rig git tools: ${details || `zig exited with code ${build.exitCode}`}`);
|
|
1568
1622
|
}
|
|
1569
1623
|
chmodSync(tempOutputPath, 493);
|
|
1570
|
-
if (
|
|
1571
|
-
|
|
1624
|
+
if (existsSync8(outputPath) && hasMatchingNativeBuildManifestSync(manifestPath, buildKey)) {
|
|
1625
|
+
rmSync2(tempOutputPath, { force: true });
|
|
1572
1626
|
chmodSync(outputPath, 493);
|
|
1573
1627
|
return outputPath;
|
|
1574
1628
|
}
|
|
1575
1629
|
publishGitBinary(tempOutputPath, outputPath);
|
|
1576
1630
|
if (!binarySupportsTrackerCommandsSync(outputPath)) {
|
|
1577
|
-
|
|
1631
|
+
rmSync2(outputPath, { force: true });
|
|
1578
1632
|
throw new Error("Failed to build native Rig git tools: tracker command probe failed");
|
|
1579
1633
|
}
|
|
1580
|
-
|
|
1634
|
+
writeFileSync5(manifestPath, `${JSON.stringify({ version: 1, buildKey }, null, 2)}
|
|
1581
1635
|
`, "utf8");
|
|
1582
1636
|
return outputPath;
|
|
1583
1637
|
}
|
|
@@ -1599,7 +1653,7 @@ function runGitNative(command, args) {
|
|
|
1599
1653
|
}
|
|
1600
1654
|
} else {
|
|
1601
1655
|
const explicitBinaryPath = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
|
|
1602
|
-
binaryPath = explicitBinaryPath &&
|
|
1656
|
+
binaryPath = explicitBinaryPath && existsSync8(explicitBinaryPath) ? explicitBinaryPath : !explicitBinaryPath ? resolveGitBinaryPath() : null;
|
|
1603
1657
|
if (!binaryPath) {
|
|
1604
1658
|
try {
|
|
1605
1659
|
binaryPath = ensureRigGitBinaryPathSync(preferredGitBinaryOutputPath());
|
|
@@ -1696,25 +1750,25 @@ function nativeFetchRef(repoPath, remote, branch) {
|
|
|
1696
1750
|
return requireGitNativeString("fetch-ref", [repoPath, remote, branch]);
|
|
1697
1751
|
}
|
|
1698
1752
|
function nativeReadBlobAtRef(repoPath, ref, path) {
|
|
1699
|
-
const requestDir =
|
|
1700
|
-
|
|
1701
|
-
const outputPath =
|
|
1753
|
+
const requestDir = resolve10(sharedGitNativeOutputDir, "reads", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
|
|
1754
|
+
mkdirSync4(requestDir, { recursive: true });
|
|
1755
|
+
const outputPath = resolve10(requestDir, "blob.txt");
|
|
1702
1756
|
try {
|
|
1703
1757
|
requireGitNative("read-blob-at-ref", [repoPath, ref, path, outputPath]);
|
|
1704
|
-
return
|
|
1758
|
+
return readFileSync7(outputPath, "utf8");
|
|
1705
1759
|
} finally {
|
|
1706
|
-
|
|
1760
|
+
rmSync2(requestDir, { recursive: true, force: true });
|
|
1707
1761
|
}
|
|
1708
1762
|
}
|
|
1709
1763
|
function nativeReadBlobBytesAtRef(repoPath, ref, path) {
|
|
1710
|
-
const requestDir =
|
|
1711
|
-
|
|
1712
|
-
const outputPath =
|
|
1764
|
+
const requestDir = resolve10(sharedGitNativeOutputDir, "reads", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
|
|
1765
|
+
mkdirSync4(requestDir, { recursive: true });
|
|
1766
|
+
const outputPath = resolve10(requestDir, "blob.bin");
|
|
1713
1767
|
try {
|
|
1714
1768
|
requireGitNative("read-blob-at-ref", [repoPath, ref, path, outputPath]);
|
|
1715
|
-
return
|
|
1769
|
+
return readFileSync7(outputPath);
|
|
1716
1770
|
} finally {
|
|
1717
|
-
|
|
1771
|
+
rmSync2(requestDir, { recursive: true, force: true });
|
|
1718
1772
|
}
|
|
1719
1773
|
}
|
|
1720
1774
|
function serializeTreeCommitUpdates(updates) {
|
|
@@ -1733,16 +1787,16 @@ function buildTreeCommitUpdatesJson(updates) {
|
|
|
1733
1787
|
`;
|
|
1734
1788
|
}
|
|
1735
1789
|
function nativeWriteTreeCommit(repoPath, baseRef, updates, message) {
|
|
1736
|
-
const requestDir =
|
|
1737
|
-
|
|
1738
|
-
const messagePath =
|
|
1739
|
-
const updatesPath =
|
|
1790
|
+
const requestDir = resolve10(sharedGitNativeOutputDir, "requests", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
|
|
1791
|
+
mkdirSync4(requestDir, { recursive: true });
|
|
1792
|
+
const messagePath = resolve10(requestDir, "message.txt");
|
|
1793
|
+
const updatesPath = resolve10(requestDir, "updates.json");
|
|
1740
1794
|
try {
|
|
1741
|
-
|
|
1742
|
-
|
|
1795
|
+
writeFileSync5(messagePath, message, "utf8");
|
|
1796
|
+
writeFileSync5(updatesPath, buildTreeCommitUpdatesJson(updates), "utf8");
|
|
1743
1797
|
return requireGitNativeString("write-tree-commit", [repoPath, baseRef, messagePath, updatesPath]);
|
|
1744
1798
|
} finally {
|
|
1745
|
-
|
|
1799
|
+
rmSync2(requestDir, { recursive: true, force: true });
|
|
1746
1800
|
}
|
|
1747
1801
|
}
|
|
1748
1802
|
function nativePushRefWithLease(repoPath, localOid, remoteRef, expectedOldOid, remote = "origin") {
|
|
@@ -1757,35 +1811,35 @@ function nativePushRefWithLease(repoPath, localOid, remoteRef, expectedOldOid, r
|
|
|
1757
1811
|
|
|
1758
1812
|
// packages/runtime/src/control-plane/native/utils.ts
|
|
1759
1813
|
import { ptr as ptr2 } from "bun:ffi";
|
|
1760
|
-
import { existsSync as
|
|
1761
|
-
import { resolve as
|
|
1814
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
1815
|
+
import { resolve as resolve13 } from "path";
|
|
1762
1816
|
|
|
1763
1817
|
// packages/runtime/src/layout.ts
|
|
1764
|
-
import { existsSync as
|
|
1765
|
-
import { basename as basename4, dirname as dirname6, resolve as
|
|
1818
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1819
|
+
import { basename as basename4, dirname as dirname6, resolve as resolve11 } from "path";
|
|
1766
1820
|
var RIG_DEFINITION_DIRNAME = "rig";
|
|
1767
1821
|
var RIG_ARTIFACTS_DIRNAME = "artifacts";
|
|
1768
1822
|
function resolveMonorepoRoot(projectRoot) {
|
|
1769
|
-
const normalizedProjectRoot =
|
|
1823
|
+
const normalizedProjectRoot = resolve11(projectRoot);
|
|
1770
1824
|
const explicit = process.env.MONOREPO_ROOT?.trim();
|
|
1771
1825
|
if (explicit) {
|
|
1772
|
-
const explicitRoot =
|
|
1826
|
+
const explicitRoot = resolve11(explicit);
|
|
1773
1827
|
const explicitParent = dirname6(explicitRoot);
|
|
1774
1828
|
if (basename4(explicitParent) === ".worktrees") {
|
|
1775
1829
|
const owner = dirname6(explicitParent);
|
|
1776
|
-
const ownerHasGit =
|
|
1777
|
-
const ownerHasTaskConfig =
|
|
1778
|
-
const ownerHasRigConfig =
|
|
1830
|
+
const ownerHasGit = existsSync9(resolve11(owner, ".git"));
|
|
1831
|
+
const ownerHasTaskConfig = existsSync9(resolve11(owner, ".rig", "task-config.json"));
|
|
1832
|
+
const ownerHasRigConfig = existsSync9(resolve11(owner, "rig.config.ts"));
|
|
1779
1833
|
if (ownerHasGit && (ownerHasTaskConfig || ownerHasRigConfig)) {
|
|
1780
1834
|
return owner;
|
|
1781
1835
|
}
|
|
1782
1836
|
throw new Error(`MONOREPO_ROOT points to worktree ${explicitRoot}, but the owner checkout is incomplete at ${owner}.`);
|
|
1783
1837
|
}
|
|
1784
|
-
if (!
|
|
1838
|
+
if (!existsSync9(resolve11(explicitRoot, ".git"))) {
|
|
1785
1839
|
throw new Error(`MONOREPO_ROOT points to ${explicitRoot}, but no git checkout was found there.`);
|
|
1786
1840
|
}
|
|
1787
|
-
const hasTaskConfig =
|
|
1788
|
-
const hasRigConfig =
|
|
1841
|
+
const hasTaskConfig = existsSync9(resolve11(explicitRoot, ".rig", "task-config.json"));
|
|
1842
|
+
const hasRigConfig = existsSync9(resolve11(explicitRoot, "rig.config.ts"));
|
|
1789
1843
|
if (!hasTaskConfig && !hasRigConfig) {
|
|
1790
1844
|
throw new Error(`MONOREPO_ROOT points to ${explicitRoot}, but neither .rig/task-config.json nor rig.config.ts exists there.`);
|
|
1791
1845
|
}
|
|
@@ -1794,9 +1848,9 @@ function resolveMonorepoRoot(projectRoot) {
|
|
|
1794
1848
|
const projectParent = dirname6(normalizedProjectRoot);
|
|
1795
1849
|
if (basename4(projectParent) === ".worktrees") {
|
|
1796
1850
|
const worktreeOwner = dirname6(projectParent);
|
|
1797
|
-
const ownerHasGit =
|
|
1798
|
-
const ownerHasTaskConfig =
|
|
1799
|
-
const ownerHasRigConfig =
|
|
1851
|
+
const ownerHasGit = existsSync9(resolve11(worktreeOwner, ".git"));
|
|
1852
|
+
const ownerHasTaskConfig = existsSync9(resolve11(worktreeOwner, ".rig", "task-config.json"));
|
|
1853
|
+
const ownerHasRigConfig = existsSync9(resolve11(worktreeOwner, "rig.config.ts"));
|
|
1800
1854
|
if (ownerHasGit && (ownerHasTaskConfig || ownerHasRigConfig)) {
|
|
1801
1855
|
return worktreeOwner;
|
|
1802
1856
|
}
|
|
@@ -1804,28 +1858,28 @@ function resolveMonorepoRoot(projectRoot) {
|
|
|
1804
1858
|
return normalizedProjectRoot;
|
|
1805
1859
|
}
|
|
1806
1860
|
function resolveRuntimeWorkspaceLayout(workspaceDir) {
|
|
1807
|
-
const root =
|
|
1808
|
-
const rigRoot =
|
|
1809
|
-
const logsDir =
|
|
1810
|
-
const stateDir =
|
|
1811
|
-
const runtimeDir =
|
|
1812
|
-
const binDir =
|
|
1861
|
+
const root = resolve11(workspaceDir);
|
|
1862
|
+
const rigRoot = resolve11(root, ".rig");
|
|
1863
|
+
const logsDir = resolve11(rigRoot, "logs");
|
|
1864
|
+
const stateDir = resolve11(rigRoot, "state");
|
|
1865
|
+
const runtimeDir = resolve11(rigRoot, "runtime");
|
|
1866
|
+
const binDir = resolve11(rigRoot, "bin");
|
|
1813
1867
|
return {
|
|
1814
1868
|
workspaceDir: root,
|
|
1815
1869
|
rigRoot,
|
|
1816
1870
|
stateDir,
|
|
1817
1871
|
logsDir,
|
|
1818
|
-
artifactsRoot:
|
|
1872
|
+
artifactsRoot: resolve11(root, RIG_ARTIFACTS_DIRNAME),
|
|
1819
1873
|
runtimeDir,
|
|
1820
|
-
homeDir:
|
|
1821
|
-
tmpDir:
|
|
1822
|
-
cacheDir:
|
|
1823
|
-
sessionDir:
|
|
1874
|
+
homeDir: resolve11(rigRoot, "home"),
|
|
1875
|
+
tmpDir: resolve11(rigRoot, "tmp"),
|
|
1876
|
+
cacheDir: resolve11(rigRoot, "cache"),
|
|
1877
|
+
sessionDir: resolve11(rigRoot, "session"),
|
|
1824
1878
|
binDir,
|
|
1825
|
-
distDir:
|
|
1826
|
-
pluginBinDir:
|
|
1827
|
-
contextPath:
|
|
1828
|
-
controlPlaneEventsFile:
|
|
1879
|
+
distDir: resolve11(rigRoot, "dist"),
|
|
1880
|
+
pluginBinDir: resolve11(binDir, "plugins"),
|
|
1881
|
+
contextPath: resolve11(rigRoot, "runtime-context.json"),
|
|
1882
|
+
controlPlaneEventsFile: resolve11(logsDir, "control-plane.events.jsonl")
|
|
1829
1883
|
};
|
|
1830
1884
|
}
|
|
1831
1885
|
function resolveActiveRuntimeWorkspaceRoot(monorepoRoot) {
|
|
@@ -1833,14 +1887,14 @@ function resolveActiveRuntimeWorkspaceRoot(monorepoRoot) {
|
|
|
1833
1887
|
if (!explicit) {
|
|
1834
1888
|
throw new Error("No active runtime workspace. Set RIG_TASK_WORKSPACE or provision a task runtime first.");
|
|
1835
1889
|
}
|
|
1836
|
-
return
|
|
1890
|
+
return resolve11(explicit);
|
|
1837
1891
|
}
|
|
1838
1892
|
function resolveRigLayout(projectRoot) {
|
|
1839
1893
|
const monorepoRoot = resolveMonorepoRoot(projectRoot);
|
|
1840
|
-
const definitionRoot =
|
|
1894
|
+
const definitionRoot = resolve11(projectRoot, RIG_DEFINITION_DIRNAME);
|
|
1841
1895
|
const runtimeWorkspaceRoot = resolveActiveRuntimeWorkspaceRoot(monorepoRoot);
|
|
1842
1896
|
const runtimeLayout = resolveRuntimeWorkspaceLayout(runtimeWorkspaceRoot);
|
|
1843
|
-
const policyDir =
|
|
1897
|
+
const policyDir = resolve11(definitionRoot, "policy");
|
|
1844
1898
|
return {
|
|
1845
1899
|
projectRoot,
|
|
1846
1900
|
monorepoRoot,
|
|
@@ -1848,34 +1902,34 @@ function resolveRigLayout(projectRoot) {
|
|
|
1848
1902
|
runtimeWorkspaceRoot,
|
|
1849
1903
|
stateRoot: runtimeLayout.rigRoot,
|
|
1850
1904
|
artifactsRoot: runtimeLayout.artifactsRoot,
|
|
1851
|
-
configPath:
|
|
1852
|
-
taskConfigPath:
|
|
1905
|
+
configPath: resolve11(definitionRoot, "config.sh"),
|
|
1906
|
+
taskConfigPath: resolve11(runtimeWorkspaceRoot, ".rig", "task-config.json"),
|
|
1853
1907
|
policyDir,
|
|
1854
|
-
policyFile:
|
|
1855
|
-
pluginsDir:
|
|
1856
|
-
hooksDir:
|
|
1857
|
-
toolsDir:
|
|
1858
|
-
templatesDir:
|
|
1859
|
-
validationDir:
|
|
1908
|
+
policyFile: resolve11(policyDir, "policy.json"),
|
|
1909
|
+
pluginsDir: resolve11(definitionRoot, "plugins"),
|
|
1910
|
+
hooksDir: resolve11(definitionRoot, "hooks"),
|
|
1911
|
+
toolsDir: resolve11(definitionRoot, "tools"),
|
|
1912
|
+
templatesDir: resolve11(definitionRoot, "templates"),
|
|
1913
|
+
validationDir: resolve11(definitionRoot, "validation"),
|
|
1860
1914
|
stateDir: runtimeLayout.stateDir,
|
|
1861
1915
|
logsDir: runtimeLayout.logsDir,
|
|
1862
|
-
notificationsDir:
|
|
1916
|
+
notificationsDir: resolve11(definitionRoot, "notifications"),
|
|
1863
1917
|
runtimeDir: runtimeLayout.runtimeDir,
|
|
1864
1918
|
distDir: runtimeLayout.distDir,
|
|
1865
1919
|
binDir: runtimeLayout.binDir,
|
|
1866
1920
|
pluginBinDir: runtimeLayout.pluginBinDir,
|
|
1867
|
-
keybindingsPath:
|
|
1921
|
+
keybindingsPath: resolve11(definitionRoot, "keybindings.json"),
|
|
1868
1922
|
controlPlaneEventsFile: runtimeLayout.controlPlaneEventsFile
|
|
1869
1923
|
};
|
|
1870
1924
|
}
|
|
1871
1925
|
|
|
1872
1926
|
// packages/runtime/src/control-plane/native/runtime-native.ts
|
|
1873
1927
|
import { dlopen, ptr, suffix, toBuffer } from "bun:ffi";
|
|
1874
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
1928
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync5, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync2 } from "fs";
|
|
1875
1929
|
import { tmpdir as tmpdir4 } from "os";
|
|
1876
|
-
import { dirname as dirname7, resolve as
|
|
1877
|
-
var sharedNativeRuntimeOutputDir =
|
|
1878
|
-
var sharedNativeRuntimeOutputPath =
|
|
1930
|
+
import { dirname as dirname7, resolve as resolve12 } from "path";
|
|
1931
|
+
var sharedNativeRuntimeOutputDir = resolve12(tmpdir4(), "rig-native");
|
|
1932
|
+
var sharedNativeRuntimeOutputPath = resolve12(sharedNativeRuntimeOutputDir, `runtime-native-${process.platform}-${process.arch}.${suffix}`);
|
|
1879
1933
|
var colocatedNativeRuntimeFileName = `runtime-native.${suffix}`;
|
|
1880
1934
|
var nativeRuntimeLibrary = await loadNativeRuntimeLibrary();
|
|
1881
1935
|
function requireNativeRuntimeLibrary(feature) {
|
|
@@ -1888,14 +1942,14 @@ async function ensureNativeRuntimeLibraryPath(outputPath = sharedNativeRuntimeOu
|
|
|
1888
1942
|
if (await buildNativeRuntimeLibrary(outputPath, options)) {
|
|
1889
1943
|
return outputPath;
|
|
1890
1944
|
}
|
|
1891
|
-
return !options.force &&
|
|
1945
|
+
return !options.force && existsSync10(outputPath) ? outputPath : null;
|
|
1892
1946
|
}
|
|
1893
1947
|
async function loadNativeRuntimeLibrary() {
|
|
1894
1948
|
if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
|
|
1895
1949
|
return null;
|
|
1896
1950
|
}
|
|
1897
1951
|
for (const candidate of nativeRuntimeLibraryCandidates()) {
|
|
1898
|
-
if (!candidate || !
|
|
1952
|
+
if (!candidate || !existsSync10(candidate)) {
|
|
1899
1953
|
continue;
|
|
1900
1954
|
}
|
|
1901
1955
|
const loaded = tryDlopenNativeRuntimeLibrary(candidate);
|
|
@@ -1911,10 +1965,10 @@ async function loadNativeRuntimeLibrary() {
|
|
|
1911
1965
|
}
|
|
1912
1966
|
function nativePackageLibraryCandidates(fromDir, names) {
|
|
1913
1967
|
const candidates = [];
|
|
1914
|
-
let cursor =
|
|
1968
|
+
let cursor = resolve12(fromDir);
|
|
1915
1969
|
for (let index = 0;index < 8; index += 1) {
|
|
1916
1970
|
for (const name of names) {
|
|
1917
|
-
candidates.push(
|
|
1971
|
+
candidates.push(resolve12(cursor, "native", `${process.platform}-${process.arch}`, name), resolve12(cursor, "native", `${process.platform}-${process.arch}`, "lib", name), resolve12(cursor, "native", name), resolve12(cursor, "native", "lib", name));
|
|
1918
1972
|
}
|
|
1919
1973
|
const parent = dirname7(cursor);
|
|
1920
1974
|
if (parent === cursor)
|
|
@@ -1930,22 +1984,22 @@ function nativeRuntimeLibraryCandidates() {
|
|
|
1930
1984
|
return [...new Set([
|
|
1931
1985
|
explicit,
|
|
1932
1986
|
...nativePackageLibraryCandidates(import.meta.dir, [colocatedNativeRuntimeFileName, platformSpecific]),
|
|
1933
|
-
execDir ?
|
|
1934
|
-
execDir ?
|
|
1935
|
-
execDir ?
|
|
1936
|
-
execDir ?
|
|
1937
|
-
execDir ?
|
|
1938
|
-
execDir ?
|
|
1987
|
+
execDir ? resolve12(execDir, colocatedNativeRuntimeFileName) : "",
|
|
1988
|
+
execDir ? resolve12(execDir, platformSpecific) : "",
|
|
1989
|
+
execDir ? resolve12(execDir, "..", colocatedNativeRuntimeFileName) : "",
|
|
1990
|
+
execDir ? resolve12(execDir, "..", platformSpecific) : "",
|
|
1991
|
+
execDir ? resolve12(execDir, "lib", colocatedNativeRuntimeFileName) : "",
|
|
1992
|
+
execDir ? resolve12(execDir, "..", "lib", colocatedNativeRuntimeFileName) : "",
|
|
1939
1993
|
sharedNativeRuntimeOutputPath
|
|
1940
1994
|
].filter(Boolean))];
|
|
1941
1995
|
}
|
|
1942
1996
|
function resolveNativeRuntimeSourcePath() {
|
|
1943
1997
|
const explicit = process.env.RIG_NATIVE_RUNTIME_SOURCE?.trim();
|
|
1944
|
-
if (explicit &&
|
|
1998
|
+
if (explicit && existsSync10(explicit)) {
|
|
1945
1999
|
return explicit;
|
|
1946
2000
|
}
|
|
1947
|
-
const bundled =
|
|
1948
|
-
return
|
|
2001
|
+
const bundled = resolve12(import.meta.dir, "../../../native/snapshot.zig");
|
|
2002
|
+
return existsSync10(bundled) ? bundled : null;
|
|
1949
2003
|
}
|
|
1950
2004
|
async function buildNativeRuntimeLibrary(outputPath, options = {}) {
|
|
1951
2005
|
if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
|
|
@@ -1958,8 +2012,8 @@ async function buildNativeRuntimeLibrary(outputPath, options = {}) {
|
|
|
1958
2012
|
}
|
|
1959
2013
|
const tempOutputPath = `${outputPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
1960
2014
|
try {
|
|
1961
|
-
|
|
1962
|
-
const needsBuild = options.force === true || !
|
|
2015
|
+
mkdirSync5(dirname7(outputPath), { recursive: true });
|
|
2016
|
+
const needsBuild = options.force === true || !existsSync10(outputPath) || statSync2(sourcePath).mtimeMs > statSync2(outputPath).mtimeMs;
|
|
1963
2017
|
if (!needsBuild) {
|
|
1964
2018
|
return true;
|
|
1965
2019
|
}
|
|
@@ -1977,14 +2031,14 @@ async function buildNativeRuntimeLibrary(outputPath, options = {}) {
|
|
|
1977
2031
|
stderr: "pipe"
|
|
1978
2032
|
});
|
|
1979
2033
|
const exitCode = await build.exited;
|
|
1980
|
-
if (exitCode !== 0 || !
|
|
1981
|
-
|
|
2034
|
+
if (exitCode !== 0 || !existsSync10(tempOutputPath)) {
|
|
2035
|
+
rmSync3(tempOutputPath, { force: true });
|
|
1982
2036
|
return false;
|
|
1983
2037
|
}
|
|
1984
2038
|
renameSync2(tempOutputPath, outputPath);
|
|
1985
2039
|
return true;
|
|
1986
2040
|
} catch {
|
|
1987
|
-
|
|
2041
|
+
rmSync3(tempOutputPath, { force: true });
|
|
1988
2042
|
return false;
|
|
1989
2043
|
}
|
|
1990
2044
|
}
|
|
@@ -2108,11 +2162,11 @@ async function runCaptureAsync(command, cwd, env, timeoutMs) {
|
|
|
2108
2162
|
return { exitCode, stdout, stderr };
|
|
2109
2163
|
}
|
|
2110
2164
|
function readJsonFile(path, fallback) {
|
|
2111
|
-
if (!
|
|
2165
|
+
if (!existsSync11(path)) {
|
|
2112
2166
|
return fallback;
|
|
2113
2167
|
}
|
|
2114
2168
|
try {
|
|
2115
|
-
return JSON.parse(
|
|
2169
|
+
return JSON.parse(readFileSync8(path, "utf-8"));
|
|
2116
2170
|
} catch {
|
|
2117
2171
|
return fallback;
|
|
2118
2172
|
}
|
|
@@ -2126,31 +2180,31 @@ function unique(values) {
|
|
|
2126
2180
|
function resolveHarnessPaths(projectRoot) {
|
|
2127
2181
|
const hasRuntimeWorkspace = Boolean(process.env.RIG_TASK_WORKSPACE?.trim());
|
|
2128
2182
|
const monorepoRoot = resolveMonorepoRoot2(projectRoot);
|
|
2129
|
-
const harnessRoot =
|
|
2130
|
-
const stateRoot =
|
|
2183
|
+
const harnessRoot = resolve13(projectRoot, "rig");
|
|
2184
|
+
const stateRoot = resolve13(projectRoot, ".rig");
|
|
2131
2185
|
const layout = hasRuntimeWorkspace ? resolveRigLayout(projectRoot) : null;
|
|
2132
|
-
const stateDir = layout?.stateDir ??
|
|
2133
|
-
const logsDir = layout?.logsDir ??
|
|
2134
|
-
const artifactsDir = layout?.artifactsRoot ??
|
|
2135
|
-
const taskConfigPath = layout?.taskConfigPath ??
|
|
2136
|
-
const binDir = layout?.binDir ??
|
|
2186
|
+
const stateDir = layout?.stateDir ?? resolve13(stateRoot, "state");
|
|
2187
|
+
const logsDir = layout?.logsDir ?? resolve13(stateRoot, "logs");
|
|
2188
|
+
const artifactsDir = layout?.artifactsRoot ?? resolve13(monorepoRoot, "artifacts");
|
|
2189
|
+
const taskConfigPath = layout?.taskConfigPath ?? resolve13(monorepoRoot, ".rig", "task-config.json");
|
|
2190
|
+
const binDir = layout?.binDir ?? resolve13(stateRoot, "bin");
|
|
2137
2191
|
return {
|
|
2138
2192
|
harnessRoot,
|
|
2139
2193
|
stateDir: process.env.RIG_STATE_DIR || stateDir,
|
|
2140
2194
|
artifactsDir,
|
|
2141
2195
|
logsDir: process.env.RIG_LOGS_DIR || logsDir,
|
|
2142
2196
|
binDir,
|
|
2143
|
-
hooksDir:
|
|
2144
|
-
validationDir:
|
|
2197
|
+
hooksDir: resolve13(harnessRoot, "hooks"),
|
|
2198
|
+
validationDir: resolve13(harnessRoot, "validation"),
|
|
2145
2199
|
taskConfigPath,
|
|
2146
|
-
sessionPath: process.env.RIG_SESSION_FILE ||
|
|
2200
|
+
sessionPath: process.env.RIG_SESSION_FILE || resolve13(stateRoot, "session", "session.json"),
|
|
2147
2201
|
monorepoRoot,
|
|
2148
|
-
tsApiTestsDir: process.env.TS_API_TESTS_DIR ||
|
|
2149
|
-
taskRepoCommitsPath:
|
|
2150
|
-
baseRepoPinsPath:
|
|
2151
|
-
failedApproachesPath:
|
|
2152
|
-
agentProfilePath:
|
|
2153
|
-
reviewProfilePath:
|
|
2202
|
+
tsApiTestsDir: process.env.TS_API_TESTS_DIR || resolve13(monorepoRoot, "TSAPITests"),
|
|
2203
|
+
taskRepoCommitsPath: resolve13(stateDir, "task-repo-commits.json"),
|
|
2204
|
+
baseRepoPinsPath: resolve13(stateDir, "base-repo-pins.json"),
|
|
2205
|
+
failedApproachesPath: resolve13(stateDir, "failed_approaches.md"),
|
|
2206
|
+
agentProfilePath: resolve13(stateDir, "agent-profile.json"),
|
|
2207
|
+
reviewProfilePath: resolve13(stateDir, "review-profile.json")
|
|
2154
2208
|
};
|
|
2155
2209
|
}
|
|
2156
2210
|
function normalizeRelativeScopePath(inputPath) {
|
|
@@ -2243,34 +2297,34 @@ function createNativeScopeMatcher() {
|
|
|
2243
2297
|
}
|
|
2244
2298
|
|
|
2245
2299
|
// packages/runtime/src/control-plane/state-sync/repo.ts
|
|
2246
|
-
import { existsSync as
|
|
2247
|
-
import { resolve as
|
|
2300
|
+
import { existsSync as existsSync13 } from "fs";
|
|
2301
|
+
import { resolve as resolve15 } from "path";
|
|
2248
2302
|
|
|
2249
2303
|
// packages/runtime/src/control-plane/repos/layout.ts
|
|
2250
|
-
import { existsSync as
|
|
2251
|
-
import { basename as basename5, dirname as dirname8, join as join3, resolve as
|
|
2304
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2305
|
+
import { basename as basename5, dirname as dirname8, join as join3, resolve as resolve14 } from "path";
|
|
2252
2306
|
function resolveRepoStateDir(projectRoot) {
|
|
2253
|
-
const normalizedProjectRoot =
|
|
2307
|
+
const normalizedProjectRoot = resolve14(projectRoot);
|
|
2254
2308
|
const projectParent = dirname8(normalizedProjectRoot);
|
|
2255
2309
|
if (basename5(projectParent) === ".worktrees") {
|
|
2256
2310
|
const ownerRoot = dirname8(projectParent);
|
|
2257
|
-
const ownerHasRepoMarkers =
|
|
2311
|
+
const ownerHasRepoMarkers = existsSync12(resolve14(ownerRoot, ".git")) || existsSync12(resolve14(ownerRoot, ".rig", "state"));
|
|
2258
2312
|
if (ownerHasRepoMarkers) {
|
|
2259
|
-
return
|
|
2313
|
+
return resolve14(ownerRoot, ".rig", "state");
|
|
2260
2314
|
}
|
|
2261
2315
|
}
|
|
2262
|
-
return
|
|
2316
|
+
return resolve14(projectRoot, ".rig", "state");
|
|
2263
2317
|
}
|
|
2264
2318
|
function resolveManagedRepoLayout(projectRoot, repoId) {
|
|
2265
|
-
const normalizedProjectRoot =
|
|
2319
|
+
const normalizedProjectRoot = resolve14(projectRoot);
|
|
2266
2320
|
const entry = getManagedRepoEntry(repoId);
|
|
2267
2321
|
const stateDir = resolveRepoStateDir(normalizedProjectRoot);
|
|
2268
2322
|
const metadataRelativePath = join3("repos", entry.id);
|
|
2269
|
-
const metadataRoot =
|
|
2323
|
+
const metadataRoot = resolve14(stateDir, metadataRelativePath);
|
|
2270
2324
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
2271
|
-
const runsInsideTaskWorktree = runtimeWorkspace &&
|
|
2325
|
+
const runsInsideTaskWorktree = runtimeWorkspace && resolve14(runtimeWorkspace) === normalizedProjectRoot || basename5(dirname8(normalizedProjectRoot)) === ".worktrees";
|
|
2272
2326
|
const isPrimaryManagedRepo = listManagedRepoEntries()[0]?.id === repoId;
|
|
2273
|
-
const checkoutRoot = isPrimaryManagedRepo && runsInsideTaskWorktree ? resolveMonorepoRoot(normalizedProjectRoot) : entry.checkoutEnvVar && process.env[entry.checkoutEnvVar]?.trim() ?
|
|
2327
|
+
const checkoutRoot = isPrimaryManagedRepo && runsInsideTaskWorktree ? resolveMonorepoRoot(normalizedProjectRoot) : entry.checkoutEnvVar && process.env[entry.checkoutEnvVar]?.trim() ? resolve14(process.env[entry.checkoutEnvVar].trim()) : resolve14(normalizedProjectRoot, entry.alias);
|
|
2274
2328
|
return {
|
|
2275
2329
|
projectRoot: normalizedProjectRoot,
|
|
2276
2330
|
repoId: entry.id,
|
|
@@ -2278,12 +2332,12 @@ function resolveManagedRepoLayout(projectRoot, repoId) {
|
|
|
2278
2332
|
defaultBranch: entry.defaultBranch,
|
|
2279
2333
|
remoteUrl: entry.remoteEnvVar && process.env[entry.remoteEnvVar]?.trim() ? process.env[entry.remoteEnvVar].trim() : entry.defaultRemoteUrl,
|
|
2280
2334
|
checkoutRoot,
|
|
2281
|
-
worktreesRoot:
|
|
2335
|
+
worktreesRoot: resolve14(checkoutRoot, ".worktrees"),
|
|
2282
2336
|
stateDir,
|
|
2283
2337
|
metadataRoot,
|
|
2284
2338
|
metadataRelativePath,
|
|
2285
|
-
mirrorRoot:
|
|
2286
|
-
mirrorStatePath:
|
|
2339
|
+
mirrorRoot: resolve14(metadataRoot, "mirror.git"),
|
|
2340
|
+
mirrorStatePath: resolve14(metadataRoot, "mirror-state.json"),
|
|
2287
2341
|
mirrorStateRelativePath: join3(metadataRelativePath, "mirror-state.json")
|
|
2288
2342
|
};
|
|
2289
2343
|
}
|
|
@@ -2305,7 +2359,7 @@ function resolveTrackerRepoPath(projectRoot) {
|
|
|
2305
2359
|
const monorepoRoot = resolveMonorepoRoot2(projectRoot);
|
|
2306
2360
|
try {
|
|
2307
2361
|
const layout = resolveMonorepoRepoLayout(projectRoot);
|
|
2308
|
-
if (
|
|
2362
|
+
if (existsSync13(resolve15(layout.mirrorRoot, "HEAD"))) {
|
|
2309
2363
|
return layout.mirrorRoot;
|
|
2310
2364
|
}
|
|
2311
2365
|
} catch {}
|
|
@@ -2316,8 +2370,8 @@ function resolveTrackerRepoPath(projectRoot) {
|
|
|
2316
2370
|
var DEFAULT_READ_DEPS = {
|
|
2317
2371
|
fetchRef: nativeFetchRef,
|
|
2318
2372
|
readBlobAtRef: nativeReadBlobAtRef,
|
|
2319
|
-
exists:
|
|
2320
|
-
readFile: (path) =>
|
|
2373
|
+
exists: existsSync14,
|
|
2374
|
+
readFile: (path) => readFileSync9(path, "utf8")
|
|
2321
2375
|
};
|
|
2322
2376
|
function parseIssueStatus(rawStatus) {
|
|
2323
2377
|
const normalized = normalizeTaskLifecycleStatus(rawStatus);
|
|
@@ -2398,12 +2452,12 @@ function shouldPreferLocalTrackerState(options) {
|
|
|
2398
2452
|
if (runtimeContextPath) {
|
|
2399
2453
|
return true;
|
|
2400
2454
|
}
|
|
2401
|
-
return
|
|
2455
|
+
return existsSync14(resolve16(runtimeWorkspace, ".rig", "runtime-context.json"));
|
|
2402
2456
|
}
|
|
2403
2457
|
function readLocalTrackerState(projectRoot, deps) {
|
|
2404
2458
|
const monorepoRoot = resolveMonorepoRoot2(projectRoot);
|
|
2405
|
-
const issuesPath =
|
|
2406
|
-
const taskStatePath =
|
|
2459
|
+
const issuesPath = resolve16(monorepoRoot, ".beads", "issues.jsonl");
|
|
2460
|
+
const taskStatePath = resolve16(monorepoRoot, ".beads", "task-state.json");
|
|
2407
2461
|
return projectSyncedTrackerSnapshot({
|
|
2408
2462
|
source: "local",
|
|
2409
2463
|
issuesBaseOid: null,
|
|
@@ -2465,7 +2519,7 @@ function readValidationDescriptions(projectRoot) {
|
|
|
2465
2519
|
return readValidationDescriptionMap(raw);
|
|
2466
2520
|
}
|
|
2467
2521
|
function readSourceValidationDescriptions(projectRoot) {
|
|
2468
|
-
const rootRaw = readJsonFile(
|
|
2522
|
+
const rootRaw = readJsonFile(resolve17(projectRoot, "rig", "task-config.json"), {});
|
|
2469
2523
|
const sourcePath = findSourceTaskConfigPath(projectRoot);
|
|
2470
2524
|
const sourceRaw = sourcePath ? readJsonFile(sourcePath, {}) : {};
|
|
2471
2525
|
const rootDescriptions = readValidationDescriptionMap(rootRaw);
|
|
@@ -2541,15 +2595,15 @@ function readValidationDescriptionsFromMeta(meta) {
|
|
|
2541
2595
|
return meta.validation_descriptions;
|
|
2542
2596
|
}
|
|
2543
2597
|
function readLocalSourceTaskStateEnvelope(projectRoot) {
|
|
2544
|
-
const taskStatePath =
|
|
2598
|
+
const taskStatePath = resolve17(resolveMonorepoRoot2(projectRoot), ".beads", "task-state.json");
|
|
2545
2599
|
return readTaskStateMetadataEnvelope(readJsonFile(taskStatePath, {}));
|
|
2546
2600
|
}
|
|
2547
2601
|
function readLocalSourceTaskLifecycleStatus(projectRoot, taskId) {
|
|
2548
|
-
const issuesPath =
|
|
2549
|
-
if (!
|
|
2602
|
+
const issuesPath = resolve17(resolveMonorepoRoot2(projectRoot), ".beads", "issues.jsonl");
|
|
2603
|
+
if (!existsSync15(issuesPath)) {
|
|
2550
2604
|
return null;
|
|
2551
2605
|
}
|
|
2552
|
-
for (const line of
|
|
2606
|
+
for (const line of readFileSync10(issuesPath, "utf8").split(/\r?\n/)) {
|
|
2553
2607
|
const trimmed = line.trim();
|
|
2554
2608
|
if (!trimmed) {
|
|
2555
2609
|
continue;
|
|
@@ -2590,25 +2644,25 @@ function lookupTask(projectRoot, input) {
|
|
|
2590
2644
|
function artifactDirForId(projectRoot, id) {
|
|
2591
2645
|
const workspaceDir = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
2592
2646
|
if (workspaceDir) {
|
|
2593
|
-
const worktreeArtifacts =
|
|
2594
|
-
if (
|
|
2647
|
+
const worktreeArtifacts = resolve17(workspaceDir, "artifacts", id);
|
|
2648
|
+
if (existsSync15(worktreeArtifacts) || existsSync15(resolve17(workspaceDir, "artifacts"))) {
|
|
2595
2649
|
return worktreeArtifacts;
|
|
2596
2650
|
}
|
|
2597
2651
|
}
|
|
2598
2652
|
try {
|
|
2599
2653
|
const paths = resolveHarnessPaths(projectRoot);
|
|
2600
|
-
return
|
|
2654
|
+
return resolve17(paths.artifactsDir, id);
|
|
2601
2655
|
} catch {
|
|
2602
|
-
return
|
|
2656
|
+
return resolve17(resolveMonorepoRoot2(projectRoot), "artifacts", id);
|
|
2603
2657
|
}
|
|
2604
2658
|
}
|
|
2605
2659
|
function resolveTaskConfigPath(projectRoot) {
|
|
2606
2660
|
const paths = resolveHarnessPaths(projectRoot);
|
|
2607
|
-
if (
|
|
2661
|
+
if (existsSync15(paths.taskConfigPath)) {
|
|
2608
2662
|
return paths.taskConfigPath;
|
|
2609
2663
|
}
|
|
2610
2664
|
for (const candidate of sourceTaskConfigCandidates(projectRoot)) {
|
|
2611
|
-
if (
|
|
2665
|
+
if (existsSync15(candidate)) {
|
|
2612
2666
|
return candidate;
|
|
2613
2667
|
}
|
|
2614
2668
|
}
|
|
@@ -2616,7 +2670,7 @@ function resolveTaskConfigPath(projectRoot) {
|
|
|
2616
2670
|
}
|
|
2617
2671
|
function findSourceTaskConfigPath(projectRoot) {
|
|
2618
2672
|
for (const candidate of sourceTaskConfigCandidates(projectRoot)) {
|
|
2619
|
-
if (
|
|
2673
|
+
if (existsSync15(candidate)) {
|
|
2620
2674
|
return candidate;
|
|
2621
2675
|
}
|
|
2622
2676
|
}
|
|
@@ -2629,7 +2683,7 @@ function readAndSyncSourceTaskConfig(projectRoot) {
|
|
|
2629
2683
|
const synced = synchronizeTaskConfigWithTracker(projectRoot, raw);
|
|
2630
2684
|
if (sourcePath && synced.updated) {
|
|
2631
2685
|
try {
|
|
2632
|
-
|
|
2686
|
+
writeFileSync6(sourcePath, `${JSON.stringify(synced.config, null, 2)}
|
|
2633
2687
|
`, "utf-8");
|
|
2634
2688
|
} catch {}
|
|
2635
2689
|
}
|
|
@@ -2681,12 +2735,12 @@ function shouldRefreshAutoSyncedTaskConfigEntry(entry) {
|
|
|
2681
2735
|
return !candidate.role;
|
|
2682
2736
|
}
|
|
2683
2737
|
function readSourceIssueRecords(projectRoot) {
|
|
2684
|
-
const issuesPath =
|
|
2685
|
-
if (!
|
|
2738
|
+
const issuesPath = resolve17(resolveMonorepoRoot2(projectRoot), ".beads", "issues.jsonl");
|
|
2739
|
+
if (!existsSync15(issuesPath)) {
|
|
2686
2740
|
return [];
|
|
2687
2741
|
}
|
|
2688
2742
|
const records = [];
|
|
2689
|
-
for (const line of
|
|
2743
|
+
for (const line of readFileSync10(issuesPath, "utf-8").split(/\r?\n/)) {
|
|
2690
2744
|
const trimmed = line.trim();
|
|
2691
2745
|
if (!trimmed) {
|
|
2692
2746
|
continue;
|
|
@@ -2742,19 +2796,19 @@ function readConfiguredFileTaskConfig(projectRoot) {
|
|
|
2742
2796
|
if (!sourcePath) {
|
|
2743
2797
|
return {};
|
|
2744
2798
|
}
|
|
2745
|
-
const directory =
|
|
2746
|
-
if (!
|
|
2799
|
+
const directory = resolve17(projectRoot, sourcePath);
|
|
2800
|
+
if (!existsSync15(directory)) {
|
|
2747
2801
|
return {};
|
|
2748
2802
|
}
|
|
2749
2803
|
const config = {};
|
|
2750
|
-
for (const name of
|
|
2804
|
+
for (const name of readdirSync3(directory)) {
|
|
2751
2805
|
if (!FILE_TASK_PATTERN2.test(name))
|
|
2752
2806
|
continue;
|
|
2753
|
-
const file =
|
|
2807
|
+
const file = resolve17(directory, name);
|
|
2754
2808
|
try {
|
|
2755
2809
|
if (!statSync3(file).isFile())
|
|
2756
2810
|
continue;
|
|
2757
|
-
const raw = JSON.parse(
|
|
2811
|
+
const raw = JSON.parse(readFileSync10(file, "utf8"));
|
|
2758
2812
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
2759
2813
|
continue;
|
|
2760
2814
|
const record = raw;
|
|
@@ -2796,10 +2850,10 @@ function firstStringList2(...candidates) {
|
|
|
2796
2850
|
return [];
|
|
2797
2851
|
}
|
|
2798
2852
|
function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
2799
|
-
const jsonPath =
|
|
2800
|
-
if (
|
|
2853
|
+
const jsonPath = resolve17(projectRoot, "rig.config.json");
|
|
2854
|
+
if (existsSync15(jsonPath)) {
|
|
2801
2855
|
try {
|
|
2802
|
-
const parsed = JSON.parse(
|
|
2856
|
+
const parsed = JSON.parse(readFileSync10(jsonPath, "utf8"));
|
|
2803
2857
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2804
2858
|
const taskSource = parsed.taskSource;
|
|
2805
2859
|
if (taskSource && typeof taskSource === "object" && !Array.isArray(taskSource)) {
|
|
@@ -2811,12 +2865,12 @@ function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
|
2811
2865
|
return null;
|
|
2812
2866
|
}
|
|
2813
2867
|
}
|
|
2814
|
-
const tsPath =
|
|
2815
|
-
if (!
|
|
2868
|
+
const tsPath = resolve17(projectRoot, "rig.config.ts");
|
|
2869
|
+
if (!existsSync15(tsPath)) {
|
|
2816
2870
|
return null;
|
|
2817
2871
|
}
|
|
2818
2872
|
try {
|
|
2819
|
-
const source =
|
|
2873
|
+
const source = readFileSync10(tsPath, "utf8");
|
|
2820
2874
|
const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
|
|
2821
2875
|
const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
|
|
2822
2876
|
if (kind !== "files") {
|
|
@@ -2830,23 +2884,23 @@ function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
|
2830
2884
|
function sourceTaskConfigCandidates(projectRoot) {
|
|
2831
2885
|
const runtimeContext = loadRuntimeContextFromEnv();
|
|
2832
2886
|
return [
|
|
2833
|
-
runtimeContext?.monorepoMainRoot ?
|
|
2834
|
-
process.env.MONOREPO_MAIN_ROOT?.trim() ?
|
|
2835
|
-
|
|
2887
|
+
runtimeContext?.monorepoMainRoot ? resolve17(runtimeContext.monorepoMainRoot, ".rig", "task-config.json") : "",
|
|
2888
|
+
process.env.MONOREPO_MAIN_ROOT?.trim() ? resolve17(process.env.MONOREPO_MAIN_ROOT.trim(), ".rig", "task-config.json") : "",
|
|
2889
|
+
resolve17(resolveMonorepoRoot2(projectRoot), ".rig", "task-config.json")
|
|
2836
2890
|
].filter(Boolean);
|
|
2837
2891
|
}
|
|
2838
2892
|
|
|
2839
2893
|
// packages/runtime/src/control-plane/native/validator.ts
|
|
2840
|
-
import { existsSync as
|
|
2841
|
-
import { resolve as
|
|
2894
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
2895
|
+
import { resolve as resolve22 } from "path";
|
|
2842
2896
|
|
|
2843
2897
|
// packages/runtime/src/control-plane/native/validator-binaries.ts
|
|
2844
|
-
import { existsSync as
|
|
2845
|
-
import { dirname as dirname10, resolve as
|
|
2898
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync7, rmSync as rmSync5, statSync as statSync4 } from "fs";
|
|
2899
|
+
import { dirname as dirname10, resolve as resolve21 } from "path";
|
|
2846
2900
|
|
|
2847
2901
|
// packages/runtime/src/binary-run.ts
|
|
2848
|
-
import { chmodSync as chmodSync2, cpSync, existsSync as
|
|
2849
|
-
import { basename as basename7, dirname as dirname9, resolve as
|
|
2902
|
+
import { chmodSync as chmodSync2, cpSync, existsSync as existsSync16, mkdirSync as mkdirSync6, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
2903
|
+
import { basename as basename7, dirname as dirname9, resolve as resolve18 } from "path";
|
|
2850
2904
|
import { fileURLToPath } from "url";
|
|
2851
2905
|
import { drainMicrotasks, gcAndSweep } from "bun:jsc";
|
|
2852
2906
|
var runtimeBinaryBuildQueue = Promise.resolve();
|
|
@@ -2872,9 +2926,9 @@ async function buildRuntimeBinary(options) {
|
|
|
2872
2926
|
});
|
|
2873
2927
|
}
|
|
2874
2928
|
async function buildRuntimeBinaryInProcess(options, manifest) {
|
|
2875
|
-
const tempBuildDir =
|
|
2876
|
-
const tempOutputPath =
|
|
2877
|
-
|
|
2929
|
+
const tempBuildDir = resolve18(dirname9(options.outputPath), `.bun-build-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
2930
|
+
const tempOutputPath = resolve18(tempBuildDir, basename7(options.outputPath));
|
|
2931
|
+
mkdirSync6(tempBuildDir, { recursive: true });
|
|
2878
2932
|
await withTemporaryEnv({
|
|
2879
2933
|
...options.env,
|
|
2880
2934
|
...options.define ? { RIG_BUILD_CONFIG_JSON: JSON.stringify(options.define) } : {}
|
|
@@ -2899,7 +2953,7 @@ async function buildRuntimeBinaryInProcess(options, manifest) {
|
|
|
2899
2953
|
`);
|
|
2900
2954
|
throw new Error(`Failed to build ${options.entrypoint}: ${details || "Bun.build() returned errors"}`);
|
|
2901
2955
|
}
|
|
2902
|
-
if (!
|
|
2956
|
+
if (!existsSync16(tempOutputPath)) {
|
|
2903
2957
|
const emitted = buildResult.outputs.map((output) => output.path).join(", ") || "(none)";
|
|
2904
2958
|
throw new Error(`Failed to build ${options.entrypoint}: Bun.build() did not emit ${tempOutputPath}. Emitted: ${emitted}`);
|
|
2905
2959
|
}
|
|
@@ -2914,7 +2968,7 @@ async function buildRuntimeBinaryInProcess(options, manifest) {
|
|
|
2914
2968
|
});
|
|
2915
2969
|
}
|
|
2916
2970
|
})).finally(() => {
|
|
2917
|
-
|
|
2971
|
+
rmSync4(tempBuildDir, { recursive: true, force: true });
|
|
2918
2972
|
});
|
|
2919
2973
|
}
|
|
2920
2974
|
function runBestEffortBuildGc() {
|
|
@@ -2931,8 +2985,8 @@ function runtimeBinaryCacheManifestPath(outputPath) {
|
|
|
2931
2985
|
function resolveRuntimeBinaryBuildOptions(options) {
|
|
2932
2986
|
return {
|
|
2933
2987
|
...options,
|
|
2934
|
-
entrypoint:
|
|
2935
|
-
outputPath:
|
|
2988
|
+
entrypoint: resolve18(options.cwd, options.sourcePath),
|
|
2989
|
+
outputPath: resolve18(options.outputPath)
|
|
2936
2990
|
};
|
|
2937
2991
|
}
|
|
2938
2992
|
function shouldUseRuntimeBinaryBuildWorker() {
|
|
@@ -2946,7 +3000,7 @@ function shouldUseRuntimeBinaryBuildWorker() {
|
|
|
2946
3000
|
}
|
|
2947
3001
|
async function buildRuntimeBinaryViaWorker(options) {
|
|
2948
3002
|
const workerSourcePath = resolveRuntimeBinaryBuildWorkerSourcePath(options);
|
|
2949
|
-
if (!workerSourcePath || !
|
|
3003
|
+
if (!workerSourcePath || !existsSync16(workerSourcePath)) {
|
|
2950
3004
|
await buildRuntimeBinaryInProcess(options, {
|
|
2951
3005
|
manifestPath: runtimeBinaryCacheManifestPath(options.outputPath),
|
|
2952
3006
|
buildKey: createRuntimeBinaryBuildKey({
|
|
@@ -2977,13 +3031,13 @@ async function buildRuntimeBinaryViaWorker(options) {
|
|
|
2977
3031
|
new Response(build.stdout).text(),
|
|
2978
3032
|
new Response(build.stderr).text()
|
|
2979
3033
|
]);
|
|
2980
|
-
|
|
3034
|
+
rmSync4(payloadPath, { force: true });
|
|
2981
3035
|
if (exitCode !== 0) {
|
|
2982
3036
|
throw new Error(`Failed to build ${options.entrypoint}: ${(stderr || stdout || `worker exited ${exitCode}`).trim()}`);
|
|
2983
3037
|
}
|
|
2984
3038
|
}
|
|
2985
3039
|
function createRuntimeBinaryBuildWorkerPayloadPath(outputPath) {
|
|
2986
|
-
return
|
|
3040
|
+
return resolve18(dirname9(outputPath), `.bun-build-worker-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
|
2987
3041
|
}
|
|
2988
3042
|
function resolveRuntimeBinaryBuildWorkerSourcePath(options) {
|
|
2989
3043
|
const envRoots = [
|
|
@@ -2992,13 +3046,13 @@ function resolveRuntimeBinaryBuildWorkerSourcePath(options) {
|
|
|
2992
3046
|
process.env.PROJECT_RIG_ROOT?.trim()
|
|
2993
3047
|
].filter(Boolean);
|
|
2994
3048
|
for (const root of envRoots) {
|
|
2995
|
-
const candidate =
|
|
2996
|
-
if (
|
|
3049
|
+
const candidate = resolve18(root, "packages/runtime/src/binary-build-worker.ts");
|
|
3050
|
+
if (existsSync16(candidate)) {
|
|
2997
3051
|
return candidate;
|
|
2998
3052
|
}
|
|
2999
3053
|
}
|
|
3000
|
-
const localCandidate =
|
|
3001
|
-
return
|
|
3054
|
+
const localCandidate = resolve18(import.meta.dir, "binary-build-worker.ts");
|
|
3055
|
+
return existsSync16(localCandidate) ? localCandidate : null;
|
|
3002
3056
|
}
|
|
3003
3057
|
function resolveRuntimeBinaryBuildWorkerInvocation() {
|
|
3004
3058
|
const bunPath = Bun.which("bun");
|
|
@@ -3034,7 +3088,7 @@ function createRuntimeBinaryBuildKey(input) {
|
|
|
3034
3088
|
});
|
|
3035
3089
|
}
|
|
3036
3090
|
async function isRuntimeBinaryBuildFresh(input) {
|
|
3037
|
-
if (!
|
|
3091
|
+
if (!existsSync16(input.outputPath) || !existsSync16(input.manifestPath)) {
|
|
3038
3092
|
return false;
|
|
3039
3093
|
}
|
|
3040
3094
|
let manifest = null;
|
|
@@ -3047,7 +3101,7 @@ async function isRuntimeBinaryBuildFresh(input) {
|
|
|
3047
3101
|
return false;
|
|
3048
3102
|
}
|
|
3049
3103
|
for (const [filePath, expectedDigest] of Object.entries(manifest.inputs || {})) {
|
|
3050
|
-
if (!
|
|
3104
|
+
if (!existsSync16(filePath)) {
|
|
3051
3105
|
return false;
|
|
3052
3106
|
}
|
|
3053
3107
|
if (await sha256File(filePath) !== expectedDigest) {
|
|
@@ -3060,7 +3114,7 @@ async function writeRuntimeBinaryCacheManifest(input) {
|
|
|
3060
3114
|
const inputs = {};
|
|
3061
3115
|
for (const inputPath of Object.keys(input.metafile?.inputs || {}).sort()) {
|
|
3062
3116
|
const normalized = normalizeBuildInputPath(input.cwd, inputPath);
|
|
3063
|
-
if (!normalized || !
|
|
3117
|
+
if (!normalized || !existsSync16(normalized)) {
|
|
3064
3118
|
continue;
|
|
3065
3119
|
}
|
|
3066
3120
|
inputs[normalized] = await sha256File(normalized);
|
|
@@ -3083,7 +3137,7 @@ function normalizeBuildInputPath(cwd, inputPath) {
|
|
|
3083
3137
|
if (inputPath.startsWith("<")) {
|
|
3084
3138
|
return null;
|
|
3085
3139
|
}
|
|
3086
|
-
return
|
|
3140
|
+
return resolve18(cwd, inputPath);
|
|
3087
3141
|
}
|
|
3088
3142
|
async function sha256File(path) {
|
|
3089
3143
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
@@ -3099,8 +3153,8 @@ function sortRecord(value) {
|
|
|
3099
3153
|
async function runSerializedRuntimeBinaryBuild(action) {
|
|
3100
3154
|
const previous = runtimeBinaryBuildQueue;
|
|
3101
3155
|
let release;
|
|
3102
|
-
runtimeBinaryBuildQueue = new Promise((
|
|
3103
|
-
release =
|
|
3156
|
+
runtimeBinaryBuildQueue = new Promise((resolve19) => {
|
|
3157
|
+
release = resolve19;
|
|
3104
3158
|
});
|
|
3105
3159
|
await previous;
|
|
3106
3160
|
try {
|
|
@@ -3145,11 +3199,11 @@ async function withTemporaryCwd(cwd, action) {
|
|
|
3145
3199
|
}
|
|
3146
3200
|
|
|
3147
3201
|
// packages/runtime/src/control-plane/runtime/provisioning-env.ts
|
|
3148
|
-
import { delimiter, resolve as
|
|
3202
|
+
import { delimiter, resolve as resolve20 } from "path";
|
|
3149
3203
|
|
|
3150
3204
|
// packages/runtime/src/control-plane/runtime/runtime-paths.ts
|
|
3151
|
-
import { existsSync as
|
|
3152
|
-
import { resolve as
|
|
3205
|
+
import { existsSync as existsSync17, readdirSync as readdirSync4, realpathSync } from "fs";
|
|
3206
|
+
import { resolve as resolve19 } from "path";
|
|
3153
3207
|
|
|
3154
3208
|
// packages/runtime/src/control-plane/runtime/sandbox/utils.ts
|
|
3155
3209
|
function uniq(values) {
|
|
@@ -3167,7 +3221,7 @@ function resolveBunBinaryPath() {
|
|
|
3167
3221
|
}
|
|
3168
3222
|
const home = process.env.HOME?.trim();
|
|
3169
3223
|
const fallbackCandidates = [
|
|
3170
|
-
home ?
|
|
3224
|
+
home ? resolve19(home, ".bun/bin/bun") : "",
|
|
3171
3225
|
"/opt/homebrew/bin/bun",
|
|
3172
3226
|
"/usr/local/bin/bun",
|
|
3173
3227
|
"/usr/bin/bun"
|
|
@@ -3195,8 +3249,8 @@ function resolveClaudeBinaryPath() {
|
|
|
3195
3249
|
}
|
|
3196
3250
|
const home = process.env.HOME?.trim();
|
|
3197
3251
|
const fallbackCandidates = [
|
|
3198
|
-
home ?
|
|
3199
|
-
home ?
|
|
3252
|
+
home ? resolve19(home, ".local/bin/claude") : "",
|
|
3253
|
+
home ? resolve19(home, ".local/share/claude/local/claude") : "",
|
|
3200
3254
|
"/opt/homebrew/bin/claude",
|
|
3201
3255
|
"/usr/local/bin/claude",
|
|
3202
3256
|
"/usr/bin/claude"
|
|
@@ -3210,51 +3264,51 @@ function resolveClaudeBinaryPath() {
|
|
|
3210
3264
|
throw new Error("claude not found in PATH");
|
|
3211
3265
|
}
|
|
3212
3266
|
function resolveBunInstallDir(bunBinaryPath = resolveBunBinaryPath()) {
|
|
3213
|
-
return
|
|
3267
|
+
return resolve19(bunBinaryPath, "../..");
|
|
3214
3268
|
}
|
|
3215
3269
|
function resolveClaudeInstallDir() {
|
|
3216
3270
|
const realPath = resolveClaudeBinaryPath();
|
|
3217
|
-
return
|
|
3271
|
+
return resolve19(realPath, "..");
|
|
3218
3272
|
}
|
|
3219
3273
|
function resolveNodeInstallDir() {
|
|
3220
3274
|
const preferredNode = resolvePreferredNodeBinary();
|
|
3221
3275
|
if (!preferredNode)
|
|
3222
3276
|
return null;
|
|
3223
3277
|
const explicitNode = process.env.RIG_NODE_BIN?.trim();
|
|
3224
|
-
if (explicitNode &&
|
|
3225
|
-
return preferredNode.endsWith("/bin/node") ?
|
|
3278
|
+
if (explicitNode && resolve19(explicitNode) === resolve19(preferredNode)) {
|
|
3279
|
+
return preferredNode.endsWith("/bin/node") ? resolve19(preferredNode, "../..") : resolve19(preferredNode, "..");
|
|
3226
3280
|
}
|
|
3227
3281
|
try {
|
|
3228
3282
|
const realPath = realpathSync(preferredNode);
|
|
3229
3283
|
if (realPath.endsWith("/bin/node")) {
|
|
3230
|
-
return
|
|
3284
|
+
return resolve19(realPath, "../..");
|
|
3231
3285
|
}
|
|
3232
|
-
return
|
|
3286
|
+
return resolve19(realPath, "..");
|
|
3233
3287
|
} catch {
|
|
3234
|
-
return
|
|
3288
|
+
return resolve19(preferredNode, "..");
|
|
3235
3289
|
}
|
|
3236
3290
|
}
|
|
3237
3291
|
function resolvePreferredNodeBinary() {
|
|
3238
3292
|
const candidates = [];
|
|
3239
3293
|
const envNode = process.env.RIG_NODE_BIN?.trim();
|
|
3240
3294
|
if (envNode) {
|
|
3241
|
-
const explicit =
|
|
3242
|
-
if (
|
|
3295
|
+
const explicit = resolve19(envNode);
|
|
3296
|
+
if (existsSync17(explicit)) {
|
|
3243
3297
|
return explicit;
|
|
3244
3298
|
}
|
|
3245
3299
|
}
|
|
3246
3300
|
const nvmBin = process.env.NVM_BIN?.trim();
|
|
3247
3301
|
if (nvmBin) {
|
|
3248
|
-
candidates.push(
|
|
3302
|
+
candidates.push(resolve19(nvmBin, "node"));
|
|
3249
3303
|
}
|
|
3250
3304
|
const home = process.env.HOME?.trim();
|
|
3251
3305
|
if (home) {
|
|
3252
|
-
const nvmVersionsDir =
|
|
3253
|
-
if (
|
|
3306
|
+
const nvmVersionsDir = resolve19(home, ".nvm/versions/node");
|
|
3307
|
+
if (existsSync17(nvmVersionsDir)) {
|
|
3254
3308
|
try {
|
|
3255
|
-
const versionDirs =
|
|
3309
|
+
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/, "")));
|
|
3256
3310
|
for (const versionDir of versionDirs) {
|
|
3257
|
-
candidates.push(
|
|
3311
|
+
candidates.push(resolve19(nvmVersionsDir, versionDir, "bin/node"));
|
|
3258
3312
|
}
|
|
3259
3313
|
} catch {}
|
|
3260
3314
|
}
|
|
@@ -3263,8 +3317,8 @@ function resolvePreferredNodeBinary() {
|
|
|
3263
3317
|
if (whichNode) {
|
|
3264
3318
|
candidates.push(whichNode);
|
|
3265
3319
|
}
|
|
3266
|
-
const deduped = uniq(candidates.map((candidate) =>
|
|
3267
|
-
const existing = deduped.filter((candidate) =>
|
|
3320
|
+
const deduped = uniq(candidates.map((candidate) => resolve19(candidate)));
|
|
3321
|
+
const existing = deduped.filter((candidate) => existsSync17(candidate));
|
|
3268
3322
|
if (existing.length === 0) {
|
|
3269
3323
|
return null;
|
|
3270
3324
|
}
|
|
@@ -3278,7 +3332,7 @@ function resolvePreferredNodeBinary() {
|
|
|
3278
3332
|
return existing[0] ?? null;
|
|
3279
3333
|
}
|
|
3280
3334
|
function inferNodeMajor(nodeBinaryPath) {
|
|
3281
|
-
const normalized =
|
|
3335
|
+
const normalized = resolve19(nodeBinaryPath).replace(/\\/g, "/");
|
|
3282
3336
|
const match = normalized.match(/(?:^|\/)(?:node-)?v?(\d+)\.\d+\.\d+(?:\/|$)/);
|
|
3283
3337
|
if (!match) {
|
|
3284
3338
|
return null;
|
|
@@ -3290,8 +3344,8 @@ function normalizeExecutablePath(candidate) {
|
|
|
3290
3344
|
if (!candidate) {
|
|
3291
3345
|
return "";
|
|
3292
3346
|
}
|
|
3293
|
-
const normalized =
|
|
3294
|
-
if (!
|
|
3347
|
+
const normalized = resolve19(candidate);
|
|
3348
|
+
if (!existsSync17(normalized)) {
|
|
3295
3349
|
return "";
|
|
3296
3350
|
}
|
|
3297
3351
|
try {
|
|
@@ -3301,7 +3355,7 @@ function normalizeExecutablePath(candidate) {
|
|
|
3301
3355
|
}
|
|
3302
3356
|
}
|
|
3303
3357
|
function looksLikeRuntimeGateway(candidate) {
|
|
3304
|
-
const normalized =
|
|
3358
|
+
const normalized = resolve19(candidate).replace(/\\/g, "/");
|
|
3305
3359
|
return normalized.includes("/.rig/bin/") || normalized.endsWith("/rig-shell") || normalized.endsWith("/rig-agent");
|
|
3306
3360
|
}
|
|
3307
3361
|
|
|
@@ -3322,7 +3376,7 @@ function runtimeProvisioningEnv(baseEnv = process.env) {
|
|
|
3322
3376
|
try {
|
|
3323
3377
|
return resolveClaudeInstallDir();
|
|
3324
3378
|
} catch {
|
|
3325
|
-
return
|
|
3379
|
+
return resolve20(claudeBinary, "..");
|
|
3326
3380
|
}
|
|
3327
3381
|
})() : "";
|
|
3328
3382
|
const nodeDir = resolveNodeInstallDir();
|
|
@@ -3332,8 +3386,8 @@ function runtimeProvisioningEnv(baseEnv = process.env) {
|
|
|
3332
3386
|
`${bunDir}/bin`,
|
|
3333
3387
|
claudeDir,
|
|
3334
3388
|
nodeDir ? `${nodeDir}/bin` : "",
|
|
3335
|
-
realHome ?
|
|
3336
|
-
realHome ?
|
|
3389
|
+
realHome ? resolve20(realHome, ".local/bin") : "",
|
|
3390
|
+
realHome ? resolve20(realHome, ".cargo/bin") : "",
|
|
3337
3391
|
...inheritedPath,
|
|
3338
3392
|
"/usr/local/bin",
|
|
3339
3393
|
"/usr/local/sbin",
|
|
@@ -3364,9 +3418,9 @@ function runtimeProvisioningEnv(baseEnv = process.env) {
|
|
|
3364
3418
|
// packages/runtime/src/control-plane/native/validator-binaries.ts
|
|
3365
3419
|
function resolveValidatorBinaryPath(projectRoot, binaryName, runtimeContext) {
|
|
3366
3420
|
if (runtimeContext) {
|
|
3367
|
-
return
|
|
3421
|
+
return resolve21(runtimeContext.binDir, "validators", binaryName);
|
|
3368
3422
|
}
|
|
3369
|
-
return
|
|
3423
|
+
return resolve21(resolveHarnessPaths(projectRoot).binDir, "validators", binaryName);
|
|
3370
3424
|
}
|
|
3371
3425
|
async function ensureValidatorBinary(projectRoot, checkId, runtimeContext) {
|
|
3372
3426
|
const match = checkId.match(/^([a-z][\w-]*):([a-z][\w-]*)$/);
|
|
@@ -3381,19 +3435,19 @@ async function ensureValidatorBinary(projectRoot, checkId, runtimeContext) {
|
|
|
3381
3435
|
const binaryName = `${category}-${check}`;
|
|
3382
3436
|
const binaryPath = resolveValidatorBinaryPath(projectRoot, binaryName, runtimeContext);
|
|
3383
3437
|
const hostProjectRoot = runtimeContext?.hostProjectRoot?.trim() || projectRoot;
|
|
3384
|
-
const sourcePath =
|
|
3385
|
-
if (!
|
|
3438
|
+
const sourcePath = resolve21(hostProjectRoot, "packages/runtime/src/control-plane/validators", category, `${check}.ts`);
|
|
3439
|
+
if (!existsSync18(sourcePath)) {
|
|
3386
3440
|
return null;
|
|
3387
3441
|
}
|
|
3388
3442
|
const sourceMtime = statSync4(sourcePath).mtimeMs;
|
|
3389
|
-
const binaryExists =
|
|
3443
|
+
const binaryExists = existsSync18(binaryPath);
|
|
3390
3444
|
const binaryMtime = binaryExists ? statSync4(binaryPath).mtimeMs : 0;
|
|
3391
3445
|
if (!binaryExists || sourceMtime > binaryMtime) {
|
|
3392
3446
|
if (binaryExists) {
|
|
3393
|
-
|
|
3394
|
-
|
|
3447
|
+
rmSync5(binaryPath, { force: true });
|
|
3448
|
+
rmSync5(`${binaryPath}.build-manifest.json`, { force: true });
|
|
3395
3449
|
}
|
|
3396
|
-
|
|
3450
|
+
mkdirSync7(dirname10(binaryPath), { recursive: true });
|
|
3397
3451
|
await buildRuntimeBinary({
|
|
3398
3452
|
sourcePath: `packages/runtime/src/control-plane/validators/${category}/${check}.ts`,
|
|
3399
3453
|
outputPath: binaryPath,
|
|
@@ -3402,7 +3456,7 @@ async function ensureValidatorBinary(projectRoot, checkId, runtimeContext) {
|
|
|
3402
3456
|
env: runtimeProvisioningEnv()
|
|
3403
3457
|
});
|
|
3404
3458
|
}
|
|
3405
|
-
return
|
|
3459
|
+
return existsSync18(binaryPath) ? binaryPath : null;
|
|
3406
3460
|
}
|
|
3407
3461
|
|
|
3408
3462
|
// packages/runtime/src/control-plane/native/validator.ts
|
|
@@ -3439,20 +3493,20 @@ async function readTaskSourceValidation(projectRoot, taskId) {
|
|
|
3439
3493
|
function resolveValidationPaths(projectRoot, taskId, runtimeContext) {
|
|
3440
3494
|
if (runtimeContext) {
|
|
3441
3495
|
return {
|
|
3442
|
-
taskLogDir:
|
|
3443
|
-
artifactDir:
|
|
3496
|
+
taskLogDir: resolve22(runtimeContext.logsDir, taskId),
|
|
3497
|
+
artifactDir: resolve22(runtimeContext.workspaceDir, "artifacts", taskId)
|
|
3444
3498
|
};
|
|
3445
3499
|
}
|
|
3446
3500
|
const paths = resolveHarnessPaths(projectRoot);
|
|
3447
3501
|
return {
|
|
3448
|
-
taskLogDir:
|
|
3449
|
-
artifactDir:
|
|
3502
|
+
taskLogDir: resolve22(paths.logsDir, taskId),
|
|
3503
|
+
artifactDir: resolve22(paths.artifactsDir, taskId)
|
|
3450
3504
|
};
|
|
3451
3505
|
}
|
|
3452
3506
|
async function runValidatorBinary(projectRoot, taskId, checkId, runtimeContext) {
|
|
3453
3507
|
const binaryName = checkId.replace(":", "-");
|
|
3454
3508
|
const binaryPath = await ensureValidatorBinary(projectRoot, checkId, runtimeContext) ?? resolveValidatorBinaryPath(projectRoot, binaryName, runtimeContext);
|
|
3455
|
-
if (!
|
|
3509
|
+
if (!existsSync19(binaryPath)) {
|
|
3456
3510
|
return {
|
|
3457
3511
|
result: {
|
|
3458
3512
|
id: checkId,
|
|
@@ -3463,7 +3517,7 @@ async function runValidatorBinary(projectRoot, taskId, checkId, runtimeContext)
|
|
|
3463
3517
|
};
|
|
3464
3518
|
}
|
|
3465
3519
|
const validatorCwd = runtimeContext?.workspaceDir || resolveMonorepoRoot(projectRoot);
|
|
3466
|
-
const runtimeShellPath = runtimeContext ?
|
|
3520
|
+
const runtimeShellPath = runtimeContext ? resolve22(runtimeContext.binDir, "rig-shell") : "";
|
|
3467
3521
|
const monorepoMainRoot = runtimeContext?.monorepoMainRoot || process.env.MONOREPO_MAIN_ROOT?.trim() || resolveMonorepoRoot(projectRoot);
|
|
3468
3522
|
const validatorEnv = {
|
|
3469
3523
|
PROJECT_RIG_ROOT: runtimeContext?.hostProjectRoot || projectRoot,
|
|
@@ -3478,7 +3532,7 @@ async function runValidatorBinary(projectRoot, taskId, checkId, runtimeContext)
|
|
|
3478
3532
|
validatorEnv.RIG_LOGS_DIR = runtimeContext.logsDir;
|
|
3479
3533
|
validatorEnv.RIG_RUNTIME_BIN_DIR = runtimeContext.binDir;
|
|
3480
3534
|
}
|
|
3481
|
-
const { exitCode, stdout, stderr } = await runCaptureAsync(runtimeShellPath &&
|
|
3535
|
+
const { exitCode, stdout, stderr } = await runCaptureAsync(runtimeShellPath && existsSync19(runtimeShellPath) ? [runtimeShellPath, "run-binary", binaryPath] : [binaryPath], validatorCwd, validatorEnv);
|
|
3482
3536
|
try {
|
|
3483
3537
|
const result = JSON.parse(stdout.trim());
|
|
3484
3538
|
return { result, exitCode };
|
|
@@ -3518,8 +3572,8 @@ async function validateTask(projectRoot, taskId, runtimeContext, registry, optio
|
|
|
3518
3572
|
const configuredValidation = stringArray(taskConfig[taskId]?.validation);
|
|
3519
3573
|
const commands = resolvedContext?.validation?.length ? resolvedContext.validation : configuredValidation.length > 0 ? configuredValidation : sourceValidation.validation;
|
|
3520
3574
|
const { taskLogDir, artifactDir } = resolveValidationPaths(projectRoot, taskId, resolvedContext);
|
|
3521
|
-
|
|
3522
|
-
|
|
3575
|
+
mkdirSync8(taskLogDir, { recursive: true });
|
|
3576
|
+
mkdirSync8(artifactDir, { recursive: true });
|
|
3523
3577
|
if (commands.length === 0) {
|
|
3524
3578
|
const skipped = {
|
|
3525
3579
|
status: "skipped",
|
|
@@ -3528,7 +3582,7 @@ async function validateTask(projectRoot, taskId, runtimeContext, registry, optio
|
|
|
3528
3582
|
failed: 0,
|
|
3529
3583
|
categories: []
|
|
3530
3584
|
};
|
|
3531
|
-
|
|
3585
|
+
writeFileSync8(resolve22(artifactDir, "validation-summary.json"), `${JSON.stringify(skipped, null, 2)}
|
|
3532
3586
|
`, "utf-8");
|
|
3533
3587
|
return skipped;
|
|
3534
3588
|
}
|
|
@@ -3563,18 +3617,18 @@ async function validateTask(projectRoot, taskId, runtimeContext, registry, optio
|
|
|
3563
3617
|
exit_code: 2,
|
|
3564
3618
|
duration_seconds: 0
|
|
3565
3619
|
});
|
|
3566
|
-
const logFile2 =
|
|
3567
|
-
|
|
3568
|
-
|
|
3620
|
+
const logFile2 = resolve22(taskLogDir, `invalid-entry-validation.log`);
|
|
3621
|
+
mkdirSync8(taskLogDir, { recursive: true });
|
|
3622
|
+
writeFileSync8(logFile2, `=== ${nowIso()} :: ${cmd} ===
|
|
3569
3623
|
Invalid validation entry: not a check-ID. All entries must use format "category:check-name".
|
|
3570
3624
|
`, "utf-8");
|
|
3571
3625
|
continue;
|
|
3572
3626
|
}
|
|
3573
3627
|
const { result, exitCode } = await dispatchValidator(cmd, effectiveRegistry, validatorCtx, (id) => runValidatorBinary(projectRoot, taskId, id, resolvedContext));
|
|
3574
3628
|
const durationSeconds = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
3575
|
-
const logFile =
|
|
3576
|
-
|
|
3577
|
-
|
|
3629
|
+
const logFile = resolve22(taskLogDir, `${cmd.replace(":", "-")}-validation.log`);
|
|
3630
|
+
mkdirSync8(taskLogDir, { recursive: true });
|
|
3631
|
+
writeFileSync8(logFile, `=== ${nowIso()} :: ${cmd} ===
|
|
3578
3632
|
${JSON.stringify(result, null, 2)}
|
|
3579
3633
|
`, "utf-8");
|
|
3580
3634
|
if (result.passed) {
|
|
@@ -3589,32 +3643,1277 @@ ${JSON.stringify(result, null, 2)}
|
|
|
3589
3643
|
}
|
|
3590
3644
|
}
|
|
3591
3645
|
}
|
|
3592
|
-
const summary = {
|
|
3593
|
-
status: failed === 0 ? "pass" : "fail",
|
|
3594
|
-
total: commands.length,
|
|
3595
|
-
passed,
|
|
3596
|
-
failed,
|
|
3597
|
-
categories
|
|
3646
|
+
const summary = {
|
|
3647
|
+
status: failed === 0 ? "pass" : "fail",
|
|
3648
|
+
total: commands.length,
|
|
3649
|
+
passed,
|
|
3650
|
+
failed,
|
|
3651
|
+
categories
|
|
3652
|
+
};
|
|
3653
|
+
mkdirSync8(artifactDir, { recursive: true });
|
|
3654
|
+
writeFileSync8(resolve22(artifactDir, "validation-summary.json"), `${JSON.stringify(summary, null, 2)}
|
|
3655
|
+
`, "utf-8");
|
|
3656
|
+
return summary;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3660
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
3661
|
+
import { resolve as resolve23 } from "path";
|
|
3662
|
+
|
|
3663
|
+
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
3664
|
+
function parseJsonObject(value) {
|
|
3665
|
+
if (!value?.trim())
|
|
3666
|
+
return { value: {}, error: "empty JSON output" };
|
|
3667
|
+
try {
|
|
3668
|
+
const parsed = JSON.parse(value);
|
|
3669
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
|
|
3670
|
+
} catch (error) {
|
|
3671
|
+
return { value: {}, error: error instanceof Error ? error.message : String(error) };
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
function flattenPaginatedArray(value) {
|
|
3675
|
+
if (!Array.isArray(value))
|
|
3676
|
+
return null;
|
|
3677
|
+
if (value.every((entry) => Array.isArray(entry))) {
|
|
3678
|
+
return value.flatMap((entry) => entry);
|
|
3679
|
+
}
|
|
3680
|
+
return value;
|
|
3681
|
+
}
|
|
3682
|
+
function parseConcatenatedJsonValues(value) {
|
|
3683
|
+
const text = value.trim();
|
|
3684
|
+
const docs = [];
|
|
3685
|
+
let start = null;
|
|
3686
|
+
let depth = 0;
|
|
3687
|
+
let inString = false;
|
|
3688
|
+
let escape = false;
|
|
3689
|
+
for (let index = 0;index < text.length; index += 1) {
|
|
3690
|
+
const char = text[index];
|
|
3691
|
+
if (start === null) {
|
|
3692
|
+
if (/\s/.test(char))
|
|
3693
|
+
continue;
|
|
3694
|
+
start = index;
|
|
3695
|
+
}
|
|
3696
|
+
if (inString) {
|
|
3697
|
+
if (escape) {
|
|
3698
|
+
escape = false;
|
|
3699
|
+
} else if (char === "\\") {
|
|
3700
|
+
escape = true;
|
|
3701
|
+
} else if (char === '"') {
|
|
3702
|
+
inString = false;
|
|
3703
|
+
}
|
|
3704
|
+
continue;
|
|
3705
|
+
}
|
|
3706
|
+
if (char === '"') {
|
|
3707
|
+
inString = true;
|
|
3708
|
+
continue;
|
|
3709
|
+
}
|
|
3710
|
+
if (char === "{" || char === "[") {
|
|
3711
|
+
depth += 1;
|
|
3712
|
+
continue;
|
|
3713
|
+
}
|
|
3714
|
+
if (char === "}" || char === "]") {
|
|
3715
|
+
depth -= 1;
|
|
3716
|
+
if (depth < 0)
|
|
3717
|
+
return { value: docs, error: "unexpected JSON close delimiter" };
|
|
3718
|
+
if (depth === 0 && start !== null) {
|
|
3719
|
+
const segment = text.slice(start, index + 1);
|
|
3720
|
+
try {
|
|
3721
|
+
docs.push(JSON.parse(segment));
|
|
3722
|
+
} catch (error) {
|
|
3723
|
+
return { value: docs, error: error instanceof Error ? error.message : String(error) };
|
|
3724
|
+
}
|
|
3725
|
+
start = null;
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
if (inString || depth !== 0 || start !== null)
|
|
3730
|
+
return { value: docs, error: "incomplete JSON stream" };
|
|
3731
|
+
return { value: docs };
|
|
3732
|
+
}
|
|
3733
|
+
function parseJsonArray(value) {
|
|
3734
|
+
if (!value?.trim())
|
|
3735
|
+
return { value: [], error: "empty JSON output" };
|
|
3736
|
+
try {
|
|
3737
|
+
const parsed = JSON.parse(value);
|
|
3738
|
+
const flattened = flattenPaginatedArray(parsed);
|
|
3739
|
+
return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
3740
|
+
} catch (error) {
|
|
3741
|
+
const streamed = parseConcatenatedJsonValues(value);
|
|
3742
|
+
if (streamed.error)
|
|
3743
|
+
return { value: [], error: error instanceof Error ? error.message : String(error) };
|
|
3744
|
+
const flattened = streamed.value.flatMap((entry) => flattenPaginatedArray(entry) ?? []);
|
|
3745
|
+
return flattened.length > 0 || streamed.value.length === 0 ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
function parseGithubPrUrl(prUrl) {
|
|
3749
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
|
|
3750
|
+
if (!match)
|
|
3751
|
+
return null;
|
|
3752
|
+
const prNumber = Number.parseInt(match[3], 10);
|
|
3753
|
+
if (!Number.isFinite(prNumber))
|
|
3754
|
+
return null;
|
|
3755
|
+
return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
|
|
3756
|
+
}
|
|
3757
|
+
function checkName(check) {
|
|
3758
|
+
return String(check.name ?? check.context ?? "").trim();
|
|
3759
|
+
}
|
|
3760
|
+
function checkState(check) {
|
|
3761
|
+
return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
|
|
3762
|
+
}
|
|
3763
|
+
function isGreptileLabel(value) {
|
|
3764
|
+
return String(value ?? "").toLowerCase().includes("greptile");
|
|
3765
|
+
}
|
|
3766
|
+
function isGreptileGithubLogin(value) {
|
|
3767
|
+
const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
|
|
3768
|
+
return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
|
|
3769
|
+
}
|
|
3770
|
+
function isPassingCheck(check) {
|
|
3771
|
+
const state = checkState(check);
|
|
3772
|
+
return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
|
|
3773
|
+
}
|
|
3774
|
+
function isPendingCheck(check) {
|
|
3775
|
+
const state = checkState(check);
|
|
3776
|
+
return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
|
|
3777
|
+
}
|
|
3778
|
+
function isFailingCheck(check) {
|
|
3779
|
+
const state = checkState(check);
|
|
3780
|
+
return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
|
|
3781
|
+
}
|
|
3782
|
+
function wildcardToRegExp(pattern) {
|
|
3783
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3784
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
3785
|
+
}
|
|
3786
|
+
function isAllowedFailure(name, allowedFailures) {
|
|
3787
|
+
return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
|
|
3788
|
+
}
|
|
3789
|
+
function greptileScorePatterns() {
|
|
3790
|
+
return [
|
|
3791
|
+
/\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
|
|
3792
|
+
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
|
|
3793
|
+
/\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
|
|
3794
|
+
];
|
|
3795
|
+
}
|
|
3796
|
+
function parseGreptileScores(input) {
|
|
3797
|
+
const text = stripHtml(input);
|
|
3798
|
+
const seen = new Set;
|
|
3799
|
+
const scores = [];
|
|
3800
|
+
for (const pattern of greptileScorePatterns()) {
|
|
3801
|
+
for (const match of text.matchAll(pattern)) {
|
|
3802
|
+
const value = Number.parseInt(match[1] || "", 10);
|
|
3803
|
+
const scale = Number.parseInt(match[2] || "", 10);
|
|
3804
|
+
if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
|
|
3805
|
+
continue;
|
|
3806
|
+
const raw = match[0] || `${value}/${scale}`;
|
|
3807
|
+
const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
|
|
3808
|
+
if (seen.has(key))
|
|
3809
|
+
continue;
|
|
3810
|
+
seen.add(key);
|
|
3811
|
+
scores.push({ value, scale, raw });
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
return scores;
|
|
3815
|
+
}
|
|
3816
|
+
function parseGreptileScore(input) {
|
|
3817
|
+
return parseGreptileScores(input)[0] ?? null;
|
|
3818
|
+
}
|
|
3819
|
+
function stripHtml(input) {
|
|
3820
|
+
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
3821
|
+
|
|
3822
|
+
`).trim();
|
|
3823
|
+
}
|
|
3824
|
+
function containsBlockerText(input) {
|
|
3825
|
+
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
3826
|
+
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);
|
|
3827
|
+
}
|
|
3828
|
+
function isStrictFiveOfFive(score) {
|
|
3829
|
+
return score.value === 5 && score.scale === 5;
|
|
3830
|
+
}
|
|
3831
|
+
function containsConflictingScoreText(input) {
|
|
3832
|
+
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
3833
|
+
}
|
|
3834
|
+
function extractGreptileCommentBlock(input) {
|
|
3835
|
+
const match = input.match(/<!--\s*greptile_comment\s*-->([\s\S]*?)<!--\s*\/greptile_comment\s*-->/i);
|
|
3836
|
+
return match?.[1]?.trim() ?? null;
|
|
3837
|
+
}
|
|
3838
|
+
function extractGreptileBodyReviewedSha(input) {
|
|
3839
|
+
const block = extractGreptileCommentBlock(input);
|
|
3840
|
+
if (!block)
|
|
3841
|
+
return null;
|
|
3842
|
+
const commitLink = block.match(/\/commit\/([0-9a-f]{40})(?:\b|[^0-9a-f])/i);
|
|
3843
|
+
return commitLink?.[1]?.toLowerCase() ?? null;
|
|
3844
|
+
}
|
|
3845
|
+
function isoAtOrAfter(value, floor) {
|
|
3846
|
+
if (!value || !floor)
|
|
3847
|
+
return false;
|
|
3848
|
+
const valueMs = Date.parse(value);
|
|
3849
|
+
const floorMs = Date.parse(floor);
|
|
3850
|
+
return Number.isFinite(valueMs) && Number.isFinite(floorMs) && valueMs >= floorMs;
|
|
3851
|
+
}
|
|
3852
|
+
function greptileStatusVerdict(status) {
|
|
3853
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
3854
|
+
if (!normalized)
|
|
3855
|
+
return null;
|
|
3856
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
3857
|
+
return "approved";
|
|
3858
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
3859
|
+
return "rejected";
|
|
3860
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
3861
|
+
return "skipped";
|
|
3862
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
3863
|
+
return "failed";
|
|
3864
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
3865
|
+
return "pending";
|
|
3866
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
3867
|
+
return "completed";
|
|
3868
|
+
return null;
|
|
3869
|
+
}
|
|
3870
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
3871
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
3872
|
+
}
|
|
3873
|
+
function greptileRequestTimeoutMs(env) {
|
|
3874
|
+
const fallback = 30000;
|
|
3875
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
3876
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
3877
|
+
}
|
|
3878
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
3879
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
3880
|
+
return null;
|
|
3881
|
+
const record = entry;
|
|
3882
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
3883
|
+
if (!id)
|
|
3884
|
+
return null;
|
|
3885
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
3886
|
+
return {
|
|
3887
|
+
id,
|
|
3888
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
3889
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
3890
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
3891
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
3892
|
+
};
|
|
3893
|
+
}
|
|
3894
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
3895
|
+
const seen = new Set;
|
|
3896
|
+
const unique2 = [];
|
|
3897
|
+
for (const review of reviews) {
|
|
3898
|
+
if (seen.has(review.id))
|
|
3899
|
+
continue;
|
|
3900
|
+
seen.add(review.id);
|
|
3901
|
+
unique2.push(review);
|
|
3902
|
+
}
|
|
3903
|
+
return unique2;
|
|
3904
|
+
}
|
|
3905
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
3906
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
3907
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
3908
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
3909
|
+
const latest = sorted.slice(0, 1);
|
|
3910
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
3911
|
+
}
|
|
3912
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
3913
|
+
const selected = details ?? review;
|
|
3914
|
+
return {
|
|
3915
|
+
id: selected.id || review.id,
|
|
3916
|
+
body: selected.body ?? review.body ?? null,
|
|
3917
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
3918
|
+
status: selected.status ?? review.status ?? null
|
|
3919
|
+
};
|
|
3920
|
+
}
|
|
3921
|
+
async function callGreptileMcpToolForGate(input) {
|
|
3922
|
+
const controller = new AbortController;
|
|
3923
|
+
const timeoutId = setTimeout(() => {
|
|
3924
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
3925
|
+
}, input.timeoutMs);
|
|
3926
|
+
let response;
|
|
3927
|
+
try {
|
|
3928
|
+
response = await input.fetchFn(input.apiBase, {
|
|
3929
|
+
method: "POST",
|
|
3930
|
+
headers: {
|
|
3931
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
3932
|
+
"Content-Type": "application/json"
|
|
3933
|
+
},
|
|
3934
|
+
body: JSON.stringify({
|
|
3935
|
+
jsonrpc: "2.0",
|
|
3936
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
3937
|
+
method: "tools/call",
|
|
3938
|
+
params: { name: input.name, arguments: input.args }
|
|
3939
|
+
}),
|
|
3940
|
+
signal: controller.signal
|
|
3941
|
+
});
|
|
3942
|
+
} catch (error) {
|
|
3943
|
+
if (controller.signal.aborted) {
|
|
3944
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
3945
|
+
}
|
|
3946
|
+
throw error;
|
|
3947
|
+
} finally {
|
|
3948
|
+
clearTimeout(timeoutId);
|
|
3949
|
+
}
|
|
3950
|
+
const raw = await response.text();
|
|
3951
|
+
if (!response.ok) {
|
|
3952
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
3953
|
+
}
|
|
3954
|
+
let envelope;
|
|
3955
|
+
try {
|
|
3956
|
+
envelope = JSON.parse(raw);
|
|
3957
|
+
} catch {
|
|
3958
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
3959
|
+
}
|
|
3960
|
+
if (envelope.error?.message) {
|
|
3961
|
+
throw new Error(envelope.error.message);
|
|
3962
|
+
}
|
|
3963
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
3964
|
+
`).trim();
|
|
3965
|
+
if (!text) {
|
|
3966
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
3967
|
+
}
|
|
3968
|
+
return text;
|
|
3969
|
+
}
|
|
3970
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
3971
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
3972
|
+
try {
|
|
3973
|
+
return JSON.parse(text);
|
|
3974
|
+
} catch {
|
|
3975
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
3979
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
3980
|
+
return { signals: [], errors: [] };
|
|
3981
|
+
}
|
|
3982
|
+
const env = input.options?.env ?? process.env;
|
|
3983
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
3984
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
3985
|
+
if (!apiKey) {
|
|
3986
|
+
return { signals: [], errors: [] };
|
|
3987
|
+
}
|
|
3988
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
3989
|
+
if (typeof fetchFn !== "function") {
|
|
3990
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
3991
|
+
}
|
|
3992
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
3993
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
3994
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
3995
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
3996
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
3997
|
+
try {
|
|
3998
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
3999
|
+
apiBase,
|
|
4000
|
+
apiKey,
|
|
4001
|
+
name: "list_code_reviews",
|
|
4002
|
+
args: {
|
|
4003
|
+
name: repository,
|
|
4004
|
+
remote,
|
|
4005
|
+
defaultBranch,
|
|
4006
|
+
prNumber: input.prNumber,
|
|
4007
|
+
limit: 20
|
|
4008
|
+
},
|
|
4009
|
+
timeoutMs,
|
|
4010
|
+
fetchFn
|
|
4011
|
+
});
|
|
4012
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
4013
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
4014
|
+
const signals = [];
|
|
4015
|
+
for (const review of selectedReviews) {
|
|
4016
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
4017
|
+
apiBase,
|
|
4018
|
+
apiKey,
|
|
4019
|
+
name: "get_code_review",
|
|
4020
|
+
args: { codeReviewId: review.id },
|
|
4021
|
+
timeoutMs,
|
|
4022
|
+
fetchFn
|
|
4023
|
+
});
|
|
4024
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
4025
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
4026
|
+
}
|
|
4027
|
+
return { signals, errors: [] };
|
|
4028
|
+
} catch (error) {
|
|
4029
|
+
return {
|
|
4030
|
+
signals: [],
|
|
4031
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
4032
|
+
};
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
function firstString(record, keys) {
|
|
4036
|
+
for (const key of keys) {
|
|
4037
|
+
const value = record[key];
|
|
4038
|
+
if (typeof value === "string")
|
|
4039
|
+
return value;
|
|
4040
|
+
}
|
|
4041
|
+
return "";
|
|
4042
|
+
}
|
|
4043
|
+
function arrayField(record, key) {
|
|
4044
|
+
const value = record[key];
|
|
4045
|
+
return Array.isArray(value) ? value : [];
|
|
4046
|
+
}
|
|
4047
|
+
async function runJsonArray(command, args, cwd) {
|
|
4048
|
+
const result = await command(args, { cwd });
|
|
4049
|
+
const label = `gh ${args.join(" ")}`;
|
|
4050
|
+
if (result.exitCode !== 0) {
|
|
4051
|
+
return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
4052
|
+
}
|
|
4053
|
+
const parsed = parseJsonArray(result.stdout);
|
|
4054
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
4055
|
+
}
|
|
4056
|
+
async function runJsonObject(command, args, cwd) {
|
|
4057
|
+
const result = await command(args, { cwd });
|
|
4058
|
+
const label = `gh ${args.join(" ")}`;
|
|
4059
|
+
if (result.exitCode !== 0) {
|
|
4060
|
+
return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
4061
|
+
}
|
|
4062
|
+
const parsed = parseJsonObject(result.stdout);
|
|
4063
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
4064
|
+
}
|
|
4065
|
+
function normalizeStatusCheck(entry) {
|
|
4066
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4067
|
+
return null;
|
|
4068
|
+
const record = entry;
|
|
4069
|
+
const name = firstString(record, ["name", "context"]);
|
|
4070
|
+
if (!name.trim())
|
|
4071
|
+
return null;
|
|
4072
|
+
const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
|
|
4073
|
+
const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
|
|
4074
|
+
return {
|
|
4075
|
+
__typename: typeof record.__typename === "string" ? record.__typename : null,
|
|
4076
|
+
name,
|
|
4077
|
+
context: typeof record.context === "string" ? record.context : null,
|
|
4078
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
4079
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
4080
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
4081
|
+
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,
|
|
4082
|
+
link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
|
|
4083
|
+
headSha: typeof record.headSha === "string" ? record.headSha : null,
|
|
4084
|
+
head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
|
|
4085
|
+
output: output ? {
|
|
4086
|
+
title: typeof output.title === "string" ? output.title : null,
|
|
4087
|
+
summary: typeof output.summary === "string" ? output.summary : null,
|
|
4088
|
+
text: typeof output.text === "string" ? output.text : null
|
|
4089
|
+
} : null,
|
|
4090
|
+
app: app ? {
|
|
4091
|
+
slug: typeof app.slug === "string" ? app.slug : null,
|
|
4092
|
+
name: typeof app.name === "string" ? app.name : null,
|
|
4093
|
+
owner: app.owner && typeof app.owner === "object" ? app.owner : null
|
|
4094
|
+
} : null
|
|
4095
|
+
};
|
|
4096
|
+
}
|
|
4097
|
+
function normalizeReview(entry) {
|
|
4098
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4099
|
+
return null;
|
|
4100
|
+
const record = entry;
|
|
4101
|
+
return {
|
|
4102
|
+
id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
|
|
4103
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
4104
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
4105
|
+
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,
|
|
4106
|
+
html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
|
|
4107
|
+
author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
function normalizeReviewComment(entry) {
|
|
4111
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4112
|
+
return null;
|
|
4113
|
+
const record = entry;
|
|
4114
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
4115
|
+
const path = typeof record.path === "string" ? record.path : null;
|
|
4116
|
+
if (!body && !path)
|
|
4117
|
+
return null;
|
|
4118
|
+
return {
|
|
4119
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
4120
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
4121
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
4122
|
+
body,
|
|
4123
|
+
path,
|
|
4124
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
4125
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
4126
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
|
|
4127
|
+
original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
4130
|
+
function normalizeIssueComment(entry) {
|
|
4131
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4132
|
+
return null;
|
|
4133
|
+
const record = entry;
|
|
4134
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
4135
|
+
if (!body)
|
|
4136
|
+
return null;
|
|
4137
|
+
return {
|
|
4138
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
4139
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
4140
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
4141
|
+
body,
|
|
4142
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
4143
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
4144
|
+
created_at: typeof record.created_at === "string" ? record.created_at : null
|
|
4145
|
+
};
|
|
4146
|
+
}
|
|
4147
|
+
function normalizeReviewThread(entry) {
|
|
4148
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4149
|
+
return null;
|
|
4150
|
+
const record = entry;
|
|
4151
|
+
return {
|
|
4152
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
4153
|
+
isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
|
|
4154
|
+
isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
|
|
4155
|
+
comments: record.comments && typeof record.comments === "object" ? record.comments : null
|
|
4156
|
+
};
|
|
4157
|
+
}
|
|
4158
|
+
function relevantIssueComment(comment) {
|
|
4159
|
+
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
4160
|
+
const body = comment.body ?? "";
|
|
4161
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
4162
|
+
}
|
|
4163
|
+
function latestThreadComment(thread) {
|
|
4164
|
+
const nodes = thread.comments?.nodes ?? [];
|
|
4165
|
+
return nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
4166
|
+
}
|
|
4167
|
+
function unresolvedThreadSummaries(threads) {
|
|
4168
|
+
return threads.flatMap((thread) => {
|
|
4169
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4170
|
+
return [];
|
|
4171
|
+
const latest = latestThreadComment(thread);
|
|
4172
|
+
if (!latest)
|
|
4173
|
+
return ["Unresolved review thread"];
|
|
4174
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4175
|
+
return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4176
|
+
});
|
|
4177
|
+
}
|
|
4178
|
+
function collectBodies(evidence) {
|
|
4179
|
+
return [
|
|
4180
|
+
evidence.title ?? "",
|
|
4181
|
+
evidence.body,
|
|
4182
|
+
...evidence.reviews.map((review) => review.body ?? ""),
|
|
4183
|
+
...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
|
|
4184
|
+
...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
|
|
4185
|
+
...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
|
|
4186
|
+
...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
|
|
4187
|
+
].filter((body) => body.trim().length > 0);
|
|
4188
|
+
}
|
|
4189
|
+
function bodyExcerpt(body) {
|
|
4190
|
+
const text = stripHtml(body).replace(/\s+/g, " ").trim();
|
|
4191
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
4192
|
+
}
|
|
4193
|
+
function makeGreptileSignal(input) {
|
|
4194
|
+
const scores = parseGreptileScores(input.body);
|
|
4195
|
+
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
4196
|
+
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
4197
|
+
const verdict = input.verdict ?? null;
|
|
4198
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
4199
|
+
const explicitApproval = input.explicitApproval ?? false;
|
|
4200
|
+
return {
|
|
4201
|
+
source: input.source,
|
|
4202
|
+
trusted: input.trusted,
|
|
4203
|
+
authorLogin: input.authorLogin ?? null,
|
|
4204
|
+
reviewedSha,
|
|
4205
|
+
current,
|
|
4206
|
+
stale: current === false,
|
|
4207
|
+
score: scores[0] ?? null,
|
|
4208
|
+
scores,
|
|
4209
|
+
explicitApproval,
|
|
4210
|
+
verdict,
|
|
4211
|
+
blocker,
|
|
4212
|
+
actionable: input.actionable ?? blocker,
|
|
4213
|
+
bodyExcerpt: bodyExcerpt(input.body),
|
|
4214
|
+
body: input.body,
|
|
4215
|
+
allScores: scores
|
|
4216
|
+
};
|
|
4217
|
+
}
|
|
4218
|
+
function reviewAuthorLogin(review) {
|
|
4219
|
+
return review.author?.login ?? null;
|
|
4220
|
+
}
|
|
4221
|
+
function commentAuthorLogin(comment) {
|
|
4222
|
+
return comment.user?.login ?? comment.author?.login ?? null;
|
|
4223
|
+
}
|
|
4224
|
+
function collectGreptileSignals(evidence) {
|
|
4225
|
+
const signals = [];
|
|
4226
|
+
const greptileBodyReviewedSha = extractGreptileBodyReviewedSha(evidence.body);
|
|
4227
|
+
const trustedGreptileBody = Boolean(greptileBodyReviewedSha && isGreptileGithubLogin(evidence.bodyEditorLogin) && isoAtOrAfter(evidence.bodyLastEditedAt, evidence.headCommittedDate));
|
|
4228
|
+
const contextSources = [
|
|
4229
|
+
{ source: "pr-title", body: evidence.title ?? "" },
|
|
4230
|
+
{
|
|
4231
|
+
source: "pr-body",
|
|
4232
|
+
body: evidence.body,
|
|
4233
|
+
trusted: trustedGreptileBody,
|
|
4234
|
+
authorLogin: trustedGreptileBody ? evidence.bodyEditorLogin ?? null : null,
|
|
4235
|
+
reviewedSha: greptileBodyReviewedSha,
|
|
4236
|
+
verdict: trustedGreptileBody ? "completed" : null
|
|
4237
|
+
}
|
|
4238
|
+
];
|
|
4239
|
+
for (const context of contextSources) {
|
|
4240
|
+
if (!context.body.trim())
|
|
4241
|
+
continue;
|
|
4242
|
+
const contextBlocker = containsBlockerText(context.body);
|
|
4243
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
4244
|
+
continue;
|
|
4245
|
+
signals.push(makeGreptileSignal({
|
|
4246
|
+
source: context.source,
|
|
4247
|
+
body: context.body,
|
|
4248
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4249
|
+
trusted: context.trusted === true,
|
|
4250
|
+
authorLogin: context.authorLogin,
|
|
4251
|
+
reviewedSha: context.reviewedSha,
|
|
4252
|
+
verdict: context.verdict,
|
|
4253
|
+
blocker: contextBlocker,
|
|
4254
|
+
actionable: contextBlocker
|
|
4255
|
+
}));
|
|
4256
|
+
}
|
|
4257
|
+
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
4258
|
+
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
4259
|
+
|
|
4260
|
+
`) || "Status: UNKNOWN";
|
|
4261
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
4262
|
+
signals.push(makeGreptileSignal({
|
|
4263
|
+
source: "api",
|
|
4264
|
+
body,
|
|
4265
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4266
|
+
trusted: true,
|
|
4267
|
+
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
4268
|
+
explicitApproval: verdict === "approved",
|
|
4269
|
+
verdict
|
|
4270
|
+
}));
|
|
4271
|
+
}
|
|
4272
|
+
for (const review of evidence.reviews) {
|
|
4273
|
+
const login = reviewAuthorLogin(review);
|
|
4274
|
+
if (!isGreptileGithubLogin(login))
|
|
4275
|
+
continue;
|
|
4276
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4277
|
+
const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
|
|
4278
|
+
|
|
4279
|
+
`);
|
|
4280
|
+
if (!body.trim())
|
|
4281
|
+
continue;
|
|
4282
|
+
const dismissed = state === "DISMISSED";
|
|
4283
|
+
signals.push(makeGreptileSignal({
|
|
4284
|
+
source: "github-review",
|
|
4285
|
+
body,
|
|
4286
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4287
|
+
trusted: !dismissed,
|
|
4288
|
+
authorLogin: login,
|
|
4289
|
+
reviewedSha: review.commit_id ?? null,
|
|
4290
|
+
explicitApproval: undefined,
|
|
4291
|
+
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
4292
|
+
}));
|
|
4293
|
+
}
|
|
4294
|
+
for (const comment of evidence.relevantIssueComments) {
|
|
4295
|
+
const login = commentAuthorLogin(comment);
|
|
4296
|
+
const body = comment.body ?? "";
|
|
4297
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4298
|
+
continue;
|
|
4299
|
+
signals.push(makeGreptileSignal({
|
|
4300
|
+
source: "issue-comment",
|
|
4301
|
+
body,
|
|
4302
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4303
|
+
trusted: true,
|
|
4304
|
+
authorLogin: login
|
|
4305
|
+
}));
|
|
4306
|
+
}
|
|
4307
|
+
for (const thread of evidence.reviewThreads) {
|
|
4308
|
+
if (thread.isOutdated === true || thread.isResolved === true)
|
|
4309
|
+
continue;
|
|
4310
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
4311
|
+
const login = comment.author?.login ?? null;
|
|
4312
|
+
const body = comment.body ?? "";
|
|
4313
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4314
|
+
continue;
|
|
4315
|
+
signals.push(makeGreptileSignal({
|
|
4316
|
+
source: "review-thread",
|
|
4317
|
+
body,
|
|
4318
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4319
|
+
trusted: true,
|
|
4320
|
+
authorLogin: login
|
|
4321
|
+
}));
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
for (const check of evidence.checks) {
|
|
4325
|
+
if (!isGreptileLabel(checkName(check)))
|
|
4326
|
+
continue;
|
|
4327
|
+
const reviewedSha = check.headSha ?? check.head_sha ?? null;
|
|
4328
|
+
const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
|
|
4329
|
+
const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
|
|
4330
|
+
|
|
4331
|
+
`);
|
|
4332
|
+
signals.push(makeGreptileSignal({
|
|
4333
|
+
source: "github-check",
|
|
4334
|
+
body,
|
|
4335
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4336
|
+
trusted: false,
|
|
4337
|
+
reviewedSha,
|
|
4338
|
+
explicitApproval: false,
|
|
4339
|
+
blocker: isFailingCheck(check),
|
|
4340
|
+
actionable: isFailingCheck(check)
|
|
4341
|
+
}));
|
|
4342
|
+
}
|
|
4343
|
+
return signals;
|
|
4344
|
+
}
|
|
4345
|
+
function unresolvedGreptileThreadSummaries(threads) {
|
|
4346
|
+
return threads.flatMap((thread) => {
|
|
4347
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4348
|
+
return [];
|
|
4349
|
+
const comments = thread.comments?.nodes ?? [];
|
|
4350
|
+
if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
|
|
4351
|
+
return [];
|
|
4352
|
+
const latest = latestThreadComment(thread);
|
|
4353
|
+
if (!latest)
|
|
4354
|
+
return ["Unresolved Greptile review thread"];
|
|
4355
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4356
|
+
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4357
|
+
});
|
|
4358
|
+
}
|
|
4359
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
4360
|
+
return [];
|
|
4361
|
+
}
|
|
4362
|
+
function issueLevelBlockerSummaries(comments) {
|
|
4363
|
+
return comments.flatMap((comment) => {
|
|
4364
|
+
const body = comment.body?.trim() ?? "";
|
|
4365
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4366
|
+
return [];
|
|
4367
|
+
const login = commentAuthorLogin(comment) ?? "unknown";
|
|
4368
|
+
const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
|
|
4369
|
+
return [`${author}: ${body}`];
|
|
4370
|
+
});
|
|
4371
|
+
}
|
|
4372
|
+
function reviewBodyBlockerSummaries(reviews) {
|
|
4373
|
+
return reviews.flatMap((review) => {
|
|
4374
|
+
const login = reviewAuthorLogin(review) ?? "unknown";
|
|
4375
|
+
if (isGreptileGithubLogin(login))
|
|
4376
|
+
return [];
|
|
4377
|
+
const body = review.body?.trim() ?? "";
|
|
4378
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4379
|
+
return [];
|
|
4380
|
+
const state = review.state ? ` (${review.state})` : "";
|
|
4381
|
+
return [`PR review summary by ${login}${state}: ${body}`];
|
|
4382
|
+
});
|
|
4383
|
+
}
|
|
4384
|
+
function signalLabel(signal) {
|
|
4385
|
+
const source = signal.source.replace(/-/g, " ");
|
|
4386
|
+
const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
|
|
4387
|
+
const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
|
|
4388
|
+
return `${source}${author}${sha}`;
|
|
4389
|
+
}
|
|
4390
|
+
function deriveGreptileEvidence(input) {
|
|
4391
|
+
const rawBodies = collectBodies(input);
|
|
4392
|
+
const signals = collectGreptileSignals(input);
|
|
4393
|
+
const trustedSignals = signals.filter((signal) => signal.trusted);
|
|
4394
|
+
const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4395
|
+
const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4396
|
+
const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
|
|
4397
|
+
const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
|
|
4398
|
+
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4399
|
+
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4400
|
+
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4401
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
4402
|
+
const signalCanApproveByScore = (signal) => {
|
|
4403
|
+
if (signal.source === "api")
|
|
4404
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
4405
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
4406
|
+
};
|
|
4407
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4408
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
4409
|
+
const approvedByScore = !!approvingScoreEntry;
|
|
4410
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
4411
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
4412
|
+
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4413
|
+
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4414
|
+
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4415
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4416
|
+
const staleBlockingSignals = [];
|
|
4417
|
+
const blockers = [
|
|
4418
|
+
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
4419
|
+
...reviewBodyBlockerSummaries(input.reviews),
|
|
4420
|
+
...issueLevelBlockerSummaries(input.relevantIssueComments),
|
|
4421
|
+
...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
|
|
4422
|
+
...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
|
|
4423
|
+
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4424
|
+
];
|
|
4425
|
+
const unresolvedComments = [
|
|
4426
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
4427
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
4428
|
+
];
|
|
4429
|
+
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4430
|
+
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
4431
|
+
const completedGreptileCheck = greptileChecks.some((check) => {
|
|
4432
|
+
const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
|
|
4433
|
+
return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
|
|
4434
|
+
});
|
|
4435
|
+
const completedGreptileReview = greptileReviews.some((review) => {
|
|
4436
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4437
|
+
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4438
|
+
return completedState && review.commit_id === input.currentHeadSha;
|
|
4439
|
+
});
|
|
4440
|
+
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"));
|
|
4441
|
+
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4442
|
+
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4443
|
+
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4444
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
4445
|
+
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4446
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
4447
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
4448
|
+
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";
|
|
4449
|
+
return {
|
|
4450
|
+
source,
|
|
4451
|
+
currentHeadSha: input.currentHeadSha,
|
|
4452
|
+
reviewedSha,
|
|
4453
|
+
fresh,
|
|
4454
|
+
completed,
|
|
4455
|
+
approved,
|
|
4456
|
+
score,
|
|
4457
|
+
explicitApproval: approvedByExplicitMapping,
|
|
4458
|
+
blockers,
|
|
4459
|
+
unresolvedComments,
|
|
4460
|
+
rawBodies,
|
|
4461
|
+
signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
|
|
4462
|
+
mapping
|
|
4463
|
+
};
|
|
4464
|
+
}
|
|
4465
|
+
function isGreptileCheckDetail(check) {
|
|
4466
|
+
return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
|
|
4467
|
+
}
|
|
4468
|
+
async function collectGreptileCheckDetails(input) {
|
|
4469
|
+
const checkRunsRead = await runJsonObject(input.command, [
|
|
4470
|
+
"api",
|
|
4471
|
+
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
4472
|
+
"-F",
|
|
4473
|
+
"per_page=100"
|
|
4474
|
+
], input.projectRoot);
|
|
4475
|
+
const checkRuns = arrayField(checkRunsRead.value, "check_runs").map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
|
|
4476
|
+
return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
|
|
4477
|
+
}
|
|
4478
|
+
async function collectPullRequestProvenance(input) {
|
|
4479
|
+
const response = await runJsonObject(input.command, [
|
|
4480
|
+
"api",
|
|
4481
|
+
"graphql",
|
|
4482
|
+
"-F",
|
|
4483
|
+
`owner=${input.owner}`,
|
|
4484
|
+
"-F",
|
|
4485
|
+
`name=${input.name}`,
|
|
4486
|
+
"-F",
|
|
4487
|
+
`prNumber=${input.prNumber}`,
|
|
4488
|
+
"-f",
|
|
4489
|
+
"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 } } } } } }"
|
|
4490
|
+
], input.projectRoot);
|
|
4491
|
+
if (response.error)
|
|
4492
|
+
return { value: {}, error: response.error };
|
|
4493
|
+
const data = response.value.data;
|
|
4494
|
+
const repository = data?.repository;
|
|
4495
|
+
const pullRequest = repository?.pullRequest;
|
|
4496
|
+
if (!pullRequest)
|
|
4497
|
+
return { value: {}, error: "GitHub pullRequest provenance response did not include a pullRequest object" };
|
|
4498
|
+
const editor = pullRequest.editor;
|
|
4499
|
+
const commits = pullRequest.commits;
|
|
4500
|
+
const nodes = Array.isArray(commits?.nodes) ? commits.nodes : [];
|
|
4501
|
+
const latestCommitNode = nodes[nodes.length - 1];
|
|
4502
|
+
const latestCommit = latestCommitNode?.commit;
|
|
4503
|
+
return {
|
|
4504
|
+
value: {
|
|
4505
|
+
bodyEditorLogin: typeof editor?.login === "string" ? editor.login : null,
|
|
4506
|
+
bodyLastEditedAt: typeof pullRequest.lastEditedAt === "string" ? pullRequest.lastEditedAt : null,
|
|
4507
|
+
headCommittedDate: typeof latestCommit?.committedDate === "string" ? latestCommit.committedDate : null
|
|
4508
|
+
}
|
|
4509
|
+
};
|
|
4510
|
+
}
|
|
4511
|
+
async function collectReviewThreads(input) {
|
|
4512
|
+
const reviewThreads = [];
|
|
4513
|
+
let afterCursor = null;
|
|
4514
|
+
for (let page = 0;page < 100; page += 1) {
|
|
4515
|
+
const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
|
|
4516
|
+
const threadsResponse = await runJsonObject(input.command, [
|
|
4517
|
+
"api",
|
|
4518
|
+
"graphql",
|
|
4519
|
+
"-F",
|
|
4520
|
+
`owner=${input.owner}`,
|
|
4521
|
+
"-F",
|
|
4522
|
+
`name=${input.name}`,
|
|
4523
|
+
"-F",
|
|
4524
|
+
`prNumber=${input.prNumber}`,
|
|
4525
|
+
"-f",
|
|
4526
|
+
`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 } } } } }`
|
|
4527
|
+
], input.projectRoot);
|
|
4528
|
+
if (threadsResponse.error) {
|
|
4529
|
+
return { value: reviewThreads, error: threadsResponse.error };
|
|
4530
|
+
}
|
|
4531
|
+
const data = threadsResponse.value.data;
|
|
4532
|
+
const repository = data?.repository;
|
|
4533
|
+
const pullRequest = repository?.pullRequest;
|
|
4534
|
+
const threads = pullRequest?.reviewThreads;
|
|
4535
|
+
const nodes = threads?.nodes;
|
|
4536
|
+
if (!Array.isArray(nodes)) {
|
|
4537
|
+
return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
|
|
4538
|
+
}
|
|
4539
|
+
const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
|
|
4540
|
+
reviewThreads.push(...normalized);
|
|
4541
|
+
const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
|
|
4542
|
+
if (truncatedCommentThread) {
|
|
4543
|
+
return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
|
|
4544
|
+
}
|
|
4545
|
+
const pageInfo = threads?.pageInfo;
|
|
4546
|
+
if (!pageInfo) {
|
|
4547
|
+
if (nodes.length >= 100) {
|
|
4548
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
|
|
4549
|
+
}
|
|
4550
|
+
return { value: reviewThreads };
|
|
4551
|
+
}
|
|
4552
|
+
if (pageInfo.hasNextPage !== true) {
|
|
4553
|
+
return { value: reviewThreads };
|
|
4554
|
+
}
|
|
4555
|
+
if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
|
|
4556
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
|
|
4557
|
+
}
|
|
4558
|
+
afterCursor = pageInfo.endCursor;
|
|
4559
|
+
}
|
|
4560
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
|
|
4561
|
+
}
|
|
4562
|
+
async function collectPrReviewEvidence(input) {
|
|
4563
|
+
const parsed = parseGithubPrUrl(input.prUrl);
|
|
4564
|
+
if (!parsed) {
|
|
4565
|
+
throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
|
|
4566
|
+
}
|
|
4567
|
+
const readErrors = [];
|
|
4568
|
+
const viewRead = await runJsonObject(input.command, [
|
|
4569
|
+
"pr",
|
|
4570
|
+
"view",
|
|
4571
|
+
input.prUrl,
|
|
4572
|
+
"--json",
|
|
4573
|
+
"title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
|
|
4574
|
+
], input.projectRoot);
|
|
4575
|
+
if (viewRead.error)
|
|
4576
|
+
readErrors.push(viewRead.error);
|
|
4577
|
+
const view = viewRead.value;
|
|
4578
|
+
if (!Array.isArray(view.statusCheckRollup)) {
|
|
4579
|
+
readErrors.push("gh pr view did not return required statusCheckRollup array");
|
|
4580
|
+
}
|
|
4581
|
+
if (!Array.isArray(view.reviews)) {
|
|
4582
|
+
readErrors.push("gh pr view did not return required reviews array");
|
|
4583
|
+
}
|
|
4584
|
+
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4585
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
4586
|
+
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4587
|
+
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4588
|
+
const provenanceRead = await collectPullRequestProvenance({
|
|
4589
|
+
command: input.command,
|
|
4590
|
+
projectRoot: input.projectRoot,
|
|
4591
|
+
owner: parsed.owner,
|
|
4592
|
+
name: parsed.repo,
|
|
4593
|
+
prNumber: parsed.prNumber
|
|
4594
|
+
});
|
|
4595
|
+
const provenance = provenanceRead.value;
|
|
4596
|
+
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
|
|
4597
|
+
if (reviewCommentsRead.error)
|
|
4598
|
+
readErrors.push(reviewCommentsRead.error);
|
|
4599
|
+
const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
|
|
4600
|
+
const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
|
|
4601
|
+
if (issueCommentsRead.error)
|
|
4602
|
+
readErrors.push(issueCommentsRead.error);
|
|
4603
|
+
const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
|
|
4604
|
+
const reviewThreadsRead = await collectReviewThreads({
|
|
4605
|
+
command: input.command,
|
|
4606
|
+
projectRoot: input.projectRoot,
|
|
4607
|
+
owner: parsed.owner,
|
|
4608
|
+
name: parsed.repo,
|
|
4609
|
+
prNumber: parsed.prNumber
|
|
4610
|
+
});
|
|
4611
|
+
if (reviewThreadsRead.error)
|
|
4612
|
+
readErrors.push(reviewThreadsRead.error);
|
|
4613
|
+
const reviewThreads = reviewThreadsRead.value;
|
|
4614
|
+
const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
|
|
4615
|
+
let greptileCheckDetails = [];
|
|
4616
|
+
if (headSha && greptileRollupChecks.length > 0) {
|
|
4617
|
+
const checkDetailsRead = await collectGreptileCheckDetails({
|
|
4618
|
+
command: input.command,
|
|
4619
|
+
projectRoot: input.projectRoot,
|
|
4620
|
+
repoName: parsed.repoName,
|
|
4621
|
+
headSha
|
|
4622
|
+
});
|
|
4623
|
+
greptileCheckDetails = checkDetailsRead.value;
|
|
4624
|
+
}
|
|
4625
|
+
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
4626
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
4627
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
4628
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
4629
|
+
options: input.greptileApi,
|
|
4630
|
+
repoName: parsed.repoName,
|
|
4631
|
+
prNumber: parsed.prNumber,
|
|
4632
|
+
headSha,
|
|
4633
|
+
baseRefName
|
|
4634
|
+
});
|
|
4635
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
4636
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
4637
|
+
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})` : ""}`);
|
|
4638
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4639
|
+
const evidenceBase = {
|
|
4640
|
+
title: firstString(view, ["title"]),
|
|
4641
|
+
body: firstString(view, ["body"]),
|
|
4642
|
+
bodyEditorLogin: provenance.bodyEditorLogin ?? null,
|
|
4643
|
+
bodyLastEditedAt: provenance.bodyLastEditedAt ?? null,
|
|
4644
|
+
headCommittedDate: provenance.headCommittedDate ?? null,
|
|
4645
|
+
reviews,
|
|
4646
|
+
changedFileReviewComments: reviewComments,
|
|
4647
|
+
relevantIssueComments: issueComments,
|
|
4648
|
+
reviewThreads,
|
|
4649
|
+
checks: checksWithGreptileDetails,
|
|
4650
|
+
currentHeadSha: headSha,
|
|
4651
|
+
apiSignals
|
|
4652
|
+
};
|
|
4653
|
+
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4654
|
+
return {
|
|
4655
|
+
prUrl: input.prUrl,
|
|
4656
|
+
prNumber: parsed.prNumber,
|
|
4657
|
+
repoName: parsed.repoName,
|
|
4658
|
+
title: evidenceBase.title,
|
|
4659
|
+
body: evidenceBase.body,
|
|
4660
|
+
bodyEditorLogin: evidenceBase.bodyEditorLogin,
|
|
4661
|
+
bodyLastEditedAt: evidenceBase.bodyLastEditedAt,
|
|
4662
|
+
headCommittedDate: evidenceBase.headCommittedDate,
|
|
4663
|
+
headSha,
|
|
4664
|
+
headRefName: firstString(view, ["headRefName"]),
|
|
4665
|
+
baseRefName,
|
|
4666
|
+
state: firstString(view, ["state"]),
|
|
4667
|
+
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4668
|
+
mergeable: firstString(view, ["mergeable"]),
|
|
4669
|
+
mergeStateStatus: firstString(view, ["mergeStateStatus"]),
|
|
4670
|
+
reviewDecision: firstString(view, ["reviewDecision"]),
|
|
4671
|
+
reviews,
|
|
4672
|
+
reviewThreads,
|
|
4673
|
+
changedFileReviewComments: reviewComments,
|
|
4674
|
+
relevantIssueComments: issueComments,
|
|
4675
|
+
statusCheckRollup: checksWithGreptileDetails,
|
|
4676
|
+
checkFailures,
|
|
4677
|
+
pendingChecks,
|
|
4678
|
+
readErrors,
|
|
4679
|
+
greptile
|
|
4680
|
+
};
|
|
4681
|
+
}
|
|
4682
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
4683
|
+
const normalized = value.trim();
|
|
4684
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
4685
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
4686
|
+
}
|
|
4687
|
+
function evaluateEvidence(evidence) {
|
|
4688
|
+
const reasonDetails = [];
|
|
4689
|
+
const warnings = [];
|
|
4690
|
+
const seen = new Set;
|
|
4691
|
+
const addReason = (reason) => {
|
|
4692
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
4693
|
+
const key = `${capped.code}:${capped.message}`;
|
|
4694
|
+
if (seen.has(key))
|
|
4695
|
+
return;
|
|
4696
|
+
seen.add(key);
|
|
4697
|
+
reasonDetails.push(capped);
|
|
4698
|
+
};
|
|
4699
|
+
const greptile = evidence.greptile;
|
|
4700
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4701
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
4702
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4703
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4704
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
4705
|
+
for (const error of evidence.readErrors) {
|
|
4706
|
+
addReason({
|
|
4707
|
+
code: "read_error",
|
|
4708
|
+
reasonClass: "reject",
|
|
4709
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
4710
|
+
suggestedAction: "needs_attention",
|
|
4711
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
4712
|
+
headSha: evidence.headSha || null
|
|
4713
|
+
});
|
|
4714
|
+
}
|
|
4715
|
+
if (!evidence.headSha) {
|
|
4716
|
+
addReason({
|
|
4717
|
+
code: "missing_head_sha",
|
|
4718
|
+
reasonClass: "reject",
|
|
4719
|
+
surface: "github",
|
|
4720
|
+
suggestedAction: "needs_attention",
|
|
4721
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
4722
|
+
headSha: null
|
|
4723
|
+
});
|
|
4724
|
+
}
|
|
4725
|
+
for (const failure of evidence.checkFailures) {
|
|
4726
|
+
addReason({
|
|
4727
|
+
code: "ci_failed",
|
|
4728
|
+
reasonClass: "reject",
|
|
4729
|
+
surface: "ci",
|
|
4730
|
+
suggestedAction: "fix",
|
|
4731
|
+
message: failure,
|
|
4732
|
+
headSha: evidence.headSha || null
|
|
4733
|
+
});
|
|
4734
|
+
}
|
|
4735
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
4736
|
+
addReason({
|
|
4737
|
+
code: "check_pending",
|
|
4738
|
+
reasonClass: "pending",
|
|
4739
|
+
surface: "ci",
|
|
4740
|
+
suggestedAction: "wait",
|
|
4741
|
+
message: pendingCheck,
|
|
4742
|
+
headSha: evidence.headSha || null
|
|
4743
|
+
});
|
|
4744
|
+
}
|
|
4745
|
+
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4746
|
+
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4747
|
+
addReason({
|
|
4748
|
+
code: "review_decision_blocking",
|
|
4749
|
+
reasonClass: "reject",
|
|
4750
|
+
surface: "review",
|
|
4751
|
+
suggestedAction: "fix",
|
|
4752
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
4753
|
+
headSha: evidence.headSha || null
|
|
4754
|
+
});
|
|
4755
|
+
}
|
|
4756
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
4757
|
+
addReason({
|
|
4758
|
+
code: "review_thread_unresolved",
|
|
4759
|
+
reasonClass: "reject",
|
|
4760
|
+
surface: "review",
|
|
4761
|
+
suggestedAction: "fix",
|
|
4762
|
+
message: thread,
|
|
4763
|
+
headSha: evidence.headSha || null
|
|
4764
|
+
});
|
|
4765
|
+
}
|
|
4766
|
+
if (greptile.mapping === "missing") {
|
|
4767
|
+
addReason({
|
|
4768
|
+
code: "greptile_missing",
|
|
4769
|
+
reasonClass: "pending",
|
|
4770
|
+
surface: "greptile",
|
|
4771
|
+
suggestedAction: "wait",
|
|
4772
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
4773
|
+
headSha: evidence.headSha || null
|
|
4774
|
+
});
|
|
4775
|
+
}
|
|
4776
|
+
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
4777
|
+
addReason({
|
|
4778
|
+
code: "greptile_stale",
|
|
4779
|
+
reasonClass: "pending",
|
|
4780
|
+
surface: "greptile",
|
|
4781
|
+
suggestedAction: "wait",
|
|
4782
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
4783
|
+
headSha: evidence.headSha || null,
|
|
4784
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
4785
|
+
});
|
|
4786
|
+
}
|
|
4787
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
4788
|
+
addReason({
|
|
4789
|
+
code: "greptile_pending",
|
|
4790
|
+
reasonClass: "pending",
|
|
4791
|
+
surface: "greptile",
|
|
4792
|
+
suggestedAction: "wait",
|
|
4793
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
4794
|
+
headSha: evidence.headSha || null,
|
|
4795
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4796
|
+
});
|
|
4797
|
+
}
|
|
4798
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
4799
|
+
addReason({
|
|
4800
|
+
code: "greptile_api_status_unknown",
|
|
4801
|
+
reasonClass: "reject",
|
|
4802
|
+
surface: "greptile",
|
|
4803
|
+
suggestedAction: "needs_attention",
|
|
4804
|
+
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}` : "."}`,
|
|
4805
|
+
headSha: evidence.headSha || null,
|
|
4806
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
4807
|
+
});
|
|
4808
|
+
}
|
|
4809
|
+
if (!greptile.completed) {
|
|
4810
|
+
addReason({
|
|
4811
|
+
code: "greptile_pending",
|
|
4812
|
+
reasonClass: "pending",
|
|
4813
|
+
surface: "greptile",
|
|
4814
|
+
suggestedAction: "wait",
|
|
4815
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
4816
|
+
headSha: evidence.headSha || null,
|
|
4817
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4818
|
+
});
|
|
4819
|
+
}
|
|
4820
|
+
if (!greptile.fresh) {
|
|
4821
|
+
addReason({
|
|
4822
|
+
code: "greptile_not_current_head",
|
|
4823
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4824
|
+
surface: "greptile",
|
|
4825
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4826
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
4827
|
+
headSha: evidence.headSha || null,
|
|
4828
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4829
|
+
});
|
|
4830
|
+
}
|
|
4831
|
+
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
4832
|
+
addReason({
|
|
4833
|
+
code: "greptile_score_not_5",
|
|
4834
|
+
reasonClass: "reject",
|
|
4835
|
+
surface: "greptile",
|
|
4836
|
+
suggestedAction: "fix",
|
|
4837
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
4838
|
+
headSha: evidence.headSha || null,
|
|
4839
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4840
|
+
});
|
|
4841
|
+
}
|
|
4842
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
4843
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
4844
|
+
addReason({
|
|
4845
|
+
code: "greptile_score_missing",
|
|
4846
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4847
|
+
surface: "greptile",
|
|
4848
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4849
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
4850
|
+
headSha: evidence.headSha || null,
|
|
4851
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4852
|
+
});
|
|
4853
|
+
}
|
|
4854
|
+
if (greptile.mapping === "unproven") {
|
|
4855
|
+
addReason({
|
|
4856
|
+
code: "greptile_mapping_unproven",
|
|
4857
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
4858
|
+
surface: "greptile",
|
|
4859
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
4860
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
4861
|
+
headSha: evidence.headSha || null,
|
|
4862
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4863
|
+
});
|
|
4864
|
+
}
|
|
4865
|
+
for (const blocker of greptile.blockers) {
|
|
4866
|
+
addReason({
|
|
4867
|
+
code: "greptile_blocker_text",
|
|
4868
|
+
reasonClass: "reject",
|
|
4869
|
+
surface: "greptile",
|
|
4870
|
+
suggestedAction: "fix",
|
|
4871
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
4872
|
+
headSha: evidence.headSha || null,
|
|
4873
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4874
|
+
});
|
|
4875
|
+
}
|
|
4876
|
+
for (const comment of greptile.unresolvedComments) {
|
|
4877
|
+
addReason({
|
|
4878
|
+
code: "greptile_unresolved_comment",
|
|
4879
|
+
reasonClass: "reject",
|
|
4880
|
+
surface: "greptile",
|
|
4881
|
+
suggestedAction: "fix",
|
|
4882
|
+
message: comment,
|
|
4883
|
+
headSha: evidence.headSha || null,
|
|
4884
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
4885
|
+
});
|
|
4886
|
+
}
|
|
4887
|
+
if (!greptile.approved)
|
|
4888
|
+
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
4889
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
4890
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
4891
|
+
}
|
|
4892
|
+
function evaluateStrictPrMergeGate(evidence) {
|
|
4893
|
+
const evaluated = evaluateEvidence(evidence);
|
|
4894
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
4895
|
+
return {
|
|
4896
|
+
approved,
|
|
4897
|
+
pending: evaluated.pending,
|
|
4898
|
+
reasons: evaluated.reasons,
|
|
4899
|
+
reasonDetails: evaluated.reasonDetails,
|
|
4900
|
+
warnings: evaluated.warnings,
|
|
4901
|
+
actionableFeedback: evaluated.reasons,
|
|
4902
|
+
evidence
|
|
3598
4903
|
};
|
|
3599
|
-
mkdirSync7(artifactDir, { recursive: true });
|
|
3600
|
-
writeFileSync7(resolve21(artifactDir, "validation-summary.json"), `${JSON.stringify(summary, null, 2)}
|
|
3601
|
-
`, "utf-8");
|
|
3602
|
-
return summary;
|
|
3603
4904
|
}
|
|
3604
4905
|
|
|
3605
4906
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3606
|
-
import { existsSync as existsSync19, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
3607
|
-
import { resolve as resolve22 } from "path";
|
|
3608
4907
|
async function verifyTask(options) {
|
|
3609
4908
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
3610
4909
|
const taskId = options.taskId;
|
|
3611
4910
|
const normalizedTaskId = lookupTask(options.projectRoot, taskId);
|
|
3612
4911
|
const artifactDir = artifactDirForId(options.projectRoot, taskId);
|
|
3613
|
-
|
|
3614
|
-
const validationSummaryPath =
|
|
3615
|
-
const reviewFeedbackPath =
|
|
3616
|
-
const reviewStatePath =
|
|
3617
|
-
const greptileRawPath =
|
|
4912
|
+
mkdirSync9(artifactDir, { recursive: true });
|
|
4913
|
+
const validationSummaryPath = resolve23(artifactDir, "validation-summary.json");
|
|
4914
|
+
const reviewFeedbackPath = resolve23(artifactDir, "review-feedback.md");
|
|
4915
|
+
const reviewStatePath = resolve23(artifactDir, "review-state.json");
|
|
4916
|
+
const greptileRawPath = resolve23(artifactDir, "review-greptile-raw.json");
|
|
3618
4917
|
const prStates = readPrMetadata(options.projectRoot, taskId);
|
|
3619
4918
|
const prState = prStates[0] || null;
|
|
3620
4919
|
const localReasons = [];
|
|
@@ -3626,7 +4925,7 @@ async function verifyTask(options) {
|
|
|
3626
4925
|
if (!normalizedTaskId && !await hasConfiguredSourceTask(options.projectRoot, taskId)) {
|
|
3627
4926
|
localReasons.push(`[Task Config] Unknown task id '${taskId}' in task-config or configured task source.`);
|
|
3628
4927
|
}
|
|
3629
|
-
if (!
|
|
4928
|
+
if (!existsSync20(validationSummaryPath)) {
|
|
3630
4929
|
localReasons.push(`[Artifact Quality] validation-summary.json not found at ${validationSummaryPath}.`);
|
|
3631
4930
|
} else {
|
|
3632
4931
|
const summary = await parseValidationSummary(validationSummaryPath);
|
|
@@ -3635,13 +4934,13 @@ async function verifyTask(options) {
|
|
|
3635
4934
|
}
|
|
3636
4935
|
}
|
|
3637
4936
|
for (const file of ["task-result.json", "decision-log.md", "next-actions.md", "changed-files.txt"]) {
|
|
3638
|
-
const requiredPath =
|
|
3639
|
-
if (!
|
|
4937
|
+
const requiredPath = resolve23(artifactDir, file);
|
|
4938
|
+
if (!existsSync20(requiredPath)) {
|
|
3640
4939
|
localReasons.push(`[Artifact Quality] Missing required artifact file: ${requiredPath}`);
|
|
3641
4940
|
}
|
|
3642
4941
|
}
|
|
3643
|
-
const taskResultPath =
|
|
3644
|
-
if (
|
|
4942
|
+
const taskResultPath = resolve23(artifactDir, "task-result.json");
|
|
4943
|
+
if (existsSync20(taskResultPath)) {
|
|
3645
4944
|
const taskResult = await readJsonFile2(taskResultPath);
|
|
3646
4945
|
const artifactStatus = typeof taskResult?.status === "string" ? taskResult.status.trim().toLowerCase() : "";
|
|
3647
4946
|
if (artifactStatus === "partial") {
|
|
@@ -3654,8 +4953,8 @@ async function verifyTask(options) {
|
|
|
3654
4953
|
localReasons.push("[Artifact Quality] task-result.json next actions indicate remaining implementation scope.");
|
|
3655
4954
|
}
|
|
3656
4955
|
}
|
|
3657
|
-
const nextActionsPath =
|
|
3658
|
-
if (
|
|
4956
|
+
const nextActionsPath = resolve23(artifactDir, "next-actions.md");
|
|
4957
|
+
if (existsSync20(nextActionsPath)) {
|
|
3659
4958
|
const nextActionsContent = await Bun.file(nextActionsPath).text();
|
|
3660
4959
|
if (nextActionsContent.includes("TODO: Replace this scaffold") || nextActionsContent.includes("bd-<downstream-task-id>")) {
|
|
3661
4960
|
localReasons.push("[Artifact Quality] next-actions.md still contains scaffold placeholder text. Replace with real recommendations.");
|
|
@@ -3668,12 +4967,6 @@ async function verifyTask(options) {
|
|
|
3668
4967
|
if (sourceCloseoutIssueId) {
|
|
3669
4968
|
localReasons.push(...evaluateGithubSourceIssuePrCloseout(options.projectRoot, prStates, sourceCloseoutIssueId));
|
|
3670
4969
|
}
|
|
3671
|
-
const pluginResults = await options.plugins.runValidators(taskId);
|
|
3672
|
-
for (const result of pluginResults) {
|
|
3673
|
-
if (!result.passed) {
|
|
3674
|
-
localReasons.push(`[Plugin Validator] ${result.id}: ${result.summary}`);
|
|
3675
|
-
}
|
|
3676
|
-
}
|
|
3677
4970
|
const reviewMode = await loadReviewMode(paths.reviewProfilePath, process.env.AI_REVIEW_MODE || "advisory");
|
|
3678
4971
|
const reviewProvider = await loadReviewProvider(paths.reviewProfilePath, process.env.AI_REVIEW_PROVIDER || "greptile");
|
|
3679
4972
|
if (!options.skipAiReview && localReasons.length === 0 && reviewProvider === "greptile" && reviewMode !== "off") {
|
|
@@ -3692,7 +4985,7 @@ async function verifyTask(options) {
|
|
|
3692
4985
|
aiReasons.push(`[AI Review] Required mode needs a completed Greptile approval; current verdict is ${ai.verdict}.`);
|
|
3693
4986
|
}
|
|
3694
4987
|
if (persistArtifacts && ai.rawResponse) {
|
|
3695
|
-
|
|
4988
|
+
writeFileSync9(greptileRawPath, `${ai.rawResponse}
|
|
3696
4989
|
`, "utf-8");
|
|
3697
4990
|
}
|
|
3698
4991
|
} else if (!options.skipAiReview && reviewMode === "off") {
|
|
@@ -4031,7 +5324,7 @@ function isAcceptedValidationSummary(summary) {
|
|
|
4031
5324
|
return summary.status === "skipped" && summary.total === 0 && summary.failed === 0;
|
|
4032
5325
|
}
|
|
4033
5326
|
async function loadReviewMode(reviewProfilePath, fallback) {
|
|
4034
|
-
const parsed =
|
|
5327
|
+
const parsed = existsSync20(reviewProfilePath) ? await readJsonFile2(reviewProfilePath) : null;
|
|
4035
5328
|
const mode = parsed?.mode;
|
|
4036
5329
|
if (mode === "off" || mode === "advisory" || mode === "required") {
|
|
4037
5330
|
return mode;
|
|
@@ -4042,7 +5335,7 @@ async function loadReviewMode(reviewProfilePath, fallback) {
|
|
|
4042
5335
|
return "advisory";
|
|
4043
5336
|
}
|
|
4044
5337
|
async function loadReviewProvider(reviewProfilePath, fallback) {
|
|
4045
|
-
const parsed =
|
|
5338
|
+
const parsed = existsSync20(reviewProfilePath) ? await readJsonFile2(reviewProfilePath) : null;
|
|
4046
5339
|
const provider = parsed?.provider;
|
|
4047
5340
|
if (typeof provider === "string" && provider.trim().length > 0) {
|
|
4048
5341
|
return provider;
|
|
@@ -4201,7 +5494,7 @@ function writeFeedbackFile(options) {
|
|
|
4201
5494
|
if (options.aiRawFeedback) {
|
|
4202
5495
|
lines.push("## Raw Reviewer Feedback", "", "```text", options.aiRawFeedback, "```", "");
|
|
4203
5496
|
}
|
|
4204
|
-
|
|
5497
|
+
writeFileSync9(options.output, `${lines.join(`
|
|
4205
5498
|
`)}
|
|
4206
5499
|
`, "utf-8");
|
|
4207
5500
|
}
|
|
@@ -4218,7 +5511,7 @@ function writeReviewStateFile(options) {
|
|
|
4218
5511
|
ai_warnings: options.aiWarnings,
|
|
4219
5512
|
updated_at: nowIso()
|
|
4220
5513
|
};
|
|
4221
|
-
|
|
5514
|
+
writeFileSync9(options.output, `${JSON.stringify(payload, null, 2)}
|
|
4222
5515
|
`, "utf-8");
|
|
4223
5516
|
}
|
|
4224
5517
|
async function runGreptileReviewForPr(options) {
|
|
@@ -4400,7 +5693,8 @@ async function runGreptileReviewForPr(options) {
|
|
|
4400
5693
|
}
|
|
4401
5694
|
};
|
|
4402
5695
|
}
|
|
4403
|
-
|
|
5696
|
+
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
5697
|
+
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)) {
|
|
4404
5698
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
4405
5699
|
return {
|
|
4406
5700
|
verdict: "REJECT",
|
|
@@ -4416,44 +5710,79 @@ async function runGreptileReviewForPr(options) {
|
|
|
4416
5710
|
}
|
|
4417
5711
|
};
|
|
4418
5712
|
}
|
|
4419
|
-
if (score) {
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
5713
|
+
if (score?.scale === 5 && score.value < 5) {
|
|
5714
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
|
|
5715
|
+
return {
|
|
5716
|
+
verdict: "REJECT",
|
|
5717
|
+
feedback,
|
|
5718
|
+
reasons,
|
|
5719
|
+
warnings,
|
|
5720
|
+
rawPayload: {
|
|
5721
|
+
pr: options.prState,
|
|
5722
|
+
codeReviews: reviewsPayload,
|
|
5723
|
+
selectedReview,
|
|
5724
|
+
reviewDetails,
|
|
5725
|
+
comments: commentsPayload,
|
|
5726
|
+
score
|
|
5727
|
+
}
|
|
5728
|
+
};
|
|
5729
|
+
}
|
|
5730
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5731
|
+
let strictGate = null;
|
|
5732
|
+
try {
|
|
5733
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5734
|
+
projectRoot: options.projectRoot,
|
|
5735
|
+
taskId: options.taskId,
|
|
5736
|
+
prUrl,
|
|
5737
|
+
apiSignals: [{
|
|
5738
|
+
id: selectedReview.id,
|
|
5739
|
+
body: reviewBody,
|
|
5740
|
+
reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
|
|
5741
|
+
status: selectedReview.status
|
|
5742
|
+
}]
|
|
5743
|
+
});
|
|
5744
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5745
|
+
} catch (error) {
|
|
5746
|
+
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5747
|
+
return {
|
|
5748
|
+
verdict: "REJECT",
|
|
5749
|
+
feedback,
|
|
5750
|
+
reasons,
|
|
5751
|
+
warnings,
|
|
5752
|
+
rawPayload: {
|
|
5753
|
+
pr: options.prState,
|
|
5754
|
+
codeReviews: reviewsPayload,
|
|
5755
|
+
selectedReview,
|
|
5756
|
+
reviewDetails,
|
|
5757
|
+
comments: commentsPayload,
|
|
5758
|
+
score
|
|
5759
|
+
}
|
|
5760
|
+
};
|
|
5761
|
+
}
|
|
5762
|
+
if (!strictGate.approved) {
|
|
5763
|
+
return {
|
|
5764
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
5765
|
+
feedback,
|
|
5766
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5767
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
5768
|
+
rawPayload: {
|
|
5769
|
+
pr: options.prState,
|
|
5770
|
+
codeReviews: reviewsPayload,
|
|
5771
|
+
selectedReview,
|
|
5772
|
+
reviewDetails,
|
|
5773
|
+
comments: commentsPayload,
|
|
5774
|
+
score,
|
|
5775
|
+
strictGate: {
|
|
5776
|
+
approved: strictGate.approved,
|
|
5777
|
+
pending: strictGate.pending,
|
|
5778
|
+
reasons: strictGate.reasons,
|
|
5779
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5780
|
+
warnings: strictGate.warnings,
|
|
5781
|
+
greptile: strictGate.evidence.greptile,
|
|
5782
|
+
readErrors: strictGate.evidence.readErrors
|
|
4451
5783
|
}
|
|
4452
|
-
}
|
|
4453
|
-
}
|
|
4454
|
-
if (score.scale === 5 && score.value < 5) {
|
|
4455
|
-
warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
|
|
4456
|
-
}
|
|
5784
|
+
}
|
|
5785
|
+
};
|
|
4457
5786
|
}
|
|
4458
5787
|
return {
|
|
4459
5788
|
verdict: "APPROVE",
|
|
@@ -4465,7 +5794,16 @@ async function runGreptileReviewForPr(options) {
|
|
|
4465
5794
|
codeReviews: reviewsPayload,
|
|
4466
5795
|
selectedReview,
|
|
4467
5796
|
reviewDetails,
|
|
4468
|
-
comments: commentsPayload
|
|
5797
|
+
comments: commentsPayload,
|
|
5798
|
+
strictGate: {
|
|
5799
|
+
approved: strictGate.approved,
|
|
5800
|
+
pending: strictGate.pending,
|
|
5801
|
+
reasons: strictGate.reasons,
|
|
5802
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5803
|
+
warnings: strictGate.warnings,
|
|
5804
|
+
greptile: strictGate.evidence.greptile,
|
|
5805
|
+
readErrors: strictGate.evidence.readErrors
|
|
5806
|
+
}
|
|
4469
5807
|
}
|
|
4470
5808
|
};
|
|
4471
5809
|
}
|
|
@@ -4489,7 +5827,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4489
5827
|
let threads = [];
|
|
4490
5828
|
let actionableThreads = [];
|
|
4491
5829
|
let checkRollup = [];
|
|
4492
|
-
let
|
|
5830
|
+
let checkState2 = { pending: false, completed: false };
|
|
4493
5831
|
for (let attempt = 0;; attempt += 1) {
|
|
4494
5832
|
reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
|
|
4495
5833
|
selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
|
|
@@ -4498,15 +5836,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4498
5836
|
threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
|
|
4499
5837
|
actionableThreads = filterActionableGithubGreptileThreads(threads);
|
|
4500
5838
|
checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
4501
|
-
|
|
4502
|
-
const
|
|
5839
|
+
checkState2 = classifyGithubGreptileCheckState(checkRollup);
|
|
5840
|
+
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4503
5841
|
if (!shouldContinueGithubGreptileFallbackPolling({
|
|
4504
5842
|
attempt,
|
|
4505
5843
|
pollAttempts: options.pollAttempts,
|
|
4506
|
-
checkState,
|
|
5844
|
+
checkState: checkState2,
|
|
4507
5845
|
fallbackReview,
|
|
4508
5846
|
selectedReview,
|
|
4509
|
-
approvedViaReviewedAncestor
|
|
5847
|
+
approvedViaReviewedAncestor
|
|
4510
5848
|
})) {
|
|
4511
5849
|
break;
|
|
4512
5850
|
}
|
|
@@ -4534,7 +5872,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4534
5872
|
].filter(Boolean).join(`
|
|
4535
5873
|
`);
|
|
4536
5874
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
4537
|
-
if (
|
|
5875
|
+
if (checkState2.pending) {
|
|
4538
5876
|
return {
|
|
4539
5877
|
verdict: "SKIP",
|
|
4540
5878
|
feedback,
|
|
@@ -4545,34 +5883,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4545
5883
|
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4546
5884
|
};
|
|
4547
5885
|
}
|
|
4548
|
-
const
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
};
|
|
4559
|
-
}
|
|
4560
|
-
return {
|
|
4561
|
-
verdict: "SKIP",
|
|
4562
|
-
feedback,
|
|
4563
|
-
reasons: [
|
|
4564
|
-
`[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
|
|
4565
|
-
],
|
|
4566
|
-
warnings,
|
|
4567
|
-
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
4568
|
-
};
|
|
4569
|
-
}
|
|
4570
|
-
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
4571
|
-
if (actionableThreads.length > 0) {
|
|
5886
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
5887
|
+
let strictGate;
|
|
5888
|
+
try {
|
|
5889
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
5890
|
+
projectRoot: options.projectRoot,
|
|
5891
|
+
taskId: options.taskId,
|
|
5892
|
+
prUrl
|
|
5893
|
+
});
|
|
5894
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
5895
|
+
} catch (error) {
|
|
4572
5896
|
return {
|
|
4573
5897
|
verdict: "REJECT",
|
|
4574
5898
|
feedback,
|
|
4575
|
-
reasons:
|
|
5899
|
+
reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
|
|
4576
5900
|
warnings,
|
|
4577
5901
|
rawPayload: {
|
|
4578
5902
|
pr: options.prState,
|
|
@@ -4585,44 +5909,31 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4585
5909
|
}
|
|
4586
5910
|
};
|
|
4587
5911
|
}
|
|
4588
|
-
if (!
|
|
4589
|
-
if (approvedViaCompletedCheck) {
|
|
4590
|
-
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.`);
|
|
4591
|
-
return {
|
|
4592
|
-
verdict: "APPROVE",
|
|
4593
|
-
feedback,
|
|
4594
|
-
reasons: [],
|
|
4595
|
-
warnings,
|
|
4596
|
-
rawPayload: {
|
|
4597
|
-
pr: options.prState,
|
|
4598
|
-
selectedReview: fallbackReview,
|
|
4599
|
-
reviews,
|
|
4600
|
-
threads,
|
|
4601
|
-
checkRollup,
|
|
4602
|
-
...buildGithubGreptileFallbackRawPayload(options)
|
|
4603
|
-
}
|
|
4604
|
-
};
|
|
4605
|
-
}
|
|
5912
|
+
if (!strictGate.approved) {
|
|
4606
5913
|
return {
|
|
4607
|
-
verdict: "SKIP",
|
|
5914
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
4608
5915
|
feedback,
|
|
4609
|
-
reasons: [
|
|
4610
|
-
|
|
4611
|
-
],
|
|
4612
|
-
warnings,
|
|
5916
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
5917
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
4613
5918
|
rawPayload: {
|
|
4614
5919
|
pr: options.prState,
|
|
4615
5920
|
selectedReview: fallbackReview,
|
|
4616
5921
|
reviews,
|
|
4617
5922
|
threads,
|
|
4618
5923
|
checkRollup,
|
|
5924
|
+
actionableThreads,
|
|
5925
|
+
strictGate: {
|
|
5926
|
+
approved: strictGate.approved,
|
|
5927
|
+
pending: strictGate.pending,
|
|
5928
|
+
reasons: strictGate.reasons,
|
|
5929
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5930
|
+
warnings: strictGate.warnings,
|
|
5931
|
+
greptile: strictGate.evidence.greptile
|
|
5932
|
+
},
|
|
4619
5933
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4620
5934
|
}
|
|
4621
5935
|
};
|
|
4622
5936
|
}
|
|
4623
|
-
if (approvedViaReviewedAncestor) {
|
|
4624
|
-
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.`);
|
|
4625
|
-
}
|
|
4626
5937
|
return {
|
|
4627
5938
|
verdict: "APPROVE",
|
|
4628
5939
|
feedback,
|
|
@@ -4634,6 +5945,14 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
4634
5945
|
reviews,
|
|
4635
5946
|
threads,
|
|
4636
5947
|
checkRollup,
|
|
5948
|
+
strictGate: {
|
|
5949
|
+
approved: strictGate.approved,
|
|
5950
|
+
pending: strictGate.pending,
|
|
5951
|
+
reasons: strictGate.reasons,
|
|
5952
|
+
reasonDetails: strictGate.reasonDetails,
|
|
5953
|
+
warnings: strictGate.warnings,
|
|
5954
|
+
greptile: strictGate.evidence.greptile
|
|
5955
|
+
},
|
|
4637
5956
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
4638
5957
|
}
|
|
4639
5958
|
};
|
|
@@ -4746,19 +6065,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
4746
6065
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
4747
6066
|
return true;
|
|
4748
6067
|
}
|
|
4749
|
-
return
|
|
6068
|
+
return false;
|
|
4750
6069
|
}
|
|
4751
6070
|
function shouldContinueGreptileMcpPolling(options) {
|
|
4752
6071
|
if (options.githubCheckState.completed) {
|
|
4753
6072
|
return false;
|
|
4754
6073
|
}
|
|
6074
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6075
|
+
return false;
|
|
6076
|
+
}
|
|
4755
6077
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
4756
6078
|
return true;
|
|
4757
6079
|
}
|
|
4758
|
-
return
|
|
6080
|
+
return true;
|
|
4759
6081
|
}
|
|
4760
6082
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
4761
6083
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
6084
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6085
|
+
return false;
|
|
6086
|
+
}
|
|
4762
6087
|
if (waitingForVisiblePendingReview) {
|
|
4763
6088
|
return true;
|
|
4764
6089
|
}
|
|
@@ -4819,6 +6144,20 @@ function runGhJson(projectRoot, args) {
|
|
|
4819
6144
|
throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
|
|
4820
6145
|
}
|
|
4821
6146
|
}
|
|
6147
|
+
async function collectStrictPrEvidenceForVerifier(input) {
|
|
6148
|
+
return collectPrReviewEvidence({
|
|
6149
|
+
projectRoot: input.projectRoot,
|
|
6150
|
+
prUrl: input.prUrl,
|
|
6151
|
+
taskId: input.taskId,
|
|
6152
|
+
runId: "verifier",
|
|
6153
|
+
cycle: 0,
|
|
6154
|
+
apiSignals: input.apiSignals ?? [],
|
|
6155
|
+
command: async (args, options) => {
|
|
6156
|
+
const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
6157
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
6158
|
+
}
|
|
6159
|
+
});
|
|
6160
|
+
}
|
|
4822
6161
|
function deriveRepoName(projectRoot, prState) {
|
|
4823
6162
|
const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
|
|
4824
6163
|
if (fromUrl?.[1]) {
|
|
@@ -4833,8 +6172,9 @@ function resolvePrHeadSha(projectRoot, prState) {
|
|
|
4833
6172
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
4834
6173
|
return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
4835
6174
|
}
|
|
4836
|
-
function
|
|
4837
|
-
|
|
6175
|
+
function isGreptileGithubLogin2(login) {
|
|
6176
|
+
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
6177
|
+
return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
|
|
4838
6178
|
}
|
|
4839
6179
|
function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
|
|
4840
6180
|
const matching = sortGithubGreptileReviews(reviews);
|
|
@@ -4851,7 +6191,7 @@ function pickLatestGithubGreptileReview(reviews) {
|
|
|
4851
6191
|
return sortGithubGreptileReviews(reviews)[0] || null;
|
|
4852
6192
|
}
|
|
4853
6193
|
function sortGithubGreptileReviews(reviews) {
|
|
4854
|
-
return reviews.filter((review) =>
|
|
6194
|
+
return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
|
|
4855
6195
|
}
|
|
4856
6196
|
function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
4857
6197
|
const response = runGhJson(projectRoot, [
|
|
@@ -4924,32 +6264,6 @@ function classifyGithubGreptileCheckState(checks) {
|
|
|
4924
6264
|
}
|
|
4925
6265
|
return { pending: false, completed: false };
|
|
4926
6266
|
}
|
|
4927
|
-
function isGithubGreptileCheckApproved(checks) {
|
|
4928
|
-
const greptileChecks = checks.filter((check) => {
|
|
4929
|
-
const label = (check.name || check.context || "").toLowerCase();
|
|
4930
|
-
return label.includes("greptile");
|
|
4931
|
-
});
|
|
4932
|
-
if (greptileChecks.length === 0) {
|
|
4933
|
-
return false;
|
|
4934
|
-
}
|
|
4935
|
-
for (const check of greptileChecks) {
|
|
4936
|
-
if ((check.__typename || "") === "CheckRun") {
|
|
4937
|
-
if ((check.status || "").toUpperCase() !== "COMPLETED") {
|
|
4938
|
-
return false;
|
|
4939
|
-
}
|
|
4940
|
-
const conclusion = (check.conclusion || "").toUpperCase();
|
|
4941
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
|
|
4942
|
-
return false;
|
|
4943
|
-
}
|
|
4944
|
-
continue;
|
|
4945
|
-
}
|
|
4946
|
-
const state = (check.state || "").toUpperCase();
|
|
4947
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
|
|
4948
|
-
return false;
|
|
4949
|
-
}
|
|
4950
|
-
}
|
|
4951
|
-
return true;
|
|
4952
|
-
}
|
|
4953
6267
|
function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
|
|
4954
6268
|
const [owner, name] = repoName.split("/");
|
|
4955
6269
|
if (!owner || !name) {
|
|
@@ -4975,7 +6289,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
4975
6289
|
return [];
|
|
4976
6290
|
}
|
|
4977
6291
|
const comments = thread.comments?.nodes || [];
|
|
4978
|
-
const latestGreptileComment = [...comments].reverse().find((comment) =>
|
|
6292
|
+
const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
|
|
4979
6293
|
if (!latestGreptileComment?.path?.trim()) {
|
|
4980
6294
|
return [];
|
|
4981
6295
|
}
|
|
@@ -4984,7 +6298,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
4984
6298
|
}
|
|
4985
6299
|
function resolvePrRepoRoot(projectRoot, prState) {
|
|
4986
6300
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
4987
|
-
if (prState.target === "monorepo" && runtimeWorkspace &&
|
|
6301
|
+
if (prState.target === "monorepo" && runtimeWorkspace && existsSync20(resolve23(runtimeWorkspace, ".git"))) {
|
|
4988
6302
|
return runtimeWorkspace;
|
|
4989
6303
|
}
|
|
4990
6304
|
const paths = resolveHarnessPaths(projectRoot);
|
|
@@ -4997,11 +6311,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
4997
6311
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
4998
6312
|
return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
4999
6313
|
}
|
|
5000
|
-
function stripHtml(input) {
|
|
5001
|
-
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
5002
|
-
|
|
5003
|
-
`).trim();
|
|
5004
|
-
}
|
|
5005
6314
|
function summarizeComment(input) {
|
|
5006
6315
|
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
5007
6316
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
@@ -5010,31 +6319,14 @@ function asGreptileInfrastructureWarning(reason) {
|
|
|
5010
6319
|
return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
|
|
5011
6320
|
}
|
|
5012
6321
|
function isAiReviewApproved(input) {
|
|
6322
|
+
if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
|
|
6323
|
+
return false;
|
|
6324
|
+
}
|
|
5013
6325
|
if (input.reviewMode !== "required") {
|
|
5014
6326
|
return true;
|
|
5015
6327
|
}
|
|
5016
6328
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
5017
6329
|
}
|
|
5018
|
-
function parseGreptileScore(input) {
|
|
5019
|
-
const text = stripHtml(input);
|
|
5020
|
-
const patterns = [
|
|
5021
|
-
/confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5022
|
-
/\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5023
|
-
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
|
|
5024
|
-
];
|
|
5025
|
-
for (const pattern of patterns) {
|
|
5026
|
-
const match = pattern.exec(text);
|
|
5027
|
-
if (!match) {
|
|
5028
|
-
continue;
|
|
5029
|
-
}
|
|
5030
|
-
const value = Number.parseInt(match[1] || "", 10);
|
|
5031
|
-
const scale = Number.parseInt(match[2] || "", 10);
|
|
5032
|
-
if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
|
|
5033
|
-
return { value, scale };
|
|
5034
|
-
}
|
|
5035
|
-
}
|
|
5036
|
-
return null;
|
|
5037
|
-
}
|
|
5038
6330
|
|
|
5039
6331
|
// packages/runtime/src/control-plane/provider/runtime-instructions.ts
|
|
5040
6332
|
var CLAUDE_ROUTER_TOOL_NAMES = [
|
|
@@ -5378,16 +6670,16 @@ async function taskDeps(projectRoot, taskId) {
|
|
|
5378
6670
|
for (const dep of deps) {
|
|
5379
6671
|
const artifactDir = artifactDirForId(projectRoot, dep);
|
|
5380
6672
|
console.log(`=== ${dep} ===`);
|
|
5381
|
-
if (!
|
|
6673
|
+
if (!existsSync21(artifactDir)) {
|
|
5382
6674
|
console.log(` (no artifacts yet)
|
|
5383
6675
|
`);
|
|
5384
6676
|
continue;
|
|
5385
6677
|
}
|
|
5386
|
-
printArtifactSection(
|
|
5387
|
-
printArtifactSection(
|
|
5388
|
-
const changedFiles =
|
|
5389
|
-
if (
|
|
5390
|
-
const lines =
|
|
6678
|
+
printArtifactSection(resolve24(artifactDir, "decision-log.md"), "--- Decisions ---");
|
|
6679
|
+
printArtifactSection(resolve24(artifactDir, "next-actions.md"), "--- Next Actions (for you) ---");
|
|
6680
|
+
const changedFiles = resolve24(artifactDir, "changed-files.txt");
|
|
6681
|
+
if (existsSync21(changedFiles)) {
|
|
6682
|
+
const lines = readFileSync11(changedFiles, "utf-8").split(/\r?\n/).filter(Boolean);
|
|
5391
6683
|
console.log(`--- Changed Files (${lines.length}) ---`);
|
|
5392
6684
|
for (const line of lines) {
|
|
5393
6685
|
console.log(line);
|
|
@@ -5432,12 +6724,12 @@ function taskRecord(projectRoot, type, text, taskId) {
|
|
|
5432
6724
|
throw new Error("No active task.");
|
|
5433
6725
|
}
|
|
5434
6726
|
const paths = resolveHarnessPaths(projectRoot);
|
|
5435
|
-
|
|
6727
|
+
mkdirSync10(paths.stateDir, { recursive: true });
|
|
5436
6728
|
if (type === "decision") {
|
|
5437
|
-
const artifactDir =
|
|
5438
|
-
|
|
6729
|
+
const artifactDir = resolve24(paths.artifactsDir, activeTask);
|
|
6730
|
+
mkdirSync10(artifactDir, { recursive: true });
|
|
5439
6731
|
const timestamp = nowIso();
|
|
5440
|
-
appendFileSync(
|
|
6732
|
+
appendFileSync(resolve24(artifactDir, "decision-log.md"), `
|
|
5441
6733
|
### ${timestamp}
|
|
5442
6734
|
|
|
5443
6735
|
${text}
|
|
@@ -5447,15 +6739,15 @@ ${text}
|
|
|
5447
6739
|
return;
|
|
5448
6740
|
}
|
|
5449
6741
|
const failedPath = paths.failedApproachesPath;
|
|
5450
|
-
if (!
|
|
5451
|
-
|
|
6742
|
+
if (!existsSync21(failedPath)) {
|
|
6743
|
+
writeFileSync10(failedPath, `# Failed Approaches Log
|
|
5452
6744
|
|
|
5453
6745
|
This file records approaches that did not work.
|
|
5454
6746
|
|
|
5455
6747
|
`, "utf-8");
|
|
5456
6748
|
}
|
|
5457
|
-
const content =
|
|
5458
|
-
const attempts = (content.match(new RegExp(`^## ${
|
|
6749
|
+
const content = readFileSync11(failedPath, "utf-8");
|
|
6750
|
+
const attempts = (content.match(new RegExp(`^## ${escapeRegExp(activeTask)}\\b`, "gm")) || []).length + 1;
|
|
5459
6751
|
appendFileSync(failedPath, `
|
|
5460
6752
|
## ${activeTask} - Attempt ${attempts} (${nowIso()})
|
|
5461
6753
|
|
|
@@ -5471,40 +6763,40 @@ function taskArtifacts(projectRoot, taskId) {
|
|
|
5471
6763
|
throw new Error("No active task.");
|
|
5472
6764
|
}
|
|
5473
6765
|
const paths = resolveHarnessPaths(projectRoot);
|
|
5474
|
-
const artifactDir =
|
|
5475
|
-
|
|
6766
|
+
const artifactDir = resolve24(paths.artifactsDir, activeTask);
|
|
6767
|
+
mkdirSync10(artifactDir, { recursive: true });
|
|
5476
6768
|
const changed = changedFilesForTask(projectRoot, activeTask, true);
|
|
5477
|
-
|
|
6769
|
+
writeFileSync10(resolve24(artifactDir, "changed-files.txt"), `${changed.join(`
|
|
5478
6770
|
`)}
|
|
5479
6771
|
`, "utf-8");
|
|
5480
6772
|
console.log(`changed-files.txt: ${changed.length} files`);
|
|
5481
|
-
const taskResultPath =
|
|
5482
|
-
if (!
|
|
6773
|
+
const taskResultPath = resolve24(artifactDir, "task-result.json");
|
|
6774
|
+
if (!existsSync21(taskResultPath)) {
|
|
5483
6775
|
const template = {
|
|
5484
6776
|
task_id: activeTask,
|
|
5485
6777
|
status: "completed",
|
|
5486
6778
|
summary: "TODO: Write a one-line summary of what you did",
|
|
5487
6779
|
completed_at: nowIso()
|
|
5488
6780
|
};
|
|
5489
|
-
|
|
6781
|
+
writeFileSync10(taskResultPath, `${JSON.stringify(template, null, 2)}
|
|
5490
6782
|
`, "utf-8");
|
|
5491
6783
|
console.log("task-result.json: created (update the summary!)");
|
|
5492
6784
|
} else {
|
|
5493
6785
|
console.log("task-result.json: already exists");
|
|
5494
6786
|
}
|
|
5495
|
-
const decisionLogPath =
|
|
5496
|
-
if (!
|
|
6787
|
+
const decisionLogPath = resolve24(artifactDir, "decision-log.md");
|
|
6788
|
+
if (!existsSync21(decisionLogPath)) {
|
|
5497
6789
|
const content = `# Decision Log: ${activeTask}
|
|
5498
6790
|
|
|
5499
6791
|
Record key decisions here using: rig-agent record decision "..."
|
|
5500
6792
|
`;
|
|
5501
|
-
|
|
6793
|
+
writeFileSync10(decisionLogPath, content, "utf-8");
|
|
5502
6794
|
console.log("decision-log.md: created (record your decisions!)");
|
|
5503
6795
|
} else {
|
|
5504
6796
|
console.log("decision-log.md: already exists");
|
|
5505
6797
|
}
|
|
5506
|
-
const nextActionsPath =
|
|
5507
|
-
if (!
|
|
6798
|
+
const nextActionsPath = resolve24(artifactDir, "next-actions.md");
|
|
6799
|
+
if (!existsSync21(nextActionsPath)) {
|
|
5508
6800
|
const content = [
|
|
5509
6801
|
`# Next Actions: ${activeTask}`,
|
|
5510
6802
|
"",
|
|
@@ -5521,13 +6813,13 @@ Record key decisions here using: rig-agent record decision "..."
|
|
|
5521
6813
|
""
|
|
5522
6814
|
].join(`
|
|
5523
6815
|
`);
|
|
5524
|
-
|
|
6816
|
+
writeFileSync10(nextActionsPath, content, "utf-8");
|
|
5525
6817
|
console.log("next-actions.md: created (add recommendations for downstream tasks!)");
|
|
5526
6818
|
} else {
|
|
5527
6819
|
console.log("next-actions.md: already exists");
|
|
5528
6820
|
}
|
|
5529
|
-
const validationSummaryPath =
|
|
5530
|
-
if (
|
|
6821
|
+
const validationSummaryPath = resolve24(artifactDir, "validation-summary.json");
|
|
6822
|
+
if (existsSync21(validationSummaryPath)) {
|
|
5531
6823
|
console.log("validation-summary.json: already exists");
|
|
5532
6824
|
} else {
|
|
5533
6825
|
console.log("validation-summary.json: not yet created (run: rig-agent validate)");
|
|
@@ -5570,12 +6862,12 @@ async function taskValidate(projectRoot, taskId, validatorRegistry) {
|
|
|
5570
6862
|
console.log(`Validation passed: ${summary.passed}/${summary.total}`);
|
|
5571
6863
|
return true;
|
|
5572
6864
|
}
|
|
5573
|
-
async function taskVerify(projectRoot,
|
|
6865
|
+
async function taskVerify(projectRoot, taskId) {
|
|
5574
6866
|
const activeTask = taskId || currentTaskId(projectRoot);
|
|
5575
6867
|
if (!activeTask) {
|
|
5576
6868
|
throw new Error("No active task.");
|
|
5577
6869
|
}
|
|
5578
|
-
const outcome = await verifyTask({ projectRoot, taskId: activeTask
|
|
6870
|
+
const outcome = await verifyTask({ projectRoot, taskId: activeTask });
|
|
5579
6871
|
if (!outcome.approved) {
|
|
5580
6872
|
console.log("REJECT:");
|
|
5581
6873
|
for (const reason of outcome.localReasons) {
|
|
@@ -5618,7 +6910,7 @@ function collectTaskChangedFiles(projectRoot, taskId, includeCommitted) {
|
|
|
5618
6910
|
[projectRoot, ""],
|
|
5619
6911
|
[monorepoRepoRoot, ""]
|
|
5620
6912
|
]) {
|
|
5621
|
-
if (!
|
|
6913
|
+
if (!existsSync21(resolve24(repo, ".git"))) {
|
|
5622
6914
|
continue;
|
|
5623
6915
|
}
|
|
5624
6916
|
if (includeCommitted && repo === monorepoRepoRoot) {
|
|
@@ -5656,8 +6948,8 @@ function filterTaskChangedFiles(projectRoot, taskId, files, scoped) {
|
|
|
5656
6948
|
}
|
|
5657
6949
|
function resolveTaskMonorepoRoot(projectRoot) {
|
|
5658
6950
|
const runtimeWorkspace = loadRuntimeContextFromEnv()?.workspaceDir || process.env.RIG_TASK_WORKSPACE?.trim();
|
|
5659
|
-
if (runtimeWorkspace &&
|
|
5660
|
-
return
|
|
6951
|
+
if (runtimeWorkspace && existsSync21(resolve24(runtimeWorkspace, ".git"))) {
|
|
6952
|
+
return resolve24(runtimeWorkspace);
|
|
5661
6953
|
}
|
|
5662
6954
|
return resolveHarnessPaths(projectRoot).monorepoRoot;
|
|
5663
6955
|
}
|
|
@@ -5685,7 +6977,7 @@ function resolveRuntimeInitialHeadCommit(projectRoot, repo) {
|
|
|
5685
6977
|
const runtimeContext = loadRuntimeContextFromEnv();
|
|
5686
6978
|
if (runtimeContext?.initialHeadCommits?.monorepo?.trim()) {
|
|
5687
6979
|
const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
|
|
5688
|
-
if (
|
|
6980
|
+
if (resolve24(monorepoRoot) === resolve24(repo)) {
|
|
5689
6981
|
return runtimeContext.initialHeadCommits.monorepo.trim();
|
|
5690
6982
|
}
|
|
5691
6983
|
}
|
|
@@ -5695,7 +6987,7 @@ function resolveMonorepoBaseCommit(projectRoot, repo) {
|
|
|
5695
6987
|
const runtimeContext = loadRuntimeContextFromEnv();
|
|
5696
6988
|
if (runtimeContext?.monorepoBaseCommit?.trim()) {
|
|
5697
6989
|
const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
|
|
5698
|
-
if (
|
|
6990
|
+
if (resolve24(monorepoRoot) === resolve24(repo)) {
|
|
5699
6991
|
return runtimeContext.monorepoBaseCommit.trim();
|
|
5700
6992
|
}
|
|
5701
6993
|
}
|
|
@@ -5729,7 +7021,7 @@ function resolveRuntimeDirtyBaseline(projectRoot, repo) {
|
|
|
5729
7021
|
return new Set;
|
|
5730
7022
|
}
|
|
5731
7023
|
const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
|
|
5732
|
-
const selected =
|
|
7024
|
+
const selected = resolve24(repo) === resolve24(monorepoRoot) ? dirtyFiles.monorepo : resolve24(repo) === resolve24(projectRoot) ? dirtyFiles.project : undefined;
|
|
5733
7025
|
return new Set((selected || []).map((file) => normalizeChangedFilePath(file)).filter(Boolean));
|
|
5734
7026
|
}
|
|
5735
7027
|
function normalizeChangedFilePath(file) {
|
|
@@ -5829,12 +7121,12 @@ function printIndented(text) {
|
|
|
5829
7121
|
}
|
|
5830
7122
|
}
|
|
5831
7123
|
function readLocalBeadsTasks(projectRoot) {
|
|
5832
|
-
const issuesPath =
|
|
5833
|
-
if (!
|
|
7124
|
+
const issuesPath = resolve24(resolveMonorepoRoot2(projectRoot), ".beads/issues.jsonl");
|
|
7125
|
+
if (!existsSync21(issuesPath)) {
|
|
5834
7126
|
return [];
|
|
5835
7127
|
}
|
|
5836
7128
|
const tasks = [];
|
|
5837
|
-
for (const line of
|
|
7129
|
+
for (const line of readFileSync11(issuesPath, "utf-8").split(/\r?\n/)) {
|
|
5838
7130
|
const trimmed = line.trim();
|
|
5839
7131
|
if (!trimmed) {
|
|
5840
7132
|
continue;
|
|
@@ -5947,14 +7239,14 @@ function taskDependencies(projectRoot, taskId, tracker) {
|
|
|
5947
7239
|
return [...ids].sort();
|
|
5948
7240
|
}
|
|
5949
7241
|
function printArtifactSection(path, header) {
|
|
5950
|
-
if (!
|
|
7242
|
+
if (!existsSync21(path)) {
|
|
5951
7243
|
return;
|
|
5952
7244
|
}
|
|
5953
7245
|
console.log(header);
|
|
5954
|
-
process.stdout.write(
|
|
7246
|
+
process.stdout.write(readFileSync11(path, "utf-8"));
|
|
5955
7247
|
console.log("");
|
|
5956
7248
|
}
|
|
5957
|
-
function
|
|
7249
|
+
function escapeRegExp(value) {
|
|
5958
7250
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5959
7251
|
}
|
|
5960
7252
|
function changedFilesForTask(projectRoot, taskId, scoped) {
|
|
@@ -5983,16 +7275,16 @@ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
|
5983
7275
|
"task-result.json",
|
|
5984
7276
|
"validation-summary.json"
|
|
5985
7277
|
]);
|
|
5986
|
-
function resolveHostRigBinDir(root) {
|
|
5987
|
-
return resolve24(root, ".rig", "bin");
|
|
5988
|
-
}
|
|
5989
7278
|
function isRuntimeGatewayGitPath(candidate) {
|
|
5990
7279
|
return /\/\.rig\/bin\/git$/.test(candidate.replace(/\\/g, "/"));
|
|
5991
7280
|
}
|
|
7281
|
+
function isRuntimeGatewayGhPath(candidate) {
|
|
7282
|
+
return /\/\.rig\/bin\/gh$/.test(candidate.replace(/\\/g, "/"));
|
|
7283
|
+
}
|
|
5992
7284
|
function resolveOptionalMonorepoRoot(projectRoot) {
|
|
5993
7285
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
5994
|
-
if (runtimeWorkspace &&
|
|
5995
|
-
return
|
|
7286
|
+
if (runtimeWorkspace && existsSync22(resolve25(runtimeWorkspace, ".git"))) {
|
|
7287
|
+
return resolve25(runtimeWorkspace);
|
|
5996
7288
|
}
|
|
5997
7289
|
try {
|
|
5998
7290
|
return resolveMonorepoRoot2(projectRoot);
|
|
@@ -6017,12 +7309,15 @@ function resolveGitBinary(projectRoot) {
|
|
|
6017
7309
|
if (!candidate || isRuntimeGatewayGitPath(candidate)) {
|
|
6018
7310
|
continue;
|
|
6019
7311
|
}
|
|
6020
|
-
if (
|
|
7312
|
+
if (existsSync22(candidate)) {
|
|
6021
7313
|
return candidate;
|
|
6022
7314
|
}
|
|
6023
7315
|
}
|
|
6024
7316
|
return "git";
|
|
6025
7317
|
}
|
|
7318
|
+
function escapeRegExp2(value) {
|
|
7319
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7320
|
+
}
|
|
6026
7321
|
function safeCurrentTaskId(projectRoot) {
|
|
6027
7322
|
try {
|
|
6028
7323
|
const taskId = currentTaskId(projectRoot);
|
|
@@ -6074,11 +7369,11 @@ function gitPreflight(projectRoot, taskId, strict) {
|
|
|
6074
7369
|
const expected = resolvedTask ? `rig/${resolveTaskBranchId(projectRoot, resolvedTask)}` : "";
|
|
6075
7370
|
console.log("=== Git Flow Preflight ===");
|
|
6076
7371
|
let issues = 0;
|
|
6077
|
-
if (!
|
|
7372
|
+
if (!existsSync22(resolve25(projectRoot, ".git"))) {
|
|
6078
7373
|
console.log(`ERROR: project root is not a git repo (${projectRoot})`);
|
|
6079
7374
|
issues += 1;
|
|
6080
7375
|
}
|
|
6081
|
-
if (monorepoRoot &&
|
|
7376
|
+
if (monorepoRoot && existsSync22(resolve25(monorepoRoot, ".git"))) {
|
|
6082
7377
|
const monoBranch = branchName(projectRoot, monorepoRoot);
|
|
6083
7378
|
if (expected && monoBranch !== expected) {
|
|
6084
7379
|
console.log(`WARN: monorepo branch is ${monoBranch}, expected ${expected} for task ${resolvedTask}`);
|
|
@@ -6112,7 +7407,7 @@ function gitSyncBranch(projectRoot, taskId, targetRepo = "monorepo") {
|
|
|
6112
7407
|
}
|
|
6113
7408
|
const repoRoot = targetRepo === "monorepo" ? resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot2(projectRoot) : projectRoot;
|
|
6114
7409
|
const repoLabel = targetRepo === "monorepo" ? "Monorepo" : "Project";
|
|
6115
|
-
if (!
|
|
7410
|
+
if (!existsSync22(resolve25(repoRoot, ".git"))) {
|
|
6116
7411
|
throw new Error(`${repoLabel} repo not found at ${repoRoot}`);
|
|
6117
7412
|
}
|
|
6118
7413
|
const branchId = resolveTaskBranchId(projectRoot, resolvedTask);
|
|
@@ -6156,8 +7451,8 @@ function gitCommit(options) {
|
|
|
6156
7451
|
function gitSnapshot(projectRoot, taskId, outputPath) {
|
|
6157
7452
|
const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot);
|
|
6158
7453
|
const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
|
|
6159
|
-
const output = outputPath || (resolvedTask ? resolveArtifactSnapshot(projectRoot, resolvedTask) :
|
|
6160
|
-
|
|
7454
|
+
const output = outputPath || (resolvedTask ? resolveArtifactSnapshot(projectRoot, resolvedTask) : resolve25(resolve25(projectRoot, ".rig", "state"), "git-state.txt"));
|
|
7455
|
+
mkdirSync11(dirname11(output), { recursive: true });
|
|
6161
7456
|
const lines = ["# Git Snapshot", `timestamp: ${nowIso()}`];
|
|
6162
7457
|
if (resolvedTask) {
|
|
6163
7458
|
lines.push(`task: ${resolvedTask}`);
|
|
@@ -6167,7 +7462,7 @@ function gitSnapshot(projectRoot, taskId, outputPath) {
|
|
|
6167
7462
|
if (monorepoRoot && monorepoRoot !== projectRoot) {
|
|
6168
7463
|
lines.push(...snapshotRepo(projectRoot, "monorepo", monorepoRoot));
|
|
6169
7464
|
}
|
|
6170
|
-
|
|
7465
|
+
writeFileSync11(output, `${lines.join(`
|
|
6171
7466
|
`)}
|
|
6172
7467
|
`, "utf-8");
|
|
6173
7468
|
return output;
|
|
@@ -6191,7 +7486,7 @@ function gitOpenPr(options) {
|
|
|
6191
7486
|
} else if (taskId) {
|
|
6192
7487
|
gitSyncBranch(options.projectRoot, taskId, "project");
|
|
6193
7488
|
}
|
|
6194
|
-
if (!
|
|
7489
|
+
if (!existsSync22(resolve25(repoRoot, ".git"))) {
|
|
6195
7490
|
throw new Error(`Repository not available for open-pr target ${target}: ${repoRoot}`);
|
|
6196
7491
|
}
|
|
6197
7492
|
const branch = branchName(options.projectRoot, repoRoot);
|
|
@@ -6236,10 +7531,11 @@ function gitOpenPr(options) {
|
|
|
6236
7531
|
"",
|
|
6237
7532
|
"## Task",
|
|
6238
7533
|
`- beads: ${taskId || "n/a"}`,
|
|
7534
|
+
...defaultPrRunLines(taskId, repoNameWithOwner),
|
|
6239
7535
|
"",
|
|
6240
7536
|
"## Review",
|
|
6241
7537
|
"- Completion verification will run validation, verifier review, and PR policy checks.",
|
|
6242
|
-
"- When repository policy allows it, Rig
|
|
7538
|
+
"- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
|
|
6243
7539
|
].join(`
|
|
6244
7540
|
`);
|
|
6245
7541
|
const preCheck = runCapture2(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
|
|
@@ -6327,6 +7623,30 @@ function gitOpenPr(options) {
|
|
|
6327
7623
|
}
|
|
6328
7624
|
return result;
|
|
6329
7625
|
}
|
|
7626
|
+
function defaultPrRunLines(taskId, repoNameWithOwner) {
|
|
7627
|
+
const lines = [];
|
|
7628
|
+
const runId = process.env.RIG_SERVER_RUN_ID?.trim();
|
|
7629
|
+
if (runId) {
|
|
7630
|
+
lines.push(`- Run: ${runId}`);
|
|
7631
|
+
}
|
|
7632
|
+
const closeout = defaultPrCloseoutLine(taskId, repoNameWithOwner);
|
|
7633
|
+
if (closeout) {
|
|
7634
|
+
lines.push(`- ${closeout}`);
|
|
7635
|
+
}
|
|
7636
|
+
return lines;
|
|
7637
|
+
}
|
|
7638
|
+
function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
|
|
7639
|
+
const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
|
|
7640
|
+
if (sourceIssueId) {
|
|
7641
|
+
const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
|
|
7642
|
+
if (match?.[1] && match[2]) {
|
|
7643
|
+
const sourceRepo = match[1];
|
|
7644
|
+
const issueNumber = match[2];
|
|
7645
|
+
return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
|
|
7646
|
+
}
|
|
7647
|
+
}
|
|
7648
|
+
return /^\d+$/.test(taskId) ? `Closes #${taskId}` : "";
|
|
7649
|
+
}
|
|
6330
7650
|
function readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl) {
|
|
6331
7651
|
const view = runCapture2(withGhRepo([
|
|
6332
7652
|
gh,
|
|
@@ -6379,12 +7699,12 @@ function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
|
|
|
6379
7699
|
}
|
|
6380
7700
|
function writePrMetadata(projectRoot, taskId, result) {
|
|
6381
7701
|
const dir = artifactDirForId(projectRoot, taskId);
|
|
6382
|
-
|
|
6383
|
-
const path =
|
|
7702
|
+
mkdirSync11(dir, { recursive: true });
|
|
7703
|
+
const path = resolve25(dir, "pr-state.json");
|
|
6384
7704
|
let prs = {};
|
|
6385
|
-
if (
|
|
7705
|
+
if (existsSync22(path)) {
|
|
6386
7706
|
try {
|
|
6387
|
-
const parsed = JSON.parse(
|
|
7707
|
+
const parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
6388
7708
|
if (parsed && typeof parsed === "object" && parsed.prs && typeof parsed.prs === "object") {
|
|
6389
7709
|
prs = parsed.prs;
|
|
6390
7710
|
}
|
|
@@ -6400,16 +7720,16 @@ function writePrMetadata(projectRoot, taskId, result) {
|
|
|
6400
7720
|
...primary || {},
|
|
6401
7721
|
updated_at: nowIso()
|
|
6402
7722
|
};
|
|
6403
|
-
|
|
7723
|
+
writeFileSync11(path, `${JSON.stringify(artifact, null, 2)}
|
|
6404
7724
|
`, "utf-8");
|
|
6405
7725
|
}
|
|
6406
7726
|
function readPrMetadata(projectRoot, taskId) {
|
|
6407
|
-
const path =
|
|
6408
|
-
if (!
|
|
7727
|
+
const path = resolve25(artifactDirForId(projectRoot, taskId), "pr-state.json");
|
|
7728
|
+
if (!existsSync22(path)) {
|
|
6409
7729
|
return [];
|
|
6410
7730
|
}
|
|
6411
7731
|
try {
|
|
6412
|
-
const parsed = JSON.parse(
|
|
7732
|
+
const parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
6413
7733
|
if (!parsed || typeof parsed !== "object") {
|
|
6414
7734
|
return [];
|
|
6415
7735
|
}
|
|
@@ -6422,8 +7742,8 @@ function readPrMetadata(projectRoot, taskId) {
|
|
|
6422
7742
|
}
|
|
6423
7743
|
}
|
|
6424
7744
|
function resolveArtifactSnapshot(projectRoot, taskId) {
|
|
6425
|
-
const artifactDir =
|
|
6426
|
-
return
|
|
7745
|
+
const artifactDir = resolve25(resolveHarnessPaths(projectRoot).artifactsDir, taskId);
|
|
7746
|
+
return resolve25(artifactDir, "git-state.txt");
|
|
6427
7747
|
}
|
|
6428
7748
|
function isGitOpenPrResult(value) {
|
|
6429
7749
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -6477,32 +7797,19 @@ function resolveGithubCliBinary(projectRoot) {
|
|
|
6477
7797
|
if (explicit) {
|
|
6478
7798
|
candidates.add(explicit);
|
|
6479
7799
|
}
|
|
7800
|
+
for (const candidate of ["/usr/bin/gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh"]) {
|
|
7801
|
+
candidates.add(candidate);
|
|
7802
|
+
}
|
|
6480
7803
|
const explicitPathEntries = (process.env.PATH || "").split(":").map((entry) => entry.trim()).filter(Boolean);
|
|
6481
7804
|
for (const entry of explicitPathEntries) {
|
|
6482
|
-
candidates.add(
|
|
6483
|
-
}
|
|
6484
|
-
const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim();
|
|
6485
|
-
if (hostProjectRoot) {
|
|
6486
|
-
candidates.add(resolve24(resolveHostRigBinDir(hostProjectRoot), "gh"));
|
|
6487
|
-
}
|
|
6488
|
-
candidates.add(resolve24(resolveHostRigBinDir(projectRoot), "gh"));
|
|
6489
|
-
const runtimeContext = loadRuntimeContextFromEnv();
|
|
6490
|
-
if (runtimeContext?.binDir) {
|
|
6491
|
-
candidates.add(resolve24(runtimeContext.binDir, "gh"));
|
|
6492
|
-
}
|
|
6493
|
-
const runtimeHome = process.env.RIG_RUNTIME_HOME?.trim();
|
|
6494
|
-
if (runtimeHome) {
|
|
6495
|
-
candidates.add(resolve24(runtimeHome, "bin", "gh"));
|
|
6496
|
-
}
|
|
6497
|
-
for (const candidate of ["/opt/homebrew/bin/gh", "/usr/local/bin/gh", "/usr/bin/gh"]) {
|
|
6498
|
-
candidates.add(candidate);
|
|
7805
|
+
candidates.add(resolve25(entry, "gh"));
|
|
6499
7806
|
}
|
|
6500
7807
|
const bunResolved = Bun.which("gh");
|
|
6501
7808
|
if (bunResolved) {
|
|
6502
7809
|
candidates.add(bunResolved);
|
|
6503
7810
|
}
|
|
6504
7811
|
for (const candidate of candidates) {
|
|
6505
|
-
if (candidate &&
|
|
7812
|
+
if (candidate && existsSync22(candidate) && !isRuntimeGatewayGhPath(candidate)) {
|
|
6506
7813
|
return candidate;
|
|
6507
7814
|
}
|
|
6508
7815
|
}
|
|
@@ -6532,7 +7839,7 @@ function resolveRepoNameWithOwner(projectRoot, repoRoot) {
|
|
|
6532
7839
|
return resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, repoRoot, repoRoot, visited);
|
|
6533
7840
|
}
|
|
6534
7841
|
function resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, gitRoot, cwd, visited) {
|
|
6535
|
-
const normalizedGitRoot =
|
|
7842
|
+
const normalizedGitRoot = resolve25(gitRoot);
|
|
6536
7843
|
if (visited.has(normalizedGitRoot)) {
|
|
6537
7844
|
return "";
|
|
6538
7845
|
}
|
|
@@ -6604,7 +7911,7 @@ function resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner) {
|
|
|
6604
7911
|
return remotes.includes("origin") ? "origin" : remotes[0];
|
|
6605
7912
|
}
|
|
6606
7913
|
function gitQuery(projectRoot, gitRoot, cwd, ...args) {
|
|
6607
|
-
const gitArgs =
|
|
7914
|
+
const gitArgs = existsSync22(resolve25(gitRoot, ".git")) ? gitCmd(projectRoot, gitRoot, ...args) : [resolveGitBinary(projectRoot), "--git-dir", gitRoot, ...args];
|
|
6608
7915
|
return runCapture2(gitArgs, cwd, projectRoot);
|
|
6609
7916
|
}
|
|
6610
7917
|
function resolveLocalGitRemoteRoot(remoteUrl, gitRoot) {
|
|
@@ -6622,9 +7929,9 @@ function resolveLocalGitRemoteRoot(remoteUrl, gitRoot) {
|
|
|
6622
7929
|
} else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /^[^@]+@[^:]+:.+$/.test(normalized)) {
|
|
6623
7930
|
return "";
|
|
6624
7931
|
} else if (!isAbsolute2(normalized)) {
|
|
6625
|
-
candidate =
|
|
7932
|
+
candidate = resolve25(gitRoot, normalized);
|
|
6626
7933
|
}
|
|
6627
|
-
return
|
|
7934
|
+
return existsSync22(candidate) ? candidate : "";
|
|
6628
7935
|
}
|
|
6629
7936
|
function normalizeGithubRepoNameWithOwner(value) {
|
|
6630
7937
|
const normalized = value.trim();
|
|
@@ -6655,7 +7962,7 @@ function inferRepositoryDefaultBase(projectRoot, repoRoot, repoNameWithOwner, re
|
|
|
6655
7962
|
const remote = remoteName || "origin";
|
|
6656
7963
|
const symbolic = runCapture2(gitCmd(projectRoot, repoRoot, "symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`), projectRoot);
|
|
6657
7964
|
if (symbolic.exitCode === 0) {
|
|
6658
|
-
const ref = symbolic.stdout.trim().replace(new RegExp(`^${
|
|
7965
|
+
const ref = symbolic.stdout.trim().replace(new RegExp(`^${escapeRegExp2(remote)}/`), "");
|
|
6659
7966
|
if (ref && ref !== "HEAD") {
|
|
6660
7967
|
return ref;
|
|
6661
7968
|
}
|
|
@@ -6751,7 +8058,7 @@ function inferReviewerFromChangedFiles(projectRoot, repoRoot, baseRef, branchRef
|
|
|
6751
8058
|
return best;
|
|
6752
8059
|
}
|
|
6753
8060
|
function snapshotRepo(projectRoot, label, repo) {
|
|
6754
|
-
if (!
|
|
8061
|
+
if (!existsSync22(resolve25(repo, ".git"))) {
|
|
6755
8062
|
return [`## ${label}`, `repo: ${repo}`, "status: unavailable", ""];
|
|
6756
8063
|
}
|
|
6757
8064
|
const status = runCapture2(gitCmd(projectRoot, repo, "status", "--short"), projectRoot).stdout.trim();
|
|
@@ -6774,7 +8081,7 @@ function snapshotRepo(projectRoot, label, repo) {
|
|
|
6774
8081
|
];
|
|
6775
8082
|
}
|
|
6776
8083
|
function commitRepo(projectRoot, repo, label, message, allowEmpty, scoped, files, changedFilesManifest) {
|
|
6777
|
-
if (!
|
|
8084
|
+
if (!existsSync22(resolve25(repo, ".git"))) {
|
|
6778
8085
|
console.log(`Skipping ${label}: repo not available (${repo})`);
|
|
6779
8086
|
return;
|
|
6780
8087
|
}
|
|
@@ -6806,18 +8113,18 @@ function commitRepo(projectRoot, repo, label, message, allowEmpty, scoped, files
|
|
|
6806
8113
|
console.log(`Committed ${label}: ${message}`);
|
|
6807
8114
|
}
|
|
6808
8115
|
function readChangedFilesManifest(projectRoot, taskId) {
|
|
6809
|
-
const manifestPath =
|
|
6810
|
-
if (!
|
|
8116
|
+
const manifestPath = resolve25(artifactDirForId(projectRoot, taskId), "changed-files.txt");
|
|
8117
|
+
if (!existsSync22(manifestPath)) {
|
|
6811
8118
|
return [];
|
|
6812
8119
|
}
|
|
6813
|
-
const files =
|
|
8120
|
+
const files = readFileSync12(manifestPath, "utf-8").split(/\r?\n/).map((line) => normalizeChangedFilePath2(line)).filter(Boolean);
|
|
6814
8121
|
return [...new Set(files)];
|
|
6815
8122
|
}
|
|
6816
8123
|
function refreshChangedFilesManifest(projectRoot, taskId) {
|
|
6817
|
-
const manifestPath =
|
|
6818
|
-
|
|
8124
|
+
const manifestPath = resolve25(artifactDirForId(projectRoot, taskId), "changed-files.txt");
|
|
8125
|
+
mkdirSync11(dirname11(manifestPath), { recursive: true });
|
|
6819
8126
|
const changedFiles = changedFilesForTask(projectRoot, taskId, true);
|
|
6820
|
-
|
|
8127
|
+
writeFileSync11(manifestPath, `${changedFiles.join(`
|
|
6821
8128
|
`)}
|
|
6822
8129
|
`, "utf-8");
|
|
6823
8130
|
return manifestPath;
|
|
@@ -6930,7 +8237,7 @@ function repoHasPathChange(projectRoot, repoRoot, relativePath) {
|
|
|
6930
8237
|
return result.exitCode === 0 && result.stdout.trim().length > 0;
|
|
6931
8238
|
}
|
|
6932
8239
|
function stageExcludePathspecs(repoRoot) {
|
|
6933
|
-
const patterns =
|
|
8240
|
+
const patterns = existsSync22(resolve25(repoRoot, ".rig", "task-config.json")) ? [...TASK_RUNTIME_STAGE_EXCLUDES, ...GENERATED_STAGE_EXCLUDES] : [".rig/**", ...GENERATED_STAGE_EXCLUDES];
|
|
6934
8241
|
return patterns.map((pattern) => `:(glob,exclude)${pattern}`);
|
|
6935
8242
|
}
|
|
6936
8243
|
function pathResolvesBeyondSymlink(repoRoot, relativePath) {
|
|
@@ -6940,7 +8247,7 @@ function pathResolvesBeyondSymlink(repoRoot, relativePath) {
|
|
|
6940
8247
|
}
|
|
6941
8248
|
let current = repoRoot;
|
|
6942
8249
|
for (let index = 0;index < parts.length - 1; index += 1) {
|
|
6943
|
-
current =
|
|
8250
|
+
current = resolve25(current, parts[index]);
|
|
6944
8251
|
try {
|
|
6945
8252
|
if (lstatSync(current).isSymbolicLink()) {
|
|
6946
8253
|
return true;
|
|
@@ -6952,7 +8259,7 @@ function pathResolvesBeyondSymlink(repoRoot, relativePath) {
|
|
|
6952
8259
|
return false;
|
|
6953
8260
|
}
|
|
6954
8261
|
function printRepoStatus(projectRoot, label, repo, expectedBranch) {
|
|
6955
|
-
if (!
|
|
8262
|
+
if (!existsSync22(resolve25(repo, ".git"))) {
|
|
6956
8263
|
console.log(`${label}: unavailable (${repo})`);
|
|
6957
8264
|
return;
|
|
6958
8265
|
}
|
|
@@ -6988,7 +8295,7 @@ function resolveTaskBranchId(projectRoot, taskId) {
|
|
|
6988
8295
|
}
|
|
6989
8296
|
} catch {}
|
|
6990
8297
|
const artifactDir = artifactDirForId(projectRoot, taskId);
|
|
6991
|
-
if (
|
|
8298
|
+
if (existsSync22(artifactDir)) {
|
|
6992
8299
|
return taskId;
|
|
6993
8300
|
}
|
|
6994
8301
|
throw new Error(`Unknown task id: ${taskId}`);
|
|
@@ -7024,11 +8331,11 @@ function runCapture2(command, cwd, projectRoot = cwd) {
|
|
|
7024
8331
|
}
|
|
7025
8332
|
function runtimeGitEnv(projectRoot) {
|
|
7026
8333
|
const { ctx, runtimeRoot } = resolveRuntimeMetadata(projectRoot);
|
|
7027
|
-
const runtimeHome = runtimeRoot ?
|
|
7028
|
-
const runtimeTmp = runtimeRoot ?
|
|
7029
|
-
const runtimeCache = runtimeRoot ?
|
|
7030
|
-
const runtimeKnownHosts = runtimeHome ?
|
|
7031
|
-
const runtimeKey = runtimeHome ?
|
|
8334
|
+
const runtimeHome = runtimeRoot ? resolve25(runtimeRoot, "home") : "";
|
|
8335
|
+
const runtimeTmp = runtimeRoot ? resolve25(runtimeRoot, "tmp") : "";
|
|
8336
|
+
const runtimeCache = runtimeRoot ? resolve25(runtimeRoot, "cache") : "";
|
|
8337
|
+
const runtimeKnownHosts = runtimeHome ? resolve25(runtimeHome, ".ssh", "known_hosts") : "";
|
|
8338
|
+
const runtimeKey = runtimeHome ? resolve25(runtimeHome, ".ssh", "rig-agent-key") : "";
|
|
7032
8339
|
const env = {};
|
|
7033
8340
|
if (ctx?.workspaceDir) {
|
|
7034
8341
|
env.PROJECT_RIG_ROOT = projectRoot;
|
|
@@ -7041,14 +8348,14 @@ function runtimeGitEnv(projectRoot) {
|
|
|
7041
8348
|
if (runtimeRoot) {
|
|
7042
8349
|
env.RIG_RUNTIME_HOME = runtimeRoot;
|
|
7043
8350
|
}
|
|
7044
|
-
if (runtimeHome &&
|
|
8351
|
+
if (runtimeHome && existsSync22(runtimeHome)) {
|
|
7045
8352
|
env.HOME = runtimeHome;
|
|
7046
8353
|
env.OPENSSL_CONF = ensureRuntimeOpenSslConfig(runtimeHome);
|
|
7047
8354
|
}
|
|
7048
|
-
if (runtimeTmp &&
|
|
8355
|
+
if (runtimeTmp && existsSync22(runtimeTmp)) {
|
|
7049
8356
|
env.TMPDIR = runtimeTmp;
|
|
7050
8357
|
}
|
|
7051
|
-
if (runtimeCache &&
|
|
8358
|
+
if (runtimeCache && existsSync22(runtimeCache)) {
|
|
7052
8359
|
env.XDG_CACHE_HOME = runtimeCache;
|
|
7053
8360
|
}
|
|
7054
8361
|
const workspaceSecrets = loadDotEnvSecrets(ctx?.workspaceDir || projectRoot, process.env);
|
|
@@ -7092,14 +8399,14 @@ function runtimeGitEnv(projectRoot) {
|
|
|
7092
8399
|
env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
|
|
7093
8400
|
applyGitHubCredentialHelperEnv(env);
|
|
7094
8401
|
}
|
|
7095
|
-
if (runtimeKnownHosts &&
|
|
8402
|
+
if (runtimeKnownHosts && existsSync22(runtimeKnownHosts)) {
|
|
7096
8403
|
const sshParts = [
|
|
7097
8404
|
"ssh",
|
|
7098
8405
|
`-o UserKnownHostsFile="${runtimeKnownHosts}"`,
|
|
7099
8406
|
"-o StrictHostKeyChecking=yes",
|
|
7100
8407
|
"-F /dev/null"
|
|
7101
8408
|
];
|
|
7102
|
-
if (runtimeKey &&
|
|
8409
|
+
if (runtimeKey && existsSync22(runtimeKey)) {
|
|
7103
8410
|
sshParts.splice(1, 0, `-i "${runtimeKey}"`, "-o IdentitiesOnly=yes");
|
|
7104
8411
|
}
|
|
7105
8412
|
env.GIT_SSH_COMMAND = sshParts.join(" ");
|
|
@@ -7120,12 +8427,12 @@ function loadPersistedRuntimeSecrets(runtimeRoot) {
|
|
|
7120
8427
|
if (!runtimeRoot) {
|
|
7121
8428
|
return {};
|
|
7122
8429
|
}
|
|
7123
|
-
const path =
|
|
7124
|
-
if (!
|
|
8430
|
+
const path = resolve25(runtimeRoot, "runtime-secrets.json");
|
|
8431
|
+
if (!existsSync22(path)) {
|
|
7125
8432
|
return {};
|
|
7126
8433
|
}
|
|
7127
8434
|
try {
|
|
7128
|
-
const parsed = JSON.parse(
|
|
8435
|
+
const parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
7129
8436
|
const entries = Object.entries(parsed).filter((entry) => typeof entry[1] === "string");
|
|
7130
8437
|
return Object.fromEntries(entries);
|
|
7131
8438
|
} catch {
|
|
@@ -7133,13 +8440,13 @@ function loadPersistedRuntimeSecrets(runtimeRoot) {
|
|
|
7133
8440
|
}
|
|
7134
8441
|
}
|
|
7135
8442
|
function ensureRuntimeOpenSslConfig(runtimeHome) {
|
|
7136
|
-
const sslDir =
|
|
7137
|
-
const sslConfig =
|
|
7138
|
-
if (!
|
|
7139
|
-
|
|
8443
|
+
const sslDir = resolve25(runtimeHome, ".ssl");
|
|
8444
|
+
const sslConfig = resolve25(sslDir, "openssl.cnf");
|
|
8445
|
+
if (!existsSync22(sslDir)) {
|
|
8446
|
+
mkdirSync11(sslDir, { recursive: true });
|
|
7140
8447
|
}
|
|
7141
|
-
if (!
|
|
7142
|
-
|
|
8448
|
+
if (!existsSync22(sslConfig)) {
|
|
8449
|
+
writeFileSync11(sslConfig, `# Rig runtime OpenSSL config placeholder
|
|
7143
8450
|
`);
|
|
7144
8451
|
}
|
|
7145
8452
|
return sslConfig;
|
|
@@ -7157,11 +8464,11 @@ function resolveRuntimeMetadata(projectRoot) {
|
|
|
7157
8464
|
if (contextFile) {
|
|
7158
8465
|
return {
|
|
7159
8466
|
ctx,
|
|
7160
|
-
runtimeRoot: dirname11(
|
|
8467
|
+
runtimeRoot: dirname11(resolve25(contextFile))
|
|
7161
8468
|
};
|
|
7162
8469
|
}
|
|
7163
8470
|
const inferredContextFile = findRuntimeContextFile2(projectRoot);
|
|
7164
|
-
if (
|
|
8471
|
+
if (existsSync22(inferredContextFile)) {
|
|
7165
8472
|
try {
|
|
7166
8473
|
ctx = loadRuntimeContext(inferredContextFile);
|
|
7167
8474
|
} catch {}
|
|
@@ -7173,10 +8480,10 @@ function resolveRuntimeMetadata(projectRoot) {
|
|
|
7173
8480
|
return { ctx, runtimeRoot: "" };
|
|
7174
8481
|
}
|
|
7175
8482
|
function findRuntimeContextFile2(startPath) {
|
|
7176
|
-
let current =
|
|
8483
|
+
let current = resolve25(startPath);
|
|
7177
8484
|
while (true) {
|
|
7178
|
-
const candidate =
|
|
7179
|
-
if (
|
|
8485
|
+
const candidate = resolve25(current, "runtime-context.json");
|
|
8486
|
+
if (existsSync22(candidate)) {
|
|
7180
8487
|
return candidate;
|
|
7181
8488
|
}
|
|
7182
8489
|
const parent = dirname11(current);
|
|
@@ -7188,7 +8495,7 @@ function findRuntimeContextFile2(startPath) {
|
|
|
7188
8495
|
}
|
|
7189
8496
|
|
|
7190
8497
|
// packages/runtime/src/control-plane/native/profile-ops.ts
|
|
7191
|
-
import { existsSync as
|
|
8498
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
|
|
7192
8499
|
var DEFAULTS = {
|
|
7193
8500
|
model: parseEnvOrDefault(process.env.DEFAULT_AGENT_MODEL, ["claude", "gpt-codex", "pi"], "pi"),
|
|
7194
8501
|
runtime: parseEnvOrDefault(process.env.DEFAULT_AGENT_RUNTIME, ["claude-code", "codex-app-server", "pi"], "pi"),
|
|
@@ -7203,7 +8510,7 @@ function parseEnvOrDefault(value, allowed, fallback) {
|
|
|
7203
8510
|
return allowed.includes(value) ? value : fallback;
|
|
7204
8511
|
}
|
|
7205
8512
|
async function readProfileFile(path) {
|
|
7206
|
-
if (!
|
|
8513
|
+
if (!existsSync23(path)) {
|
|
7207
8514
|
return null;
|
|
7208
8515
|
}
|
|
7209
8516
|
try {
|
|
@@ -7256,14 +8563,14 @@ async function setProfile(projectRoot, options) {
|
|
|
7256
8563
|
plugin = model === "gpt-codex" ? "codex" : model === "pi" ? "pi" : "claude";
|
|
7257
8564
|
}
|
|
7258
8565
|
const paths = resolveHarnessPaths(projectRoot);
|
|
7259
|
-
|
|
8566
|
+
mkdirSync12(paths.stateDir, { recursive: true });
|
|
7260
8567
|
const next = {
|
|
7261
8568
|
model,
|
|
7262
8569
|
runtime,
|
|
7263
8570
|
agent_plugin: plugin,
|
|
7264
8571
|
updated_at: new Date().toISOString()
|
|
7265
8572
|
};
|
|
7266
|
-
|
|
8573
|
+
writeFileSync12(paths.agentProfilePath, `${JSON.stringify(next, null, 2)}
|
|
7267
8574
|
`, "utf-8");
|
|
7268
8575
|
await showProfile(projectRoot, false);
|
|
7269
8576
|
}
|
|
@@ -7299,13 +8606,13 @@ async function setReviewProfile(projectRoot, mode, provider) {
|
|
|
7299
8606
|
throw new Error(`Invalid provider: ${resolvedProvider}. Supported: greptile.`);
|
|
7300
8607
|
}
|
|
7301
8608
|
const paths = resolveHarnessPaths(projectRoot);
|
|
7302
|
-
|
|
8609
|
+
mkdirSync12(paths.stateDir, { recursive: true });
|
|
7303
8610
|
const next = {
|
|
7304
8611
|
mode,
|
|
7305
8612
|
provider: resolvedProvider,
|
|
7306
8613
|
updated_at: new Date().toISOString()
|
|
7307
8614
|
};
|
|
7308
|
-
|
|
8615
|
+
writeFileSync12(paths.reviewProfilePath, `${JSON.stringify(next, null, 2)}
|
|
7309
8616
|
`, "utf-8");
|
|
7310
8617
|
await showReviewProfile(projectRoot);
|
|
7311
8618
|
}
|
|
@@ -7343,44 +8650,44 @@ async function loadReviewProfile(path) {
|
|
|
7343
8650
|
}
|
|
7344
8651
|
|
|
7345
8652
|
// packages/runtime/src/control-plane/native/repo-ops.ts
|
|
7346
|
-
import { existsSync as
|
|
7347
|
-
import { basename as basename8, dirname as dirname13, resolve as
|
|
8653
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync16, readFileSync as readFileSync14, readdirSync as readdirSync6, rmSync as rmSync7, writeFileSync as writeFileSync14 } from "fs";
|
|
8654
|
+
import { basename as basename8, dirname as dirname13, resolve as resolve29 } from "path";
|
|
7348
8655
|
// packages/runtime/src/control-plane/repos/mirror/bootstrap.ts
|
|
7349
|
-
import { existsSync as
|
|
7350
|
-
import { resolve as
|
|
8656
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync14, realpathSync as realpathSync2 } from "fs";
|
|
8657
|
+
import { resolve as resolve27 } from "path";
|
|
7351
8658
|
|
|
7352
8659
|
// packages/runtime/src/control-plane/authority-files.ts
|
|
7353
|
-
import { existsSync as
|
|
7354
|
-
import { dirname as dirname12, join as join4, relative, resolve as
|
|
8660
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync13, appendFileSync as appendFileSync2, copyFileSync as copyFileSync3, statSync as statSync5, readdirSync as readdirSync5, chmodSync as chmodSync3 } from "fs";
|
|
8661
|
+
import { dirname as dirname12, join as join4, relative, resolve as resolve26 } from "path";
|
|
7355
8662
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
7356
8663
|
function resolveAuthorityProjectStateDir(projectRoot) {
|
|
7357
8664
|
const explicit = process.env.RIG_STATE_DIR?.trim();
|
|
7358
8665
|
if (explicit) {
|
|
7359
|
-
return
|
|
8666
|
+
return resolve26(explicit);
|
|
7360
8667
|
}
|
|
7361
|
-
return
|
|
8668
|
+
return resolve26(resolve26(projectRoot), ".rig", "state");
|
|
7362
8669
|
}
|
|
7363
8670
|
function readJsonAtPath(path, fallback) {
|
|
7364
|
-
if (!
|
|
8671
|
+
if (!existsSync24(path)) {
|
|
7365
8672
|
return fallback;
|
|
7366
8673
|
}
|
|
7367
8674
|
try {
|
|
7368
|
-
return JSON.parse(
|
|
8675
|
+
return JSON.parse(readFileSync13(path, "utf-8"));
|
|
7369
8676
|
} catch {
|
|
7370
8677
|
return fallback;
|
|
7371
8678
|
}
|
|
7372
8679
|
}
|
|
7373
8680
|
function writeJsonAtPath(path, value) {
|
|
7374
|
-
|
|
7375
|
-
|
|
8681
|
+
mkdirSync13(dirname12(path), { recursive: true });
|
|
8682
|
+
writeFileSync13(path, `${JSON.stringify(value, null, 2)}
|
|
7376
8683
|
`, "utf8");
|
|
7377
8684
|
return path;
|
|
7378
8685
|
}
|
|
7379
8686
|
function readAuthorityProjectStateJson(projectRoot, relativePath, fallback) {
|
|
7380
|
-
return readJsonAtPath(
|
|
8687
|
+
return readJsonAtPath(resolve26(resolveAuthorityProjectStateDir(projectRoot), relativePath), fallback);
|
|
7381
8688
|
}
|
|
7382
8689
|
function writeAuthorityProjectStateJson(projectRoot, relativePath, value) {
|
|
7383
|
-
return writeJsonAtPath(
|
|
8690
|
+
return writeJsonAtPath(resolve26(resolveAuthorityProjectStateDir(projectRoot), relativePath), value);
|
|
7384
8691
|
}
|
|
7385
8692
|
|
|
7386
8693
|
// packages/runtime/src/control-plane/repos/mirror/state.ts
|
|
@@ -7440,7 +8747,7 @@ function sameExistingPath(left, right) {
|
|
|
7440
8747
|
try {
|
|
7441
8748
|
return realpathSync2(left) === realpathSync2(right);
|
|
7442
8749
|
} catch {
|
|
7443
|
-
return
|
|
8750
|
+
return resolve27(left) === resolve27(right);
|
|
7444
8751
|
}
|
|
7445
8752
|
}
|
|
7446
8753
|
function repoLooksUsable(repoRoot, projectRoot) {
|
|
@@ -7476,7 +8783,7 @@ function resolveMirrorRemoteUrl(layout) {
|
|
|
7476
8783
|
}
|
|
7477
8784
|
}
|
|
7478
8785
|
}
|
|
7479
|
-
if (
|
|
8786
|
+
if (existsSync25(resolve27(layout.checkoutRoot, ".git")) && checkoutLooksUsable(layout)) {
|
|
7480
8787
|
const checkoutOrigin = runGit(["git", "-C", layout.checkoutRoot, "remote", "get-url", "origin"], layout.projectRoot);
|
|
7481
8788
|
if (checkoutOrigin.exitCode === 0) {
|
|
7482
8789
|
const currentOrigin = checkoutOrigin.stdout.trim();
|
|
@@ -7489,9 +8796,9 @@ function resolveMirrorRemoteUrl(layout) {
|
|
|
7489
8796
|
}
|
|
7490
8797
|
function ensureManagedRepoMirror(projectRoot, repoId) {
|
|
7491
8798
|
const layout = resolveManagedRepoLayout(projectRoot, repoId);
|
|
7492
|
-
|
|
8799
|
+
mkdirSync14(layout.metadataRoot, { recursive: true });
|
|
7493
8800
|
const remoteUrl = resolveMirrorRemoteUrl(layout);
|
|
7494
|
-
if (!
|
|
8801
|
+
if (!existsSync25(resolve27(layout.mirrorRoot, "HEAD"))) {
|
|
7495
8802
|
ensureGitSuccess(runGit(["git", "init", "--bare", layout.mirrorRoot], layout.projectRoot), ["git", "init", "--bare", layout.mirrorRoot]);
|
|
7496
8803
|
}
|
|
7497
8804
|
const getOrigin = runGit(["git", "--git-dir", layout.mirrorRoot, "remote", "get-url", "origin"], layout.projectRoot);
|
|
@@ -7511,8 +8818,8 @@ function ensureManagedRepoMirror(projectRoot, repoId) {
|
|
|
7511
8818
|
return layout;
|
|
7512
8819
|
}
|
|
7513
8820
|
// packages/runtime/src/control-plane/repos/mirror/refresh.ts
|
|
7514
|
-
import { existsSync as
|
|
7515
|
-
import { resolve as
|
|
8821
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync15, realpathSync as realpathSync3, rmSync as rmSync6 } from "fs";
|
|
8822
|
+
import { resolve as resolve28 } from "path";
|
|
7516
8823
|
function nowIso3() {
|
|
7517
8824
|
return new Date().toISOString();
|
|
7518
8825
|
}
|
|
@@ -7539,7 +8846,7 @@ function sameExistingPath2(left, right) {
|
|
|
7539
8846
|
try {
|
|
7540
8847
|
return realpathSync3(left) === realpathSync3(right);
|
|
7541
8848
|
} catch {
|
|
7542
|
-
return
|
|
8849
|
+
return resolve28(left) === resolve28(right);
|
|
7543
8850
|
}
|
|
7544
8851
|
}
|
|
7545
8852
|
function ensureMirrorHead(layout) {
|
|
@@ -7585,12 +8892,12 @@ function checkoutLooksUsable2(layout) {
|
|
|
7585
8892
|
return probe.exitCode === 0 && sameExistingPath2(probe.stdout.trim(), layout.checkoutRoot);
|
|
7586
8893
|
}
|
|
7587
8894
|
function ensureCheckoutFromMirror(layout) {
|
|
7588
|
-
|
|
7589
|
-
const gitPath =
|
|
7590
|
-
if (
|
|
7591
|
-
|
|
8895
|
+
mkdirSync15(resolve28(layout.checkoutRoot, ".."), { recursive: true });
|
|
8896
|
+
const gitPath = resolve28(layout.checkoutRoot, ".git");
|
|
8897
|
+
if (existsSync26(layout.checkoutRoot) && (!existsSync26(gitPath) || !checkoutLooksUsable2(layout))) {
|
|
8898
|
+
rmSync6(layout.checkoutRoot, { recursive: true, force: true });
|
|
7592
8899
|
}
|
|
7593
|
-
if (!
|
|
8900
|
+
if (!existsSync26(gitPath)) {
|
|
7594
8901
|
ensureGitSuccess2(runGit2(["git", "clone", layout.mirrorRoot, layout.checkoutRoot], layout.projectRoot), ["git", "clone", layout.mirrorRoot, layout.checkoutRoot]);
|
|
7595
8902
|
}
|
|
7596
8903
|
const getOrigin = runGit2(["git", "-C", layout.checkoutRoot, "remote", "get-url", "origin"], layout.projectRoot);
|
|
@@ -7691,7 +8998,7 @@ function repoDiscover(projectRoot, taskId) {
|
|
|
7691
8998
|
}
|
|
7692
8999
|
function repoBaseline(projectRoot, refresh = false) {
|
|
7693
9000
|
const paths = resolveRepoDiscoveryPaths(projectRoot);
|
|
7694
|
-
if (!refresh &&
|
|
9001
|
+
if (!refresh && existsSync27(paths.baseRepoPinsPath)) {
|
|
7695
9002
|
const baseline = readJsonFile(paths.baseRepoPinsPath, { recorded_at: nowIso(), repos: {} });
|
|
7696
9003
|
return baseline.repos || {};
|
|
7697
9004
|
}
|
|
@@ -7715,8 +9022,8 @@ function ensureMonorepoReady(projectRoot) {
|
|
|
7715
9022
|
}
|
|
7716
9023
|
function persistBaselinePins(projectRoot, repos) {
|
|
7717
9024
|
const paths = resolveRepoDiscoveryPaths(projectRoot);
|
|
7718
|
-
|
|
7719
|
-
|
|
9025
|
+
mkdirSync16(resolve29(paths.baseRepoPinsPath, ".."), { recursive: true });
|
|
9026
|
+
writeFileSync14(paths.baseRepoPinsPath, `${JSON.stringify({ recorded_at: nowIso(), repos }, null, 2)}
|
|
7720
9027
|
`, "utf-8");
|
|
7721
9028
|
return repos;
|
|
7722
9029
|
}
|
|
@@ -7795,28 +9102,28 @@ function readRepoDiscoveryTaskConfig(projectRoot) {
|
|
|
7795
9102
|
function resolveRepoDiscoveryPaths(projectRoot) {
|
|
7796
9103
|
const monorepoRoot = resolveMonorepoRepoLayout(projectRoot).checkoutRoot;
|
|
7797
9104
|
const explicitHostProjectRoot = (process.env.RIG_HOST_PROJECT_ROOT || "").trim();
|
|
7798
|
-
const normalizedProjectRoot =
|
|
9105
|
+
const normalizedProjectRoot = resolve29(projectRoot);
|
|
7799
9106
|
const hostProjectRoot = explicitHostProjectRoot && shouldUseHostProjectStateRoot(normalizedProjectRoot) ? explicitHostProjectRoot : normalizedProjectRoot;
|
|
7800
|
-
const stateDir =
|
|
9107
|
+
const stateDir = resolve29(hostProjectRoot, ".rig", "state");
|
|
7801
9108
|
return {
|
|
7802
9109
|
monorepoRoot,
|
|
7803
|
-
taskRepoCommitsPath:
|
|
7804
|
-
baseRepoPinsPath:
|
|
9110
|
+
taskRepoCommitsPath: resolve29(stateDir, "task-repo-commits.json"),
|
|
9111
|
+
baseRepoPinsPath: resolve29(stateDir, "base-repo-pins.json")
|
|
7805
9112
|
};
|
|
7806
9113
|
}
|
|
7807
9114
|
function shouldUseHostProjectStateRoot(projectRoot) {
|
|
7808
9115
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
7809
|
-
if (runtimeWorkspace &&
|
|
9116
|
+
if (runtimeWorkspace && resolve29(runtimeWorkspace) === projectRoot) {
|
|
7810
9117
|
return true;
|
|
7811
9118
|
}
|
|
7812
9119
|
return basename8(dirname13(projectRoot)) === ".worktrees";
|
|
7813
9120
|
}
|
|
7814
9121
|
function readPinFromArtifact(projectRoot, depTask, repoKey) {
|
|
7815
|
-
const snapshot =
|
|
7816
|
-
if (!
|
|
9122
|
+
const snapshot = resolve29(artifactDirForId(projectRoot, depTask), "git-state.txt");
|
|
9123
|
+
if (!existsSync27(snapshot)) {
|
|
7817
9124
|
return "";
|
|
7818
9125
|
}
|
|
7819
|
-
const content =
|
|
9126
|
+
const content = readFileSync14(snapshot, "utf-8");
|
|
7820
9127
|
const chunk = content.split(/\r?\n/);
|
|
7821
9128
|
let inSection = false;
|
|
7822
9129
|
for (const line of chunk) {
|
|
@@ -7838,12 +9145,12 @@ function repoPath(projectRoot, key) {
|
|
|
7838
9145
|
if (managed) {
|
|
7839
9146
|
return managed.checkoutRoot;
|
|
7840
9147
|
}
|
|
7841
|
-
return key.startsWith("/") ? key :
|
|
9148
|
+
return key.startsWith("/") ? key : resolve29(projectRoot, key);
|
|
7842
9149
|
}
|
|
7843
9150
|
function applyPins(projectRoot, pins) {
|
|
7844
9151
|
for (const [key, commit] of Object.entries(pins)) {
|
|
7845
9152
|
const path = repoPath(projectRoot, key);
|
|
7846
|
-
if (!
|
|
9153
|
+
if (!existsSync27(resolve29(path, ".git"))) {
|
|
7847
9154
|
throw new Error(`Repo for pin not available: ${key} (${path})`);
|
|
7848
9155
|
}
|
|
7849
9156
|
let hasCommit = runGitCapture(["git", "-C", path, "cat-file", "-e", `${commit}^{commit}`], projectRoot).exitCode === 0;
|
|
@@ -7872,7 +9179,7 @@ function verifyPins(projectRoot, pins) {
|
|
|
7872
9179
|
let ok = true;
|
|
7873
9180
|
for (const [key, expected] of Object.entries(pins)) {
|
|
7874
9181
|
const path = repoPath(projectRoot, key);
|
|
7875
|
-
if (!
|
|
9182
|
+
if (!existsSync27(resolve29(path, ".git"))) {
|
|
7876
9183
|
console.error(`ERROR: repo missing during pin verification: ${key}`);
|
|
7877
9184
|
ok = false;
|
|
7878
9185
|
continue;
|
|
@@ -7897,23 +9204,23 @@ function resolveRuntimeGitEnv() {
|
|
|
7897
9204
|
GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND
|
|
7898
9205
|
};
|
|
7899
9206
|
}
|
|
7900
|
-
const runtimeRoot = process.env.RIG_RUNTIME_HOME?.trim() || (process.env.RIG_RUNTIME_CONTEXT_FILE?.trim() ?
|
|
7901
|
-
const runtimeHome = runtimeRoot ?
|
|
9207
|
+
const runtimeRoot = process.env.RIG_RUNTIME_HOME?.trim() || (process.env.RIG_RUNTIME_CONTEXT_FILE?.trim() ? resolve29(process.env.RIG_RUNTIME_CONTEXT_FILE, "..") : inferRuntimeRootFromWorkspace(process.cwd()));
|
|
9208
|
+
const runtimeHome = runtimeRoot ? resolve29(runtimeRoot, "home") : process.env.HOME?.trim() || "";
|
|
7902
9209
|
if (!runtimeHome) {
|
|
7903
9210
|
return;
|
|
7904
9211
|
}
|
|
7905
|
-
const knownHostsPath =
|
|
7906
|
-
if (!
|
|
9212
|
+
const knownHostsPath = resolve29(runtimeHome, ".ssh", "known_hosts");
|
|
9213
|
+
if (!existsSync27(knownHostsPath)) {
|
|
7907
9214
|
return { HOME: runtimeHome };
|
|
7908
9215
|
}
|
|
7909
|
-
const agentSshKey =
|
|
9216
|
+
const agentSshKey = resolve29(runtimeHome, ".ssh", "rig-agent-key");
|
|
7910
9217
|
const sshParts = [
|
|
7911
9218
|
"ssh",
|
|
7912
9219
|
`-o UserKnownHostsFile="${knownHostsPath}"`,
|
|
7913
9220
|
"-o StrictHostKeyChecking=yes",
|
|
7914
9221
|
"-F /dev/null"
|
|
7915
9222
|
];
|
|
7916
|
-
if (
|
|
9223
|
+
if (existsSync27(agentSshKey)) {
|
|
7917
9224
|
sshParts.splice(1, 0, `-i "${agentSshKey}"`, "-o IdentitiesOnly=yes");
|
|
7918
9225
|
}
|
|
7919
9226
|
return {
|
|
@@ -7923,24 +9230,24 @@ function resolveRuntimeGitEnv() {
|
|
|
7923
9230
|
}
|
|
7924
9231
|
function inferRuntimeRootFromWorkspace(cwd) {
|
|
7925
9232
|
const contextPath = findRuntimeContextFile3(cwd);
|
|
7926
|
-
if (!contextPath || !
|
|
9233
|
+
if (!contextPath || !existsSync27(contextPath)) {
|
|
7927
9234
|
return "";
|
|
7928
9235
|
}
|
|
7929
9236
|
try {
|
|
7930
9237
|
loadRuntimeContext(contextPath);
|
|
7931
|
-
return
|
|
9238
|
+
return resolve29(contextPath, "..");
|
|
7932
9239
|
} catch {
|
|
7933
9240
|
return "";
|
|
7934
9241
|
}
|
|
7935
9242
|
}
|
|
7936
9243
|
function findRuntimeContextFile3(startPath) {
|
|
7937
|
-
let current =
|
|
9244
|
+
let current = resolve29(startPath);
|
|
7938
9245
|
while (true) {
|
|
7939
|
-
const candidate =
|
|
7940
|
-
if (
|
|
9246
|
+
const candidate = resolve29(current, "runtime-context.json");
|
|
9247
|
+
if (existsSync27(candidate)) {
|
|
7941
9248
|
return candidate;
|
|
7942
9249
|
}
|
|
7943
|
-
const parent =
|
|
9250
|
+
const parent = resolve29(current, "..");
|
|
7944
9251
|
if (parent === current) {
|
|
7945
9252
|
return "";
|
|
7946
9253
|
}
|
|
@@ -7949,12 +9256,12 @@ function findRuntimeContextFile3(startPath) {
|
|
|
7949
9256
|
}
|
|
7950
9257
|
|
|
7951
9258
|
// packages/runtime/src/control-plane/memory-sync/cli.ts
|
|
7952
|
-
import { existsSync as
|
|
9259
|
+
import { existsSync as existsSync28 } from "fs";
|
|
7953
9260
|
import { randomUUID } from "crypto";
|
|
7954
9261
|
|
|
7955
9262
|
// packages/runtime/src/control-plane/memory-sync/db.ts
|
|
7956
9263
|
import { Database } from "bun:sqlite";
|
|
7957
|
-
import { mkdirSync as
|
|
9264
|
+
import { mkdirSync as mkdirSync17 } from "fs";
|
|
7958
9265
|
import { dirname as dirname14 } from "path";
|
|
7959
9266
|
|
|
7960
9267
|
// packages/runtime/src/control-plane/memory-sync/types.ts
|
|
@@ -8551,7 +9858,7 @@ async function validateEventTargets(executor, event) {
|
|
|
8551
9858
|
}
|
|
8552
9859
|
}
|
|
8553
9860
|
async function openMemoryDb(dbPath) {
|
|
8554
|
-
|
|
9861
|
+
mkdirSync17(dirname14(dbPath), { recursive: true });
|
|
8555
9862
|
const sqlite = new Database(dbPath, { create: true, strict: true });
|
|
8556
9863
|
const client = createMemoryDbClient(sqlite);
|
|
8557
9864
|
const db = {
|
|
@@ -8970,7 +10277,7 @@ function formatMemoryQueryResults(results) {
|
|
|
8970
10277
|
}
|
|
8971
10278
|
|
|
8972
10279
|
// packages/runtime/src/control-plane/memory-sync/read.ts
|
|
8973
|
-
import { mkdtempSync, rmSync as
|
|
10280
|
+
import { mkdtempSync, rmSync as rmSync8, writeFileSync as writeFileSync15 } from "fs";
|
|
8974
10281
|
import { tmpdir as tmpdir5 } from "os";
|
|
8975
10282
|
import { join as join5 } from "path";
|
|
8976
10283
|
var CANONICAL_MEMORY_DB_PATH = "rig/memory/project-memory.db";
|
|
@@ -8979,7 +10286,7 @@ var DEFAULT_READ_DEPS2 = {
|
|
|
8979
10286
|
readBlobBytesAtRef: nativeReadBlobBytesAtRef,
|
|
8980
10287
|
openMemoryDb,
|
|
8981
10288
|
makeTempDir: () => mkdtempSync(join5(tmpdir5(), "memory-sync-read-")),
|
|
8982
|
-
removeDir: (path) =>
|
|
10289
|
+
removeDir: (path) => rmSync8(path, { recursive: true, force: true })
|
|
8983
10290
|
};
|
|
8984
10291
|
function isMissingCanonicalMemoryBlobError(error) {
|
|
8985
10292
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -8999,7 +10306,7 @@ async function readCanonicalMemoryDb(projectRoot, deps = {}) {
|
|
|
8999
10306
|
try {
|
|
9000
10307
|
try {
|
|
9001
10308
|
const bytes = readDeps.readBlobBytesAtRef(repoPath2, baseOid, CANONICAL_MEMORY_DB_PATH);
|
|
9002
|
-
|
|
10309
|
+
writeFileSync15(dbPath, bytes);
|
|
9003
10310
|
} catch (error) {
|
|
9004
10311
|
if (!isMissingCanonicalMemoryBlobError(error)) {
|
|
9005
10312
|
throw error;
|
|
@@ -9157,7 +10464,7 @@ function requireRuntimeMemoryContext(runtimeContext) {
|
|
|
9157
10464
|
}
|
|
9158
10465
|
async function ensureRuntimeMemoryUsable(runtimeContext) {
|
|
9159
10466
|
const memory = requireRuntimeMemoryContext(runtimeContext);
|
|
9160
|
-
if (!
|
|
10467
|
+
if (!existsSync28(memory.hydratedPath)) {
|
|
9161
10468
|
throw new Error(`Shared memory database is missing: ${memory.hydratedPath}`);
|
|
9162
10469
|
}
|
|
9163
10470
|
const db = await openMemoryDb(memory.hydratedPath);
|
|
@@ -9420,7 +10727,7 @@ async function executeMemoryCommand(options, deps = {}) {
|
|
|
9420
10727
|
}
|
|
9421
10728
|
|
|
9422
10729
|
// packages/runtime/src/control-plane/native/harness-cli.ts
|
|
9423
|
-
async function executeHarnessCommand(projectRoot,
|
|
10730
|
+
async function executeHarnessCommand(projectRoot, args, eventBus, pluginHostCtx) {
|
|
9424
10731
|
const [command = "help", ...rest] = args;
|
|
9425
10732
|
switch (command) {
|
|
9426
10733
|
case "memory": {
|
|
@@ -9458,8 +10765,8 @@ async function executeHarnessCommand(projectRoot, plugins, args, eventBus, plugi
|
|
|
9458
10765
|
}
|
|
9459
10766
|
case "completion-verification":
|
|
9460
10767
|
case "completition-verification": {
|
|
9461
|
-
const hookPath =
|
|
9462
|
-
if (!
|
|
10768
|
+
const hookPath = resolve30(projectRoot, ".rig/bin/hooks/completion-verification");
|
|
10769
|
+
if (!existsSync29(hookPath)) {
|
|
9463
10770
|
throw new Error(`completion-verification hook binary not found: ${hookPath}`);
|
|
9464
10771
|
}
|
|
9465
10772
|
const proc = Bun.spawn([hookPath], {
|
|
@@ -9477,7 +10784,7 @@ async function executeHarnessCommand(projectRoot, plugins, args, eventBus, plugi
|
|
|
9477
10784
|
}
|
|
9478
10785
|
case "verify": {
|
|
9479
10786
|
const task = rest[0];
|
|
9480
|
-
const ok = await taskVerify(projectRoot,
|
|
10787
|
+
const ok = await taskVerify(projectRoot, task);
|
|
9481
10788
|
if (!ok) {
|
|
9482
10789
|
throw new Error("Verification rejected.");
|
|
9483
10790
|
}
|