@@ -426,6 +426,13 @@ func (c *CLI) registerCommands() {
426426 Run : c .showHistory ,
427427 }
428428
429+ repoCmd .Subcommands ["hibernate" ] = & Command {
430+ Name : "hibernate" ,
431+ Description : "Hibernate a repository, archiving uncommitted changes" ,
432+ Usage : "multiclaude repo hibernate [--repo <repo>] [--all] [--yes]" ,
433+ Run : c .hibernateRepo ,
434+ }
435+
429436 c .rootCmd .Subcommands ["repo" ] = repoCmd
430437
431438 // Backward compatibility aliases for root-level repo commands
@@ -2859,6 +2866,245 @@ func (c *CLI) removeWorker(args []string) error {
28592866 return nil
28602867}
28612868
2869+ // hibernateRepo stops all work in a repository and archives uncommitted changes
2870+ func (c * CLI ) hibernateRepo (args []string ) error {
2871+ flags , _ := ParseFlags (args )
2872+ skipConfirm := flags ["yes" ] == "true"
2873+ hibernateAll := flags ["all" ] == "true" // Also hibernate persistent agents (supervisor, workspace)
2874+
2875+ // Determine repository
2876+ repoName , err := c .resolveRepo (flags )
2877+ if err != nil {
2878+ return errors .NotInRepo ()
2879+ }
2880+
2881+ // Get agent list from daemon
2882+ client := socket .NewClient (c .paths .DaemonSock )
2883+ resp , err := client .Send (socket.Request {
2884+ Command : "list_agents" ,
2885+ Args : map [string ]interface {}{
2886+ "repo" : repoName ,
2887+ },
2888+ })
2889+ if err != nil {
2890+ return errors .DaemonCommunicationFailed ("getting agent info" , err )
2891+ }
2892+ if ! resp .Success {
2893+ return errors .Wrap (errors .CategoryRuntime , "failed to get agent info" , fmt .Errorf ("%s" , resp .Error ))
2894+ }
2895+
2896+ agents , _ := resp .Data .([]interface {})
2897+ if len (agents ) == 0 {
2898+ fmt .Printf ("No agents running in repository '%s'\n " , repoName )
2899+ return nil
2900+ }
2901+
2902+ // Filter agents to hibernate (workers, review agents; optionally all)
2903+ var agentsToHibernate []map [string ]interface {}
2904+ var agentsWithChanges []map [string ]interface {}
2905+
2906+ for _ , agent := range agents {
2907+ agentMap , ok := agent .(map [string ]interface {})
2908+ if ! ok {
2909+ continue
2910+ }
2911+
2912+ agentType , _ := agentMap ["type" ].(string )
2913+ wtPath , _ := agentMap ["worktree_path" ].(string )
2914+
2915+ // Determine if this agent should be hibernated
2916+ shouldHibernate := false
2917+ switch agentType {
2918+ case "worker" , "review" :
2919+ shouldHibernate = true
2920+ case "supervisor" , "merge-queue" , "pr-shepherd" , "workspace" , "generic-persistent" :
2921+ shouldHibernate = hibernateAll
2922+ }
2923+
2924+ if ! shouldHibernate {
2925+ continue
2926+ }
2927+
2928+ agentsToHibernate = append (agentsToHibernate , agentMap )
2929+
2930+ // Check for uncommitted changes
2931+ if wtPath != "" {
2932+ hasUncommitted , err := worktree .HasUncommittedChanges (wtPath )
2933+ if err == nil && hasUncommitted {
2934+ agentsWithChanges = append (agentsWithChanges , agentMap )
2935+ }
2936+ }
2937+ }
2938+
2939+ if len (agentsToHibernate ) == 0 {
2940+ fmt .Printf ("No agents to hibernate in repository '%s'\n " , repoName )
2941+ if ! hibernateAll {
2942+ fmt .Println ("Use --all to also hibernate persistent agents (supervisor, workspace, etc.)" )
2943+ }
2944+ return nil
2945+ }
2946+
2947+ // Show summary and confirm
2948+ fmt .Printf ("Hibernating %d agent(s) in repository '%s':\n " , len (agentsToHibernate ), repoName )
2949+ for _ , agent := range agentsToHibernate {
2950+ name , _ := agent ["name" ].(string )
2951+ agentType , _ := agent ["type" ].(string )
2952+ hasChanges := false
2953+ for _ , changed := range agentsWithChanges {
2954+ if changed ["name" ] == name {
2955+ hasChanges = true
2956+ break
2957+ }
2958+ }
2959+ changeMarker := ""
2960+ if hasChanges {
2961+ changeMarker = " [has uncommitted changes]"
2962+ }
2963+ fmt .Printf (" - %s (%s)%s\n " , name , agentType , changeMarker )
2964+ }
2965+
2966+ if len (agentsWithChanges ) > 0 {
2967+ fmt .Printf ("\n %d agent(s) have uncommitted changes that will be archived.\n " , len (agentsWithChanges ))
2968+ }
2969+
2970+ if ! skipConfirm {
2971+ fmt .Print ("\n Continue? [y/N]: " )
2972+ var response string
2973+ fmt .Scanln (& response )
2974+ if response != "y" && response != "Y" {
2975+ fmt .Println ("Cancelled" )
2976+ return nil
2977+ }
2978+ }
2979+
2980+ // Create archive directory with timestamp
2981+ timestamp := time .Now ().Format ("2006-01-02_15-04-05" )
2982+ archiveDir := filepath .Join (c .paths .RepoArchiveDir (repoName ), timestamp )
2983+ if len (agentsWithChanges ) > 0 {
2984+ if err := os .MkdirAll (archiveDir , 0755 ); err != nil {
2985+ return fmt .Errorf ("failed to create archive directory: %w" , err )
2986+ }
2987+ fmt .Printf ("\n Archiving to: %s\n " , archiveDir )
2988+ }
2989+
2990+ // Archive uncommitted changes
2991+ var archivedAgents []string
2992+ for _ , agent := range agentsWithChanges {
2993+ name , _ := agent ["name" ].(string )
2994+ wtPath , _ := agent ["worktree_path" ].(string )
2995+ branch , _ := agent ["branch" ].(string )
2996+ task , _ := agent ["task" ].(string )
2997+
2998+ fmt .Printf ("Archiving changes from %s...\n " , name )
2999+
3000+ // Create patch file with git diff
3001+ patchPath := filepath .Join (archiveDir , name + ".patch" )
3002+ cmd := exec .Command ("git" , "diff" , "HEAD" )
3003+ cmd .Dir = wtPath
3004+ output , err := cmd .Output ()
3005+ if err != nil {
3006+ fmt .Printf ("Warning: failed to create patch for %s: %v\n " , name , err )
3007+ continue
3008+ }
3009+
3010+ // Include untracked files in the patch
3011+ untrackedCmd := exec .Command ("git" , "ls-files" , "--others" , "--exclude-standard" )
3012+ untrackedCmd .Dir = wtPath
3013+ untrackedOutput , _ := untrackedCmd .Output ()
3014+
3015+ // Write patch file
3016+ if err := os .WriteFile (patchPath , output , 0644 ); err != nil {
3017+ fmt .Printf ("Warning: failed to write patch for %s: %v\n " , name , err )
3018+ continue
3019+ }
3020+
3021+ // Write untracked files list if any
3022+ if len (untrackedOutput ) > 0 {
3023+ untrackedPath := filepath .Join (archiveDir , name + ".untracked" )
3024+ os .WriteFile (untrackedPath , untrackedOutput , 0644 )
3025+ }
3026+
3027+ // Write metadata for this agent
3028+ metaPath := filepath .Join (archiveDir , name + ".json" )
3029+ meta := map [string ]interface {}{
3030+ "name" : name ,
3031+ "type" : agent ["type" ],
3032+ "branch" : branch ,
3033+ "task" : task ,
3034+ "worktree_path" : wtPath ,
3035+ "archived_at" : time .Now ().Format (time .RFC3339 ),
3036+ }
3037+ metaData , _ := json .MarshalIndent (meta , "" , " " )
3038+ os .WriteFile (metaPath , metaData , 0644 )
3039+
3040+ archivedAgents = append (archivedAgents , name )
3041+ }
3042+
3043+ // Write summary metadata
3044+ if len (agentsWithChanges ) > 0 {
3045+ summaryPath := filepath .Join (archiveDir , "hibernate-summary.json" )
3046+ summary := map [string ]interface {}{
3047+ "repo" : repoName ,
3048+ "hibernated_at" : time .Now ().Format (time .RFC3339 ),
3049+ "agents_hibernated" : len (agentsToHibernate ),
3050+ "agents_archived" : archivedAgents ,
3051+ }
3052+ summaryData , _ := json .MarshalIndent (summary , "" , " " )
3053+ os .WriteFile (summaryPath , summaryData , 0644 )
3054+ }
3055+
3056+ // Stop agents
3057+ tmuxSession := sanitizeTmuxSessionName (repoName )
3058+ repoPath := c .paths .RepoDir (repoName )
3059+ wt := worktree .NewManager (repoPath )
3060+
3061+ fmt .Println ()
3062+ for _ , agent := range agentsToHibernate {
3063+ name , _ := agent ["name" ].(string )
3064+ wtPath , _ := agent ["worktree_path" ].(string )
3065+ tmuxWindow , _ := agent ["tmux_window" ].(string )
3066+
3067+ fmt .Printf ("Stopping %s...\n " , name )
3068+
3069+ // Kill tmux window
3070+ if tmuxWindow != "" {
3071+ cmd := exec .Command ("tmux" , "kill-window" , "-t" , fmt .Sprintf ("%s:%s" , tmuxSession , tmuxWindow ))
3072+ cmd .Run () // Ignore errors
3073+ }
3074+
3075+ // Remove worktree (force since we archived changes)
3076+ if wtPath != "" {
3077+ if err := wt .Remove (wtPath , true ); err != nil {
3078+ // Try harder with force
3079+ cmd := exec .Command ("git" , "worktree" , "remove" , "--force" , wtPath )
3080+ cmd .Dir = repoPath
3081+ cmd .Run ()
3082+ }
3083+ }
3084+
3085+ // Unregister from daemon (ignore errors during cleanup)
3086+ _ , _ = client .Send (socket.Request {
3087+ Command : "remove_agent" ,
3088+ Args : map [string ]interface {}{
3089+ "repo" : repoName ,
3090+ "agent" : name ,
3091+ },
3092+ })
3093+ }
3094+
3095+ fmt .Println ()
3096+ fmt .Printf ("✓ Hibernated %d agent(s) in '%s'\n " , len (agentsToHibernate ), repoName )
3097+ if len (archivedAgents ) > 0 {
3098+ fmt .Printf ("✓ Archived %d agent(s) with uncommitted changes to:\n " , len (archivedAgents ))
3099+ fmt .Printf (" %s\n " , archiveDir )
3100+ fmt .Println ("\n To restore archived patches:" )
3101+ fmt .Println (" cd <worktree>" )
3102+ fmt .Printf (" git apply %s/<agent>.patch\n " , archiveDir )
3103+ }
3104+
3105+ return nil
3106+ }
3107+
28623108// Workspace command implementations
28633109
28643110// workspaceDefault handles `multiclaude workspace` with no subcommand or `multiclaude workspace <name>`
0 commit comments