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) <noreply@anthropic.com>
This commit is contained in:
yushen 2026-03-26 16:04:33 +08:00
parent aa3f927a37
commit 7b4a73c989
7 changed files with 15 additions and 36 deletions

View file

@ -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)

View file

@ -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"),

View file

@ -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,

View file

@ -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,

View file

@ -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.

View file

@ -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"},

View file

@ -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.