diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 3e06628..01d8b61 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -73,11 +73,12 @@ func IsDevVersion() bool { // Command represents a CLI command type Command struct { - Name string - Description string - Usage string - Run func(args []string) error - Subcommands map[string]*Command + Name string + Description string + LongDescription string // Detailed help text shown with --help + Usage string + Run func(args []string) error + Subcommands map[string]*Command } // CLI manages the command-line interface @@ -295,6 +296,11 @@ func (c *CLI) showCommandHelp(cmd *Command) error { fmt.Println() } + if cmd.LongDescription != "" { + fmt.Println(cmd.LongDescription) + fmt.Println() + } + if len(cmd.Subcommands) > 0 { fmt.Println("Subcommands:") for name, subcmd := range cmd.Subcommands { @@ -438,8 +444,27 @@ func (c *CLI) registerCommands() { repoCmd.Subcommands["hibernate"] = &Command{ Name: "hibernate", Description: "Hibernate a repository, archiving uncommitted changes", - Usage: "multiclaude repo hibernate [--repo ] [--all] [--yes]", - Run: c.hibernateRepo, + LongDescription: `Hibernating a repository STOPS ALL TOKEN CONSUMPTION by killing agents. + +Running agents (supervisor, merge-queue, workspace, workers) continuously +consume API tokens even when idle. Use hibernate to pause billing. + +Options: + --repo Repository to hibernate (auto-detected if in worktree) + --all Also hibernate persistent agents (supervisor, workspace) + --yes Skip confirmation prompt + +By default, only workers and review agents are hibernated. Use --all to +stop ALL agents including supervisor, merge-queue, and workspace. + +Uncommitted changes are archived to ~/.multiclaude/archives// and +can be recovered later. + +Example: + multiclaude repo hibernate --all # Stop all agents, stop token usage + multiclaude repo hibernate # Stop workers only, core agents remain`, + Usage: "multiclaude repo hibernate [--repo ] [--all] [--yes]", + Run: c.hibernateRepo, } c.rootCmd.Subcommands["repo"] = repoCmd @@ -891,6 +916,9 @@ func (c *CLI) systemStatus(args []string) error { fmt.Printf(" Repos: %d\n", len(repos)) fmt.Println() + // Track total active agents for token warning + totalActiveAgents := 0 + // Show each repo with agents for _, repo := range repos { repoMap, ok := repo.(map[string]interface{}) @@ -903,10 +931,8 @@ func (c *CLI) systemStatus(args []string) error { if v, ok := repoMap["total_agents"].(float64); ok { totalAgents = int(v) } - workerCount := 0 - if v, ok := repoMap["worker_count"].(float64); ok { - workerCount = int(v) - } + totalActiveAgents += totalAgents + sessionHealthy, _ := repoMap["session_healthy"].(bool) // Repo line @@ -916,12 +942,31 @@ func (c *CLI) systemStatus(args []string) error { } fmt.Printf(" %s %s\n", repoStatus, format.Bold.Sprint(name)) - // Agent summary - coreAgents := totalAgents - workerCount - if coreAgents < 0 { - coreAgents = 0 + // Show core agents by name and type + if coreAgents, ok := repoMap["core_agents"].([]interface{}); ok && len(coreAgents) > 0 { + var coreNames []string + for _, ca := range coreAgents { + if caMap, ok := ca.(map[string]interface{}); ok { + agentName, _ := caMap["name"].(string) + agentType, _ := caMap["type"].(string) + coreNames = append(coreNames, fmt.Sprintf("%s (%s)", agentName, agentType)) + } + } + fmt.Printf(" Core: %s\n", strings.Join(coreNames, ", ")) + } + + // Show workers + if workerNames, ok := repoMap["worker_names"].([]interface{}); ok && len(workerNames) > 0 { + var names []string + for _, wn := range workerNames { + if name, ok := wn.(string); ok { + names = append(names, name) + } + } + fmt.Printf(" Workers: %s\n", strings.Join(names, ", ")) + } else { + fmt.Printf(" Workers: none\n") } - fmt.Printf(" Agents: %d core, %d workers\n", coreAgents, workerCount) // Show fork info if applicable if isFork, _ := repoMap["is_fork"].(bool); isFork { @@ -934,6 +979,16 @@ func (c *CLI) systemStatus(args []string) error { } fmt.Println() + + // Token consumption warning + if totalActiveAgents > 0 { + fmt.Printf(" %s %d active agent(s) consuming API tokens\n", + format.Yellow.Sprint("⚠"), + totalActiveAgents) + format.Dimmed(" Stop token usage: multiclaude repo hibernate --all") + fmt.Println() + } + format.Dimmed("Details: multiclaude repo list | multiclaude worker list") return nil } diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index c755412..521f832 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -728,12 +728,17 @@ func (d *Daemon) handleListRepos(req socket.Request) socket.Response { // Return detailed repo info repoDetails := make([]map[string]interface{}, 0, len(repos)) for repoName, repo := range repos { - // Count agents by type - workerCount := 0 - totalAgents := len(repo.Agents) - for _, agent := range repo.Agents { + // Group agents by type + workerNames := []string{} + coreAgents := []map[string]string{} // name -> type for core agents + for agentName, agent := range repo.Agents { if agent.Type == state.AgentTypeWorker { - workerCount++ + workerNames = append(workerNames, agentName) + } else { + coreAgents = append(coreAgents, map[string]string{ + "name": agentName, + "type": string(agent.Type), + }) } } @@ -753,8 +758,10 @@ func (d *Daemon) handleListRepos(req socket.Request) socket.Response { "name": repoName, "github_url": repo.GithubURL, "tmux_session": repo.TmuxSession, - "total_agents": totalAgents, - "worker_count": workerCount, + "total_agents": len(repo.Agents), + "worker_count": len(workerNames), + "worker_names": workerNames, + "core_agents": coreAgents, "session_healthy": sessionHealthy, "is_fork": repo.ForkConfig.IsFork, "upstream_owner": repo.ForkConfig.UpstreamOwner,