@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.
- package/dist/bin/rig-agent-dispatch.js +333 -23
- package/dist/src/control-plane/agent-wrapper.js +336 -23
- package/dist/src/control-plane/harness-main.js +142 -17
- package/dist/src/control-plane/hooks/completion-verification.js +142 -17
- package/dist/src/control-plane/native/harness-cli.js +142 -17
- package/dist/src/control-plane/native/pr-automation.js +142 -17
- package/dist/src/control-plane/native/pr-review-gate.js +142 -17
- package/dist/src/control-plane/native/run-ops.js +1 -1
- package/dist/src/control-plane/native/task-ops.js +142 -17
- package/dist/src/control-plane/native/verifier.js +142 -17
- package/dist/src/control-plane/pi-sessiond/bin.js +793 -0
- package/dist/src/control-plane/pi-sessiond/client.js +41 -0
- package/dist/src/control-plane/pi-sessiond/event-hub.js +59 -0
- package/dist/src/control-plane/pi-sessiond/extension-ui-context.js +198 -0
- package/dist/src/control-plane/pi-sessiond/launcher.js +163 -0
- package/dist/src/control-plane/pi-sessiond/server.js +802 -0
- package/dist/src/control-plane/pi-sessiond/session-service.js +540 -0
- package/dist/src/control-plane/pi-sessiond/types.js +1 -0
- package/dist/src/control-plane/runtime/index.js +17 -0
- package/dist/src/control-plane/runtime/isolation/home.js +17 -0
- package/dist/src/control-plane/runtime/isolation/index.js +17 -0
- package/dist/src/control-plane/runtime/isolation/runner.js +17 -0
- package/dist/src/control-plane/runtime/isolation.js +17 -0
- package/dist/src/control-plane/runtime/queue.js +17 -0
- package/package.json +7 -7
|
@@ -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
|
-
|
|
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
|
-
{
|
|
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:
|
|
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
|
|
4415
|
+
const checkRunsRead = await runJsonObject(input.command, [
|
|
4331
4416
|
"api",
|
|
4332
4417
|
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
4333
|
-
"
|
|
4334
|
-
"
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
-
{
|
|
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:
|
|
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
|
|
852
|
+
const checkRunsRead = await runJsonObject(input.command, [
|
|
768
853
|
"api",
|
|
769
854
|
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
770
|
-
"
|
|
771
|
-
"
|
|
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
|
|
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"
|
|
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,
|