@h-rig/runtime 0.0.6-alpha.3 → 0.0.6-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/rig-agent-dispatch.js +1165 -785
- package/dist/bin/rig-agent.js +458 -389
- package/dist/src/control-plane/agent-wrapper.js +1191 -504
- package/dist/src/control-plane/authority-files.js +12 -6
- package/dist/src/control-plane/harness-main.js +2186 -1786
- package/dist/src/control-plane/hooks/completion-verification.js +2084 -1019
- package/dist/src/control-plane/hooks/inject-context.js +193 -139
- package/dist/src/control-plane/hooks/submodule-branch.js +603 -545
- package/dist/src/control-plane/hooks/task-runtime-start.js +603 -545
- package/dist/src/control-plane/materialize-task-config.js +64 -8
- package/dist/src/control-plane/native/git-ops.js +90 -64
- package/dist/src/control-plane/native/harness-cli.js +1989 -682
- package/dist/src/control-plane/native/pr-automation.js +1657 -54
- package/dist/src/control-plane/native/pr-review-gate.js +1455 -0
- package/dist/src/control-plane/native/repo-ops.js +3 -0
- package/dist/src/control-plane/native/run-ops.js +39 -13
- package/dist/src/control-plane/native/task-ops.js +1819 -527
- package/dist/src/control-plane/native/validator.js +163 -109
- package/dist/src/control-plane/native/verifier.js +1616 -323
- package/dist/src/control-plane/native/workspace-ops.js +12 -6
- package/dist/src/control-plane/pi-sessiond/bin.js +793 -0
- package/dist/src/control-plane/pi-sessiond/client.js +41 -0
- package/dist/src/control-plane/pi-sessiond/event-hub.js +59 -0
- package/dist/src/control-plane/pi-sessiond/extension-ui-context.js +198 -0
- package/dist/src/control-plane/pi-sessiond/launcher.js +173 -0
- package/dist/src/control-plane/pi-sessiond/server.js +802 -0
- package/dist/src/control-plane/pi-sessiond/session-service.js +540 -0
- package/dist/src/control-plane/pi-sessiond/types.js +1 -0
- package/dist/src/control-plane/plugin-host-context.js +54 -0
- package/dist/src/control-plane/runtime/image/fingerprint-sidecar.js +3 -0
- package/dist/src/control-plane/runtime/image/index.js +3 -0
- package/dist/src/control-plane/runtime/image-fingerprint-sidecar.js +3 -0
- package/dist/src/control-plane/runtime/image.js +3 -0
- package/dist/src/control-plane/runtime/index.js +517 -722
- package/dist/src/control-plane/runtime/isolation/home.js +28 -6
- package/dist/src/control-plane/runtime/isolation/index.js +541 -461
- package/dist/src/control-plane/runtime/isolation/runner.js +28 -6
- package/dist/src/control-plane/runtime/isolation/shared.js +9 -6
- package/dist/src/control-plane/runtime/isolation.js +541 -461
- package/dist/src/control-plane/runtime/plugin-mode.js +3 -27
- package/dist/src/control-plane/runtime/queue.js +458 -385
- package/dist/src/control-plane/runtime/snapshot/task-run.js +3 -0
- package/dist/src/control-plane/runtime/task-run-snapshot.js +3 -0
- package/dist/src/control-plane/skill-materializer.js +46 -0
- package/dist/src/control-plane/tasks/source-aware-task-config-source.js +14 -2
- package/dist/src/control-plane/tasks/source-lifecycle.js +86 -32
- package/dist/src/index.js +27 -298
- package/dist/src/layout.js +12 -7
- package/dist/src/local-server.js +20 -14
- package/native/darwin-arm64/rig-git +0 -0
- package/native/darwin-arm64/rig-git.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-shell +0 -0
- package/native/darwin-arm64/rig-shell.build-manifest.json +1 -1
- package/native/darwin-arm64/rig-tools +0 -0
- package/native/darwin-arm64/rig-tools.build-manifest.json +1 -1
- package/native/darwin-arm64/runtime-native.dylib +0 -0
- package/package.json +8 -6
- package/dist/src/control-plane/runtime/plugins.js +0 -1131
- package/dist/src/plugins.js +0 -329
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// packages/runtime/src/control-plane/hooks/completion-verification.ts
|
|
5
|
-
import { appendFileSync as appendFileSync2, existsSync as existsSync21, mkdirSync as
|
|
5
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync21, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync13 } from "fs";
|
|
6
6
|
import { resolve as resolve24 } from "path";
|
|
7
7
|
import {
|
|
8
8
|
escapeRegExp as escapeRegExp2,
|
|
@@ -13,10 +13,15 @@ import {
|
|
|
13
13
|
resolvePolicyContent
|
|
14
14
|
} from "@rig/hook-kit";
|
|
15
15
|
|
|
16
|
-
// packages/runtime/src/control-plane/runtime/
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
16
|
+
// packages/runtime/src/control-plane/runtime/guard.ts
|
|
17
|
+
import { optimizeNextInvocation } from "bun:jsc";
|
|
18
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
19
|
+
import { resolve as resolve4 } from "path";
|
|
20
|
+
|
|
21
|
+
// packages/runtime/src/control-plane/native/utils.ts
|
|
22
|
+
import { ptr as ptr2 } from "bun:ffi";
|
|
23
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
24
|
+
import { resolve as resolve3 } from "path";
|
|
20
25
|
|
|
21
26
|
// packages/runtime/src/layout.ts
|
|
22
27
|
import { existsSync } from "fs";
|
|
@@ -127,126 +132,13 @@ function resolveRigLayout(projectRoot) {
|
|
|
127
132
|
};
|
|
128
133
|
}
|
|
129
134
|
|
|
130
|
-
// packages/runtime/src/control-plane/runtime/events.ts
|
|
131
|
-
async function appendEvent(eventsFile, event) {
|
|
132
|
-
await mkdir(dirname2(eventsFile), { recursive: true });
|
|
133
|
-
await appendFile(eventsFile, `${JSON.stringify(event)}
|
|
134
|
-
`, "utf-8");
|
|
135
|
-
}
|
|
136
|
-
function createEvent(runId, type, payload) {
|
|
137
|
-
return {
|
|
138
|
-
id: randomUUID(),
|
|
139
|
-
runId,
|
|
140
|
-
timestamp: new Date().toISOString(),
|
|
141
|
-
type,
|
|
142
|
-
payload
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
class RuntimeEventBus {
|
|
147
|
-
runId;
|
|
148
|
-
eventsFile;
|
|
149
|
-
memoryEvents = [];
|
|
150
|
-
constructor(options) {
|
|
151
|
-
this.runId = options.runId || randomUUID();
|
|
152
|
-
this.eventsFile = options.eventsFile ?? resolveRigLayout(options.projectRoot).controlPlaneEventsFile;
|
|
153
|
-
}
|
|
154
|
-
getRunId() {
|
|
155
|
-
return this.runId;
|
|
156
|
-
}
|
|
157
|
-
getEventsFile() {
|
|
158
|
-
return this.eventsFile;
|
|
159
|
-
}
|
|
160
|
-
getMemoryEvents() {
|
|
161
|
-
return [...this.memoryEvents];
|
|
162
|
-
}
|
|
163
|
-
async attachRuntimeBus(runtimeBus) {
|
|
164
|
-
if (runtimeBus.getEventsFile() === this.eventsFile) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
for (const event of this.memoryEvents) {
|
|
168
|
-
await runtimeBus.ingest(event);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
async attachRuntimeWorkspace(projectRoot) {
|
|
172
|
-
const runtimeBus = new RuntimeEventBus({ projectRoot, runId: this.runId });
|
|
173
|
-
await this.attachRuntimeBus(runtimeBus);
|
|
174
|
-
return runtimeBus;
|
|
175
|
-
}
|
|
176
|
-
async ingest(event) {
|
|
177
|
-
this.memoryEvents.push(event);
|
|
178
|
-
await appendEvent(this.eventsFile, event);
|
|
179
|
-
}
|
|
180
|
-
async emit(type, payload) {
|
|
181
|
-
const event = createEvent(this.runId, type, payload);
|
|
182
|
-
await this.ingest(event);
|
|
183
|
-
return event;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
class GeneralCliEventBus {
|
|
188
|
-
runId;
|
|
189
|
-
eventsFile;
|
|
190
|
-
memoryEvents = [];
|
|
191
|
-
runtimeBuses = new Map;
|
|
192
|
-
constructor(options) {
|
|
193
|
-
this.runId = options.runId || randomUUID();
|
|
194
|
-
this.eventsFile = options.eventsFile ?? resolve2(options.projectRoot, ".rig", "logs", "control-plane.events.jsonl");
|
|
195
|
-
}
|
|
196
|
-
getRunId() {
|
|
197
|
-
return this.runId;
|
|
198
|
-
}
|
|
199
|
-
getEventsFile() {
|
|
200
|
-
return this.eventsFile;
|
|
201
|
-
}
|
|
202
|
-
getMemoryEvents() {
|
|
203
|
-
return [...this.memoryEvents];
|
|
204
|
-
}
|
|
205
|
-
async attachRuntimeBus(runtimeBus) {
|
|
206
|
-
const key = runtimeBus.getEventsFile();
|
|
207
|
-
if (this.runtimeBuses.has(key)) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
this.runtimeBuses.set(key, runtimeBus);
|
|
211
|
-
for (const event of this.memoryEvents) {
|
|
212
|
-
await runtimeBus.ingest(event);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
async attachRuntimeWorkspace(projectRoot) {
|
|
216
|
-
const runtimeBus = new RuntimeEventBus({ projectRoot, runId: this.runId });
|
|
217
|
-
await this.attachRuntimeBus(runtimeBus);
|
|
218
|
-
return runtimeBus;
|
|
219
|
-
}
|
|
220
|
-
async emit(type, payload) {
|
|
221
|
-
const event = createEvent(this.runId, type, payload);
|
|
222
|
-
this.memoryEvents.push(event);
|
|
223
|
-
await appendEvent(this.eventsFile, event);
|
|
224
|
-
await Promise.all(Array.from(this.runtimeBuses.values()).map((bus) => bus.ingest(event)));
|
|
225
|
-
return event;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// packages/runtime/src/control-plane/runtime/plugins.ts
|
|
230
|
-
import { existsSync as existsSync5, readdirSync } from "fs";
|
|
231
|
-
import { resolve as resolve6, basename as basename2 } from "path";
|
|
232
|
-
|
|
233
|
-
// packages/runtime/src/control-plane/runtime/guard.ts
|
|
234
|
-
import { optimizeNextInvocation } from "bun:jsc";
|
|
235
|
-
import { existsSync as existsSync4, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
236
|
-
import { resolve as resolve5 } from "path";
|
|
237
|
-
|
|
238
|
-
// packages/runtime/src/control-plane/native/utils.ts
|
|
239
|
-
import { ptr as ptr2 } from "bun:ffi";
|
|
240
|
-
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
241
|
-
import { resolve as resolve4 } from "path";
|
|
242
|
-
|
|
243
135
|
// packages/runtime/src/control-plane/native/runtime-native.ts
|
|
244
136
|
import { dlopen, ptr, suffix, toBuffer } from "bun:ffi";
|
|
245
137
|
import { copyFileSync, existsSync as existsSync2, mkdirSync, renameSync, rmSync, statSync } from "fs";
|
|
246
138
|
import { tmpdir } from "os";
|
|
247
|
-
import { dirname as
|
|
248
|
-
var sharedNativeRuntimeOutputDir =
|
|
249
|
-
var sharedNativeRuntimeOutputPath =
|
|
139
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
140
|
+
var sharedNativeRuntimeOutputDir = resolve2(tmpdir(), "rig-native");
|
|
141
|
+
var sharedNativeRuntimeOutputPath = resolve2(sharedNativeRuntimeOutputDir, `runtime-native-${process.platform}-${process.arch}.${suffix}`);
|
|
250
142
|
var colocatedNativeRuntimeFileName = `runtime-native.${suffix}`;
|
|
251
143
|
var nativeRuntimeLibrary = await loadNativeRuntimeLibrary();
|
|
252
144
|
function requireNativeRuntimeLibrary(feature) {
|
|
@@ -282,12 +174,12 @@ async function loadNativeRuntimeLibrary() {
|
|
|
282
174
|
}
|
|
283
175
|
function nativePackageLibraryCandidates(fromDir, names) {
|
|
284
176
|
const candidates = [];
|
|
285
|
-
let cursor =
|
|
177
|
+
let cursor = resolve2(fromDir);
|
|
286
178
|
for (let index = 0;index < 8; index += 1) {
|
|
287
179
|
for (const name of names) {
|
|
288
|
-
candidates.push(
|
|
180
|
+
candidates.push(resolve2(cursor, "native", `${process.platform}-${process.arch}`, name), resolve2(cursor, "native", `${process.platform}-${process.arch}`, "lib", name), resolve2(cursor, "native", name), resolve2(cursor, "native", "lib", name));
|
|
289
181
|
}
|
|
290
|
-
const parent =
|
|
182
|
+
const parent = dirname2(cursor);
|
|
291
183
|
if (parent === cursor)
|
|
292
184
|
break;
|
|
293
185
|
cursor = parent;
|
|
@@ -296,17 +188,17 @@ function nativePackageLibraryCandidates(fromDir, names) {
|
|
|
296
188
|
}
|
|
297
189
|
function nativeRuntimeLibraryCandidates() {
|
|
298
190
|
const explicit = process.env.RIG_NATIVE_RUNTIME_LIB?.trim() || "";
|
|
299
|
-
const execDir = process.execPath?.trim() ?
|
|
191
|
+
const execDir = process.execPath?.trim() ? dirname2(process.execPath.trim()) : "";
|
|
300
192
|
const platformSpecific = `runtime-native-${process.platform}-${process.arch}.${suffix}`;
|
|
301
193
|
return [...new Set([
|
|
302
194
|
explicit,
|
|
303
195
|
...nativePackageLibraryCandidates(import.meta.dir, [colocatedNativeRuntimeFileName, platformSpecific]),
|
|
304
|
-
execDir ?
|
|
305
|
-
execDir ?
|
|
306
|
-
execDir ?
|
|
307
|
-
execDir ?
|
|
308
|
-
execDir ?
|
|
309
|
-
execDir ?
|
|
196
|
+
execDir ? resolve2(execDir, colocatedNativeRuntimeFileName) : "",
|
|
197
|
+
execDir ? resolve2(execDir, platformSpecific) : "",
|
|
198
|
+
execDir ? resolve2(execDir, "..", colocatedNativeRuntimeFileName) : "",
|
|
199
|
+
execDir ? resolve2(execDir, "..", platformSpecific) : "",
|
|
200
|
+
execDir ? resolve2(execDir, "lib", colocatedNativeRuntimeFileName) : "",
|
|
201
|
+
execDir ? resolve2(execDir, "..", "lib", colocatedNativeRuntimeFileName) : "",
|
|
310
202
|
sharedNativeRuntimeOutputPath
|
|
311
203
|
].filter(Boolean))];
|
|
312
204
|
}
|
|
@@ -315,7 +207,7 @@ function resolveNativeRuntimeSourcePath() {
|
|
|
315
207
|
if (explicit && existsSync2(explicit)) {
|
|
316
208
|
return explicit;
|
|
317
209
|
}
|
|
318
|
-
const bundled =
|
|
210
|
+
const bundled = resolve2(import.meta.dir, "../../../native/snapshot.zig");
|
|
319
211
|
return existsSync2(bundled) ? bundled : null;
|
|
320
212
|
}
|
|
321
213
|
async function buildNativeRuntimeLibrary(outputPath, options = {}) {
|
|
@@ -329,7 +221,7 @@ async function buildNativeRuntimeLibrary(outputPath, options = {}) {
|
|
|
329
221
|
}
|
|
330
222
|
const tempOutputPath = `${outputPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
331
223
|
try {
|
|
332
|
-
mkdirSync(
|
|
224
|
+
mkdirSync(dirname2(outputPath), { recursive: true });
|
|
333
225
|
const needsBuild = options.force === true || !existsSync2(outputPath) || statSync(sourcePath).mtimeMs > statSync(outputPath).mtimeMs;
|
|
334
226
|
if (!needsBuild) {
|
|
335
227
|
return true;
|
|
@@ -506,31 +398,31 @@ function unique(values) {
|
|
|
506
398
|
function resolveHarnessPaths(projectRoot) {
|
|
507
399
|
const hasRuntimeWorkspace = Boolean(process.env.RIG_TASK_WORKSPACE?.trim());
|
|
508
400
|
const monorepoRoot = resolveMonorepoRoot2(projectRoot);
|
|
509
|
-
const harnessRoot =
|
|
510
|
-
const stateRoot =
|
|
401
|
+
const harnessRoot = resolve3(projectRoot, "rig");
|
|
402
|
+
const stateRoot = resolve3(projectRoot, ".rig");
|
|
511
403
|
const layout = hasRuntimeWorkspace ? resolveRigLayout(projectRoot) : null;
|
|
512
|
-
const stateDir = layout?.stateDir ??
|
|
513
|
-
const logsDir = layout?.logsDir ??
|
|
514
|
-
const artifactsDir = layout?.artifactsRoot ??
|
|
515
|
-
const taskConfigPath = layout?.taskConfigPath ??
|
|
516
|
-
const binDir = layout?.binDir ??
|
|
404
|
+
const stateDir = layout?.stateDir ?? resolve3(stateRoot, "state");
|
|
405
|
+
const logsDir = layout?.logsDir ?? resolve3(stateRoot, "logs");
|
|
406
|
+
const artifactsDir = layout?.artifactsRoot ?? resolve3(monorepoRoot, "artifacts");
|
|
407
|
+
const taskConfigPath = layout?.taskConfigPath ?? resolve3(monorepoRoot, ".rig", "task-config.json");
|
|
408
|
+
const binDir = layout?.binDir ?? resolve3(stateRoot, "bin");
|
|
517
409
|
return {
|
|
518
410
|
harnessRoot,
|
|
519
411
|
stateDir: process.env.RIG_STATE_DIR || stateDir,
|
|
520
412
|
artifactsDir,
|
|
521
413
|
logsDir: process.env.RIG_LOGS_DIR || logsDir,
|
|
522
414
|
binDir,
|
|
523
|
-
hooksDir:
|
|
524
|
-
validationDir:
|
|
415
|
+
hooksDir: resolve3(harnessRoot, "hooks"),
|
|
416
|
+
validationDir: resolve3(harnessRoot, "validation"),
|
|
525
417
|
taskConfigPath,
|
|
526
|
-
sessionPath: process.env.RIG_SESSION_FILE ||
|
|
418
|
+
sessionPath: process.env.RIG_SESSION_FILE || resolve3(stateRoot, "session", "session.json"),
|
|
527
419
|
monorepoRoot,
|
|
528
|
-
tsApiTestsDir: process.env.TS_API_TESTS_DIR ||
|
|
529
|
-
taskRepoCommitsPath:
|
|
530
|
-
baseRepoPinsPath:
|
|
531
|
-
failedApproachesPath:
|
|
532
|
-
agentProfilePath:
|
|
533
|
-
reviewProfilePath:
|
|
420
|
+
tsApiTestsDir: process.env.TS_API_TESTS_DIR || resolve3(monorepoRoot, "TSAPITests"),
|
|
421
|
+
taskRepoCommitsPath: resolve3(stateDir, "task-repo-commits.json"),
|
|
422
|
+
baseRepoPinsPath: resolve3(stateDir, "base-repo-pins.json"),
|
|
423
|
+
failedApproachesPath: resolve3(stateDir, "failed_approaches.md"),
|
|
424
|
+
agentProfilePath: resolve3(stateDir, "agent-profile.json"),
|
|
425
|
+
reviewProfilePath: resolve3(stateDir, "review-profile.json")
|
|
534
426
|
};
|
|
535
427
|
}
|
|
536
428
|
function normalizeRelativeScopePath(inputPath) {
|
|
@@ -658,7 +550,7 @@ function loadPolicy(projectRoot) {
|
|
|
658
550
|
if (seededPolicyConfig) {
|
|
659
551
|
return seededPolicyConfig;
|
|
660
552
|
}
|
|
661
|
-
const configPath =
|
|
553
|
+
const configPath = resolve4(projectRoot, "rig/policy/policy.json");
|
|
662
554
|
if (!existsSync4(configPath)) {
|
|
663
555
|
return defaultPolicy();
|
|
664
556
|
}
|
|
@@ -889,28 +781,28 @@ function resolveAction(mode, matched) {
|
|
|
889
781
|
}
|
|
890
782
|
function resolveAbsolutePath(projectRoot, rawPath) {
|
|
891
783
|
if (rawPath.startsWith("/"))
|
|
892
|
-
return
|
|
893
|
-
return
|
|
784
|
+
return resolve4(rawPath);
|
|
785
|
+
return resolve4(projectRoot, rawPath);
|
|
894
786
|
}
|
|
895
787
|
function isHarnessPath(projectRoot, rawPath) {
|
|
896
788
|
const absPath = resolveAbsolutePath(projectRoot, rawPath);
|
|
897
789
|
const managedRoots = [
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
790
|
+
resolve4(projectRoot, "rig"),
|
|
791
|
+
resolve4(projectRoot, ".rig"),
|
|
792
|
+
resolve4(projectRoot, "artifacts")
|
|
901
793
|
];
|
|
902
794
|
return managedRoots.some((root) => absPath === root || absPath.startsWith(root + "/"));
|
|
903
795
|
}
|
|
904
796
|
function isRuntimePath(projectRoot, rawPath, taskWorkspace) {
|
|
905
797
|
const absPath = resolveAbsolutePath(projectRoot, rawPath);
|
|
906
798
|
if (taskWorkspace) {
|
|
907
|
-
const workspaceRigRoot =
|
|
908
|
-
const workspaceArtifactsRoot =
|
|
799
|
+
const workspaceRigRoot = resolve4(taskWorkspace, ".rig");
|
|
800
|
+
const workspaceArtifactsRoot = resolve4(taskWorkspace, "artifacts");
|
|
909
801
|
if (absPath === workspaceRigRoot || absPath.startsWith(workspaceRigRoot + "/") || absPath === workspaceArtifactsRoot || absPath.startsWith(workspaceArtifactsRoot + "/")) {
|
|
910
802
|
return true;
|
|
911
803
|
}
|
|
912
804
|
}
|
|
913
|
-
const runtimeRoot =
|
|
805
|
+
const runtimeRoot = resolve4(projectRoot, ".rig/runtime/agents");
|
|
914
806
|
return absPath === runtimeRoot || absPath.startsWith(runtimeRoot + "/");
|
|
915
807
|
}
|
|
916
808
|
function isTestFile(path) {
|
|
@@ -958,7 +850,7 @@ function evaluateScope(policy, context, filePath, access) {
|
|
|
958
850
|
return allowed();
|
|
959
851
|
}
|
|
960
852
|
if (context.taskWorkspace && context.taskWorkspace !== context.projectRoot && filePath.startsWith("/")) {
|
|
961
|
-
const absPath =
|
|
853
|
+
const absPath = resolve4(filePath);
|
|
962
854
|
if (!absPath.startsWith(context.taskWorkspace + "/") && !isHarnessPath(context.projectRoot, filePath)) {
|
|
963
855
|
const reason2 = `Absolute path '${filePath}' is outside task runtime boundary. Allowed root: ${context.taskWorkspace}`;
|
|
964
856
|
const matched2 = [{ id: "scope:workspace-boundary", category: "command", reason: reason2 }];
|
|
@@ -1167,12 +1059,6 @@ function extractContentFromToolInput(input) {
|
|
|
1167
1059
|
return input.new_string;
|
|
1168
1060
|
return "";
|
|
1169
1061
|
}
|
|
1170
|
-
function loadRuntimeImageConfig(projectRoot) {
|
|
1171
|
-
return loadPolicy(projectRoot).runtime_image ?? {
|
|
1172
|
-
deps: { ...DEFAULT_RUNTIME_IMAGE.deps },
|
|
1173
|
-
plugins_require_binaries: DEFAULT_RUNTIME_IMAGE.plugins_require_binaries
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
1062
|
var guardHotPathPrimed = false;
|
|
1177
1063
|
function primeGuardHotPaths() {
|
|
1178
1064
|
if (guardHotPathPrimed) {
|
|
@@ -1186,290 +1072,99 @@ function primeGuardHotPaths() {
|
|
|
1186
1072
|
}
|
|
1187
1073
|
primeGuardHotPaths();
|
|
1188
1074
|
|
|
1189
|
-
// packages/runtime/src/control-plane/
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
1194
|
-
}
|
|
1075
|
+
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
1076
|
+
import { existsSync as existsSync20, lstatSync, mkdirSync as mkdirSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync12 } from "fs";
|
|
1077
|
+
import { dirname as dirname10, isAbsolute as isAbsolute2, resolve as resolve23 } from "path";
|
|
1078
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1195
1079
|
|
|
1196
|
-
// packages/runtime/src/control-plane/runtime/
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
const pluginDir = resolve6(options.projectRoot, "rig/plugins");
|
|
1237
|
-
const runtimeImageConfig = loadRuntimeImageConfig(options.projectRoot);
|
|
1238
|
-
const localBinDir = options.runtimeContext ? resolve6(options.runtimeContext.binDir, "plugins") : resolve6(options.projectRoot, "rig/plugins");
|
|
1239
|
-
const legacyPluginScan = options.legacyPluginScan ?? isLegacyPluginScanEnabled(options.env);
|
|
1240
|
-
const files = legacyPluginScan ? safeReadDir(pluginDir).filter((entry) => /\.(ts|js|mjs|cjs)$/.test(entry)) : [];
|
|
1241
|
-
const pluginNames = files.map((file) => basename2(file).replace(/\.plugin\.(ts|js|mjs|cjs)$/, ""));
|
|
1242
|
-
const validatorProjectRoot = options.runtimeContext?.workspaceDir || options.projectRoot;
|
|
1243
|
-
const context = {
|
|
1244
|
-
projectRoot: validatorProjectRoot,
|
|
1245
|
-
runId: options.runId,
|
|
1246
|
-
eventBus: options.eventBus
|
|
1247
|
-
};
|
|
1248
|
-
return new PluginManager({
|
|
1249
|
-
eventBus: options.eventBus,
|
|
1250
|
-
context,
|
|
1251
|
-
pluginDir,
|
|
1252
|
-
pluginFiles: files,
|
|
1253
|
-
pluginNames,
|
|
1254
|
-
localBinDir,
|
|
1255
|
-
pluginsRequireBinaries: options.pluginsRequireBinaries ?? (runtimeImageConfig.plugins_require_binaries && Boolean(options.runtimeContext))
|
|
1256
|
-
});
|
|
1257
|
-
}
|
|
1258
|
-
list() {
|
|
1259
|
-
if (this.plugins) {
|
|
1260
|
-
return this.plugins.map((plugin) => ({
|
|
1261
|
-
name: plugin.name,
|
|
1262
|
-
validators: plugin.validators?.map((validator) => validator.id) ?? []
|
|
1263
|
-
}));
|
|
1264
|
-
}
|
|
1265
|
-
return this.pluginNames.map((name) => ({
|
|
1266
|
-
name,
|
|
1267
|
-
validators: []
|
|
1268
|
-
}));
|
|
1269
|
-
}
|
|
1270
|
-
async beforeCommand(ctx) {
|
|
1271
|
-
const plugins = await this.ensureLoaded();
|
|
1272
|
-
for (const plugin of plugins) {
|
|
1273
|
-
if (!plugin.beforeCommand) {
|
|
1274
|
-
continue;
|
|
1275
|
-
}
|
|
1276
|
-
await this.safeInvoke(plugin.name, "beforeCommand", () => plugin.beforeCommand?.(ctx, this.context));
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
async afterCommand(result) {
|
|
1280
|
-
const plugins = await this.ensureLoaded();
|
|
1281
|
-
for (const plugin of plugins) {
|
|
1282
|
-
if (!plugin.afterCommand) {
|
|
1283
|
-
continue;
|
|
1284
|
-
}
|
|
1285
|
-
await this.safeInvoke(plugin.name, "afterCommand", () => plugin.afterCommand?.(result, this.context));
|
|
1080
|
+
// packages/runtime/src/control-plane/runtime/baked-secrets.ts
|
|
1081
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
1082
|
+
import { resolve as resolve5 } from "path";
|
|
1083
|
+
var BAKED_RUNTIME_SECRETS = {
|
|
1084
|
+
ANTHROPIC_API_KEY: typeof RIG_BAKED_ANTHROPIC_API_KEY !== "undefined" ? RIG_BAKED_ANTHROPIC_API_KEY : "",
|
|
1085
|
+
OPENAI_API_KEY: typeof RIG_BAKED_OPENAI_API_KEY !== "undefined" ? RIG_BAKED_OPENAI_API_KEY : "",
|
|
1086
|
+
OPENROUTER_API_KEY: typeof RIG_BAKED_OPENROUTER_API_KEY !== "undefined" ? RIG_BAKED_OPENROUTER_API_KEY : "",
|
|
1087
|
+
AI_REVIEW_MODE: typeof RIG_BAKED_AI_REVIEW_MODE !== "undefined" ? RIG_BAKED_AI_REVIEW_MODE : "",
|
|
1088
|
+
AI_REVIEW_PROVIDER: typeof RIG_BAKED_AI_REVIEW_PROVIDER !== "undefined" ? RIG_BAKED_AI_REVIEW_PROVIDER : "",
|
|
1089
|
+
GREPTILE_API_BASE: typeof RIG_BAKED_GREPTILE_API_BASE !== "undefined" ? RIG_BAKED_GREPTILE_API_BASE : "",
|
|
1090
|
+
GREPTILE_REMOTE: typeof RIG_BAKED_GREPTILE_REMOTE !== "undefined" ? RIG_BAKED_GREPTILE_REMOTE : "",
|
|
1091
|
+
GREPTILE_REPOSITORY: typeof RIG_BAKED_GREPTILE_REPOSITORY !== "undefined" ? RIG_BAKED_GREPTILE_REPOSITORY : "",
|
|
1092
|
+
GREPTILE_CONTEXT_BRANCH: typeof RIG_BAKED_GREPTILE_CONTEXT_BRANCH !== "undefined" ? RIG_BAKED_GREPTILE_CONTEXT_BRANCH : "",
|
|
1093
|
+
GREPTILE_DEFAULT_BRANCH: typeof RIG_BAKED_GREPTILE_DEFAULT_BRANCH !== "undefined" ? RIG_BAKED_GREPTILE_DEFAULT_BRANCH : "",
|
|
1094
|
+
GREPTILE_API_KEY: typeof RIG_BAKED_GREPTILE_API_KEY !== "undefined" ? RIG_BAKED_GREPTILE_API_KEY : "",
|
|
1095
|
+
GREPTILE_GITHUB_TOKEN: typeof RIG_BAKED_GREPTILE_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GREPTILE_GITHUB_TOKEN : "",
|
|
1096
|
+
GREPTILE_POLL_ATTEMPTS: typeof RIG_BAKED_GREPTILE_POLL_ATTEMPTS !== "undefined" ? RIG_BAKED_GREPTILE_POLL_ATTEMPTS : "",
|
|
1097
|
+
GREPTILE_POLL_INTERVAL_MS: typeof RIG_BAKED_GREPTILE_POLL_INTERVAL_MS !== "undefined" ? RIG_BAKED_GREPTILE_POLL_INTERVAL_MS : "",
|
|
1098
|
+
GH_TOKEN: typeof RIG_BAKED_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GITHUB_TOKEN : "",
|
|
1099
|
+
GITHUB_TOKEN: typeof RIG_BAKED_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GITHUB_TOKEN : "",
|
|
1100
|
+
GITHUB_SSH_KEY: typeof RIG_BAKED_GITHUB_SSH_KEY !== "undefined" ? RIG_BAKED_GITHUB_SSH_KEY : "",
|
|
1101
|
+
AWS_ACCESS_KEY_ID: typeof RIG_BAKED_AWS_ACCESS_KEY_ID !== "undefined" ? RIG_BAKED_AWS_ACCESS_KEY_ID : "",
|
|
1102
|
+
AWS_SECRET_ACCESS_KEY: typeof RIG_BAKED_AWS_SECRET_ACCESS_KEY !== "undefined" ? RIG_BAKED_AWS_SECRET_ACCESS_KEY : "",
|
|
1103
|
+
AWS_REGION: typeof RIG_BAKED_AWS_REGION !== "undefined" ? RIG_BAKED_AWS_REGION : "",
|
|
1104
|
+
LINEAR_API_KEY: typeof RIG_BAKED_LINEAR_API_KEY !== "undefined" ? RIG_BAKED_LINEAR_API_KEY : "",
|
|
1105
|
+
LINEAR_WEBHOOK_SECRET: typeof RIG_BAKED_LINEAR_WEBHOOK_SECRET !== "undefined" ? RIG_BAKED_LINEAR_WEBHOOK_SECRET : ""
|
|
1106
|
+
};
|
|
1107
|
+
function resolveRuntimeSecrets(env, baked = BAKED_RUNTIME_SECRETS) {
|
|
1108
|
+
const resolved = {};
|
|
1109
|
+
const keys = new Set([
|
|
1110
|
+
...Object.keys(BAKED_RUNTIME_SECRETS),
|
|
1111
|
+
...Object.keys(baked)
|
|
1112
|
+
]);
|
|
1113
|
+
for (const key of keys) {
|
|
1114
|
+
const envValue = env[key]?.trim();
|
|
1115
|
+
const bakedValue = baked[key]?.trim();
|
|
1116
|
+
if (envValue) {
|
|
1117
|
+
resolved[key] = envValue;
|
|
1118
|
+
} else if (bakedValue) {
|
|
1119
|
+
resolved[key] = bakedValue;
|
|
1286
1120
|
}
|
|
1287
1121
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
if (!plugin.onEvent) {
|
|
1295
|
-
continue;
|
|
1296
|
-
}
|
|
1297
|
-
await this.safeInvoke(plugin.name, "onEvent", () => plugin.onEvent?.(event, this.context));
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
async runValidators(taskId) {
|
|
1301
|
-
const plugins = await this.ensureLoaded();
|
|
1302
|
-
const results = [];
|
|
1303
|
-
for (const plugin of plugins) {
|
|
1304
|
-
for (const validator of plugin.validators ?? []) {
|
|
1305
|
-
await this.eventBus.emit("validator.started", {
|
|
1306
|
-
plugin: plugin.name,
|
|
1307
|
-
validator: validator.id,
|
|
1308
|
-
taskId
|
|
1309
|
-
});
|
|
1310
|
-
try {
|
|
1311
|
-
const result = await validator.run({ taskId, projectRoot: this.context.projectRoot }, this.context);
|
|
1312
|
-
results.push(result);
|
|
1313
|
-
await this.eventBus.emit("validator.finished", {
|
|
1314
|
-
plugin: plugin.name,
|
|
1315
|
-
validator: validator.id,
|
|
1316
|
-
taskId,
|
|
1317
|
-
passed: result.passed,
|
|
1318
|
-
summary: result.summary
|
|
1319
|
-
});
|
|
1320
|
-
} catch (error) {
|
|
1321
|
-
const failed = {
|
|
1322
|
-
id: validator.id,
|
|
1323
|
-
passed: false,
|
|
1324
|
-
summary: `${plugin.name}/${validator.id} failed unexpectedly`,
|
|
1325
|
-
details: `${error}`
|
|
1326
|
-
};
|
|
1327
|
-
results.push(failed);
|
|
1328
|
-
await this.eventBus.emit("validator.finished", {
|
|
1329
|
-
plugin: plugin.name,
|
|
1330
|
-
validator: validator.id,
|
|
1331
|
-
taskId,
|
|
1332
|
-
passed: false,
|
|
1333
|
-
summary: failed.summary,
|
|
1334
|
-
details: failed.details
|
|
1335
|
-
});
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
return results;
|
|
1122
|
+
return resolved;
|
|
1123
|
+
}
|
|
1124
|
+
function loadDotEnvSecrets(projectRoot, env = process.env) {
|
|
1125
|
+
const dotenvPath = resolve5(projectRoot, ".env");
|
|
1126
|
+
if (!existsSync5(dotenvPath)) {
|
|
1127
|
+
return {};
|
|
1340
1128
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
phase: hook,
|
|
1348
|
-
error: `${error}`
|
|
1349
|
-
});
|
|
1129
|
+
const parsed = {};
|
|
1130
|
+
const lines = readFileSync3(dotenvPath, "utf-8").split(/\r?\n/);
|
|
1131
|
+
for (const rawLine of lines) {
|
|
1132
|
+
const line = rawLine.trim();
|
|
1133
|
+
if (!line || line.startsWith("#")) {
|
|
1134
|
+
continue;
|
|
1350
1135
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
return this.plugins;
|
|
1136
|
+
const exportMatch = line.match(/^(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)$/);
|
|
1137
|
+
if (!exportMatch) {
|
|
1138
|
+
continue;
|
|
1355
1139
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1140
|
+
const key = exportMatch[1];
|
|
1141
|
+
if (!(key in BAKED_RUNTIME_SECRETS)) {
|
|
1142
|
+
continue;
|
|
1358
1143
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
return this.plugins;
|
|
1363
|
-
} finally {
|
|
1364
|
-
this.loadPromise = null;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
resolveBinPath(binName) {
|
|
1368
|
-
const candidates = [this.localBinDir].filter(Boolean).map((dir) => resolve6(dir, binName));
|
|
1369
|
-
return candidates.find((candidate) => existsSync5(candidate));
|
|
1370
|
-
}
|
|
1371
|
-
async loadCompiledPlugins() {
|
|
1372
|
-
const plugins = [];
|
|
1373
|
-
for (const file of this.pluginFiles) {
|
|
1374
|
-
const binName = basename2(file).replace(/\.plugin\.(ts|js|mjs|cjs)$/, "");
|
|
1375
|
-
let binPath = this.resolveBinPath(binName);
|
|
1376
|
-
if (!binPath) {
|
|
1377
|
-
const triedPaths = [this.localBinDir].filter(Boolean).map((dir) => resolve6(dir, binName));
|
|
1378
|
-
const missingError = `Compiled plugin binary not found for '${binName}'. Tried: ${triedPaths.join(", ")}`;
|
|
1379
|
-
await this.eventBus.emit("plugin.error", {
|
|
1380
|
-
file: resolve6(this.pluginDir, file),
|
|
1381
|
-
phase: "load",
|
|
1382
|
-
error: missingError
|
|
1383
|
-
});
|
|
1384
|
-
if (this.pluginsRequireBinaries) {
|
|
1385
|
-
throw new Error(missingError);
|
|
1386
|
-
}
|
|
1387
|
-
plugins.push({
|
|
1388
|
-
name: binName,
|
|
1389
|
-
validators: []
|
|
1390
|
-
});
|
|
1391
|
-
await this.eventBus.emit("plugin.loaded", {
|
|
1392
|
-
plugin: binName,
|
|
1393
|
-
file: resolve6(this.pluginDir, file),
|
|
1394
|
-
source: "metadata-only"
|
|
1395
|
-
});
|
|
1396
|
-
continue;
|
|
1397
|
-
}
|
|
1398
|
-
const wrapper = createBinaryPluginWrapper(binName, binPath, this.context.projectRoot);
|
|
1399
|
-
plugins.push(wrapper);
|
|
1400
|
-
await this.eventBus.emit("plugin.loaded", {
|
|
1401
|
-
plugin: wrapper.name,
|
|
1402
|
-
file: binPath,
|
|
1403
|
-
source: "compiled-binary"
|
|
1404
|
-
});
|
|
1144
|
+
const value = expandShellValue(exportMatch[2] ?? "", { ...env, ...parsed });
|
|
1145
|
+
if (value) {
|
|
1146
|
+
parsed[key] = value;
|
|
1405
1147
|
}
|
|
1406
|
-
return plugins;
|
|
1407
1148
|
}
|
|
1149
|
+
return parsed;
|
|
1408
1150
|
}
|
|
1409
|
-
function
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
{
|
|
1414
|
-
id: `${name}:compiled`,
|
|
1415
|
-
async run(ctx) {
|
|
1416
|
-
const proc = Bun.spawn([binPath, "--validate", ctx.taskId, ctx.projectRoot], {
|
|
1417
|
-
cwd: projectRoot,
|
|
1418
|
-
stdout: "pipe",
|
|
1419
|
-
stderr: "pipe"
|
|
1420
|
-
});
|
|
1421
|
-
const exitCode = await proc.exited;
|
|
1422
|
-
const stdout = await new Response(proc.stdout).text();
|
|
1423
|
-
const stderr = await new Response(proc.stderr).text();
|
|
1424
|
-
if (exitCode !== 0) {
|
|
1425
|
-
return {
|
|
1426
|
-
id: `${name}:compiled`,
|
|
1427
|
-
passed: false,
|
|
1428
|
-
summary: `Plugin binary ${name} exited with code ${exitCode}`,
|
|
1429
|
-
details: stderr || stdout
|
|
1430
|
-
};
|
|
1431
|
-
}
|
|
1432
|
-
try {
|
|
1433
|
-
const results = JSON.parse(stdout.trim());
|
|
1434
|
-
const failed = results.filter((r) => !r.passed);
|
|
1435
|
-
if (failed.length > 0) {
|
|
1436
|
-
return {
|
|
1437
|
-
id: `${name}:compiled`,
|
|
1438
|
-
passed: false,
|
|
1439
|
-
summary: `${failed.length} of ${results.length} validator(s) failed`,
|
|
1440
|
-
details: failed.map((f) => `${f.id}: ${f.summary}`).join(`
|
|
1441
|
-
`)
|
|
1442
|
-
};
|
|
1443
|
-
}
|
|
1444
|
-
return {
|
|
1445
|
-
id: `${name}:compiled`,
|
|
1446
|
-
passed: true,
|
|
1447
|
-
summary: `All ${results.length} validator(s) passed`
|
|
1448
|
-
};
|
|
1449
|
-
} catch {
|
|
1450
|
-
return {
|
|
1451
|
-
id: `${name}:compiled`,
|
|
1452
|
-
passed: false,
|
|
1453
|
-
summary: `Failed to parse output from compiled plugin ${name}`,
|
|
1454
|
-
details: stdout.slice(0, 500)
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
]
|
|
1460
|
-
};
|
|
1461
|
-
}
|
|
1462
|
-
function safeReadDir(path) {
|
|
1463
|
-
try {
|
|
1464
|
-
return readdirSync(path, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
|
|
1465
|
-
} catch {
|
|
1466
|
-
return [];
|
|
1151
|
+
function expandShellValue(rawValue, env) {
|
|
1152
|
+
let value = rawValue.trim();
|
|
1153
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1154
|
+
value = value.slice(1, -1);
|
|
1467
1155
|
}
|
|
1156
|
+
return value.replace(/\$\{([A-Z0-9_]+)(:-([^}]*))?\}/g, (_match, name, _defaultGroup, fallback) => {
|
|
1157
|
+
const envValue = env[name]?.trim();
|
|
1158
|
+
if (envValue) {
|
|
1159
|
+
return envValue;
|
|
1160
|
+
}
|
|
1161
|
+
return fallback ?? "";
|
|
1162
|
+
});
|
|
1468
1163
|
}
|
|
1469
1164
|
|
|
1470
1165
|
// packages/runtime/src/control-plane/runtime/context.ts
|
|
1471
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as
|
|
1472
|
-
import { dirname as
|
|
1166
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
1167
|
+
import { dirname as dirname3, resolve as resolve6 } from "path";
|
|
1473
1168
|
var RUNTIME_CONTEXT_ENV = "RIG_RUNTIME_CONTEXT_FILE";
|
|
1474
1169
|
var runtimeContextStringFields = [
|
|
1475
1170
|
"runtimeId",
|
|
@@ -1493,13 +1188,13 @@ var runtimeContextOptionalStringFields = [
|
|
|
1493
1188
|
"monorepoBaseCommit"
|
|
1494
1189
|
];
|
|
1495
1190
|
function loadRuntimeContext(path) {
|
|
1496
|
-
const absPath =
|
|
1191
|
+
const absPath = resolve6(path);
|
|
1497
1192
|
if (!existsSync6(absPath)) {
|
|
1498
1193
|
throw new Error(`RuntimeTaskContext file not found: ${absPath}`);
|
|
1499
1194
|
}
|
|
1500
1195
|
let raw;
|
|
1501
1196
|
try {
|
|
1502
|
-
raw = JSON.parse(
|
|
1197
|
+
raw = JSON.parse(readFileSync4(absPath, "utf8"));
|
|
1503
1198
|
} catch (err) {
|
|
1504
1199
|
throw new Error(`Failed to parse RuntimeTaskContext at ${absPath}: ${String(err)}`);
|
|
1505
1200
|
}
|
|
@@ -1622,13 +1317,13 @@ function loadRuntimeContextFromEnv(env = process.env) {
|
|
|
1622
1317
|
return loadRuntimeContext(inferred);
|
|
1623
1318
|
}
|
|
1624
1319
|
function findRuntimeContextFile(startPath) {
|
|
1625
|
-
let current =
|
|
1320
|
+
let current = resolve6(startPath);
|
|
1626
1321
|
while (true) {
|
|
1627
|
-
const candidate =
|
|
1322
|
+
const candidate = resolve6(current, "runtime-context.json");
|
|
1628
1323
|
if (existsSync6(candidate) && isAgentRuntimeContextPath(candidate)) {
|
|
1629
1324
|
return candidate;
|
|
1630
1325
|
}
|
|
1631
|
-
const parent =
|
|
1326
|
+
const parent = dirname3(current);
|
|
1632
1327
|
if (parent === current) {
|
|
1633
1328
|
return "";
|
|
1634
1329
|
}
|
|
@@ -1640,98 +1335,8 @@ function isAgentRuntimeContextPath(path) {
|
|
|
1640
1335
|
return /\/\.rig\/runtime-context\.json$/.test(normalized);
|
|
1641
1336
|
}
|
|
1642
1337
|
|
|
1643
|
-
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
1644
|
-
import { existsSync as existsSync20, lstatSync, mkdirSync as mkdirSync10, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
|
|
1645
|
-
import { dirname as dirname11, isAbsolute as isAbsolute2, resolve as resolve23 } from "path";
|
|
1646
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1647
|
-
|
|
1648
|
-
// packages/runtime/src/control-plane/runtime/baked-secrets.ts
|
|
1649
|
-
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
|
|
1650
|
-
import { resolve as resolve8 } from "path";
|
|
1651
|
-
var BAKED_RUNTIME_SECRETS = {
|
|
1652
|
-
ANTHROPIC_API_KEY: typeof RIG_BAKED_ANTHROPIC_API_KEY !== "undefined" ? RIG_BAKED_ANTHROPIC_API_KEY : "",
|
|
1653
|
-
OPENAI_API_KEY: typeof RIG_BAKED_OPENAI_API_KEY !== "undefined" ? RIG_BAKED_OPENAI_API_KEY : "",
|
|
1654
|
-
OPENROUTER_API_KEY: typeof RIG_BAKED_OPENROUTER_API_KEY !== "undefined" ? RIG_BAKED_OPENROUTER_API_KEY : "",
|
|
1655
|
-
AI_REVIEW_MODE: typeof RIG_BAKED_AI_REVIEW_MODE !== "undefined" ? RIG_BAKED_AI_REVIEW_MODE : "",
|
|
1656
|
-
AI_REVIEW_PROVIDER: typeof RIG_BAKED_AI_REVIEW_PROVIDER !== "undefined" ? RIG_BAKED_AI_REVIEW_PROVIDER : "",
|
|
1657
|
-
GREPTILE_API_BASE: typeof RIG_BAKED_GREPTILE_API_BASE !== "undefined" ? RIG_BAKED_GREPTILE_API_BASE : "",
|
|
1658
|
-
GREPTILE_REMOTE: typeof RIG_BAKED_GREPTILE_REMOTE !== "undefined" ? RIG_BAKED_GREPTILE_REMOTE : "",
|
|
1659
|
-
GREPTILE_REPOSITORY: typeof RIG_BAKED_GREPTILE_REPOSITORY !== "undefined" ? RIG_BAKED_GREPTILE_REPOSITORY : "",
|
|
1660
|
-
GREPTILE_CONTEXT_BRANCH: typeof RIG_BAKED_GREPTILE_CONTEXT_BRANCH !== "undefined" ? RIG_BAKED_GREPTILE_CONTEXT_BRANCH : "",
|
|
1661
|
-
GREPTILE_DEFAULT_BRANCH: typeof RIG_BAKED_GREPTILE_DEFAULT_BRANCH !== "undefined" ? RIG_BAKED_GREPTILE_DEFAULT_BRANCH : "",
|
|
1662
|
-
GREPTILE_API_KEY: typeof RIG_BAKED_GREPTILE_API_KEY !== "undefined" ? RIG_BAKED_GREPTILE_API_KEY : "",
|
|
1663
|
-
GREPTILE_GITHUB_TOKEN: typeof RIG_BAKED_GREPTILE_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GREPTILE_GITHUB_TOKEN : "",
|
|
1664
|
-
GREPTILE_POLL_ATTEMPTS: typeof RIG_BAKED_GREPTILE_POLL_ATTEMPTS !== "undefined" ? RIG_BAKED_GREPTILE_POLL_ATTEMPTS : "",
|
|
1665
|
-
GREPTILE_POLL_INTERVAL_MS: typeof RIG_BAKED_GREPTILE_POLL_INTERVAL_MS !== "undefined" ? RIG_BAKED_GREPTILE_POLL_INTERVAL_MS : "",
|
|
1666
|
-
GH_TOKEN: typeof RIG_BAKED_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GITHUB_TOKEN : "",
|
|
1667
|
-
GITHUB_TOKEN: typeof RIG_BAKED_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GITHUB_TOKEN : "",
|
|
1668
|
-
GITHUB_SSH_KEY: typeof RIG_BAKED_GITHUB_SSH_KEY !== "undefined" ? RIG_BAKED_GITHUB_SSH_KEY : "",
|
|
1669
|
-
AWS_ACCESS_KEY_ID: typeof RIG_BAKED_AWS_ACCESS_KEY_ID !== "undefined" ? RIG_BAKED_AWS_ACCESS_KEY_ID : "",
|
|
1670
|
-
AWS_SECRET_ACCESS_KEY: typeof RIG_BAKED_AWS_SECRET_ACCESS_KEY !== "undefined" ? RIG_BAKED_AWS_SECRET_ACCESS_KEY : "",
|
|
1671
|
-
AWS_REGION: typeof RIG_BAKED_AWS_REGION !== "undefined" ? RIG_BAKED_AWS_REGION : "",
|
|
1672
|
-
LINEAR_API_KEY: typeof RIG_BAKED_LINEAR_API_KEY !== "undefined" ? RIG_BAKED_LINEAR_API_KEY : "",
|
|
1673
|
-
LINEAR_WEBHOOK_SECRET: typeof RIG_BAKED_LINEAR_WEBHOOK_SECRET !== "undefined" ? RIG_BAKED_LINEAR_WEBHOOK_SECRET : ""
|
|
1674
|
-
};
|
|
1675
|
-
function resolveRuntimeSecrets(env, baked = BAKED_RUNTIME_SECRETS) {
|
|
1676
|
-
const resolved = {};
|
|
1677
|
-
const keys = new Set([
|
|
1678
|
-
...Object.keys(BAKED_RUNTIME_SECRETS),
|
|
1679
|
-
...Object.keys(baked)
|
|
1680
|
-
]);
|
|
1681
|
-
for (const key of keys) {
|
|
1682
|
-
const envValue = env[key]?.trim();
|
|
1683
|
-
const bakedValue = baked[key]?.trim();
|
|
1684
|
-
if (envValue) {
|
|
1685
|
-
resolved[key] = envValue;
|
|
1686
|
-
} else if (bakedValue) {
|
|
1687
|
-
resolved[key] = bakedValue;
|
|
1688
|
-
}
|
|
1689
|
-
}
|
|
1690
|
-
return resolved;
|
|
1691
|
-
}
|
|
1692
|
-
function loadDotEnvSecrets(projectRoot, env = process.env) {
|
|
1693
|
-
const dotenvPath = resolve8(projectRoot, ".env");
|
|
1694
|
-
if (!existsSync7(dotenvPath)) {
|
|
1695
|
-
return {};
|
|
1696
|
-
}
|
|
1697
|
-
const parsed = {};
|
|
1698
|
-
const lines = readFileSync4(dotenvPath, "utf-8").split(/\r?\n/);
|
|
1699
|
-
for (const rawLine of lines) {
|
|
1700
|
-
const line = rawLine.trim();
|
|
1701
|
-
if (!line || line.startsWith("#")) {
|
|
1702
|
-
continue;
|
|
1703
|
-
}
|
|
1704
|
-
const exportMatch = line.match(/^(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)$/);
|
|
1705
|
-
if (!exportMatch) {
|
|
1706
|
-
continue;
|
|
1707
|
-
}
|
|
1708
|
-
const key = exportMatch[1];
|
|
1709
|
-
if (!(key in BAKED_RUNTIME_SECRETS)) {
|
|
1710
|
-
continue;
|
|
1711
|
-
}
|
|
1712
|
-
const value = expandShellValue(exportMatch[2] ?? "", { ...env, ...parsed });
|
|
1713
|
-
if (value) {
|
|
1714
|
-
parsed[key] = value;
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
return parsed;
|
|
1718
|
-
}
|
|
1719
|
-
function expandShellValue(rawValue, env) {
|
|
1720
|
-
let value = rawValue.trim();
|
|
1721
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1722
|
-
value = value.slice(1, -1);
|
|
1723
|
-
}
|
|
1724
|
-
return value.replace(/\$\{([A-Z0-9_]+)(:-([^}]*))?\}/g, (_match, name, _defaultGroup, fallback) => {
|
|
1725
|
-
const envValue = env[name]?.trim();
|
|
1726
|
-
if (envValue) {
|
|
1727
|
-
return envValue;
|
|
1728
|
-
}
|
|
1729
|
-
return fallback ?? "";
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
1338
|
// packages/runtime/src/control-plane/native/task-ops.ts
|
|
1734
|
-
import { appendFileSync, existsSync as existsSync19, mkdirSync as
|
|
1339
|
+
import { appendFileSync, existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
1735
1340
|
import { resolve as resolve22 } from "path";
|
|
1736
1341
|
|
|
1737
1342
|
// packages/runtime/src/build-time-config.ts
|
|
@@ -1758,14 +1363,14 @@ function readBuildConfig() {
|
|
|
1758
1363
|
|
|
1759
1364
|
// packages/runtime/src/control-plane/runtime/tooling/shell.ts
|
|
1760
1365
|
import { tmpdir as tmpdir2 } from "os";
|
|
1761
|
-
import { basename as
|
|
1762
|
-
var sharedNativeShellOutputDir =
|
|
1763
|
-
var sharedNativeShellOutputPath =
|
|
1366
|
+
import { basename as basename2, dirname as dirname4, resolve as resolve7 } from "path";
|
|
1367
|
+
var sharedNativeShellOutputDir = resolve7(tmpdir2(), "rig-native");
|
|
1368
|
+
var sharedNativeShellOutputPath = resolve7(sharedNativeShellOutputDir, `rig-shell-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
|
|
1764
1369
|
// packages/runtime/src/control-plane/runtime/tooling/file-tools.ts
|
|
1765
1370
|
import { tmpdir as tmpdir3 } from "os";
|
|
1766
|
-
import { basename as
|
|
1767
|
-
var sharedNativeToolsOutputDir =
|
|
1768
|
-
var sharedNativeToolsOutputPath =
|
|
1371
|
+
import { basename as basename3, dirname as dirname5, resolve as resolve8 } from "path";
|
|
1372
|
+
var sharedNativeToolsOutputDir = resolve8(tmpdir3(), "rig-native");
|
|
1373
|
+
var sharedNativeToolsOutputPath = resolve8(sharedNativeToolsOutputDir, `rig-tools-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
|
|
1769
1374
|
// packages/runtime/src/control-plane/plugin-host-context.ts
|
|
1770
1375
|
import { createPluginHost } from "@rig/core";
|
|
1771
1376
|
import { loadConfig } from "@rig/core/load-config";
|
|
@@ -1912,7 +1517,7 @@ function createTaskFieldRegistry(extensions) {
|
|
|
1912
1517
|
}
|
|
1913
1518
|
|
|
1914
1519
|
// packages/runtime/src/control-plane/validators/runtime-registration.ts
|
|
1915
|
-
import { existsSync as
|
|
1520
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1916
1521
|
import { join } from "path";
|
|
1917
1522
|
function createValidatorRegistry() {
|
|
1918
1523
|
const map = new Map;
|
|
@@ -1945,7 +1550,7 @@ function registerBuiltInValidators(registry) {
|
|
|
1945
1550
|
}
|
|
1946
1551
|
async function runStdTypecheck(ctx) {
|
|
1947
1552
|
const packageJsonPath = join(ctx.workspaceRoot, "package.json");
|
|
1948
|
-
if (!
|
|
1553
|
+
if (!existsSync7(packageJsonPath)) {
|
|
1949
1554
|
return {
|
|
1950
1555
|
id: "std:typecheck",
|
|
1951
1556
|
passed: false,
|
|
@@ -1973,8 +1578,8 @@ async function runStdTypecheck(ctx) {
|
|
|
1973
1578
|
}
|
|
1974
1579
|
|
|
1975
1580
|
// packages/runtime/src/control-plane/hook-materializer.ts
|
|
1976
|
-
import { existsSync as
|
|
1977
|
-
import { dirname as
|
|
1581
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
1582
|
+
import { dirname as dirname6, resolve as resolve9 } from "path";
|
|
1978
1583
|
var MARKER_PLUGIN = "_rigPlugin";
|
|
1979
1584
|
var MARKER_HOOK_ID = "_rigHookId";
|
|
1980
1585
|
function matcherToString(matcher) {
|
|
@@ -1988,8 +1593,8 @@ function isPluginOwned(cmd) {
|
|
|
1988
1593
|
return typeof cmd[MARKER_PLUGIN] === "string";
|
|
1989
1594
|
}
|
|
1990
1595
|
function materializeHooks(projectRoot, entries) {
|
|
1991
|
-
const settingsPath =
|
|
1992
|
-
const existing =
|
|
1596
|
+
const settingsPath = resolve9(projectRoot, ".claude", "settings.json");
|
|
1597
|
+
const existing = existsSync8(settingsPath) ? safeReadJson(settingsPath) : {};
|
|
1993
1598
|
const hooks = existing.hooks ?? {};
|
|
1994
1599
|
for (const event of Object.keys(hooks)) {
|
|
1995
1600
|
const groups = hooks[event] ?? [];
|
|
@@ -2031,7 +1636,7 @@ function materializeHooks(projectRoot, entries) {
|
|
|
2031
1636
|
} else {
|
|
2032
1637
|
delete next.hooks;
|
|
2033
1638
|
}
|
|
2034
|
-
mkdirSync3(
|
|
1639
|
+
mkdirSync3(dirname6(settingsPath), { recursive: true });
|
|
2035
1640
|
writeFileSync2(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
2036
1641
|
`, "utf-8");
|
|
2037
1642
|
return settingsPath;
|
|
@@ -2044,6 +1649,49 @@ function safeReadJson(path) {
|
|
|
2044
1649
|
}
|
|
2045
1650
|
}
|
|
2046
1651
|
|
|
1652
|
+
// packages/runtime/src/control-plane/skill-materializer.ts
|
|
1653
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync6, readdirSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1654
|
+
import { resolve as resolve10 } from "path";
|
|
1655
|
+
import { loadSkill } from "@rig/skill-loader";
|
|
1656
|
+
var MARKER_FILENAME = ".rig-plugin";
|
|
1657
|
+
function skillDirName(id) {
|
|
1658
|
+
return id.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
1659
|
+
}
|
|
1660
|
+
async function materializeSkills(projectRoot, entries) {
|
|
1661
|
+
const skillsRoot = resolve10(projectRoot, ".pi", "skills");
|
|
1662
|
+
if (existsSync9(skillsRoot)) {
|
|
1663
|
+
for (const name of readdirSync(skillsRoot)) {
|
|
1664
|
+
const dir = resolve10(skillsRoot, name);
|
|
1665
|
+
if (existsSync9(resolve10(dir, MARKER_FILENAME))) {
|
|
1666
|
+
rmSync2(dir, { recursive: true, force: true });
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
const written = [];
|
|
1671
|
+
for (const { pluginName, skill } of entries) {
|
|
1672
|
+
const sourcePath = resolve10(projectRoot, skill.path);
|
|
1673
|
+
if (!existsSync9(sourcePath)) {
|
|
1674
|
+
console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${sourcePath} does not exist`);
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
let body;
|
|
1678
|
+
try {
|
|
1679
|
+
await loadSkill(sourcePath);
|
|
1680
|
+
body = readFileSync6(sourcePath, "utf-8");
|
|
1681
|
+
} catch (err) {
|
|
1682
|
+
console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${err instanceof Error ? err.message : err}`);
|
|
1683
|
+
continue;
|
|
1684
|
+
}
|
|
1685
|
+
const dir = resolve10(skillsRoot, skillDirName(skill.id));
|
|
1686
|
+
mkdirSync4(dir, { recursive: true });
|
|
1687
|
+
writeFileSync3(resolve10(dir, "SKILL.md"), body, "utf-8");
|
|
1688
|
+
writeFileSync3(resolve10(dir, MARKER_FILENAME), `${JSON.stringify({ plugin: pluginName, skillId: skill.id }, null, 2)}
|
|
1689
|
+
`, "utf-8");
|
|
1690
|
+
written.push({ id: skill.id, pluginName, directory: dir });
|
|
1691
|
+
}
|
|
1692
|
+
return written;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
2047
1695
|
// packages/runtime/src/control-plane/plugin-host-context.ts
|
|
2048
1696
|
async function buildPluginHostContext(projectRoot) {
|
|
2049
1697
|
let config;
|
|
@@ -2080,6 +1728,17 @@ async function buildPluginHostContext(projectRoot) {
|
|
|
2080
1728
|
} catch (err) {
|
|
2081
1729
|
console.warn(`[plugin-host] hook materialization failed: ${err instanceof Error ? err.message : err}`);
|
|
2082
1730
|
}
|
|
1731
|
+
try {
|
|
1732
|
+
const skillEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.skills ?? []).map((skill) => ({
|
|
1733
|
+
pluginName: plugin.name,
|
|
1734
|
+
skill
|
|
1735
|
+
})));
|
|
1736
|
+
if (skillEntries.length > 0) {
|
|
1737
|
+
await materializeSkills(projectRoot, skillEntries);
|
|
1738
|
+
}
|
|
1739
|
+
} catch (err) {
|
|
1740
|
+
console.warn(`[plugin-host] skill materialization failed: ${err instanceof Error ? err.message : err}`);
|
|
1741
|
+
}
|
|
2083
1742
|
return {
|
|
2084
1743
|
config,
|
|
2085
1744
|
pluginHost,
|
|
@@ -2093,12 +1752,12 @@ async function buildPluginHostContext(projectRoot) {
|
|
|
2093
1752
|
|
|
2094
1753
|
// packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
|
|
2095
1754
|
import { spawnSync } from "child_process";
|
|
2096
|
-
import { existsSync as existsSync11, readFileSync as
|
|
2097
|
-
import { basename as
|
|
1755
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8, readdirSync as readdirSync2, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1756
|
+
import { basename as basename4, join as join2, resolve as resolve12 } from "path";
|
|
2098
1757
|
|
|
2099
1758
|
// packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
|
|
2100
|
-
import { existsSync as existsSync10, readFileSync as
|
|
2101
|
-
import { resolve as
|
|
1759
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
1760
|
+
import { resolve as resolve11 } from "path";
|
|
2102
1761
|
|
|
2103
1762
|
// packages/runtime/src/control-plane/tasks/task-record-reader.ts
|
|
2104
1763
|
async function findTaskById(reader, id) {
|
|
@@ -2121,7 +1780,7 @@ class LegacyTaskConfigReadError extends Error {
|
|
|
2121
1780
|
}
|
|
2122
1781
|
}
|
|
2123
1782
|
function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
|
|
2124
|
-
const configPath = options.configPath ??
|
|
1783
|
+
const configPath = options.configPath ?? resolve11(projectRoot, ".rig", "task-config.json");
|
|
2125
1784
|
const reader = {
|
|
2126
1785
|
async listTasks() {
|
|
2127
1786
|
return readLegacyTaskRecords(projectRoot, configPath);
|
|
@@ -2132,7 +1791,7 @@ function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
|
|
|
2132
1791
|
};
|
|
2133
1792
|
return reader;
|
|
2134
1793
|
}
|
|
2135
|
-
function readLegacyTaskRecords(projectRoot, configPath =
|
|
1794
|
+
function readLegacyTaskRecords(projectRoot, configPath = resolve11(projectRoot, ".rig", "task-config.json")) {
|
|
2136
1795
|
if (!existsSync10(configPath)) {
|
|
2137
1796
|
return [];
|
|
2138
1797
|
}
|
|
@@ -2141,7 +1800,7 @@ function readLegacyTaskRecords(projectRoot, configPath = resolve12(projectRoot,
|
|
|
2141
1800
|
}
|
|
2142
1801
|
function readLegacyTaskConfigJson(projectRoot, configPath) {
|
|
2143
1802
|
try {
|
|
2144
|
-
const parsed = JSON.parse(
|
|
1803
|
+
const parsed = JSON.parse(readFileSync7(configPath, "utf8"));
|
|
2145
1804
|
if (isPlainRecord(parsed)) {
|
|
2146
1805
|
return parsed;
|
|
2147
1806
|
}
|
|
@@ -2225,7 +1884,7 @@ function isPlainRecord(candidate) {
|
|
|
2225
1884
|
var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
|
|
2226
1885
|
var FILE_TASK_PATTERN = /\.(task\.)?json$/;
|
|
2227
1886
|
function createSourceAwareTaskConfigRecordReader(projectRoot, options = {}) {
|
|
2228
|
-
const configPath = options.configPath ??
|
|
1887
|
+
const configPath = options.configPath ?? resolve12(projectRoot, ".rig", "task-config.json");
|
|
2229
1888
|
const legacy = createLegacyTaskConfigRecordReader(projectRoot, { configPath });
|
|
2230
1889
|
const spawnFn = options.spawn ?? spawnSync;
|
|
2231
1890
|
const ghBinary = options.ghBinary ?? "gh";
|
|
@@ -2291,7 +1950,7 @@ async function readSourceAwareTaskStatus(projectRoot, taskId, options = {}) {
|
|
|
2291
1950
|
}
|
|
2292
1951
|
}
|
|
2293
1952
|
function updateSourceAwareTaskConfigTask(projectRoot, taskId, update, options = {}) {
|
|
2294
|
-
const configPath = options.configPath ??
|
|
1953
|
+
const configPath = options.configPath ?? resolve12(projectRoot, ".rig", "task-config.json");
|
|
2295
1954
|
const rawEntry = readRawTaskEntry(configPath, taskId);
|
|
2296
1955
|
if (!rawEntry) {
|
|
2297
1956
|
const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
|
|
@@ -2344,10 +2003,10 @@ function readMaterializedTaskMetadata(entry) {
|
|
|
2344
2003
|
return metadata;
|
|
2345
2004
|
}
|
|
2346
2005
|
function readConfiguredFilesTaskSourcePath(projectRoot) {
|
|
2347
|
-
const jsonPath =
|
|
2006
|
+
const jsonPath = resolve12(projectRoot, "rig.config.json");
|
|
2348
2007
|
if (existsSync11(jsonPath)) {
|
|
2349
2008
|
try {
|
|
2350
|
-
const parsed = JSON.parse(
|
|
2009
|
+
const parsed = JSON.parse(readFileSync8(jsonPath, "utf8"));
|
|
2351
2010
|
if (isPlainRecord2(parsed) && isPlainRecord2(parsed.taskSource)) {
|
|
2352
2011
|
const source = parsed.taskSource;
|
|
2353
2012
|
return source.kind === "files" && typeof source.path === "string" ? source.path : null;
|
|
@@ -2356,12 +2015,12 @@ function readConfiguredFilesTaskSourcePath(projectRoot) {
|
|
|
2356
2015
|
return null;
|
|
2357
2016
|
}
|
|
2358
2017
|
}
|
|
2359
|
-
const tsPath =
|
|
2018
|
+
const tsPath = resolve12(projectRoot, "rig.config.ts");
|
|
2360
2019
|
if (!existsSync11(tsPath)) {
|
|
2361
2020
|
return null;
|
|
2362
2021
|
}
|
|
2363
2022
|
try {
|
|
2364
|
-
const source =
|
|
2023
|
+
const source = readFileSync8(tsPath, "utf8");
|
|
2365
2024
|
const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
|
|
2366
2025
|
const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
|
|
2367
2026
|
if (kind !== "files") {
|
|
@@ -2384,7 +2043,7 @@ function readRawTaskConfig(configPath) {
|
|
|
2384
2043
|
if (!existsSync11(configPath)) {
|
|
2385
2044
|
return null;
|
|
2386
2045
|
}
|
|
2387
|
-
const parsed = JSON.parse(
|
|
2046
|
+
const parsed = JSON.parse(readFileSync8(configPath, "utf8"));
|
|
2388
2047
|
return isPlainRecord2(parsed) ? parsed : null;
|
|
2389
2048
|
}
|
|
2390
2049
|
function stripLegacyTaskConfigMetadata2(raw) {
|
|
@@ -2401,16 +2060,16 @@ function writeLegacyTaskStatus(configPath, taskId, status) {
|
|
|
2401
2060
|
return;
|
|
2402
2061
|
}
|
|
2403
2062
|
entry.status = status;
|
|
2404
|
-
|
|
2063
|
+
writeFileSync4(configPath, `${JSON.stringify(rawConfig, null, 2)}
|
|
2405
2064
|
`, "utf8");
|
|
2406
2065
|
}
|
|
2407
2066
|
function updateFileBackedTask(projectRoot, sourcePath, taskId, update) {
|
|
2408
|
-
const directory =
|
|
2067
|
+
const directory = resolve12(projectRoot, sourcePath);
|
|
2409
2068
|
const file = findFileBackedTaskFile(directory, taskId);
|
|
2410
2069
|
if (!file) {
|
|
2411
2070
|
return false;
|
|
2412
2071
|
}
|
|
2413
|
-
const raw = JSON.parse(
|
|
2072
|
+
const raw = JSON.parse(readFileSync8(file, "utf8"));
|
|
2414
2073
|
if (!isPlainRecord2(raw)) {
|
|
2415
2074
|
return false;
|
|
2416
2075
|
}
|
|
@@ -2427,12 +2086,12 @@ function updateFileBackedTask(projectRoot, sourcePath, taskId, update) {
|
|
|
2427
2086
|
{ body: update.comment, createdAt: new Date().toISOString(), source: "rig" }
|
|
2428
2087
|
];
|
|
2429
2088
|
}
|
|
2430
|
-
|
|
2089
|
+
writeFileSync4(file, `${JSON.stringify(raw, null, 2)}
|
|
2431
2090
|
`, "utf8");
|
|
2432
2091
|
return true;
|
|
2433
2092
|
}
|
|
2434
2093
|
function listFileBackedTasks(projectRoot, sourcePath) {
|
|
2435
|
-
const directory =
|
|
2094
|
+
const directory = resolve12(projectRoot, sourcePath);
|
|
2436
2095
|
if (!existsSync11(directory)) {
|
|
2437
2096
|
return [];
|
|
2438
2097
|
}
|
|
@@ -2440,7 +2099,7 @@ function listFileBackedTasks(projectRoot, sourcePath) {
|
|
|
2440
2099
|
for (const name of readdirSync2(directory)) {
|
|
2441
2100
|
if (!FILE_TASK_PATTERN.test(name))
|
|
2442
2101
|
continue;
|
|
2443
|
-
const inferredId =
|
|
2102
|
+
const inferredId = basename4(name).replace(FILE_TASK_PATTERN, "");
|
|
2444
2103
|
const task = readFileBackedTask(projectRoot, sourcePath, inferredId, {});
|
|
2445
2104
|
if (task)
|
|
2446
2105
|
tasks.push(task);
|
|
@@ -2448,11 +2107,11 @@ function listFileBackedTasks(projectRoot, sourcePath) {
|
|
|
2448
2107
|
return tasks;
|
|
2449
2108
|
}
|
|
2450
2109
|
function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
|
|
2451
|
-
const file = findFileBackedTaskFile(
|
|
2110
|
+
const file = findFileBackedTaskFile(resolve12(projectRoot, sourcePath), taskId);
|
|
2452
2111
|
if (!file) {
|
|
2453
2112
|
return null;
|
|
2454
2113
|
}
|
|
2455
|
-
const raw = JSON.parse(
|
|
2114
|
+
const raw = JSON.parse(readFileSync8(file, "utf8"));
|
|
2456
2115
|
if (!isPlainRecord2(raw)) {
|
|
2457
2116
|
return null;
|
|
2458
2117
|
}
|
|
@@ -2475,8 +2134,8 @@ function findFileBackedTaskFile(directory, taskId) {
|
|
|
2475
2134
|
try {
|
|
2476
2135
|
if (!statSync3(file).isFile())
|
|
2477
2136
|
continue;
|
|
2478
|
-
const raw = JSON.parse(
|
|
2479
|
-
const inferredId =
|
|
2137
|
+
const raw = JSON.parse(readFileSync8(file, "utf8"));
|
|
2138
|
+
const inferredId = basename4(file).replace(FILE_TASK_PATTERN, "");
|
|
2480
2139
|
const id = isPlainRecord2(raw) && typeof raw.id === "string" ? raw.id : inferredId;
|
|
2481
2140
|
if (id === taskId) {
|
|
2482
2141
|
return file;
|
|
@@ -2645,8 +2304,8 @@ function ensureStatusLabel(bin, repo, spawnFn, label) {
|
|
|
2645
2304
|
}
|
|
2646
2305
|
}
|
|
2647
2306
|
function selectedGitHubEnv() {
|
|
2648
|
-
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim()
|
|
2649
|
-
return { GH_TOKEN: token, GITHUB_TOKEN: token };
|
|
2307
|
+
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
|
|
2308
|
+
return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
|
|
2650
2309
|
}
|
|
2651
2310
|
function ghSpawnOptions() {
|
|
2652
2311
|
return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
|
|
@@ -2824,8 +2483,8 @@ function buildTaskRunLifecycleComment(input) {
|
|
|
2824
2483
|
}
|
|
2825
2484
|
|
|
2826
2485
|
// packages/runtime/src/control-plane/native/task-state.ts
|
|
2827
|
-
import { existsSync as existsSync13, readFileSync as
|
|
2828
|
-
import { basename as
|
|
2486
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10, readdirSync as readdirSync3, statSync as statSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
2487
|
+
import { basename as basename5, resolve as resolve14 } from "path";
|
|
2829
2488
|
|
|
2830
2489
|
// packages/runtime/src/control-plane/state-sync/types.ts
|
|
2831
2490
|
var CANONICAL_TASK_LIFECYCLE_STATUSES = new Set([
|
|
@@ -2840,23 +2499,23 @@ var CANONICAL_TASK_LIFECYCLE_STATUSES = new Set([
|
|
|
2840
2499
|
"cancelled"
|
|
2841
2500
|
]);
|
|
2842
2501
|
// packages/runtime/src/control-plane/native/git-native.ts
|
|
2843
|
-
import { chmodSync, copyFileSync as copyFileSync2, existsSync as existsSync12, mkdirSync as
|
|
2502
|
+
import { chmodSync, copyFileSync as copyFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync9, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
2844
2503
|
import { tmpdir as tmpdir4 } from "os";
|
|
2845
|
-
import { dirname as
|
|
2504
|
+
import { dirname as dirname7, isAbsolute, resolve as resolve13 } from "path";
|
|
2846
2505
|
import { createHash } from "crypto";
|
|
2847
|
-
var sharedGitNativeOutputDir =
|
|
2848
|
-
var sharedGitNativeOutputPath =
|
|
2506
|
+
var sharedGitNativeOutputDir = resolve13(tmpdir4(), "rig-native");
|
|
2507
|
+
var sharedGitNativeOutputPath = resolve13(sharedGitNativeOutputDir, `rig-git-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
|
|
2849
2508
|
var trackerCommandUsageProbe = "usage: rig-git fetch-ref <repo-path> <remote> <branch>";
|
|
2850
2509
|
function temporaryGitBinaryOutputPath(outputPath) {
|
|
2851
2510
|
const suffix2 = process.platform === "win32" ? ".exe" : "";
|
|
2852
|
-
return
|
|
2511
|
+
return resolve13(dirname7(outputPath), `.rig-git-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}${suffix2}`);
|
|
2853
2512
|
}
|
|
2854
2513
|
function publishGitBinary(tempOutputPath, outputPath) {
|
|
2855
2514
|
try {
|
|
2856
2515
|
renameSync2(tempOutputPath, outputPath);
|
|
2857
2516
|
} catch (error) {
|
|
2858
2517
|
if (process.platform === "win32" && existsSync12(outputPath)) {
|
|
2859
|
-
|
|
2518
|
+
rmSync3(outputPath, { force: true });
|
|
2860
2519
|
renameSync2(tempOutputPath, outputPath);
|
|
2861
2520
|
return;
|
|
2862
2521
|
}
|
|
@@ -2867,27 +2526,27 @@ function runtimeRigGitFileName() {
|
|
|
2867
2526
|
return `rig-git${process.platform === "win32" ? ".exe" : ""}`;
|
|
2868
2527
|
}
|
|
2869
2528
|
function rigGitSourceCandidates() {
|
|
2870
|
-
const execDir = process.execPath?.trim() ?
|
|
2529
|
+
const execDir = process.execPath?.trim() ? dirname7(process.execPath.trim()) : "";
|
|
2871
2530
|
const cwd = process.cwd()?.trim() || "";
|
|
2872
2531
|
const projectRoot = process.env.PROJECT_RIG_ROOT?.trim() || "";
|
|
2873
2532
|
const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim() || "";
|
|
2874
|
-
const moduleRelativeSource =
|
|
2533
|
+
const moduleRelativeSource = resolve13(import.meta.dir, "../../../native/rig-git.zig");
|
|
2875
2534
|
return [...new Set([
|
|
2876
2535
|
process.env.RIG_NATIVE_GIT_SOURCE?.trim() || "",
|
|
2877
2536
|
moduleRelativeSource,
|
|
2878
|
-
projectRoot ?
|
|
2879
|
-
hostProjectRoot ?
|
|
2880
|
-
cwd ?
|
|
2881
|
-
execDir ?
|
|
2882
|
-
execDir ?
|
|
2537
|
+
projectRoot ? resolve13(projectRoot, "packages/runtime/native/rig-git.zig") : "",
|
|
2538
|
+
hostProjectRoot ? resolve13(hostProjectRoot, "packages/runtime/native/rig-git.zig") : "",
|
|
2539
|
+
cwd ? resolve13(cwd, "packages/runtime/native/rig-git.zig") : "",
|
|
2540
|
+
execDir ? resolve13(execDir, "..", "..", "packages/runtime/native/rig-git.zig") : "",
|
|
2541
|
+
execDir ? resolve13(execDir, "..", "native", "rig-git.zig") : ""
|
|
2883
2542
|
].filter(Boolean))];
|
|
2884
2543
|
}
|
|
2885
2544
|
function nativePackageBinaryCandidates(fromDir, fileName) {
|
|
2886
2545
|
const candidates = [];
|
|
2887
|
-
let cursor =
|
|
2546
|
+
let cursor = resolve13(fromDir);
|
|
2888
2547
|
for (let index = 0;index < 8; index += 1) {
|
|
2889
|
-
candidates.push(
|
|
2890
|
-
const parent =
|
|
2548
|
+
candidates.push(resolve13(cursor, "native", `${process.platform}-${process.arch}`, fileName), resolve13(cursor, "native", `${process.platform}-${process.arch}`, "bin", fileName), resolve13(cursor, "native", fileName), resolve13(cursor, "native", "bin", fileName));
|
|
2549
|
+
const parent = dirname7(cursor);
|
|
2891
2550
|
if (parent === cursor)
|
|
2892
2551
|
break;
|
|
2893
2552
|
cursor = parent;
|
|
@@ -2895,15 +2554,15 @@ function nativePackageBinaryCandidates(fromDir, fileName) {
|
|
|
2895
2554
|
return candidates;
|
|
2896
2555
|
}
|
|
2897
2556
|
function rigGitBinaryCandidates() {
|
|
2898
|
-
const execDir = process.execPath?.trim() ?
|
|
2557
|
+
const execDir = process.execPath?.trim() ? dirname7(process.execPath.trim()) : "";
|
|
2899
2558
|
const fileName = runtimeRigGitFileName();
|
|
2900
2559
|
const explicit = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
|
|
2901
2560
|
return [...new Set([
|
|
2902
2561
|
explicit,
|
|
2903
2562
|
...nativePackageBinaryCandidates(import.meta.dir, fileName),
|
|
2904
|
-
execDir ?
|
|
2905
|
-
execDir ?
|
|
2906
|
-
execDir ?
|
|
2563
|
+
execDir ? resolve13(execDir, fileName) : "",
|
|
2564
|
+
execDir ? resolve13(execDir, "..", fileName) : "",
|
|
2565
|
+
execDir ? resolve13(execDir, "..", "bin", fileName) : "",
|
|
2907
2566
|
sharedGitNativeOutputPath
|
|
2908
2567
|
].filter(Boolean))];
|
|
2909
2568
|
}
|
|
@@ -2954,14 +2613,14 @@ function hasMatchingNativeBuildManifestSync(manifestPath, buildKey) {
|
|
|
2954
2613
|
return false;
|
|
2955
2614
|
}
|
|
2956
2615
|
try {
|
|
2957
|
-
const manifest = JSON.parse(
|
|
2616
|
+
const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
|
|
2958
2617
|
return manifest.version === 1 && manifest.buildKey === buildKey;
|
|
2959
2618
|
} catch {
|
|
2960
2619
|
return false;
|
|
2961
2620
|
}
|
|
2962
2621
|
}
|
|
2963
2622
|
function sha256FileSync(path) {
|
|
2964
|
-
return createHash("sha256").update(
|
|
2623
|
+
return createHash("sha256").update(readFileSync9(path)).digest("hex");
|
|
2965
2624
|
}
|
|
2966
2625
|
function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath()) {
|
|
2967
2626
|
if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
|
|
@@ -2979,7 +2638,7 @@ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath())
|
|
|
2979
2638
|
if (!zigBinary) {
|
|
2980
2639
|
throw new Error("zig is required to build native Rig git tools.");
|
|
2981
2640
|
}
|
|
2982
|
-
|
|
2641
|
+
mkdirSync5(dirname7(outputPath), { recursive: true });
|
|
2983
2642
|
const sourceDigest = sha256FileSync(sourcePath);
|
|
2984
2643
|
const buildKey = JSON.stringify({
|
|
2985
2644
|
version: 1,
|
|
@@ -3004,7 +2663,7 @@ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath())
|
|
|
3004
2663
|
"ReleaseFast",
|
|
3005
2664
|
`-femit-bin=${tempOutputPath}`
|
|
3006
2665
|
], {
|
|
3007
|
-
cwd:
|
|
2666
|
+
cwd: dirname7(sourcePath),
|
|
3008
2667
|
stdout: "pipe",
|
|
3009
2668
|
stderr: "pipe"
|
|
3010
2669
|
});
|
|
@@ -3017,16 +2676,16 @@ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath())
|
|
|
3017
2676
|
}
|
|
3018
2677
|
chmodSync(tempOutputPath, 493);
|
|
3019
2678
|
if (existsSync12(outputPath) && hasMatchingNativeBuildManifestSync(manifestPath, buildKey)) {
|
|
3020
|
-
|
|
2679
|
+
rmSync3(tempOutputPath, { force: true });
|
|
3021
2680
|
chmodSync(outputPath, 493);
|
|
3022
2681
|
return outputPath;
|
|
3023
2682
|
}
|
|
3024
2683
|
publishGitBinary(tempOutputPath, outputPath);
|
|
3025
2684
|
if (!binarySupportsTrackerCommandsSync(outputPath)) {
|
|
3026
|
-
|
|
2685
|
+
rmSync3(outputPath, { force: true });
|
|
3027
2686
|
throw new Error("Failed to build native Rig git tools: tracker command probe failed");
|
|
3028
2687
|
}
|
|
3029
|
-
|
|
2688
|
+
writeFileSync5(manifestPath, `${JSON.stringify({ version: 1, buildKey }, null, 2)}
|
|
3030
2689
|
`, "utf8");
|
|
3031
2690
|
return outputPath;
|
|
3032
2691
|
}
|
|
@@ -3135,7 +2794,7 @@ function readValidationDescriptions(projectRoot) {
|
|
|
3135
2794
|
return readValidationDescriptionMap(raw);
|
|
3136
2795
|
}
|
|
3137
2796
|
function readSourceValidationDescriptions(projectRoot) {
|
|
3138
|
-
const rootRaw = readJsonFile(
|
|
2797
|
+
const rootRaw = readJsonFile(resolve14(projectRoot, "rig", "task-config.json"), {});
|
|
3139
2798
|
const sourcePath = findSourceTaskConfigPath(projectRoot);
|
|
3140
2799
|
const sourceRaw = sourcePath ? readJsonFile(sourcePath, {}) : {};
|
|
3141
2800
|
const rootDescriptions = readValidationDescriptionMap(rootRaw);
|
|
@@ -3226,16 +2885,16 @@ function lookupTask(projectRoot, input) {
|
|
|
3226
2885
|
function artifactDirForId(projectRoot, id) {
|
|
3227
2886
|
const workspaceDir = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
3228
2887
|
if (workspaceDir) {
|
|
3229
|
-
const worktreeArtifacts =
|
|
3230
|
-
if (existsSync13(worktreeArtifacts) || existsSync13(
|
|
2888
|
+
const worktreeArtifacts = resolve14(workspaceDir, "artifacts", id);
|
|
2889
|
+
if (existsSync13(worktreeArtifacts) || existsSync13(resolve14(workspaceDir, "artifacts"))) {
|
|
3231
2890
|
return worktreeArtifacts;
|
|
3232
2891
|
}
|
|
3233
2892
|
}
|
|
3234
2893
|
try {
|
|
3235
2894
|
const paths = resolveHarnessPaths(projectRoot);
|
|
3236
|
-
return
|
|
2895
|
+
return resolve14(paths.artifactsDir, id);
|
|
3237
2896
|
} catch {
|
|
3238
|
-
return
|
|
2897
|
+
return resolve14(resolveMonorepoRoot2(projectRoot), "artifacts", id);
|
|
3239
2898
|
}
|
|
3240
2899
|
}
|
|
3241
2900
|
function resolveTaskConfigPath(projectRoot) {
|
|
@@ -3265,7 +2924,7 @@ function readAndSyncSourceTaskConfig(projectRoot) {
|
|
|
3265
2924
|
const synced = synchronizeTaskConfigWithTracker(projectRoot, raw);
|
|
3266
2925
|
if (sourcePath && synced.updated) {
|
|
3267
2926
|
try {
|
|
3268
|
-
|
|
2927
|
+
writeFileSync6(sourcePath, `${JSON.stringify(synced.config, null, 2)}
|
|
3269
2928
|
`, "utf-8");
|
|
3270
2929
|
} catch {}
|
|
3271
2930
|
}
|
|
@@ -3317,12 +2976,12 @@ function shouldRefreshAutoSyncedTaskConfigEntry(entry) {
|
|
|
3317
2976
|
return !candidate.role;
|
|
3318
2977
|
}
|
|
3319
2978
|
function readSourceIssueRecords(projectRoot) {
|
|
3320
|
-
const issuesPath =
|
|
2979
|
+
const issuesPath = resolve14(resolveMonorepoRoot2(projectRoot), ".beads", "issues.jsonl");
|
|
3321
2980
|
if (!existsSync13(issuesPath)) {
|
|
3322
2981
|
return [];
|
|
3323
2982
|
}
|
|
3324
2983
|
const records = [];
|
|
3325
|
-
for (const line of
|
|
2984
|
+
for (const line of readFileSync10(issuesPath, "utf-8").split(/\r?\n/)) {
|
|
3326
2985
|
const trimmed = line.trim();
|
|
3327
2986
|
if (!trimmed) {
|
|
3328
2987
|
continue;
|
|
@@ -3378,7 +3037,7 @@ function readConfiguredFileTaskConfig(projectRoot) {
|
|
|
3378
3037
|
if (!sourcePath) {
|
|
3379
3038
|
return {};
|
|
3380
3039
|
}
|
|
3381
|
-
const directory =
|
|
3040
|
+
const directory = resolve14(projectRoot, sourcePath);
|
|
3382
3041
|
if (!existsSync13(directory)) {
|
|
3383
3042
|
return {};
|
|
3384
3043
|
}
|
|
@@ -3386,15 +3045,15 @@ function readConfiguredFileTaskConfig(projectRoot) {
|
|
|
3386
3045
|
for (const name of readdirSync3(directory)) {
|
|
3387
3046
|
if (!FILE_TASK_PATTERN2.test(name))
|
|
3388
3047
|
continue;
|
|
3389
|
-
const file =
|
|
3048
|
+
const file = resolve14(directory, name);
|
|
3390
3049
|
try {
|
|
3391
3050
|
if (!statSync4(file).isFile())
|
|
3392
3051
|
continue;
|
|
3393
|
-
const raw = JSON.parse(
|
|
3052
|
+
const raw = JSON.parse(readFileSync10(file, "utf8"));
|
|
3394
3053
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
3395
3054
|
continue;
|
|
3396
3055
|
const record = raw;
|
|
3397
|
-
const inferredId =
|
|
3056
|
+
const inferredId = basename5(name).replace(FILE_TASK_PATTERN2, "");
|
|
3398
3057
|
const id = typeof record.id === "string" && record.id.trim().length > 0 ? record.id.trim() : inferredId;
|
|
3399
3058
|
config[id] = fileTaskToConfigEntry(record, { kind: "files", path: sourcePath });
|
|
3400
3059
|
} catch {}
|
|
@@ -3432,10 +3091,10 @@ function firstStringList2(...candidates) {
|
|
|
3432
3091
|
return [];
|
|
3433
3092
|
}
|
|
3434
3093
|
function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
3435
|
-
const jsonPath =
|
|
3094
|
+
const jsonPath = resolve14(projectRoot, "rig.config.json");
|
|
3436
3095
|
if (existsSync13(jsonPath)) {
|
|
3437
3096
|
try {
|
|
3438
|
-
const parsed = JSON.parse(
|
|
3097
|
+
const parsed = JSON.parse(readFileSync10(jsonPath, "utf8"));
|
|
3439
3098
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3440
3099
|
const taskSource = parsed.taskSource;
|
|
3441
3100
|
if (taskSource && typeof taskSource === "object" && !Array.isArray(taskSource)) {
|
|
@@ -3447,12 +3106,12 @@ function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
|
3447
3106
|
return null;
|
|
3448
3107
|
}
|
|
3449
3108
|
}
|
|
3450
|
-
const tsPath =
|
|
3109
|
+
const tsPath = resolve14(projectRoot, "rig.config.ts");
|
|
3451
3110
|
if (!existsSync13(tsPath)) {
|
|
3452
3111
|
return null;
|
|
3453
3112
|
}
|
|
3454
3113
|
try {
|
|
3455
|
-
const source =
|
|
3114
|
+
const source = readFileSync10(tsPath, "utf8");
|
|
3456
3115
|
const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
|
|
3457
3116
|
const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
|
|
3458
3117
|
if (kind !== "files") {
|
|
@@ -3466,23 +3125,23 @@ function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
|
3466
3125
|
function sourceTaskConfigCandidates(projectRoot) {
|
|
3467
3126
|
const runtimeContext = loadRuntimeContextFromEnv();
|
|
3468
3127
|
return [
|
|
3469
|
-
runtimeContext?.monorepoMainRoot ?
|
|
3470
|
-
process.env.MONOREPO_MAIN_ROOT?.trim() ?
|
|
3471
|
-
|
|
3128
|
+
runtimeContext?.monorepoMainRoot ? resolve14(runtimeContext.monorepoMainRoot, ".rig", "task-config.json") : "",
|
|
3129
|
+
process.env.MONOREPO_MAIN_ROOT?.trim() ? resolve14(process.env.MONOREPO_MAIN_ROOT.trim(), ".rig", "task-config.json") : "",
|
|
3130
|
+
resolve14(resolveMonorepoRoot2(projectRoot), ".rig", "task-config.json")
|
|
3472
3131
|
].filter(Boolean);
|
|
3473
3132
|
}
|
|
3474
3133
|
|
|
3475
3134
|
// packages/runtime/src/control-plane/native/validator.ts
|
|
3476
|
-
import { existsSync as existsSync17, mkdirSync as
|
|
3477
|
-
import { resolve as
|
|
3135
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
3136
|
+
import { resolve as resolve19 } from "path";
|
|
3478
3137
|
|
|
3479
3138
|
// packages/runtime/src/control-plane/native/validator-binaries.ts
|
|
3480
|
-
import { existsSync as existsSync16, mkdirSync as
|
|
3481
|
-
import { dirname as
|
|
3139
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7, rmSync as rmSync5, statSync as statSync5 } from "fs";
|
|
3140
|
+
import { dirname as dirname9, resolve as resolve18 } from "path";
|
|
3482
3141
|
|
|
3483
3142
|
// packages/runtime/src/binary-run.ts
|
|
3484
|
-
import { chmodSync as chmodSync2, cpSync, existsSync as existsSync14, mkdirSync as
|
|
3485
|
-
import { basename as
|
|
3143
|
+
import { chmodSync as chmodSync2, cpSync, existsSync as existsSync14, mkdirSync as mkdirSync6, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
3144
|
+
import { basename as basename6, dirname as dirname8, resolve as resolve15 } from "path";
|
|
3486
3145
|
import { fileURLToPath } from "url";
|
|
3487
3146
|
import { drainMicrotasks, gcAndSweep } from "bun:jsc";
|
|
3488
3147
|
var runtimeBinaryBuildQueue = Promise.resolve();
|
|
@@ -3508,9 +3167,9 @@ async function buildRuntimeBinary(options) {
|
|
|
3508
3167
|
});
|
|
3509
3168
|
}
|
|
3510
3169
|
async function buildRuntimeBinaryInProcess(options, manifest) {
|
|
3511
|
-
const tempBuildDir =
|
|
3512
|
-
const tempOutputPath =
|
|
3513
|
-
|
|
3170
|
+
const tempBuildDir = resolve15(dirname8(options.outputPath), `.bun-build-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
3171
|
+
const tempOutputPath = resolve15(tempBuildDir, basename6(options.outputPath));
|
|
3172
|
+
mkdirSync6(tempBuildDir, { recursive: true });
|
|
3514
3173
|
await withTemporaryEnv({
|
|
3515
3174
|
...options.env,
|
|
3516
3175
|
...options.define ? { RIG_BUILD_CONFIG_JSON: JSON.stringify(options.define) } : {}
|
|
@@ -3550,7 +3209,7 @@ async function buildRuntimeBinaryInProcess(options, manifest) {
|
|
|
3550
3209
|
});
|
|
3551
3210
|
}
|
|
3552
3211
|
})).finally(() => {
|
|
3553
|
-
|
|
3212
|
+
rmSync4(tempBuildDir, { recursive: true, force: true });
|
|
3554
3213
|
});
|
|
3555
3214
|
}
|
|
3556
3215
|
function runBestEffortBuildGc() {
|
|
@@ -3567,8 +3226,8 @@ function runtimeBinaryCacheManifestPath(outputPath) {
|
|
|
3567
3226
|
function resolveRuntimeBinaryBuildOptions(options) {
|
|
3568
3227
|
return {
|
|
3569
3228
|
...options,
|
|
3570
|
-
entrypoint:
|
|
3571
|
-
outputPath:
|
|
3229
|
+
entrypoint: resolve15(options.cwd, options.sourcePath),
|
|
3230
|
+
outputPath: resolve15(options.outputPath)
|
|
3572
3231
|
};
|
|
3573
3232
|
}
|
|
3574
3233
|
function shouldUseRuntimeBinaryBuildWorker() {
|
|
@@ -3613,13 +3272,13 @@ async function buildRuntimeBinaryViaWorker(options) {
|
|
|
3613
3272
|
new Response(build.stdout).text(),
|
|
3614
3273
|
new Response(build.stderr).text()
|
|
3615
3274
|
]);
|
|
3616
|
-
|
|
3275
|
+
rmSync4(payloadPath, { force: true });
|
|
3617
3276
|
if (exitCode !== 0) {
|
|
3618
3277
|
throw new Error(`Failed to build ${options.entrypoint}: ${(stderr || stdout || `worker exited ${exitCode}`).trim()}`);
|
|
3619
3278
|
}
|
|
3620
3279
|
}
|
|
3621
3280
|
function createRuntimeBinaryBuildWorkerPayloadPath(outputPath) {
|
|
3622
|
-
return
|
|
3281
|
+
return resolve15(dirname8(outputPath), `.bun-build-worker-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
|
3623
3282
|
}
|
|
3624
3283
|
function resolveRuntimeBinaryBuildWorkerSourcePath(options) {
|
|
3625
3284
|
const envRoots = [
|
|
@@ -3628,12 +3287,12 @@ function resolveRuntimeBinaryBuildWorkerSourcePath(options) {
|
|
|
3628
3287
|
process.env.PROJECT_RIG_ROOT?.trim()
|
|
3629
3288
|
].filter(Boolean);
|
|
3630
3289
|
for (const root of envRoots) {
|
|
3631
|
-
const candidate =
|
|
3290
|
+
const candidate = resolve15(root, "packages/runtime/src/binary-build-worker.ts");
|
|
3632
3291
|
if (existsSync14(candidate)) {
|
|
3633
3292
|
return candidate;
|
|
3634
3293
|
}
|
|
3635
3294
|
}
|
|
3636
|
-
const localCandidate =
|
|
3295
|
+
const localCandidate = resolve15(import.meta.dir, "binary-build-worker.ts");
|
|
3637
3296
|
return existsSync14(localCandidate) ? localCandidate : null;
|
|
3638
3297
|
}
|
|
3639
3298
|
function resolveRuntimeBinaryBuildWorkerInvocation() {
|
|
@@ -3719,7 +3378,7 @@ function normalizeBuildInputPath(cwd, inputPath) {
|
|
|
3719
3378
|
if (inputPath.startsWith("<")) {
|
|
3720
3379
|
return null;
|
|
3721
3380
|
}
|
|
3722
|
-
return
|
|
3381
|
+
return resolve15(cwd, inputPath);
|
|
3723
3382
|
}
|
|
3724
3383
|
async function sha256File(path) {
|
|
3725
3384
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
@@ -3735,8 +3394,8 @@ function sortRecord(value) {
|
|
|
3735
3394
|
async function runSerializedRuntimeBinaryBuild(action) {
|
|
3736
3395
|
const previous = runtimeBinaryBuildQueue;
|
|
3737
3396
|
let release;
|
|
3738
|
-
runtimeBinaryBuildQueue = new Promise((
|
|
3739
|
-
release =
|
|
3397
|
+
runtimeBinaryBuildQueue = new Promise((resolve16) => {
|
|
3398
|
+
release = resolve16;
|
|
3740
3399
|
});
|
|
3741
3400
|
await previous;
|
|
3742
3401
|
try {
|
|
@@ -3781,11 +3440,11 @@ async function withTemporaryCwd(cwd, action) {
|
|
|
3781
3440
|
}
|
|
3782
3441
|
|
|
3783
3442
|
// packages/runtime/src/control-plane/runtime/provisioning-env.ts
|
|
3784
|
-
import { delimiter, resolve as
|
|
3443
|
+
import { delimiter, resolve as resolve17 } from "path";
|
|
3785
3444
|
|
|
3786
3445
|
// packages/runtime/src/control-plane/runtime/runtime-paths.ts
|
|
3787
3446
|
import { existsSync as existsSync15, readdirSync as readdirSync4, realpathSync } from "fs";
|
|
3788
|
-
import { resolve as
|
|
3447
|
+
import { resolve as resolve16 } from "path";
|
|
3789
3448
|
|
|
3790
3449
|
// packages/runtime/src/control-plane/runtime/sandbox/utils.ts
|
|
3791
3450
|
function uniq(values) {
|
|
@@ -3803,7 +3462,7 @@ function resolveBunBinaryPath() {
|
|
|
3803
3462
|
}
|
|
3804
3463
|
const home = process.env.HOME?.trim();
|
|
3805
3464
|
const fallbackCandidates = [
|
|
3806
|
-
home ?
|
|
3465
|
+
home ? resolve16(home, ".bun/bin/bun") : "",
|
|
3807
3466
|
"/opt/homebrew/bin/bun",
|
|
3808
3467
|
"/usr/local/bin/bun",
|
|
3809
3468
|
"/usr/bin/bun"
|
|
@@ -3831,8 +3490,8 @@ function resolveClaudeBinaryPath() {
|
|
|
3831
3490
|
}
|
|
3832
3491
|
const home = process.env.HOME?.trim();
|
|
3833
3492
|
const fallbackCandidates = [
|
|
3834
|
-
home ?
|
|
3835
|
-
home ?
|
|
3493
|
+
home ? resolve16(home, ".local/bin/claude") : "",
|
|
3494
|
+
home ? resolve16(home, ".local/share/claude/local/claude") : "",
|
|
3836
3495
|
"/opt/homebrew/bin/claude",
|
|
3837
3496
|
"/usr/local/bin/claude",
|
|
3838
3497
|
"/usr/bin/claude"
|
|
@@ -3846,51 +3505,51 @@ function resolveClaudeBinaryPath() {
|
|
|
3846
3505
|
throw new Error("claude not found in PATH");
|
|
3847
3506
|
}
|
|
3848
3507
|
function resolveBunInstallDir(bunBinaryPath = resolveBunBinaryPath()) {
|
|
3849
|
-
return
|
|
3508
|
+
return resolve16(bunBinaryPath, "../..");
|
|
3850
3509
|
}
|
|
3851
3510
|
function resolveClaudeInstallDir() {
|
|
3852
3511
|
const realPath = resolveClaudeBinaryPath();
|
|
3853
|
-
return
|
|
3512
|
+
return resolve16(realPath, "..");
|
|
3854
3513
|
}
|
|
3855
3514
|
function resolveNodeInstallDir() {
|
|
3856
3515
|
const preferredNode = resolvePreferredNodeBinary();
|
|
3857
3516
|
if (!preferredNode)
|
|
3858
3517
|
return null;
|
|
3859
3518
|
const explicitNode = process.env.RIG_NODE_BIN?.trim();
|
|
3860
|
-
if (explicitNode &&
|
|
3861
|
-
return preferredNode.endsWith("/bin/node") ?
|
|
3519
|
+
if (explicitNode && resolve16(explicitNode) === resolve16(preferredNode)) {
|
|
3520
|
+
return preferredNode.endsWith("/bin/node") ? resolve16(preferredNode, "../..") : resolve16(preferredNode, "..");
|
|
3862
3521
|
}
|
|
3863
3522
|
try {
|
|
3864
3523
|
const realPath = realpathSync(preferredNode);
|
|
3865
3524
|
if (realPath.endsWith("/bin/node")) {
|
|
3866
|
-
return
|
|
3525
|
+
return resolve16(realPath, "../..");
|
|
3867
3526
|
}
|
|
3868
|
-
return
|
|
3527
|
+
return resolve16(realPath, "..");
|
|
3869
3528
|
} catch {
|
|
3870
|
-
return
|
|
3529
|
+
return resolve16(preferredNode, "..");
|
|
3871
3530
|
}
|
|
3872
3531
|
}
|
|
3873
3532
|
function resolvePreferredNodeBinary() {
|
|
3874
3533
|
const candidates = [];
|
|
3875
3534
|
const envNode = process.env.RIG_NODE_BIN?.trim();
|
|
3876
3535
|
if (envNode) {
|
|
3877
|
-
const explicit =
|
|
3536
|
+
const explicit = resolve16(envNode);
|
|
3878
3537
|
if (existsSync15(explicit)) {
|
|
3879
3538
|
return explicit;
|
|
3880
3539
|
}
|
|
3881
3540
|
}
|
|
3882
3541
|
const nvmBin = process.env.NVM_BIN?.trim();
|
|
3883
3542
|
if (nvmBin) {
|
|
3884
|
-
candidates.push(
|
|
3543
|
+
candidates.push(resolve16(nvmBin, "node"));
|
|
3885
3544
|
}
|
|
3886
3545
|
const home = process.env.HOME?.trim();
|
|
3887
3546
|
if (home) {
|
|
3888
|
-
const nvmVersionsDir =
|
|
3547
|
+
const nvmVersionsDir = resolve16(home, ".nvm/versions/node");
|
|
3889
3548
|
if (existsSync15(nvmVersionsDir)) {
|
|
3890
3549
|
try {
|
|
3891
3550
|
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/, "")));
|
|
3892
3551
|
for (const versionDir of versionDirs) {
|
|
3893
|
-
candidates.push(
|
|
3552
|
+
candidates.push(resolve16(nvmVersionsDir, versionDir, "bin/node"));
|
|
3894
3553
|
}
|
|
3895
3554
|
} catch {}
|
|
3896
3555
|
}
|
|
@@ -3899,7 +3558,7 @@ function resolvePreferredNodeBinary() {
|
|
|
3899
3558
|
if (whichNode) {
|
|
3900
3559
|
candidates.push(whichNode);
|
|
3901
3560
|
}
|
|
3902
|
-
const deduped = uniq(candidates.map((candidate) =>
|
|
3561
|
+
const deduped = uniq(candidates.map((candidate) => resolve16(candidate)));
|
|
3903
3562
|
const existing = deduped.filter((candidate) => existsSync15(candidate));
|
|
3904
3563
|
if (existing.length === 0) {
|
|
3905
3564
|
return null;
|
|
@@ -3914,7 +3573,7 @@ function resolvePreferredNodeBinary() {
|
|
|
3914
3573
|
return existing[0] ?? null;
|
|
3915
3574
|
}
|
|
3916
3575
|
function inferNodeMajor(nodeBinaryPath) {
|
|
3917
|
-
const normalized =
|
|
3576
|
+
const normalized = resolve16(nodeBinaryPath).replace(/\\/g, "/");
|
|
3918
3577
|
const match = normalized.match(/(?:^|\/)(?:node-)?v?(\d+)\.\d+\.\d+(?:\/|$)/);
|
|
3919
3578
|
if (!match) {
|
|
3920
3579
|
return null;
|
|
@@ -3926,7 +3585,7 @@ function normalizeExecutablePath(candidate) {
|
|
|
3926
3585
|
if (!candidate) {
|
|
3927
3586
|
return "";
|
|
3928
3587
|
}
|
|
3929
|
-
const normalized =
|
|
3588
|
+
const normalized = resolve16(candidate);
|
|
3930
3589
|
if (!existsSync15(normalized)) {
|
|
3931
3590
|
return "";
|
|
3932
3591
|
}
|
|
@@ -3937,7 +3596,7 @@ function normalizeExecutablePath(candidate) {
|
|
|
3937
3596
|
}
|
|
3938
3597
|
}
|
|
3939
3598
|
function looksLikeRuntimeGateway(candidate) {
|
|
3940
|
-
const normalized =
|
|
3599
|
+
const normalized = resolve16(candidate).replace(/\\/g, "/");
|
|
3941
3600
|
return normalized.includes("/.rig/bin/") || normalized.endsWith("/rig-shell") || normalized.endsWith("/rig-agent");
|
|
3942
3601
|
}
|
|
3943
3602
|
|
|
@@ -3958,7 +3617,7 @@ function runtimeProvisioningEnv(baseEnv = process.env) {
|
|
|
3958
3617
|
try {
|
|
3959
3618
|
return resolveClaudeInstallDir();
|
|
3960
3619
|
} catch {
|
|
3961
|
-
return
|
|
3620
|
+
return resolve17(claudeBinary, "..");
|
|
3962
3621
|
}
|
|
3963
3622
|
})() : "";
|
|
3964
3623
|
const nodeDir = resolveNodeInstallDir();
|
|
@@ -3968,8 +3627,8 @@ function runtimeProvisioningEnv(baseEnv = process.env) {
|
|
|
3968
3627
|
`${bunDir}/bin`,
|
|
3969
3628
|
claudeDir,
|
|
3970
3629
|
nodeDir ? `${nodeDir}/bin` : "",
|
|
3971
|
-
realHome ?
|
|
3972
|
-
realHome ?
|
|
3630
|
+
realHome ? resolve17(realHome, ".local/bin") : "",
|
|
3631
|
+
realHome ? resolve17(realHome, ".cargo/bin") : "",
|
|
3973
3632
|
...inheritedPath,
|
|
3974
3633
|
"/usr/local/bin",
|
|
3975
3634
|
"/usr/local/sbin",
|
|
@@ -4000,9 +3659,9 @@ function runtimeProvisioningEnv(baseEnv = process.env) {
|
|
|
4000
3659
|
// packages/runtime/src/control-plane/native/validator-binaries.ts
|
|
4001
3660
|
function resolveValidatorBinaryPath(projectRoot, binaryName, runtimeContext) {
|
|
4002
3661
|
if (runtimeContext) {
|
|
4003
|
-
return
|
|
3662
|
+
return resolve18(runtimeContext.binDir, "validators", binaryName);
|
|
4004
3663
|
}
|
|
4005
|
-
return
|
|
3664
|
+
return resolve18(resolveHarnessPaths(projectRoot).binDir, "validators", binaryName);
|
|
4006
3665
|
}
|
|
4007
3666
|
async function ensureValidatorBinary(projectRoot, checkId, runtimeContext) {
|
|
4008
3667
|
const match = checkId.match(/^([a-z][\w-]*):([a-z][\w-]*)$/);
|
|
@@ -4017,7 +3676,7 @@ async function ensureValidatorBinary(projectRoot, checkId, runtimeContext) {
|
|
|
4017
3676
|
const binaryName = `${category}-${check}`;
|
|
4018
3677
|
const binaryPath = resolveValidatorBinaryPath(projectRoot, binaryName, runtimeContext);
|
|
4019
3678
|
const hostProjectRoot = runtimeContext?.hostProjectRoot?.trim() || projectRoot;
|
|
4020
|
-
const sourcePath =
|
|
3679
|
+
const sourcePath = resolve18(hostProjectRoot, "packages/runtime/src/control-plane/validators", category, `${check}.ts`);
|
|
4021
3680
|
if (!existsSync16(sourcePath)) {
|
|
4022
3681
|
return null;
|
|
4023
3682
|
}
|
|
@@ -4026,10 +3685,10 @@ async function ensureValidatorBinary(projectRoot, checkId, runtimeContext) {
|
|
|
4026
3685
|
const binaryMtime = binaryExists ? statSync5(binaryPath).mtimeMs : 0;
|
|
4027
3686
|
if (!binaryExists || sourceMtime > binaryMtime) {
|
|
4028
3687
|
if (binaryExists) {
|
|
4029
|
-
|
|
4030
|
-
|
|
3688
|
+
rmSync5(binaryPath, { force: true });
|
|
3689
|
+
rmSync5(`${binaryPath}.build-manifest.json`, { force: true });
|
|
4031
3690
|
}
|
|
4032
|
-
|
|
3691
|
+
mkdirSync7(dirname9(binaryPath), { recursive: true });
|
|
4033
3692
|
await buildRuntimeBinary({
|
|
4034
3693
|
sourcePath: `packages/runtime/src/control-plane/validators/${category}/${check}.ts`,
|
|
4035
3694
|
outputPath: binaryPath,
|
|
@@ -4075,14 +3734,14 @@ async function readTaskSourceValidation(projectRoot, taskId) {
|
|
|
4075
3734
|
function resolveValidationPaths(projectRoot, taskId, runtimeContext) {
|
|
4076
3735
|
if (runtimeContext) {
|
|
4077
3736
|
return {
|
|
4078
|
-
taskLogDir:
|
|
4079
|
-
artifactDir:
|
|
3737
|
+
taskLogDir: resolve19(runtimeContext.logsDir, taskId),
|
|
3738
|
+
artifactDir: resolve19(runtimeContext.workspaceDir, "artifacts", taskId)
|
|
4080
3739
|
};
|
|
4081
3740
|
}
|
|
4082
3741
|
const paths = resolveHarnessPaths(projectRoot);
|
|
4083
3742
|
return {
|
|
4084
|
-
taskLogDir:
|
|
4085
|
-
artifactDir:
|
|
3743
|
+
taskLogDir: resolve19(paths.logsDir, taskId),
|
|
3744
|
+
artifactDir: resolve19(paths.artifactsDir, taskId)
|
|
4086
3745
|
};
|
|
4087
3746
|
}
|
|
4088
3747
|
async function runValidatorBinary(projectRoot, taskId, checkId, runtimeContext) {
|
|
@@ -4099,7 +3758,7 @@ async function runValidatorBinary(projectRoot, taskId, checkId, runtimeContext)
|
|
|
4099
3758
|
};
|
|
4100
3759
|
}
|
|
4101
3760
|
const validatorCwd = runtimeContext?.workspaceDir || resolveMonorepoRoot(projectRoot);
|
|
4102
|
-
const runtimeShellPath = runtimeContext ?
|
|
3761
|
+
const runtimeShellPath = runtimeContext ? resolve19(runtimeContext.binDir, "rig-shell") : "";
|
|
4103
3762
|
const monorepoMainRoot = runtimeContext?.monorepoMainRoot || process.env.MONOREPO_MAIN_ROOT?.trim() || resolveMonorepoRoot(projectRoot);
|
|
4104
3763
|
const validatorEnv = {
|
|
4105
3764
|
PROJECT_RIG_ROOT: runtimeContext?.hostProjectRoot || projectRoot,
|
|
@@ -4154,8 +3813,8 @@ async function validateTask(projectRoot, taskId, runtimeContext, registry, optio
|
|
|
4154
3813
|
const configuredValidation = stringArray(taskConfig[taskId]?.validation);
|
|
4155
3814
|
const commands = resolvedContext?.validation?.length ? resolvedContext.validation : configuredValidation.length > 0 ? configuredValidation : sourceValidation.validation;
|
|
4156
3815
|
const { taskLogDir, artifactDir } = resolveValidationPaths(projectRoot, taskId, resolvedContext);
|
|
4157
|
-
|
|
4158
|
-
|
|
3816
|
+
mkdirSync8(taskLogDir, { recursive: true });
|
|
3817
|
+
mkdirSync8(artifactDir, { recursive: true });
|
|
4159
3818
|
if (commands.length === 0) {
|
|
4160
3819
|
const skipped = {
|
|
4161
3820
|
status: "skipped",
|
|
@@ -4164,89 +3823,1488 @@ async function validateTask(projectRoot, taskId, runtimeContext, registry, optio
|
|
|
4164
3823
|
failed: 0,
|
|
4165
3824
|
categories: []
|
|
4166
3825
|
};
|
|
4167
|
-
|
|
3826
|
+
writeFileSync8(resolve19(artifactDir, "validation-summary.json"), `${JSON.stringify(skipped, null, 2)}
|
|
4168
3827
|
`, "utf-8");
|
|
4169
3828
|
return skipped;
|
|
4170
3829
|
}
|
|
4171
|
-
const effectiveRegistry = registry ?? createValidatorRegistry();
|
|
4172
|
-
const workspaceRoot = resolvedContext?.workspaceDir ?? resolveMonorepoRoot(projectRoot);
|
|
4173
|
-
const monorepoRoot = resolvedContext?.monorepoMainRoot ?? process.env.MONOREPO_MAIN_ROOT?.trim() ?? resolveMonorepoRoot(projectRoot);
|
|
4174
|
-
const validatorCtx = {
|
|
4175
|
-
taskId,
|
|
4176
|
-
workspaceRoot,
|
|
4177
|
-
scope: resolvedContext?.scopes ?? (stringArray(taskConfig[taskId]?.scope).length > 0 ? stringArray(taskConfig[taskId]?.scope) : sourceValidation.scope),
|
|
4178
|
-
monorepoRoot,
|
|
4179
|
-
artifactsDir: artifactDir,
|
|
4180
|
-
taskConfig: sourceValidation.taskConfig ?? taskConfig[taskId] ?? undefined
|
|
3830
|
+
const effectiveRegistry = registry ?? createValidatorRegistry();
|
|
3831
|
+
const workspaceRoot = resolvedContext?.workspaceDir ?? resolveMonorepoRoot(projectRoot);
|
|
3832
|
+
const monorepoRoot = resolvedContext?.monorepoMainRoot ?? process.env.MONOREPO_MAIN_ROOT?.trim() ?? resolveMonorepoRoot(projectRoot);
|
|
3833
|
+
const validatorCtx = {
|
|
3834
|
+
taskId,
|
|
3835
|
+
workspaceRoot,
|
|
3836
|
+
scope: resolvedContext?.scopes ?? (stringArray(taskConfig[taskId]?.scope).length > 0 ? stringArray(taskConfig[taskId]?.scope) : sourceValidation.scope),
|
|
3837
|
+
monorepoRoot,
|
|
3838
|
+
artifactsDir: artifactDir,
|
|
3839
|
+
taskConfig: sourceValidation.taskConfig ?? taskConfig[taskId] ?? undefined
|
|
3840
|
+
};
|
|
3841
|
+
const valDescriptions = resolvedContext ? {} : options.validationDescriptions ?? (() => {
|
|
3842
|
+
try {
|
|
3843
|
+
return readValidationDescriptions(projectRoot);
|
|
3844
|
+
} catch {
|
|
3845
|
+
return {};
|
|
3846
|
+
}
|
|
3847
|
+
})();
|
|
3848
|
+
const categories = [];
|
|
3849
|
+
let passed = 0;
|
|
3850
|
+
let failed = 0;
|
|
3851
|
+
for (const cmd of commands) {
|
|
3852
|
+
const startedAt = Date.now();
|
|
3853
|
+
if (!isCheckId(cmd)) {
|
|
3854
|
+
failed += 1;
|
|
3855
|
+
categories.push({
|
|
3856
|
+
category: cmd,
|
|
3857
|
+
status: "fail",
|
|
3858
|
+
exit_code: 2,
|
|
3859
|
+
duration_seconds: 0
|
|
3860
|
+
});
|
|
3861
|
+
const logFile2 = resolve19(taskLogDir, `invalid-entry-validation.log`);
|
|
3862
|
+
mkdirSync8(taskLogDir, { recursive: true });
|
|
3863
|
+
writeFileSync8(logFile2, `=== ${nowIso()} :: ${cmd} ===
|
|
3864
|
+
Invalid validation entry: not a check-ID. All entries must use format "category:check-name".
|
|
3865
|
+
`, "utf-8");
|
|
3866
|
+
continue;
|
|
3867
|
+
}
|
|
3868
|
+
const { result, exitCode } = await dispatchValidator(cmd, effectiveRegistry, validatorCtx, (id) => runValidatorBinary(projectRoot, taskId, id, resolvedContext));
|
|
3869
|
+
const durationSeconds = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
3870
|
+
const logFile = resolve19(taskLogDir, `${cmd.replace(":", "-")}-validation.log`);
|
|
3871
|
+
mkdirSync8(taskLogDir, { recursive: true });
|
|
3872
|
+
writeFileSync8(logFile, `=== ${nowIso()} :: ${cmd} ===
|
|
3873
|
+
${JSON.stringify(result, null, 2)}
|
|
3874
|
+
`, "utf-8");
|
|
3875
|
+
if (result.passed) {
|
|
3876
|
+
passed += 1;
|
|
3877
|
+
categories.push({ category: cmd, status: "pass", duration_seconds: durationSeconds });
|
|
3878
|
+
} else {
|
|
3879
|
+
failed += 1;
|
|
3880
|
+
categories.push({ category: cmd, status: "fail", exit_code: exitCode, duration_seconds: durationSeconds });
|
|
3881
|
+
const desc = valDescriptions[cmd];
|
|
3882
|
+
if (desc) {
|
|
3883
|
+
console.log(` What this checks (${cmd}): ${desc}`);
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
const summary = {
|
|
3888
|
+
status: failed === 0 ? "pass" : "fail",
|
|
3889
|
+
total: commands.length,
|
|
3890
|
+
passed,
|
|
3891
|
+
failed,
|
|
3892
|
+
categories
|
|
3893
|
+
};
|
|
3894
|
+
mkdirSync8(artifactDir, { recursive: true });
|
|
3895
|
+
writeFileSync8(resolve19(artifactDir, "validation-summary.json"), `${JSON.stringify(summary, null, 2)}
|
|
3896
|
+
`, "utf-8");
|
|
3897
|
+
return summary;
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3901
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
3902
|
+
import { resolve as resolve21 } from "path";
|
|
3903
|
+
|
|
3904
|
+
// packages/runtime/src/control-plane/native/pr-review-gate.ts
|
|
3905
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
3906
|
+
import { resolve as resolve20 } from "path";
|
|
3907
|
+
function parseJsonObject(value) {
|
|
3908
|
+
if (!value?.trim())
|
|
3909
|
+
return { value: {}, error: "empty JSON output" };
|
|
3910
|
+
try {
|
|
3911
|
+
const parsed = JSON.parse(value);
|
|
3912
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { value: parsed } : { value: {}, error: "JSON output was not an object" };
|
|
3913
|
+
} catch (error) {
|
|
3914
|
+
return { value: {}, error: error instanceof Error ? error.message : String(error) };
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
function flattenPaginatedArray(value) {
|
|
3918
|
+
if (!Array.isArray(value))
|
|
3919
|
+
return null;
|
|
3920
|
+
if (value.every((entry) => Array.isArray(entry))) {
|
|
3921
|
+
return value.flatMap((entry) => entry);
|
|
3922
|
+
}
|
|
3923
|
+
return value;
|
|
3924
|
+
}
|
|
3925
|
+
function parseConcatenatedJsonValues(value) {
|
|
3926
|
+
const text = value.trim();
|
|
3927
|
+
const docs = [];
|
|
3928
|
+
let start = null;
|
|
3929
|
+
let depth = 0;
|
|
3930
|
+
let inString = false;
|
|
3931
|
+
let escape = false;
|
|
3932
|
+
for (let index = 0;index < text.length; index += 1) {
|
|
3933
|
+
const char = text[index];
|
|
3934
|
+
if (start === null) {
|
|
3935
|
+
if (/\s/.test(char))
|
|
3936
|
+
continue;
|
|
3937
|
+
start = index;
|
|
3938
|
+
}
|
|
3939
|
+
if (inString) {
|
|
3940
|
+
if (escape) {
|
|
3941
|
+
escape = false;
|
|
3942
|
+
} else if (char === "\\") {
|
|
3943
|
+
escape = true;
|
|
3944
|
+
} else if (char === '"') {
|
|
3945
|
+
inString = false;
|
|
3946
|
+
}
|
|
3947
|
+
continue;
|
|
3948
|
+
}
|
|
3949
|
+
if (char === '"') {
|
|
3950
|
+
inString = true;
|
|
3951
|
+
continue;
|
|
3952
|
+
}
|
|
3953
|
+
if (char === "{" || char === "[") {
|
|
3954
|
+
depth += 1;
|
|
3955
|
+
continue;
|
|
3956
|
+
}
|
|
3957
|
+
if (char === "}" || char === "]") {
|
|
3958
|
+
depth -= 1;
|
|
3959
|
+
if (depth < 0)
|
|
3960
|
+
return { value: docs, error: "unexpected JSON close delimiter" };
|
|
3961
|
+
if (depth === 0 && start !== null) {
|
|
3962
|
+
const segment = text.slice(start, index + 1);
|
|
3963
|
+
try {
|
|
3964
|
+
docs.push(JSON.parse(segment));
|
|
3965
|
+
} catch (error) {
|
|
3966
|
+
return { value: docs, error: error instanceof Error ? error.message : String(error) };
|
|
3967
|
+
}
|
|
3968
|
+
start = null;
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
if (inString || depth !== 0 || start !== null)
|
|
3973
|
+
return { value: docs, error: "incomplete JSON stream" };
|
|
3974
|
+
return { value: docs };
|
|
3975
|
+
}
|
|
3976
|
+
function parseJsonArray(value) {
|
|
3977
|
+
if (!value?.trim())
|
|
3978
|
+
return { value: [], error: "empty JSON output" };
|
|
3979
|
+
try {
|
|
3980
|
+
const parsed = JSON.parse(value);
|
|
3981
|
+
const flattened = flattenPaginatedArray(parsed);
|
|
3982
|
+
return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
3983
|
+
} catch (error) {
|
|
3984
|
+
const streamed = parseConcatenatedJsonValues(value);
|
|
3985
|
+
if (streamed.error)
|
|
3986
|
+
return { value: [], error: error instanceof Error ? error.message : String(error) };
|
|
3987
|
+
const flattened = streamed.value.flatMap((entry) => flattenPaginatedArray(entry) ?? []);
|
|
3988
|
+
return flattened.length > 0 || streamed.value.length === 0 ? { value: flattened } : { value: [], error: "JSON output was not an array" };
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
function parseGithubPrUrl(prUrl) {
|
|
3992
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/i.exec(prUrl.trim());
|
|
3993
|
+
if (!match)
|
|
3994
|
+
return null;
|
|
3995
|
+
const prNumber = Number.parseInt(match[3], 10);
|
|
3996
|
+
if (!Number.isFinite(prNumber))
|
|
3997
|
+
return null;
|
|
3998
|
+
return { owner: match[1], repo: match[2], repoName: `${match[1]}/${match[2]}`, prNumber };
|
|
3999
|
+
}
|
|
4000
|
+
function checkName(check) {
|
|
4001
|
+
return String(check.name ?? check.context ?? "").trim();
|
|
4002
|
+
}
|
|
4003
|
+
function checkState(check) {
|
|
4004
|
+
return String(check.conclusion ?? check.state ?? check.status ?? "").trim().toLowerCase();
|
|
4005
|
+
}
|
|
4006
|
+
function isGreptileLabel(value) {
|
|
4007
|
+
return String(value ?? "").toLowerCase().includes("greptile");
|
|
4008
|
+
}
|
|
4009
|
+
function isGreptileGithubLogin(value) {
|
|
4010
|
+
const login = String(value ?? "").toLowerCase().replace(/\[bot\]$/, "");
|
|
4011
|
+
return login === "greptile" || login === "greptile-ai" || login === "greptileai" || login === "greptile-apps";
|
|
4012
|
+
}
|
|
4013
|
+
function isPassingCheck(check) {
|
|
4014
|
+
const state = checkState(check);
|
|
4015
|
+
return ["success", "successful", "passed", "neutral", "skipped", "completed"].includes(state);
|
|
4016
|
+
}
|
|
4017
|
+
function isPendingCheck(check) {
|
|
4018
|
+
const state = checkState(check);
|
|
4019
|
+
return ["pending", "queued", "in_progress", "waiting", "requested", "expected", "action_required"].includes(state);
|
|
4020
|
+
}
|
|
4021
|
+
function isFailingCheck(check) {
|
|
4022
|
+
const state = checkState(check);
|
|
4023
|
+
return ["failure", "failed", "timed_out", "action_required", "cancelled", "canceled", "error"].includes(state);
|
|
4024
|
+
}
|
|
4025
|
+
function wildcardToRegExp(pattern) {
|
|
4026
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
4027
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
4028
|
+
}
|
|
4029
|
+
function isAllowedFailure(name, allowedFailures) {
|
|
4030
|
+
return allowedFailures.some((pattern) => wildcardToRegExp(pattern).test(name));
|
|
4031
|
+
}
|
|
4032
|
+
function greptileScorePatterns() {
|
|
4033
|
+
return [
|
|
4034
|
+
/\b(?:confidence\s+score|confidence|rating|score)\s*:?\s*(\d+)\s*\/\s*(\d+)/gi,
|
|
4035
|
+
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|rating|score)/gi,
|
|
4036
|
+
/\bgreptile[^\n]{0,80}?(\d+)\s*\/\s*(\d+)/gi
|
|
4037
|
+
];
|
|
4038
|
+
}
|
|
4039
|
+
function parseGreptileScores(input) {
|
|
4040
|
+
const text = stripHtml(input);
|
|
4041
|
+
const seen = new Set;
|
|
4042
|
+
const scores = [];
|
|
4043
|
+
for (const pattern of greptileScorePatterns()) {
|
|
4044
|
+
for (const match of text.matchAll(pattern)) {
|
|
4045
|
+
const value = Number.parseInt(match[1] || "", 10);
|
|
4046
|
+
const scale = Number.parseInt(match[2] || "", 10);
|
|
4047
|
+
if (!Number.isFinite(value) || !Number.isFinite(scale) || scale <= 0)
|
|
4048
|
+
continue;
|
|
4049
|
+
const raw = match[0] || `${value}/${scale}`;
|
|
4050
|
+
const key = `${match.index ?? -1}:${value}/${scale}:${raw.toLowerCase()}`;
|
|
4051
|
+
if (seen.has(key))
|
|
4052
|
+
continue;
|
|
4053
|
+
seen.add(key);
|
|
4054
|
+
scores.push({ value, scale, raw });
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
return scores;
|
|
4058
|
+
}
|
|
4059
|
+
function parseGreptileScore(input) {
|
|
4060
|
+
return parseGreptileScores(input)[0] ?? null;
|
|
4061
|
+
}
|
|
4062
|
+
function stripHtml(input) {
|
|
4063
|
+
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
4064
|
+
|
|
4065
|
+
`).trim();
|
|
4066
|
+
}
|
|
4067
|
+
function containsBlockerText(input) {
|
|
4068
|
+
const text = stripHtml(input).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
4069
|
+
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);
|
|
4070
|
+
}
|
|
4071
|
+
function isStrictFiveOfFive(score) {
|
|
4072
|
+
return score.value === 5 && score.scale === 5;
|
|
4073
|
+
}
|
|
4074
|
+
function containsConflictingScoreText(input) {
|
|
4075
|
+
return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
|
|
4076
|
+
}
|
|
4077
|
+
function extractGreptileCommentBlock(input) {
|
|
4078
|
+
const match = input.match(/<!--\s*greptile_comment\s*-->([\s\S]*?)<!--\s*\/greptile_comment\s*-->/i);
|
|
4079
|
+
return match?.[1]?.trim() ?? null;
|
|
4080
|
+
}
|
|
4081
|
+
function extractGreptileBodyReviewedSha(input) {
|
|
4082
|
+
const block = extractGreptileCommentBlock(input);
|
|
4083
|
+
if (!block)
|
|
4084
|
+
return null;
|
|
4085
|
+
const commitLink = block.match(/\/commit\/([0-9a-f]{40})(?:\b|[^0-9a-f])/i);
|
|
4086
|
+
return commitLink?.[1]?.toLowerCase() ?? null;
|
|
4087
|
+
}
|
|
4088
|
+
function isoAtOrAfter(value, floor) {
|
|
4089
|
+
if (!value || !floor)
|
|
4090
|
+
return false;
|
|
4091
|
+
const valueMs = Date.parse(value);
|
|
4092
|
+
const floorMs = Date.parse(floor);
|
|
4093
|
+
return Number.isFinite(valueMs) && Number.isFinite(floorMs) && valueMs >= floorMs;
|
|
4094
|
+
}
|
|
4095
|
+
function greptileStatusVerdict(status) {
|
|
4096
|
+
const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
|
|
4097
|
+
if (!normalized)
|
|
4098
|
+
return null;
|
|
4099
|
+
if (["APPROVE", "APPROVED"].includes(normalized))
|
|
4100
|
+
return "approved";
|
|
4101
|
+
if (["REJECT", "REJECTED", "CHANGES_REQUESTED", "CHANGE_REQUESTED"].includes(normalized))
|
|
4102
|
+
return "rejected";
|
|
4103
|
+
if (["SKIP", "SKIPPED"].includes(normalized))
|
|
4104
|
+
return "skipped";
|
|
4105
|
+
if (["FAIL", "FAILED", "FAILURE", "ERROR"].includes(normalized))
|
|
4106
|
+
return "failed";
|
|
4107
|
+
if (["PENDING", "QUEUED", "IN_PROGRESS", "RUNNING", "STARTED", "REQUESTED", "REVIEWING_FILES", "GENERATING_SUMMARY"].includes(normalized))
|
|
4108
|
+
return "pending";
|
|
4109
|
+
if (["COMPLETE", "COMPLETED"].includes(normalized))
|
|
4110
|
+
return "completed";
|
|
4111
|
+
return null;
|
|
4112
|
+
}
|
|
4113
|
+
function isBlockingGreptileVerdict(verdict) {
|
|
4114
|
+
return verdict === "rejected" || verdict === "skipped" || verdict === "failed";
|
|
4115
|
+
}
|
|
4116
|
+
function greptileRequestTimeoutMs(env) {
|
|
4117
|
+
const fallback = 30000;
|
|
4118
|
+
const parsed = Number.parseInt(env.GREPTILE_REQUEST_TIMEOUT_MS || `${fallback}`, 10);
|
|
4119
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : fallback;
|
|
4120
|
+
}
|
|
4121
|
+
function normalizeGreptileMcpCodeReview(entry, fallbackId) {
|
|
4122
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4123
|
+
return null;
|
|
4124
|
+
const record = entry;
|
|
4125
|
+
const id = typeof record.id === "string" ? record.id.trim() : fallbackId?.trim() ?? "";
|
|
4126
|
+
if (!id)
|
|
4127
|
+
return null;
|
|
4128
|
+
const metadataRecord = record.metadata && typeof record.metadata === "object" && !Array.isArray(record.metadata) ? record.metadata : null;
|
|
4129
|
+
return {
|
|
4130
|
+
id,
|
|
4131
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
4132
|
+
createdAt: typeof record.createdAt === "string" ? record.createdAt : null,
|
|
4133
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
4134
|
+
metadata: metadataRecord ? { checkHeadSha: typeof metadataRecord.checkHeadSha === "string" ? metadataRecord.checkHeadSha : null } : null
|
|
4135
|
+
};
|
|
4136
|
+
}
|
|
4137
|
+
function uniqueGreptileCodeReviews(reviews) {
|
|
4138
|
+
const seen = new Set;
|
|
4139
|
+
const unique2 = [];
|
|
4140
|
+
for (const review of reviews) {
|
|
4141
|
+
if (seen.has(review.id))
|
|
4142
|
+
continue;
|
|
4143
|
+
seen.add(review.id);
|
|
4144
|
+
unique2.push(review);
|
|
4145
|
+
}
|
|
4146
|
+
return unique2;
|
|
4147
|
+
}
|
|
4148
|
+
function selectGreptileApiReviewsForGate(reviews, headSha) {
|
|
4149
|
+
const sorted = [...reviews].sort((left, right) => Date.parse(right.createdAt ?? "") - Date.parse(left.createdAt ?? ""));
|
|
4150
|
+
const current = headSha ? sorted.filter((review) => review.metadata?.checkHeadSha === headSha) : [];
|
|
4151
|
+
const untied = sorted.filter((review) => !review.metadata?.checkHeadSha);
|
|
4152
|
+
const latest = sorted.slice(0, 1);
|
|
4153
|
+
return uniqueGreptileCodeReviews([...current, ...untied, ...latest]);
|
|
4154
|
+
}
|
|
4155
|
+
function greptileApiSignalFromCodeReview(review, details) {
|
|
4156
|
+
const selected = details ?? review;
|
|
4157
|
+
return {
|
|
4158
|
+
id: selected.id || review.id,
|
|
4159
|
+
body: selected.body ?? review.body ?? null,
|
|
4160
|
+
reviewedSha: selected.metadata?.checkHeadSha ?? review.metadata?.checkHeadSha ?? null,
|
|
4161
|
+
status: selected.status ?? review.status ?? null
|
|
4162
|
+
};
|
|
4163
|
+
}
|
|
4164
|
+
async function callGreptileMcpToolForGate(input) {
|
|
4165
|
+
const controller = new AbortController;
|
|
4166
|
+
const timeoutId = setTimeout(() => {
|
|
4167
|
+
controller.abort(new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`));
|
|
4168
|
+
}, input.timeoutMs);
|
|
4169
|
+
let response;
|
|
4170
|
+
try {
|
|
4171
|
+
response = await input.fetchFn(input.apiBase, {
|
|
4172
|
+
method: "POST",
|
|
4173
|
+
headers: {
|
|
4174
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
4175
|
+
"Content-Type": "application/json"
|
|
4176
|
+
},
|
|
4177
|
+
body: JSON.stringify({
|
|
4178
|
+
jsonrpc: "2.0",
|
|
4179
|
+
id: `rig-strict-gate-${input.name}-${Date.now()}`,
|
|
4180
|
+
method: "tools/call",
|
|
4181
|
+
params: { name: input.name, arguments: input.args }
|
|
4182
|
+
}),
|
|
4183
|
+
signal: controller.signal
|
|
4184
|
+
});
|
|
4185
|
+
} catch (error) {
|
|
4186
|
+
if (controller.signal.aborted) {
|
|
4187
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${input.name} timed out after ${input.timeoutMs}ms.`);
|
|
4188
|
+
}
|
|
4189
|
+
throw error;
|
|
4190
|
+
} finally {
|
|
4191
|
+
clearTimeout(timeoutId);
|
|
4192
|
+
}
|
|
4193
|
+
const raw = await response.text();
|
|
4194
|
+
if (!response.ok) {
|
|
4195
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
4196
|
+
}
|
|
4197
|
+
let envelope;
|
|
4198
|
+
try {
|
|
4199
|
+
envelope = JSON.parse(raw);
|
|
4200
|
+
} catch {
|
|
4201
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
4202
|
+
}
|
|
4203
|
+
if (envelope.error?.message) {
|
|
4204
|
+
throw new Error(envelope.error.message);
|
|
4205
|
+
}
|
|
4206
|
+
const text = (envelope.result?.content ?? []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text ?? "").join(`
|
|
4207
|
+
`).trim();
|
|
4208
|
+
if (!text) {
|
|
4209
|
+
throw new Error(`MCP tool ${input.name} returned no text payload.`);
|
|
4210
|
+
}
|
|
4211
|
+
return text;
|
|
4212
|
+
}
|
|
4213
|
+
async function callGreptileMcpToolJsonForGate(input) {
|
|
4214
|
+
const text = await callGreptileMcpToolForGate(input);
|
|
4215
|
+
try {
|
|
4216
|
+
return JSON.parse(text);
|
|
4217
|
+
} catch {
|
|
4218
|
+
throw new Error(`MCP tool ${input.name} returned malformed JSON: ${text}`);
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
async function collectConfiguredGreptileApiSignals(input) {
|
|
4222
|
+
if (!input.enabled || input.options?.enabled === false) {
|
|
4223
|
+
return { signals: [], errors: [] };
|
|
4224
|
+
}
|
|
4225
|
+
const env = input.options?.env ?? process.env;
|
|
4226
|
+
const secrets = resolveRuntimeSecrets(env);
|
|
4227
|
+
const apiKey = secrets.GREPTILE_API_KEY?.trim() ?? "";
|
|
4228
|
+
if (!apiKey) {
|
|
4229
|
+
return { signals: [], errors: [] };
|
|
4230
|
+
}
|
|
4231
|
+
const fetchFn = input.options?.fetch ?? globalThis.fetch;
|
|
4232
|
+
if (typeof fetchFn !== "function") {
|
|
4233
|
+
return { signals: [], errors: ["Greptile API/MCP evidence read failed: fetch is not available."] };
|
|
4234
|
+
}
|
|
4235
|
+
const apiBase = secrets.GREPTILE_API_BASE?.trim() || "https://api.greptile.com/mcp";
|
|
4236
|
+
const remote = secrets.GREPTILE_REMOTE?.trim() || "github";
|
|
4237
|
+
const repository = secrets.GREPTILE_REPOSITORY?.trim() || input.repoName;
|
|
4238
|
+
const defaultBranch = secrets.GREPTILE_DEFAULT_BRANCH?.trim() || input.baseRefName?.trim() || "main";
|
|
4239
|
+
const timeoutMs = greptileRequestTimeoutMs(env);
|
|
4240
|
+
try {
|
|
4241
|
+
const listPayload = await callGreptileMcpToolJsonForGate({
|
|
4242
|
+
apiBase,
|
|
4243
|
+
apiKey,
|
|
4244
|
+
name: "list_code_reviews",
|
|
4245
|
+
args: {
|
|
4246
|
+
name: repository,
|
|
4247
|
+
remote,
|
|
4248
|
+
defaultBranch,
|
|
4249
|
+
prNumber: input.prNumber,
|
|
4250
|
+
limit: 20
|
|
4251
|
+
},
|
|
4252
|
+
timeoutMs,
|
|
4253
|
+
fetchFn
|
|
4254
|
+
});
|
|
4255
|
+
const reviews = (listPayload.codeReviews ?? []).map((entry) => normalizeGreptileMcpCodeReview(entry)).filter((review) => !!review);
|
|
4256
|
+
const selectedReviews = selectGreptileApiReviewsForGate(reviews, input.headSha);
|
|
4257
|
+
const signals = [];
|
|
4258
|
+
for (const review of selectedReviews) {
|
|
4259
|
+
const detailsPayload = await callGreptileMcpToolJsonForGate({
|
|
4260
|
+
apiBase,
|
|
4261
|
+
apiKey,
|
|
4262
|
+
name: "get_code_review",
|
|
4263
|
+
args: { codeReviewId: review.id },
|
|
4264
|
+
timeoutMs,
|
|
4265
|
+
fetchFn
|
|
4266
|
+
});
|
|
4267
|
+
const details = normalizeGreptileMcpCodeReview(detailsPayload.codeReview, review.id) ?? review;
|
|
4268
|
+
signals.push(greptileApiSignalFromCodeReview(review, details));
|
|
4269
|
+
}
|
|
4270
|
+
return { signals, errors: [] };
|
|
4271
|
+
} catch (error) {
|
|
4272
|
+
return {
|
|
4273
|
+
signals: [],
|
|
4274
|
+
errors: [`Greptile API/MCP evidence read failed: ${error instanceof Error ? error.message : String(error)}`]
|
|
4275
|
+
};
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
function firstString(record, keys) {
|
|
4279
|
+
for (const key of keys) {
|
|
4280
|
+
const value = record[key];
|
|
4281
|
+
if (typeof value === "string")
|
|
4282
|
+
return value;
|
|
4283
|
+
}
|
|
4284
|
+
return "";
|
|
4285
|
+
}
|
|
4286
|
+
function arrayField(record, key) {
|
|
4287
|
+
const value = record[key];
|
|
4288
|
+
return Array.isArray(value) ? value : [];
|
|
4289
|
+
}
|
|
4290
|
+
async function runJsonArray(command, args, cwd) {
|
|
4291
|
+
const result = await command(args, { cwd });
|
|
4292
|
+
const label = `gh ${args.join(" ")}`;
|
|
4293
|
+
if (result.exitCode !== 0) {
|
|
4294
|
+
return { value: [], error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
4295
|
+
}
|
|
4296
|
+
const parsed = parseJsonArray(result.stdout);
|
|
4297
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
4298
|
+
}
|
|
4299
|
+
async function runJsonObject(command, args, cwd) {
|
|
4300
|
+
const result = await command(args, { cwd });
|
|
4301
|
+
const label = `gh ${args.join(" ")}`;
|
|
4302
|
+
if (result.exitCode !== 0) {
|
|
4303
|
+
return { value: {}, error: `${label} failed (${result.exitCode}): ${result.stderr ?? result.stdout ?? ""}`.trim() };
|
|
4304
|
+
}
|
|
4305
|
+
const parsed = parseJsonObject(result.stdout);
|
|
4306
|
+
return parsed.error ? { value: parsed.value, error: `${label} returned invalid JSON: ${parsed.error}` } : { value: parsed.value };
|
|
4307
|
+
}
|
|
4308
|
+
function normalizeStatusCheck(entry) {
|
|
4309
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4310
|
+
return null;
|
|
4311
|
+
const record = entry;
|
|
4312
|
+
const name = firstString(record, ["name", "context"]);
|
|
4313
|
+
if (!name.trim())
|
|
4314
|
+
return null;
|
|
4315
|
+
const output = record.output && typeof record.output === "object" && !Array.isArray(record.output) ? record.output : null;
|
|
4316
|
+
const app = record.app && typeof record.app === "object" && !Array.isArray(record.app) ? record.app : null;
|
|
4317
|
+
return {
|
|
4318
|
+
__typename: typeof record.__typename === "string" ? record.__typename : null,
|
|
4319
|
+
name,
|
|
4320
|
+
context: typeof record.context === "string" ? record.context : null,
|
|
4321
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
4322
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
4323
|
+
conclusion: typeof record.conclusion === "string" ? record.conclusion : null,
|
|
4324
|
+
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,
|
|
4325
|
+
link: typeof record.link === "string" ? record.link : typeof record.html_url === "string" ? record.html_url : null,
|
|
4326
|
+
headSha: typeof record.headSha === "string" ? record.headSha : null,
|
|
4327
|
+
head_sha: typeof record.head_sha === "string" ? record.head_sha : null,
|
|
4328
|
+
output: output ? {
|
|
4329
|
+
title: typeof output.title === "string" ? output.title : null,
|
|
4330
|
+
summary: typeof output.summary === "string" ? output.summary : null,
|
|
4331
|
+
text: typeof output.text === "string" ? output.text : null
|
|
4332
|
+
} : null,
|
|
4333
|
+
app: app ? {
|
|
4334
|
+
slug: typeof app.slug === "string" ? app.slug : null,
|
|
4335
|
+
name: typeof app.name === "string" ? app.name : null,
|
|
4336
|
+
owner: app.owner && typeof app.owner === "object" ? app.owner : null
|
|
4337
|
+
} : null
|
|
4338
|
+
};
|
|
4339
|
+
}
|
|
4340
|
+
function normalizeReview(entry) {
|
|
4341
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4342
|
+
return null;
|
|
4343
|
+
const record = entry;
|
|
4344
|
+
return {
|
|
4345
|
+
id: typeof record.id === "string" ? record.id : typeof record.id === "number" ? String(record.id) : null,
|
|
4346
|
+
state: typeof record.state === "string" ? record.state : null,
|
|
4347
|
+
body: typeof record.body === "string" ? record.body : null,
|
|
4348
|
+
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,
|
|
4349
|
+
html_url: typeof record.html_url === "string" ? record.html_url : typeof record.url === "string" ? record.url : null,
|
|
4350
|
+
author: record.author && typeof record.author === "object" ? record.author : record.user && typeof record.user === "object" ? record.user : null
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
4353
|
+
function normalizeReviewComment(entry) {
|
|
4354
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4355
|
+
return null;
|
|
4356
|
+
const record = entry;
|
|
4357
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
4358
|
+
const path = typeof record.path === "string" ? record.path : null;
|
|
4359
|
+
if (!body && !path)
|
|
4360
|
+
return null;
|
|
4361
|
+
return {
|
|
4362
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
4363
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
4364
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
4365
|
+
body,
|
|
4366
|
+
path,
|
|
4367
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
4368
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
4369
|
+
commit_id: typeof record.commit_id === "string" ? record.commit_id : null,
|
|
4370
|
+
original_commit_id: typeof record.original_commit_id === "string" ? record.original_commit_id : null
|
|
4371
|
+
};
|
|
4372
|
+
}
|
|
4373
|
+
function normalizeIssueComment(entry) {
|
|
4374
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4375
|
+
return null;
|
|
4376
|
+
const record = entry;
|
|
4377
|
+
const body = typeof record.body === "string" ? record.body : null;
|
|
4378
|
+
if (!body)
|
|
4379
|
+
return null;
|
|
4380
|
+
return {
|
|
4381
|
+
id: typeof record.id === "string" || typeof record.id === "number" ? record.id : null,
|
|
4382
|
+
user: record.user && typeof record.user === "object" ? record.user : null,
|
|
4383
|
+
author: record.author && typeof record.author === "object" ? record.author : null,
|
|
4384
|
+
body,
|
|
4385
|
+
html_url: typeof record.html_url === "string" ? record.html_url : null,
|
|
4386
|
+
url: typeof record.url === "string" ? record.url : null,
|
|
4387
|
+
created_at: typeof record.created_at === "string" ? record.created_at : null
|
|
4388
|
+
};
|
|
4389
|
+
}
|
|
4390
|
+
function normalizeReviewThread(entry) {
|
|
4391
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
4392
|
+
return null;
|
|
4393
|
+
const record = entry;
|
|
4394
|
+
return {
|
|
4395
|
+
id: typeof record.id === "string" ? record.id : null,
|
|
4396
|
+
isResolved: typeof record.isResolved === "boolean" ? record.isResolved : null,
|
|
4397
|
+
isOutdated: typeof record.isOutdated === "boolean" ? record.isOutdated : null,
|
|
4398
|
+
comments: record.comments && typeof record.comments === "object" ? record.comments : null
|
|
4399
|
+
};
|
|
4400
|
+
}
|
|
4401
|
+
function relevantIssueComment(comment) {
|
|
4402
|
+
const login = comment.user?.login ?? comment.author?.login ?? "";
|
|
4403
|
+
const body = comment.body ?? "";
|
|
4404
|
+
return isGreptileGithubLogin(login) || containsBlockerText(body) || /greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(body);
|
|
4405
|
+
}
|
|
4406
|
+
function latestThreadComment(thread) {
|
|
4407
|
+
const nodes = thread.comments?.nodes ?? [];
|
|
4408
|
+
return nodes.length > 0 ? nodes[nodes.length - 1] : null;
|
|
4409
|
+
}
|
|
4410
|
+
function unresolvedThreadSummaries(threads) {
|
|
4411
|
+
return threads.flatMap((thread) => {
|
|
4412
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4413
|
+
return [];
|
|
4414
|
+
const latest = latestThreadComment(thread);
|
|
4415
|
+
if (!latest)
|
|
4416
|
+
return ["Unresolved review thread"];
|
|
4417
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4418
|
+
return [`Unresolved review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4419
|
+
});
|
|
4420
|
+
}
|
|
4421
|
+
function collectBodies(evidence) {
|
|
4422
|
+
return [
|
|
4423
|
+
evidence.title ?? "",
|
|
4424
|
+
evidence.body,
|
|
4425
|
+
...evidence.reviews.map((review) => review.body ?? ""),
|
|
4426
|
+
...evidence.changedFileReviewComments.map((comment) => comment.body ?? ""),
|
|
4427
|
+
...evidence.relevantIssueComments.map((comment) => comment.body ?? ""),
|
|
4428
|
+
...evidence.reviewThreads.flatMap((thread) => thread.comments?.nodes?.map((comment) => comment.body ?? "") ?? []),
|
|
4429
|
+
...(evidence.apiSignals ?? []).map((signal) => signal.body ?? "")
|
|
4430
|
+
].filter((body) => body.trim().length > 0);
|
|
4431
|
+
}
|
|
4432
|
+
function bodyExcerpt(body) {
|
|
4433
|
+
const text = stripHtml(body).replace(/\s+/g, " ").trim();
|
|
4434
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
4435
|
+
}
|
|
4436
|
+
function makeGreptileSignal(input) {
|
|
4437
|
+
const scores = parseGreptileScores(input.body);
|
|
4438
|
+
const reviewedSha = input.reviewedSha?.trim() || null;
|
|
4439
|
+
const current = reviewedSha ? reviewedSha === input.currentHeadSha : null;
|
|
4440
|
+
const verdict = input.verdict ?? null;
|
|
4441
|
+
const blocker = input.blocker ?? (isBlockingGreptileVerdict(verdict) || containsBlockerText(input.body));
|
|
4442
|
+
const explicitApproval = input.explicitApproval ?? false;
|
|
4443
|
+
return {
|
|
4444
|
+
source: input.source,
|
|
4445
|
+
trusted: input.trusted,
|
|
4446
|
+
authorLogin: input.authorLogin ?? null,
|
|
4447
|
+
reviewedSha,
|
|
4448
|
+
current,
|
|
4449
|
+
stale: current === false,
|
|
4450
|
+
score: scores[0] ?? null,
|
|
4451
|
+
scores,
|
|
4452
|
+
explicitApproval,
|
|
4453
|
+
verdict,
|
|
4454
|
+
blocker,
|
|
4455
|
+
actionable: input.actionable ?? blocker,
|
|
4456
|
+
bodyExcerpt: bodyExcerpt(input.body),
|
|
4457
|
+
body: input.body,
|
|
4458
|
+
allScores: scores
|
|
4459
|
+
};
|
|
4460
|
+
}
|
|
4461
|
+
function reviewAuthorLogin(review) {
|
|
4462
|
+
return review.author?.login ?? null;
|
|
4463
|
+
}
|
|
4464
|
+
function commentAuthorLogin(comment) {
|
|
4465
|
+
return comment.user?.login ?? comment.author?.login ?? null;
|
|
4466
|
+
}
|
|
4467
|
+
function collectGreptileSignals(evidence) {
|
|
4468
|
+
const signals = [];
|
|
4469
|
+
const greptileBodyReviewedSha = extractGreptileBodyReviewedSha(evidence.body);
|
|
4470
|
+
const trustedGreptileBody = Boolean(greptileBodyReviewedSha && isGreptileGithubLogin(evidence.bodyEditorLogin) && isoAtOrAfter(evidence.bodyLastEditedAt, evidence.headCommittedDate));
|
|
4471
|
+
const contextSources = [
|
|
4472
|
+
{ source: "pr-title", body: evidence.title ?? "" },
|
|
4473
|
+
{
|
|
4474
|
+
source: "pr-body",
|
|
4475
|
+
body: evidence.body,
|
|
4476
|
+
trusted: trustedGreptileBody,
|
|
4477
|
+
authorLogin: trustedGreptileBody ? evidence.bodyEditorLogin ?? null : null,
|
|
4478
|
+
reviewedSha: greptileBodyReviewedSha,
|
|
4479
|
+
verdict: trustedGreptileBody ? "completed" : null
|
|
4480
|
+
}
|
|
4481
|
+
];
|
|
4482
|
+
for (const context of contextSources) {
|
|
4483
|
+
if (!context.body.trim())
|
|
4484
|
+
continue;
|
|
4485
|
+
const contextBlocker = containsBlockerText(context.body);
|
|
4486
|
+
if (!contextBlocker && !/greptile|score|confidence|\b\d+\s*\/\s*5\b/i.test(context.body))
|
|
4487
|
+
continue;
|
|
4488
|
+
signals.push(makeGreptileSignal({
|
|
4489
|
+
source: context.source,
|
|
4490
|
+
body: context.body,
|
|
4491
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4492
|
+
trusted: context.trusted === true,
|
|
4493
|
+
authorLogin: context.authorLogin,
|
|
4494
|
+
reviewedSha: context.reviewedSha,
|
|
4495
|
+
verdict: context.verdict,
|
|
4496
|
+
blocker: contextBlocker,
|
|
4497
|
+
actionable: contextBlocker
|
|
4498
|
+
}));
|
|
4499
|
+
}
|
|
4500
|
+
for (const apiSignal of evidence.apiSignals ?? []) {
|
|
4501
|
+
const body = [apiSignal.status ? `Status: ${apiSignal.status}` : "", apiSignal.body ?? ""].filter(Boolean).join(`
|
|
4502
|
+
|
|
4503
|
+
`) || "Status: UNKNOWN";
|
|
4504
|
+
const verdict = greptileStatusVerdict(apiSignal.status);
|
|
4505
|
+
signals.push(makeGreptileSignal({
|
|
4506
|
+
source: "api",
|
|
4507
|
+
body,
|
|
4508
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4509
|
+
trusted: true,
|
|
4510
|
+
reviewedSha: apiSignal.reviewedSha ?? null,
|
|
4511
|
+
explicitApproval: verdict === "approved",
|
|
4512
|
+
verdict
|
|
4513
|
+
}));
|
|
4514
|
+
}
|
|
4515
|
+
for (const review of evidence.reviews) {
|
|
4516
|
+
const login = reviewAuthorLogin(review);
|
|
4517
|
+
if (!isGreptileGithubLogin(login))
|
|
4518
|
+
continue;
|
|
4519
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4520
|
+
const body = [state ? `Review state: ${state}` : "", review.body ?? ""].filter(Boolean).join(`
|
|
4521
|
+
|
|
4522
|
+
`);
|
|
4523
|
+
if (!body.trim())
|
|
4524
|
+
continue;
|
|
4525
|
+
const dismissed = state === "DISMISSED";
|
|
4526
|
+
signals.push(makeGreptileSignal({
|
|
4527
|
+
source: "github-review",
|
|
4528
|
+
body,
|
|
4529
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4530
|
+
trusted: !dismissed,
|
|
4531
|
+
authorLogin: login,
|
|
4532
|
+
reviewedSha: review.commit_id ?? null,
|
|
4533
|
+
explicitApproval: undefined,
|
|
4534
|
+
blocker: state === "CHANGES_REQUESTED" || undefined
|
|
4535
|
+
}));
|
|
4536
|
+
}
|
|
4537
|
+
for (const comment of evidence.relevantIssueComments) {
|
|
4538
|
+
const login = commentAuthorLogin(comment);
|
|
4539
|
+
const body = comment.body ?? "";
|
|
4540
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4541
|
+
continue;
|
|
4542
|
+
signals.push(makeGreptileSignal({
|
|
4543
|
+
source: "issue-comment",
|
|
4544
|
+
body,
|
|
4545
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4546
|
+
trusted: true,
|
|
4547
|
+
authorLogin: login
|
|
4548
|
+
}));
|
|
4549
|
+
}
|
|
4550
|
+
for (const thread of evidence.reviewThreads) {
|
|
4551
|
+
if (thread.isOutdated === true || thread.isResolved === true)
|
|
4552
|
+
continue;
|
|
4553
|
+
for (const comment of thread.comments?.nodes ?? []) {
|
|
4554
|
+
const login = comment.author?.login ?? null;
|
|
4555
|
+
const body = comment.body ?? "";
|
|
4556
|
+
if (!body.trim() || !isGreptileGithubLogin(login))
|
|
4557
|
+
continue;
|
|
4558
|
+
signals.push(makeGreptileSignal({
|
|
4559
|
+
source: "review-thread",
|
|
4560
|
+
body,
|
|
4561
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4562
|
+
trusted: true,
|
|
4563
|
+
authorLogin: login
|
|
4564
|
+
}));
|
|
4565
|
+
}
|
|
4566
|
+
}
|
|
4567
|
+
for (const check of evidence.checks) {
|
|
4568
|
+
if (!isGreptileLabel(checkName(check)))
|
|
4569
|
+
continue;
|
|
4570
|
+
const reviewedSha = check.headSha ?? check.head_sha ?? null;
|
|
4571
|
+
const label = `${checkName(check)} (${checkState(check) || "unknown"})`;
|
|
4572
|
+
const body = [label, check.output?.title ?? "", check.output?.summary ?? "", check.output?.text ?? ""].filter((entry) => entry.trim().length > 0).join(`
|
|
4573
|
+
|
|
4574
|
+
`);
|
|
4575
|
+
signals.push(makeGreptileSignal({
|
|
4576
|
+
source: "github-check",
|
|
4577
|
+
body,
|
|
4578
|
+
currentHeadSha: evidence.currentHeadSha,
|
|
4579
|
+
trusted: false,
|
|
4580
|
+
reviewedSha,
|
|
4581
|
+
explicitApproval: false,
|
|
4582
|
+
blocker: isFailingCheck(check),
|
|
4583
|
+
actionable: isFailingCheck(check)
|
|
4584
|
+
}));
|
|
4585
|
+
}
|
|
4586
|
+
return signals;
|
|
4587
|
+
}
|
|
4588
|
+
function unresolvedGreptileThreadSummaries(threads) {
|
|
4589
|
+
return threads.flatMap((thread) => {
|
|
4590
|
+
if (thread.isResolved === true || thread.isOutdated === true)
|
|
4591
|
+
return [];
|
|
4592
|
+
const comments = thread.comments?.nodes ?? [];
|
|
4593
|
+
if (!comments.some((comment) => isGreptileGithubLogin(comment.author?.login)))
|
|
4594
|
+
return [];
|
|
4595
|
+
const latest = latestThreadComment(thread);
|
|
4596
|
+
if (!latest)
|
|
4597
|
+
return ["Unresolved Greptile review thread"];
|
|
4598
|
+
const path = latest.path ? ` on ${latest.path}` : "";
|
|
4599
|
+
return [`Unresolved Greptile review thread${path}: ${(latest.body ?? "").trim() || "(empty comment)"}`];
|
|
4600
|
+
});
|
|
4601
|
+
}
|
|
4602
|
+
function actionableChangedFileCommentSummaries(_comments) {
|
|
4603
|
+
return [];
|
|
4604
|
+
}
|
|
4605
|
+
function issueLevelBlockerSummaries(comments) {
|
|
4606
|
+
return comments.flatMap((comment) => {
|
|
4607
|
+
const body = comment.body?.trim() ?? "";
|
|
4608
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4609
|
+
return [];
|
|
4610
|
+
const login = commentAuthorLogin(comment) ?? "unknown";
|
|
4611
|
+
const author = isGreptileGithubLogin(login) ? `Greptile issue comment by ${login}` : `Issue-level PR comment by ${login}`;
|
|
4612
|
+
return [`${author}: ${body}`];
|
|
4613
|
+
});
|
|
4614
|
+
}
|
|
4615
|
+
function reviewBodyBlockerSummaries(reviews) {
|
|
4616
|
+
return reviews.flatMap((review) => {
|
|
4617
|
+
const login = reviewAuthorLogin(review) ?? "unknown";
|
|
4618
|
+
if (isGreptileGithubLogin(login))
|
|
4619
|
+
return [];
|
|
4620
|
+
const body = review.body?.trim() ?? "";
|
|
4621
|
+
if (!body || !containsBlockerText(body) && !containsConflictingScoreText(body))
|
|
4622
|
+
return [];
|
|
4623
|
+
const state = review.state ? ` (${review.state})` : "";
|
|
4624
|
+
return [`PR review summary by ${login}${state}: ${body}`];
|
|
4625
|
+
});
|
|
4626
|
+
}
|
|
4627
|
+
function signalLabel(signal) {
|
|
4628
|
+
const source = signal.source.replace(/-/g, " ");
|
|
4629
|
+
const author = signal.authorLogin ? ` by ${signal.authorLogin}` : "";
|
|
4630
|
+
const sha = signal.reviewedSha ? ` at ${signal.reviewedSha}` : "";
|
|
4631
|
+
return `${source}${author}${sha}`;
|
|
4632
|
+
}
|
|
4633
|
+
function deriveGreptileEvidence(input) {
|
|
4634
|
+
const rawBodies = collectBodies(input);
|
|
4635
|
+
const signals = collectGreptileSignals(input);
|
|
4636
|
+
const trustedSignals = signals.filter((signal) => signal.trusted);
|
|
4637
|
+
const trustedScoreEntries = trustedSignals.flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4638
|
+
const contextScoreEntries = signals.filter((signal) => !signal.trusted).flatMap((signal) => signal.allScores.map((score2) => ({ score: score2, signal })));
|
|
4639
|
+
const allScoreEntries = [...trustedScoreEntries, ...contextScoreEntries];
|
|
4640
|
+
const staleSignals = signals.filter((signal) => !!signal.reviewedSha && signal.reviewedSha !== input.currentHeadSha && (signal.trusted || signal.source === "github-check"));
|
|
4641
|
+
const isCurrentOrUntied = (signal) => !signal.reviewedSha || signal.reviewedSha === input.currentHeadSha;
|
|
4642
|
+
const currentOrUntiedScoreEntries = allScoreEntries.filter((entry) => isCurrentOrUntied(entry.signal));
|
|
4643
|
+
const lowScoreEntries = currentOrUntiedScoreEntries.filter((entry) => !isStrictFiveOfFive(entry.score));
|
|
4644
|
+
const currentPendingApiSignals = trustedSignals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && isCurrentOrUntied(signal));
|
|
4645
|
+
const signalCanApproveByScore = (signal) => {
|
|
4646
|
+
if (signal.source === "api")
|
|
4647
|
+
return signal.verdict === "approved" || signal.verdict === "completed";
|
|
4648
|
+
return signal.verdict !== "pending" && !isBlockingGreptileVerdict(signal.verdict);
|
|
4649
|
+
};
|
|
4650
|
+
const approvingScoreEntry = trustedScoreEntries.find((entry) => entry.signal.reviewedSha === input.currentHeadSha && entry.signal.source !== "github-check" && signalCanApproveByScore(entry.signal) && isStrictFiveOfFive(entry.score)) ?? null;
|
|
4651
|
+
const approvingExplicitSignal = trustedSignals.find((signal) => signal.source === "api" && signal.reviewedSha === input.currentHeadSha && signal.explicitApproval === true && !signal.blocker) ?? null;
|
|
4652
|
+
const approvedByScore = !!approvingScoreEntry;
|
|
4653
|
+
const approvedByExplicitMapping = !!approvingExplicitSignal;
|
|
4654
|
+
const approvingSignal = approvingScoreEntry?.signal ?? approvingExplicitSignal;
|
|
4655
|
+
const lowestScore = lowScoreEntries.map((entry) => entry.score).sort((left, right) => left.value - right.value)[0] ?? null;
|
|
4656
|
+
const score = lowestScore ?? approvingScoreEntry?.score ?? trustedScoreEntries[0]?.score ?? contextScoreEntries[0]?.score ?? null;
|
|
4657
|
+
const failedGreptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)) && isFailingCheck(check)).map((check) => `${checkName(check)} (${checkState(check) || "failed"})`);
|
|
4658
|
+
const blockerSignals = signals.filter((signal) => (signal.blocker || signal.actionable) && (!signal.reviewedSha || signal.reviewedSha === input.currentHeadSha));
|
|
4659
|
+
const staleBlockingSignals = [];
|
|
4660
|
+
const blockers = [
|
|
4661
|
+
...blockerSignals.map((signal) => `${signalLabel(signal)}: ${signal.bodyExcerpt || "blocker text"}`),
|
|
4662
|
+
...reviewBodyBlockerSummaries(input.reviews),
|
|
4663
|
+
...issueLevelBlockerSummaries(input.relevantIssueComments),
|
|
4664
|
+
...lowScoreEntries.map((entry) => `Greptile score from ${signalLabel(entry.signal)} is ${entry.score.value}/${entry.score.scale}; strict merge requires trusted current-head 5/5.`),
|
|
4665
|
+
...staleBlockingSignals.map((signal) => `Greptile blocking signal from ${signalLabel(signal)} is stale; current PR head is ${input.currentHeadSha || "unknown"}.`),
|
|
4666
|
+
...failedGreptileChecks.map((entry) => `Greptile check failed: ${entry}`)
|
|
4667
|
+
];
|
|
4668
|
+
const unresolvedComments = [
|
|
4669
|
+
...unresolvedGreptileThreadSummaries(input.reviewThreads),
|
|
4670
|
+
...actionableChangedFileCommentSummaries(input.changedFileReviewComments)
|
|
4671
|
+
];
|
|
4672
|
+
const greptileChecks = input.checks.filter((check) => isGreptileLabel(checkName(check)));
|
|
4673
|
+
const greptileReviews = input.reviews.filter((review) => isGreptileGithubLogin(review.author?.login));
|
|
4674
|
+
const completedGreptileCheck = greptileChecks.some((check) => {
|
|
4675
|
+
const reviewedSha2 = check.headSha ?? check.head_sha ?? null;
|
|
4676
|
+
return reviewedSha2 === input.currentHeadSha && (isPassingCheck(check) || isFailingCheck(check));
|
|
4677
|
+
});
|
|
4678
|
+
const completedGreptileReview = greptileReviews.some((review) => {
|
|
4679
|
+
const state = String(review.state ?? "").toUpperCase();
|
|
4680
|
+
const completedState = ["APPROVED", "COMMENTED", "CHANGES_REQUESTED"].includes(state) || !!review.body?.trim();
|
|
4681
|
+
return completedState && review.commit_id === input.currentHeadSha;
|
|
4682
|
+
});
|
|
4683
|
+
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"));
|
|
4684
|
+
const approvalReviewedSha = approvingSignal?.reviewedSha ?? null;
|
|
4685
|
+
const reviewedSha = approvalReviewedSha ?? staleSignals[0]?.reviewedSha ?? trustedSignals.map((signal) => signal.reviewedSha ?? null).find(Boolean) ?? null;
|
|
4686
|
+
const fresh = !!approvalReviewedSha && approvalReviewedSha === input.currentHeadSha;
|
|
4687
|
+
const completed = completedGreptileCheck || completedGreptileReview || completedGreptileApi || !!approvingSignal;
|
|
4688
|
+
const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
|
|
4689
|
+
const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
|
|
4690
|
+
const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
|
|
4691
|
+
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";
|
|
4692
|
+
return {
|
|
4693
|
+
source,
|
|
4694
|
+
currentHeadSha: input.currentHeadSha,
|
|
4695
|
+
reviewedSha,
|
|
4696
|
+
fresh,
|
|
4697
|
+
completed,
|
|
4698
|
+
approved,
|
|
4699
|
+
score,
|
|
4700
|
+
explicitApproval: approvedByExplicitMapping,
|
|
4701
|
+
blockers,
|
|
4702
|
+
unresolvedComments,
|
|
4703
|
+
rawBodies,
|
|
4704
|
+
signals: signals.map(({ body: _body, allScores: _allScores, ...signal }) => signal),
|
|
4705
|
+
mapping
|
|
4706
|
+
};
|
|
4707
|
+
}
|
|
4708
|
+
function isGreptileCheckDetail(check) {
|
|
4709
|
+
return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
|
|
4710
|
+
}
|
|
4711
|
+
async function collectGreptileCheckDetails(input) {
|
|
4712
|
+
const checkRunsRead = await runJsonObject(input.command, [
|
|
4713
|
+
"api",
|
|
4714
|
+
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
4715
|
+
"-F",
|
|
4716
|
+
"per_page=100"
|
|
4717
|
+
], input.projectRoot);
|
|
4718
|
+
const checkRuns = arrayField(checkRunsRead.value, "check_runs").map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
|
|
4719
|
+
return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
|
|
4720
|
+
}
|
|
4721
|
+
async function collectPullRequestProvenance(input) {
|
|
4722
|
+
const response = await runJsonObject(input.command, [
|
|
4723
|
+
"api",
|
|
4724
|
+
"graphql",
|
|
4725
|
+
"-F",
|
|
4726
|
+
`owner=${input.owner}`,
|
|
4727
|
+
"-F",
|
|
4728
|
+
`name=${input.name}`,
|
|
4729
|
+
"-F",
|
|
4730
|
+
`prNumber=${input.prNumber}`,
|
|
4731
|
+
"-f",
|
|
4732
|
+
"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 } } } } } }"
|
|
4733
|
+
], input.projectRoot);
|
|
4734
|
+
if (response.error)
|
|
4735
|
+
return { value: {}, error: response.error };
|
|
4736
|
+
const data = response.value.data;
|
|
4737
|
+
const repository = data?.repository;
|
|
4738
|
+
const pullRequest = repository?.pullRequest;
|
|
4739
|
+
if (!pullRequest)
|
|
4740
|
+
return { value: {}, error: "GitHub pullRequest provenance response did not include a pullRequest object" };
|
|
4741
|
+
const editor = pullRequest.editor;
|
|
4742
|
+
const commits = pullRequest.commits;
|
|
4743
|
+
const nodes = Array.isArray(commits?.nodes) ? commits.nodes : [];
|
|
4744
|
+
const latestCommitNode = nodes[nodes.length - 1];
|
|
4745
|
+
const latestCommit = latestCommitNode?.commit;
|
|
4746
|
+
return {
|
|
4747
|
+
value: {
|
|
4748
|
+
bodyEditorLogin: typeof editor?.login === "string" ? editor.login : null,
|
|
4749
|
+
bodyLastEditedAt: typeof pullRequest.lastEditedAt === "string" ? pullRequest.lastEditedAt : null,
|
|
4750
|
+
headCommittedDate: typeof latestCommit?.committedDate === "string" ? latestCommit.committedDate : null
|
|
4751
|
+
}
|
|
4752
|
+
};
|
|
4753
|
+
}
|
|
4754
|
+
async function collectReviewThreads(input) {
|
|
4755
|
+
const reviewThreads = [];
|
|
4756
|
+
let afterCursor = null;
|
|
4757
|
+
for (let page = 0;page < 100; page += 1) {
|
|
4758
|
+
const afterLiteral = afterCursor ? JSON.stringify(afterCursor) : "null";
|
|
4759
|
+
const threadsResponse = await runJsonObject(input.command, [
|
|
4760
|
+
"api",
|
|
4761
|
+
"graphql",
|
|
4762
|
+
"-F",
|
|
4763
|
+
`owner=${input.owner}`,
|
|
4764
|
+
"-F",
|
|
4765
|
+
`name=${input.name}`,
|
|
4766
|
+
"-F",
|
|
4767
|
+
`prNumber=${input.prNumber}`,
|
|
4768
|
+
"-f",
|
|
4769
|
+
`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 } } } } }`
|
|
4770
|
+
], input.projectRoot);
|
|
4771
|
+
if (threadsResponse.error) {
|
|
4772
|
+
return { value: reviewThreads, error: threadsResponse.error };
|
|
4773
|
+
}
|
|
4774
|
+
const data = threadsResponse.value.data;
|
|
4775
|
+
const repository = data?.repository;
|
|
4776
|
+
const pullRequest = repository?.pullRequest;
|
|
4777
|
+
const threads = pullRequest?.reviewThreads;
|
|
4778
|
+
const nodes = threads?.nodes;
|
|
4779
|
+
if (!Array.isArray(nodes)) {
|
|
4780
|
+
return { value: reviewThreads, error: "GitHub reviewThreads response did not include a nodes array" };
|
|
4781
|
+
}
|
|
4782
|
+
const normalized = nodes.map(normalizeReviewThread).filter((entry) => !!entry);
|
|
4783
|
+
reviewThreads.push(...normalized);
|
|
4784
|
+
const truncatedCommentThread = normalized.find((thread) => thread.comments?.pageInfo?.hasNextPage === true);
|
|
4785
|
+
if (truncatedCommentThread) {
|
|
4786
|
+
return { value: reviewThreads, error: `GitHub review thread ${truncatedCommentThread.id ?? "unknown"} has more than 100 comments; nested pagination is incomplete` };
|
|
4787
|
+
}
|
|
4788
|
+
const pageInfo = threads?.pageInfo;
|
|
4789
|
+
if (!pageInfo) {
|
|
4790
|
+
if (nodes.length >= 100) {
|
|
4791
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination metadata missing after a full page" };
|
|
4792
|
+
}
|
|
4793
|
+
return { value: reviewThreads };
|
|
4794
|
+
}
|
|
4795
|
+
if (pageInfo.hasNextPage !== true) {
|
|
4796
|
+
return { value: reviewThreads };
|
|
4797
|
+
}
|
|
4798
|
+
if (typeof pageInfo.endCursor !== "string" || !pageInfo.endCursor.trim()) {
|
|
4799
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination reported hasNextPage without endCursor" };
|
|
4800
|
+
}
|
|
4801
|
+
afterCursor = pageInfo.endCursor;
|
|
4802
|
+
}
|
|
4803
|
+
return { value: reviewThreads, error: "GitHub reviewThreads pagination exceeded 100 pages" };
|
|
4804
|
+
}
|
|
4805
|
+
async function collectPrReviewEvidence(input) {
|
|
4806
|
+
const parsed = parseGithubPrUrl(input.prUrl);
|
|
4807
|
+
if (!parsed) {
|
|
4808
|
+
throw new Error(`Cannot parse GitHub PR URL: ${input.prUrl}`);
|
|
4809
|
+
}
|
|
4810
|
+
const readErrors = [];
|
|
4811
|
+
const viewRead = await runJsonObject(input.command, [
|
|
4812
|
+
"pr",
|
|
4813
|
+
"view",
|
|
4814
|
+
input.prUrl,
|
|
4815
|
+
"--json",
|
|
4816
|
+
"title,body,headRefOid,headRefName,baseRefName,state,isDraft,mergeable,mergeStateStatus,reviewDecision,reviews,statusCheckRollup"
|
|
4817
|
+
], input.projectRoot);
|
|
4818
|
+
if (viewRead.error)
|
|
4819
|
+
readErrors.push(viewRead.error);
|
|
4820
|
+
const view = viewRead.value;
|
|
4821
|
+
if (!Array.isArray(view.statusCheckRollup)) {
|
|
4822
|
+
readErrors.push("gh pr view did not return required statusCheckRollup array");
|
|
4823
|
+
}
|
|
4824
|
+
if (!Array.isArray(view.reviews)) {
|
|
4825
|
+
readErrors.push("gh pr view did not return required reviews array");
|
|
4826
|
+
}
|
|
4827
|
+
const headSha = firstString(view, ["headRefOid", "headSha", "head_sha"]);
|
|
4828
|
+
const baseRefName = firstString(view, ["baseRefName"]);
|
|
4829
|
+
const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
|
|
4830
|
+
const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
|
|
4831
|
+
const provenanceRead = await collectPullRequestProvenance({
|
|
4832
|
+
command: input.command,
|
|
4833
|
+
projectRoot: input.projectRoot,
|
|
4834
|
+
owner: parsed.owner,
|
|
4835
|
+
name: parsed.repo,
|
|
4836
|
+
prNumber: parsed.prNumber
|
|
4837
|
+
});
|
|
4838
|
+
const provenance = provenanceRead.value;
|
|
4839
|
+
const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
|
|
4840
|
+
if (reviewCommentsRead.error)
|
|
4841
|
+
readErrors.push(reviewCommentsRead.error);
|
|
4842
|
+
const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
|
|
4843
|
+
const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
|
|
4844
|
+
if (issueCommentsRead.error)
|
|
4845
|
+
readErrors.push(issueCommentsRead.error);
|
|
4846
|
+
const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
|
|
4847
|
+
const reviewThreadsRead = await collectReviewThreads({
|
|
4848
|
+
command: input.command,
|
|
4849
|
+
projectRoot: input.projectRoot,
|
|
4850
|
+
owner: parsed.owner,
|
|
4851
|
+
name: parsed.repo,
|
|
4852
|
+
prNumber: parsed.prNumber
|
|
4853
|
+
});
|
|
4854
|
+
if (reviewThreadsRead.error)
|
|
4855
|
+
readErrors.push(reviewThreadsRead.error);
|
|
4856
|
+
const reviewThreads = reviewThreadsRead.value;
|
|
4857
|
+
const greptileRollupChecks = statusCheckRollup.filter((check) => isGreptileLabel(checkName(check)));
|
|
4858
|
+
let greptileCheckDetails = [];
|
|
4859
|
+
if (headSha && greptileRollupChecks.length > 0) {
|
|
4860
|
+
const checkDetailsRead = await collectGreptileCheckDetails({
|
|
4861
|
+
command: input.command,
|
|
4862
|
+
projectRoot: input.projectRoot,
|
|
4863
|
+
repoName: parsed.repoName,
|
|
4864
|
+
headSha
|
|
4865
|
+
});
|
|
4866
|
+
greptileCheckDetails = checkDetailsRead.value;
|
|
4867
|
+
}
|
|
4868
|
+
const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
|
|
4869
|
+
const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
|
|
4870
|
+
const configuredGreptileApiRead = await collectConfiguredGreptileApiSignals({
|
|
4871
|
+
enabled: shouldCollectConfiguredGreptileApi,
|
|
4872
|
+
options: input.greptileApi,
|
|
4873
|
+
repoName: parsed.repoName,
|
|
4874
|
+
prNumber: parsed.prNumber,
|
|
4875
|
+
headSha,
|
|
4876
|
+
baseRefName
|
|
4877
|
+
});
|
|
4878
|
+
readErrors.push(...configuredGreptileApiRead.errors);
|
|
4879
|
+
const apiSignals = [...input.apiSignals ?? [], ...configuredGreptileApiRead.signals];
|
|
4880
|
+
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})` : ""}`);
|
|
4881
|
+
const pendingChecks = statusCheckRollup.filter((check) => isPendingCheck(check) && (isGreptileLabel(checkName(check)) || !isAllowedFailure(checkName(check), input.allowedFailures ?? []))).map((check) => `Check pending: ${checkName(check)}`);
|
|
4882
|
+
const evidenceBase = {
|
|
4883
|
+
title: firstString(view, ["title"]),
|
|
4884
|
+
body: firstString(view, ["body"]),
|
|
4885
|
+
bodyEditorLogin: provenance.bodyEditorLogin ?? null,
|
|
4886
|
+
bodyLastEditedAt: provenance.bodyLastEditedAt ?? null,
|
|
4887
|
+
headCommittedDate: provenance.headCommittedDate ?? null,
|
|
4888
|
+
reviews,
|
|
4889
|
+
changedFileReviewComments: reviewComments,
|
|
4890
|
+
relevantIssueComments: issueComments,
|
|
4891
|
+
reviewThreads,
|
|
4892
|
+
checks: checksWithGreptileDetails,
|
|
4893
|
+
currentHeadSha: headSha,
|
|
4894
|
+
apiSignals
|
|
4895
|
+
};
|
|
4896
|
+
const greptile = deriveGreptileEvidence(evidenceBase);
|
|
4897
|
+
return {
|
|
4898
|
+
prUrl: input.prUrl,
|
|
4899
|
+
prNumber: parsed.prNumber,
|
|
4900
|
+
repoName: parsed.repoName,
|
|
4901
|
+
title: evidenceBase.title,
|
|
4902
|
+
body: evidenceBase.body,
|
|
4903
|
+
bodyEditorLogin: evidenceBase.bodyEditorLogin,
|
|
4904
|
+
bodyLastEditedAt: evidenceBase.bodyLastEditedAt,
|
|
4905
|
+
headCommittedDate: evidenceBase.headCommittedDate,
|
|
4906
|
+
headSha,
|
|
4907
|
+
headRefName: firstString(view, ["headRefName"]),
|
|
4908
|
+
baseRefName,
|
|
4909
|
+
state: firstString(view, ["state"]),
|
|
4910
|
+
isDraft: typeof view.isDraft === "boolean" ? view.isDraft : null,
|
|
4911
|
+
mergeable: firstString(view, ["mergeable"]),
|
|
4912
|
+
mergeStateStatus: firstString(view, ["mergeStateStatus"]),
|
|
4913
|
+
reviewDecision: firstString(view, ["reviewDecision"]),
|
|
4914
|
+
reviews,
|
|
4915
|
+
reviewThreads,
|
|
4916
|
+
changedFileReviewComments: reviewComments,
|
|
4917
|
+
relevantIssueComments: issueComments,
|
|
4918
|
+
statusCheckRollup: checksWithGreptileDetails,
|
|
4919
|
+
checkFailures,
|
|
4920
|
+
pendingChecks,
|
|
4921
|
+
readErrors,
|
|
4922
|
+
greptile
|
|
4923
|
+
};
|
|
4924
|
+
}
|
|
4925
|
+
function capGateMessage(value, maxChars = 1200) {
|
|
4926
|
+
const normalized = value.trim();
|
|
4927
|
+
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}
|
|
4928
|
+
[truncated for gate summary; see full evidence artifact]` : normalized;
|
|
4929
|
+
}
|
|
4930
|
+
function evaluateEvidence(evidence) {
|
|
4931
|
+
const reasonDetails = [];
|
|
4932
|
+
const warnings = [];
|
|
4933
|
+
const seen = new Set;
|
|
4934
|
+
const addReason = (reason) => {
|
|
4935
|
+
const capped = { ...reason, message: capGateMessage(reason.message) };
|
|
4936
|
+
const key = `${capped.code}:${capped.message}`;
|
|
4937
|
+
if (seen.has(key))
|
|
4938
|
+
return;
|
|
4939
|
+
seen.add(key);
|
|
4940
|
+
reasonDetails.push(capped);
|
|
4941
|
+
};
|
|
4942
|
+
const greptile = evidence.greptile;
|
|
4943
|
+
const staleSignal = greptile.signals.find((signal) => signal.reviewedSha && signal.reviewedSha !== evidence.headSha);
|
|
4944
|
+
const hasPendingGreptileCheck = evidence.pendingChecks.some((check) => /greptile/i.test(check));
|
|
4945
|
+
const pendingGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && signal.verdict === "pending" && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4946
|
+
const unknownGreptileApiSignals = greptile.signals.filter((signal) => signal.source === "api" && !signal.verdict && (!signal.reviewedSha || signal.reviewedSha === evidence.headSha));
|
|
4947
|
+
const awaitingFreshGreptileProof = hasPendingGreptileCheck || pendingGreptileApiSignals.length > 0 || !greptile.completed || greptile.mapping === "missing" || greptile.mapping === "stale";
|
|
4948
|
+
for (const error of evidence.readErrors) {
|
|
4949
|
+
addReason({
|
|
4950
|
+
code: "read_error",
|
|
4951
|
+
reasonClass: "reject",
|
|
4952
|
+
surface: error.startsWith("Greptile API/MCP") ? "greptile" : "github",
|
|
4953
|
+
suggestedAction: "needs_attention",
|
|
4954
|
+
message: `Required PR evidence surface could not be read completely: ${error}`,
|
|
4955
|
+
headSha: evidence.headSha || null
|
|
4956
|
+
});
|
|
4957
|
+
}
|
|
4958
|
+
if (!evidence.headSha) {
|
|
4959
|
+
addReason({
|
|
4960
|
+
code: "missing_head_sha",
|
|
4961
|
+
reasonClass: "reject",
|
|
4962
|
+
surface: "github",
|
|
4963
|
+
suggestedAction: "needs_attention",
|
|
4964
|
+
message: "PR head SHA could not be read; current-head Greptile approval cannot be proven.",
|
|
4965
|
+
headSha: null
|
|
4966
|
+
});
|
|
4967
|
+
}
|
|
4968
|
+
for (const failure of evidence.checkFailures) {
|
|
4969
|
+
addReason({
|
|
4970
|
+
code: "ci_failed",
|
|
4971
|
+
reasonClass: "reject",
|
|
4972
|
+
surface: "ci",
|
|
4973
|
+
suggestedAction: "fix",
|
|
4974
|
+
message: failure,
|
|
4975
|
+
headSha: evidence.headSha || null
|
|
4976
|
+
});
|
|
4977
|
+
}
|
|
4978
|
+
for (const pendingCheck of evidence.pendingChecks) {
|
|
4979
|
+
addReason({
|
|
4980
|
+
code: "check_pending",
|
|
4981
|
+
reasonClass: "pending",
|
|
4982
|
+
surface: "ci",
|
|
4983
|
+
suggestedAction: "wait",
|
|
4984
|
+
message: pendingCheck,
|
|
4985
|
+
headSha: evidence.headSha || null
|
|
4986
|
+
});
|
|
4987
|
+
}
|
|
4988
|
+
const reviewDecision = String(evidence.reviewDecision ?? "").toUpperCase();
|
|
4989
|
+
if (reviewDecision === "CHANGES_REQUESTED" || reviewDecision === "REVIEW_REQUIRED") {
|
|
4990
|
+
addReason({
|
|
4991
|
+
code: "review_decision_blocking",
|
|
4992
|
+
reasonClass: "reject",
|
|
4993
|
+
surface: "review",
|
|
4994
|
+
suggestedAction: "fix",
|
|
4995
|
+
message: `Required review is unresolved (${evidence.reviewDecision}).`,
|
|
4996
|
+
headSha: evidence.headSha || null
|
|
4997
|
+
});
|
|
4998
|
+
}
|
|
4999
|
+
for (const thread of unresolvedThreadSummaries(evidence.reviewThreads)) {
|
|
5000
|
+
addReason({
|
|
5001
|
+
code: "review_thread_unresolved",
|
|
5002
|
+
reasonClass: "reject",
|
|
5003
|
+
surface: "review",
|
|
5004
|
+
suggestedAction: "fix",
|
|
5005
|
+
message: thread,
|
|
5006
|
+
headSha: evidence.headSha || null
|
|
5007
|
+
});
|
|
5008
|
+
}
|
|
5009
|
+
if (greptile.mapping === "missing") {
|
|
5010
|
+
addReason({
|
|
5011
|
+
code: "greptile_missing",
|
|
5012
|
+
reasonClass: "pending",
|
|
5013
|
+
surface: "greptile",
|
|
5014
|
+
suggestedAction: "wait",
|
|
5015
|
+
message: "Missing Greptile check/review evidence for this PR.",
|
|
5016
|
+
headSha: evidence.headSha || null
|
|
5017
|
+
});
|
|
5018
|
+
}
|
|
5019
|
+
if (greptile.mapping === "stale" || greptile.reviewedSha && greptile.reviewedSha !== evidence.headSha || !greptile.approved && staleSignal) {
|
|
5020
|
+
addReason({
|
|
5021
|
+
code: "greptile_stale",
|
|
5022
|
+
reasonClass: "pending",
|
|
5023
|
+
surface: "greptile",
|
|
5024
|
+
suggestedAction: "wait",
|
|
5025
|
+
message: `Greptile evidence is stale (reviewed ${greptile.reviewedSha ?? staleSignal?.reviewedSha ?? "unknown"}, current ${evidence.headSha || "unknown"}).`,
|
|
5026
|
+
headSha: evidence.headSha || null,
|
|
5027
|
+
reviewedSha: greptile.reviewedSha ?? staleSignal?.reviewedSha ?? null
|
|
5028
|
+
});
|
|
5029
|
+
}
|
|
5030
|
+
for (const signal of pendingGreptileApiSignals) {
|
|
5031
|
+
addReason({
|
|
5032
|
+
code: "greptile_pending",
|
|
5033
|
+
reasonClass: "pending",
|
|
5034
|
+
surface: "greptile",
|
|
5035
|
+
suggestedAction: "wait",
|
|
5036
|
+
message: `Greptile API/MCP review is pending for the current PR head${signal.bodyExcerpt ? `: ${signal.bodyExcerpt}` : "."}`,
|
|
5037
|
+
headSha: evidence.headSha || null,
|
|
5038
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
5039
|
+
});
|
|
5040
|
+
}
|
|
5041
|
+
for (const signal of unknownGreptileApiSignals) {
|
|
5042
|
+
addReason({
|
|
5043
|
+
code: "greptile_api_status_unknown",
|
|
5044
|
+
reasonClass: "reject",
|
|
5045
|
+
surface: "greptile",
|
|
5046
|
+
suggestedAction: "needs_attention",
|
|
5047
|
+
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}` : "."}`,
|
|
5048
|
+
headSha: evidence.headSha || null,
|
|
5049
|
+
reviewedSha: signal.reviewedSha ?? null
|
|
5050
|
+
});
|
|
5051
|
+
}
|
|
5052
|
+
if (!greptile.completed) {
|
|
5053
|
+
addReason({
|
|
5054
|
+
code: "greptile_pending",
|
|
5055
|
+
reasonClass: "pending",
|
|
5056
|
+
surface: "greptile",
|
|
5057
|
+
suggestedAction: "wait",
|
|
5058
|
+
message: "Greptile check/review has not completed for the current PR head.",
|
|
5059
|
+
headSha: evidence.headSha || null,
|
|
5060
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5061
|
+
});
|
|
5062
|
+
}
|
|
5063
|
+
if (!greptile.fresh) {
|
|
5064
|
+
addReason({
|
|
5065
|
+
code: "greptile_not_current_head",
|
|
5066
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
5067
|
+
surface: "greptile",
|
|
5068
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
5069
|
+
message: "Greptile approval is not tied to the current PR head SHA.",
|
|
5070
|
+
headSha: evidence.headSha || null,
|
|
5071
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5072
|
+
});
|
|
5073
|
+
}
|
|
5074
|
+
if (greptile.score && !(greptile.score.scale === 5 && greptile.score.value === 5)) {
|
|
5075
|
+
addReason({
|
|
5076
|
+
code: "greptile_score_not_5",
|
|
5077
|
+
reasonClass: "reject",
|
|
5078
|
+
surface: "greptile",
|
|
5079
|
+
suggestedAction: "fix",
|
|
5080
|
+
message: `Greptile score is ${greptile.score.value}/${greptile.score.scale}; strict merge requires trusted current-head 5/5.`,
|
|
5081
|
+
headSha: evidence.headSha || null,
|
|
5082
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5083
|
+
});
|
|
5084
|
+
}
|
|
5085
|
+
const hasApprovedMapping = greptile.mapping === "score-5-of-5" || greptile.mapping === "explicit-approved";
|
|
5086
|
+
if (!greptile.score && !hasApprovedMapping) {
|
|
5087
|
+
addReason({
|
|
5088
|
+
code: "greptile_score_missing",
|
|
5089
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
5090
|
+
surface: "greptile",
|
|
5091
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
5092
|
+
message: "No parseable Greptile 5/5 score or direct current-head Greptile API APPROVED mapping was found from trusted evidence; merge is blocked.",
|
|
5093
|
+
headSha: evidence.headSha || null,
|
|
5094
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5095
|
+
});
|
|
5096
|
+
}
|
|
5097
|
+
if (greptile.mapping === "unproven") {
|
|
5098
|
+
addReason({
|
|
5099
|
+
code: "greptile_mapping_unproven",
|
|
5100
|
+
reasonClass: awaitingFreshGreptileProof ? "pending" : "reject",
|
|
5101
|
+
surface: "greptile",
|
|
5102
|
+
suggestedAction: awaitingFreshGreptileProof ? "wait" : "ask_greptile",
|
|
5103
|
+
message: "Greptile approval mapping is unproven; PR body/title or a green check alone cannot approve merge.",
|
|
5104
|
+
headSha: evidence.headSha || null,
|
|
5105
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5106
|
+
});
|
|
5107
|
+
}
|
|
5108
|
+
for (const blocker of greptile.blockers) {
|
|
5109
|
+
addReason({
|
|
5110
|
+
code: "greptile_blocker_text",
|
|
5111
|
+
reasonClass: "reject",
|
|
5112
|
+
surface: "greptile",
|
|
5113
|
+
suggestedAction: "fix",
|
|
5114
|
+
message: `Greptile/blocker text: ${blocker}`,
|
|
5115
|
+
headSha: evidence.headSha || null,
|
|
5116
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5117
|
+
});
|
|
5118
|
+
}
|
|
5119
|
+
for (const comment of greptile.unresolvedComments) {
|
|
5120
|
+
addReason({
|
|
5121
|
+
code: "greptile_unresolved_comment",
|
|
5122
|
+
reasonClass: "reject",
|
|
5123
|
+
surface: "greptile",
|
|
5124
|
+
suggestedAction: "fix",
|
|
5125
|
+
message: comment,
|
|
5126
|
+
headSha: evidence.headSha || null,
|
|
5127
|
+
reviewedSha: greptile.reviewedSha ?? null
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
if (!greptile.approved)
|
|
5131
|
+
warnings.push(`Greptile approval mapping is ${greptile.mapping}.`);
|
|
5132
|
+
const pending = reasonDetails.length > 0 && reasonDetails.every((reason) => reason.reasonClass === "pending");
|
|
5133
|
+
return { reasons: reasonDetails.map((reason) => reason.message), reasonDetails, warnings, pending };
|
|
5134
|
+
}
|
|
5135
|
+
function evaluateStrictPrMergeGate(evidence) {
|
|
5136
|
+
const evaluated = evaluateEvidence(evidence);
|
|
5137
|
+
const approved = evaluated.reasonDetails.length === 0 && evidence.greptile.approved;
|
|
5138
|
+
return {
|
|
5139
|
+
approved,
|
|
5140
|
+
pending: evaluated.pending,
|
|
5141
|
+
reasons: evaluated.reasons,
|
|
5142
|
+
reasonDetails: evaluated.reasonDetails,
|
|
5143
|
+
warnings: evaluated.warnings,
|
|
5144
|
+
actionableFeedback: evaluated.reasons,
|
|
5145
|
+
evidence
|
|
5146
|
+
};
|
|
5147
|
+
}
|
|
5148
|
+
function strictMergeHeadShaFromGate(result, prUrl) {
|
|
5149
|
+
if (!result.approved) {
|
|
5150
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate is not approved.`);
|
|
5151
|
+
}
|
|
5152
|
+
if (result.evidence.prUrl !== prUrl) {
|
|
5153
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate evidence belongs to ${result.evidence.prUrl}.`);
|
|
5154
|
+
}
|
|
5155
|
+
const headSha = result.evidence.headSha?.trim();
|
|
5156
|
+
if (!headSha) {
|
|
5157
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate did not provide a current head SHA.`);
|
|
5158
|
+
}
|
|
5159
|
+
if (!/^[0-9a-f]{40}$/i.test(headSha)) {
|
|
5160
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate head is not a raw 40-character commit SHA.`);
|
|
5161
|
+
}
|
|
5162
|
+
if (!result.evidence.greptile.fresh || result.evidence.greptile.currentHeadSha !== headSha) {
|
|
5163
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate approval is not tied to head ${headSha}.`);
|
|
5164
|
+
}
|
|
5165
|
+
if (result.evidence.greptile.mapping !== "score-5-of-5" && result.evidence.greptile.mapping !== "explicit-approved") {
|
|
5166
|
+
throw new Error(`Refusing to merge ${prUrl}: strict merge gate mapping is ${result.evidence.greptile.mapping}.`);
|
|
5167
|
+
}
|
|
5168
|
+
return headSha;
|
|
5169
|
+
}
|
|
5170
|
+
function promptExcerpt(value, maxChars = 4000) {
|
|
5171
|
+
return value.length > maxChars ? `${value.slice(0, maxChars)}
|
|
5172
|
+
|
|
5173
|
+
[truncated for prompt; see full evidence artifact]` : value;
|
|
5174
|
+
}
|
|
5175
|
+
function promptJsonExcerpt(value, maxChars = 6000) {
|
|
5176
|
+
return promptExcerpt(JSON.stringify(value, null, 2), maxChars);
|
|
5177
|
+
}
|
|
5178
|
+
function buildStrictPrGateSteeringPrompt(result) {
|
|
5179
|
+
const evidence = result.evidence;
|
|
5180
|
+
const unresolvedReviewThreads = evidence.reviewThreads.filter((thread) => thread.isResolved !== true && thread.isOutdated !== true);
|
|
5181
|
+
const displayedReasons = result.reasons.slice(0, 20).map((reason) => `- ${promptExcerpt(reason, 1200)}`);
|
|
5182
|
+
if (result.reasons.length > displayedReasons.length) {
|
|
5183
|
+
displayedReasons.push(`- ${result.reasons.length - displayedReasons.length} additional gate reasons omitted from prompt; see merge-gate-result.json.`);
|
|
5184
|
+
}
|
|
5185
|
+
const lines = [
|
|
5186
|
+
`Strict PR merge gate blocked ${evidence.prUrl}.`,
|
|
5187
|
+
`PR title: ${evidence.title || "(empty)"}`,
|
|
5188
|
+
`Current PR head SHA: ${evidence.headSha || "unknown"}`,
|
|
5189
|
+
`Greptile mapping: ${evidence.greptile.mapping}`,
|
|
5190
|
+
evidence.greptile.score ? `Greptile score: ${evidence.greptile.score.value}/${evidence.greptile.score.scale}` : "Greptile score: not proven",
|
|
5191
|
+
"",
|
|
5192
|
+
"Gate reasons:",
|
|
5193
|
+
...displayedReasons.length ? displayedReasons : ["- No reasons recorded"],
|
|
5194
|
+
"",
|
|
5195
|
+
"Structured gate reason details:",
|
|
5196
|
+
result.reasonDetails.length ? promptJsonExcerpt(result.reasonDetails, 4000) : "[]",
|
|
5197
|
+
"",
|
|
5198
|
+
"Required evidence read status:",
|
|
5199
|
+
evidence.readErrors.length ? promptJsonExcerpt(evidence.readErrors, 2000) : "All required PR evidence surfaces were read completely.",
|
|
5200
|
+
"",
|
|
5201
|
+
"Full PR title:",
|
|
5202
|
+
evidence.title || "(empty)",
|
|
5203
|
+
"",
|
|
5204
|
+
"PR body excerpt:",
|
|
5205
|
+
evidence.body ? promptExcerpt(evidence.body) : "(empty)",
|
|
5206
|
+
"",
|
|
5207
|
+
"All review comments on changed files:",
|
|
5208
|
+
evidence.changedFileReviewComments.length ? promptJsonExcerpt(evidence.changedFileReviewComments) : "[]",
|
|
5209
|
+
"",
|
|
5210
|
+
"Unresolved review threads:",
|
|
5211
|
+
unresolvedReviewThreads.length ? promptJsonExcerpt(unresolvedReviewThreads) : "[]",
|
|
5212
|
+
"",
|
|
5213
|
+
"Relevant issue-level PR comments:",
|
|
5214
|
+
evidence.relevantIssueComments.length ? promptJsonExcerpt(evidence.relevantIssueComments) : "[]",
|
|
5215
|
+
"",
|
|
5216
|
+
"CI/check failures and pending checks:",
|
|
5217
|
+
promptJsonExcerpt({ failures: evidence.checkFailures, pending: evidence.pendingChecks, rollup: evidence.statusCheckRollup }),
|
|
5218
|
+
"",
|
|
5219
|
+
"Greptile evidence:",
|
|
5220
|
+
promptJsonExcerpt(evidence.greptile)
|
|
5221
|
+
];
|
|
5222
|
+
if (result.artifacts) {
|
|
5223
|
+
lines.push("", "Full evidence artifacts:", JSON.stringify(result.artifacts, null, 2));
|
|
5224
|
+
}
|
|
5225
|
+
return lines.join(`
|
|
5226
|
+
`);
|
|
5227
|
+
}
|
|
5228
|
+
function persistPrReviewCycleArtifacts(input) {
|
|
5229
|
+
const cycleName = input.final ? `${input.cycle}-final` : String(input.cycle);
|
|
5230
|
+
const taskArtifactRoot = input.artifactRoot?.trim() ? input.artifactRoot : resolve20(input.projectRoot, "artifacts", input.taskId);
|
|
5231
|
+
const root = resolve20(taskArtifactRoot, "pr-review-cycles", cycleName);
|
|
5232
|
+
mkdirSync9(root, { recursive: true });
|
|
5233
|
+
const finalMergeGateResultPath = input.final ? resolve20(taskArtifactRoot, "merge-gate-final.json") : undefined;
|
|
5234
|
+
const paths = {
|
|
5235
|
+
root,
|
|
5236
|
+
prTitlePath: resolve20(root, "pr-title.md"),
|
|
5237
|
+
prBodyPath: resolve20(root, "pr-body.md"),
|
|
5238
|
+
prCommentsPath: resolve20(root, "pr-comments.json"),
|
|
5239
|
+
reviewThreadsPath: resolve20(root, "review-threads.json"),
|
|
5240
|
+
reviewCommentsPath: resolve20(root, "review-comments.json"),
|
|
5241
|
+
checkRollupPath: resolve20(root, "check-rollup.json"),
|
|
5242
|
+
greptileEvidencePath: resolve20(root, "greptile-evidence.json"),
|
|
5243
|
+
mergeGateResultPath: resolve20(root, "merge-gate-result.json"),
|
|
5244
|
+
steeringPromptPath: resolve20(root, "agent-steering-prompt.md"),
|
|
5245
|
+
...finalMergeGateResultPath ? { finalMergeGateResultPath } : {}
|
|
4181
5246
|
};
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
}
|
|
4209
|
-
const { result, exitCode } = await dispatchValidator(cmd, effectiveRegistry, validatorCtx, (id) => runValidatorBinary(projectRoot, taskId, id, resolvedContext));
|
|
4210
|
-
const durationSeconds = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
4211
|
-
const logFile = resolve20(taskLogDir, `${cmd.replace(":", "-")}-validation.log`);
|
|
4212
|
-
mkdirSync7(taskLogDir, { recursive: true });
|
|
4213
|
-
writeFileSync7(logFile, `=== ${nowIso()} :: ${cmd} ===
|
|
4214
|
-
${JSON.stringify(result, null, 2)}
|
|
4215
|
-
`, "utf-8");
|
|
4216
|
-
if (result.passed) {
|
|
4217
|
-
passed += 1;
|
|
4218
|
-
categories.push({ category: cmd, status: "pass", duration_seconds: durationSeconds });
|
|
4219
|
-
} else {
|
|
4220
|
-
failed += 1;
|
|
4221
|
-
categories.push({ category: cmd, status: "fail", exit_code: exitCode, duration_seconds: durationSeconds });
|
|
4222
|
-
const desc = valDescriptions[cmd];
|
|
4223
|
-
if (desc) {
|
|
4224
|
-
console.log(` What this checks (${cmd}): ${desc}`);
|
|
4225
|
-
}
|
|
4226
|
-
}
|
|
4227
|
-
}
|
|
4228
|
-
const summary = {
|
|
4229
|
-
status: failed === 0 ? "pass" : "fail",
|
|
4230
|
-
total: commands.length,
|
|
4231
|
-
passed,
|
|
4232
|
-
failed,
|
|
4233
|
-
categories
|
|
5247
|
+
writeFileSync9(paths.prTitlePath, input.result.evidence.title || "", "utf8");
|
|
5248
|
+
writeFileSync9(paths.prBodyPath, input.result.evidence.body || "", "utf8");
|
|
5249
|
+
writeFileSync9(paths.prCommentsPath, `${JSON.stringify(input.result.evidence.relevantIssueComments, null, 2)}
|
|
5250
|
+
`, "utf8");
|
|
5251
|
+
writeFileSync9(paths.reviewThreadsPath, `${JSON.stringify(input.result.evidence.reviewThreads, null, 2)}
|
|
5252
|
+
`, "utf8");
|
|
5253
|
+
writeFileSync9(paths.reviewCommentsPath, `${JSON.stringify(input.result.evidence.changedFileReviewComments, null, 2)}
|
|
5254
|
+
`, "utf8");
|
|
5255
|
+
writeFileSync9(paths.checkRollupPath, `${JSON.stringify(input.result.evidence.statusCheckRollup, null, 2)}
|
|
5256
|
+
`, "utf8");
|
|
5257
|
+
writeFileSync9(paths.greptileEvidencePath, `${JSON.stringify(input.result.evidence.greptile, null, 2)}
|
|
5258
|
+
`, "utf8");
|
|
5259
|
+
const mergeGatePayload = {
|
|
5260
|
+
approved: input.result.approved,
|
|
5261
|
+
pending: input.result.pending,
|
|
5262
|
+
reasons: input.result.reasons,
|
|
5263
|
+
reasonDetails: input.result.reasonDetails,
|
|
5264
|
+
warnings: input.result.warnings,
|
|
5265
|
+
actionableFeedback: input.result.actionableFeedback,
|
|
5266
|
+
prUrl: input.result.evidence.prUrl,
|
|
5267
|
+
title: input.result.evidence.title,
|
|
5268
|
+
headSha: input.result.evidence.headSha,
|
|
5269
|
+
readErrors: input.result.evidence.readErrors,
|
|
5270
|
+
greptile: input.result.evidence.greptile,
|
|
5271
|
+
evidence: input.result.evidence,
|
|
5272
|
+
cycleArtifactRoot: root
|
|
4234
5273
|
};
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
5274
|
+
writeFileSync9(paths.mergeGateResultPath, `${JSON.stringify(mergeGatePayload, null, 2)}
|
|
5275
|
+
`, "utf8");
|
|
5276
|
+
if (paths.finalMergeGateResultPath) {
|
|
5277
|
+
writeFileSync9(paths.finalMergeGateResultPath, `${JSON.stringify(mergeGatePayload, null, 2)}
|
|
5278
|
+
`, "utf8");
|
|
5279
|
+
}
|
|
5280
|
+
writeFileSync9(paths.steeringPromptPath, input.steeringPrompt, "utf8");
|
|
5281
|
+
return paths;
|
|
5282
|
+
}
|
|
5283
|
+
async function runStrictPrMergeGate(input) {
|
|
5284
|
+
const evidence = await collectPrReviewEvidence(input);
|
|
5285
|
+
const base = evaluateStrictPrMergeGate(evidence);
|
|
5286
|
+
const preliminaryPrompt = buildStrictPrGateSteeringPrompt({ ...base, artifacts: undefined });
|
|
5287
|
+
const artifacts = persistPrReviewCycleArtifacts({
|
|
5288
|
+
projectRoot: input.projectRoot,
|
|
5289
|
+
taskId: input.taskId,
|
|
5290
|
+
cycle: input.cycle,
|
|
5291
|
+
artifactRoot: input.artifactRoot,
|
|
5292
|
+
result: base,
|
|
5293
|
+
steeringPrompt: preliminaryPrompt,
|
|
5294
|
+
final: input.final
|
|
5295
|
+
});
|
|
5296
|
+
const steeringPrompt = buildStrictPrGateSteeringPrompt({ ...base, artifacts });
|
|
5297
|
+
writeFileSync9(artifacts.steeringPromptPath, steeringPrompt, "utf8");
|
|
5298
|
+
return { ...base, artifacts, steeringPrompt };
|
|
4239
5299
|
}
|
|
4240
5300
|
|
|
4241
5301
|
// packages/runtime/src/control-plane/native/verifier.ts
|
|
4242
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
4243
|
-
import { resolve as resolve21 } from "path";
|
|
4244
5302
|
async function verifyTask(options) {
|
|
4245
5303
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
4246
5304
|
const taskId = options.taskId;
|
|
4247
5305
|
const normalizedTaskId = lookupTask(options.projectRoot, taskId);
|
|
4248
5306
|
const artifactDir = artifactDirForId(options.projectRoot, taskId);
|
|
4249
|
-
|
|
5307
|
+
mkdirSync10(artifactDir, { recursive: true });
|
|
4250
5308
|
const validationSummaryPath = resolve21(artifactDir, "validation-summary.json");
|
|
4251
5309
|
const reviewFeedbackPath = resolve21(artifactDir, "review-feedback.md");
|
|
4252
5310
|
const reviewStatePath = resolve21(artifactDir, "review-state.json");
|
|
@@ -4304,12 +5362,6 @@ async function verifyTask(options) {
|
|
|
4304
5362
|
if (sourceCloseoutIssueId) {
|
|
4305
5363
|
localReasons.push(...evaluateGithubSourceIssuePrCloseout(options.projectRoot, prStates, sourceCloseoutIssueId));
|
|
4306
5364
|
}
|
|
4307
|
-
const pluginResults = await options.plugins.runValidators(taskId);
|
|
4308
|
-
for (const result of pluginResults) {
|
|
4309
|
-
if (!result.passed) {
|
|
4310
|
-
localReasons.push(`[Plugin Validator] ${result.id}: ${result.summary}`);
|
|
4311
|
-
}
|
|
4312
|
-
}
|
|
4313
5365
|
const reviewMode = await loadReviewMode(paths.reviewProfilePath, process.env.AI_REVIEW_MODE || "advisory");
|
|
4314
5366
|
const reviewProvider = await loadReviewProvider(paths.reviewProfilePath, process.env.AI_REVIEW_PROVIDER || "greptile");
|
|
4315
5367
|
if (!options.skipAiReview && localReasons.length === 0 && reviewProvider === "greptile" && reviewMode !== "off") {
|
|
@@ -4328,7 +5380,7 @@ async function verifyTask(options) {
|
|
|
4328
5380
|
aiReasons.push(`[AI Review] Required mode needs a completed Greptile approval; current verdict is ${ai.verdict}.`);
|
|
4329
5381
|
}
|
|
4330
5382
|
if (persistArtifacts && ai.rawResponse) {
|
|
4331
|
-
|
|
5383
|
+
writeFileSync10(greptileRawPath, `${ai.rawResponse}
|
|
4332
5384
|
`, "utf-8");
|
|
4333
5385
|
}
|
|
4334
5386
|
} else if (!options.skipAiReview && reviewMode === "off") {
|
|
@@ -4837,7 +5889,7 @@ function writeFeedbackFile(options) {
|
|
|
4837
5889
|
if (options.aiRawFeedback) {
|
|
4838
5890
|
lines.push("## Raw Reviewer Feedback", "", "```text", options.aiRawFeedback, "```", "");
|
|
4839
5891
|
}
|
|
4840
|
-
|
|
5892
|
+
writeFileSync10(options.output, `${lines.join(`
|
|
4841
5893
|
`)}
|
|
4842
5894
|
`, "utf-8");
|
|
4843
5895
|
}
|
|
@@ -4854,7 +5906,7 @@ function writeReviewStateFile(options) {
|
|
|
4854
5906
|
ai_warnings: options.aiWarnings,
|
|
4855
5907
|
updated_at: nowIso()
|
|
4856
5908
|
};
|
|
4857
|
-
|
|
5909
|
+
writeFileSync10(options.output, `${JSON.stringify(payload, null, 2)}
|
|
4858
5910
|
`, "utf-8");
|
|
4859
5911
|
}
|
|
4860
5912
|
async function runGreptileReviewForPr(options) {
|
|
@@ -5036,7 +6088,8 @@ async function runGreptileReviewForPr(options) {
|
|
|
5036
6088
|
}
|
|
5037
6089
|
};
|
|
5038
6090
|
}
|
|
5039
|
-
|
|
6091
|
+
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
6092
|
+
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)) {
|
|
5040
6093
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
5041
6094
|
return {
|
|
5042
6095
|
verdict: "REJECT",
|
|
@@ -5052,44 +6105,79 @@ async function runGreptileReviewForPr(options) {
|
|
|
5052
6105
|
}
|
|
5053
6106
|
};
|
|
5054
6107
|
}
|
|
5055
|
-
if (score) {
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
6108
|
+
if (score?.scale === 5 && score.value < 5) {
|
|
6109
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; strict review requires 5/5 before merge.`);
|
|
6110
|
+
return {
|
|
6111
|
+
verdict: "REJECT",
|
|
6112
|
+
feedback,
|
|
6113
|
+
reasons,
|
|
6114
|
+
warnings,
|
|
6115
|
+
rawPayload: {
|
|
6116
|
+
pr: options.prState,
|
|
6117
|
+
codeReviews: reviewsPayload,
|
|
6118
|
+
selectedReview,
|
|
6119
|
+
reviewDetails,
|
|
6120
|
+
comments: commentsPayload,
|
|
6121
|
+
score
|
|
6122
|
+
}
|
|
6123
|
+
};
|
|
6124
|
+
}
|
|
6125
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
6126
|
+
let strictGate = null;
|
|
6127
|
+
try {
|
|
6128
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
6129
|
+
projectRoot: options.projectRoot,
|
|
6130
|
+
taskId: options.taskId,
|
|
6131
|
+
prUrl,
|
|
6132
|
+
apiSignals: [{
|
|
6133
|
+
id: selectedReview.id,
|
|
6134
|
+
body: reviewBody,
|
|
6135
|
+
reviewedSha: selectedReview.metadata?.checkHeadSha ?? null,
|
|
6136
|
+
status: selectedReview.status
|
|
6137
|
+
}]
|
|
6138
|
+
});
|
|
6139
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
6140
|
+
} catch (error) {
|
|
6141
|
+
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
6142
|
+
return {
|
|
6143
|
+
verdict: "REJECT",
|
|
6144
|
+
feedback,
|
|
6145
|
+
reasons,
|
|
6146
|
+
warnings,
|
|
6147
|
+
rawPayload: {
|
|
6148
|
+
pr: options.prState,
|
|
6149
|
+
codeReviews: reviewsPayload,
|
|
6150
|
+
selectedReview,
|
|
6151
|
+
reviewDetails,
|
|
6152
|
+
comments: commentsPayload,
|
|
6153
|
+
score
|
|
6154
|
+
}
|
|
6155
|
+
};
|
|
6156
|
+
}
|
|
6157
|
+
if (!strictGate.approved) {
|
|
6158
|
+
return {
|
|
6159
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
6160
|
+
feedback,
|
|
6161
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
6162
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
6163
|
+
rawPayload: {
|
|
6164
|
+
pr: options.prState,
|
|
6165
|
+
codeReviews: reviewsPayload,
|
|
6166
|
+
selectedReview,
|
|
6167
|
+
reviewDetails,
|
|
6168
|
+
comments: commentsPayload,
|
|
6169
|
+
score,
|
|
6170
|
+
strictGate: {
|
|
6171
|
+
approved: strictGate.approved,
|
|
6172
|
+
pending: strictGate.pending,
|
|
6173
|
+
reasons: strictGate.reasons,
|
|
6174
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6175
|
+
warnings: strictGate.warnings,
|
|
6176
|
+
greptile: strictGate.evidence.greptile,
|
|
6177
|
+
readErrors: strictGate.evidence.readErrors
|
|
5087
6178
|
}
|
|
5088
|
-
}
|
|
5089
|
-
}
|
|
5090
|
-
if (score.scale === 5 && score.value < 5) {
|
|
5091
|
-
warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
|
|
5092
|
-
}
|
|
6179
|
+
}
|
|
6180
|
+
};
|
|
5093
6181
|
}
|
|
5094
6182
|
return {
|
|
5095
6183
|
verdict: "APPROVE",
|
|
@@ -5101,7 +6189,16 @@ async function runGreptileReviewForPr(options) {
|
|
|
5101
6189
|
codeReviews: reviewsPayload,
|
|
5102
6190
|
selectedReview,
|
|
5103
6191
|
reviewDetails,
|
|
5104
|
-
comments: commentsPayload
|
|
6192
|
+
comments: commentsPayload,
|
|
6193
|
+
strictGate: {
|
|
6194
|
+
approved: strictGate.approved,
|
|
6195
|
+
pending: strictGate.pending,
|
|
6196
|
+
reasons: strictGate.reasons,
|
|
6197
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6198
|
+
warnings: strictGate.warnings,
|
|
6199
|
+
greptile: strictGate.evidence.greptile,
|
|
6200
|
+
readErrors: strictGate.evidence.readErrors
|
|
6201
|
+
}
|
|
5105
6202
|
}
|
|
5106
6203
|
};
|
|
5107
6204
|
}
|
|
@@ -5125,7 +6222,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5125
6222
|
let threads = [];
|
|
5126
6223
|
let actionableThreads = [];
|
|
5127
6224
|
let checkRollup = [];
|
|
5128
|
-
let
|
|
6225
|
+
let checkState2 = { pending: false, completed: false };
|
|
5129
6226
|
for (let attempt = 0;; attempt += 1) {
|
|
5130
6227
|
reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
|
|
5131
6228
|
selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
|
|
@@ -5134,15 +6231,15 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5134
6231
|
threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
|
|
5135
6232
|
actionableThreads = filterActionableGithubGreptileThreads(threads);
|
|
5136
6233
|
checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
5137
|
-
|
|
5138
|
-
const
|
|
6234
|
+
checkState2 = classifyGithubGreptileCheckState(checkRollup);
|
|
6235
|
+
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
5139
6236
|
if (!shouldContinueGithubGreptileFallbackPolling({
|
|
5140
6237
|
attempt,
|
|
5141
6238
|
pollAttempts: options.pollAttempts,
|
|
5142
|
-
checkState,
|
|
6239
|
+
checkState: checkState2,
|
|
5143
6240
|
fallbackReview,
|
|
5144
6241
|
selectedReview,
|
|
5145
|
-
approvedViaReviewedAncestor
|
|
6242
|
+
approvedViaReviewedAncestor
|
|
5146
6243
|
})) {
|
|
5147
6244
|
break;
|
|
5148
6245
|
}
|
|
@@ -5170,7 +6267,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5170
6267
|
].filter(Boolean).join(`
|
|
5171
6268
|
`);
|
|
5172
6269
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
5173
|
-
if (
|
|
6270
|
+
if (checkState2.pending) {
|
|
5174
6271
|
return {
|
|
5175
6272
|
verdict: "SKIP",
|
|
5176
6273
|
feedback,
|
|
@@ -5181,34 +6278,20 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5181
6278
|
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
5182
6279
|
};
|
|
5183
6280
|
}
|
|
5184
|
-
const
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
};
|
|
5195
|
-
}
|
|
5196
|
-
return {
|
|
5197
|
-
verdict: "SKIP",
|
|
5198
|
-
feedback,
|
|
5199
|
-
reasons: [
|
|
5200
|
-
`[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
|
|
5201
|
-
],
|
|
5202
|
-
warnings,
|
|
5203
|
-
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
5204
|
-
};
|
|
5205
|
-
}
|
|
5206
|
-
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
5207
|
-
if (actionableThreads.length > 0) {
|
|
6281
|
+
const prUrl = options.prState.url || `https://github.com/${repoName}/pull/${prNumber}`;
|
|
6282
|
+
let strictGate;
|
|
6283
|
+
try {
|
|
6284
|
+
const strictEvidence = await collectStrictPrEvidenceForVerifier({
|
|
6285
|
+
projectRoot: options.projectRoot,
|
|
6286
|
+
taskId: options.taskId,
|
|
6287
|
+
prUrl
|
|
6288
|
+
});
|
|
6289
|
+
strictGate = evaluateStrictPrMergeGate(strictEvidence);
|
|
6290
|
+
} catch (error) {
|
|
5208
6291
|
return {
|
|
5209
6292
|
verdict: "REJECT",
|
|
5210
6293
|
feedback,
|
|
5211
|
-
reasons:
|
|
6294
|
+
reasons: [`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`],
|
|
5212
6295
|
warnings,
|
|
5213
6296
|
rawPayload: {
|
|
5214
6297
|
pr: options.prState,
|
|
@@ -5221,44 +6304,31 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5221
6304
|
}
|
|
5222
6305
|
};
|
|
5223
6306
|
}
|
|
5224
|
-
if (!
|
|
5225
|
-
if (approvedViaCompletedCheck) {
|
|
5226
|
-
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.`);
|
|
5227
|
-
return {
|
|
5228
|
-
verdict: "APPROVE",
|
|
5229
|
-
feedback,
|
|
5230
|
-
reasons: [],
|
|
5231
|
-
warnings,
|
|
5232
|
-
rawPayload: {
|
|
5233
|
-
pr: options.prState,
|
|
5234
|
-
selectedReview: fallbackReview,
|
|
5235
|
-
reviews,
|
|
5236
|
-
threads,
|
|
5237
|
-
checkRollup,
|
|
5238
|
-
...buildGithubGreptileFallbackRawPayload(options)
|
|
5239
|
-
}
|
|
5240
|
-
};
|
|
5241
|
-
}
|
|
6307
|
+
if (!strictGate.approved) {
|
|
5242
6308
|
return {
|
|
5243
|
-
verdict: "SKIP",
|
|
6309
|
+
verdict: strictGate.pending ? "SKIP" : "REJECT",
|
|
5244
6310
|
feedback,
|
|
5245
|
-
reasons: [
|
|
5246
|
-
|
|
5247
|
-
],
|
|
5248
|
-
warnings,
|
|
6311
|
+
reasons: strictGate.reasons.map((reason) => reason.startsWith("[AI Review]") ? reason : `[AI Review] ${reason}`),
|
|
6312
|
+
warnings: [...warnings, ...strictGate.warnings],
|
|
5249
6313
|
rawPayload: {
|
|
5250
6314
|
pr: options.prState,
|
|
5251
6315
|
selectedReview: fallbackReview,
|
|
5252
6316
|
reviews,
|
|
5253
6317
|
threads,
|
|
5254
6318
|
checkRollup,
|
|
6319
|
+
actionableThreads,
|
|
6320
|
+
strictGate: {
|
|
6321
|
+
approved: strictGate.approved,
|
|
6322
|
+
pending: strictGate.pending,
|
|
6323
|
+
reasons: strictGate.reasons,
|
|
6324
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6325
|
+
warnings: strictGate.warnings,
|
|
6326
|
+
greptile: strictGate.evidence.greptile
|
|
6327
|
+
},
|
|
5255
6328
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
5256
6329
|
}
|
|
5257
6330
|
};
|
|
5258
6331
|
}
|
|
5259
|
-
if (approvedViaReviewedAncestor) {
|
|
5260
|
-
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.`);
|
|
5261
|
-
}
|
|
5262
6332
|
return {
|
|
5263
6333
|
verdict: "APPROVE",
|
|
5264
6334
|
feedback,
|
|
@@ -5270,6 +6340,14 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
5270
6340
|
reviews,
|
|
5271
6341
|
threads,
|
|
5272
6342
|
checkRollup,
|
|
6343
|
+
strictGate: {
|
|
6344
|
+
approved: strictGate.approved,
|
|
6345
|
+
pending: strictGate.pending,
|
|
6346
|
+
reasons: strictGate.reasons,
|
|
6347
|
+
reasonDetails: strictGate.reasonDetails,
|
|
6348
|
+
warnings: strictGate.warnings,
|
|
6349
|
+
greptile: strictGate.evidence.greptile
|
|
6350
|
+
},
|
|
5273
6351
|
...buildGithubGreptileFallbackRawPayload(options)
|
|
5274
6352
|
}
|
|
5275
6353
|
};
|
|
@@ -5382,19 +6460,25 @@ function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
|
5382
6460
|
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
5383
6461
|
return true;
|
|
5384
6462
|
}
|
|
5385
|
-
return
|
|
6463
|
+
return false;
|
|
5386
6464
|
}
|
|
5387
6465
|
function shouldContinueGreptileMcpPolling(options) {
|
|
5388
6466
|
if (options.githubCheckState.completed) {
|
|
5389
6467
|
return false;
|
|
5390
6468
|
}
|
|
6469
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6470
|
+
return false;
|
|
6471
|
+
}
|
|
5391
6472
|
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
5392
6473
|
return true;
|
|
5393
6474
|
}
|
|
5394
|
-
return
|
|
6475
|
+
return true;
|
|
5395
6476
|
}
|
|
5396
6477
|
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
5397
6478
|
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
6479
|
+
if (options.attempt + 1 >= options.pollAttempts) {
|
|
6480
|
+
return false;
|
|
6481
|
+
}
|
|
5398
6482
|
if (waitingForVisiblePendingReview) {
|
|
5399
6483
|
return true;
|
|
5400
6484
|
}
|
|
@@ -5455,6 +6539,20 @@ function runGhJson(projectRoot, args) {
|
|
|
5455
6539
|
throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
|
|
5456
6540
|
}
|
|
5457
6541
|
}
|
|
6542
|
+
async function collectStrictPrEvidenceForVerifier(input) {
|
|
6543
|
+
return collectPrReviewEvidence({
|
|
6544
|
+
projectRoot: input.projectRoot,
|
|
6545
|
+
prUrl: input.prUrl,
|
|
6546
|
+
taskId: input.taskId,
|
|
6547
|
+
runId: "verifier",
|
|
6548
|
+
cycle: 0,
|
|
6549
|
+
apiSignals: input.apiSignals ?? [],
|
|
6550
|
+
command: async (args, options) => {
|
|
6551
|
+
const result = runCapture(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
6552
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
6553
|
+
}
|
|
6554
|
+
});
|
|
6555
|
+
}
|
|
5458
6556
|
function deriveRepoName(projectRoot, prState) {
|
|
5459
6557
|
const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
|
|
5460
6558
|
if (fromUrl?.[1]) {
|
|
@@ -5469,8 +6567,9 @@ function resolvePrHeadSha(projectRoot, prState) {
|
|
|
5469
6567
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
5470
6568
|
return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
5471
6569
|
}
|
|
5472
|
-
function
|
|
5473
|
-
|
|
6570
|
+
function isGreptileGithubLogin2(login) {
|
|
6571
|
+
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
6572
|
+
return normalized === "greptile" || normalized === "greptile-ai" || normalized === "greptileai" || normalized === "greptile-apps";
|
|
5474
6573
|
}
|
|
5475
6574
|
function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
|
|
5476
6575
|
const matching = sortGithubGreptileReviews(reviews);
|
|
@@ -5487,7 +6586,7 @@ function pickLatestGithubGreptileReview(reviews) {
|
|
|
5487
6586
|
return sortGithubGreptileReviews(reviews)[0] || null;
|
|
5488
6587
|
}
|
|
5489
6588
|
function sortGithubGreptileReviews(reviews) {
|
|
5490
|
-
return reviews.filter((review) =>
|
|
6589
|
+
return reviews.filter((review) => isGreptileGithubLogin2(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
|
|
5491
6590
|
}
|
|
5492
6591
|
function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
5493
6592
|
const response = runGhJson(projectRoot, [
|
|
@@ -5560,32 +6659,6 @@ function classifyGithubGreptileCheckState(checks) {
|
|
|
5560
6659
|
}
|
|
5561
6660
|
return { pending: false, completed: false };
|
|
5562
6661
|
}
|
|
5563
|
-
function isGithubGreptileCheckApproved(checks) {
|
|
5564
|
-
const greptileChecks = checks.filter((check) => {
|
|
5565
|
-
const label = (check.name || check.context || "").toLowerCase();
|
|
5566
|
-
return label.includes("greptile");
|
|
5567
|
-
});
|
|
5568
|
-
if (greptileChecks.length === 0) {
|
|
5569
|
-
return false;
|
|
5570
|
-
}
|
|
5571
|
-
for (const check of greptileChecks) {
|
|
5572
|
-
if ((check.__typename || "") === "CheckRun") {
|
|
5573
|
-
if ((check.status || "").toUpperCase() !== "COMPLETED") {
|
|
5574
|
-
return false;
|
|
5575
|
-
}
|
|
5576
|
-
const conclusion = (check.conclusion || "").toUpperCase();
|
|
5577
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
|
|
5578
|
-
return false;
|
|
5579
|
-
}
|
|
5580
|
-
continue;
|
|
5581
|
-
}
|
|
5582
|
-
const state = (check.state || "").toUpperCase();
|
|
5583
|
-
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
|
|
5584
|
-
return false;
|
|
5585
|
-
}
|
|
5586
|
-
}
|
|
5587
|
-
return true;
|
|
5588
|
-
}
|
|
5589
6662
|
function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
|
|
5590
6663
|
const [owner, name] = repoName.split("/");
|
|
5591
6664
|
if (!owner || !name) {
|
|
@@ -5611,7 +6684,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
5611
6684
|
return [];
|
|
5612
6685
|
}
|
|
5613
6686
|
const comments = thread.comments?.nodes || [];
|
|
5614
|
-
const latestGreptileComment = [...comments].reverse().find((comment) =>
|
|
6687
|
+
const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin2(comment.author?.login));
|
|
5615
6688
|
if (!latestGreptileComment?.path?.trim()) {
|
|
5616
6689
|
return [];
|
|
5617
6690
|
}
|
|
@@ -5633,11 +6706,6 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
5633
6706
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
5634
6707
|
return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
5635
6708
|
}
|
|
5636
|
-
function stripHtml(input) {
|
|
5637
|
-
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
5638
|
-
|
|
5639
|
-
`).trim();
|
|
5640
|
-
}
|
|
5641
6709
|
function summarizeComment(input) {
|
|
5642
6710
|
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
5643
6711
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
@@ -5646,31 +6714,14 @@ function asGreptileInfrastructureWarning(reason) {
|
|
|
5646
6714
|
return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
|
|
5647
6715
|
}
|
|
5648
6716
|
function isAiReviewApproved(input) {
|
|
6717
|
+
if (input.aiVerdict === "REJECT" && input.aiReasons.length > 0) {
|
|
6718
|
+
return false;
|
|
6719
|
+
}
|
|
5649
6720
|
if (input.reviewMode !== "required") {
|
|
5650
6721
|
return true;
|
|
5651
6722
|
}
|
|
5652
6723
|
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
5653
6724
|
}
|
|
5654
|
-
function parseGreptileScore(input) {
|
|
5655
|
-
const text = stripHtml(input);
|
|
5656
|
-
const patterns = [
|
|
5657
|
-
/confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5658
|
-
/\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
5659
|
-
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
|
|
5660
|
-
];
|
|
5661
|
-
for (const pattern of patterns) {
|
|
5662
|
-
const match = pattern.exec(text);
|
|
5663
|
-
if (!match) {
|
|
5664
|
-
continue;
|
|
5665
|
-
}
|
|
5666
|
-
const value = Number.parseInt(match[1] || "", 10);
|
|
5667
|
-
const scale = Number.parseInt(match[2] || "", 10);
|
|
5668
|
-
if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
|
|
5669
|
-
return { value, scale };
|
|
5670
|
-
}
|
|
5671
|
-
}
|
|
5672
|
-
return null;
|
|
5673
|
-
}
|
|
5674
6725
|
|
|
5675
6726
|
// packages/runtime/src/control-plane/provider/runtime-instructions.ts
|
|
5676
6727
|
var CLAUDE_ROUTER_TOOL_NAMES = [
|
|
@@ -5711,9 +6762,9 @@ function taskArtifacts(projectRoot, taskId) {
|
|
|
5711
6762
|
}
|
|
5712
6763
|
const paths = resolveHarnessPaths(projectRoot);
|
|
5713
6764
|
const artifactDir = resolve22(paths.artifactsDir, activeTask);
|
|
5714
|
-
|
|
6765
|
+
mkdirSync11(artifactDir, { recursive: true });
|
|
5715
6766
|
const changed = changedFilesForTask(projectRoot, activeTask, true);
|
|
5716
|
-
|
|
6767
|
+
writeFileSync11(resolve22(artifactDir, "changed-files.txt"), `${changed.join(`
|
|
5717
6768
|
`)}
|
|
5718
6769
|
`, "utf-8");
|
|
5719
6770
|
console.log(`changed-files.txt: ${changed.length} files`);
|
|
@@ -5725,7 +6776,7 @@ function taskArtifacts(projectRoot, taskId) {
|
|
|
5725
6776
|
summary: "TODO: Write a one-line summary of what you did",
|
|
5726
6777
|
completed_at: nowIso()
|
|
5727
6778
|
};
|
|
5728
|
-
|
|
6779
|
+
writeFileSync11(taskResultPath, `${JSON.stringify(template, null, 2)}
|
|
5729
6780
|
`, "utf-8");
|
|
5730
6781
|
console.log("task-result.json: created (update the summary!)");
|
|
5731
6782
|
} else {
|
|
@@ -5737,7 +6788,7 @@ function taskArtifacts(projectRoot, taskId) {
|
|
|
5737
6788
|
|
|
5738
6789
|
Record key decisions here using: rig-agent record decision "..."
|
|
5739
6790
|
`;
|
|
5740
|
-
|
|
6791
|
+
writeFileSync11(decisionLogPath, content, "utf-8");
|
|
5741
6792
|
console.log("decision-log.md: created (record your decisions!)");
|
|
5742
6793
|
} else {
|
|
5743
6794
|
console.log("decision-log.md: already exists");
|
|
@@ -5760,7 +6811,7 @@ Record key decisions here using: rig-agent record decision "..."
|
|
|
5760
6811
|
""
|
|
5761
6812
|
].join(`
|
|
5762
6813
|
`);
|
|
5763
|
-
|
|
6814
|
+
writeFileSync11(nextActionsPath, content, "utf-8");
|
|
5764
6815
|
console.log("next-actions.md: created (add recommendations for downstream tasks!)");
|
|
5765
6816
|
} else {
|
|
5766
6817
|
console.log("next-actions.md: already exists");
|
|
@@ -5993,12 +7044,12 @@ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
|
5993
7044
|
"task-result.json",
|
|
5994
7045
|
"validation-summary.json"
|
|
5995
7046
|
]);
|
|
5996
|
-
function resolveHostRigBinDir(root) {
|
|
5997
|
-
return resolve23(root, ".rig", "bin");
|
|
5998
|
-
}
|
|
5999
7047
|
function isRuntimeGatewayGitPath(candidate) {
|
|
6000
7048
|
return /\/\.rig\/bin\/git$/.test(candidate.replace(/\\/g, "/"));
|
|
6001
7049
|
}
|
|
7050
|
+
function isRuntimeGatewayGhPath(candidate) {
|
|
7051
|
+
return /\/\.rig\/bin\/gh$/.test(candidate.replace(/\\/g, "/"));
|
|
7052
|
+
}
|
|
6002
7053
|
function resolveOptionalMonorepoRoot(projectRoot) {
|
|
6003
7054
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
6004
7055
|
if (runtimeWorkspace && existsSync20(resolve23(runtimeWorkspace, ".git"))) {
|
|
@@ -6033,6 +7084,9 @@ function resolveGitBinary(projectRoot) {
|
|
|
6033
7084
|
}
|
|
6034
7085
|
return "git";
|
|
6035
7086
|
}
|
|
7087
|
+
function escapeRegExp(value) {
|
|
7088
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7089
|
+
}
|
|
6036
7090
|
function safeCurrentTaskId(projectRoot) {
|
|
6037
7091
|
try {
|
|
6038
7092
|
const taskId = currentTaskId(projectRoot);
|
|
@@ -6156,10 +7210,11 @@ function gitOpenPr(options) {
|
|
|
6156
7210
|
"",
|
|
6157
7211
|
"## Task",
|
|
6158
7212
|
`- beads: ${taskId || "n/a"}`,
|
|
7213
|
+
...defaultPrRunLines(taskId, repoNameWithOwner),
|
|
6159
7214
|
"",
|
|
6160
7215
|
"## Review",
|
|
6161
7216
|
"- Completion verification will run validation, verifier review, and PR policy checks.",
|
|
6162
|
-
"- When repository policy allows it, Rig
|
|
7217
|
+
"- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
|
|
6163
7218
|
].join(`
|
|
6164
7219
|
`);
|
|
6165
7220
|
const preCheck = runCapture2(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
|
|
@@ -6247,6 +7302,30 @@ function gitOpenPr(options) {
|
|
|
6247
7302
|
}
|
|
6248
7303
|
return result;
|
|
6249
7304
|
}
|
|
7305
|
+
function defaultPrRunLines(taskId, repoNameWithOwner) {
|
|
7306
|
+
const lines = [];
|
|
7307
|
+
const runId = process.env.RIG_SERVER_RUN_ID?.trim();
|
|
7308
|
+
if (runId) {
|
|
7309
|
+
lines.push(`- Run: ${runId}`);
|
|
7310
|
+
}
|
|
7311
|
+
const closeout = defaultPrCloseoutLine(taskId, repoNameWithOwner);
|
|
7312
|
+
if (closeout) {
|
|
7313
|
+
lines.push(`- ${closeout}`);
|
|
7314
|
+
}
|
|
7315
|
+
return lines;
|
|
7316
|
+
}
|
|
7317
|
+
function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
|
|
7318
|
+
const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
|
|
7319
|
+
if (sourceIssueId) {
|
|
7320
|
+
const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
|
|
7321
|
+
if (match?.[1] && match[2]) {
|
|
7322
|
+
const sourceRepo = match[1];
|
|
7323
|
+
const issueNumber = match[2];
|
|
7324
|
+
return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
return /^\d+$/.test(taskId) ? `Closes #${taskId}` : "";
|
|
7328
|
+
}
|
|
6250
7329
|
function resolveTaskBranchRef(projectRoot, taskId) {
|
|
6251
7330
|
return `rig/${resolveTaskBranchId(projectRoot, taskId)}`;
|
|
6252
7331
|
}
|
|
@@ -6331,61 +7410,45 @@ function gitMergePr(options) {
|
|
|
6331
7410
|
return { status: "already-merged", url: options.pr.url };
|
|
6332
7411
|
}
|
|
6333
7412
|
if (state !== "OPEN") {
|
|
6334
|
-
throw new Error(`Cannot
|
|
7413
|
+
throw new Error(`Cannot merge PR ${options.pr.url}: state is ${state}.`);
|
|
6335
7414
|
}
|
|
6336
7415
|
if (isDraft) {
|
|
6337
|
-
throw new Error(`Cannot
|
|
7416
|
+
throw new Error(`Cannot merge draft PR ${options.pr.url}.`);
|
|
6338
7417
|
}
|
|
7418
|
+
const strictGateHeadSha = strictMergeHeadShaFromGate(options.strictGate, options.pr.url);
|
|
6339
7419
|
const mergeArgs = withGhRepo([gh, "pr", "merge", options.pr.url], repoNameWithOwner);
|
|
6340
7420
|
const method = options.method || "squash";
|
|
6341
7421
|
mergeArgs.push(method === "merge" ? "--merge" : method === "rebase" ? "--rebase" : "--squash");
|
|
7422
|
+
mergeArgs.push("--match-head-commit", strictGateHeadSha);
|
|
6342
7423
|
if (options.deleteBranch !== false) {
|
|
6343
7424
|
mergeArgs.push("--delete-branch");
|
|
6344
7425
|
}
|
|
6345
|
-
const
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
}
|
|
6359
|
-
adminMergeArgs.push("--admin");
|
|
6360
|
-
const adminMerge = runCapture2(adminMergeArgs, repoRoot);
|
|
6361
|
-
if (adminMerge.exitCode === 0) {
|
|
6362
|
-
const postAdminMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
|
|
6363
|
-
if (postAdminMergeState.state === "MERGED" || postAdminMergeState.mergedAt) {
|
|
6364
|
-
console.log(`Merged PR (${options.pr.repoLabel}) with admin fallback: ${options.pr.url}`);
|
|
6365
|
-
return { status: "merged", url: options.pr.url };
|
|
6366
|
-
}
|
|
6367
|
-
throw new Error(`Admin merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub still reports it open.`);
|
|
6368
|
-
}
|
|
6369
|
-
const adminMergeMessage = `${adminMerge.stderr}
|
|
6370
|
-
${adminMerge.stdout}`.trim();
|
|
6371
|
-
if (!/admin|administrator|permission|not permitted|not allowed/i.test(adminMergeMessage)) {
|
|
6372
|
-
throw new Error(`Failed to admin-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${adminMergeMessage}`);
|
|
6373
|
-
}
|
|
7426
|
+
const directMerge = runCapture2(mergeArgs, repoRoot);
|
|
7427
|
+
if (directMerge.exitCode === 0) {
|
|
7428
|
+
console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
|
|
7429
|
+
return { status: "merged", url: options.pr.url };
|
|
7430
|
+
}
|
|
7431
|
+
const postDirectState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
|
|
7432
|
+
if (canAdminMergeApprovedPr(postDirectState)) {
|
|
7433
|
+
const adminMergeArgs = [...mergeArgs, "--admin"];
|
|
7434
|
+
const adminMerge = runCapture2(adminMergeArgs, repoRoot);
|
|
7435
|
+
if (adminMerge.exitCode === 0) {
|
|
7436
|
+
const postAdminMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
|
|
7437
|
+
if (postAdminMergeState.state === "MERGED" || postAdminMergeState.mergedAt) {
|
|
7438
|
+
console.log(`Merged PR (${options.pr.repoLabel}) with admin fallback: ${options.pr.url}`);
|
|
7439
|
+
return { status: "merged", url: options.pr.url };
|
|
6374
7440
|
}
|
|
6375
|
-
|
|
6376
|
-
|
|
7441
|
+
throw new Error(`Admin merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub still reports it open.`);
|
|
7442
|
+
}
|
|
7443
|
+
const adminMergeMessage = `${adminMerge.stderr}
|
|
7444
|
+
${adminMerge.stdout}`.trim();
|
|
7445
|
+
if (!/admin|administrator|permission|not permitted|not allowed/i.test(adminMergeMessage)) {
|
|
7446
|
+
throw new Error(`Failed to admin-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${adminMergeMessage}`);
|
|
6377
7447
|
}
|
|
6378
|
-
throw new Error(`Auto-merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub did not report a merged or auto-merge-enabled state.`);
|
|
6379
|
-
}
|
|
6380
|
-
const autoMergeMessage = `${autoMerge.stderr}
|
|
6381
|
-
${autoMerge.stdout}`.trim();
|
|
6382
|
-
const autoMergeUnsupported = /auto.?merge.*(not enabled|not allowed|disabled|unsupported)|enablePullRequestAutoMerge|Auto merge is not allowed/i.test(autoMergeMessage);
|
|
6383
|
-
if (!autoMergeUnsupported) {
|
|
6384
|
-
throw new Error(`Failed to auto-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${autoMergeMessage}`);
|
|
6385
7448
|
}
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
7449
|
+
const directMergeMessage = `${directMerge.stderr}
|
|
7450
|
+
${directMerge.stdout}`.trim();
|
|
7451
|
+
throw new Error(`Failed to merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${directMergeMessage}`);
|
|
6389
7452
|
}
|
|
6390
7453
|
function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
|
|
6391
7454
|
const mergeable = prState.mergeable.toUpperCase();
|
|
@@ -6396,12 +7459,12 @@ function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
|
|
|
6396
7459
|
}
|
|
6397
7460
|
function writePrMetadata(projectRoot, taskId, result) {
|
|
6398
7461
|
const dir = artifactDirForId(projectRoot, taskId);
|
|
6399
|
-
|
|
7462
|
+
mkdirSync12(dir, { recursive: true });
|
|
6400
7463
|
const path = resolve23(dir, "pr-state.json");
|
|
6401
7464
|
let prs = {};
|
|
6402
7465
|
if (existsSync20(path)) {
|
|
6403
7466
|
try {
|
|
6404
|
-
const parsed = JSON.parse(
|
|
7467
|
+
const parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
6405
7468
|
if (parsed && typeof parsed === "object" && parsed.prs && typeof parsed.prs === "object") {
|
|
6406
7469
|
prs = parsed.prs;
|
|
6407
7470
|
}
|
|
@@ -6417,7 +7480,7 @@ function writePrMetadata(projectRoot, taskId, result) {
|
|
|
6417
7480
|
...primary || {},
|
|
6418
7481
|
updated_at: nowIso()
|
|
6419
7482
|
};
|
|
6420
|
-
|
|
7483
|
+
writeFileSync12(path, `${JSON.stringify(artifact, null, 2)}
|
|
6421
7484
|
`, "utf-8");
|
|
6422
7485
|
}
|
|
6423
7486
|
function readPrMetadata(projectRoot, taskId) {
|
|
@@ -6426,7 +7489,7 @@ function readPrMetadata(projectRoot, taskId) {
|
|
|
6426
7489
|
return [];
|
|
6427
7490
|
}
|
|
6428
7491
|
try {
|
|
6429
|
-
const parsed = JSON.parse(
|
|
7492
|
+
const parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
6430
7493
|
if (!parsed || typeof parsed !== "object") {
|
|
6431
7494
|
return [];
|
|
6432
7495
|
}
|
|
@@ -6493,32 +7556,19 @@ function resolveGithubCliBinary(projectRoot) {
|
|
|
6493
7556
|
if (explicit) {
|
|
6494
7557
|
candidates.add(explicit);
|
|
6495
7558
|
}
|
|
7559
|
+
for (const candidate of ["/usr/bin/gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh"]) {
|
|
7560
|
+
candidates.add(candidate);
|
|
7561
|
+
}
|
|
6496
7562
|
const explicitPathEntries = (process.env.PATH || "").split(":").map((entry) => entry.trim()).filter(Boolean);
|
|
6497
7563
|
for (const entry of explicitPathEntries) {
|
|
6498
7564
|
candidates.add(resolve23(entry, "gh"));
|
|
6499
7565
|
}
|
|
6500
|
-
const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim();
|
|
6501
|
-
if (hostProjectRoot) {
|
|
6502
|
-
candidates.add(resolve23(resolveHostRigBinDir(hostProjectRoot), "gh"));
|
|
6503
|
-
}
|
|
6504
|
-
candidates.add(resolve23(resolveHostRigBinDir(projectRoot), "gh"));
|
|
6505
|
-
const runtimeContext = loadRuntimeContextFromEnv();
|
|
6506
|
-
if (runtimeContext?.binDir) {
|
|
6507
|
-
candidates.add(resolve23(runtimeContext.binDir, "gh"));
|
|
6508
|
-
}
|
|
6509
|
-
const runtimeHome = process.env.RIG_RUNTIME_HOME?.trim();
|
|
6510
|
-
if (runtimeHome) {
|
|
6511
|
-
candidates.add(resolve23(runtimeHome, "bin", "gh"));
|
|
6512
|
-
}
|
|
6513
|
-
for (const candidate of ["/opt/homebrew/bin/gh", "/usr/local/bin/gh", "/usr/bin/gh"]) {
|
|
6514
|
-
candidates.add(candidate);
|
|
6515
|
-
}
|
|
6516
7566
|
const bunResolved = Bun.which("gh");
|
|
6517
7567
|
if (bunResolved) {
|
|
6518
7568
|
candidates.add(bunResolved);
|
|
6519
7569
|
}
|
|
6520
7570
|
for (const candidate of candidates) {
|
|
6521
|
-
if (candidate && existsSync20(candidate)) {
|
|
7571
|
+
if (candidate && existsSync20(candidate) && !isRuntimeGatewayGhPath(candidate)) {
|
|
6522
7572
|
return candidate;
|
|
6523
7573
|
}
|
|
6524
7574
|
}
|
|
@@ -6803,14 +7853,14 @@ function readChangedFilesManifest(projectRoot, taskId) {
|
|
|
6803
7853
|
if (!existsSync20(manifestPath)) {
|
|
6804
7854
|
return [];
|
|
6805
7855
|
}
|
|
6806
|
-
const files =
|
|
7856
|
+
const files = readFileSync12(manifestPath, "utf-8").split(/\r?\n/).map((line) => normalizeChangedFilePath2(line)).filter(Boolean);
|
|
6807
7857
|
return [...new Set(files)];
|
|
6808
7858
|
}
|
|
6809
7859
|
function refreshChangedFilesManifest(projectRoot, taskId) {
|
|
6810
7860
|
const manifestPath = resolve23(artifactDirForId(projectRoot, taskId), "changed-files.txt");
|
|
6811
|
-
|
|
7861
|
+
mkdirSync12(dirname10(manifestPath), { recursive: true });
|
|
6812
7862
|
const changedFiles = changedFilesForTask(projectRoot, taskId, true);
|
|
6813
|
-
|
|
7863
|
+
writeFileSync12(manifestPath, `${changedFiles.join(`
|
|
6814
7864
|
`)}
|
|
6815
7865
|
`, "utf-8");
|
|
6816
7866
|
return manifestPath;
|
|
@@ -7104,7 +8154,7 @@ function loadPersistedRuntimeSecrets(runtimeRoot) {
|
|
|
7104
8154
|
return {};
|
|
7105
8155
|
}
|
|
7106
8156
|
try {
|
|
7107
|
-
const parsed = JSON.parse(
|
|
8157
|
+
const parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
7108
8158
|
const entries = Object.entries(parsed).filter((entry) => typeof entry[1] === "string");
|
|
7109
8159
|
return Object.fromEntries(entries);
|
|
7110
8160
|
} catch {
|
|
@@ -7115,10 +8165,10 @@ function ensureRuntimeOpenSslConfig(runtimeHome) {
|
|
|
7115
8165
|
const sslDir = resolve23(runtimeHome, ".ssl");
|
|
7116
8166
|
const sslConfig = resolve23(sslDir, "openssl.cnf");
|
|
7117
8167
|
if (!existsSync20(sslDir)) {
|
|
7118
|
-
|
|
8168
|
+
mkdirSync12(sslDir, { recursive: true });
|
|
7119
8169
|
}
|
|
7120
8170
|
if (!existsSync20(sslConfig)) {
|
|
7121
|
-
|
|
8171
|
+
writeFileSync12(sslConfig, `# Rig runtime OpenSSL config placeholder
|
|
7122
8172
|
`);
|
|
7123
8173
|
}
|
|
7124
8174
|
return sslConfig;
|
|
@@ -7136,7 +8186,7 @@ function resolveRuntimeMetadata(projectRoot) {
|
|
|
7136
8186
|
if (contextFile) {
|
|
7137
8187
|
return {
|
|
7138
8188
|
ctx,
|
|
7139
|
-
runtimeRoot:
|
|
8189
|
+
runtimeRoot: dirname10(resolve23(contextFile))
|
|
7140
8190
|
};
|
|
7141
8191
|
}
|
|
7142
8192
|
const inferredContextFile = findRuntimeContextFile2(projectRoot);
|
|
@@ -7146,7 +8196,7 @@ function resolveRuntimeMetadata(projectRoot) {
|
|
|
7146
8196
|
} catch {}
|
|
7147
8197
|
return {
|
|
7148
8198
|
ctx,
|
|
7149
|
-
runtimeRoot:
|
|
8199
|
+
runtimeRoot: dirname10(inferredContextFile)
|
|
7150
8200
|
};
|
|
7151
8201
|
}
|
|
7152
8202
|
return { ctx, runtimeRoot: "" };
|
|
@@ -7158,7 +8208,7 @@ function findRuntimeContextFile2(startPath) {
|
|
|
7158
8208
|
if (existsSync20(candidate)) {
|
|
7159
8209
|
return candidate;
|
|
7160
8210
|
}
|
|
7161
|
-
const parent =
|
|
8211
|
+
const parent = dirname10(current);
|
|
7162
8212
|
if (parent === current) {
|
|
7163
8213
|
return "";
|
|
7164
8214
|
}
|
|
@@ -7207,6 +8257,7 @@ async function main() {
|
|
|
7207
8257
|
}
|
|
7208
8258
|
const paths = resolveHarnessPaths(projectRoot);
|
|
7209
8259
|
let failed = false;
|
|
8260
|
+
let sourceCloseoutAllowed = false;
|
|
7210
8261
|
console.log(`=== Completion Verification: ${taskId} ===`);
|
|
7211
8262
|
const scopes = await resolveTaskScopes(projectRoot, taskId);
|
|
7212
8263
|
const taskChangedFiles = changedFilesForTask(projectRoot, taskId, true);
|
|
@@ -7245,21 +8296,12 @@ async function main() {
|
|
|
7245
8296
|
const policy = loadPolicy(projectRoot);
|
|
7246
8297
|
const openPrEnabled = policy.completion.checks.includes("open-pr");
|
|
7247
8298
|
const autoMergeEnabled = policy.completion.checks.includes("auto-merge");
|
|
7248
|
-
const eventBus = new RuntimeEventBus({ projectRoot });
|
|
7249
|
-
const runtimeContext = loadRuntimeContextFromEnv() ?? undefined;
|
|
7250
|
-
const plugins = await PluginManager.load({
|
|
7251
|
-
projectRoot,
|
|
7252
|
-
runId: eventBus.getRunId(),
|
|
7253
|
-
eventBus,
|
|
7254
|
-
runtimeContext
|
|
7255
|
-
});
|
|
7256
8299
|
console.log(`
|
|
7257
8300
|
[2/3] Verifier preflight...`);
|
|
7258
8301
|
if (!failed) {
|
|
7259
8302
|
const localVerifyOutcome = await verifyTask({
|
|
7260
8303
|
projectRoot,
|
|
7261
8304
|
taskId,
|
|
7262
|
-
plugins,
|
|
7263
8305
|
skipAiReview: true,
|
|
7264
8306
|
persistArtifacts: false
|
|
7265
8307
|
});
|
|
@@ -7330,8 +8372,7 @@ async function main() {
|
|
|
7330
8372
|
[3/3] Verifier review...`);
|
|
7331
8373
|
const verifyOutcome = await verifyTask({
|
|
7332
8374
|
projectRoot,
|
|
7333
|
-
taskId
|
|
7334
|
-
plugins
|
|
8375
|
+
taskId
|
|
7335
8376
|
});
|
|
7336
8377
|
if (!verifyOutcome.approved) {
|
|
7337
8378
|
console.log("REJECT:");
|
|
@@ -7355,22 +8396,42 @@ async function main() {
|
|
|
7355
8396
|
if (prs.length === 0) {
|
|
7356
8397
|
console.log("Auto-merge: skipped (no PR metadata found)");
|
|
7357
8398
|
} else {
|
|
7358
|
-
let
|
|
8399
|
+
let cycle = 0;
|
|
7359
8400
|
for (const pr of prs) {
|
|
8401
|
+
cycle += 1;
|
|
8402
|
+
const gate = await runStrictPrMergeGate({
|
|
8403
|
+
projectRoot,
|
|
8404
|
+
prUrl: pr.url,
|
|
8405
|
+
taskId,
|
|
8406
|
+
runId: "completion-verification",
|
|
8407
|
+
cycle,
|
|
8408
|
+
final: true,
|
|
8409
|
+
command: async (args, options) => {
|
|
8410
|
+
const result = runCapture(["gh", ...args], options?.cwd ?? projectRoot);
|
|
8411
|
+
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
8412
|
+
}
|
|
8413
|
+
});
|
|
8414
|
+
if (!gate.approved) {
|
|
8415
|
+
console.log(`FAIL: Strict merge gate blocked PR (${pr.repoLabel}): ${pr.url}`);
|
|
8416
|
+
for (const reason of gate.reasons) {
|
|
8417
|
+
console.log(`- ${reason}`);
|
|
8418
|
+
}
|
|
8419
|
+
failed = true;
|
|
8420
|
+
continue;
|
|
8421
|
+
}
|
|
7360
8422
|
const mergeResult = gitMergePr({
|
|
7361
8423
|
projectRoot,
|
|
7362
8424
|
pr,
|
|
7363
8425
|
method: "squash",
|
|
7364
|
-
deleteBranch: true
|
|
8426
|
+
deleteBranch: true,
|
|
8427
|
+
strictGate: gate
|
|
7365
8428
|
});
|
|
7366
|
-
if (mergeResult.status === "
|
|
7367
|
-
|
|
7368
|
-
console.log(`WAIT: Auto-merge enabled but PR is still open (${pr.repoLabel}): ${pr.url}`);
|
|
8429
|
+
if (mergeResult.status === "merged" || mergeResult.status === "already-merged") {
|
|
8430
|
+
console.log(`OK: PR merge confirmed (${pr.repoLabel}): ${pr.url}`);
|
|
7369
8431
|
}
|
|
7370
8432
|
}
|
|
7371
|
-
if (
|
|
7372
|
-
|
|
7373
|
-
} else {
|
|
8433
|
+
if (!failed) {
|
|
8434
|
+
sourceCloseoutAllowed = true;
|
|
7374
8435
|
console.log("OK: Auto-merge complete");
|
|
7375
8436
|
}
|
|
7376
8437
|
}
|
|
@@ -7384,18 +8445,22 @@ async function main() {
|
|
|
7384
8445
|
[post] Auto-merge: skipped (not in policy completion.checks)`);
|
|
7385
8446
|
}
|
|
7386
8447
|
const artifactDir = resolve24(paths.artifactsDir, taskId);
|
|
7387
|
-
|
|
7388
|
-
|
|
8448
|
+
mkdirSync13(artifactDir, { recursive: true });
|
|
8449
|
+
writeFileSync13(resolve24(artifactDir, "review-status.txt"), failed ? `REJECTED
|
|
7389
8450
|
` : `APPROVED
|
|
7390
8451
|
`, "utf-8");
|
|
7391
8452
|
if (!failed) {
|
|
7392
8453
|
await recordTaskRepoCommits(projectRoot, taskId, paths);
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
8454
|
+
if (sourceCloseoutAllowed) {
|
|
8455
|
+
const closeout = await closeCompletedTaskSource(projectRoot, taskId);
|
|
8456
|
+
if (!closeout.ok) {
|
|
8457
|
+
console.log(`FAIL: ${closeout.message}`);
|
|
8458
|
+
failed = true;
|
|
8459
|
+
} else {
|
|
8460
|
+
console.log(`OK: ${closeout.message}`);
|
|
8461
|
+
}
|
|
7397
8462
|
} else {
|
|
7398
|
-
console.log(
|
|
8463
|
+
console.log("Task source closeout skipped until an approved PR merge completes.");
|
|
7399
8464
|
}
|
|
7400
8465
|
}
|
|
7401
8466
|
if (!failed) {
|
|
@@ -7482,7 +8547,7 @@ async function runProtoQualityGate(monorepoRoot) {
|
|
|
7482
8547
|
console.log(`FAIL: Missing workflow gate file at ${workflowPath}`);
|
|
7483
8548
|
ok = false;
|
|
7484
8549
|
} else {
|
|
7485
|
-
const workflow =
|
|
8550
|
+
const workflow = readFileSync13(workflowPath, "utf-8");
|
|
7486
8551
|
if (workflow.includes("if: false && needs.detect.outputs.protos_changed == 'true'")) {
|
|
7487
8552
|
console.log("FAIL: Proto quality CI gate is disabled in pull-request-gate.yml");
|
|
7488
8553
|
ok = false;
|
|
@@ -7535,11 +8600,11 @@ async function recordVerifierFailure(projectRoot, taskId, paths) {
|
|
|
7535
8600
|
}
|
|
7536
8601
|
let attempts = 1;
|
|
7537
8602
|
if (existsSync21(failedApproachesPath)) {
|
|
7538
|
-
const content =
|
|
8603
|
+
const content = readFileSync13(failedApproachesPath, "utf-8");
|
|
7539
8604
|
attempts = (content.match(new RegExp(`^## ${escapeRegExp2(taskId)}\\b`, "gm")) || []).length + 1;
|
|
7540
8605
|
} else {
|
|
7541
|
-
|
|
7542
|
-
|
|
8606
|
+
mkdirSync13(resolve24(failedApproachesPath, ".."), { recursive: true });
|
|
8607
|
+
writeFileSync13(failedApproachesPath, `# Failed Approaches
|
|
7543
8608
|
|
|
7544
8609
|
`, "utf-8");
|
|
7545
8610
|
}
|
|
@@ -7577,8 +8642,8 @@ async function recordTaskRepoCommits(projectRoot, taskId, paths) {
|
|
|
7577
8642
|
recorded_at: new Date().toISOString(),
|
|
7578
8643
|
repos
|
|
7579
8644
|
};
|
|
7580
|
-
|
|
7581
|
-
|
|
8645
|
+
mkdirSync13(resolve24(statePath, ".."), { recursive: true });
|
|
8646
|
+
writeFileSync13(statePath, `${JSON.stringify(state, null, 2)}
|
|
7582
8647
|
`, "utf-8");
|
|
7583
8648
|
}
|
|
7584
8649
|
}
|