diff --git a/docs/implementation-roadmap.md b/docs/implementation-roadmap.md index 7ce5c13..679001a 100644 --- a/docs/implementation-roadmap.md +++ b/docs/implementation-roadmap.md @@ -26,11 +26,11 @@ As of now: - an execution-roadmap workflow now exists under `docs/roadmaps/active/` and `docs/roadmaps/archive/` for agent-level work traces and completion archives - `orch` now implements `run init/show`, `task add`, `dep add`, `ready`, `dispatch`, `reconcile`, `wait`, `blocked`, `answer`, `retry`, `reassign`, `cancel`, `cleanup`, and `status` - `orch` can create runs, gate tasks through dependencies, dispatch work through `inbox`, reconcile worker thread state back into task state, answer blocked tasks, retry or reassign work, cancel tasks or runs, clean attempt worktrees, and create per-attempt Git worktrees during strict dispatch -- `orch dispatch` now supports `--repo-path`, `--workspace-root`, and `--strict-worktree`, resolves committed base revisions, records workspace metadata on attempts, and writes that metadata into inbox task payloads +- `orch dispatch` now supports `--repo-path`, `--workspace-root`, and `--strict-worktree`, auto-enables strict worktree mode for code-like tasks inferred from task metadata, resolves committed base revisions, records workspace metadata on attempts, and writes that metadata into inbox task payloads - `orch wait` now blocks on run-scoped task events and reconciles inbox state while polling so leader waits can wake on worker progress without manual sleep loops -- automated integration tests now cover the main `orch` scheduler slice, including dependency gating, dispatch, blocked-answer flow, retry, reassign, cancel, cleanup, strict worktree creation, dirty-repo rejection rules, and wait wake/timeout behavior +- automated integration tests now cover the main `orch` scheduler slice, including dependency gating, dispatch, blocked-answer flow, retry, reassign, cancel, cleanup, strict worktree creation, automatic code-task worktree enablement, dirty-repo rejection rules, and wait wake/timeout behavior -This means the project now has a working `orch` core scheduler with strict worktree-backed dispatch and the main leader-side control loop, and is ready for worktree ergonomics follow-up plus council workflows. +This means the project now has a working `orch` core scheduler with automatic worktree selection for code-like tasks, strict worktree-backed dispatch, and the main leader-side control loop, and is ready for council workflows. ## Source Of Truth @@ -71,10 +71,10 @@ Current implementation status: - `Milestone 2: Shared DB Layer` is complete enough for both CLIs - `Milestone 3: Inbox Happy Path` is complete - `Milestone 4: Orch Core Scheduling` is complete for the current non-worktree scheduler scope -- `Milestone 5: Strict Worktree Support` is complete for the current explicit dispatch worktree mode +- `Milestone 5: Strict Worktree Support` is complete - `Milestone 6: Waiting Primitives` is complete -The next practical coding target is the remaining worktree ergonomics item: automatic code-task detection for worktree mode selection. +The next practical coding target is `Milestone 7: Council Review`. ### Milestone 1: Go Skeleton @@ -268,21 +268,22 @@ Definition of done: Status: -- completed for the current explicit `orch dispatch` worktree mode +- completed Completed so far: - `orch dispatch` can use `--repo-path` to target a source Git repository without relying on the caller's current working directory - `orch dispatch --strict-worktree` resolves `base_ref` to a concrete commit, defaults to `HEAD` on clean repositories, and rejects dirty repositories when `--base-ref` is omitted +- `orch dispatch` auto-selects worktree mode for code-like tasks inferred from existing task metadata such as worker role and acceptance markers - dispatch creates a fresh branch and Git worktree per attempt and persists `base_ref`, `base_commit`, `branch_name`, `worktree_path`, and `workspace_status` - dispatch writes workspace metadata into the inbox task payload for worker runtimes - reconcile now advances `workspace_status` from `created` to `active`, `completed`, or `abandoned` based on thread state - `orch cleanup` removes completed or abandoned worktrees and marks attempt workspace state as `cleaned` -- CLI integration tests cover strict worktree creation, explicit-base dispatch on dirty repos, strict dirty-repo rejection, and cleanup +- CLI integration tests cover strict worktree creation, auto-enabled worktrees for code-like tasks, explicit-base dispatch on dirty repos, strict dirty-repo rejection, and cleanup Remaining: -- automatic code-task detection so worktree mode can be selected without explicit flags +- none ### Milestone 6: Waiting Primitives @@ -334,11 +335,11 @@ Definition of done: If a new agent is taking over now, the next concrete step should be: -1. decide whether worktree mode should be selected automatically for code tasks without explicit flags -2. either implement that worktree-mode auto-selection or explicitly defer it +1. start `Milestone 7: Council Review` +2. define the persisted storage shape for grouped reviewer recommendations and council wake/tally state 3. keep the authored inbox test-plan set in `docs/tests/inbox/` synchronized if CLI behavior changes during further `orch` work -The inbox implementation and its human-readable test-plan set are already in place, and `orch` now supports strict worktree-backed dispatch plus the main leader-side control loop, so the next meaningful project step is to smooth worktree ergonomics and then move on to council workflows. +The inbox implementation and its human-readable test-plan set are already in place, and `orch` now supports the main scheduler loop plus automatic worktree selection for code-like tasks, so the next meaningful project step is the council workflow layer. ## Recommended Driver Choices diff --git a/docs/orch-cli.md b/docs/orch-cli.md index 371b4a9..c271588 100644 --- a/docs/orch-cli.md +++ b/docs/orch-cli.md @@ -187,6 +187,7 @@ Suggested flags: Behavior: - creates a new attempt +- automatically enables strict worktree mode for code-like tasks inferred from task metadata when worktree flags are omitted - resolves the source repository from `--repo-path` or the current working directory - resolves a committed base revision - creates a branch and worktree for the attempt when the task writes code diff --git a/docs/roadmaps/archive/orch-auto-code-worktree.md b/docs/roadmaps/archive/orch-auto-code-worktree.md new file mode 100644 index 0000000..4677420 --- /dev/null +++ b/docs/roadmaps/archive/orch-auto-code-worktree.md @@ -0,0 +1,59 @@ +# Orch Auto Code Worktree + +## Status + +- `completed` + +## Owner + +- codex + +## Started At + +- `2026-03-19` + +## Goal + +- automatically select worktree mode for code-like `orch` tasks so leaders do not need to pass worktree flags for common code-writing dispatches + +## Scope + +- extend dispatch-time worktree selection to infer code-like tasks from existing task metadata +- keep explicit worktree flags authoritative when provided +- add integration tests for automatic worktree enablement and non-code fallback behavior +- update the implementation roadmap and archive this workstream when complete + +## Checklist + +- [x] inspect current dispatch/worktree code and the remaining roadmap item +- [x] implement automatic code-task detection for worktree selection +- [x] add integration coverage for auto-enable and non-code fallback +- [x] run `go test ./...` +- [x] update `docs/implementation-roadmap.md` +- [x] archive this roadmap with a completion summary + +## Files + +- `docs/roadmaps/archive/orch-auto-code-worktree.md` +- `docs/implementation-roadmap.md` +- `internal/cli/orch/worktree.go` +- `internal/cli/orch/integration_test.go` + +## Decisions + +- infer code-like tasks from existing metadata instead of introducing a new required task field in this slice +- prioritize explicit worktree flags over automatic detection so users can still force or shape dispatch behavior + +## Blockers + +- none + +## Next Step + +- move to `Milestone 7: Council Review` + +## Completion Summary + +- `orch dispatch` now auto-enables strict worktree mode for code-like tasks inferred from existing task metadata when explicit worktree flags are omitted +- explicit worktree flags remain authoritative and non-code tasks still fall back to ordinary non-worktree dispatch +- integration tests now cover both auto-enabled worktree dispatch and non-code fallback behavior diff --git a/internal/cli/orch/integration_test.go b/internal/cli/orch/integration_test.go index 8f3f82d..ec7f1b9 100644 --- a/internal/cli/orch/integration_test.go +++ b/internal/cli/orch/integration_test.go @@ -728,6 +728,102 @@ func TestOrchStrictWorktreeAllowsExplicitBaseRefOnDirtyRepo(t *testing.T) { } } +func TestOrchDispatchAutoEnablesWorktreeForCodeLikeTask(t *testing.T) { + dbPath := filepath.Join(t.TempDir(), "coord.db") + repoPath := initGitRepo(t) + + runOrchCommand( + t, + "--db", dbPath, + "--json", + "run", "init", + "--run", "run_blog_auto_worktree_001", + "--goal", "Validate auto worktree detection", + ) + runOrchCommand( + t, + "--db", dbPath, + "--json", + "task", "add", + "--run", "run_blog_auto_worktree_001", + "--task", "T1", + "--title", "Implement backend API", + "--default-to", "backend-worker", + ) + + dispatchOut := runOrchCommand( + t, + "--db", dbPath, + "--json", + "dispatch", + "--run", "run_blog_auto_worktree_001", + "--task", "T1", + "--repo-path", repoPath, + ) + + var dispatchResp map[string]any + mustDecodeJSON(t, dispatchOut, &dispatchResp) + attempt := nestedValue(t, dispatchResp, "data", "attempt").(map[string]any) + + worktreePath, _ := attempt["worktree_path"].(string) + if worktreePath == "" { + t.Fatalf("expected auto-detected code task to allocate a worktree, got %#v", attempt) + } + if got, _ := attempt["workspace_status"].(string); got != "created" { + t.Fatalf("expected created workspace status, got %#v", attempt["workspace_status"]) + } + if _, err := os.Stat(worktreePath); err != nil { + t.Fatalf("stat auto worktree path %s: %v", worktreePath, err) + } +} + +func TestOrchDispatchDoesNotAutoEnableWorktreeForNonCodeTask(t *testing.T) { + t.Parallel() + + dbPath := filepath.Join(t.TempDir(), "coord.db") + repoPath := initGitRepo(t) + + runOrchCommand( + t, + "--db", dbPath, + "--json", + "run", "init", + "--run", "run_blog_auto_worktree_002", + "--goal", "Validate non-code dispatch fallback", + ) + runOrchCommand( + t, + "--db", dbPath, + "--json", + "task", "add", + "--run", "run_blog_auto_worktree_002", + "--task", "T1", + "--title", "Review QA findings", + "--summary", "Summarize test failures and next steps", + "--default-to", "qa-worker", + ) + + dispatchOut := runOrchCommand( + t, + "--db", dbPath, + "--json", + "dispatch", + "--run", "run_blog_auto_worktree_002", + "--task", "T1", + "--repo-path", repoPath, + ) + + var dispatchResp map[string]any + mustDecodeJSON(t, dispatchOut, &dispatchResp) + attempt := nestedValue(t, dispatchResp, "data", "attempt").(map[string]any) + if got, _ := attempt["worktree_path"].(string); got != "" { + t.Fatalf("expected non-code task to stay on non-worktree path, got %#v", attempt["worktree_path"]) + } + if got, _ := attempt["workspace_status"].(string); got != "" { + t.Fatalf("expected no workspace status for non-code task, got %#v", attempt["workspace_status"]) + } +} + func TestOrchWaitWakesOnBlockedEvent(t *testing.T) { t.Parallel() diff --git a/internal/cli/orch/worktree.go b/internal/cli/orch/worktree.go index b380bdb..9b7ac63 100644 --- a/internal/cli/orch/worktree.go +++ b/internal/cli/orch/worktree.go @@ -2,6 +2,7 @@ package orch import ( "context" + "encoding/json" "fmt" "os" "os/exec" @@ -15,14 +16,14 @@ import ( ) 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) + effectiveOpts, useWorktree := resolveDispatchWorktreeOptions(task, opts) + if !useWorktree { + return store.DispatchWorkspace{}, func() {}, nil + } + return provisionDispatchWorkspace(ctx, effectiveOpts, task, attemptNo) } } @@ -52,11 +53,104 @@ func newAttemptReuseWorkspacePreparer(cmd *cobra.Command, task store.Task, attem } func dispatchUsesWorktree(opts dispatchOptions) bool { - return strings.TrimSpace(opts.repoPath) != "" || - strings.TrimSpace(opts.workspaceRoot) != "" || + return strings.TrimSpace(opts.workspaceRoot) != "" || opts.strictWorktree } +func resolveDispatchWorktreeOptions(task store.Task, opts dispatchOptions) (dispatchOptions, bool) { + if dispatchUsesWorktree(opts) { + return opts, true + } + if !taskLooksLikeCodeWork(task) { + return opts, false + } + + auto := opts + auto.strictWorktree = true + return auto, true +} + +func taskLooksLikeCodeWork(task store.Task) bool { + if acceptanceJSONLooksCodeLike(task.AcceptanceJSON) { + return true + } + return roleLooksCodeLike(task.DefaultTo) +} + +func acceptanceJSONLooksCodeLike(raw json.RawMessage) bool { + if len(raw) == 0 { + return false + } + + var value any + if err := json.Unmarshal(raw, &value); err != nil { + return false + } + return acceptanceValueLooksCodeLike(value) +} + +func acceptanceValueLooksCodeLike(value any) bool { + switch typed := value.(type) { + case map[string]any: + for key, raw := range typed { + lowerKey := strings.ToLower(strings.TrimSpace(key)) + switch lowerKey { + case "code", "code_task", "writes_code", "worktree": + if boolValue, ok := raw.(bool); ok && boolValue { + return true + } + case "kind", "task_type", "mode", "type": + if stringValue, ok := raw.(string); ok && isCodeLikeMarker(stringValue) { + return true + } + } + if acceptanceValueLooksCodeLike(raw) { + return true + } + } + case []any: + for _, item := range typed { + if acceptanceValueLooksCodeLike(item) { + return true + } + } + case string: + return isCodeLikeMarker(typed) + } + return false +} + +func roleLooksCodeLike(role string) bool { + role = strings.ToLower(strings.TrimSpace(role)) + if role == "" { + return false + } + + for _, token := range splitIdentifierTokens(role) { + switch token { + case "backend", "frontend", "front", "admin", "ui", "fullstack", "foundation", "db", "database", "mobile", "ios", "android", "web", "platform", "infra", "api": + return true + } + } + return false +} + +func isCodeLikeMarker(value string) bool { + value = strings.ToLower(strings.TrimSpace(value)) + switch value { + case "code", "code_task", "code-task", "code-change", "code_change", "implementation", "patch", "diff", "repo": + return true + default: + return false + } +} + +func splitIdentifierTokens(value string) []string { + return strings.FieldsFunc(value, func(r rune) bool { + return !((r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')) + }) +} + 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 {