@@ -708,7 +708,6 @@ impl RpgServer {
708708 Ok ( result)
709709 }
710710
711- #[ cfg( feature = "auto-lift" ) ]
712711 #[ tool(
713712 description = "Autonomous lifting via LLM API — lifts all unlifted entities, synthesizes file features, and builds semantic hierarchy in one call. Uses a cheap external LLM (Haiku, GPT-4o-mini) instead of consuming your coding agent's subscription tokens. Providers: 'anthropic' (default model: claude-haiku-4-5), 'openai' (default: gpt-4o-mini). For OpenRouter or Gemini, use provider='openai' with a custom base_url. Set dry_run=true to estimate cost first. Requires an API key." ,
714713 annotations(
@@ -719,113 +718,121 @@ impl RpgServer {
719718 ) ]
720719 async fn auto_lift (
721720 & self ,
722- Parameters ( params) : Parameters < AutoLiftParams > ,
721+ # [ allow ( unused_variables ) ] Parameters ( params) : Parameters < AutoLiftParams > ,
723722 ) -> Result < String , String > {
724- /// Drop guard that clears the `lift_in_progress` flag on scope exit.
725- struct LiftGuard ( std:: sync:: Arc < std:: sync:: atomic:: AtomicBool > ) ;
726- impl Drop for LiftGuard {
727- fn drop ( & mut self ) {
728- self . 0 . store ( false , std:: sync:: atomic:: Ordering :: SeqCst ) ;
729- }
723+ #[ cfg( not( feature = "auto-lift" ) ) ]
724+ {
725+ return Err ( "auto_lift is not available: this binary was compiled without the auto-lift feature." . into ( ) ) ;
730726 }
731727
732- self . ensure_graph ( ) . await ?;
733-
734- // Reject concurrent lift calls
735- if self
736- . lift_in_progress
737- . swap ( true , std:: sync:: atomic:: Ordering :: SeqCst )
728+ #[ cfg( feature = "auto-lift" ) ]
738729 {
739- return Err ( "A lift is already in progress. Wait for it to complete." . into ( ) ) ;
740- }
741- let _guard = LiftGuard ( std:: sync:: Arc :: clone ( & self . lift_in_progress ) ) ;
730+ /// Drop guard that clears the `lift_in_progress` flag on scope exit.
731+ struct LiftGuard ( std:: sync:: Arc < std:: sync:: atomic:: AtomicBool > ) ;
732+ impl Drop for LiftGuard {
733+ fn drop ( & mut self ) {
734+ self . 0 . store ( false , std:: sync:: atomic:: Ordering :: SeqCst ) ;
735+ }
736+ }
742737
743- // Resolve API key: prefer api_key_env (safe), fall back to api_key (raw)
744- let api_key = if let Some ( ref env_var) = params. api_key_env {
745- std:: env:: var ( env_var) . map_err ( |_| {
746- format ! (
747- "Environment variable '{}' not set. Set it or use api_key instead." ,
748- env_var
749- )
750- } ) ?
751- } else if let Some ( ref key) = params. api_key {
752- key. clone ( )
753- } else {
754- // Auto-detect from standard env vars based on provider
755- let env_var = match params. provider . as_str ( ) {
756- "anthropic" => "ANTHROPIC_API_KEY" ,
757- "openai" => "OPENAI_API_KEY" ,
758- _ => "OPENAI_API_KEY" ,
759- } ;
760- std:: env:: var ( env_var) . map_err ( |_| {
738+ self . ensure_graph ( ) . await ?;
739+
740+ // Reject concurrent lift calls
741+ if self
742+ . lift_in_progress
743+ . swap ( true , std:: sync:: atomic:: Ordering :: SeqCst )
744+ {
745+ return Err ( "A lift is already in progress. Wait for it to complete." . into ( ) ) ;
746+ }
747+ let _guard = LiftGuard ( std:: sync:: Arc :: clone ( & self . lift_in_progress ) ) ;
748+
749+ // Resolve API key: prefer api_key_env (safe), fall back to api_key (raw)
750+ let api_key = if let Some ( ref env_var) = params. api_key_env {
751+ std:: env:: var ( env_var) . map_err ( |_| {
752+ format ! (
753+ "Environment variable '{}' not set. Set it or use api_key instead." ,
754+ env_var
755+ )
756+ } ) ?
757+ } else if let Some ( ref key) = params. api_key {
758+ key. clone ( )
759+ } else {
760+ // Auto-detect from standard env vars based on provider
761+ let env_var = match params. provider . as_str ( ) {
762+ "anthropic" => "ANTHROPIC_API_KEY" ,
763+ "openai" => "OPENAI_API_KEY" ,
764+ _ => "OPENAI_API_KEY" ,
765+ } ;
766+ std:: env:: var ( env_var) . map_err ( |_| {
761767 format ! (
762768 "No API key provided. Use api_key_env=\" {}\" or api_key, or set {} env var." ,
763769 env_var, env_var
764770 )
765771 } ) ?
766- } ;
772+ } ;
767773
768- let provider = rpg_lift:: create_provider (
769- & params. provider ,
770- & api_key,
771- params. model . as_deref ( ) ,
772- params. base_url . as_deref ( ) ,
773- )
774- . map_err ( |e| format ! ( "Failed to create LLM provider: {}" , e) ) ?;
774+ let provider = rpg_lift:: create_provider (
775+ & params. provider ,
776+ & api_key,
777+ params. model . as_deref ( ) ,
778+ params. base_url . as_deref ( ) ,
779+ )
780+ . map_err ( |e| format ! ( "Failed to create LLM provider: {}" , e) ) ?;
775781
776- let scope = params. scope . as_deref ( ) . unwrap_or ( "*" ) ;
777- let dry_run = params. dry_run . unwrap_or ( false ) ;
782+ let scope = params. scope . as_deref ( ) . unwrap_or ( "*" ) ;
783+ let dry_run = params. dry_run . unwrap_or ( false ) ;
778784
779- // Dry run: estimate cost without lifting
780- if dry_run {
781- let guard = self . graph . read ( ) . await ;
782- let graph = guard. as_ref ( ) . unwrap ( ) ;
783- let estimate = rpg_lift:: estimate_cost ( graph, provider. as_ref ( ) , & self . project_root ) ;
784- return Ok ( format ! (
785- "Cost estimate for lifting with {} ({}):\n \n {}" ,
786- params. provider,
787- provider. model_name( ) ,
788- estimate,
789- ) ) ;
790- }
785+ // Dry run: estimate cost without lifting
786+ if dry_run {
787+ let guard = self . graph . read ( ) . await ;
788+ let graph = guard. as_ref ( ) . unwrap ( ) ;
789+ let estimate =
790+ rpg_lift:: estimate_cost ( graph, provider. as_ref ( ) , & self . project_root ) ;
791+ return Ok ( format ! (
792+ "Cost estimate for lifting with {} ({}):\n \n {}" ,
793+ params. provider,
794+ provider. model_name( ) ,
795+ estimate,
796+ ) ) ;
797+ }
791798
792- // Hold the write lock for the entire pipeline. The graph never leaves
793- // shared state, so cancellation or concurrent tools can't corrupt it.
794- let mut guard = self . graph . write ( ) . await ;
795- let graph = guard. as_mut ( ) . ok_or ( "No RPG loaded" ) ?;
799+ // Hold the write lock for the entire pipeline. The graph never leaves
800+ // shared state, so cancellation or concurrent tools can't corrupt it.
801+ let mut guard = self . graph . write ( ) . await ;
802+ let graph = guard. as_mut ( ) . ok_or ( "No RPG loaded" ) ?;
803+
804+ let project_root = self . project_root . clone ( ) ;
805+ let scope_owned = scope. to_string ( ) ;
806+
807+ // Run the blocking pipeline on the current thread (tells tokio we're blocking).
808+ // This is safe because MCP stdio is serial — no concurrent requests.
809+ let report = tokio:: task:: block_in_place ( || {
810+ let config = rpg_lift:: LiftConfig {
811+ provider : provider. as_ref ( ) ,
812+ project_root : & project_root,
813+ scope : & scope_owned,
814+ max_retries : 2 ,
815+ batch_size : 25 ,
816+ batch_tokens : 8000 ,
817+ } ;
818+ let result = rpg_lift:: run_pipeline ( graph, & config) ;
819+ let _ = rpg_core:: storage:: save ( & project_root, graph) ;
820+ result
821+ } )
822+ . map_err ( |e| format ! ( "Lift failed: {}" , e) ) ?;
796823
797- let project_root = self . project_root . clone ( ) ;
798- let scope_owned = scope. to_string ( ) ;
799-
800- // Run the blocking pipeline on the current thread (tells tokio we're blocking).
801- // This is safe because MCP stdio is serial — no concurrent requests.
802- let report = tokio:: task:: block_in_place ( || {
803- let config = rpg_lift:: LiftConfig {
804- provider : provider. as_ref ( ) ,
805- project_root : & project_root,
806- scope : & scope_owned,
807- max_retries : 2 ,
808- batch_size : 25 ,
809- batch_tokens : 8000 ,
810- } ;
811- let result = rpg_lift:: run_pipeline ( graph, & config) ;
812- let _ = rpg_core:: storage:: save ( & project_root, graph) ;
813- result
814- } )
815- . map_err ( |e| format ! ( "Lift failed: {}" , e) ) ?;
824+ drop ( guard) ;
816825
817- drop ( guard) ;
826+ // Clear sessions — entity list changed
827+ * self . lifting_session . write ( ) . await = None ;
828+ * self . hierarchy_session . write ( ) . await = None ;
818829
819- // Clear sessions — entity list changed
820- * self . lifting_session . write ( ) . await = None ;
821- * self . hierarchy_session . write ( ) . await = None ;
822-
823- // Update auto-sync HEAD
824- * self . last_auto_sync_head . write ( ) . await =
825- rpg_encoder:: evolution:: get_head_sha ( & self . project_root ) . ok ( ) ;
830+ // Update auto-sync HEAD
831+ * self . last_auto_sync_head . write ( ) . await =
832+ rpg_encoder:: evolution:: get_head_sha ( & self . project_root ) . ok ( ) ;
826833
827- let mut out = format ! (
828- "Lifting complete ({}, {}).\n \
834+ let mut out = format ! (
835+ "Lifting complete ({}, {}).\n \
829836 auto_lifted: {}\n \
830837 llm_lifted: {}\n \
831838 failed: {}\n \
@@ -834,29 +841,32 @@ impl RpgServer {
834841 hierarchy: {}\n \
835842 tokens: {} in / {} out\n \
836843 cost: ${:.4}",
837- params. provider,
838- report. total_input_tokens + report. total_output_tokens,
839- report. entities_auto_lifted,
840- report. entities_llm_lifted,
841- report. entities_failed,
842- report. batches_processed,
843- report. files_synthesized,
844- if report. hierarchy_assigned {
845- "assigned"
846- } else {
847- "not assigned"
848- } ,
849- report. total_input_tokens,
850- report. total_output_tokens,
851- report. total_cost_usd,
852- ) ;
844+ params. provider,
845+ report. total_input_tokens + report. total_output_tokens,
846+ report. entities_auto_lifted,
847+ report. entities_llm_lifted,
848+ report. entities_failed,
849+ report. batches_processed,
850+ report. files_synthesized,
851+ if report. hierarchy_assigned {
852+ "assigned"
853+ } else {
854+ "not assigned"
855+ } ,
856+ report. total_input_tokens,
857+ report. total_output_tokens,
858+ report. total_cost_usd,
859+ ) ;
853860
854- if !report. errors . is_empty ( ) {
855- out. push_str ( & format ! ( "\n errors: {}" , report. errors. join( "; " ) ) ) ;
856- }
861+ if !report. errors . is_empty ( ) {
862+ out. push_str ( & format ! ( "\n errors: {}" , report. errors. join( "; " ) ) ) ;
863+ }
857864
858- out. push_str ( "\n \n NEXT STEP: Call semantic_snapshot to see the full repo understanding." ) ;
859- Ok ( out)
865+ out. push_str (
866+ "\n \n NEXT STEP: Call semantic_snapshot to see the full repo understanding." ,
867+ ) ;
868+ Ok ( out)
869+ } // #[cfg(feature = "auto-lift")]
860870 }
861871
862872 #[ tool(
0 commit comments