11#!/usr/bin/env node
22import { readFileSync } from "node:fs" ;
3+ import { pathToFileURL } from "node:url" ;
34import {
5+ DEFAULT_GITHUB_API_TIMEOUT_MS ,
46 evaluateClawSweeperExactHeadProof ,
57 evaluateRealBehaviorProof ,
68 isMaintainerTeamMember ,
9+ withGitHubApiTimeout ,
710} from "./real-behavior-proof-policy.mjs" ;
811
912function escapeCommandValue ( value ) {
@@ -14,7 +17,14 @@ function escapeCommandValue(value) {
1417 . replace ( / : / g, "%3A" ) ;
1518}
1619
17- async function fetchProofComments ( { owner, repo, issueNumber, tokens } ) {
20+ export async function fetchProofComments ( {
21+ owner,
22+ repo,
23+ issueNumber,
24+ tokens,
25+ fetchImpl = fetch ,
26+ timeoutMs = DEFAULT_GITHUB_API_TIMEOUT_MS ,
27+ } ) {
1828 let lastError ;
1929 for ( const token of tokens . filter ( Boolean ) ) {
2030 const comments = [ ] ;
@@ -25,17 +35,27 @@ async function fetchProofComments({ owner, repo, issueNumber, tokens }) {
2535 ) ;
2636 url . searchParams . set ( "per_page" , "100" ) ;
2737 url . searchParams . set ( "page" , String ( page ) ) ;
28- const response = await fetch ( url , {
29- headers : {
30- Accept : "application/vnd.github+json" ,
31- Authorization : `Bearer ${ token } ` ,
32- "X-GitHub-Api-Version" : "2022-11-28" ,
33- } ,
34- } ) ;
38+ const response = await withGitHubApiTimeout (
39+ `proof comment lookup page ${ page } ` ,
40+ timeoutMs ,
41+ ( signal ) =>
42+ fetchImpl ( url , {
43+ headers : {
44+ Accept : "application/vnd.github+json" ,
45+ Authorization : `Bearer ${ token } ` ,
46+ "X-GitHub-Api-Version" : "2022-11-28" ,
47+ } ,
48+ signal,
49+ } ) ,
50+ ) ;
3551 if ( ! response . ok ) {
3652 throw new Error ( `comments API returned ${ response . status } ` ) ;
3753 }
38- const pageComments = await response . json ( ) ;
54+ const pageComments = await withGitHubApiTimeout (
55+ `proof comment response page ${ page } ` ,
56+ timeoutMs ,
57+ ( ) => response . json ( ) ,
58+ ) ;
3959 comments . push ( ...pageComments ) ;
4060 if ( pageComments . length < 100 ) {
4161 break ;
@@ -49,69 +69,83 @@ async function fetchProofComments({ owner, repo, issueNumber, tokens }) {
4969 throw lastError ?? new Error ( "No GitHub token available for proof comment lookup." ) ;
5070}
5171
52- const eventPath = process . env . GITHUB_EVENT_PATH ;
53- if ( ! eventPath ) {
54- console . error ( "::error title=Real behavior proof failed::GITHUB_EVENT_PATH is not set." ) ;
55- process . exit ( 1 ) ;
72+ function isMainModule ( ) {
73+ return Boolean ( process . argv [ 1 ] && import . meta. url === pathToFileURL ( process . argv [ 1 ] ) . href ) ;
5674}
5775
58- const event = JSON . parse ( readFileSync ( eventPath , "utf8" ) ) ;
59- const pullRequest = event . pull_request ;
60- if ( ! pullRequest ) {
61- console . log ( "No pull_request payload found; skipping real behavior proof gate .") ;
62- process . exit ( 0 ) ;
63- }
76+ async function main ( env = process . env ) {
77+ const eventPath = env . GITHUB_EVENT_PATH ;
78+ if ( ! eventPath ) {
79+ console . error ( "::error title=Real behavior proof failed::GITHUB_EVENT_PATH is not set .") ;
80+ process . exit ( 1 ) ;
81+ }
6482
65- const appToken = process . env . GH_APP_TOKEN ;
66- const org = event . repository ?. owner ?. login ;
67- const authorLogin = pullRequest . user ?. login ;
68- if ( appToken && org && authorLogin ) {
69- try {
70- if ( await isMaintainerTeamMember ( { token : appToken , org, login : authorLogin } ) ) {
71- console . log (
72- `PR author @${ authorLogin } is an active member of the ${ org } /maintainer team; skipping real behavior proof gate.` ,
83+ const event = JSON . parse ( readFileSync ( eventPath , "utf8" ) ) ;
84+ const pullRequest = event . pull_request ;
85+ if ( ! pullRequest ) {
86+ console . log ( "No pull_request payload found; skipping real behavior proof gate." ) ;
87+ process . exit ( 0 ) ;
88+ }
89+
90+ const appToken = env . GH_APP_TOKEN ;
91+ const org = event . repository ?. owner ?. login ;
92+ const authorLogin = pullRequest . user ?. login ;
93+ if ( appToken && org && authorLogin ) {
94+ try {
95+ if ( await isMaintainerTeamMember ( { token : appToken , org, login : authorLogin } ) ) {
96+ console . log (
97+ `PR author @${ authorLogin } is an active member of the ${ org } /maintainer team; skipping real behavior proof gate.` ,
98+ ) ;
99+ process . exit ( 0 ) ;
100+ }
101+ } catch ( error ) {
102+ console . warn (
103+ `::warning title=Maintainer membership check failed::${ escapeCommandValue ( error ?. message ?? String ( error ) ) } ` ,
73104 ) ;
74- process . exit ( 0 ) ;
75105 }
76- } catch ( error ) {
77- console . warn (
78- `::warning title=Maintainer membership check failed::${ escapeCommandValue ( error ?. message ?? String ( error ) ) } ` ,
79- ) ;
80106 }
81- }
82107
83- const evaluation = evaluateRealBehaviorProof ( { pullRequest } ) ;
84- if ( evaluation . passed ) {
85- console . log ( evaluation . reason ) ;
86- process . exit ( 0 ) ;
87- }
108+ const evaluation = evaluateRealBehaviorProof ( { pullRequest } ) ;
109+ if ( evaluation . passed ) {
110+ console . log ( evaluation . reason ) ;
111+ process . exit ( 0 ) ;
112+ }
88113
89- const repository = process . env . GITHUB_REPOSITORY ;
90- if ( ( appToken || process . env . GITHUB_TOKEN ) && repository && pullRequest . number ) {
91- const [ owner , repo ] = repository . split ( "/" ) ;
92- try {
93- const comments = await fetchProofComments ( {
94- owner,
95- repo,
96- issueNumber : pullRequest . number ,
97- tokens : [ appToken , process . env . GITHUB_TOKEN ] ,
98- } ) ;
114+ const repository = env . GITHUB_REPOSITORY ;
115+ if ( ( appToken || env . GITHUB_TOKEN ) && repository && pullRequest . number ) {
116+ const [ owner , repo ] = repository . split ( "/" ) ;
117+ try {
118+ const comments = await fetchProofComments ( {
119+ owner,
120+ repo,
121+ issueNumber : pullRequest . number ,
122+ tokens : [ appToken , env . GITHUB_TOKEN ] ,
123+ } ) ;
99124
100- const clawSweeperEvaluation = evaluateClawSweeperExactHeadProof ( {
101- pullRequest,
102- comments,
103- } ) ;
104- if ( clawSweeperEvaluation . passed ) {
105- console . log ( clawSweeperEvaluation . reason ) ;
106- process . exit ( 0 ) ;
125+ const clawSweeperEvaluation = evaluateClawSweeperExactHeadProof ( {
126+ pullRequest,
127+ comments,
128+ } ) ;
129+ if ( clawSweeperEvaluation . passed ) {
130+ console . log ( clawSweeperEvaluation . reason ) ;
131+ process . exit ( 0 ) ;
132+ }
133+ } catch ( error ) {
134+ console . warn (
135+ `::warning title=Proof verdict comment lookup failed::${ escapeCommandValue ( error ?. message ?? String ( error ) ) } ` ,
136+ ) ;
107137 }
108- } catch ( error ) {
109- console . warn (
110- `::warning title=Proof verdict comment lookup failed::${ escapeCommandValue ( error ?. message ?? String ( error ) ) } ` ,
111- ) ;
112138 }
139+
140+ const message = `${ evaluation . reason } Add after-fix evidence from a real OpenClaw setup in the PR body. Screenshots, recordings, terminal screenshots, console output, redacted runtime logs, linked artifacts, or copied live output count. Unit tests, mocks, snapshots, lint, typechecks, and CI are supplemental only. A maintainer can apply proof: override when appropriate.` ;
141+ console . error ( `::error title=Real behavior proof required::${ escapeCommandValue ( message ) } ` ) ;
142+ process . exit ( 1 ) ;
113143}
114144
115- const message = `${ evaluation . reason } Add after-fix evidence from a real OpenClaw setup in the PR body. Screenshots, recordings, terminal screenshots, console output, redacted runtime logs, linked artifacts, or copied live output count. Unit tests, mocks, snapshots, lint, typechecks, and CI are supplemental only. A maintainer can apply proof: override when appropriate.` ;
116- console . error ( `::error title=Real behavior proof required::${ escapeCommandValue ( message ) } ` ) ;
117- process . exit ( 1 ) ;
145+ export const testing = {
146+ fetchProofComments,
147+ } ;
148+
149+ if ( isMainModule ( ) ) {
150+ await main ( ) ;
151+ }
0 commit comments