@h-rig/runtime 0.0.6-alpha.21 → 0.0.6-alpha.23

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 (25) hide show
  1. package/dist/bin/rig-agent-dispatch.js +588 -28
  2. package/dist/src/control-plane/agent-wrapper.js +592 -28
  3. package/dist/src/control-plane/harness-main.js +142 -17
  4. package/dist/src/control-plane/hooks/completion-verification.js +142 -17
  5. package/dist/src/control-plane/native/harness-cli.js +142 -17
  6. package/dist/src/control-plane/native/pr-automation.js +142 -17
  7. package/dist/src/control-plane/native/pr-review-gate.js +142 -17
  8. package/dist/src/control-plane/native/run-ops.js +1 -1
  9. package/dist/src/control-plane/native/task-ops.js +142 -17
  10. package/dist/src/control-plane/native/verifier.js +142 -17
  11. package/dist/src/control-plane/pi-sessiond/bin.js +793 -0
  12. package/dist/src/control-plane/pi-sessiond/client.js +41 -0
  13. package/dist/src/control-plane/pi-sessiond/event-hub.js +59 -0
  14. package/dist/src/control-plane/pi-sessiond/extension-ui-context.js +198 -0
  15. package/dist/src/control-plane/pi-sessiond/launcher.js +163 -0
  16. package/dist/src/control-plane/pi-sessiond/server.js +802 -0
  17. package/dist/src/control-plane/pi-sessiond/session-service.js +540 -0
  18. package/dist/src/control-plane/pi-sessiond/types.js +1 -0
  19. package/dist/src/control-plane/runtime/index.js +17 -0
  20. package/dist/src/control-plane/runtime/isolation/home.js +17 -0
  21. package/dist/src/control-plane/runtime/isolation/index.js +17 -0
  22. package/dist/src/control-plane/runtime/isolation/runner.js +17 -0
  23. package/dist/src/control-plane/runtime/isolation.js +17 -0
  24. package/dist/src/control-plane/runtime/queue.js +17 -0
  25. package/package.json +7 -6
@@ -3631,6 +3631,57 @@ function flattenPaginatedArray(value) {
3631
3631
  }
3632
3632
  return value;
3633
3633
  }
3634
+ function parseConcatenatedJsonValues(value) {
3635
+ const text = value.trim();
3636
+ const docs = [];
3637
+ let start = null;
3638
+ let depth = 0;
3639
+ let inString = false;
3640
+ let escape = false;
3641
+ for (let index = 0;index < text.length; index += 1) {
3642
+ const char = text[index];
3643
+ if (start === null) {
3644
+ if (/\s/.test(char))
3645
+ continue;
3646
+ start = index;
3647
+ }
3648
+ if (inString) {
3649
+ if (escape) {
3650
+ escape = false;
3651
+ } else if (char === "\\") {
3652
+ escape = true;
3653
+ } else if (char === '"') {
3654
+ inString = false;
3655
+ }
3656
+ continue;
3657
+ }
3658
+ if (char === '"') {
3659
+ inString = true;
3660
+ continue;
3661
+ }
3662
+ if (char === "{" || char === "[") {
3663
+ depth += 1;
3664
+ continue;
3665
+ }
3666
+ if (char === "}" || char === "]") {
3667
+ depth -= 1;
3668
+ if (depth < 0)
3669
+ return { value: docs, error: "unexpected JSON close delimiter" };
3670
+ if (depth === 0 && start !== null) {
3671
+ const segment = text.slice(start, index + 1);
3672
+ try {
3673
+ docs.push(JSON.parse(segment));
3674
+ } catch (error) {
3675
+ return { value: docs, error: error instanceof Error ? error.message : String(error) };
3676
+ }
3677
+ start = null;
3678
+ }
3679
+ }
3680
+ }
3681
+ if (inString || depth !== 0 || start !== null)
3682
+ return { value: docs, error: "incomplete JSON stream" };
3683
+ return { value: docs };
3684
+ }
3634
3685
  function parseJsonArray(value) {
3635
3686
  if (!value?.trim())
3636
3687
  return { value: [], error: "empty JSON output" };
@@ -3639,7 +3690,11 @@ function parseJsonArray(value) {
3639
3690
  const flattened = flattenPaginatedArray(parsed);
3640
3691
  return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
3641
3692
  } catch (error) {
3642
- return { value: [], error: error instanceof Error ? error.message : String(error) };
3693
+ const streamed = parseConcatenatedJsonValues(value);
3694
+ if (streamed.error)
3695
+ return { value: [], error: error instanceof Error ? error.message : String(error) };
3696
+ const flattened = streamed.value.flatMap((entry) => flattenPaginatedArray(entry) ?? []);
3697
+ return flattened.length > 0 || streamed.value.length === 0 ? { value: flattened } : { value: [], error: "JSON output was not an array" };
3643
3698
  }
3644
3699
  }
