package orch import ( "context" "fmt" "os" "os/exec" "path/filepath" "strings" "ai-workflow-skill/internal/protocol" "ai-workflow-skill/internal/store" "github.com/spf13/cobra" ) func newDispatchWorkspacePreparer(cmd *cobra.Command, opts dispatchOptions) store.DispatchWorkspacePreparer { if !dispatchUsesWorktree(opts) { return nil } ctx := cmd.Context() return func(task store.Task, attemptNo int) (store.DispatchWorkspace, func(), error) { return provisionDispatchWorkspace(ctx, opts, task, attemptNo) } } func newAttemptReuseWorkspacePreparer(cmd *cobra.Command, task store.Task, attempt *store.TaskAttempt) store.DispatchWorkspacePreparer { if attempt == nil || attempt.WorktreePath == "" { return nil } workspaceRoot, ok := deriveWorkspaceRootFromAttempt(task.RunID, task.TaskID, attempt.WorktreePath) if !ok { return nil } baseRef := attempt.BaseRef if strings.TrimSpace(baseRef) == "" { baseRef = attempt.BaseCommit } opts := dispatchOptions{ repoPath: attempt.WorktreePath, workspaceRoot: workspaceRoot, strictWorktree: true, baseRef: baseRef, } return newDispatchWorkspacePreparer(cmd, opts) } func dispatchUsesWorktree(opts dispatchOptions) bool { return strings.TrimSpace(opts.repoPath) != "" || strings.TrimSpace(opts.workspaceRoot) != "" || opts.strictWorktree } func provisionDispatchWorkspace(ctx context.Context, opts dispatchOptions, task store.Task, attemptNo int) (store.DispatchWorkspace, func(), error) { repoRoot, err := resolveRepoRoot(ctx, opts.repoPath) if err != nil { return store.DispatchWorkspace{}, nil, err } workspaceRoot := resolveWorkspaceRoot(repoRoot, opts.workspaceRoot) if err := ensureWorkspaceRootIgnored(repoRoot, workspaceRoot); err != nil { return store.DispatchWorkspace{}, nil, err } baseRef, baseCommit, err := resolveDispatchBase(ctx, repoRoot, workspaceRoot, opts.baseRef, opts.strictWorktree) if err != nil { return store.DispatchWorkspace{}, nil, err } branchName := buildAttemptBranchName(task.RunID, task.TaskID, attemptNo) worktreePath := buildAttemptWorktreePath(workspaceRoot, task.RunID, task.TaskID, attemptNo) if err := os.MkdirAll(filepath.Dir(worktreePath), 0o755); err != nil { return store.DispatchWorkspace{}, nil, fmt.Errorf("create worktree parent dir: %w", err) } if _, err := os.Stat(worktreePath); err == nil { return store.DispatchWorkspace{}, nil, fmt.Errorf("%w: worktree path already exists: %s", store.ErrInvalidState, worktreePath) } else if err != nil && !os.IsNotExist(err) { return store.DispatchWorkspace{}, nil, fmt.Errorf("stat worktree path: %w", err) } if _, _, err := runGit(ctx, repoRoot, "worktree", "add", "-b", branchName, worktreePath, baseCommit); err != nil { return store.DispatchWorkspace{}, nil, err } cleanup := func() { _, _, _ = runGit(context.Background(), repoRoot, "worktree", "remove", "--force", worktreePath) _, _, _ = runGit(context.Background(), repoRoot, "branch", "-D", branchName) _ = os.RemoveAll(worktreePath) } return store.DispatchWorkspace{ BaseRef: baseRef, BaseCommit: baseCommit, BranchName: branchName, WorktreePath: worktreePath, WorkspaceStatus: "created", }, cleanup, nil } func resolveRepoRoot(ctx context.Context, repoPath string) (string, error) { startPath := strings.TrimSpace(repoPath) if startPath == "" { var err error startPath, err = os.Getwd() if err != nil { return "", fmt.Errorf("get current working directory: %w", err) } } absPath, err := filepath.Abs(startPath) if err != nil { return "", fmt.Errorf("resolve repo path: %w", err) } if _, _, err := runGit(ctx, absPath, "rev-parse", "--show-toplevel"); err != nil { return "", protocol.InvalidInput("repo-path must point to a Git worktree", err) } commonDir, err := resolveCommonGitDir(ctx, absPath) if err != nil { return "", protocol.InvalidInput("repo-path must point to a Git worktree", err) } return filepath.Dir(commonDir), nil } func resolveDispatchBase(ctx context.Context, repoRoot, workspaceRoot, requestedBaseRef string, strict bool) (string, string, error) { baseRef := strings.TrimSpace(requestedBaseRef) if baseRef != "" { baseCommit, err := resolveCommit(ctx, repoRoot, baseRef) if err != nil { return "", "", protocol.InvalidInput("base-ref must resolve to a commit", err) } return baseRef, baseCommit, nil } if strict { dirty, err := repoHasUncommittedChanges(ctx, repoRoot, workspaceRoot) if err != nil { return "", "", err } if dirty { return "", "", fmt.Errorf("%w: repository has uncommitted changes; specify --base-ref or clean the repo", store.ErrInvalidState) } } baseCommit, err := resolveCommit(ctx, repoRoot, "HEAD") if err != nil { return "", "", protocol.InvalidInput("failed to resolve HEAD commit", err) } return "HEAD", baseCommit, nil } func resolveCommit(ctx context.Context, repoRoot, ref string) (string, error) { stdout, _, err := runGit(ctx, repoRoot, "rev-parse", "--verify", ref+"^{commit}") if err != nil { return "", err } return strings.TrimSpace(stdout), nil } func repoHasUncommittedChanges(ctx context.Context, repoRoot, workspaceRoot string) (bool, error) { stdout, _, err := runGit(ctx, repoRoot, "status", "--porcelain") if err != nil { return false, fmt.Errorf("check repository status: %w", err) } for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") { line = strings.TrimSpace(line) if line == "" { continue } if len(line) >= 3 { path := strings.TrimSpace(line[3:]) if shouldIgnoreStatusPath(repoRoot, workspaceRoot, path) { continue } } return true, nil } return false, nil } func resolveWorkspaceRoot(repoRoot, configuredRoot string) string { root := strings.TrimSpace(configuredRoot) if root == "" { return filepath.Join(repoRoot, ".orch", "worktrees") } if filepath.IsAbs(root) { return root } return filepath.Join(repoRoot, root) } func ensureWorkspaceRootIgnored(repoRoot, workspaceRoot string) error { relative, err := filepath.Rel(repoRoot, workspaceRoot) if err != nil { return fmt.Errorf("resolve workspace root exclude path: %w", err) } if relative == "." || strings.HasPrefix(relative, "..") { return nil } pattern := filepath.ToSlash(relative) if !strings.HasSuffix(pattern, "/") { pattern += "/" } excludePath := filepath.Join(repoRoot, ".git", "info", "exclude") if err := os.MkdirAll(filepath.Dir(excludePath), 0o755); err != nil { return fmt.Errorf("create git info dir: %w", err) } content, err := os.ReadFile(excludePath) if err != nil && !os.IsNotExist(err) { return fmt.Errorf("read git exclude file: %w", err) } if strings.Contains(string(content), pattern) { return nil } appendContent := pattern + "\n" if len(content) > 0 && !strings.HasSuffix(string(content), "\n") { appendContent = "\n" + appendContent } file, err := os.OpenFile(excludePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return fmt.Errorf("open git exclude file: %w", err) } defer file.Close() if _, err := file.WriteString(appendContent); err != nil { return fmt.Errorf("append git exclude file: %w", err) } return nil } func shouldIgnoreStatusPath(repoRoot, workspaceRoot, statusPath string) bool { relative, err := filepath.Rel(repoRoot, workspaceRoot) if err != nil || relative == "." || strings.HasPrefix(relative, "..") { return false } relative = filepath.ToSlash(relative) statusPath = filepath.ToSlash(strings.Trim(statusPath, `"`)) return statusPath == relative || strings.HasPrefix(statusPath, relative+"/") } func buildAttemptBranchName(runID, taskID string, attemptNo int) string { return fmt.Sprintf( "orch/%s/%s/attempt-%d", sanitizeGitSegment(runID), sanitizeGitSegment(taskID), attemptNo, ) } func buildAttemptWorktreePath(workspaceRoot, runID, taskID string, attemptNo int) string { return filepath.Join( workspaceRoot, sanitizePathSegment(runID), sanitizePathSegment(taskID), fmt.Sprintf("attempt-%d", attemptNo), ) } func deriveWorkspaceRootFromAttempt(runID, taskID, worktreePath string) (string, bool) { suffix := filepath.Join( sanitizePathSegment(runID), sanitizePathSegment(taskID), filepath.Base(worktreePath), ) parent := filepath.Dir(worktreePath) if filepath.Base(parent) != sanitizePathSegment(taskID) { return "", false } runDir := filepath.Dir(parent) if filepath.Base(runDir) != sanitizePathSegment(runID) { return "", false } root := filepath.Dir(runDir) if filepath.Clean(filepath.Join(root, suffix)) != filepath.Clean(worktreePath) { return "", false } return root, true } func sanitizeGitSegment(value string) string { return sanitizeSegment(value) } func sanitizePathSegment(value string) string { return sanitizeSegment(value) } func sanitizeSegment(value string) string { value = strings.TrimSpace(value) if value == "" { return "item" } var b strings.Builder lastDash := false for _, r := range value { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { b.WriteRune(r) lastDash = false continue } if r == '-' || r == '_' || r == '.' { if !lastDash { b.WriteByte('-') lastDash = true } continue } if !lastDash { b.WriteByte('-') lastDash = true } } result := strings.Trim(b.String(), "-.") if result == "" { return "item" } if strings.HasSuffix(result, ".lock") { result = strings.TrimSuffix(result, ".lock") + "-lock" } return result } func runGit(ctx context.Context, repoRoot string, args ...string) (string, string, error) { cmdArgs := append([]string{"-C", repoRoot}, args...) cmd := exec.CommandContext(ctx, "git", cmdArgs...) output, err := cmd.CombinedOutput() if err == nil { return string(output), "", nil } message := strings.TrimSpace(string(output)) if message == "" { message = err.Error() } return "", message, fmt.Errorf("git %s: %s", strings.Join(args, " "), message) } func cleanupAttemptWorktree(ctx context.Context, attempt store.TaskAttempt, force bool) error { if strings.TrimSpace(attempt.WorktreePath) == "" { return nil } if _, err := os.Stat(attempt.WorktreePath); err != nil { if os.IsNotExist(err) { return nil } return fmt.Errorf("stat worktree path: %w", err) } repoRoot, err := resolveRepoRootFromExistingWorktree(ctx, attempt.WorktreePath) if err != nil { if force { return os.RemoveAll(attempt.WorktreePath) } return err } args := []string{"worktree", "remove"} if force { args = append(args, "--force") } args = append(args, attempt.WorktreePath) if _, _, err := runGit(ctx, repoRoot, args...); err != nil { if force { return os.RemoveAll(attempt.WorktreePath) } return err } return nil } func resolveRepoRootFromExistingWorktree(ctx context.Context, worktreePath string) (string, error) { commonDir, err := resolveCommonGitDir(ctx, worktreePath) if err != nil { return "", err } return filepath.Dir(commonDir), nil } func resolveCommonGitDir(ctx context.Context, repoPath string) (string, error) { stdout, _, err := runGit(ctx, repoPath, "rev-parse", "--path-format=absolute", "--git-common-dir") if err != nil { return "", err } commonDir := strings.TrimSpace(stdout) if !filepath.IsAbs(commonDir) { commonDir = filepath.Join(repoPath, commonDir) } return filepath.Clean(commonDir), nil }