diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c5f8e..e0dec76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed + +- `orc validate` no longer rejects a workspace-level `next_action.cwd` (such as + `"."`) when the feature also has a worktree. Workspace-level stages like + qa-automation intentionally run at the workspace root, which `ResolveCWD` + already blesses; validation now agrees with it. + ## [0.5.0] - 2026-06-19 ### Added diff --git a/internal/state/state.go b/internal/state/state.go index 5f164d2..62f3e2f 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -608,7 +608,11 @@ func ValidateRepos(s *State, workspaceRoot string) error { } } - // next_action.cwd must be under a recorded worktree when any worktree is set + // next_action.cwd must resolve to the workspace root or under a recorded + // worktree when any worktree is set. The workspace root is allowed because + // workspace-level stages (e.g. qa-automation) intentionally run there via + // cwd "." even when the feature has a worktree; this mirrors ResolveCWD, + // which maps "."/"" to the workspace root. hasWorktrees := false for _, r := range s.Repos { if r.Worktree != "" { @@ -621,8 +625,13 @@ func ValidateRepos(s *State, workspaceRoot string) error { if !filepath.IsAbs(cwd) { cwd = filepath.Join(workspaceRoot, cwd) } - matched := false + cwd = filepath.Clean(cwd) + // A cwd at the workspace root is a valid workspace-level stage. + matched := cwd == filepath.Clean(workspaceRoot) for _, r := range s.Repos { + if matched { + break + } if r.Worktree == "" { continue } diff --git a/internal/state/state_test.go b/internal/state/state_test.go index d524526..9c1d96b 100644 --- a/internal/state/state_test.go +++ b/internal/state/state_test.go @@ -513,6 +513,31 @@ func TestValidateRepos_CWDNotUnderWorktree(t *testing.T) { } } +func TestValidateRepos_CWDWorkspaceRootWithWorktree(t *testing.T) { + root := t.TempDir() + wt := filepath.Join(root, "worktrees", "my-app", "TICKET-1") + if err := os.MkdirAll(wt, 0755); err != nil { + t.Fatal(err) + } + mainPath := filepath.Join(root, "my-app") + if err := os.MkdirAll(mainPath, 0755); err != nil { + t.Fatal(err) + } + // A workspace-level stage (e.g. qa-automation) intentionally runs at the + // workspace root via cwd "." even when the feature has a worktree. ResolveCWD + // blesses this, so validation must accept it rather than reject it as "not + // under any worktree". + s := &state.State{ + Repos: map[string]state.Repo{ + "my-app": {Main: mainPath, Worktree: wt, Branch: "feature/x"}, + }, + NextAction: state.NextAction{CWD: "."}, + } + if err := state.ValidateRepos(s, root); err != nil { + t.Errorf("expected nil for workspace-root cwd with worktree, got %v", err) + } +} + func TestValidateRepos_CWDSkippedWhenNoWorktrees(t *testing.T) { root := t.TempDir() // Repos set but no worktrees recorded — cwd check should be skipped