@h-rig/runtime 0.0.6-alpha.22 → 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 +333 -23
  2. package/dist/src/control-plane/agent-wrapper.js +336 -23
  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 -7
@@ -65,6 +65,57 @@ function flattenPaginatedArray(value) {
65
65
  }
66
66
  return value;
67
67
  }
68
+ function parseConcatenatedJsonValues(value) {
69
+ const text = value.trim();
70
+ const docs = [];
71
+ let start = null;
72
+ let depth = 0;
73
+ let inString = false;
74
+ let escape = false;
75
+ for (let index = 0;index < text.length; index += 1) {
76
+ const char = text[index];
77
+ if (start === null) {
78
+ if (/\s/.test(char))
79
+ continue;
80
+ start = index;
81
+ }
82
+ if (inString) {
83
+ if (escape) {
84
+ escape = false;
85
+ } else if (char === "\\") {
86
+ escape = true;
87
+ } else if (char === '"') {
88
+ inString = false;
89
+ }
90
+ continue;
91
+ }
92
+ if (char === '"') {
93
+ inString = true;
94
+ continue;
95
+ }
96
+ if (char === "{" || char === "[") {
97
+ depth += 1;
98
+ continue;
99
+ }
100
+ if (char === "}" || char === "]") {
101
+ depth -= 1;
102
+ if (depth < 0)
103
+ return { value: docs, error: "unexpected JSON close delimiter" };
104
+ if (depth === 0 && start !== null) {
105
+ const segment = text.slice(start, index + 1);
106
+ try {
107
+ docs.push(JSON.parse(segment));
108
+ } catch (error) {
109
+ return { value: docs, error: error instanceof Error ? error.message : String(error) };
110
+ }
111
+ start = null;
112
+ }
113
+ }
114
+ }
115
+ if (inString || depth !== 0 || start !== null)
116
+ return { value: docs, error: "incomplete JSON stream" };
117
+ return { value: docs };
118
+ }
68
119
  function parseJsonArray(value) {
69
120
  if (!value?.trim())
70
121
  return { value: [], error: "empty JSON output" };
@@ -73,7 +124,11 @@ function parseJsonArray(value) {
73
124
  const flattened = flattenPaginatedArray(parsed);
74
125
  return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
75
126
  } catch (error) {
76
- return { value: [], error: error instanceof Error ? error.message : String(error) };
127
+ const streamed = parseConcatenatedJsonValues(value);
128
+ if (streamed.error)
129
+ return { value: [], error: error instanceof Error ? error.message : String(error) };
130
+ const flattened = streamed.value.flatMap((entry) => flattenPaginatedArray(entry) ?? []);
131
+ return flattened.length > 0 || streamed.value.length === 0 ? { value: flattened } : { value: [], error: "JSON output was not an array" };
77
132
  }
78
133
  }
