@h-rig/runtime 0.0.6-alpha.3 → 0.0.6-alpha.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/bin/rig-agent-dispatch.js +1165 -785
  2. package/dist/bin/rig-agent.js +458 -389
  3. package/dist/src/control-plane/agent-wrapper.js +1191 -504
  4. package/dist/src/control-plane/authority-files.js +12 -6
  5. package/dist/src/control-plane/harness-main.js +2186 -1786
  6. package/dist/src/control-plane/hooks/completion-verification.js +2084 -1019
  7. package/dist/src/control-plane/hooks/inject-context.js +193 -139
  8. package/dist/src/control-plane/hooks/submodule-branch.js +603 -545
  9. package/dist/src/control-plane/hooks/task-runtime-start.js +603 -545
  10. package/dist/src/control-plane/materialize-task-config.js +64 -8
  11. package/dist/src/control-plane/native/git-ops.js +90 -64
  12. package/dist/src/control-plane/native/harness-cli.js +1989 -682
  13. package/dist/src/control-plane/native/pr-automation.js +1657 -54
  14. package/dist/src/control-plane/native/pr-review-gate.js +1455 -0
  15. package/dist/src/control-plane/native/repo-ops.js +3 -0
  16. package/dist/src/control-plane/native/run-ops.js +39 -13
  17. package/dist/src/control-plane/native/task-ops.js +1819 -527
  18. package/dist/src/control-plane/native/validator.js +163 -109
  19. package/dist/src/control-plane/native/verifier.js +1616 -323
  20. package/dist/src/control-plane/native/workspace-ops.js +12 -6
  21. package/dist/src/control-plane/pi-sessiond/bin.js +793 -0
  22. package/dist/src/control-plane/pi-sessiond/client.js +41 -0
  23. package/dist/src/control-plane/pi-sessiond/event-hub.js +59 -0
  24. package/dist/src/control-plane/pi-sessiond/extension-ui-context.js +198 -0
  25. package/dist/src/control-plane/pi-sessiond/launcher.js +173 -0
  26. package/dist/src/control-plane/pi-sessiond/server.js +802 -0
  27. package/dist/src/control-plane/pi-sessiond/session-service.js +540 -0
  28. package/dist/src/control-plane/pi-sessiond/types.js +1 -0
  29. package/dist/src/control-plane/plugin-host-context.js +54 -0
  30. package/dist/src/control-plane/runtime/image/fingerprint-sidecar.js +3 -0
  31. package/dist/src/control-plane/runtime/image/index.js +3 -0
  32. package/dist/src/control-plane/runtime/image-fingerprint-sidecar.js +3 -0
  33. package/dist/src/control-plane/runtime/image.js +3 -0
  34. package/dist/src/control-plane/runtime/index.js +517 -722
  35. package/dist/src/control-plane/runtime/isolation/home.js +28 -6
  36. package/dist/src/control-plane/runtime/isolation/index.js +541 -461
  37. package/dist/src/control-plane/runtime/isolation/runner.js +28 -6
  38. package/dist/src/control-plane/runtime/isolation/shared.js +9 -6
  39. package/dist/src/control-plane/runtime/isolation.js +541 -461
  40. package/dist/src/control-plane/runtime/plugin-mode.js +3 -27
  41. package/dist/src/control-plane/runtime/queue.js +458 -385
  42. package/dist/src/control-plane/runtime/snapshot/task-run.js +3 -0
  43. package/dist/src/control-plane/runtime/task-run-snapshot.js +3 -0
  44. package/dist/src/control-plane/skill-materializer.js +46 -0
  45. package/dist/src/control-plane/tasks/source-aware-task-config-source.js +14 -2
  46. package/dist/src/control-plane/tasks/source-lifecycle.js +86 -32
  47. package/dist/src/index.js +27 -298
  48. package/dist/src/layout.js +12 -7
  49. package/dist/src/local-server.js +20 -14
  50. package/native/darwin-arm64/rig-git +0 -0
  51. package/native/darwin-arm64/rig-git.build-manifest.json +1 -1
  52. package/native/darwin-arm64/rig-shell +0 -0
  53. package/native/darwin-arm64/rig-shell.build-manifest.json +1 -1
  54. package/native/darwin-arm64/rig-tools +0 -0
  55. package/native/darwin-arm64/rig-tools.build-manifest.json +1 -1
  56. package/native/darwin-arm64/runtime-native.dylib +0 -0
  57. package/package.json +8 -6
  58. package/dist/src/control-plane/runtime/plugins.js +0 -1131
  59. 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 mkdirSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
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/events.ts
17
- import { appendFile, mkdir } from "fs/promises";
18
- import { randomUUID } from "crypto";
19
- import { dirname as dirname2, resolve as resolve2 } from "path";
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 dirname3, resolve as resolve3 } from "path";
248
- var sharedNativeRuntimeOutputDir = resolve3(tmpdir(), "rig-native");
249
- var sharedNativeRuntimeOutputPath = resolve3(sharedNativeRuntimeOutputDir, `runtime-native-${process.platform}-${process.arch}.${suffix}`);
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 = resolve3(fromDir);
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(resolve3(cursor, "native", `${process.platform}-${process.arch}`, name), resolve3(cursor, "native", `${process.platform}-${process.arch}`, "lib", name), resolve3(cursor, "native", name), resolve3(cursor, "native", "lib", name));
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 = dirname3(cursor);
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() ? dirname3(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 ? resolve3(execDir, colocatedNativeRuntimeFileName) : "",
305
- execDir ? resolve3(execDir, platformSpecific) : "",
306
- execDir ? resolve3(execDir, "..", colocatedNativeRuntimeFileName) : "",
307
- execDir ? resolve3(execDir, "..", platformSpecific) : "",
308
- execDir ? resolve3(execDir, "lib", colocatedNativeRuntimeFileName) : "",
309
- execDir ? resolve3(execDir, "..", "lib", colocatedNativeRuntimeFileName) : "",
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 = resolve3(import.meta.dir, "../../../native/snapshot.zig");
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(dirname3(outputPath), { recursive: true });
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 = resolve4(projectRoot, "rig");
510
- const stateRoot = resolve4(projectRoot, ".rig");
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 ?? resolve4(stateRoot, "state");
513
- const logsDir = layout?.logsDir ?? resolve4(stateRoot, "logs");
514
- const artifactsDir = layout?.artifactsRoot ?? resolve4(monorepoRoot, "artifacts");
515
- const taskConfigPath = layout?.taskConfigPath ?? resolve4(monorepoRoot, ".rig", "task-config.json");
516
- const binDir = layout?.binDir ?? resolve4(stateRoot, "bin");
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: resolve4(harnessRoot, "hooks"),
524
- validationDir: resolve4(harnessRoot, "validation"),
415
+ hooksDir: resolve3(harnessRoot, "hooks"),
416
+ validationDir: resolve3(harnessRoot, "validation"),
525
417
  taskConfigPath,
526
- sessionPath: process.env.RIG_SESSION_FILE || resolve4(stateRoot, "session", "session.json"),
418
+ sessionPath: process.env.RIG_SESSION_FILE || resolve3(stateRoot, "session", "session.json"),
527
419
  monorepoRoot,
528
- tsApiTestsDir: process.env.TS_API_TESTS_DIR || resolve4(monorepoRoot, "TSAPITests"),
529
- taskRepoCommitsPath: resolve4(stateDir, "task-repo-commits.json"),
530
- baseRepoPinsPath: resolve4(stateDir, "base-repo-pins.json"),
531
- failedApproachesPath: resolve4(stateDir, "failed_approaches.md"),
532
- agentProfilePath: resolve4(stateDir, "agent-profile.json"),
533
- reviewProfilePath: resolve4(stateDir, "review-profile.json")
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 = resolve5(projectRoot, "rig/policy/policy.json");
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 resolve5(rawPath);
893
- return resolve5(projectRoot, rawPath);
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
- resolve5(projectRoot, "rig"),
899
- resolve5(projectRoot, ".rig"),
900
- resolve5(projectRoot, "artifacts")
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 = resolve5(taskWorkspace, ".rig");
908
- const workspaceArtifactsRoot = resolve5(taskWorkspace, "artifacts");
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 = resolve5(projectRoot, ".rig/runtime/agents");
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 = resolve5(filePath);
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/runtime/plugin-mode.ts
1190
- var LEGACY_PLUGIN_SCAN_ENV = "RIG_LEGACY_PLUGIN_SCAN";
1191
- function isLegacyPluginScanEnabled(env = process.env) {
1192
- const value = env[LEGACY_PLUGIN_SCAN_ENV]?.trim().toLowerCase();
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/plugins.ts
1197
- class PluginManager {
1198
- eventBus;
1199
- context;
1200
- pluginDir;
1201
- pluginFiles;
1202
- pluginNames;
1203
- localBinDir;
1204
- pluginsRequireBinaries;
1205
- plugins;
1206
- loadPromise;
1207
- constructor(options) {
1208
- this.eventBus = options.eventBus;
1209
- this.context = options.context;
1210
- this.pluginDir = options.pluginDir;
1211
- this.pluginFiles = options.pluginFiles;
1212
- this.pluginNames = options.pluginNames;
1213
- this.localBinDir = options.localBinDir;
1214
- this.pluginsRequireBinaries = options.pluginsRequireBinaries;
1215
- this.plugins = options.preloadedPlugins ?? null;
1216
- this.loadPromise = null;
1217
- }
1218
- static disabled(options) {
1219
- const validatorProjectRoot = options.runtimeContext?.workspaceDir || options.projectRoot;
1220
- return new PluginManager({
1221
- eventBus: options.eventBus,
1222
- context: {
1223
- projectRoot: validatorProjectRoot,
1224
- runId: options.runId,
1225
- eventBus: options.eventBus
1226
- },
1227
- pluginDir: resolve6(options.projectRoot, "rig/plugins"),
1228
- pluginFiles: [],
1229
- pluginNames: [],
1230
- localBinDir: options.runtimeContext ? resolve6(options.runtimeContext.binDir, "plugins") : resolve6(options.projectRoot, "rig/plugins"),
1231
- pluginsRequireBinaries: false,
1232
- preloadedPlugins: []
1233
- });
1234
- }
1235
- static async load(options) {
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
- async onEvent(event) {
1289
- const plugins = this.plugins;
1290
- if (!plugins) {
1291
- return;
1292
- }
1293
- for (const plugin of plugins) {
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
- async safeInvoke(pluginName, hook, call) {
1342
- try {
1343
- await call();
1344
- } catch (error) {
1345
- await this.eventBus.emit("plugin.error", {
1346
- plugin: pluginName,
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
- async ensureLoaded() {
1353
- if (this.plugins) {
1354
- return this.plugins;
1136
+ const exportMatch = line.match(/^(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)$/);
1137
+ if (!exportMatch) {
1138
+ continue;
1355
1139
  }
1356
- if (this.loadPromise) {
1357
- return this.loadPromise;
1140
+ const key = exportMatch[1];
1141
+ if (!(key in BAKED_RUNTIME_SECRETS)) {
1142
+ continue;
1358
1143
  }
1359
- this.loadPromise = this.loadCompiledPlugins();
1360
- try {
1361
- this.plugins = await this.loadPromise;
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 createBinaryPluginWrapper(name, binPath, projectRoot) {
1410
- return {
1411
- name,
1412
- validators: [
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 readFileSync3, writeFileSync } from "fs";
1472
- import { dirname as dirname4, resolve as resolve7 } from "path";
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 = resolve7(path);
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(readFileSync3(absPath, "utf8"));
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 = resolve7(startPath);
1320
+ let current = resolve6(startPath);
1626
1321
  while (true) {
1627
- const candidate = resolve7(current, "runtime-context.json");
1322
+ const candidate = resolve6(current, "runtime-context.json");
1628
1323
  if (existsSync6(candidate) && isAgentRuntimeContextPath(candidate)) {
1629
1324
  return candidate;
1630
1325
  }
1631
- const parent = dirname4(current);
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 mkdirSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync9 } from "fs";
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 basename3, dirname as dirname5, resolve as resolve9 } from "path";
1762
- var sharedNativeShellOutputDir = resolve9(tmpdir2(), "rig-native");
1763
- var sharedNativeShellOutputPath = resolve9(sharedNativeShellOutputDir, `rig-shell-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
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 basename4, dirname as dirname6, resolve as resolve10 } from "path";
1767
- var sharedNativeToolsOutputDir = resolve10(tmpdir3(), "rig-native");
1768
- var sharedNativeToolsOutputPath = resolve10(sharedNativeToolsOutputDir, `rig-tools-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
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 existsSync8 } from "fs";
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 (!existsSync8(packageJsonPath)) {
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 existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1977
- import { dirname as dirname7, resolve as resolve11 } from "path";
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 = resolve11(projectRoot, ".claude", "settings.json");
1992
- const existing = existsSync9(settingsPath) ? safeReadJson(settingsPath) : {};
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(dirname7(settingsPath), { recursive: true });
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 readFileSync7, readdirSync as readdirSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
2097
- import { basename as basename5, join as join2, resolve as resolve13 } from "path";
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 readFileSync6 } from "fs";
2101
- import { resolve as resolve12 } from "path";
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 ?? resolve12(projectRoot, ".rig", "task-config.json");
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 = resolve12(projectRoot, ".rig", "task-config.json")) {
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(readFileSync6(configPath, "utf8"));
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 ?? resolve13(projectRoot, ".rig", "task-config.json");
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 ?? resolve13(projectRoot, ".rig", "task-config.json");
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 = resolve13(projectRoot, "rig.config.json");
2006
+ const jsonPath = resolve12(projectRoot, "rig.config.json");
2348
2007
  if (existsSync11(jsonPath)) {
2349
2008
  try {
2350
- const parsed = JSON.parse(readFileSync7(jsonPath, "utf8"));
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 = resolve13(projectRoot, "rig.config.ts");
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 = readFileSync7(tsPath, "utf8");
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(readFileSync7(configPath, "utf8"));
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
- writeFileSync3(configPath, `${JSON.stringify(rawConfig, null, 2)}
2063
+ writeFileSync4(configPath, `${JSON.stringify(rawConfig, null, 2)}
2405
2064
  `, "utf8");
2406
2065
  }
2407
2066
  function updateFileBackedTask(projectRoot, sourcePath, taskId, update) {
2408
- const directory = resolve13(projectRoot, sourcePath);
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(readFileSync7(file, "utf8"));
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
- writeFileSync3(file, `${JSON.stringify(raw, null, 2)}
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 = resolve13(projectRoot, sourcePath);
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 = basename5(name).replace(FILE_TASK_PATTERN, "");
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(resolve13(projectRoot, sourcePath), taskId);
2110
+ const file = findFileBackedTaskFile(resolve12(projectRoot, sourcePath), taskId);
2452
2111
  if (!file) {
2453
2112
  return null;
2454
2113
  }
2455
- const raw = JSON.parse(readFileSync7(file, "utf8"));
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(readFileSync7(file, "utf8"));
2479
- const inferredId = basename5(file).replace(FILE_TASK_PATTERN, "");
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 readFileSync9, readdirSync as readdirSync3, statSync as statSync4, writeFileSync as writeFileSync5 } from "fs";
2828
- import { basename as basename6, resolve as resolve15 } from "path";
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 mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
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 dirname8, isAbsolute, resolve as resolve14 } from "path";
2504
+ import { dirname as dirname7, isAbsolute, resolve as resolve13 } from "path";
2846
2505
  import { createHash } from "crypto";
2847
- var sharedGitNativeOutputDir = resolve14(tmpdir4(), "rig-native");
2848
- var sharedGitNativeOutputPath = resolve14(sharedGitNativeOutputDir, `rig-git-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
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 resolve14(dirname8(outputPath), `.rig-git-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}${suffix2}`);
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
- rmSync2(outputPath, { force: true });
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() ? dirname8(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 = resolve14(import.meta.dir, "../../../native/rig-git.zig");
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 ? resolve14(projectRoot, "packages/runtime/native/rig-git.zig") : "",
2879
- hostProjectRoot ? resolve14(hostProjectRoot, "packages/runtime/native/rig-git.zig") : "",
2880
- cwd ? resolve14(cwd, "packages/runtime/native/rig-git.zig") : "",
2881
- execDir ? resolve14(execDir, "..", "..", "packages/runtime/native/rig-git.zig") : "",
2882
- execDir ? resolve14(execDir, "..", "native", "rig-git.zig") : ""
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 = resolve14(fromDir);
2546
+ let cursor = resolve13(fromDir);
2888
2547
  for (let index = 0;index < 8; index += 1) {
2889
- candidates.push(resolve14(cursor, "native", `${process.platform}-${process.arch}`, fileName), resolve14(cursor, "native", `${process.platform}-${process.arch}`, "bin", fileName), resolve14(cursor, "native", fileName), resolve14(cursor, "native", "bin", fileName));
2890
- const parent = dirname8(cursor);
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() ? dirname8(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 ? resolve14(execDir, fileName) : "",
2905
- execDir ? resolve14(execDir, "..", fileName) : "",
2906
- execDir ? resolve14(execDir, "..", "bin", fileName) : "",
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(readFileSync8(manifestPath, "utf8"));
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(readFileSync8(path)).digest("hex");
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
- mkdirSync4(dirname8(outputPath), { recursive: true });
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: dirname8(sourcePath),
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
- rmSync2(tempOutputPath, { force: true });
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
- rmSync2(outputPath, { force: true });
2685
+ rmSync3(outputPath, { force: true });
3027
2686
  throw new Error("Failed to build native Rig git tools: tracker command probe failed");
3028
2687
  }
3029
- writeFileSync4(manifestPath, `${JSON.stringify({ version: 1, buildKey }, null, 2)}
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(resolve15(projectRoot, "rig", "task-config.json"), {});
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 = resolve15(workspaceDir, "artifacts", id);
3230
- if (existsSync13(worktreeArtifacts) || existsSync13(resolve15(workspaceDir, "artifacts"))) {
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 resolve15(paths.artifactsDir, id);
2895
+ return resolve14(paths.artifactsDir, id);
3237
2896
  } catch {
3238
- return resolve15(resolveMonorepoRoot2(projectRoot), "artifacts", id);
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
- writeFileSync5(sourcePath, `${JSON.stringify(synced.config, null, 2)}
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 = resolve15(resolveMonorepoRoot2(projectRoot), ".beads", "issues.jsonl");
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 readFileSync9(issuesPath, "utf-8").split(/\r?\n/)) {
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 = resolve15(projectRoot, sourcePath);
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 = resolve15(directory, name);
3048
+ const file = resolve14(directory, name);
3390
3049
  try {
3391
3050
  if (!statSync4(file).isFile())
3392
3051
  continue;
3393
- const raw = JSON.parse(readFileSync9(file, "utf8"));
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 = basename6(name).replace(FILE_TASK_PATTERN2, "");
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 = resolve15(projectRoot, "rig.config.json");
3094
+ const jsonPath = resolve14(projectRoot, "rig.config.json");
3436
3095
  if (existsSync13(jsonPath)) {
3437
3096
  try {
3438
- const parsed = JSON.parse(readFileSync9(jsonPath, "utf8"));
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 = resolve15(projectRoot, "rig.config.ts");
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 = readFileSync9(tsPath, "utf8");
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 ? resolve15(runtimeContext.monorepoMainRoot, ".rig", "task-config.json") : "",
3470
- process.env.MONOREPO_MAIN_ROOT?.trim() ? resolve15(process.env.MONOREPO_MAIN_ROOT.trim(), ".rig", "task-config.json") : "",
3471
- resolve15(resolveMonorepoRoot2(projectRoot), ".rig", "task-config.json")
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 mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
3477
- import { resolve as resolve20 } from "path";
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 mkdirSync6, rmSync as rmSync4, statSync as statSync5 } from "fs";
3481
- import { dirname as dirname10, resolve as resolve19 } from "path";
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 mkdirSync5, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
3485
- import { basename as basename7, dirname as dirname9, resolve as resolve16 } from "path";
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 = resolve16(dirname9(options.outputPath), `.bun-build-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
3512
- const tempOutputPath = resolve16(tempBuildDir, basename7(options.outputPath));
3513
- mkdirSync5(tempBuildDir, { recursive: true });
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
- rmSync3(tempBuildDir, { recursive: true, force: true });
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: resolve16(options.cwd, options.sourcePath),
3571
- outputPath: resolve16(options.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
- rmSync3(payloadPath, { force: true });
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 resolve16(dirname9(outputPath), `.bun-build-worker-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
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 = resolve16(root, "packages/runtime/src/binary-build-worker.ts");
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 = resolve16(import.meta.dir, "binary-build-worker.ts");
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 resolve16(cwd, inputPath);
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((resolve17) => {
3739
- release = resolve17;
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 resolve18 } from "path";
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 resolve17 } from "path";
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 ? resolve17(home, ".bun/bin/bun") : "",
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 ? resolve17(home, ".local/bin/claude") : "",
3835
- home ? resolve17(home, ".local/share/claude/local/claude") : "",
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 resolve17(bunBinaryPath, "../..");
3508
+ return resolve16(bunBinaryPath, "../..");
3850
3509
  }
3851
3510
  function resolveClaudeInstallDir() {
3852
3511
  const realPath = resolveClaudeBinaryPath();
3853
- return resolve17(realPath, "..");
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 && resolve17(explicitNode) === resolve17(preferredNode)) {
3861
- return preferredNode.endsWith("/bin/node") ? resolve17(preferredNode, "../..") : resolve17(preferredNode, "..");
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 resolve17(realPath, "../..");
3525
+ return resolve16(realPath, "../..");
3867
3526
  }
3868
- return resolve17(realPath, "..");
3527
+ return resolve16(realPath, "..");
3869
3528
  } catch {
3870
- return resolve17(preferredNode, "..");
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 = resolve17(envNode);
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(resolve17(nvmBin, "node"));
3543
+ candidates.push(resolve16(nvmBin, "node"));
3885
3544
  }
3886
3545
  const home = process.env.HOME?.trim();
3887
3546
  if (home) {
3888
- const nvmVersionsDir = resolve17(home, ".nvm/versions/node");
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(resolve17(nvmVersionsDir, versionDir, "bin/node"));
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) => resolve17(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 = resolve17(nodeBinaryPath).replace(/\\/g, "/");
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 = resolve17(candidate);
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 = resolve17(candidate).replace(/\\/g, "/");
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 resolve18(claudeBinary, "..");
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 ? resolve18(realHome, ".local/bin") : "",
3972
- realHome ? resolve18(realHome, ".cargo/bin") : "",
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 resolve19(runtimeContext.binDir, "validators", binaryName);
3662
+ return resolve18(runtimeContext.binDir, "validators", binaryName);
4004
3663
  }
4005
- return resolve19(resolveHarnessPaths(projectRoot).binDir, "validators", binaryName);
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 = resolve19(hostProjectRoot, "packages/runtime/src/control-plane/validators", category, `${check}.ts`);
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
- rmSync4(binaryPath, { force: true });
4030
- rmSync4(`${binaryPath}.build-manifest.json`, { force: true });
3688
+ rmSync5(binaryPath, { force: true });
3689
+ rmSync5(`${binaryPath}.build-manifest.json`, { force: true });
4031
3690
  }
4032
- mkdirSync6(dirname10(binaryPath), { recursive: true });
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: resolve20(runtimeContext.logsDir, taskId),
4079
- artifactDir: resolve20(runtimeContext.workspaceDir, "artifacts", taskId)
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: resolve20(paths.logsDir, taskId),
4085
- artifactDir: resolve20(paths.artifactsDir, taskId)
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 ? resolve20(runtimeContext.binDir, "rig-shell") : "";
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
- mkdirSync7(taskLogDir, { recursive: true });
4158
- mkdirSync7(artifactDir, { recursive: true });
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
- writeFileSync7(resolve20(artifactDir, "validation-summary.json"), `${JSON.stringify(skipped, null, 2)}
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(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/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
- const valDescriptions = resolvedContext ? {} : options.validationDescriptions ?? (() => {
4183
- try {
4184
- return readValidationDescriptions(projectRoot);
4185
- } catch {
4186
- return {};
4187
- }
4188
- })();
4189
- const categories = [];
4190
- let passed = 0;
4191
- let failed = 0;
4192
- for (const cmd of commands) {
4193
- const startedAt = Date.now();
4194
- if (!isCheckId(cmd)) {
4195
- failed += 1;
4196
- categories.push({
4197
- category: cmd,
4198
- status: "fail",
4199
- exit_code: 2,
4200
- duration_seconds: 0
4201
- });
4202
- const logFile2 = resolve20(taskLogDir, `invalid-entry-validation.log`);
4203
- mkdirSync7(taskLogDir, { recursive: true });
4204
- writeFileSync7(logFile2, `=== ${nowIso()} :: ${cmd} ===
4205
- Invalid validation entry: not a check-ID. All entries must use format "category:check-name".
4206
- `, "utf-8");
4207
- continue;
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
- mkdirSync7(artifactDir, { recursive: true });
4236
- writeFileSync7(resolve20(artifactDir, "validation-summary.json"), `${JSON.stringify(summary, null, 2)}
4237
- `, "utf-8");
4238
- return summary;
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
- mkdirSync8(artifactDir, { recursive: true });
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
- writeFileSync8(greptileRawPath, `${ai.rawResponse}
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
- writeFileSync8(options.output, `${lines.join(`
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
- writeFileSync8(options.output, `${JSON.stringify(payload, null, 2)}
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
- if (/not safe to merge|unsafe to merge|do not merge|blocker/i.test(reviewBody)) {
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
- if (score.scale === 5 && score.value < 5 && options.reviewMode === "required") {
5057
- reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; required mode needs 5/5 before merge.`);
5058
- return {
5059
- verdict: "REJECT",
5060
- feedback,
5061
- reasons,
5062
- warnings,
5063
- rawPayload: {
5064
- pr: options.prState,
5065
- codeReviews: reviewsPayload,
5066
- selectedReview,
5067
- reviewDetails,
5068
- comments: commentsPayload,
5069
- score
5070
- }
5071
- };
5072
- }
5073
- if (score.scale === 5 && score.value <= 2) {
5074
- reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; this requires rework before merge.`);
5075
- return {
5076
- verdict: "REJECT",
5077
- feedback,
5078
- reasons,
5079
- warnings,
5080
- rawPayload: {
5081
- pr: options.prState,
5082
- codeReviews: reviewsPayload,
5083
- selectedReview,
5084
- reviewDetails,
5085
- comments: commentsPayload,
5086
- score
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 checkState = { pending: false, completed: false };
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
- checkState = classifyGithubGreptileCheckState(checkRollup);
5138
- const approvedViaReviewedAncestor2 = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
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: approvedViaReviewedAncestor2
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 (checkState.pending) {
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 approvedViaCompletedCheck = isGithubGreptileCheckApproved(checkRollup);
5185
- if (!fallbackReview) {
5186
- if (approvedViaCompletedCheck) {
5187
- warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no GitHub Greptile review object, but the Greptile check completed successfully and no unresolved Greptile threads remain.`);
5188
- return {
5189
- verdict: "APPROVE",
5190
- feedback,
5191
- reasons: [],
5192
- warnings,
5193
- rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
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: actionableThreads.map((comment) => `[AI Review] ${repoName}#${prNumber} has unresolved Greptile comment on ${comment.path}: ${summarizeComment(comment.body || "")}`),
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 (!selectedReview && !approvedViaReviewedAncestor) {
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
- `[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available for the current head.`
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 isGreptileReviewTerminal(existingReview.status);
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 options.attempt + 1 < options.pollAttempts;
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 isGreptileGithubLogin(login) {
5473
- return (login || "").replace(/\[bot\]$/, "") === "greptile-apps";
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) => isGreptileGithubLogin(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
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) => isGreptileGithubLogin(comment.author?.login));
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(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/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
- mkdirSync9(artifactDir, { recursive: true });
6765
+ mkdirSync11(artifactDir, { recursive: true });
5715
6766
  const changed = changedFilesForTask(projectRoot, activeTask, true);
5716
- writeFileSync9(resolve22(artifactDir, "changed-files.txt"), `${changed.join(`
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
- writeFileSync9(taskResultPath, `${JSON.stringify(template, null, 2)}
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
- writeFileSync9(decisionLogPath, content, "utf-8");
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
- writeFileSync9(nextActionsPath, content, "utf-8");
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 enables GitHub auto-merge after approval."
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 auto-merge PR ${options.pr.url}: state is ${state}.`);
7413
+ throw new Error(`Cannot merge PR ${options.pr.url}: state is ${state}.`);
6335
7414
  }
6336
7415
  if (isDraft) {
6337
- throw new Error(`Cannot auto-merge draft PR ${options.pr.url}.`);
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 autoMergeArgs = [...mergeArgs, "--auto"];
6346
- const autoMerge = runCapture2(autoMergeArgs, repoRoot);
6347
- if (autoMerge.exitCode === 0) {
6348
- const postAutoMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
6349
- if (postAutoMergeState.state === "MERGED" || postAutoMergeState.mergedAt) {
6350
- console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
6351
- return { status: "merged", url: options.pr.url };
6352
- }
6353
- if (postAutoMergeState.state === "OPEN" && postAutoMergeState.autoMergeRequest) {
6354
- if (canAdminMergeApprovedPr(postAutoMergeState)) {
6355
- const adminMergeArgs = [...mergeArgs];
6356
- if (postAutoMergeState.headRefOid) {
6357
- adminMergeArgs.push("--match-head-commit", postAutoMergeState.headRefOid);
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
- console.log(`Auto-merge enabled (${options.pr.repoLabel}): ${options.pr.url}`);
6376
- return { status: "auto-merge-enabled", url: options.pr.url };
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
- runOrThrow(options.projectRoot, mergeArgs, `Failed to merge PR ${options.pr.url} in ${options.pr.repoLabel}`);
6387
- console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
6388
- return { status: "merged", url: options.pr.url };
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
- mkdirSync10(dir, { recursive: true });
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(readFileSync11(path, "utf-8"));
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
- writeFileSync10(path, `${JSON.stringify(artifact, null, 2)}
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(readFileSync11(path, "utf-8"));
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 = readFileSync11(manifestPath, "utf-8").split(/\r?\n/).map((line) => normalizeChangedFilePath2(line)).filter(Boolean);
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
- mkdirSync10(dirname11(manifestPath), { recursive: true });
7861
+ mkdirSync12(dirname10(manifestPath), { recursive: true });
6812
7862
  const changedFiles = changedFilesForTask(projectRoot, taskId, true);
6813
- writeFileSync10(manifestPath, `${changedFiles.join(`
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(readFileSync11(path, "utf-8"));
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
- mkdirSync10(sslDir, { recursive: true });
8168
+ mkdirSync12(sslDir, { recursive: true });
7119
8169
  }
7120
8170
  if (!existsSync20(sslConfig)) {
7121
- writeFileSync10(sslConfig, `# Rig runtime OpenSSL config placeholder
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: dirname11(resolve23(contextFile))
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: dirname11(inferredContextFile)
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 = dirname11(current);
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 mergePending = false;
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 === "auto-merge-enabled") {
7367
- mergePending = true;
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 (mergePending) {
7372
- failed = true;
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
- mkdirSync11(artifactDir, { recursive: true });
7388
- writeFileSync11(resolve24(artifactDir, "review-status.txt"), failed ? `REJECTED
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
- const closeout = await closeCompletedTaskSource(projectRoot, taskId);
7394
- if (!closeout.ok) {
7395
- console.log(`FAIL: ${closeout.message}`);
7396
- failed = true;
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(`OK: ${closeout.message}`);
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 = readFileSync12(workflowPath, "utf-8");
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 = readFileSync12(failedApproachesPath, "utf-8");
8603
+ const content = readFileSync13(failedApproachesPath, "utf-8");
7539
8604
  attempts = (content.match(new RegExp(`^## ${escapeRegExp2(taskId)}\\b`, "gm")) || []).length + 1;
7540
8605
  } else {
7541
- mkdirSync11(resolve24(failedApproachesPath, ".."), { recursive: true });
7542
- writeFileSync11(failedApproachesPath, `# Failed Approaches
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
- mkdirSync11(resolve24(statePath, ".."), { recursive: true });
7581
- writeFileSync11(statePath, `${JSON.stringify(state, null, 2)}
8645
+ mkdirSync13(resolve24(statePath, ".."), { recursive: true });
8646
+ writeFileSync13(statePath, `${JSON.stringify(state, null, 2)}
7582
8647
  `, "utf-8");
7583
8648
  }
7584
8649
  }