@@ -73,16 +73,22 @@ pub struct SetupArgs {
7373 #[ arg( long) ]
7474 instrument : bool ,
7575
76- /// Do not run instrumentation agent
77- #[ arg( long, conflicts_with = "instrument" ) ]
76+ /// Do not run instrumentation agent (skills and MCP are still configured)
77+ #[ arg(
78+ long,
79+ conflicts_with = "instrument" ,
80+ conflicts_with = "tui" ,
81+ conflicts_with = "background" ,
82+ conflicts_with = "yolo"
83+ ) ]
7884 no_instrument : bool ,
7985
8086 /// Run the agent in interactive TUI mode [default]
81- #[ arg( long, conflicts_with = "background" ) ]
87+ #[ arg( long, conflicts_with = "background" , conflicts_with = "no_instrument" ) ]
8288 tui : bool ,
8389
8490 /// Run the agent in background (non-interactive) mode
85- #[ arg( long, conflicts_with = "tui" ) ]
91+ #[ arg( long, conflicts_with = "tui" , conflicts_with = "no_instrument" ) ]
8692 background : bool ,
8793
8894 /// Language(s) to instrument (repeatable; case-insensitive).
@@ -150,7 +156,7 @@ struct AgentsSetupArgs {
150156 workers : usize ,
151157
152158 /// Grant the agent full permissions (bypass permission prompts)
153- #[ arg( long) ]
159+ #[ arg( long, conflicts_with = "no_instrument" ) ]
154160 yolo : bool ,
155161}
156162
@@ -215,6 +221,10 @@ struct InstrumentSetupArgs {
215221 /// Grant the agent full permissions (bypass permission prompts)
216222 #[ arg( long) ]
217223 yolo : bool ,
224+
225+ /// Set up skills and write the task file but do not launch the agent.
226+ #[ arg( skip) ]
227+ skip_launch : bool ,
218228}
219229
220230#[ derive( Debug , Clone , Args ) ]
@@ -651,9 +661,6 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()>
651661 any_installed = true ;
652662 }
653663 }
654- if outcome. installed_count == 0 && !agents. is_empty ( ) {
655- had_failures = true ;
656- }
657664 if !verbose {
658665 let label = if any_installed {
659666 "configured"
@@ -681,8 +688,10 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()>
681688 print_wizard_step ( 4 , "Instrument" ) ;
682689 }
683690 if git_root. is_some ( ) {
684- // --no-instrument opts out; --instrument opts in; otherwise prompt or default on.
685- let do_instrument = if flag_no_instrument {
691+ // Whether to launch the agent at the end of this step.
692+ // --no-instrument / interactive "no": set up skills/docs but skip the launch.
693+ // --instrument / non-interactive: always launch.
694+ let launch_agent = if flag_no_instrument {
686695 false
687696 } else if flag_instrument || !interactive {
688697 true
@@ -692,7 +701,10 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()>
692701 . default ( true )
693702 . interact ( ) ?
694703 } ;
695- if do_instrument {
704+
705+ // Skills, docs, and the task file are always set up when a git root exists.
706+ // Only the agent launch is conditional.
707+ {
696708 // Determine agent: explicit flag > single detected > ask user
697709 let instrument_agent =
698710 determine_wizard_instrument_agent ( flag_agent, git_root. as_deref ( ) , & home) ;
@@ -749,32 +761,35 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()>
749761 vec ! [ WorkflowArg :: Instrument , WorkflowArg :: Observe ]
750762 } ;
751763
752- // Run mode: --tui / --background flag > interactive prompt > default TUI.
753- let run_tui = if flag_tui {
754- true
755- } else if flag_background {
756- false
757- } else if interactive {
758- let idx = Select :: with_theme ( & ColorfulTheme :: default ( ) )
759- . with_prompt ( "How do you want to run the agent?" )
760- . items ( & [ "Interactive (TUI)" , "Background" ] )
761- . default ( 0 )
762- . interact ( ) ?;
763- idx == 0
764- } else {
765- true
766- } ;
767-
768- // Yolo: explicit flag > interactive prompt > default false.
769- let effective_yolo = if flag_yolo {
770- true
771- } else if interactive {
772- Confirm :: new ( )
773- . with_prompt ( "Grant agent full permissions? (bypass permission prompts)" )
774- . default ( false )
775- . interact ( ) ?
764+ // Run mode and yolo are only meaningful when actually launching.
765+ let ( run_tui, effective_yolo) = if launch_agent {
766+ let tui = if flag_tui {
767+ true
768+ } else if flag_background {
769+ false
770+ } else if interactive {
771+ let idx = Select :: with_theme ( & ColorfulTheme :: default ( ) )
772+ . with_prompt ( "How do you want to run the agent?" )
773+ . items ( & [ "Interactive (TUI)" , "Background" ] )
774+ . default ( 0 )
775+ . interact ( ) ?;
776+ idx == 0
777+ } else {
778+ true
779+ } ;
780+ let yolo = if flag_yolo {
781+ true
782+ } else if interactive {
783+ Confirm :: new ( )
784+ . with_prompt ( "Grant agent full permissions? (bypass permission prompts)" )
785+ . default ( false )
786+ . interact ( ) ?
787+ } else {
788+ false
789+ } ;
790+ ( tui, yolo)
776791 } else {
777- false
792+ ( false , false )
778793 } ;
779794
780795 run_instrument_setup (
@@ -791,12 +806,11 @@ async fn run_setup_wizard(mut base: BaseArgs, flags: WizardFlags) -> Result<()>
791806 tui : run_tui,
792807 background : !run_tui,
793808 yolo : effective_yolo,
809+ skip_launch : !launch_agent,
794810 } ,
795811 false ,
796812 )
797813 . await ?;
798- } else if verbose {
799- eprintln ! ( " {}" , style( "Skipped" ) . dim( ) ) ;
800814 }
801815 } else if verbose {
802816 eprintln ! ( " {}" , style( "Skipped" ) . dim( ) ) ;
@@ -1274,8 +1288,11 @@ async fn run_instrument_setup(
12741288 // Determine run mode: interactive TUI vs background (autonomous).
12751289 // --tui: interactive TUI (inherits terminal)
12761290 // --background / --yes / non-interactive terminal: background (autonomous)
1291+ // skip_launch: not launching at all — default to non-interactive for task rendering
12771292 // Otherwise: ask the user.
1278- let run_interactive = if args. tui {
1293+ let run_interactive = if args. skip_launch {
1294+ false
1295+ } else if args. tui {
12791296 true
12801297 } else if args. background || args. yes || !ui:: is_interactive ( ) {
12811298 false
@@ -1325,6 +1342,37 @@ async fn run_instrument_setup(
13251342 task_path. display( )
13261343 ) ) ;
13271344
1345+ // --no-instrument (skip_launch): skills configured, task file written — done.
1346+ if args. skip_launch {
1347+ if base. json {
1348+ let report = SetupJsonReport {
1349+ scope : InstallScope :: Local . as_str ( ) . to_string ( ) ,
1350+ selected_agents : vec ! [ selected] ,
1351+ detected_agents : detected,
1352+ results : results. clone ( ) ,
1353+ warnings,
1354+ notes,
1355+ } ;
1356+ println ! (
1357+ "{}" ,
1358+ serde_json:: to_string_pretty( & report)
1359+ . context( "failed to serialize setup report" ) ?
1360+ ) ;
1361+ } else {
1362+ eprintln ! ( ) ;
1363+ for result in & results {
1364+ print_wizard_agent_result ( result) ;
1365+ }
1366+ eprintln ! (
1367+ " {} Task file: {}" ,
1368+ style( "✓" ) . green( ) ,
1369+ task_path. display( )
1370+ ) ;
1371+ print_wizard_done ( false ) ;
1372+ }
1373+ return Ok ( ( ) ) ;
1374+ }
1375+
13281376 let invocation = resolve_instrument_invocation (
13291377 selected,
13301378 args. agent_cmd . as_deref ( ) ,
@@ -3027,6 +3075,8 @@ fn install_mcp_for_agent(
30273075/// Install the Braintrust MCP server into Claude Code's user-wide config (~/.claude.json).
30283076/// Falls back to ~/.mcp.json if the `claude` CLI is not available.
30293077fn install_mcp_for_claude_user ( home : & Path , api_key : Option < & str > ) -> Result < AgentInstallResult > {
3078+ let claude_json_path = home. join ( ".claude.json" ) ;
3079+
30303080 let Some ( key) = api_key else {
30313081 // No API key available — fall back to ~/.mcp.json with env-var placeholder and
30323082 // tell the user they need BRAINTRUST_API_KEY set for Claude Code to authenticate.
@@ -3041,6 +3091,25 @@ fn install_mcp_for_claude_user(home: &Path, api_key: Option<&str>) -> Result<Age
30413091 } ) ;
30423092 } ;
30433093
3094+ // If braintrust is already registered in ~/.claude.json (written by a prior `claude mcp add
3095+ // -s user`), skip the subprocess call — `claude mcp add` exits non-zero when the name
3096+ // already exists.
3097+ if let Ok ( root) = load_json_object_or_default ( & claude_json_path) {
3098+ if root
3099+ . get ( "mcpServers" )
3100+ . and_then ( |v| v. as_object ( ) )
3101+ . map ( |m| m. contains_key ( "braintrust" ) )
3102+ . unwrap_or ( false )
3103+ {
3104+ return Ok ( AgentInstallResult {
3105+ agent : Agent :: Claude ,
3106+ status : InstallStatus :: Skipped ,
3107+ message : "already configured" . to_string ( ) ,
3108+ paths : vec ! [ claude_json_path. display( ) . to_string( ) ] ,
3109+ } ) ;
3110+ }
3111+ }
3112+
30443113 let header = format ! ( "Authorization: Bearer {key}" ) ;
30453114 let output = std:: process:: Command :: new ( "claude" )
30463115 . args ( [
@@ -3062,7 +3131,7 @@ fn install_mcp_for_claude_user(home: &Path, api_key: Option<&str>) -> Result<Age
30623131 agent : Agent :: Claude ,
30633132 status : InstallStatus :: Installed ,
30643133 message : "installed MCP config" . to_string ( ) ,
3065- paths : vec ! [ home . join ( ".claude.json" ) . display( ) . to_string( ) ] ,
3134+ paths : vec ! [ claude_json_path . display( ) . to_string( ) ] ,
30663135 } ) ,
30673136 Ok ( out) => {
30683137 let stderr = String :: from_utf8_lossy ( & out. stderr ) ;
@@ -3618,6 +3687,7 @@ mod tests {
36183687 tui : false ,
36193688 background : false ,
36203689 yolo : false ,
3690+ skip_launch : false ,
36213691 } ;
36223692
36233693 let selected = resolve_instrument_workflow_selection ( & args, & mut false )
@@ -3641,6 +3711,7 @@ mod tests {
36413711 tui : false ,
36423712 background : false ,
36433713 yolo : false ,
3714+ skip_launch : false ,
36443715 } ;
36453716
36463717 let selected = resolve_instrument_workflow_selection ( & args, & mut false )
0 commit comments