@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
|
@@ -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) {
|
|
@@ -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
|
-
{
|
|
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:
|
|
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
|
|
855
|
+
const checkRunsRead = await runJsonObject(input.command, [
|
|
771
856
|
"api",
|
|
772
857
|
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
773
|
-
"
|
|
774
|
-
"
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
-
{
|
|
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:
|
|
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
|
|
4549
|
+
const checkRunsRead = await runJsonObject(input.command, [
|
|
4465
4550
|
"api",
|
|
4466
4551
|
`repos/${input.repoName}/commits/${input.headSha}/check-runs`,
|
|
4467
|
-
"
|
|
4468
|
-
"
|
|
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
|
|
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"
|
|
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,
|