79
134
  function parseGithubPrUrl(prUrl) {
@@ -162,6 +217,24 @@ function isStrictFiveOfFive(score) {
162
217
  function containsConflictingScoreText(input) {
163
218
  return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
164
219
  }
220
+ function extractGreptileCommentBlock(input) {
221
+ const match = input.match(/<!--\s*greptile_comment\s*-->([\s\S]*?)<!--\s*\/greptile_comment\s*-->/i);
222
+ return match?.[1]?.trim() ?? null;
223
+ }
224
+ function extractGreptileBodyReviewedSha(input) {
225
+ const block = extractGreptileCommentBlock(input);
226
+ if (!block)
227
+ return null;
228
+ const commitLink = block.match(/\/commit\/([0-9a-f]{40})(?:\b|[^0-9a-f])/i);
229
+ return commitLink?.[1]?.toLowerCase() ?? null;
230
+ }
231
+ function isoAtOrAfter(value, floor) {
232
+ if (!value || !floor)
233
+ return false;
234
+ const valueMs = Date.parse(value);
235
+ const floorMs = Date.parse(floor);
236
+ return Number.isFinite(valueMs) && Number.isFinite(floorMs) && valueMs >= floorMs;
237
+ }
165
238
  function greptileStatusVerdict(status) {
166
239
  const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
167
240
  if (!normalized)
@@ -536,9 +609,18 @@ function commentAuthorLogin(comment) {
536
609
  }
537
610
  function collectGreptileSignals(evidence) {
538
611
  const signals = [];
612
+ const greptileBodyReviewedSha = extractGreptileBodyReviewedSha(evidence.body);
613
+ const trustedGreptileBody = Boolean(greptileBodyReviewedSha && isGreptileGithubLogin(evidence.bodyEditorLogin) && isoAtOrAfter(evidence.bodyLastEditedAt, evidence.headCommittedDate));
539
614
  const contextSources = [
540
615
  { source: "pr-title", body: evidence.title ?? "" },
541
- { source: "pr-body", body: evidence.body }
616
+ {
617
+ source: "pr-body",
618
+ body: evidence.body,
619
+ trusted: trustedGreptileBody,
620
+ authorLogin: trustedGreptileBody ? evidence.bodyEditorLogin ?? null : null,
621
+ reviewedSha: greptileBodyReviewedSha,
622
+ verdict: trustedGreptileBody ? "completed" : null
623
+ }
542
624
  ];
543
625
  for (const context of contextSources) {
544
626
  if (!context.body.trim())
@@ -550,7 +632,10 @@ function collectGreptileSignals(evidence) {
550
632
  source: context.source,
551
633
  body: context.body,
552
634
  currentHeadSha: evidence.currentHeadSha,
553
- trusted: false,
635
+ trusted: context.trusted === true,
636
+ authorLogin: context.authorLogin,
637
+ reviewedSha: context.reviewedSha,
638
+ verdict: context.verdict,
554
639
  blocker: contextBlocker,
555
640
  actionable: contextBlocker
556
641
  }));
@@ -746,7 +831,7 @@ function deriveGreptileEvidence(input) {
746
831
  const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
747
832
  const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
748
833
  const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
749
- 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";
834
+ 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";
750
835
  return {
751
836
  source,
752
837
  currentHeadSha: input.currentHeadSha,
@@ -767,17 +852,48 @@ function isGreptileCheckDetail(check) {
767
852
  return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
768
853
  }
769
854
  async function collectGreptileCheckDetails(input) {
770
- const checkRunsRead = await runJsonArray(input.command, [
855
+ const checkRunsRead = await runJsonObject(input.command, [
771
856
  "api",
772
857
  `repos/${input.repoName}/commits/${input.headSha}/check-runs`,
773
- "--paginate",
774
- "--slurp",
775
- "--jq",
776
- "map(.check_runs // []) | add // []"
858
+ "-F",
859
+ "per_page=100"
777
860
  ], input.projectRoot);
778
- const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
861
+ const checkRuns = arrayField(checkRunsRead.value, "check_runs").map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
779
862
  return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
780
863
  }
864
+ async function collectPullRequestProvenance(input) {
865
+ const response = await runJsonObject(input.command, [
866
+ "api",
867
+ "graphql",
868
+ "-F",
869
+ `owner=${input.owner}`,
870
+ "-F",
871
+ `name=${input.name}`,
872
+ "-F",
873
+ `prNumber=${input.prNumber}`,
874
+ "-f",
875
+ "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 } } } } } }"
876
+ ], input.projectRoot);
877
+ if (response.error)
878
+ return { value: {}, error: response.error };
879
+ const data = response.value.data;
880
+ const repository = data?.repository;
881
+ const pullRequest = repository?.pullRequest;
882
+ if (!pullRequest)
883
+ return { value: {}, error: "GitHub pullRequest provenance response did not include a pullRequest object" };
884
+ const editor = pullRequest.editor;
885
+ const commits = pullRequest.commits;
886
+ const nodes = Array.isArray(commits?.nodes) ? commits.nodes : [];
887
+ const latestCommitNode = nodes[nodes.length - 1];
888
+ const latestCommit = latestCommitNode?.commit;
889
+ return {
890
+ value: {
891
+ bodyEditorLogin: typeof editor?.login === "string" ? editor.login : null,
892
+ bodyLastEditedAt: typeof pullRequest.lastEditedAt === "string" ? pullRequest.lastEditedAt : null,
893
+ headCommittedDate: typeof latestCommit?.committedDate === "string" ? latestCommit.committedDate : null
894
+ }
895
+ };
896
+ }
781
897
  async function collectReviewThreads(input) {
782
898
  const reviewThreads = [];
783
899
  let afterCursor = null;
@@ -855,11 +971,19 @@ async function collectPrReviewEvidence(input) {
855
971
  const baseRefName = firstString(view, ["baseRefName"]);
856
972
  const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
857
973
  const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
858
- const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
974
+ const provenanceRead = await collectPullRequestProvenance({
975
+ command: input.command,
976
+ projectRoot: input.projectRoot,
977
+ owner: parsed.owner,
978
+ name: parsed.repo,
979
+ prNumber: parsed.prNumber
980
+ });
981
+ const provenance = provenanceRead.value;
982
+ const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
859
983
  if (reviewCommentsRead.error)
860
984
  readErrors.push(reviewCommentsRead.error);
861
985
  const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
862
- const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
986
+ const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
863
987
  if (issueCommentsRead.error)
864
988
  readErrors.push(issueCommentsRead.error);
865
989
  const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
@@ -882,12 +1006,7 @@ async function collectPrReviewEvidence(input) {
882
1006
  repoName: parsed.repoName,
883
1007
  headSha
884
1008
  });
885
- if (checkDetailsRead.error)
886
- readErrors.push(checkDetailsRead.error);
887
1009
  greptileCheckDetails = checkDetailsRead.value;
888
- if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
889
- readErrors.push("Greptile check details could not be found for the current PR head");
890
- }
891
1010
  }
892
1011
  const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
893
1012
  const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
@@ -906,6 +1025,9 @@ async function collectPrReviewEvidence(input) {
906
1025
  const evidenceBase = {
907
1026
  title: firstString(view, ["title"]),
908
1027
  body: firstString(view, ["body"]),
1028
+ bodyEditorLogin: provenance.bodyEditorLogin ?? null,
1029
+ bodyLastEditedAt: provenance.bodyLastEditedAt ?? null,
1030
+ headCommittedDate: provenance.headCommittedDate ?? null,
909
1031
  reviews,
910
1032
  changedFileReviewComments: reviewComments,
911
1033
  relevantIssueComments: issueComments,
@@ -921,6 +1043,9 @@ async function collectPrReviewEvidence(input) {
921
1043
  repoName: parsed.repoName,
922
1044
  title: evidenceBase.title,
923
1045
  body: evidenceBase.body,
1046
+ bodyEditorLogin: evidenceBase.bodyEditorLogin,
1047
+ bodyLastEditedAt: evidenceBase.bodyLastEditedAt,
1048
+ headCommittedDate: evidenceBase.headCommittedDate,
924
1049
  headSha,
925
1050
  headRefName: firstString(view, ["headRefName"]),
926
1051
  baseRefName,
@@ -3090,7 +3090,7 @@ async function ensureLocalControlBinaries(projectRoot, sourceRoot = resolveLocal
3090
3090
  sourcePath: target.sourcePath,
3091
3091
  outputPath: target.outputPath,
3092
3092
  cwd: sourceRoot,
3093
- define: target.entrypoint === "packages/runtime/bin/rig-agent-dispatch.ts" ? secretDefines : undefined
3093
+ define: target.entrypoint === "packages/runtime/bin/rig-agent-dispatch.ts" ? { ...secretDefines, RIG_SOURCE_ROOT: sourceRoot } : undefined
3094
3094
  });
3095
3095
  } catch (error) {
3096
3096
  throw new RemoteCliError("RIG_RUN_BINARY_BUILD_FAILED", `Failed to compile ${target.entrypoint}: ${error instanceof Error ? error.message : "unknown error"}`, 2);
@@ -3759,6 +3759,57 @@ function flattenPaginatedArray(value) {
3759
3759
  }
3760
3760
  return value;
3761
3761
  }
3762
+ function parseConcatenatedJsonValues(value) {
3763
+ const text = value.trim();
3764
+ const docs = [];
3765
+ let start = null;
3766
+ let depth = 0;
3767
+ let inString = false;
3768
+ let escape = false;
3769
+ for (let index = 0;index < text.length; index += 1) {
3770
+ const char = text[index];
3771
+ if (start === null) {
3772
+ if (/\s/.test(char))
3773
+ continue;
3774
+ start = index;
3775
+ }
3776
+ if (inString) {
3777
+ if (escape) {
3778
+ escape = false;
3779
+ } else if (char === "\\") {
3780
+ escape = true;
3781
+ } else if (char === '"') {
3782
+ inString = false;
3783
+ }
3784
+ continue;
3785
+ }
3786
+ if (char === '"') {
3787
+ inString = true;
3788
+ continue;
3789
+ }
3790
+ if (char === "{" || char === "[") {
3791
+ depth += 1;
3792
+ continue;
3793
+ }
3794
+ if (char === "}" || char === "]") {
3795
+ depth -= 1;
3796
+ if (depth < 0)
3797
+ return { value: docs, error: "unexpected JSON close delimiter" };
3798
+ if (depth === 0 && start !== null) {
3799
+ const segment = text.slice(start, index + 1);
3800
+ try {
3801
+ docs.push(JSON.parse(segment));
3802
+ } catch (error) {
3803
+ return { value: docs, error: error instanceof Error ? error.message : String(error) };
3804
+ }
3805
+ start = null;
3806
+ }
3807
+ }
3808
+ }
3809
+ if (inString || depth !== 0 || start !== null)
3810
+ return { value: docs, error: "incomplete JSON stream" };
3811
+ return { value: docs };
3812
+ }
3762
3813
  function parseJsonArray(value) {
3763
3814
  if (!value?.trim())
3764
3815
  return { value: [], error: "empty JSON output" };
@@ -3767,7 +3818,11 @@ function parseJsonArray(value) {
3767
3818
  const flattened = flattenPaginatedArray(parsed);
3768
3819
  return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
3769
3820
  } catch (error) {
3770
- return { value: [], error: error instanceof Error ? error.message : String(error) };
3821
+ const streamed = parseConcatenatedJsonValues(value);
3822
+ if (streamed.error)
3823
+ return { value: [], error: error instanceof Error ? error.message : String(error) };
3824
+ const flattened = streamed.value.flatMap((entry) => flattenPaginatedArray(entry) ?? []);
3825
+ return flattened.length > 0 || streamed.value.length === 0 ? { value: flattened } : { value: [], error: "JSON output was not an array" };
3771
3826
  }
3772
3827
  }
3773
3828
  function parseGithubPrUrl(prUrl) {
@@ -3856,6 +3911,24 @@ function isStrictFiveOfFive(score) {
3856
3911
  function containsConflictingScoreText(input) {
3857
3912
  return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
3858
3913
  }
3914
+ function extractGreptileCommentBlock(input) {
3915
+ const match = input.match(/<!--\s*greptile_comment\s*-->([\s\S]*?)<!--\s*\/greptile_comment\s*-->/i);
3916
+ return match?.[1]?.trim() ?? null;
3917
+ }
3918
+ function extractGreptileBodyReviewedSha(input) {
3919
+ const block = extractGreptileCommentBlock(input);
3920
+ if (!block)
3921
+ return null;
3922
+ const commitLink = block.match(/\/commit\/([0-9a-f]{40})(?:\b|[^0-9a-f])/i);
3923
+ return commitLink?.[1]?.toLowerCase() ?? null;
3924
+ }
3925
+ function isoAtOrAfter(value, floor) {
3926
+ if (!value || !floor)
3927
+ return false;
3928
+ const valueMs = Date.parse(value);
3929
+ const floorMs = Date.parse(floor);
3930
+ return Number.isFinite(valueMs) && Number.isFinite(floorMs) && valueMs >= floorMs;
3931
+ }
3859
3932
  function greptileStatusVerdict(status) {
3860
3933
  const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
3861
3934
  if (!normalized)
@@ -4230,9 +4303,18 @@ function commentAuthorLogin(comment) {
4230
4303
  }
4231
4304
  function collectGreptileSignals(evidence) {
4232
4305
  const signals = [];
4306
+ const greptileBodyReviewedSha = extractGreptileBodyReviewedSha(evidence.body);
4307
+ const trustedGreptileBody = Boolean(greptileBodyReviewedSha && isGreptileGithubLogin(evidence.bodyEditorLogin) && isoAtOrAfter(evidence.bodyLastEditedAt, evidence.headCommittedDate));
4233
4308
  const contextSources = [
4234
4309
  { source: "pr-title", body: evidence.title ?? "" },
4235
- { source: "pr-body", body: evidence.body }
4310
+ {
4311
+ source: "pr-body",
4312
+ body: evidence.body,
4313
+ trusted: trustedGreptileBody,
4314
+ authorLogin: trustedGreptileBody ? evidence.bodyEditorLogin ?? null : null,
4315
+ reviewedSha: greptileBodyReviewedSha,
4316
+ verdict: trustedGreptileBody ? "completed" : null
4317
+ }
4236
4318
  ];
4237
4319
  for (const context of contextSources) {
4238
4320
  if (!context.body.trim())
@@ -4244,7 +4326,10 @@ function collectGreptileSignals(evidence) {
4244
4326
  source: context.source,
4245
4327
  body: context.body,
4246
4328
  currentHeadSha: evidence.currentHeadSha,
4247
- trusted: false,
4329
+ trusted: context.trusted === true,
4330
+ authorLogin: context.authorLogin,
4331
+ reviewedSha: context.reviewedSha,
4332
+ verdict: context.verdict,
4248
4333
  blocker: contextBlocker,
4249
4334
  actionable: contextBlocker
4250
4335
  }));
@@ -4440,7 +4525,7 @@ function deriveGreptileEvidence(input) {
4440
4525
  const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
4441
4526
  const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
4442
4527
  const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
4443
- 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";
4528
+ 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";
4444
4529
  return {
4445
4530
  source,
4446
4531
  currentHeadSha: input.currentHeadSha,
@@ -4461,17 +4546,48 @@ function isGreptileCheckDetail(check) {
4461
4546
  return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
4462
4547
  }
4463
4548
  async function collectGreptileCheckDetails(input) {
4464
- const checkRunsRead = await runJsonArray(input.command, [
4549
+ const checkRunsRead = await runJsonObject(input.command, [
4465
4550
  "api",
4466
4551
  `repos/${input.repoName}/commits/${input.headSha}/check-runs`,
4467
- "--paginate",
4468
- "--slurp",
4469
- "--jq",
4470
- "map(.check_runs // []) | add // []"
4552
+ "-F",
4553
+ "per_page=100"
4471
4554
  ], input.projectRoot);
4472
- const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
4555
+ const checkRuns = arrayField(checkRunsRead.value, "check_runs").map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
4473
4556
  return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
4474
4557
  }
4558
+ async function collectPullRequestProvenance(input) {
4559
+ const response = await runJsonObject(input.command, [
4560
+ "api",
4561
+ "graphql",
4562
+ "-F",
4563
+ `owner=${input.owner}`,
4564
+ "-F",
4565
+ `name=${input.name}`,
4566
+ "-F",
4567
+ `prNumber=${input.prNumber}`,
4568
+ "-f",
4569
+ "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 } } } } } }"
4570
+ ], input.projectRoot);
4571
+ if (response.error)
4572
+ return { value: {}, error: response.error };
4573
+ const data = response.value.data;
4574
+ const repository = data?.repository;
4575
+ const pullRequest = repository?.pullRequest;
4576
+ if (!pullRequest)
4577
+ return { value: {}, error: "GitHub pullRequest provenance response did not include a pullRequest object" };
4578
+ const editor = pullRequest.editor;
4579
+ const commits = pullRequest.commits;
4580
+ const nodes = Array.isArray(commits?.nodes) ? commits.nodes : [];
4581
+ const latestCommitNode = nodes[nodes.length - 1];
4582
+ const latestCommit = latestCommitNode?.commit;
4583
+ return {
4584
+ value: {
4585
+ bodyEditorLogin: typeof editor?.login === "string" ? editor.login : null,
4586
+ bodyLastEditedAt: typeof pullRequest.lastEditedAt === "string" ? pullRequest.lastEditedAt : null,
4587
+ headCommittedDate: typeof latestCommit?.committedDate === "string" ? latestCommit.committedDate : null
4588
+ }
4589
+ };
4590
+ }
4475
4591
  async function collectReviewThreads(input) {
4476
4592
  const reviewThreads = [];
4477
4593
  let afterCursor = null;
@@ -4549,11 +4665,19 @@ async function collectPrReviewEvidence(input) {
4549
4665
  const baseRefName = firstString(view, ["baseRefName"]);
4550
4666
  const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
4551
4667
  const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
4552
- const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
4668
+ const provenanceRead = await collectPullRequestProvenance({
4669
+ command: input.command,
4670
+ projectRoot: input.projectRoot,
4671
+ owner: parsed.owner,
4672
+ name: parsed.repo,
4673
+ prNumber: parsed.prNumber
4674
+ });
4675
+ const provenance = provenanceRead.value;
4676
+ const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
4553
4677
  if (reviewCommentsRead.error)
4554
4678
  readErrors.push(reviewCommentsRead.error);
4555
4679
  const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
4556
- const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
4680
+ const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
4557
4681
  if (issueCommentsRead.error)
4558
4682
  readErrors.push(issueCommentsRead.error);
4559
4683
  const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
@@ -4576,12 +4700,7 @@ async function collectPrReviewEvidence(input) {
4576
4700
  repoName: parsed.repoName,
4577
4701
  headSha
4578
4702
  });
4579
- if (checkDetailsRead.error)
4580
- readErrors.push(checkDetailsRead.error);
4581
4703
  greptileCheckDetails = checkDetailsRead.value;
4582
- if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
4583
- readErrors.push("Greptile check details could not be found for the current PR head");
4584
- }
4585
4704
  }
4586
4705
  const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
4587
4706
  const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
@@ -4600,6 +4719,9 @@ async function collectPrReviewEvidence(input) {
4600
4719
  const evidenceBase = {
4601
4720
  title: firstString(view, ["title"]),
4602
4721
  body: firstString(view, ["body"]),
4722
+ bodyEditorLogin: provenance.bodyEditorLogin ?? null,
4723
+ bodyLastEditedAt: provenance.bodyLastEditedAt ?? null,
4724
+ headCommittedDate: provenance.headCommittedDate ?? null,
4603
4725
  reviews,
4604
4726
  changedFileReviewComments: reviewComments,
4605
4727
  relevantIssueComments: issueComments,
@@ -4615,6 +4737,9 @@ async function collectPrReviewEvidence(input) {
4615
4737
  repoName: parsed.repoName,
4616
4738
  title: evidenceBase.title,
4617
4739
  body: evidenceBase.body,
4740
+ bodyEditorLogin: evidenceBase.bodyEditorLogin,
4741
+ bodyLastEditedAt: evidenceBase.bodyLastEditedAt,
4742
+ headCommittedDate: evidenceBase.headCommittedDate,
4618
4743
  headSha,
4619
4744
  headRefName: firstString(view, ["headRefName"]),
4620
4745
  baseRefName,