@@ -2371,9 +2371,9 @@ function isActionableCommentEvidence(
23712371 const author = String ( comment . user ?. login ?? comment . author ?. login ?? "" ) . toLowerCase ( ) ;
23722372
23732373 if ( isClawSweeperAuthor ( author ) ) {
2374- const currentReview = body . toLowerCase ( ) . split ( / < d e t a i l s > / , 1 ) [ 0 ] ;
2375- if ( hasActionableClawSweeperReviewSignal ( currentReview , { view } ) ) return true ;
2376- if ( hasClawSweeperReadyReviewSignal ( currentReview ) ) return true ;
2374+ const normalized = body . toLowerCase ( ) ;
2375+ if ( hasActionableClawSweeperReviewSignal ( normalized , { view } ) ) return true ;
2376+ if ( hasClawSweeperReadyReviewSignal ( normalized ) ) return true ;
23772377 }
23782378 if ( isReviewBot ( { author : { login : author } , body } ) ) {
23792379 return / f o u n d i s s u e s | r e q u e s t e d c h a n g e s | c h a n g e s r e q u e s t e d | n e e d s c h a n g e s ? | n e e d s h u m a n | d o n o t m e r g e | d u p l i c a t e | s u p e r s e d e d | s e c u r i t y / i. test (
@@ -2472,9 +2472,8 @@ function hasActionableApprovedReviewBody(body) {
24722472}
24732473
24742474function isBenignAutomationComment ( { author, body, pull, view } ) {
2475- const currentReview = String ( body ) . split ( / < d e t a i l s > / , 1 ) [ 0 ] ;
24762475 if ( isClawSweeperReviewStartComment ( { author, body, pull } ) ) return true ;
2477- if ( isClawSweeperAuthor ( author ) && hasClawSweeperReadyReviewSignal ( currentReview ) ) {
2476+ if ( isClawSweeperAuthor ( author ) && hasClawSweeperReadyReviewSignal ( body ) ) {
24782477 return isClawSweeperReadyReviewComment ( { author, body, pull, view } ) ;
24792478 }
24802479 if ( isStaleAutomationReviewComment ( { author, body, pull } ) ) return true ;
@@ -2493,6 +2492,7 @@ function isDependencyGuardAutomationComment({ body, pull }) {
24932492 if ( ! / ^ < ! - - \s * o p e n c l a w : d e p e n d e n c y - g r a p h - g u a r d \s * - - > / . test ( body ) ) return false ;
24942493 const headSha = String ( pull ?. head ?. sha ?? "" ) . toLowerCase ( ) ;
24952494 if ( ! / ^ [ 0 - 9 a - f ] { 40 } $ / . test ( headSha ) ) return false ;
2495+ if ( isTrustedDependencyGraphAutomationComment ( { body, headSha } ) ) return true ;
24962496 if ( / # # # d e p e n d e n c y g r a p h c h a n g e a u t h o r i z e d \b / . test ( body ) ) {
24972497 const approvedSha = body . match ( / \b a p p r o v e d s h a : \s * ` ( [ 0 - 9 a - f ] { 40 } ) ` / ) ?. [ 1 ] ;
24982498 return approvedSha === headSha ;
@@ -2503,6 +2503,38 @@ function isDependencyGuardAutomationComment({ body, pull }) {
25032503 return cleared ?. [ 1 ] ?. toLowerCase ( ) === headSha ;
25042504}
25052505
2506+ function isTrustedDependencyGraphAutomationComment ( { body, headSha } ) {
2507+ const lines = String ( body )
2508+ . trim ( )
2509+ . toLowerCase ( )
2510+ . split ( / \r ? \n / )
2511+ . map ( ( line ) => line . trim ( ) )
2512+ . filter ( Boolean ) ;
2513+ if ( lines . length !== 7 ) return false ;
2514+ if ( lines [ 0 ] !== "<!-- openclaw:dependency-graph-guard -->" ) return false ;
2515+ if ( lines [ 1 ] !== "### dependency graph changes noted" ) return false ;
2516+ if (
2517+ lines [ 2 ] !==
2518+ "this pr includes dependency graph changes. the dependency guard is informational because the pr author is a repository admin or a member of `@openclaw/openclaw-secops`."
2519+ ) {
2520+ return false ;
2521+ }
2522+ const currentSha = lines [ 3 ] . match ( / ^ - c u r r e n t s h a : \s * ` ( [ 0 - 9 a - f ] { 40 } ) ` $ / ) ?. [ 1 ] ;
2523+ if ( currentSha !== headSha ) return false ;
2524+ if ( ! / ^ - t r u s t e d a c t o r : \s * @ [ a - z 0 - 9 ] (?: [ a - z 0 - 9 - ] { 0 , 38 } ) $ / . test ( lines [ 4 ] ) ) return false ;
2525+ if (
2526+ ! / ^ - t r u s t e d r o l e : \s * ` p u l l r e q u e s t a u t h o r ; (?: r e p o s i t o r y a d m i n | o p e n c l a w - s e c o p s ) ` $ / . test (
2527+ lines [ 5 ] ,
2528+ )
2529+ ) {
2530+ return false ;
2531+ }
2532+ return (
2533+ lines [ 6 ] ===
2534+ "security review is still recommended before merge when the dependency graph change is intentional."
2535+ ) ;
2536+ }
2537+
25062538function isStaleAutomationReviewComment ( { author, body, pull } ) {
25072539 const headSha = String ( pull ?. head ?. sha ?? "" ) . toLowerCase ( ) ;
25082540 if ( ! / ^ [ 0 - 9 a - f ] { 40 } $ / . test ( headSha ) ) return false ;
@@ -2589,8 +2621,18 @@ function isClawSweeperReadyReviewComment({ author, body, pull, view = null }) {
25892621 const hasExactMarker = hasExactHeadClawSweeperReadyMarker ( { body : normalized , pull } ) ;
25902622 if ( ! hasExactMarker ) return false ;
25912623
2592- const currentReview = normalized . split ( / < d e t a i l s > / , 1 ) [ 0 ] ;
2593- return ! hasActionableClawSweeperReviewSignal ( currentReview , { view } ) && hasClawSweeperReadyReviewSignal ( currentReview ) ;
2624+ const reviewState = parseClawSweeperReviewState ( { body : normalized , pull } ) ;
2625+ if ( reviewState . kind === "malformed" ) return false ;
2626+ if ( hasActionableClawSweeperReviewSignal ( normalized , { view } ) ) return false ;
2627+ if ( ! hasClawSweeperReadyReviewSignal ( normalized ) ) return false ;
2628+ if ( reviewState . kind === "legacy" ) return hasLegacyClawSweeperReadyReviewSignal ( normalized ) ;
2629+ if ( reviewState . kind === "transitional" ) return true ;
2630+ return (
2631+ reviewState . readiness === "ready" &&
2632+ reviewState . findings === "none" &&
2633+ reviewState . security === "none" &&
2634+ reviewState . beforeMerge === "none"
2635+ ) ;
25942636}
25952637
25962638function isStaleClawSweeperReadyReviewComment ( { author, body, pull, view = null } ) {
@@ -2600,10 +2642,9 @@ function isStaleClawSweeperReadyReviewComment({ author, body, pull, view = null
26002642 const headSha = String ( pull ?. head ?. sha ?? "" ) . toLowerCase ( ) ;
26012643 if ( ! marker || ! / ^ [ 0 - 9 a - f ] { 40 } $ / . test ( headSha ) || marker . sha === headSha ) return false ;
26022644
2603- const currentReview = normalized . split ( / < d e t a i l s > / , 1 ) [ 0 ] ;
26042645 return (
2605- ! hasActionableClawSweeperReviewSignal ( currentReview , { view } ) &&
2606- hasClawSweeperReadyReviewSignal ( currentReview )
2646+ ! hasActionableClawSweeperReviewSignal ( normalized , { view } ) &&
2647+ hasClawSweeperReadyReviewSignal ( normalized )
26072648 ) ;
26082649}
26092650
@@ -2674,13 +2715,32 @@ function hasClawSweeperReadyReviewSignal(body) {
26742715 const firstLine = String ( body ) . split ( / \r ? \n / , 1 ) [ 0 ] . trim ( ) ;
26752716 return (
26762717 isClawSweeperMaintainerReviewHeader ( firstLine ) &&
2718+ ( hasLegacyClawSweeperReadyReviewSignal ( body ) ||
2719+ hasCurrentClawSweeperReadyReviewSignal ( body ) )
2720+ ) ;
2721+ }
2722+
2723+ function hasLegacyClawSweeperReadyReviewSignal ( body ) {
2724+ return (
26772725 / r e s u l t : \s * r e a d y f o r m a i n t a i n e r r e v i e w \. / . test ( body ) &&
26782726 / ( r e v i e w m e t r i c s : \* \* \s * n o n e i d e n t i f i e d | r e v i e w m e t r i c s : \s * n o n e i d e n t i f i e d | n o (?: c l a w s w e e p e r | a u t o m a t e d ) ? r e p a i r (?: j o b | l a n e ) ? i s (?: n e e d e d | i n d i c a t e d ) | n o c o n c r e t e (?: c o d e f i n d i n g | c o n t r i b u t o r - f a c i n g b l o c k e r l e f t ) | r e m a i n i n g a c t i o n i s n o r m a l m a i n t a i n e r r e v i e w ) / . test (
26792727 body ,
26802728 )
26812729 ) ;
26822730}
26832731
2732+ function hasCurrentClawSweeperReadyReviewSignal ( body ) {
2733+ const readiness = markdownReviewSection ( body , { heading : "merge readiness" , level : 2 } ) ;
2734+ const beforeMerge = markdownReviewSection ( body , { heading : "before merge" , level : 2 } ) ;
2735+ return (
2736+ readiness . kind === "present" &&
2737+ / (?: ^ | \n ) ✅ \s * \* \* r e a d y f o r m a i n t a i n e r r e v i e w \* \* (?: \n | $ ) / . test ( readiness . body ) &&
2738+ / \b n o a c t i o n a b l e f i n d i n g s \. / . test ( readiness . body ) &&
2739+ beforeMerge . kind === "present" &&
2740+ isNoneReviewSection ( beforeMerge . body )
2741+ ) ;
2742+ }
2743+
26842744function isClawSweeperAuthor ( author ) {
26852745 return [ "clawsweeper" , "clawsweeper[bot]" ] . includes ( author ) ;
26862746}
@@ -2700,10 +2760,52 @@ function hasActionableClawSweeperReviewSignal(body, { view = null } = {}) {
27002760 reviewSection ( body , "proof guidance" ) ,
27012761 ) ||
27022762 / \b (?: b l o c k e r | m u s t | n e e d s ? | r e q u i r e d | m i s s i n g ) \b / . test ( reviewSection ( body , "risk before merge" ) ) ||
2703- / \b r e m a i n i n g (?: m e r g e ) ? b l o c k e r \b / . test ( reviewSection ( body , "next step before merge" ) )
2763+ / \b r e m a i n i n g (?: m e r g e ) ? b l o c k e r \b / . test ( reviewSection ( body , "next step before merge" ) ) ||
2764+ hasActionableCurrentClawSweeperReviewSignal ( body )
27042765 ) ;
27052766}
27062767
2768+ function hasActionableCurrentClawSweeperReviewSignal ( body ) {
2769+ for ( const heading of [ "before merge" , "review findings" ] ) {
2770+ const section = markdownReviewSection ( body , { heading, level : 2 } ) ;
2771+ if (
2772+ section . kind === "duplicate" ||
2773+ ( section . kind === "present" && ! isNoneReviewSection ( section . body ) )
2774+ ) {
2775+ return true ;
2776+ }
2777+ }
2778+
2779+ const securitySections = [
2780+ markdownReviewSection ( body , { heading : "security" , level : 2 } ) ,
2781+ markdownReviewSection ( body , { heading : "security" , level : 3 } ) ,
2782+ ] ;
2783+ if (
2784+ securitySections . some (
2785+ ( section ) =>
2786+ section . kind === "duplicate" ||
2787+ ( section . kind === "present" && ! isNoneReviewSection ( section . body ) ) ,
2788+ )
2789+ ) {
2790+ return true ;
2791+ }
2792+
2793+ for ( const label of [ "findings" , "security" ] ) {
2794+ const cells = reviewTableResults ( body , label ) ;
2795+ if ( cells . length > 1 || cells . some ( ( cell ) => ! isNoneReviewSection ( cell ) ) ) return true ;
2796+ }
2797+
2798+ return String ( body )
2799+ . split ( / \r ? \n / )
2800+ . map ( ( line ) => line . trim ( ) )
2801+ . filter ( Boolean )
2802+ . some ( ( line ) =>
2803+ / \b (?: p l e a s e \s + ) ? (?: c o n f i r m | v e r i f y | f i x | a d d r e s s | r e s o l v e | a d d | r e m o v e | u p d a t e | c h a n g e | p r o v i d e | r u n ) \b .{ 0 , 200 } \b b e f o r e (?: m e r g e | m e r g i n g | l a n d i n g | s h i p p i n g ) \b / . test (
2804+ line ,
2805+ ) ,
2806+ ) ;
2807+ }
2808+
27072809function withoutAdvisoryReviewSections ( body ) {
27082810 return String ( body ) . replace (
27092811 / \* \* m a i n t a i n e r o p t i o n s : \* \* [ \s \S ] * ?(? = \n \* \* [ ^ * \n ] + \* \* \s * (?: \n | $ ) | $ ) / gi,
@@ -2723,6 +2825,41 @@ function reviewSection(body, heading) {
27232825 ) ;
27242826}
27252827
2828+ function markdownReviewSection ( body , { heading, level } ) {
2829+ const marker = `${ "#" . repeat ( level ) } ${ String ( heading ) . toLowerCase ( ) } ` ;
2830+ const lines = String ( body ) . toLowerCase ( ) . split ( / \r ? \n / ) ;
2831+ const indexes = [ ] ;
2832+ for ( let index = 0 ; index < lines . length ; index += 1 ) {
2833+ if ( lines [ index ] . trim ( ) === marker ) indexes . push ( index ) ;
2834+ }
2835+ if ( indexes . length === 0 ) return { kind : "missing" , body : "" } ;
2836+ if ( indexes . length !== 1 ) return { kind : "duplicate" , body : "" } ;
2837+
2838+ const section = [ ] ;
2839+ for ( let index = indexes [ 0 ] + 1 ; index < lines . length ; index += 1 ) {
2840+ const line = lines [ index ] ;
2841+ const headingMatch = line . match ( / ^ ( # { 1 , 6 } ) \s + \S / ) ;
2842+ if ( headingMatch && headingMatch [ 1 ] . length <= level ) break ;
2843+ if ( level === 2 && / ^ < d e t a i l s > / i. test ( line . trim ( ) ) ) break ;
2844+ if ( level === 3 && / ^ < \/ d e t a i l s > / i. test ( line . trim ( ) ) ) break ;
2845+ section . push ( line ) ;
2846+ }
2847+ return { kind : "present" , body : section . join ( "\n" ) . trim ( ) } ;
2848+ }
2849+
2850+ function isNoneReviewSection ( body ) {
2851+ return / ^ n o n e [ . ! ] ? $ / . test ( String ( body ) . trim ( ) ) ;
2852+ }
2853+
2854+ function reviewTableResults ( body , label ) {
2855+ const escapedLabel = String ( label ) . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ;
2856+ const pattern = new RegExp (
2857+ `^\\|\\s*\\*\\*${ escapedLabel } \\*\\*\\s*\\|\\s*([^|\\r\\n]+?)\\s*\\|` ,
2858+ "gim" ,
2859+ ) ;
2860+ return [ ...String ( body ) . matchAll ( pattern ) ] . map ( ( match ) => match [ 1 ] . trim ( ) . toLowerCase ( ) ) ;
2861+ }
2862+
27262863function hasExplicitMergeObjection ( body , { view = null } = { } ) {
27272864 return String ( body )
27282865 . split ( / \r ? \n / )
@@ -2831,6 +2968,48 @@ function hasExactHeadClawSweeperReadyMarker({ body, pull }) {
28312968 return Boolean ( marker && / ^ [ 0 - 9 a - f ] { 40 } $ / . test ( headSha ) && marker . sha === headSha ) ;
28322969}
28332970
2971+ function parseClawSweeperReviewState ( { body, pull } ) {
2972+ const openers = String ( body ) . match ( / < ! - - \s * c l a w s w e e p e r - r e v i e w - v e r s i o n \b / gi) ?? [ ] ;
2973+ if ( openers . length === 0 ) return { kind : "legacy" } ;
2974+ const markers = [
2975+ ...String ( body ) . matchAll ( / < ! - - \s * c l a w s w e e p e r - r e v i e w - v e r s i o n \s + ( [ ^ > ] * ) - - > / gi) ,
2976+ ] ;
2977+ if ( openers . length !== 1 || markers . length !== 1 ) return { kind : "malformed" } ;
2978+
2979+ const attributes = parseMarkerAttributes ( markers [ 0 ] [ 1 ] ) ;
2980+ const headSha = String ( pull ?. head ?. sha ?? "" ) . toLowerCase ( ) ;
2981+ const pullNumber = String ( pull ?. number ?? "" ) ;
2982+ if (
2983+ ! attributes ||
2984+ attributes . v !== "1" ||
2985+ attributes . item !== pullNumber ||
2986+ attributes . sha ?. toLowerCase ( ) !== headSha ||
2987+ ! / ^ [ 0 - 9 a - f ] { 40 } $ / . test ( headSha )
2988+ ) {
2989+ return { kind : "malformed" } ;
2990+ }
2991+
2992+ // The v1 state tuple is the producer/consumer contract. Missing every state
2993+ // field is transitional; partial tuples and unknown values fail closed.
2994+ const stateKeys = [ "readiness" , "findings" , "security" , "before_merge" ] ;
2995+ const presentStateKeys = stateKeys . filter ( ( key ) => Object . hasOwn ( attributes , key ) ) ;
2996+ if ( presentStateKeys . length === 0 ) return { kind : "transitional" } ;
2997+ if ( presentStateKeys . length !== stateKeys . length ) return { kind : "malformed" } ;
2998+ if ( ! [ "ready" , "blocked" ] . includes ( attributes . readiness ) ) return { kind : "malformed" } ;
2999+ if ( ! [ "none" , "actionable" ] . includes ( attributes . findings ) ) return { kind : "malformed" } ;
3000+ if ( ! [ "none" , "actionable" ] . includes ( attributes . security ) ) return { kind : "malformed" } ;
3001+ if ( ! [ "none" , "actionable" ] . includes ( attributes . before_merge ) ) {
3002+ return { kind : "malformed" } ;
3003+ }
3004+ return {
3005+ kind : "structured" ,
3006+ readiness : attributes . readiness ,
3007+ findings : attributes . findings ,
3008+ security : attributes . security ,
3009+ beforeMerge : attributes . before_merge ,
3010+ } ;
3011+ }
3012+
28343013function parseClawSweeperReadyMarker ( { body, pull } ) {
28353014 if ( / < ! - - \s * c l a w s w e e p e r - a c t i o n : / i. test ( body ) ) return null ;
28363015
0 commit comments