Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 164 additions & 26 deletions src/clawsweeper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5748,13 +5748,13 @@ function publicRealBehaviorProofLine(proof: RealBehaviorProof): string {
function publicPrRatingLine(rating: PrRating, proof: RealBehaviorProof): string {
const shiny = hasShinyProof(proof) ? " ✨ media proof bonus" : "";
const lines = [
`Overall: ${themedRatingName(rating.overallTier)}`,
`Proof: ${themedRatingName(rating.proofTier)}${shiny}`,
`Patch quality: ${themedRatingName(rating.patchTier)}`,
`Summary: ${sentence(rating.summary)}`,
`Overall ${themedRatingName(rating.overallTier)} with proof ${themedRatingName(
rating.proofTier,
)}${shiny} and patch quality ${themedRatingName(rating.patchTier)}.`,
sentence(rating.summary),
];
if (rating.nextSteps.length) {
lines.push("", "Rank-up moves:", ...rating.nextSteps.slice(0, 3).map((step) => `- ${step}`));
lines.push("", ...rating.nextSteps.slice(0, 3).map((step) => `- ${step}`));
}
lines.push(
"",
Expand Down Expand Up @@ -9121,8 +9121,37 @@ function collapsedDetailsBlock(summary: string, lines: readonly string[]): strin
return ["<details>", `<summary>${summary}</summary>`, "", body, "", "</details>"].join("\n");
}

function appendPublicSection(lines: string[], heading: string, body: string): void {
lines.push(`**${heading}**`, body, "");
type ReviewCommentBadge = "DONE" | "INFO" | "SKIP" | "P1" | "P2" | "P3";

function renderReviewCommentBadge(badge: ReviewCommentBadge): string {
const symbols: Record<ReviewCommentBadge, string> = {
DONE: "✅",
INFO: "ℹ️",
SKIP: "⏭️",
P1: "🚨",
P2: "💡",
P3: "🔎",
};
return `${symbols[badge]} **${badge}**`;
}

function appendPublicSection(
lines: string[],
heading: string,
body: string,
options: {
badge?: ReviewCommentBadge;
headline?: string;
metadata?: readonly string[];
} = {},
): void {
const badge = options.badge ?? "INFO";
const headline = options.headline ?? heading;
const metadata = (options.metadata ?? []).filter(Boolean);
lines.push(`${renderReviewCommentBadge(badge)} **${heading}** **${headline}**`);
if (body.trim()) lines.push("", body.trim());
if (metadata.length) lines.push("", ...metadata);
lines.push("");
}

function publicReproducibilityLine(reproductionAssessment: string): string {
Expand All @@ -9141,6 +9170,67 @@ function publicSummaryBody(summaryLine: string, reproductionAssessment: string):
.join("\n\n");
}

function reviewStatusBadge({
hasRealBehaviorProofBlocker,
isRepairLoopPass,
isPullRequest,
isRepairCandidate,
hasReviewFindings,
}: {
hasRealBehaviorProofBlocker: boolean;
isRepairLoopPass: boolean;
isPullRequest: boolean;
isRepairCandidate: boolean;
hasReviewFindings: boolean;
}): ReviewCommentBadge {
if (hasRealBehaviorProofBlocker) return "P2";
if (isRepairLoopPass) return "DONE";
if (isPullRequest && isRepairCandidate) return "P2";
if (hasReviewFindings) return "P2";
if (isPullRequest) return "INFO";
return "INFO";
}

function reviewStatusHeadline({
hasRealBehaviorProofBlocker,
isRepairLoopPass,
isPullRequest,
isRepairCandidate,
hasReviewFindings,
}: {
hasRealBehaviorProofBlocker: boolean;
isRepairLoopPass: boolean;
isPullRequest: boolean;
isRepairCandidate: boolean;
hasReviewFindings: boolean;
}): string {
if (hasRealBehaviorProofBlocker) return "Needs real behavior proof before merge";
if (isRepairLoopPass) return "Passed review";
if (isPullRequest && isRepairCandidate) return "Needs changes before merge";
if (hasReviewFindings) return "Found issues before merge";
if (isPullRequest) return "Needs maintainer review before merge";
return "Keep open for maintainer follow-up";
}

function legacyReviewStatusLine(headline: string): string {
if (headline === "Passed review") return "Codex review: passed.";
if (headline === "Needs real behavior proof before merge") {
return "Codex review: needs real behavior proof before merge.";
}
if (headline === "Needs changes before merge") return "Codex review: needs changes before merge.";
if (headline === "Found issues before merge") return "Codex review: found issues before merge.";
if (headline === "Needs maintainer review before merge") {
return "Codex review: needs maintainer review before merge.";
}
return `Codex review: ${headline.toLowerCase()}.`;
}

function reviewedHeadFooter(markdown: string): string {
const sha = frontMatterValue(markdown, "pull_head_sha");
const reviewed = sha ? ` · reviewed against ${String(sha).slice(0, 12)}` : "";
return `_ClawSweeper 🐠${reviewed}._`;
}

function publicMergeRiskLine(
risks: string,
nextStepLine: string,
Expand Down Expand Up @@ -9342,18 +9432,18 @@ function renderKeepOpenCommentFromReport(
: "";
const details: string[] = [];
const hasReviewFindings = isPullRequest && reviewFindings.length > 0;
const statusContext = {
hasRealBehaviorProofBlocker,
isRepairLoopPass,
isPullRequest,
isRepairCandidate,
hasReviewFindings,
};
const lines = [
hasRealBehaviorProofBlocker
? "Codex review: needs real behavior proof before merge."
: isRepairLoopPass
? "Codex review: passed."
: isPullRequest && isRepairCandidate
? "Codex review: needs changes before merge."
: hasReviewFindings
? "Codex review: found issues before merge."
: isPullRequest
? "Codex review: needs maintainer review before merge."
: "Codex review: keeping this open for maintainer follow-up; there is still a little grit to resolve.",
`${renderReviewCommentBadge(reviewStatusBadge(statusContext))} **Codex review** **${reviewStatusHeadline(
statusContext,
)}**`,
`<!-- ${legacyReviewStatusLine(reviewStatusHeadline(statusContext))} -->`,
"",
...reviewWorkflowCallout(),
];
Expand All @@ -9362,38 +9452,85 @@ function renderKeepOpenCommentFromReport(
lines,
"Summary",
publicSummaryBody(changeSummaryLine, reproductionAssessment),
{ badge: "INFO", headline: "Review the changed behavior" },
);
} else {
appendPublicSection(lines, "Summary", publicSummaryBody(summaryLine, reproductionAssessment));
appendPublicSection(lines, "Summary", publicSummaryBody(summaryLine, reproductionAssessment), {
badge: "INFO",
headline: "Review the reported behavior",
});
}
if (!isPullRequest) {
const reproductionHelp = issueReproductionHelpSuggestions(markdown);
if (reproductionHelp.length) {
appendPublicSection(
lines,
"Ways to help us reproduce this",
reproductionHelp.map((suggestion) => `- ${suggestion}`).join("\n"),
"The report is still missing enough real-world detail to reproduce the issue confidently.",
{
badge: "P3",
headline: "Add concrete reproduction evidence",
metadata: reproductionHelp.map((suggestion) => `- ${suggestion}`),
},
);
}
}
if (isPullRequest) {
appendPublicSection(lines, "PR rating", publicPrRatingLine(prRating, realBehaviorProof));
appendPublicSection(lines, "PR rating", publicPrRatingLine(prRating, realBehaviorProof), {
badge: isRepairLoopPass ? "DONE" : "INFO",
headline: "Rate readiness from proof and patch quality",
});
appendPublicSection(
lines,
"Real behavior proof",
publicRealBehaviorProofLine(realBehaviorProof),
{
badge: realBehaviorProofBlocksMerge(markdown) ? "P2" : "DONE",
headline: "Assess whether proof is merge-ready",
},
);
}
const mantisSuggestion = isPullRequest
? publicMantisRecommendationBlock(mantisRecommendation)
: "";
if (mantisSuggestion) appendPublicSection(lines, "Mantis proof suggestion", mantisSuggestion);
if (mergeRiskLine) appendPublicSection(lines, "Risk before merge", mergeRiskLine);
appendPublicSection(lines, isPullRequest ? "Next step before merge" : "Next step", nextStepLine);
if (mantisSuggestion) {
appendPublicSection(lines, "Mantis proof suggestion", mantisSuggestion, {
badge: "INFO",
headline: "Collect stronger validation evidence",
});
}
if (mergeRiskLine) {
appendPublicSection(lines, "Risk before merge", mergeRiskLine, {
badge: "P2",
headline: "Resolve the remaining merge risk",
});
}
appendPublicSection(lines, isPullRequest ? "Next step before merge" : "Next step", nextStepLine, {
badge: isRepairLoopPass ? "DONE" : isRepairCandidate ? "P2" : "INFO",
headline: isPullRequest ? "Take the next merge decision" : "Take the next triage step",
});
const securityLine = publicSecurityReviewLine(securityReview);
if (securityLine) appendPublicSection(lines, "Security", securityLine);
if (securityLine) {
appendPublicSection(lines, "Security", securityLine, {
badge: securityReview.status === "needs_attention" ? "P2" : "DONE",
headline:
securityReview.status === "needs_attention"
? "Confirm the security-sensitive change"
: "No security blocker found",
});
}
if (isPullRequest && reviewFindings.length) {
lines.push("**Review findings**", ...reviewFindings.slice(0, 3).map(reviewFindingSummaryLine));
const highestPriority = Math.min(...reviewFindings.map((finding) => finding.priority));
appendPublicSection(
lines,
"Review findings",
"ClawSweeper found focused review comments that should be addressed before merge.",
{
badge: highestPriority === 1 ? "P1" : highestPriority === 2 ? "P2" : "P3",
headline: "Address the highest-signal findings",
metadata: reviewFindings.slice(0, 3).map(reviewFindingSummaryLine),
},
);
}
if (bestSolutionLine && publicReviewTextDiffers(bestSolutionLine, nextStepLine)) {
details.push("Best possible solution:", "", bestSolutionLine);
Expand Down Expand Up @@ -9450,6 +9587,7 @@ function renderKeepOpenCommentFromReport(
if (reviewLine) details.push("", reviewLine);
const detailsBlock = collapsedDetailsBlock("Review details", details);
if (detailsBlock) lines.push("", detailsBlock);
lines.push("", reviewedHeadFooter(markdown));
return sanitizePublicSelfReferences(
lines.join("\n"),
Number(frontMatterValue(markdown, "number")),
Expand Down
2 changes: 1 addition & 1 deletion src/repair/comment-router-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1141,7 +1141,7 @@ function markdownSection(body: string, heading: string) {
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = String(body ?? "").match(
new RegExp(
`(?:^|\\n)\\*\\*${escaped}\\*\\*\\s*\\n([\\s\\S]*?)(?=\\n\\n\\*\\*|\\n<details|\\n<!--|$)`,
`(?:^|\\n)(?:\\*\\*${escaped}\\*\\*|(?:✅|ℹ️|⏭️|🚨|💡|🔎)\\s+\\*\\*(?:DONE|INFO|SKIP|P[123])\\*\\*\\s+\\*\\*${escaped}\\*\\*\\s+\\*\\*[^\\n*]+\\*\\*)\\s*\\n([\\s\\S]*?)(?=\\n\\n(?:\\*\\*|(?:✅|ℹ️|⏭️|🚨|💡|🔎)\\s+\\*\\*)|\\n<details|\\n<!--|$)`,
"i",
),
);
Expand Down
4 changes: 3 additions & 1 deletion src/repair/comment-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2575,7 +2575,9 @@ function extractMarkdownSection(body: JsonValue, heading: string): string | null
const pattern = new RegExp(
`(?:^|\\n)(?:\\*\\*${escapeRegExp(heading)}\\*\\*|${escapeRegExp(
heading,
)}:)\\s*\\n+([\\s\\S]*?)(?=\\n\\n(?:\\*\\*[^\\n*]{1,80}\\*\\*|[A-Z][^\\n:]{0,80}:\\n)|\\n<details>|\\n<!--|$)`,
)}:|(?:✅|ℹ️|⏭️|🚨|💡|🔎)\\s+\\*\\*(?:DONE|INFO|SKIP|P[123])\\*\\*\\s+\\*\\*${escapeRegExp(
heading,
)}\\*\\*\\s+\\*\\*[^\\n*]+\\*\\*)\\s*\\n+([\\s\\S]*?)(?=\\n\\n(?:(?:\\*\\*[^\\n*]{1,80}\\*\\*)|(?:(?:✅|ℹ️|⏭️|🚨|💡|🔎)\\s+\\*\\*)|[A-Z][^\\n:]{0,80}:\\n)|\\n<details>|\\n<!--|$)`,
"i",
);
return pattern.exec(text)?.[1]?.trim() || null;
Expand Down
46 changes: 32 additions & 14 deletions test/clawsweeper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2311,7 +2311,7 @@ Reason: Maintainers should review the tests after the targeted lane is green.
);
assert.match(
comment,
/\*\*Summary\*\*\nAdds regression coverage for session-scoped model overrides\./,
/ℹ️ \*\*INFO\*\* \*\*Summary\*\* \*\*Review the changed behavior\*\*\n\nAdds regression coverage for session-scoped model overrides\./,
);
assert.match(comment, /\*\*Next step before merge\*\*/);
assert.match(comment, /Maintainers should review the tests after the targeted lane is green\./);
Expand Down Expand Up @@ -2370,7 +2370,7 @@ Reason: The bug is narrow and source-reproducible.

assert.match(
comment,
/\*\*Summary\*\*\nKeep open\. Slack typing callbacks are disabled in message-tool-only group replies\.\n\nReproducibility: yes\. A source-level reproduction is clear/,
/ℹ️ \*\*INFO\*\* \*\*Summary\*\* \*\*Review the reported behavior\*\*\n\nKeep open\. Slack typing callbacks are disabled in message-tool-only group replies\.\n\nReproducibility: yes\. A source-level reproduction is clear/,
);
assert.ok(comment.indexOf("Reproducibility: yes.") < comment.indexOf("**Next step**"));
assert.doesNotMatch(comment, /\*\*Ways to help us reproduce this\*\*/);
Expand Down Expand Up @@ -2559,7 +2559,7 @@ Reason: The fix is narrow and can be made on the PR branch.
);
assert.match(
comment,
/\*\*Review findings\*\*\n- \[P1\] Validate replace paths — `src\/config\/apply\.ts:42-44`/,
/🚨 \*\*P1\*\* \*\*Review findings\*\* \*\*Address the highest-signal findings\*\*[\s\S]*- \[P1\] Validate replace paths — `src\/config\/apply\.ts:42-44`/,
);
assert.match(comment, /Full review comments:/);
assert.match(comment, /A misspelled replace path is currently ignored/);
Expand Down Expand Up @@ -2595,7 +2595,7 @@ Land this docs-only PR after maintainer review.

assert.match(
comment,
/\*\*Next step before merge\*\*\nLand this docs-only PR after maintainer review\./,
/ℹ️ \*\*INFO\*\* \*\*Next step before merge\*\* \*\*Take the next merge decision\*\*\n\nLand this docs-only PR after maintainer review\./,
);
assert.doesNotMatch(comment, /Best possible solution:/);
});
Expand Down Expand Up @@ -2639,7 +2639,10 @@ Full review comments:
);

assert.match(comment, /Codex review: passed\./);
assert.match(comment, /\*\*Next step before merge\*\*\nMerge after required checks are green\./);
assert.match(
comment,
/✅ \*\*DONE\*\* \*\*Next step before merge\*\* \*\*Take the next merge decision\*\*\n\nMerge after required checks are green\./,
);
assert.doesNotMatch(comment, /Automerge follow-up:/);
assert.match(comment, /<!-- clawsweeper-verdict:pass item=74453 sha=abc123def456/);
assert.doesNotMatch(comment, /clawsweeper-verdict:needs-human/);
Expand Down Expand Up @@ -2690,12 +2693,17 @@ Full review comments:
});
const markers = reviewAutomationMarkersFromReport(report);

assert.match(comment, /\*\*PR rating\*\*\nOverall: 🦞 diamond lobster/);
assert.match(comment, /Proof: 🦞 diamond lobster/);
assert.match(comment, /Patch quality: 🦞 diamond lobster/);
assert.match(comment, /✅ \*\*DONE\*\* \*\*PR rating\*\*/);
assert.match(
comment,
/Overall 🦞 diamond lobster with proof 🦞 diamond lobster and patch quality 🦞 diamond lobster\./,
);
assert.match(comment, /<summary>What the crustacean ranks mean<\/summary>/);
assert.match(comment, /🧂 unranked krab: not merge-ready/);
assert.match(comment, /\*\*Real behavior proof\*\*\nSufficient \(terminal\):/);
assert.match(
comment,
/✅ \*\*DONE\*\* \*\*Real behavior proof\*\* \*\*Assess whether proof is merge-ready\*\*\n\nSufficient \(terminal\):/,
);
assert.match(markers, /clawsweeper-verdict:pass/);
assert.doesNotMatch(markers, /clawsweeper-verdict:needs-human/);
});
Expand Down Expand Up @@ -2989,8 +2997,11 @@ Full review comments:

const comment = renderReviewCommentFromReport(report, "none");

assert.match(comment, /\*\*PR rating\*\*\nOverall: 🦀 challenger crab/);
assert.match(comment, /Proof: 🦀 challenger crab ✨ media proof bonus/);
assert.match(comment, /ℹ️ \*\*INFO\*\* \*\*PR rating\*\*/);
assert.match(
comment,
/Overall 🦀 challenger crab with proof 🦀 challenger crab ✨ media proof bonus and patch quality 🦀 challenger crab\./,
);
assert.match(comment, /Shiny media proof means a screenshot, video, or linked artifact/);
assert.doesNotMatch(comment, /Rank-up moves:/);
});
Expand Down Expand Up @@ -3860,7 +3871,10 @@ Full review comments:
const comment = renderReviewCommentFromReport(report, "none");
const markers = reviewAutomationMarkersFromReport(report);

assert.match(comment, /\*\*Real behavior proof\*\*\nNot applicable:/);
assert.match(
comment,
/✅ \*\*DONE\*\* \*\*Real behavior proof\*\* \*\*Assess whether proof is merge-ready\*\*\n\nNot applicable:/,
);
assert.match(comment, /only changes files under docs\//);
assert.match(markers, /clawsweeper-verdict:pass/);
assert.doesNotMatch(markers, /clawsweeper-verdict:needs-human/);
Expand Down Expand Up @@ -4270,7 +4284,7 @@ Full review comments:
assert.match(comment, /Codex review: passed\./);
assert.match(
comment,
/\*\*Next step before merge\*\*\nLeave this draft open after fixes are complete\./,
/\*\*DONE\*\* \*\*Next step before merge\*\* \*\*Take the next merge decision\*\*\n\nLeave this draft open after fixes are complete\./,
);
assert.doesNotMatch(comment, /Autofix follow-up:/);
assert.match(comment, /<!-- clawsweeper-verdict:pass item=74610 sha=abc123def456/);
Expand Down Expand Up @@ -6547,7 +6561,11 @@ Reason: ${duplicateRisk}
"none",
);

assert.ok(comment.includes(`**Next step before merge**\n${duplicateRisk}`));
assert.ok(
comment.includes(
`ℹ️ **INFO** **Next step before merge** **Take the next merge decision**\n\n${duplicateRisk}`,
),
);
assert.doesNotMatch(comment, /Remaining risk \/ open question:/);
assert.doesNotMatch(comment, /\*\*Risk before merge\*\*/);
assert.equal(comment.split(duplicateRisk).length - 1, 1);
Expand Down