Skip to content

Commit 5c4e111

Browse files
committed
Merge PR dlorenc#339: Context-aware refresh
2 parents be18f85 + f061129 commit 5c4e111

2 files changed

Lines changed: 130 additions & 4 deletions

File tree

internal/cli/cli.go

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,7 @@ Example:
752752
c.rootCmd.Subcommands["refresh"] = &Command{
753753
Name: "refresh",
754754
Description: "Sync agent worktrees with main branch",
755-
Usage: "multiclaude refresh",
755+
Usage: "multiclaude refresh [--all]",
756756
Run: c.refresh,
757757
Category: "maint",
758758
}
@@ -5466,16 +5466,127 @@ func (c *CLI) repair(args []string) error {
54665466
return nil
54675467
}
54685468

5469-
// refresh triggers an immediate worktree sync for all agents
5469+
// refresh syncs worktrees with main branch.
5470+
// When run inside an agent worktree, refreshes just that worktree directly.
5471+
// When run outside an agent context (or with --all), triggers global refresh via daemon.
54705472
func (c *CLI) refresh(args []string) error {
5473+
flags, _ := ParseFlags(args)
5474+
refreshAll := flags["all"] == "true"
5475+
5476+
// If --all not specified, try to detect agent context
5477+
if !refreshAll {
5478+
cwd, err := os.Getwd()
5479+
if err == nil {
5480+
// Resolve symlinks for proper path comparison
5481+
if resolved, err := filepath.EvalSymlinks(cwd); err == nil {
5482+
cwd = resolved
5483+
}
5484+
5485+
// Check if we're in a worktree path: ~/.multiclaude/wts/<repo>/<agent>
5486+
if hasPathPrefix(cwd, c.paths.WorktreesDir) {
5487+
rel, err := filepath.Rel(c.paths.WorktreesDir, cwd)
5488+
if err == nil {
5489+
parts := strings.SplitN(rel, string(filepath.Separator), 2)
5490+
if len(parts) >= 2 && parts[0] != "" && parts[1] != "" {
5491+
repoName := parts[0]
5492+
agentName := strings.SplitN(parts[1], string(filepath.Separator), 2)[0]
5493+
return c.refreshAgentWorktree(repoName, agentName, cwd)
5494+
}
5495+
}
5496+
}
5497+
}
5498+
}
5499+
5500+
// Global refresh via daemon
5501+
return c.refreshAllWorktrees()
5502+
}
5503+
5504+
// refreshAgentWorktree refreshes a single agent's worktree directly
5505+
func (c *CLI) refreshAgentWorktree(repoName, agentName, wtPath string) error {
5506+
fmt.Printf("Refreshing worktree for %s/%s...\n", repoName, agentName)
5507+
5508+
// Get the repo path to determine remote/branch
5509+
repoPath := c.paths.RepoDir(repoName)
5510+
wt := worktree.NewManager(repoPath)
5511+
5512+
// Get remote and main branch
5513+
remote, err := wt.GetUpstreamRemote()
5514+
if err != nil {
5515+
return fmt.Errorf("failed to get remote: %w", err)
5516+
}
5517+
5518+
mainBranch, err := wt.GetDefaultBranch(remote)
5519+
if err != nil {
5520+
return fmt.Errorf("failed to get default branch: %w", err)
5521+
}
5522+
5523+
// Fetch latest from remote
5524+
fmt.Printf("Fetching from %s...\n", remote)
5525+
if err := wt.FetchRemote(remote); err != nil {
5526+
return fmt.Errorf("failed to fetch: %w", err)
5527+
}
5528+
5529+
// Check worktree state
5530+
wtState, err := worktree.GetWorktreeState(wtPath, remote, mainBranch)
5531+
if err != nil {
5532+
return fmt.Errorf("failed to get worktree state: %w", err)
5533+
}
5534+
5535+
if !wtState.CanRefresh {
5536+
fmt.Printf("✓ No refresh needed: %s\n", wtState.RefreshReason)
5537+
return nil
5538+
}
5539+
5540+
if wtState.CommitsBehind == 0 {
5541+
fmt.Println("✓ Already up to date")
5542+
return nil
5543+
}
5544+
5545+
fmt.Printf("Rebasing onto %s/%s (%d commits behind)...\n", remote, mainBranch, wtState.CommitsBehind)
5546+
5547+
// Perform the refresh
5548+
result := worktree.RefreshWorktree(wtPath, remote, mainBranch)
5549+
5550+
if result.Error != nil {
5551+
if result.HasConflicts {
5552+
fmt.Println("\n⚠ Rebase has conflicts in:")
5553+
for _, f := range result.ConflictFiles {
5554+
fmt.Printf(" - %s\n", f)
5555+
}
5556+
fmt.Println("\nResolve conflicts and run 'git rebase --continue', or 'git rebase --abort' to cancel.")
5557+
return fmt.Errorf("rebase conflicts")
5558+
}
5559+
return fmt.Errorf("refresh failed: %w", result.Error)
5560+
}
5561+
5562+
if result.Skipped {
5563+
fmt.Printf("✓ Skipped: %s\n", result.SkipReason)
5564+
return nil
5565+
}
5566+
5567+
fmt.Printf("✓ Successfully rebased %d commits\n", result.CommitsRebased)
5568+
if result.WasStashed {
5569+
if result.StashRestored {
5570+
fmt.Println(" (uncommitted changes were stashed and restored)")
5571+
} else {
5572+
fmt.Println(" ⚠ Warning: uncommitted changes were stashed but could not be restored")
5573+
fmt.Println(" Run 'git stash pop' to restore them manually")
5574+
}
5575+
}
5576+
5577+
return nil
5578+
}
5579+
5580+
// refreshAllWorktrees triggers a global refresh via the daemon
5581+
func (c *CLI) refreshAllWorktrees() error {
54715582
// Connect to daemon
54725583
client := socket.NewClient(c.paths.DaemonSock)
54735584
_, err := client.Send(socket.Request{Command: "ping"})
54745585
if err != nil {
54755586
return errors.DaemonNotRunning()
54765587
}
54775588

5478-
fmt.Println("Triggering worktree refresh...")
5589+
fmt.Println("Triggering worktree refresh for all agents...")
54795590

54805591
resp, err := client.Send(socket.Request{
54815592
Command: "trigger_refresh",

internal/prompts/commands/refresh.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,22 @@
22

33
Sync your worktree with the latest changes from the main branch.
44

5-
## Instructions
5+
## Quick Method (Recommended)
6+
7+
Run this CLI command - it handles everything automatically:
8+
9+
```bash
10+
multiclaude refresh
11+
```
12+
13+
This will:
14+
- Detect your worktree context automatically
15+
- Fetch from the correct remote (upstream if fork, otherwise origin)
16+
- Stash any uncommitted changes
17+
- Rebase your branch onto main
18+
- Restore stashed changes
19+
20+
## Manual Instructions (Alternative)
621

722
1. First, determine the correct remote to use. Check if an upstream remote exists (indicates a fork):
823
```bash

0 commit comments

Comments
 (0)