33import difflib
44import hashlib
55import subprocess
6+ import sys
67import urllib .request
78import urllib .error
89from datetime import datetime , timezone
910from pathlib import Path
1011
1112import yaml
1213
14+ sys .path .insert (0 , str (Path (__file__ ).resolve ().parent ))
15+ from skill_security import scan_content
16+
1317ROOT = Path (__file__ ).resolve ().parent .parent
1418MANIFESTS = sorted (ROOT .glob ('.claude/skills/*/references/external-skills-manifest.yaml' ))
1519REPORT = ROOT / 'research/javascript-skills/reports/external-skills-refresh-report.md'
@@ -51,14 +55,25 @@ def show_diff(skill_id: str, old_content: str | None, new_content: str | None) -
5155 print (f' (no content diff for { skill_id } )' )
5256
5357
58+ def make_compare_url (repo_url : str , old_sha : str , new_sha : str ) -> str :
59+ owner_repo = repo_url .removeprefix ('https://github.com/' )
60+ if old_sha :
61+ return f'https://github.com/{ owner_repo } /compare/{ old_sha } ...{ new_sha } '
62+ return f'https://github.com/{ owner_repo } /commit/{ new_sha } '
63+
64+
5465def main () -> int :
5566 parser = argparse .ArgumentParser (description = 'Refresh pinned commits in all critic manifests.' )
5667 parser .add_argument ('--check' , action = 'store_true' , help = 'Fail if updates are needed without writing files.' )
5768 parser .add_argument ('--approve' , action = 'store_true' , help = 'Apply pin and content hash updates (writes manifests).' )
69+ parser .add_argument ('--force' , action = 'store_true' , help = 'Allow --approve to proceed despite content scan warnings.' )
5870 args = parser .parse_args ()
5971
72+ # Each entry: (manifest, skill_id, old_sha, new_sha, new_hash_or_None, repo_url)
6073 changes = []
6174 checked = 0
75+ all_scan_warnings : list [str ] = []
76+ pending_writes : list [tuple [Path , dict ]] = []
6277
6378 for manifest in MANIFESTS :
6479 data = yaml .safe_load (manifest .read_text (encoding = 'utf-8' )) or {}
@@ -84,12 +99,18 @@ def main() -> int:
8499 if new_content is not None :
85100 new_hash = sha256_of (new_content )
86101 print (f' content_sha256 (new): { new_hash } ' )
102+ scan_warnings = scan_content (new_content , s ['id' ])
103+ if scan_warnings :
104+ print (f' SCAN WARNINGS ({ len (scan_warnings )} ):' )
105+ for w in scan_warnings :
106+ print (w )
107+ all_scan_warnings .extend (scan_warnings )
87108 else :
88109 new_hash = None
89110 print (f' WARNING: Could not fetch new content — content_sha256 will be unverified.' )
90111
91112 local_changes .append ((s ['id' ], current , latest , new_hash ))
92- changes .append ((manifest , s ['id' ], current , latest , new_hash ))
113+ changes .append ((manifest , s ['id' ], current , latest , new_hash , s . get ( 'repo_url' , '' ) ))
93114
94115 if args .approve :
95116 s ['pinned_commit' ] = latest
@@ -98,7 +119,17 @@ def main() -> int:
98119 else :
99120 s .pop ('content_sha256' , None )
100121
101- if local_changes and args .approve :
122+ if local_changes :
123+ pending_writes .append ((manifest , data ))
124+
125+ # Scan gate: block all writes if warnings found without --force
126+ if all_scan_warnings and not args .force :
127+ print (f'\n Content scan found { len (all_scan_warnings )} warning(s). Review and use --approve --force to override.' )
128+ if args .approve :
129+ return 2
130+
131+ if args .approve and (not all_scan_warnings or args .force ):
132+ for manifest , data in pending_writes :
102133 data ['generated_at' ] = datetime .now (timezone .utc ).replace (microsecond = 0 ).isoformat ().replace ('+00:00' , 'Z' )
103134 manifest .write_text (yaml .safe_dump (data , sort_keys = False ), encoding = 'utf-8' )
104135
@@ -112,11 +143,12 @@ def main() -> int:
112143 ]
113144
114145 if changes :
115- lines .append ('| Manifest | Skill | Old | New | Hash Verified |' )
116- lines .append ('|---|---|---|---|---|' )
117- for m , sid , old , new , new_hash in changes :
146+ lines .append ('| Manifest | Skill | Old | New | Hash Verified | Compare | ' )
147+ lines .append ('|---|---|---|---|---|---| ' )
148+ for m , sid , old , new , new_hash , repo_url in changes :
118149 verified = 'yes' if new_hash else '# UNVERIFIED'
119- lines .append (f'| `{ m .relative_to (ROOT )} ` | `{ sid } ` | `{ old or "-" } ` | `{ new } ` | { verified } |' )
150+ compare_url = make_compare_url (repo_url , old , new )
151+ lines .append (f'| `{ m .relative_to (ROOT )} ` | `{ sid } ` | `{ old or "-" } ` | `{ new } ` | { verified } | [compare]({ compare_url } ) |' )
120152 else :
121153 lines .append ('No pin changes detected.' )
122154
0 commit comments