@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
@@ -3625,6 +3625,57 @@ function flattenPaginatedArray(value) {
3625
3625
  }
3626
3626
  return value;
3627
3627
  }
3628
+ function parseConcatenatedJsonValues(value) {
3629
+ const text = value.trim();
3630
+ const docs = [];
3631
+ let start = null;
3632
+ let depth = 0;
3633
+ let inString = false;
3634
+ let escape = false;
3635
+ for (let index = 0;index < text.length; index += 1) {
3636
+ const char = text[index];
3637
+ if (start === null) {
3638
+ if (/\s/.test(char))
3639
+ continue;
3640
+ start = index;
3641
+ }
3642
+ if (inString) {
3643
+ if (escape) {
3644
+ escape = false;
3645
+ } else if (char === "\\") {
3646
+ escape = true;
3647
+ } else if (char === '"') {
3648
+ inString = false;
3649
+ }
3650
+ continue;
3651
+ }
3652
+ if (char === '"') {
3653
+ inString = true;
3654
+ continue;
3655
+ }
3656
+ if (char === "{" || char === "[") {
3657
+ depth += 1;
3658
+ continue;
3659
+ }
3660
+ if (char === "}" || char === "]") {
3661
+ depth -= 1;
3662
+ if (depth < 0)
3663
+ return { value: docs, error: "unexpected JSON close delimiter" };
3664
+ if (depth === 0 && start !== null) {
3665
+ const segment = text.slice(start, index + 1);
3666
+ try {
3667
+ docs.push(JSON.parse(segment));
3668
+ } catch (error) {
3669
+ return { value: docs, error: error instanceof Error ? error.message : String(error) };
3670
+ }
3671
+ start = null;
3672
+ }
3673
+ }
3674
+ }
3675
+ if (inString || depth !== 0 || start !== null)
3676
+ return { value: docs, error: "incomplete JSON stream" };
3677
+ return { value: docs };
3678
+ }
3628
3679
  function parseJsonArray(value) {
3629
3680
  if (!value?.trim())
3630
3681
  return { value: [], error: "empty JSON output" };
@@ -3633,7 +3684,11 @@ function parseJsonArray(value) {
3633
3684
  const flattened = flattenPaginatedArray(parsed);
3634
3685
  return flattened ? { value: flattened } : { value: [], error: "JSON output was not an array" };
3635
3686
  } catch (error) {
3636
- return { value: [], error: error instanceof Error ? error.message : String(error) };
3687
+ const streamed = parseConcatenatedJsonValues(value);
3688
+ if (streamed.error)
3689
+ return { value: [], error: error instanceof Error ? error.message : String(error) };
3690
+ const flattened = streamed.value.flatMap((entry) => flattenPaginatedArray(entry) ?? []);
3691
+ return flattened.length > 0 || streamed.value.length === 0 ? { value: flattened } : { value: [], error: "JSON output was not an array" };
3637
3692
  }
3638
3693
  }