3645
3700
  function parseGithubPrUrl(prUrl) {
@@ -3728,6 +3783,24 @@ function isStrictFiveOfFive(score) {
3728
3783
  function containsConflictingScoreText(input) {
3729
3784
  return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
3730
3785
  }
3786
+ function extractGreptileCommentBlock(input) {
3787
+ const match = input.match(/<!--\s*greptile_comment\s*-->([\s\S]*?)<!--\s*\/greptile_comment\s*-->/i);
3788
+ return match?.[1]?.trim() ?? null;
3789
+ }
3790
+ function extractGreptileBodyReviewedSha(input) {
3791
+ const block = extractGreptileCommentBlock(input);
3792
+ if (!block)
3793
+ return null;
3794
+ const commitLink = block.match(/\/commit\/([0-9a-f]{40})(?:\b|[^0-9a-f])/i);
3795
+ return commitLink?.[1]?.toLowerCase() ?? null;
3796
+ }
3797
+ function isoAtOrAfter(value, floor) {
3798
+ if (!value || !floor)
3799
+ return false;
3800
+ const valueMs = Date.parse(value);
3801
+ const floorMs = Date.parse(floor);
3802
+ return Number.isFinite(valueMs) && Number.isFinite(floorMs) && valueMs >= floorMs;
3803
+ }
3731
3804
  function greptileStatusVerdict(status) {
3732
3805
  const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
3733
3806
  if (!normalized)
@@ -4102,9 +4175,18 @@ function commentAuthorLogin(comment) {
4102
4175
  }
4103
4176
  function collectGreptileSignals(evidence) {
4104
4177
  const signals = [];
4178
+ const greptileBodyReviewedSha = extractGreptileBodyReviewedSha(evidence.body);
4179
+ const trustedGreptileBody = Boolean(greptileBodyReviewedSha && isGreptileGithubLogin(evidence.bodyEditorLogin) && isoAtOrAfter(evidence.bodyLastEditedAt, evidence.headCommittedDate));
4105
4180
  const contextSources = [
4106
4181
  { source: "pr-title", body: evidence.title ?? "" },
4107
- { source: "pr-body", body: evidence.body }
4182
+ {
4183
+ source: "pr-body",
4184
+ body: evidence.body,
4185
+ trusted: trustedGreptileBody,
4186
+ authorLogin: trustedGreptileBody ? evidence.bodyEditorLogin ?? null : null,
4187
+ reviewedSha: greptileBodyReviewedSha,
4188
+ verdict: trustedGreptileBody ? "completed" : null
4189
+ }
4108
4190
  ];
4109
4191
  for (const context of contextSources) {
4110
4192
  if (!context.body.trim())
@@ -4116,7 +4198,10 @@ function collectGreptileSignals(evidence) {
4116
4198
  source: context.source,
4117
4199
  body: context.body,
4118
4200
  currentHeadSha: evidence.currentHeadSha,
4119
- trusted: false,
4201
+ trusted: context.trusted === true,
4202
+ authorLogin: context.authorLogin,
4203
+ reviewedSha: context.reviewedSha,
4204
+ verdict: context.verdict,
4120
4205
  blocker: contextBlocker,
4121
4206
  actionable: contextBlocker
4122
4207
  }));
@@ -4312,7 +4397,7 @@ function deriveGreptileEvidence(input) {
4312
4397
  const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
4313
4398
  const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
4314
4399
  const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
4315
- const source = approvingSignal?.source === "api" ? "api" : approvingSignal?.source === "github-review" ? "github-review" : 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";
4400
+ 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";
4316
4401
  return {
4317
4402
  source,
4318
4403
  currentHeadSha: input.currentHeadSha,
@@ -4333,17 +4418,48 @@ function isGreptileCheckDetail(check) {
4333
4418
  return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
4334
4419
  }
4335
4420
  async function collectGreptileCheckDetails(input) {
4336
- const checkRunsRead = await runJsonArray(input.command, [
4421
+ const checkRunsRead = await runJsonObject(input.command, [
4337
4422
  "api",
4338
4423
  `repos/${input.repoName}/commits/${input.headSha}/check-runs`,
4339
- "--paginate",
4340
- "--slurp",
4341
- "--jq",
4342
- "map(.check_runs // []) | add // []"
4424
+ "-F",
4425
+ "per_page=100"
4343
4426
  ], input.projectRoot);
4344
- const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
4427
+ const checkRuns = arrayField(checkRunsRead.value, "check_runs").map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
4345
4428
  return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
4346
4429
  }
4430
+ async function collectPullRequestProvenance(input) {
4431
+ const response = await runJsonObject(input.command, [
4432
+ "api",
4433
+ "graphql",
4434
+ "-F",
4435
+ `owner=${input.owner}`,
4436
+ "-F",
4437
+ `name=${input.name}`,
4438
+ "-F",
4439
+ `prNumber=${input.prNumber}`,
4440
+ "-f",
4441
+ "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 } } } } } }"
4442
+ ], input.projectRoot);
4443
+ if (response.error)
4444
+ return { value: {}, error: response.error };
4445
+ const data = response.value.data;
4446
+ const repository = data?.repository;
4447
+ const pullRequest = repository?.pullRequest;
4448
+ if (!pullRequest)
4449
+ return { value: {}, error: "GitHub pullRequest provenance response did not include a pullRequest object" };
4450
+ const editor = pullRequest.editor;
4451
+ const commits = pullRequest.commits;
4452
+ const nodes = Array.isArray(commits?.nodes) ? commits.nodes : [];
4453
+ const latestCommitNode = nodes[nodes.length - 1];
4454
+ const latestCommit = latestCommitNode?.commit;
4455
+ return {
4456
+ value: {
4457
+ bodyEditorLogin: typeof editor?.login === "string" ? editor.login : null,
4458
+ bodyLastEditedAt: typeof pullRequest.lastEditedAt === "string" ? pullRequest.lastEditedAt : null,
4459
+ headCommittedDate: typeof latestCommit?.committedDate === "string" ? latestCommit.committedDate : null
4460
+ }
4461
+ };
4462
+ }
4347
4463
  async function collectReviewThreads(input) {
4348
4464
  const reviewThreads = [];
4349
4465
  let afterCursor = null;
@@ -4421,11 +4537,19 @@ async function collectPrReviewEvidence(input) {
4421
4537
  const baseRefName = firstString(view, ["baseRefName"]);
4422
4538
  const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
4423
4539
  const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
4424
- const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
4540
+ const provenanceRead = await collectPullRequestProvenance({
4541
+ command: input.command,
4542
+ projectRoot: input.projectRoot,
4543
+ owner: parsed.owner,
4544
+ name: parsed.repo,
4545
+ prNumber: parsed.prNumber
4546
+ });
4547
+ const provenance = provenanceRead.value;
4548
+ const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
4425
4549
  if (reviewCommentsRead.error)
4426
4550
  readErrors.push(reviewCommentsRead.error);
4427
4551
  const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
4428
- const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
4552
+ const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
4429
4553
  if (issueCommentsRead.error)
4430
4554
  readErrors.push(issueCommentsRead.error);
4431
4555
  const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
@@ -4448,12 +4572,7 @@ async function collectPrReviewEvidence(input) {
4448
4572
  repoName: parsed.repoName,
4449
4573
  headSha
4450
4574
  });
4451
- if (checkDetailsRead.error)
4452
- readErrors.push(checkDetailsRead.error);
4453
4575
  greptileCheckDetails = checkDetailsRead.value;
4454
- if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
4455
- readErrors.push("Greptile check details could not be found for the current PR head");
4456
- }
4457
4576
  }
4458
4577
  const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
4459
4578
  const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
@@ -4472,6 +4591,9 @@ async function collectPrReviewEvidence(input) {
4472
4591
  const evidenceBase = {
4473
4592
  title: firstString(view, ["title"]),
4474
4593
  body: firstString(view, ["body"]),
4594
+ bodyEditorLogin: provenance.bodyEditorLogin ?? null,
4595
+ bodyLastEditedAt: provenance.bodyLastEditedAt ?? null,
4596
+ headCommittedDate: provenance.headCommittedDate ?? null,
4475
4597
  reviews,
4476
4598
  changedFileReviewComments: reviewComments,
4477
4599
  relevantIssueComments: issueComments,
@@ -4487,6 +4609,9 @@ async function collectPrReviewEvidence(input) {
4487
4609
  repoName: parsed.repoName,
4488
4610
  title: evidenceBase.title,
4489
4611
  body: evidenceBase.body,
4612
+ bodyEditorLogin: evidenceBase.bodyEditorLogin,
4613
+ bodyLastEditedAt: evidenceBase.bodyLastEditedAt,
4614
+ headCommittedDate: evidenceBase.headCommittedDate,
4490
4615
  headSha,
4491
4616
  headRefName: firstString(view, ["headRefName"]),
4492
4617
  baseRefName,
@@ -4263,6 +4263,57 @@ function flattenPaginatedArray(value) {
4263
4263
  }
4264
4264
  return value;
4265
4265
  }
4266
+ function parseConcatenatedJsonValues(value) {
4267
+ const text = value.trim();
4268
+ const docs = [];
4269
+ let start = null;
4270
+ let depth = 0;
4271
+ let inString = false;
4272
+ let escape = false;
4273
+ for (let index = 0;index < text.length; index += 1) {
4274
+ const char = text[index];
4275
+ if (start === null) {
4276
+ if (/\s/.test(char))
4277
+ continue;
4278
+ start = index;
4279
+ }
4280
+ if (inString) {
4281
+ if (escape) {
4282
+ escape = false;
4283
+ } else if (char === "\\") {
4284
+ escape = true;
4285
+ } else if (char === '"') {
4286
+ inString = false;
4287
+ }
4288
+ continue;
4289
+ }
4290
+ if (char === '"') {
4291
+ inString = true;
4292
+ continue;
4293
+ }
4294
+ if (char === "{" || char === "[") {
4295
+ depth += 1;
4296
+ continue;
4297
+ }
4298
+ if (char === "}" || char === "]") {
4299
+ depth -= 1;
4300
+ if (depth < 0)
4301
+ return { value: docs, error: "unexpected JSON close delimiter" };
4302
+ if (depth === 0 && start !== null) {
4303
+ const segment = text.slice(start, index + 1);
4304
+ try {
4305
+ docs.push(JSON.parse(segment));
4306
+ } catch (error) {
4307
+ return { value: docs, error: error instanceof Error ? error.message : String(error) };
4308
+ }
4309
+ start = null;
4310
+ }
4311
+ }
4312
+ }
4313
+ if (inString || depth !== 0 || start !== null)
4314
+ return { value: docs, error: "incomplete JSON stream" };
4315
+ return { value: docs };
4316
+ }
4266
4317
  function parseJsonArray(value) {
4267
4318
  if (!value?.trim())
4268
4319
  return { value: [], error: "empty JSON output" };
@@ -4271,7 +4322,11 @@ function parseJsonArray(value) {
4271
4322
  const flattened = flattenPaginatedArray(parsed);
4272
4323
  return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
4273
4324
  } catch (error) {
4274
- return { value: [], error: error instanceof Error ? error.message : String(error) };
4325
+ const streamed = parseConcatenatedJsonValues(value);
4326
+ if (streamed.error)
4327
+ return { value: [], error: error instanceof Error ? error.message : String(error) };
4328
+ const flattened = streamed.value.flatMap((entry) => flattenPaginatedArray(entry) ?? []);
4329
+ return flattened.length > 0 || streamed.value.length === 0 ? { value: flattened } : { value: [], error: "JSON output was not an array" };
4275
4330
  }
4276
4331
  }
4277
4332
  function parseGithubPrUrl(prUrl) {
@@ -4360,6 +4415,24 @@ function isStrictFiveOfFive(score) {
4360
4415
  function containsConflictingScoreText(input) {
4361
4416
  return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
4362
4417
  }
4418
+ function extractGreptileCommentBlock(input) {
4419
+ const match = input.match(/<!--\s*greptile_comment\s*-->([\s\S]*?)<!--\s*\/greptile_comment\s*-->/i);
4420
+ return match?.[1]?.trim() ?? null;
4421
+ }
4422
+ function extractGreptileBodyReviewedSha(input) {
4423
+ const block = extractGreptileCommentBlock(input);
4424
+ if (!block)
4425
+ return null;
4426
+ const commitLink = block.match(/\/commit\/([0-9a-f]{40})(?:\b|[^0-9a-f])/i);
4427
+ return commitLink?.[1]?.toLowerCase() ?? null;
4428
+ }
4429
+ function isoAtOrAfter(value, floor) {
4430
+ if (!value || !floor)
4431
+ return false;
4432
+ const valueMs = Date.parse(value);
4433
+ const floorMs = Date.parse(floor);
4434
+ return Number.isFinite(valueMs) && Number.isFinite(floorMs) && valueMs >= floorMs;
4435
+ }
4363
4436
  function greptileStatusVerdict(status) {
4364
4437
  const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
4365
4438
  if (!normalized)
@@ -4734,9 +4807,18 @@ function commentAuthorLogin(comment) {
4734
4807
  }
4735
4808
  function collectGreptileSignals(evidence) {
4736
4809
  const signals = [];
4810
+ const greptileBodyReviewedSha = extractGreptileBodyReviewedSha(evidence.body);
4811
+ const trustedGreptileBody = Boolean(greptileBodyReviewedSha && isGreptileGithubLogin(evidence.bodyEditorLogin) && isoAtOrAfter(evidence.bodyLastEditedAt, evidence.headCommittedDate));
4737
4812
  const contextSources = [
4738
4813
  { source: "pr-title", body: evidence.title ?? "" },
4739
- { source: "pr-body", body: evidence.body }
4814
+ {
4815
+ source: "pr-body",
4816
+ body: evidence.body,
4817
+ trusted: trustedGreptileBody,
4818
+ authorLogin: trustedGreptileBody ? evidence.bodyEditorLogin ?? null : null,
4819
+ reviewedSha: greptileBodyReviewedSha,
4820
+ verdict: trustedGreptileBody ? "completed" : null
4821
+ }
4740
4822
  ];
4741
4823
  for (const context of contextSources) {
4742
4824
  if (!context.body.trim())
@@ -4748,7 +4830,10 @@ function collectGreptileSignals(evidence) {
4748
4830
  source: context.source,
4749
4831
  body: context.body,
4750
4832
  currentHeadSha: evidence.currentHeadSha,
4751
- trusted: false,
4833
+ trusted: context.trusted === true,
4834
+ authorLogin: context.authorLogin,
4835
+ reviewedSha: context.reviewedSha,
4836
+ verdict: context.verdict,
4752
4837
  blocker: contextBlocker,
4753
4838
  actionable: contextBlocker
4754
4839
  }));
@@ -4944,7 +5029,7 @@ function deriveGreptileEvidence(input) {
4944
5029
  const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
4945
5030
  const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
4946
5031
  const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
4947
- const source = approvingSignal?.source === "api" ? "api" : approvingSignal?.source === "github-review" ? "github-review" : 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";
5032
+ 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";
4948
5033
  return {
4949
5034
  source,
4950
5035
  currentHeadSha: input.currentHeadSha,
@@ -4965,17 +5050,48 @@ function isGreptileCheckDetail(check) {
4965
5050
  return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
4966
5051
  }
4967
5052
  async function collectGreptileCheckDetails(input) {
4968
- const checkRunsRead = await runJsonArray(input.command, [
5053
+ const checkRunsRead = await runJsonObject(input.command, [
4969
5054
  "api",
4970
5055
  `repos/${input.repoName}/commits/${input.headSha}/check-runs`,
4971
- "--paginate",
4972
- "--slurp",
4973
- "--jq",
4974
- "map(.check_runs // []) | add // []"
5056
+ "-F",
5057
+ "per_page=100"
4975
5058
  ], input.projectRoot);
4976
- const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
5059
+ const checkRuns = arrayField(checkRunsRead.value, "check_runs").map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
4977
5060
  return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
4978
5061
  }
5062
+ async function collectPullRequestProvenance(input) {
5063
+ const response = await runJsonObject(input.command, [
5064
+ "api",
5065
+ "graphql",
5066
+ "-F",
5067
+ `owner=${input.owner}`,
5068
+ "-F",
5069
+ `name=${input.name}`,
5070
+ "-F",
5071
+ `prNumber=${input.prNumber}`,
5072
+ "-f",
5073
+ "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 } } } } } }"
5074
+ ], input.projectRoot);
5075
+ if (response.error)
5076
+ return { value: {}, error: response.error };
5077
+ const data = response.value.data;
5078
+ const repository = data?.repository;
5079
+ const pullRequest = repository?.pullRequest;
5080
+ if (!pullRequest)
5081
+ return { value: {}, error: "GitHub pullRequest provenance response did not include a pullRequest object" };
5082
+ const editor = pullRequest.editor;
5083
+ const commits = pullRequest.commits;
5084
+ const nodes = Array.isArray(commits?.nodes) ? commits.nodes : [];
5085
+ const latestCommitNode = nodes[nodes.length - 1];
5086
+ const latestCommit = latestCommitNode?.commit;
5087
+ return {
5088
+ value: {
5089
+ bodyEditorLogin: typeof editor?.login === "string" ? editor.login : null,
5090
+ bodyLastEditedAt: typeof pullRequest.lastEditedAt === "string" ? pullRequest.lastEditedAt : null,
5091
+ headCommittedDate: typeof latestCommit?.committedDate === "string" ? latestCommit.committedDate : null
5092
+ }
5093
+ };
5094
+ }
4979
5095
  async function collectReviewThreads(input) {
4980
5096
  const reviewThreads = [];
4981
5097
  let afterCursor = null;
@@ -5053,11 +5169,19 @@ async function collectPrReviewEvidence(input) {
5053
5169
  const baseRefName = firstString(view, ["baseRefName"]);
5054
5170
  const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
5055
5171
  const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
5056
- const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
5172
+ const provenanceRead = await collectPullRequestProvenance({
5173
+ command: input.command,
5174
+ projectRoot: input.projectRoot,
5175
+ owner: parsed.owner,
5176
+ name: parsed.repo,
5177
+ prNumber: parsed.prNumber
5178
+ });
5179
+ const provenance = provenanceRead.value;
5180
+ const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
5057
5181
  if (reviewCommentsRead.error)
5058
5182
  readErrors.push(reviewCommentsRead.error);
5059
5183
  const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
5060
- const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
5184
+ const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
5061
5185
  if (issueCommentsRead.error)
5062
5186
  readErrors.push(issueCommentsRead.error);
5063
5187
  const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
@@ -5080,12 +5204,7 @@ async function collectPrReviewEvidence(input) {
5080
5204
  repoName: parsed.repoName,
5081
5205
  headSha
5082
5206
  });
5083
- if (checkDetailsRead.error)
5084
- readErrors.push(checkDetailsRead.error);
5085
5207
  greptileCheckDetails = checkDetailsRead.value;
5086
- if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
5087
- readErrors.push("Greptile check details could not be found for the current PR head");
5088
- }
5089
5208
  }
5090
5209
  const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
5091
5210
  const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
@@ -5104,6 +5223,9 @@ async function collectPrReviewEvidence(input) {
5104
5223
  const evidenceBase = {
5105
5224
  title: firstString(view, ["title"]),
5106
5225
  body: firstString(view, ["body"]),
5226
+ bodyEditorLogin: provenance.bodyEditorLogin ?? null,
5227
+ bodyLastEditedAt: provenance.bodyLastEditedAt ?? null,
5228
+ headCommittedDate: provenance.headCommittedDate ?? null,
5107
5229
  reviews,
5108
5230
  changedFileReviewComments: reviewComments,
5109
5231
  relevantIssueComments: issueComments,
@@ -5119,6 +5241,9 @@ async function collectPrReviewEvidence(input) {
5119
5241
  repoName: parsed.repoName,
5120
5242
  title: evidenceBase.title,
5121
5243
  body: evidenceBase.body,
5244
+ bodyEditorLogin: evidenceBase.bodyEditorLogin,
5245
+ bodyLastEditedAt: evidenceBase.bodyLastEditedAt,
5246
+ headCommittedDate: evidenceBase.headCommittedDate,
5122
5247
  headSha,
5123
5248
  headRefName: firstString(view, ["headRefName"]),
5124
5249
  baseRefName,