@@ -1065,6 +1065,11 @@ impl Store {
10651065 // pack backfill so the cleared rows don't re-acquire a pack.
10661066 Self :: heal_symlinked_git_install_meta ( & tx, extensions) ?;
10671067
1068+ // Realign git install_meta the backfill stamped from a since-corrected
1069+ // source (e.g. plugins re-attributed to their marketplace repo). Run
1070+ // before pack backfill so pack re-derives from the refreshed URL.
1071+ Self :: refresh_stale_git_install_meta ( & tx) ?;
1072+
10681073 // Backfill pack from install_url or source_json URL for deployed extensions
10691074 // that lost their git context after being copied to agent directories
10701075 Self :: backfill_packs ( & tx) ?;
@@ -1171,6 +1176,8 @@ impl Store {
11711176
11721177 Self :: heal_symlinked_git_install_meta ( & tx, extensions) ?;
11731178
1179+ Self :: refresh_stale_git_install_meta ( & tx) ?;
1180+
11741181 Self :: backfill_packs ( & tx) ?;
11751182
11761183 tx. commit ( ) ?;
@@ -1214,6 +1221,30 @@ impl Store {
12141221 Ok ( ( ) )
12151222 }
12161223
1224+ /// Refresh `git` install_meta that the source backfill stamped from a now-
1225+ /// corrected source. The backfill (above) only fires on `install_type IS
1226+ /// NULL`, so a row stamped in an earlier sync keeps its old `install_url`
1227+ /// even after the scanner learns the real source (e.g. a plugin first seen
1228+ /// as the enclosing dotfiles repo, now resolved to its marketplace repo via
1229+ /// the install manifest). `deriveExtensionUrl` prefers `install_url`, so the
1230+ /// stale value would keep the extension in the wrong group. Realign
1231+ /// `install_url`/revision to the authoritative `source_json.url` and clear
1232+ /// `pack` so `backfill_packs` re-derives it. Rows already in agreement, and
1233+ /// non-git installs (marketplace/manual/cli), are untouched.
1234+ fn refresh_stale_git_install_meta ( conn : & rusqlite:: Connection ) -> Result < ( ) , HkError > {
1235+ conn. execute_batch (
1236+ "UPDATE extensions
1237+ SET install_url = json_extract(source_json, '$.url'),
1238+ install_url_resolved = NULL,
1239+ install_revision = json_extract(source_json, '$.commit_hash'),
1240+ remote_revision = NULL, checked_at = NULL, check_error = NULL, pack = NULL
1241+ WHERE install_type = 'git'
1242+ AND json_extract(source_json, '$.url') IS NOT NULL
1243+ AND install_url IS NOT json_extract(source_json, '$.url')" ,
1244+ ) ?;
1245+ Ok ( ( ) )
1246+ }
1247+
12171248 /// Backfill `pack` from install_url, source_json URL, or child extensions.
12181249 /// Deployed skills lose their git context after being copied to agent directories,
12191250 /// but install_url retains the repo URL. CLI parent extensions inherit pack from children.
@@ -2736,6 +2767,94 @@ mod tests {
27362767 ) ;
27372768 }
27382769
2770+ #[ test]
2771+ fn test_sync_refreshes_stale_git_install_meta ( ) {
2772+ // Regression: a plugin first scanned as the enclosing dotfiles repo got
2773+ // install_type='git' + install_url/pack of that repo. After the scanner
2774+ // learns the real marketplace source, the backfill (install_type IS
2775+ // NULL only) can't update it, so the stale install_url kept the plugin
2776+ // in the dotfiles group. Sync must realign install_url + pack to the
2777+ // corrected source; a git row already in agreement stays untouched.
2778+ let ( store, _dir) = test_store ( ) ;
2779+
2780+ // Polluted plugin: stale dotfiles install_meta, but the scan now reports
2781+ // the real marketplace source.
2782+ let mut polluted = sample_extension ( ) ;
2783+ polluted. id = "plugin-cr" . into ( ) ;
2784+ polluted. kind = ExtensionKind :: Plugin ;
2785+ polluted. name = "code-review" . into ( ) ;
2786+ polluted. source . origin = SourceOrigin :: Git ;
2787+ polluted. source . url = Some ( "https://github.com/anthropics/claude-plugins-official" . into ( ) ) ;
2788+ store. insert_extension ( & polluted) . unwrap ( ) ;
2789+ store
2790+ . set_install_meta (
2791+ "plugin-cr" ,
2792+ & InstallMeta {
2793+ install_type : "git" . into ( ) ,
2794+ url : Some ( "https://github.com/octo/dotfiles" . into ( ) ) ,
2795+ url_resolved : None ,
2796+ branch : None ,
2797+ subpath : None ,
2798+ revision : None ,
2799+ remote_revision : None ,
2800+ checked_at : None ,
2801+ check_error : None ,
2802+ } ,
2803+ )
2804+ . unwrap ( ) ;
2805+ store. update_pack ( "plugin-cr" , Some ( "octo/dotfiles" ) ) . unwrap ( ) ;
2806+
2807+ // Consistent git install: install_url already matches its source.
2808+ let mut consistent = sample_extension ( ) ;
2809+ consistent. id = "plugin-ok" . into ( ) ;
2810+ consistent. kind = ExtensionKind :: Plugin ;
2811+ consistent. name = "ok" . into ( ) ;
2812+ consistent. source . origin = SourceOrigin :: Git ;
2813+ consistent. source . url = Some ( "https://github.com/real/repo" . into ( ) ) ;
2814+ store. insert_extension ( & consistent) . unwrap ( ) ;
2815+ store
2816+ . set_install_meta (
2817+ "plugin-ok" ,
2818+ & InstallMeta {
2819+ install_type : "git" . into ( ) ,
2820+ url : Some ( "https://github.com/real/repo" . into ( ) ) ,
2821+ url_resolved : None ,
2822+ branch : None ,
2823+ subpath : None ,
2824+ revision : None ,
2825+ remote_revision : None ,
2826+ checked_at : None ,
2827+ check_error : None ,
2828+ } ,
2829+ )
2830+ . unwrap ( ) ;
2831+ store. update_pack ( "plugin-ok" , Some ( "real/repo" ) ) . unwrap ( ) ;
2832+
2833+ // Re-sync as the scanner now reports them (install_meta carried in DB).
2834+ let mut s_polluted = polluted. clone ( ) ;
2835+ s_polluted. install_meta = None ;
2836+ s_polluted. pack = None ;
2837+ let mut s_consistent = consistent. clone ( ) ;
2838+ s_consistent. install_meta = None ;
2839+ s_consistent. pack = None ;
2840+ store. sync_extensions ( & [ s_polluted, s_consistent] ) . unwrap ( ) ;
2841+
2842+ let fixed = store. get_extension ( "plugin-cr" ) . unwrap ( ) . unwrap ( ) ;
2843+ assert_eq ! (
2844+ fixed. install_meta. expect( "install_meta kept" ) . url. as_deref( ) ,
2845+ Some ( "https://github.com/anthropics/claude-plugins-official" ) ,
2846+ "stale install_url must be realigned to the corrected source"
2847+ ) ;
2848+ assert_eq ! (
2849+ fixed. pack. as_deref( ) ,
2850+ Some ( "anthropics/claude-plugins-official" ) ,
2851+ "pack must re-derive from the corrected source"
2852+ ) ;
2853+
2854+ let kept = store. get_extension ( "plugin-ok" ) . unwrap ( ) . unwrap ( ) ;
2855+ assert_eq ! ( kept. pack. as_deref( ) , Some ( "real/repo" ) , "consistent git row untouched" ) ;
2856+ }
2857+
27392858 #[ test]
27402859 fn test_stale_row_should_prune_decision ( ) {
27412860 let cli = ExtensionKind :: Cli . as_str ( ) ;
0 commit comments