Auto-enable worktrees for code tasks

This commit is contained in:
2026-03-19 14:36:06 +08:00
parent ae272855f6
commit 10ffb13f75
5 changed files with 269 additions and 18 deletions
+12 -11
View File
@@ -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
+1
View File
@@ -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
@@ -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
+96
View File
@@ -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()
+101 -7
View File
@@ -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 {