From 7b4a73c989dcf34c73f757dcd5902c22a2e02cf1 Mon Sep 17 00:00:00 2001 From: yushen Date: Thu, 26 Mar 2026 16:04:33 +0800 Subject: [PATCH] refactor(daemon): remove global ReposRoot, use per-task RepoPath from server ReposRoot was a daemon-level config that locked all tasks to a single git repo. Replace with RepoPath in TaskContext so the server can specify the repo per task. When not provided, daemon falls back to directory mode. Co-Authored-By: Claude Opus 4.6 (1M context) --- Makefile | 2 +- server/cmd/multica/cmd_daemon.go | 2 -- server/internal/daemon/config.go | 28 +++---------------- server/internal/daemon/daemon.go | 4 +-- server/internal/daemon/execenv/execenv.go | 6 ++-- .../internal/daemon/execenv/execenv_test.go | 8 +++--- server/internal/daemon/types.go | 1 + 7 files changed, 15 insertions(+), 36 deletions(-) diff --git a/Makefile b/Makefile index 811c8637..e7952c87 100644 --- a/Makefile +++ b/Makefile @@ -115,7 +115,7 @@ dev: cd server && go run ./cmd/server daemon: - cd server && MULTICA_REPOS_ROOT="${MULTICA_REPOS_ROOT:-$(abspath .)}" go run ./cmd/multica daemon + cd server && go run ./cmd/multica daemon cli: cd server && go run ./cmd/multica $(ARGS) diff --git a/server/cmd/multica/cmd_daemon.go b/server/cmd/multica/cmd_daemon.go index 21c994e9..d1ffc8ff 100644 --- a/server/cmd/multica/cmd_daemon.go +++ b/server/cmd/multica/cmd_daemon.go @@ -22,7 +22,6 @@ var daemonCmd = &cobra.Command{ func init() { f := daemonCmd.Flags() - f.String("repos-root", "", "Base directory for task repositories (env: MULTICA_REPOS_ROOT)") f.String("config-path", "", "Path to daemon config file (env: MULTICA_DAEMON_CONFIG)") f.String("daemon-id", "", "Unique daemon identifier (env: MULTICA_DAEMON_ID)") f.String("device-name", "", "Human-readable device name (env: MULTICA_DAEMON_DEVICE_NAME)") @@ -36,7 +35,6 @@ func runDaemon(cmd *cobra.Command, _ []string) error { overrides := daemon.Overrides{ ServerURL: cli.FlagOrEnv(cmd, "server-url", "MULTICA_SERVER_URL", ""), WorkspaceID: cli.FlagOrEnv(cmd, "workspace-id", "MULTICA_WORKSPACE_ID", ""), - ReposRoot: flagString(cmd, "repos-root"), ConfigPath: flagString(cmd, "config-path"), DaemonID: flagString(cmd, "daemon-id"), DeviceName: flagString(cmd, "device-name"), diff --git a/server/internal/daemon/config.go b/server/internal/daemon/config.go index 628a31d6..83bcdc49 100644 --- a/server/internal/daemon/config.go +++ b/server/internal/daemon/config.go @@ -30,7 +30,6 @@ type Config struct { DeviceName string RuntimeName string Agents map[string]AgentEntry // "claude" -> entry, "codex" -> entry - ReposRoot string // parent directory containing all repos WorkspacesRoot string // base path for execution envs (default: ~/multica_workspaces) KeepEnvAfterTask bool // preserve env after task for debugging PollInterval time.Duration @@ -43,7 +42,6 @@ type Config struct { type Overrides struct { ServerURL string WorkspaceID string - ReposRoot string WorkspacesRoot string ConfigPath string PollInterval time.Duration @@ -118,22 +116,6 @@ func LoadConfig(overrides Overrides) (Config, error) { host = "local-machine" } - // Repos root: override > env > cwd - reposRoot := strings.TrimSpace(os.Getenv("MULTICA_REPOS_ROOT")) - if overrides.ReposRoot != "" { - reposRoot = overrides.ReposRoot - } - if reposRoot == "" { - reposRoot, err = os.Getwd() - if err != nil { - return Config{}, fmt.Errorf("resolve working directory: %w", err) - } - } - reposRoot, err = filepath.Abs(reposRoot) - if err != nil { - return Config{}, fmt.Errorf("resolve absolute repos root: %w", err) - } - // Durations: override > env > default pollInterval, err := durationFromEnv("MULTICA_DAEMON_POLL_INTERVAL", DefaultPollInterval) if err != nil { @@ -181,12 +163,11 @@ func LoadConfig(overrides Overrides) (Config, error) { workspacesRoot = overrides.WorkspacesRoot } if workspacesRoot == "" { - home, _ := os.UserHomeDir() - if home != "" { - workspacesRoot = filepath.Join(home, "multica_workspaces") - } else { - workspacesRoot = filepath.Join(reposRoot, "multica_workspaces") + home, err := os.UserHomeDir() + if err != nil { + return Config{}, fmt.Errorf("resolve home directory: %w (set MULTICA_WORKSPACES_ROOT to override)", err) } + workspacesRoot = filepath.Join(home, "multica_workspaces") } workspacesRoot, err = filepath.Abs(workspacesRoot) if err != nil { @@ -204,7 +185,6 @@ func LoadConfig(overrides Overrides) (Config, error) { DeviceName: deviceName, RuntimeName: runtimeName, Agents: agents, - ReposRoot: reposRoot, WorkspacesRoot: workspacesRoot, KeepEnvAfterTask: keepEnv, PollInterval: pollInterval, diff --git a/server/internal/daemon/daemon.go b/server/internal/daemon/daemon.go index 5b746d65..6fd2c8b7 100644 --- a/server/internal/daemon/daemon.go +++ b/server/internal/daemon/daemon.go @@ -33,7 +33,7 @@ func (d *Daemon) Run(ctx context.Context) error { for name := range d.cfg.Agents { agentNames = append(agentNames, name) } - d.logger.Info("starting daemon", "agents", agentNames, "workspace_id", d.cfg.WorkspaceID, "server", d.cfg.ServerBaseURL, "repos_root", d.cfg.ReposRoot) + d.logger.Info("starting daemon", "agents", agentNames, "workspace_id", d.cfg.WorkspaceID, "server", d.cfg.ServerBaseURL) if strings.TrimSpace(d.cfg.WorkspaceID) == "" { workspaceID, err := d.ensurePaired(ctx) @@ -279,7 +279,7 @@ func (d *Daemon) runTask(ctx context.Context, task Task) (TaskResult, error) { } env, err := execenv.Prepare(execenv.PrepareParams{ WorkspacesRoot: d.cfg.WorkspacesRoot, - ReposRoot: d.cfg.ReposRoot, + RepoPath: task.Context.RepoPath, TaskID: task.ID, AgentName: task.Context.Agent.Name, Task: taskCtx, diff --git a/server/internal/daemon/execenv/execenv.go b/server/internal/daemon/execenv/execenv.go index a5f427c8..3d56fa46 100644 --- a/server/internal/daemon/execenv/execenv.go +++ b/server/internal/daemon/execenv/execenv.go @@ -21,7 +21,7 @@ const ( // PrepareParams holds all inputs needed to set up an execution environment. type PrepareParams struct { WorkspacesRoot string // base path for all envs (e.g., ~/multica_workspaces) - ReposRoot string // source git repo (for worktree creation) + RepoPath string // source git repo path (for worktree creation), provided per-task by server TaskID string // task UUID — used for directory name AgentName string // for git branch naming only Task TaskContextForEnv // context data for writing files @@ -100,8 +100,8 @@ func Prepare(params PrepareParams, logger *slog.Logger) (*Environment, error) { } // Detect git repo and set up worktree if available. - if params.ReposRoot != "" { - if gitRoot, ok := detectGitRepo(params.ReposRoot); ok { + if params.RepoPath != "" { + if gitRoot, ok := detectGitRepo(params.RepoPath); ok { branchName := fmt.Sprintf("agent/%s/%s", sanitizeName(params.AgentName), shortID(params.TaskID)) // Get the default branch as base ref. diff --git a/server/internal/daemon/execenv/execenv_test.go b/server/internal/daemon/execenv/execenv_test.go index 4ab8bc49..1526c53d 100644 --- a/server/internal/daemon/execenv/execenv_test.go +++ b/server/internal/daemon/execenv/execenv_test.go @@ -96,7 +96,7 @@ func TestPrepareDirectoryMode(t *testing.T) { env, err := Prepare(PrepareParams{ WorkspacesRoot: workspacesRoot, - ReposRoot: reposRoot, + RepoPath: reposRoot, TaskID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", AgentName: "Test Agent", Task: TaskContextForEnv{ @@ -176,7 +176,7 @@ func TestPrepareGitWorktreeMode(t *testing.T) { env, err := Prepare(PrepareParams{ WorkspacesRoot: workspacesRoot, - ReposRoot: reposRoot, + RepoPath: reposRoot, TaskID: "b2c3d4e5-f6a7-8901-bcde-f12345678901", AgentName: "Code Reviewer", Task: TaskContextForEnv{ @@ -334,7 +334,7 @@ func TestCleanupGitWorktree(t *testing.T) { env, err := Prepare(PrepareParams{ WorkspacesRoot: workspacesRoot, - ReposRoot: reposRoot, + RepoPath: reposRoot, TaskID: "c3d4e5f6-a7b8-9012-cdef-123456789012", AgentName: "Cleanup Test", Task: TaskContextForEnv{IssueTitle: "Cleanup test"}, @@ -477,7 +477,7 @@ func TestCleanupPreservesLogs(t *testing.T) { env, err := Prepare(PrepareParams{ WorkspacesRoot: workspacesRoot, - ReposRoot: t.TempDir(), // not a git repo + RepoPath: t.TempDir(), // not a git repo TaskID: "d4e5f6a7-b8c9-0123-defa-234567890123", AgentName: "Preserve Test", Task: TaskContextForEnv{IssueTitle: "Preserve test"}, diff --git a/server/internal/daemon/types.go b/server/internal/daemon/types.go index a9774506..47bdb9f1 100644 --- a/server/internal/daemon/types.go +++ b/server/internal/daemon/types.go @@ -49,6 +49,7 @@ type TaskContext struct { Agent AgentContext `json:"agent"` Runtime RuntimeContext `json:"runtime"` WorkspaceContext string `json:"workspace_context,omitempty"` + RepoPath string `json:"repo_path,omitempty"` } // IssueContext holds issue details for task execution.