3639
3694
  function parseGithubPrUrl(prUrl) {
@@ -3722,6 +3777,24 @@ function isStrictFiveOfFive(score) {
3722
3777
  function containsConflictingScoreText(input) {
3723
3778
  return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
3724
3779
  }
3780
+ function extractGreptileCommentBlock(input) {
3781
+ const match = input.match(/<!--\s*greptile_comment\s*-->([\s\S]*?)<!--\s*\/greptile_comment\s*-->/i);
3782
+ return match?.[1]?.trim() ?? null;
3783
+ }
3784
+ function extractGreptileBodyReviewedSha(input) {
3785
+ const block = extractGreptileCommentBlock(input);
3786
+ if (!block)
3787
+ return null;
3788
+ const commitLink = block.match(/\/commit\/([0-9a-f]{40})(?:\b|[^0-9a-f])/i);
3789
+ return commitLink?.[1]?.toLowerCase() ?? null;
3790
+ }
3791
+ function isoAtOrAfter(value, floor) {
3792
+ if (!value || !floor)
3793
+ return false;
3794
+ const valueMs = Date.parse(value);
3795
+ const floorMs = Date.parse(floor);
3796
+ return Number.isFinite(valueMs) && Number.isFinite(floorMs) && valueMs >= floorMs;
3797
+ }
3725
3798
  function greptileStatusVerdict(status) {
3726
3799
  const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
3727
3800
  if (!normalized)
@@ -4096,9 +4169,18 @@ function commentAuthorLogin(comment) {
4096
4169
  }
4097
4170
  function collectGreptileSignals(evidence) {
4098
4171
  const signals = [];
4172
+ const greptileBodyReviewedSha = extractGreptileBodyReviewedSha(evidence.body);
4173
+ const trustedGreptileBody = Boolean(greptileBodyReviewedSha && isGreptileGithubLogin(evidence.bodyEditorLogin) && isoAtOrAfter(evidence.bodyLastEditedAt, evidence.headCommittedDate));
4099
4174
  const contextSources = [
4100
4175
  { source: "pr-title", body: evidence.title ?? "" },
4101
- { source: "pr-body", body: evidence.body }
4176
+ {
4177
+ source: "pr-body",
4178
+ body: evidence.body,
4179
+ trusted: trustedGreptileBody,
4180
+ authorLogin: trustedGreptileBody ? evidence.bodyEditorLogin ?? null : null,
4181
+ reviewedSha: greptileBodyReviewedSha,
4182
+ verdict: trustedGreptileBody ? "completed" : null
4183
+ }
4102
4184
  ];
4103
4185
  for (const context of contextSources) {
4104
4186
  if (!context.body.trim())
@@ -4110,7 +4192,10 @@ function collectGreptileSignals(evidence) {
4110
4192
  source: context.source,
4111
4193
  body: context.body,
4112
4194
  currentHeadSha: evidence.currentHeadSha,
4113
- trusted: false,
4195
+ trusted: context.trusted === true,
4196
+ authorLogin: context.authorLogin,
4197
+ reviewedSha: context.reviewedSha,
4198
+ verdict: context.verdict,
4114
4199
  blocker: contextBlocker,
4115
4200
  actionable: contextBlocker
4116
4201
  }));
@@ -4306,7 +4391,7 @@ function deriveGreptileEvidence(input) {
4306
4391
  const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
4307
4392
  const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
4308
4393
  const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
4309
- 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";
4394
+ 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";
4310
4395
  return {
4311
4396
  source,
4312
4397
  currentHeadSha: input.currentHeadSha,
@@ -4327,17 +4412,48 @@ function isGreptileCheckDetail(check) {
4327
4412
  return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
4328
4413
  }
4329
4414
  async function collectGreptileCheckDetails(input) {
4330
- const checkRunsRead = await runJsonArray(input.command, [
4415
+ const checkRunsRead = await runJsonObject(input.command, [
4331
4416
  "api",
4332
4417
  `repos/${input.repoName}/commits/${input.headSha}/check-runs`,
4333
- "--paginate",
4334
- "--slurp",
4335
- "--jq",
4336
- "map(.check_runs // []) | add // []"
4418
+ "-F",
4419
+ "per_page=100"
4337
4420
  ], input.projectRoot);
4338
- const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
4421
+ const checkRuns = arrayField(checkRunsRead.value, "check_runs").map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
4339
4422
  return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
4340
4423
  }
4424
+ async function collectPullRequestProvenance(input) {
4425
+ const response = await runJsonObject(input.command, [
4426
+ "api",
4427
+ "graphql",
4428
+ "-F",
4429
+ `owner=${input.owner}`,
4430
+ "-F",
4431
+ `name=${input.name}`,
4432
+ "-F",
4433
+ `prNumber=${input.prNumber}`,
4434
+ "-f",
4435
+ "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 } } } } } }"
4436
+ ], input.projectRoot);
4437
+ if (response.error)
4438
+ return { value: {}, error: response.error };
4439
+ const data = response.value.data;
4440
+ const repository = data?.repository;
4441
+ const pullRequest = repository?.pullRequest;
4442
+ if (!pullRequest)
4443
+ return { value: {}, error: "GitHub pullRequest provenance response did not include a pullRequest object" };
4444
+ const editor = pullRequest.editor;
4445
+ const commits = pullRequest.commits;
4446
+ const nodes = Array.isArray(commits?.nodes) ? commits.nodes : [];
4447
+ const latestCommitNode = nodes[nodes.length - 1];
4448
+ const latestCommit = latestCommitNode?.commit;
4449
+ return {
4450
+ value: {
4451
+ bodyEditorLogin: typeof editor?.login === "string" ? editor.login : null,
4452
+ bodyLastEditedAt: typeof pullRequest.lastEditedAt === "string" ? pullRequest.lastEditedAt : null,
4453
+ headCommittedDate: typeof latestCommit?.committedDate === "string" ? latestCommit.committedDate : null
4454
+ }
4455
+ };
4456
+ }
4341
4457
  async function collectReviewThreads(input) {
4342
4458
  const reviewThreads = [];
4343
4459
  let afterCursor = null;
@@ -4415,11 +4531,19 @@ async function collectPrReviewEvidence(input) {
4415
4531
  const baseRefName = firstString(view, ["baseRefName"]);
4416
4532
  const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
4417
4533
  const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
4418
- const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
4534
+ const provenanceRead = await collectPullRequestProvenance({
4535
+ command: input.command,
4536
+ projectRoot: input.projectRoot,
4537
+ owner: parsed.owner,
4538
+ name: parsed.repo,
4539
+ prNumber: parsed.prNumber
4540
+ });
4541
+ const provenance = provenanceRead.value;
4542
+ const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
4419
4543
  if (reviewCommentsRead.error)
4420
4544
  readErrors.push(reviewCommentsRead.error);
4421
4545
  const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
4422
- const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
4546
+ const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
4423
4547
  if (issueCommentsRead.error)
4424
4548
  readErrors.push(issueCommentsRead.error);
4425
4549
  const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
@@ -4442,12 +4566,7 @@ async function collectPrReviewEvidence(input) {
4442
4566
  repoName: parsed.repoName,
4443
4567
  headSha
4444
4568
  });
4445
- if (checkDetailsRead.error)
4446
- readErrors.push(checkDetailsRead.error);
4447
4569
  greptileCheckDetails = checkDetailsRead.value;
4448
- if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
4449
- readErrors.push("Greptile check details could not be found for the current PR head");
4450
- }
4451
4570
  }
4452
4571
  const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
4453
4572
  const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
@@ -4466,6 +4585,9 @@ async function collectPrReviewEvidence(input) {
4466
4585
  const evidenceBase = {
4467
4586
  title: firstString(view, ["title"]),
4468
4587
  body: firstString(view, ["body"]),
4588
+ bodyEditorLogin: provenance.bodyEditorLogin ?? null,
4589
+ bodyLastEditedAt: provenance.bodyLastEditedAt ?? null,
4590
+ headCommittedDate: provenance.headCommittedDate ?? null,
4469
4591
  reviews,
4470
4592
  changedFileReviewComments: reviewComments,
4471
4593
  relevantIssueComments: issueComments,
@@ -4481,6 +4603,9 @@ async function collectPrReviewEvidence(input) {
4481
4603
  repoName: parsed.repoName,
4482
4604
  title: evidenceBase.title,
4483
4605
  body: evidenceBase.body,
4606
+ bodyEditorLogin: evidenceBase.bodyEditorLogin,
4607
+ bodyLastEditedAt: evidenceBase.bodyLastEditedAt,
4608
+ headCommittedDate: evidenceBase.headCommittedDate,
4484
4609
  headSha,
4485
4610
  headRefName: firstString(view, ["headRefName"]),
4486
4611
  baseRefName,
@@ -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) {
@@ -159,6 +214,24 @@ function isStrictFiveOfFive(score) {
159
214
  function containsConflictingScoreText(input) {
160
215
  return parseGreptileScores(input).some((score) => !isStrictFiveOfFive(score));
161
216
  }
217
+ function extractGreptileCommentBlock(input) {
218
+ const match = input.match(/<!--\s*greptile_comment\s*-->([\s\S]*?)<!--\s*\/greptile_comment\s*-->/i);
219
+ return match?.[1]?.trim() ?? null;
220
+ }
221
+ function extractGreptileBodyReviewedSha(input) {
222
+ const block = extractGreptileCommentBlock(input);
223
+ if (!block)
224
+ return null;
225
+ const commitLink = block.match(/\/commit\/([0-9a-f]{40})(?:\b|[^0-9a-f])/i);
226
+ return commitLink?.[1]?.toLowerCase() ?? null;
227
+ }
228
+ function isoAtOrAfter(value, floor) {
229
+ if (!value || !floor)
230
+ return false;
231
+ const valueMs = Date.parse(value);
232
+ const floorMs = Date.parse(floor);
233
+ return Number.isFinite(valueMs) && Number.isFinite(floorMs) && valueMs >= floorMs;
234
+ }
162
235
  function greptileStatusVerdict(status) {
163
236
  const normalized = String(status ?? "").trim().toUpperCase().replace(/[\s-]+/g, "_");
164
237
  if (!normalized)
@@ -533,9 +606,18 @@ function commentAuthorLogin(comment) {
533
606
  }
534
607
  function collectGreptileSignals(evidence) {
535
608
  const signals = [];
609
+ const greptileBodyReviewedSha = extractGreptileBodyReviewedSha(evidence.body);
610
+ const trustedGreptileBody = Boolean(greptileBodyReviewedSha && isGreptileGithubLogin(evidence.bodyEditorLogin) && isoAtOrAfter(evidence.bodyLastEditedAt, evidence.headCommittedDate));
536
611
  const contextSources = [
537
612
  { source: "pr-title", body: evidence.title ?? "" },
538
- { source: "pr-body", body: evidence.body }
613
+ {
614
+ source: "pr-body",
615
+ body: evidence.body,
616
+ trusted: trustedGreptileBody,
617
+ authorLogin: trustedGreptileBody ? evidence.bodyEditorLogin ?? null : null,
618
+ reviewedSha: greptileBodyReviewedSha,
619
+ verdict: trustedGreptileBody ? "completed" : null
620
+ }
539
621
  ];
540
622
  for (const context of contextSources) {
541
623
  if (!context.body.trim())
@@ -547,7 +629,10 @@ function collectGreptileSignals(evidence) {
547
629
  source: context.source,
548
630
  body: context.body,
549
631
  currentHeadSha: evidence.currentHeadSha,
550
- trusted: false,
632
+ trusted: context.trusted === true,
633
+ authorLogin: context.authorLogin,
634
+ reviewedSha: context.reviewedSha,
635
+ verdict: context.verdict,
551
636
  blocker: contextBlocker,
552
637
  actionable: contextBlocker
553
638
  }));
@@ -743,7 +828,7 @@ function deriveGreptileEvidence(input) {
743
828
  const hasGreptileEvidence = trustedSignals.length > 0 || signals.some((signal) => /greptile/i.test(signal.body));
744
829
  const approved = fresh && completed && !blockers.length && !unresolvedComments.length && currentPendingApiSignals.length === 0 && (approvedByScore || approvedByExplicitMapping);
745
830
  const mapping = !hasGreptileEvidence ? "missing" : staleSignals.length > 0 && !approvingSignal ? "stale" : approvedByScore ? "score-5-of-5" : approvedByExplicitMapping ? "explicit-approved" : "unproven";
746
- 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";
831
+ 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";
747
832
  return {
748
833
  source,
749
834
  currentHeadSha: input.currentHeadSha,
@@ -764,17 +849,48 @@ function isGreptileCheckDetail(check) {
764
849
  return isGreptileLabel(checkName(check)) || isGreptileGithubLogin(check.app?.slug) || isGreptileGithubLogin(check.app?.owner?.login) || isGreptileLabel(check.app?.name);
765
850
  }
766
851
  async function collectGreptileCheckDetails(input) {
767
- const checkRunsRead = await runJsonArray(input.command, [
852
+ const checkRunsRead = await runJsonObject(input.command, [
768
853
  "api",
769
854
  `repos/${input.repoName}/commits/${input.headSha}/check-runs`,
770
- "--paginate",
771
- "--slurp",
772
- "--jq",
773
- "map(.check_runs // []) | add // []"
855
+ "-F",
856
+ "per_page=100"
774
857
  ], input.projectRoot);
775
- const checkRuns = checkRunsRead.value.map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
858
+ const checkRuns = arrayField(checkRunsRead.value, "check_runs").map(normalizeStatusCheck).filter((entry) => !!entry).filter(isGreptileCheckDetail);
776
859
  return checkRunsRead.error ? { value: checkRuns, error: checkRunsRead.error } : { value: checkRuns };
777
860
  }
861
+ async function collectPullRequestProvenance(input) {
862
+ const response = await runJsonObject(input.command, [
863
+ "api",
864
+ "graphql",
865
+ "-F",
866
+ `owner=${input.owner}`,
867
+ "-F",
868
+ `name=${input.name}`,
869
+ "-F",
870
+ `prNumber=${input.prNumber}`,
871
+ "-f",
872
+ "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 } } } } } }"
873
+ ], input.projectRoot);
874
+ if (response.error)
875
+ return { value: {}, error: response.error };
876
+ const data = response.value.data;
877
+ const repository = data?.repository;
878
+ const pullRequest = repository?.pullRequest;
879
+ if (!pullRequest)
880
+ return { value: {}, error: "GitHub pullRequest provenance response did not include a pullRequest object" };
881
+ const editor = pullRequest.editor;
882
+ const commits = pullRequest.commits;
883
+ const nodes = Array.isArray(commits?.nodes) ? commits.nodes : [];
884
+ const latestCommitNode = nodes[nodes.length - 1];
885
+ const latestCommit = latestCommitNode?.commit;
886
+ return {
887
+ value: {
888
+ bodyEditorLogin: typeof editor?.login === "string" ? editor.login : null,
889
+ bodyLastEditedAt: typeof pullRequest.lastEditedAt === "string" ? pullRequest.lastEditedAt : null,
890
+ headCommittedDate: typeof latestCommit?.committedDate === "string" ? latestCommit.committedDate : null
891
+ }
892
+ };
893
+ }
778
894
  async function collectReviewThreads(input) {
779
895
  const reviewThreads = [];
780
896
  let afterCursor = null;
@@ -852,11 +968,19 @@ async function collectPrReviewEvidence(input) {
852
968
  const baseRefName = firstString(view, ["baseRefName"]);
853
969
  const statusCheckRollup = arrayField(view, "statusCheckRollup").map(normalizeStatusCheck).filter((entry) => !!entry);
854
970
  const reviews = arrayField(view, "reviews").map(normalizeReview).filter((entry) => !!entry);
855
- const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
971
+ const provenanceRead = await collectPullRequestProvenance({
972
+ command: input.command,
973
+ projectRoot: input.projectRoot,
974
+ owner: parsed.owner,
975
+ name: parsed.repo,
976
+ prNumber: parsed.prNumber
977
+ });
978
+ const provenance = provenanceRead.value;
979
+ const reviewCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/pulls/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
856
980
  if (reviewCommentsRead.error)
857
981
  readErrors.push(reviewCommentsRead.error);
858
982
  const reviewComments = reviewCommentsRead.value.map(normalizeReviewComment).filter((entry) => !!entry);
859
- const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate", "--slurp"], input.projectRoot);
983
+ const issueCommentsRead = await runJsonArray(input.command, ["api", `repos/${parsed.repoName}/issues/${parsed.prNumber}/comments`, "--paginate"], input.projectRoot);
860
984
  if (issueCommentsRead.error)
861
985
  readErrors.push(issueCommentsRead.error);
862
986
  const issueComments = issueCommentsRead.value.map(normalizeIssueComment).filter((entry) => !!entry).filter(relevantIssueComment);
@@ -879,12 +1003,7 @@ async function collectPrReviewEvidence(input) {
879
1003
  repoName: parsed.repoName,
880
1004
  headSha
881
1005
  });
882
- if (checkDetailsRead.error)
883
- readErrors.push(checkDetailsRead.error);
884
1006
  greptileCheckDetails = checkDetailsRead.value;
885
- if (!checkDetailsRead.error && greptileCheckDetails.length === 0) {
886
- readErrors.push("Greptile check details could not be found for the current PR head");
887
- }
888
1007
  }
889
1008
  const checksWithGreptileDetails = [...statusCheckRollup, ...greptileCheckDetails];
890
1009
  const shouldCollectConfiguredGreptileApi = input.greptileApi?.enabled !== false;
@@ -903,6 +1022,9 @@ async function collectPrReviewEvidence(input) {
903
1022
  const evidenceBase = {
904
1023
  title: firstString(view, ["title"]),
905
1024
  body: firstString(view, ["body"]),
1025
+ bodyEditorLogin: provenance.bodyEditorLogin ?? null,
1026
+ bodyLastEditedAt: provenance.bodyLastEditedAt ?? null,
1027
+ headCommittedDate: provenance.headCommittedDate ?? null,
906
1028
  reviews,
907
1029
  changedFileReviewComments: reviewComments,
908
1030
  relevantIssueComments: issueComments,
@@ -918,6 +1040,9 @@ async function collectPrReviewEvidence(input) {
918
1040
  repoName: parsed.repoName,
919
1041
  title: evidenceBase.title,
920
1042
  body: evidenceBase.body,
1043
+ bodyEditorLogin: evidenceBase.bodyEditorLogin,
1044
+ bodyLastEditedAt: evidenceBase.bodyLastEditedAt,
1045
+ headCommittedDate: evidenceBase.headCommittedDate,
921
1046
  headSha,
922
1047
  headRefName: firstString(view, ["headRefName"]),
923
1048
  baseRefName,