From 2d0478d42994d4ab24a63e0d8d58269cd68aeaf5 Mon Sep 17 00:00:00 2001 From: Paul Milbank Date: Fri, 16 Jan 2026 16:37:55 +1300 Subject: [PATCH 1/3] feat: add copilot as a coding agent --- internal/config/config.go | 2 + internal/executor/agent.go | 4 +- internal/executor/copilot_agent.go | 167 +++++++++++++++++++++++++++++ pkg/telemetry/attributes.go | 1 + 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 internal/executor/copilot_agent.go diff --git a/internal/config/config.go b/internal/config/config.go index 9c8755d..b3ed2ab 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -120,6 +120,8 @@ func Load() (*Config, error) { switch cfg.AgentType { case "codex": cfg.AgentPath = "codex" + case "copilot": + cfg.AgentPath = "copilot" case "amp": cfg.AgentPath = "amp" } diff --git a/internal/executor/agent.go b/internal/executor/agent.go index dde9e7f..fd2c717 100644 --- a/internal/executor/agent.go +++ b/internal/executor/agent.go @@ -23,7 +23,7 @@ type Agent interface { // AgentConfig contains configuration for creating an agent type AgentConfig struct { - // Type is the agent type: "claude", "codex", or "amp" + // Type is the agent type: "claude", "codex", "amp", or "copilot" Type string // Path is the path to the agent binary (for claude/codex/amp CLIs) @@ -45,6 +45,8 @@ func NewAgent(cfg *AgentConfig) (Agent, error) { return NewCodexAgent(cfg.Path, cfg.Timeout), nil case "amp": return NewAmpAgent(cfg.Path, cfg.Timeout), nil + case "copilot": + return NewCopilotAgent(cfg.Path, cfg.Timeout), nil case "opencode": return NewOpenCodeAgent(cfg.Path, cfg.Timeout), nil default: diff --git a/internal/executor/copilot_agent.go b/internal/executor/copilot_agent.go new file mode 100644 index 0000000..0dc0760 --- /dev/null +++ b/internal/executor/copilot_agent.go @@ -0,0 +1,167 @@ +// Package executor provides Copilot CLI agent implementation +package executor + +import ( + "context" + "fmt" + "io" + "log" + "os" + "os/exec" + "strings" + "time" + + "github.com/cloud-shuttle/drover/pkg/telemetry" + "github.com/cloud-shuttle/drover/pkg/types" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// CopilotAgent runs tasks using GitHub Copilot CLI +type CopilotAgent struct { + copilotPath string + timeout time.Duration + verbose bool +} + +// NewCopilotAgent creates a new Copilot CLI agent +func NewCopilotAgent(copilotPath string, timeout time.Duration) *CopilotAgent { + return &CopilotAgent{ + copilotPath: copilotPath, + timeout: timeout, + verbose: false, + } +} + +// SetVerbose enables or disables verbose logging +func (a *CopilotAgent) SetVerbose(v bool) { + a.verbose = v +} + +// ExecuteWithContext runs a task with a context and returns the execution result +func (a *CopilotAgent) ExecuteWithContext(ctx context.Context, worktreePath string, task *types.Task, parentSpan ...trace.Span) *ExecutionResult { + var agentCtx context.Context + var span trace.Span + if len(parentSpan) > 0 && parentSpan[0] != nil { + agentCtx, span = telemetry.StartAgentSpan(ctx, telemetry.AgentTypeCopilot, "unknown", + attribute.String(telemetry.KeyTaskID, task.ID), + attribute.String(telemetry.KeyTaskTitle, task.Title), + ) + defer span.End() + } else { + agentCtx = ctx + span = trace.SpanFromContext(ctx) + } + + telemetry.RecordAgentPrompt(agentCtx, telemetry.AgentTypeCopilot) + + prompt := a.buildPrompt(task) + + if a.verbose { + log.Printf("🤖 Sending prompt to Copilot (length: %d chars)", len(prompt)) + log.Printf("📝 Prompt preview: %s", truncateString(prompt, 200)) + } + + args := []string{ + "-p", + prompt, + "--allow-all-paths", + "--allow-all-tools", + } + + cmd := exec.CommandContext(ctx, a.copilotPath, args...) + cmd.Dir = worktreePath + + var outputBuf, errBuf strings.Builder + cmd.Stdout = io.MultiWriter(os.Stdout, &outputBuf) + cmd.Stderr = io.MultiWriter(os.Stderr, &errBuf) + + start := time.Now() + if a.verbose { + log.Printf("⏱️ Copilot execution started at %s", start.Format("15:04:05")) + } + err := cmd.Run() + duration := time.Since(start) + + fullOutput := outputBuf.String() + errBuf.String() + + if err != nil { + exitCode := 1 + if exitError, ok := err.(*exec.ExitError); ok { + exitCode = exitError.ExitCode() + } + if a.verbose { + log.Printf("❌ Copilot exited with code %d after %v", exitCode, duration) + } + + telemetry.RecordAgentError(agentCtx, telemetry.AgentTypeCopilot, "execution_failed") + + if ctx.Err() == context.DeadlineExceeded { + telemetry.RecordError(span, err, "TimeoutError", telemetry.ErrorCategoryTimeout) + telemetry.RecordAgentDuration(agentCtx, telemetry.AgentTypeCopilot, duration) + return &ExecutionResult{ + Success: false, + Output: fullOutput, + Error: fmt.Errorf("copilot timed out after %v", duration), + } + } + telemetry.RecordError(span, err, "ExecutionError", telemetry.ErrorCategoryAgent) + telemetry.RecordAgentDuration(agentCtx, telemetry.AgentTypeCopilot, duration) + return &ExecutionResult{ + Success: false, + Output: fullOutput, + Error: fmt.Errorf("copilot failed after %v: %w", duration, err), + } + } + + if a.verbose { + log.Printf("✅ Copilot completed successfully in %v", duration) + } + + telemetry.RecordAgentDuration(agentCtx, telemetry.AgentTypeCopilot, duration) + + return &ExecutionResult{ + Success: true, + Output: fullOutput, + Error: nil, + Duration: duration, + } +} + +// CheckInstalled verifies Copilot CLI is available +func (a *CopilotAgent) CheckInstalled() error { + cmd := exec.Command(a.copilotPath, "--version") + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("copilot not found at %s: %w\n%s", a.copilotPath, err, output) + } + return nil +} + +// buildPrompt creates the Copilot prompt for a task +func (a *CopilotAgent) buildPrompt(task *types.Task) string { + var prompt strings.Builder + + if task.ExecutionContext != nil && len(task.ExecutionContext.Guidance) > 0 { + prompt.WriteString("=== HUMAN GUIDANCE ===\n") + prompt.WriteString("The following guidance has been provided for this task:\n\n") + for i, msg := range task.ExecutionContext.Guidance { + prompt.WriteString(fmt.Sprintf("[%d] %s\n", i+1, msg.Message)) + } + prompt.WriteString("======================\n\n") + } + + prompt.WriteString(fmt.Sprintf("Task: %s\n", task.Title)) + + if task.Description != "" { + prompt.WriteString(fmt.Sprintf("Description: %s\n", task.Description)) + } + + prompt.WriteString("\nPlease implement this task completely.") + + if len(task.EpicID) > 0 { + prompt.WriteString(fmt.Sprintf("\n\nThis task is part of epic: %s", task.EpicID)) + } + + return prompt.String() +} diff --git a/pkg/telemetry/attributes.go b/pkg/telemetry/attributes.go index 8348449..253dc06 100644 --- a/pkg/telemetry/attributes.go +++ b/pkg/telemetry/attributes.go @@ -58,6 +58,7 @@ const ( AgentTypeCodex = "codex" AgentTypeAmp = "amp" AgentTypeOpenCode = "opencode" + AgentTypeCopilot = "copilot" // Error categories ErrorCategoryAgent = "agent" From 7cda8c5707b2dc7faba5b177c99550812c8241ee Mon Sep 17 00:00:00 2001 From: Paul Milbank Date: Fri, 16 Jan 2026 16:38:27 +1300 Subject: [PATCH 2/3] fix: commit formatting changes --- internal/config/config.go | 58 +++++++++++++++++------------------ pkg/telemetry/attributes.go | 60 ++++++++++++++++++------------------- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index b3ed2ab..b3d428a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -22,18 +22,18 @@ type Config struct { MaxTaskAttempts int // Retry settings - ClaimTimeout time.Duration - StallTimeout time.Duration - PollInterval time.Duration - AutoUnblock bool + ClaimTimeout time.Duration + StallTimeout time.Duration + PollInterval time.Duration + AutoUnblock bool // Git settings WorktreeDir string // Agent settings - AgentType string // "claude", "codex", or "amp" - AgentPath string // path to agent binary - ClaudePath string // deprecated: use AgentPath instead + AgentType string // "claude", "codex", "copilot", "amp" or "opencode" + AgentPath string // path to agent binary + ClaudePath string // deprecated: use AgentPath instead // Beads sync settings AutoSyncBeads bool @@ -45,34 +45,34 @@ type Config struct { Verbose bool // Worktree pool settings - PoolEnabled bool - PoolMinSize int - PoolMaxSize int - PoolWarmup time.Duration + PoolEnabled bool + PoolMinSize int + PoolMaxSize int + PoolWarmup time.Duration PoolCleanupOnExit bool } // Load loads configuration from environment and defaults func Load() (*Config, error) { cfg := &Config{ - DatabaseURL: defaultDatabaseURL(), - Workers: 3, - TaskTimeout: 60 * time.Minute, - MaxTaskAttempts: 3, - ClaimTimeout: 5 * time.Minute, - StallTimeout: 5 * time.Minute, - PollInterval: 2 * time.Second, - AutoUnblock: true, - WorktreeDir: ".drover/worktrees", - AgentType: "claude", // Default to Claude for backwards compatibility - AgentPath: "claude", // Will be resolved based on AgentType - ClaudePath: "claude", // Deprecated but kept for backwards compatibility - AutoSyncBeads: false, // Default to off for backwards compatibility - PoolEnabled: false, // Worktree pooling disabled by default - PoolMinSize: 2, // Minimum warm worktrees - PoolMaxSize: 10, // Maximum pooled worktrees - PoolWarmup: 5 * time.Minute, - PoolCleanupOnExit: true, // Clean up pooled worktrees on exit + DatabaseURL: defaultDatabaseURL(), + Workers: 3, + TaskTimeout: 60 * time.Minute, + MaxTaskAttempts: 3, + ClaimTimeout: 5 * time.Minute, + StallTimeout: 5 * time.Minute, + PollInterval: 2 * time.Second, + AutoUnblock: true, + WorktreeDir: ".drover/worktrees", + AgentType: "claude", // Default to Claude for backwards compatibility + AgentPath: "claude", // Will be resolved based on AgentType + ClaudePath: "claude", // Deprecated but kept for backwards compatibility + AutoSyncBeads: false, // Default to off for backwards compatibility + PoolEnabled: false, // Worktree pooling disabled by default + PoolMinSize: 2, // Minimum warm worktrees + PoolMaxSize: 10, // Maximum pooled worktrees + PoolWarmup: 5 * time.Minute, + PoolCleanupOnExit: true, // Clean up pooled worktrees on exit } // Environment overrides diff --git a/pkg/telemetry/attributes.go b/pkg/telemetry/attributes.go index 253dc06..ad67e1a 100644 --- a/pkg/telemetry/attributes.go +++ b/pkg/telemetry/attributes.go @@ -6,44 +6,44 @@ import "go.opentelemetry.io/otel/attribute" // Semantic convention keys for Drover-specific attributes const ( // Project attributes - KeyProjectID = "drover.project.id" - KeyProjectPath = "drover.project.path" - KeyProjectName = "drover.project.name" + KeyProjectID = "drover.project.id" + KeyProjectPath = "drover.project.path" + KeyProjectName = "drover.project.name" // Workflow attributes - KeyWorkflowID = "drover.workflow.id" - KeyWorkflowType = "drover.workflow.type" + KeyWorkflowID = "drover.workflow.id" + KeyWorkflowType = "drover.workflow.type" // Task attributes - KeyTaskID = "drover.task.id" - KeyTaskTitle = "drover.task.title" - KeyTaskState = "drover.task.state" - KeyTaskType = "drover.task.type" - KeyTaskPriority = "drover.task.priority" - KeyTaskAttempt = "drover.task.attempt" - KeyEpicID = "drover.epic.id" + KeyTaskID = "drover.task.id" + KeyTaskTitle = "drover.task.title" + KeyTaskState = "drover.task.state" + KeyTaskType = "drover.task.type" + KeyTaskPriority = "drover.task.priority" + KeyTaskAttempt = "drover.task.attempt" + KeyEpicID = "drover.epic.id" // Worker attributes - KeyWorkerID = "drover.worker.id" - KeyWorkerCount = "drover.worker.count" + KeyWorkerID = "drover.worker.id" + KeyWorkerCount = "drover.worker.count" // Worktree attributes - KeyWorktreePath = "drover.worktree.path" - KeyWorktreeID = "drover.worktree.id" + KeyWorktreePath = "drover.worktree.path" + KeyWorktreeID = "drover.worktree.id" // Agent attributes - KeyAgentType = "drover.agent.type" - KeyAgentModel = "drover.agent.model" - KeyAgentPromptID = "drover.agent.prompt.id" + KeyAgentType = "drover.agent.type" + KeyAgentModel = "drover.agent.model" + KeyAgentPromptID = "drover.agent.prompt.id" // Blocker attributes - KeyBlockerType = "drover.blocker.type" - KeyBlockerTaskID = "drover.blocker.task_id" - KeyBlockerReason = "drover.blocker.reason" + KeyBlockerType = "drover.blocker.type" + KeyBlockerTaskID = "drover.blocker.task_id" + KeyBlockerReason = "drover.blocker.reason" // Error attributes - KeyErrorType = "drover.error.type" - KeyErrorCategory = "drover.error.category" + KeyErrorType = "drover.error.type" + KeyErrorCategory = "drover.error.category" ) // Common attribute key values @@ -61,12 +61,12 @@ const ( AgentTypeCopilot = "copilot" // Error categories - ErrorCategoryAgent = "agent" - ErrorCategoryGit = "git" - ErrorCategoryWorktree = "worktree" - ErrorCategoryDatabase = "database" - ErrorCategoryTimeout = "timeout" - ErrorCategoryUnknown = "unknown" + ErrorCategoryAgent = "agent" + ErrorCategoryGit = "git" + ErrorCategoryWorktree = "worktree" + ErrorCategoryDatabase = "database" + ErrorCategoryTimeout = "timeout" + ErrorCategoryUnknown = "unknown" ) // TaskAttrs returns a set of attributes for a task From 67a3ec53a0eb1eb2d4dc83bee19d971b2b3652eb Mon Sep 17 00:00:00 2001 From: Paul Milbank Date: Fri, 16 Jan 2026 16:43:20 +1300 Subject: [PATCH 3/3] drover: task-1768534976872153000 Task: run go fmt on every file in the repo --- cmd/drover/commands.go | 33 ++++++----- internal/beads/sync.go | 2 +- internal/config/config_test.go | 8 +-- internal/dashboard/hooks.go | 20 +++---- internal/dashboard/queries.go | 50 ++++++++-------- internal/db/db.go | 30 +++++----- internal/db/idutil.go | 37 +++++++----- internal/db/idutil_test.go | 12 ++-- internal/executor/amp_agent.go | 6 +- internal/executor/claude.go | 18 +++--- internal/executor/claude_agent.go | 6 +- internal/executor/codex_agent.go | 6 +- internal/executor/opencode_agent.go | 6 +- internal/git/pool.go | 90 ++++++++++++++--------------- internal/git/worktree.go | 22 +++---- internal/template/task.go | 18 +++--- internal/workflow/dbos_workflow.go | 18 +++--- internal/workflow/orchestrator.go | 28 ++++----- pkg/telemetry/metrics.go | 44 +++++++------- pkg/telemetry/telemetry.go | 4 +- pkg/telemetry/tracer.go | 18 +++--- pkg/types/task.go | 62 ++++++++++---------- 22 files changed, 272 insertions(+), 266 deletions(-) diff --git a/cmd/drover/commands.go b/cmd/drover/commands.go index c664120..6977849 100644 --- a/cmd/drover/commands.go +++ b/cmd/drover/commands.go @@ -19,8 +19,8 @@ import ( "github.com/cloud-shuttle/drover/internal/db" "github.com/cloud-shuttle/drover/internal/git" "github.com/cloud-shuttle/drover/internal/template" - "github.com/cloud-shuttle/drover/pkg/types" "github.com/cloud-shuttle/drover/internal/workflow" + "github.com/cloud-shuttle/drover/pkg/types" "github.com/dbos-inc/dbos-transact-golang/dbos" "github.com/spf13/cobra" ) @@ -299,11 +299,11 @@ func runWithSQLite(cmd *cobra.Command, runCfg *config.Config, store *db.Store, p func addCmd() *cobra.Command { var ( - desc string - epicID string - parentID string - priority int - blockedBy []string + desc string + epicID string + parentID string + priority int + blockedBy []string skipValidation bool ) @@ -321,7 +321,7 @@ Hierarchical Tasks: drover add "Sub-task title" --parent task-123 Maximum depth is 2 levels (Epic → Parent → Child).`, - Args: cobra.ExactArgs(1), + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { _, store, err := requireProject() if err != nil { @@ -694,10 +694,10 @@ and other metadata. Useful for inspecting individual task details.`, func resetCmd() *cobra.Command { var ( - resetCompleted bool + resetCompleted bool resetInProgress bool - resetClaimed bool - resetFailed bool + resetClaimed bool + resetFailed bool ) command := &cobra.Command{ @@ -941,13 +941,13 @@ func exportSession(projectDir string, store *db.Store, outputPath, format string // Build session export session := map[string]interface{}{ - "version": "1.0", - "exportedAt": time.Now().Format(time.RFC3339), - "repository": projectDir, - "tasks": tasks, - "epics": epics, + "version": "1.0", + "exportedAt": time.Now().Format(time.RFC3339), + "repository": projectDir, + "tasks": tasks, + "epics": epics, "dependencies": dependencies, - "worktrees": worktrees, + "worktrees": worktrees, } // Determine output path @@ -1912,6 +1912,7 @@ func formatTimestamp(timestamp int64) string { t := time.Unix(timestamp, 0) return t.Format("2006-01-02 15:04:05") } + // worktreeCmd returns the worktree management command func worktreeCmd() *cobra.Command { cmd := &cobra.Command{ diff --git a/internal/beads/sync.go b/internal/beads/sync.go index 81bbbac..de44d81 100644 --- a/internal/beads/sync.go +++ b/internal/beads/sync.go @@ -20,7 +20,7 @@ import ( // BeadRecord represents a single line in beads.jsonl type BeadRecord struct { - Type string `json:"type"` // "bead", "epic", "link" + Type string `json:"type"` // "bead", "epic", "link" ID string `json:"id"` Timestamp time.Time `json:"timestamp"` Data json.RawMessage `json:"data"` diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 8c34abb..17ff67c 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -14,10 +14,10 @@ func TestParseIntOrDefault(t *testing.T) { {"5", 10, 5}, {"100", 0, 100}, {"-3", 10, -3}, - {"abc", 10, 10}, // invalid returns default - {"", 10, 10}, // empty returns default - {"3.14", 10, 3}, // parses integer prefix (3) - {"7xyz", 10, 7}, // parses prefix + {"abc", 10, 10}, // invalid returns default + {"", 10, 10}, // empty returns default + {"3.14", 10, 3}, // parses integer prefix (3) + {"7xyz", 10, 7}, // parses prefix } for _, tt := range tests { diff --git a/internal/dashboard/hooks.go b/internal/dashboard/hooks.go index bcfeffc..1ede475 100644 --- a/internal/dashboard/hooks.go +++ b/internal/dashboard/hooks.go @@ -26,16 +26,16 @@ func GetGlobal() *Server { // Event types for WebSocket broadcasting const ( - EventTaskClaimed = "task_claimed" - EventTaskStarted = "task_started" - EventTaskCompleted = "task_completed" - EventTaskFailed = "task_failed" - EventTaskBlocked = "task_blocked" - EventTaskPaused = "task_paused" - EventTaskResumed = "task_resumed" - EventTaskGuidance = "task_guidance" - EventWorkerStatus = "worker_status" - EventStatsUpdate = "stats_update" + EventTaskClaimed = "task_claimed" + EventTaskStarted = "task_started" + EventTaskCompleted = "task_completed" + EventTaskFailed = "task_failed" + EventTaskBlocked = "task_blocked" + EventTaskPaused = "task_paused" + EventTaskResumed = "task_resumed" + EventTaskGuidance = "task_guidance" + EventWorkerStatus = "worker_status" + EventStatsUpdate = "stats_update" ) // TaskEvent is broadcast when a task state changes diff --git a/internal/dashboard/queries.go b/internal/dashboard/queries.go index e53f80c..ec871e7 100644 --- a/internal/dashboard/queries.go +++ b/internal/dashboard/queries.go @@ -36,31 +36,31 @@ type EpicWithCount struct { // TaskWithEpic represents a task with epic information type TaskWithEpic struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - EpicID string `json:"epic_id"` - EpicTitle string `json:"epic_title"` - ParentID string `json:"parent_id"` - SequenceNumber int `json:"sequence_number"` - Priority int `json:"priority"` - Status string `json:"status"` - Attempts int `json:"attempts"` - MaxAttempts int `json:"max_attempts"` - LastError string `json:"last_error"` - ClaimedBy string `json:"claimed_by"` - ClaimedAt int64 `json:"claimed_at"` - Operator string `json:"operator"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + EpicID string `json:"epic_id"` + EpicTitle string `json:"epic_title"` + ParentID string `json:"parent_id"` + SequenceNumber int `json:"sequence_number"` + Priority int `json:"priority"` + Status string `json:"status"` + Attempts int `json:"attempts"` + MaxAttempts int `json:"max_attempts"` + LastError string `json:"last_error"` + ClaimedBy string `json:"claimed_by"` + ClaimedAt int64 `json:"claimed_at"` + Operator string `json:"operator"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } // WorkerInfo represents active worker information type WorkerInfo struct { - WorkerID string `json:"worker_id"` - TaskID string `json:"task_id"` - Title string `json:"title"` - Duration int64 `json:"duration"` // Seconds since claim + WorkerID string `json:"worker_id"` + TaskID string `json:"task_id"` + Title string `json:"title"` + Duration int64 `json:"duration"` // Seconds since claim } // GraphEdge represents a dependency edge @@ -405,10 +405,10 @@ func (s *Server) getWorktreeFiles(taskID, path string) ([]WorktreeFile, error) { } files = append(files, WorktreeFile{ - Name: entry.Name(), - Path: filepath.Join(path, entry.Name()), - Type: fileType, - Size: info.Size(), + Name: entry.Name(), + Path: filepath.Join(path, entry.Name()), + Type: fileType, + Size: info.Size(), Modified: info.ModTime().Unix(), }) } diff --git a/internal/db/db.go b/internal/db/db.go index d384e44..6a831e6 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -1143,15 +1143,15 @@ func (s *Store) ListAllDependencies() ([]types.TaskDependency, error) { // WorktreeInfo represents a worktree with its metadata type WorktreeInfo struct { - TaskID string - Path string - Branch string - CreatedAt int64 - LastUsedAt int64 - Status string - DiskSize int64 - TaskStatus string - TaskTitle string + TaskID string + Path string + Branch string + CreatedAt int64 + LastUsedAt int64 + Status string + DiskSize int64 + TaskStatus string + TaskTitle string } // CreateWorktree records a new worktree in the database @@ -1679,13 +1679,13 @@ func (s *Store) ResumeTask(taskID string) error { // SessionExport represents a complete exported session type SessionExport struct { - Version string `json:"version"` - ExportedAt string `json:"exportedAt"` - Repository string `json:"repository"` - Tasks []*types.Task `json:"tasks"` - Epics []*types.Epic `json:"epics"` + Version string `json:"version"` + ExportedAt string `json:"exportedAt"` + Repository string `json:"repository"` + Tasks []*types.Task `json:"tasks"` + Epics []*types.Epic `json:"epics"` Dependencies []types.TaskDependency `json:"dependencies"` - Worktrees []*WorktreeInfo `json:"worktrees"` + Worktrees []*WorktreeInfo `json:"worktrees"` } // ImportSession imports a session from an export diff --git a/internal/db/idutil.go b/internal/db/idutil.go index 9d8558d..f138b41 100644 --- a/internal/db/idutil.go +++ b/internal/db/idutil.go @@ -18,11 +18,12 @@ var hierarchicalIDPattern = regexp.MustCompile(`^([a-z]+-\d+)(?:\.(\d+)(?:\.(\d+ // Returns: (baseID, level1Seq, level2Seq, error) // // Examples: -// "task-123" -> ("task-123", 0, 0, nil) -// "task-123.1" -> ("task-123", 1, 0, nil) -// "task-123.1.2" -> ("task-123", 1, 2, nil) -// "task-123.5.10" -> ("task-123", 5, 10, nil) -// "invalid" -> ("", 0, 0, error) +// +// "task-123" -> ("task-123", 0, 0, nil) +// "task-123.1" -> ("task-123", 1, 0, nil) +// "task-123.1.2" -> ("task-123", 1, 2, nil) +// "task-123.5.10" -> ("task-123", 5, 10, nil) +// "invalid" -> ("", 0, 0, error) func ParseHierarchicalID(id string) (string, int, int, error) { matches := hierarchicalIDPattern.FindStringSubmatch(id) if matches == nil { @@ -45,9 +46,10 @@ func ParseHierarchicalID(id string) (string, int, int, error) { // GetIDDepth returns the depth level of a hierarchical ID // Returns: -// 0 = base task (no dots) -// 1 = first-level sub-task -// 2 = second-level sub-task +// +// 0 = base task (no dots) +// 1 = first-level sub-task +// 2 = second-level sub-task func GetIDDepth(id string) int { _, level1, level2, err := ParseHierarchicalID(id) if err != nil { @@ -69,8 +71,9 @@ func GetIDDepth(id string) int { // sequence: position among siblings (1-indexed) // // Examples: -// GenerateHierarchicalID("task-123", 1, 1) -> "task-123.1" -// GenerateHierarchicalID("task-123.1", 2, 2) -> "task-123.1.2" +// +// GenerateHierarchicalID("task-123", 1, 1) -> "task-123.1" +// GenerateHierarchicalID("task-123.1", 2, 2) -> "task-123.1.2" func GenerateHierarchicalID(baseID string, level int, sequence int) string { return fmt.Sprintf("%s.%d", baseID, sequence) } @@ -94,9 +97,10 @@ func ValidateHierarchicalID(id string, maxDepth int) error { // ExtractBaseID returns the base ID without sequence numbers // // Examples: -// "task-123.1.2" -> "task-123" -// "task-123.1" -> "task-123" -// "task-123" -> "task-123" +// +// "task-123.1.2" -> "task-123" +// "task-123.1" -> "task-123" +// "task-123" -> "task-123" func ExtractBaseID(id string) string { baseID, _, _, _ := ParseHierarchicalID(id) return baseID @@ -111,9 +115,10 @@ func IsSubTask(id string) bool { // Returns empty string if the ID is already a base task (no parent) // // Examples: -// "task-123.1" -> "task-123" -// "task-123.1.2" -> "task-123.1" -// "task-123" -> "" +// +// "task-123.1" -> "task-123" +// "task-123.1.2" -> "task-123.1" +// "task-123" -> "" func GetParentIDFromHierarchicalID(id string) string { lastDot := strings.LastIndex(id, ".") if lastDot == -1 { diff --git a/internal/db/idutil_test.go b/internal/db/idutil_test.go index 7f5bde7..de6cb81 100644 --- a/internal/db/idutil_test.go +++ b/internal/db/idutil_test.go @@ -7,12 +7,12 @@ import ( func TestParseHierarchicalID(t *testing.T) { tests := []struct { - name string - input string - wantBase string - wantLevel1 int - wantLevel2 int - wantErr bool + name string + input string + wantBase string + wantLevel1 int + wantLevel2 int + wantErr bool }{ {"base task", "task-123", "task-123", 0, 0, false}, {"first level sub-task", "task-123.1", "task-123", 1, 0, false}, diff --git a/internal/executor/amp_agent.go b/internal/executor/amp_agent.go index 0c1cb26..72f27f2 100644 --- a/internal/executor/amp_agent.go +++ b/internal/executor/amp_agent.go @@ -133,9 +133,9 @@ func (a *AmpAgent) ExecuteWithContext(ctx context.Context, worktreePath string, telemetry.RecordAgentDuration(agentCtx, telemetry.AgentTypeAmp, duration) return &ExecutionResult{ - Success: true, - Output: fullOutput, - Error: nil, + Success: true, + Output: fullOutput, + Error: nil, Duration: duration, } } diff --git a/internal/executor/claude.go b/internal/executor/claude.go index 7ab6fd5..bad647b 100644 --- a/internal/executor/claude.go +++ b/internal/executor/claude.go @@ -71,17 +71,17 @@ func (e *Executor) Execute(worktreePath string, task *types.Task) *ExecutionResu if err != nil { return &ExecutionResult{ - Success: false, - Output: fullOutput, - Error: fmt.Errorf("claude failed after %v: %w", duration, err), + Success: false, + Output: fullOutput, + Error: fmt.Errorf("claude failed after %v: %w", duration, err), Duration: duration, } } return &ExecutionResult{ - Success: true, - Output: fullOutput, - Error: nil, + Success: true, + Output: fullOutput, + Error: nil, Duration: duration, } } @@ -201,9 +201,9 @@ func (e *Executor) ExecuteWithContext(ctx context.Context, worktreePath string, telemetry.RecordAgentDuration(agentCtx, telemetry.AgentTypeClaudeCode, duration) return &ExecutionResult{ - Success: true, - Output: fullOutput, - Error: nil, + Success: true, + Output: fullOutput, + Error: nil, Duration: duration, } } diff --git a/internal/executor/claude_agent.go b/internal/executor/claude_agent.go index cd2ec59..d54fb4f 100644 --- a/internal/executor/claude_agent.go +++ b/internal/executor/claude_agent.go @@ -126,9 +126,9 @@ func (a *ClaudeAgent) ExecuteWithContext(ctx context.Context, worktreePath strin telemetry.RecordAgentDuration(agentCtx, telemetry.AgentTypeClaudeCode, duration) return &ExecutionResult{ - Success: true, - Output: fullOutput, - Error: nil, + Success: true, + Output: fullOutput, + Error: nil, Duration: duration, } } diff --git a/internal/executor/codex_agent.go b/internal/executor/codex_agent.go index 24b1ec0..0b0a4e3 100644 --- a/internal/executor/codex_agent.go +++ b/internal/executor/codex_agent.go @@ -134,9 +134,9 @@ func (a *CodexAgent) ExecuteWithContext(ctx context.Context, worktreePath string telemetry.RecordAgentDuration(agentCtx, telemetry.AgentTypeCodex, duration) return &ExecutionResult{ - Success: true, - Output: fullOutput, - Error: nil, + Success: true, + Output: fullOutput, + Error: nil, Duration: duration, } } diff --git a/internal/executor/opencode_agent.go b/internal/executor/opencode_agent.go index e23f24e..b500a08 100644 --- a/internal/executor/opencode_agent.go +++ b/internal/executor/opencode_agent.go @@ -125,9 +125,9 @@ func (a *OpenCodeAgent) ExecuteWithContext(ctx context.Context, worktreePath str telemetry.RecordAgentDuration(agentCtx, telemetry.AgentTypeOpenCode, duration) return &ExecutionResult{ - Success: true, - Output: fullOutput, - Error: nil, + Success: true, + Output: fullOutput, + Error: nil, Duration: duration, } } diff --git a/internal/git/pool.go b/internal/git/pool.go index 4c21b35..02f44e2 100644 --- a/internal/git/pool.go +++ b/internal/git/pool.go @@ -53,32 +53,32 @@ func (s WorktreeState) String() string { // PooledWorktree represents a worktree in the pool type PooledWorktree struct { - ID string // Unique identifier (pool index or timestamp) - TaskID string // Currently assigned task (empty if not in use) - Path string // File system path - Branch string // Git branch name - State WorktreeState // Current state - CreatedAt time.Time // When the worktree was created - WarmedAt time.Time // When the worktree became warm - AssignedAt time.Time // When the worktree was assigned to a task - mu sync.Mutex // Protects state transitions + ID string // Unique identifier (pool index or timestamp) + TaskID string // Currently assigned task (empty if not in use) + Path string // File system path + Branch string // Git branch name + State WorktreeState // Current state + CreatedAt time.Time // When the worktree was created + WarmedAt time.Time // When the worktree became warm + AssignedAt time.Time // When the worktree was assigned to a task + mu sync.Mutex // Protects state transitions // Sync state for async git fetch - LastFetchAt time.Time // When the last fetch completed - LastFetchStatus string // Status of last fetch ("", "ok", "error") - LastFetchError string // Error message if fetch failed - IsReadOnly bool // True when sync is in progress + LastFetchAt time.Time // When the last fetch completed + LastFetchStatus string // Status of last fetch ("", "ok", "error") + LastFetchError string // Error message if fetch failed + IsReadOnly bool // True when sync is in progress } // PoolConfig holds configuration for the worktree pool type PoolConfig struct { - MinSize int // Minimum number of warm worktrees to maintain - MaxSize int // Maximum number of worktrees in the pool - ReplenishThreshold int // Create new worktree when warm count falls below this - WarmupTimeout time.Duration // Max time to wait for worktree warmup - CleanupOnExit bool // Whether to clean up pooled worktrees on exit - EnableSymlinks bool // Enable shared node_modules via symlinks - GoModCache bool // Enable Go module cache sharing - CargoTargetDir bool // Enable shared Cargo target directory for Rust projects + MinSize int // Minimum number of warm worktrees to maintain + MaxSize int // Maximum number of worktrees in the pool + ReplenishThreshold int // Create new worktree when warm count falls below this + WarmupTimeout time.Duration // Max time to wait for worktree warmup + CleanupOnExit bool // Whether to clean up pooled worktrees on exit + EnableSymlinks bool // Enable shared node_modules via symlinks + GoModCache bool // Enable Go module cache sharing + CargoTargetDir bool // Enable shared Cargo target directory for Rust projects } // DefaultPoolConfig returns sensible defaults for the pool @@ -100,7 +100,7 @@ type WorktreePool struct { manager *WorktreeManager config *PoolConfig worktrees map[string]*PooledWorktree // worktree ID -> PooledWorktree - mu sync.RWMutex // Protects worktrees map + mu sync.RWMutex // Protects worktrees map ctx context.Context cancel context.CancelFunc wg sync.WaitGroup @@ -109,9 +109,9 @@ type WorktreePool struct { shutdownCh chan struct{} // Dependency cache paths - sharedNodeModules string // Path to shared node_modules - sharedGoModCache string // Path to Go module cache (GOMODCACHE) - sharedCargoTarget string // Path to shared Cargo target directory + sharedNodeModules string // Path to shared node_modules + sharedGoModCache string // Path to Go module cache (GOMODCACHE) + sharedCargoTarget string // Path to shared Cargo target directory } // NewWorktreePool creates a new worktree pool @@ -297,14 +297,14 @@ func (p *WorktreePool) Stats() PoolStats { defer p.mu.RUnlock() return PoolStats{ - Total: len(p.worktrees), - Cold: p.countByState(StateCold), - Warming: p.countByState(StateWarming), - Warm: p.countByState(StateWarm), - InUse: p.countByState(StateInUse), - Draining: p.countByState(StateDraining), - MinSize: p.config.MinSize, - MaxSize: p.config.MaxSize, + Total: len(p.worktrees), + Cold: p.countByState(StateCold), + Warming: p.countByState(StateWarming), + Warm: p.countByState(StateWarm), + InUse: p.countByState(StateInUse), + Draining: p.countByState(StateDraining), + MinSize: p.config.MinSize, + MaxSize: p.config.MaxSize, } } @@ -322,10 +322,10 @@ type PoolStats struct { // FetchSyncResult represents the result of an async git fetch operation type FetchSyncResult struct { - WorktreeID string // ID of the worktree that was fetched - CompletedAt time.Time // When the fetch completed - Success bool // True if fetch succeeded - Error string // Error message if fetch failed + WorktreeID string // ID of the worktree that was fetched + CompletedAt time.Time // When the fetch completed + Success bool // True if fetch succeeded + Error string // Error message if fetch failed CommitsFetched int // Number of commits fetched (if available) } @@ -373,7 +373,7 @@ func (p *WorktreePool) FetchWorktree(worktreeID string) FetchSyncResult { if !exists { return FetchSyncResult{ - WorktreeID: worktreeID, + WorktreeID: worktreeID, CompletedAt: time.Now(), Success: false, Error: "worktree not found", @@ -578,13 +578,13 @@ func (p *WorktreePool) createAndWarmWorktree(taskID string) error { // Create pooled worktree entry p.mu.Lock() wt := &PooledWorktree{ - ID: taskID, - TaskID: taskID, - Path: worktreePath, - Branch: branchName, - State: StateInUse, // Already assigned to the task - CreatedAt: time.Now(), - WarmedAt: time.Now(), + ID: taskID, + TaskID: taskID, + Path: worktreePath, + Branch: branchName, + State: StateInUse, // Already assigned to the task + CreatedAt: time.Now(), + WarmedAt: time.Now(), AssignedAt: time.Now(), } p.worktrees[wt.ID] = wt diff --git a/internal/git/worktree.go b/internal/git/worktree.go index 433a762..e352e07 100644 --- a/internal/git/worktree.go +++ b/internal/git/worktree.go @@ -333,17 +333,17 @@ func (wm *WorktreeManager) Path(taskID string) string { // Directories to clean up aggressively (build artifacts and dependencies) // These can consume massive amounts of disk space var aggressiveCleanupDirs = []string{ - "target", // Rust/Cargo build artifacts - "node_modules", // Node.js dependencies - "vendor", // PHP/Go vendor directories - "__pycache__", // Python cache - ".venv", // Python virtual environments - "venv", // Python virtual environments - "dist", // Various build outputs - "build", // Various build outputs - ".next", // Next.js cache - ".nuxt", // Nuxt.js cache - "coverage", // Code coverage reports + "target", // Rust/Cargo build artifacts + "node_modules", // Node.js dependencies + "vendor", // PHP/Go vendor directories + "__pycache__", // Python cache + ".venv", // Python virtual environments + "venv", // Python virtual environments + "dist", // Various build outputs + "build", // Various build outputs + ".next", // Next.js cache + ".nuxt", // Nuxt.js cache + "coverage", // Code coverage reports } // RemoveAggressive removes a worktree and aggressively cleans up build artifacts diff --git a/internal/template/task.go b/internal/template/task.go index cd7da1d..47dc35a 100644 --- a/internal/template/task.go +++ b/internal/template/task.go @@ -9,19 +9,19 @@ import ( // TaskTemplate defines the structure for high-quality tasks type TaskTemplate struct { - Title string `json:"title"` - Description string `json:"description"` - TargetFiles []string `json:"target_files,omitempty"` // Specific files to modify - Components []string `json:"components,omitempty"` // Specific components - Action string `json:"action"` // create, fix, update, refactor, test - AcceptanceCriteria []string `json:"acceptance_criteria,omitempty"` - Context string `json:"context,omitempty"` // Additional context + Title string `json:"title"` + Description string `json:"description"` + TargetFiles []string `json:"target_files,omitempty"` // Specific files to modify + Components []string `json:"components,omitempty"` // Specific components + Action string `json:"action"` // create, fix, update, refactor, test + AcceptanceCriteria []string `json:"acceptance_criteria,omitempty"` + Context string `json:"context,omitempty"` // Additional context } // ValidationError represents a validation error with suggestions type ValidationError struct { - Field string `json:"field"` - Message string `json:"message"` + Field string `json:"field"` + Message string `json:"message"` Suggestions []string `json:"suggestions,omitempty"` } diff --git a/internal/workflow/dbos_workflow.go b/internal/workflow/dbos_workflow.go index 5ae79d9..4d70d1b 100644 --- a/internal/workflow/dbos_workflow.go +++ b/internal/workflow/dbos_workflow.go @@ -62,15 +62,15 @@ type QueueStats struct { // DBOSOrchestrator manages workflow execution using DBOS type DBOSOrchestrator struct { - config *config.Config - git *git.WorktreeManager - agent executor.Agent // Agent interface for Claude/Codex/Amp - dbosCtx dbos.DBOSContext - queue dbos.WorkflowQueue - store *db.Store // SQLite store for worktree tracking - verbose bool - dependencyMap map[string][]string // taskID -> list of dependent task IDs - dependencyMu sync.RWMutex + config *config.Config + git *git.WorktreeManager + agent executor.Agent // Agent interface for Claude/Codex/Amp + dbosCtx dbos.DBOSContext + queue dbos.WorkflowQueue + store *db.Store // SQLite store for worktree tracking + verbose bool + dependencyMap map[string][]string // taskID -> list of dependent task IDs + dependencyMu sync.RWMutex } // NewDBOSOrchestrator creates a new DBOS-based orchestrator diff --git a/internal/workflow/orchestrator.go b/internal/workflow/orchestrator.go index 681c1b4..976e6b0 100644 --- a/internal/workflow/orchestrator.go +++ b/internal/workflow/orchestrator.go @@ -27,15 +27,15 @@ import ( // Orchestrator manages the main execution loop type Orchestrator struct { - config *config.Config - store *db.Store - git *git.WorktreeManager - pool *git.WorktreePool // Worktree pool for pre-warming - agent executor.Agent // Agent interface for Claude/Codex/Amp - workers int - verbose bool // Enable verbose logging + config *config.Config + store *db.Store + git *git.WorktreeManager + pool *git.WorktreePool // Worktree pool for pre-warming + agent executor.Agent // Agent interface for Claude/Codex/Amp + workers int + verbose bool // Enable verbose logging projectDir string // Project directory for beads sync - epicID string // Optional epic filter for task execution + epicID string // Optional epic filter for task execution } // NewOrchestrator creates a new workflow orchestrator @@ -50,12 +50,12 @@ func NewOrchestrator(cfg *config.Config, store *db.Store, projectDir string) (*O var pool *git.WorktreePool if cfg.PoolEnabled { poolConfig := &git.PoolConfig{ - MinSize: cfg.PoolMinSize, - MaxSize: cfg.PoolMaxSize, - WarmupTimeout: cfg.PoolWarmup, - CleanupOnExit: cfg.PoolCleanupOnExit, - EnableSymlinks: true, - GoModCache: true, + MinSize: cfg.PoolMinSize, + MaxSize: cfg.PoolMaxSize, + WarmupTimeout: cfg.PoolWarmup, + CleanupOnExit: cfg.PoolCleanupOnExit, + EnableSymlinks: true, + GoModCache: true, } pool = git.NewWorktreePool(gitMgr, poolConfig) if err := pool.Start(); err != nil { diff --git a/pkg/telemetry/metrics.go b/pkg/telemetry/metrics.go index 60d66eb..ae04592 100644 --- a/pkg/telemetry/metrics.go +++ b/pkg/telemetry/metrics.go @@ -16,42 +16,42 @@ var meter = otel.Meter("drover") // Counter instruments var ( // Task counters - tasksClaimedCounter metric.Int64Counter - tasksCompletedCounter metric.Int64Counter - tasksFailedCounter metric.Int64Counter - tasksRetriedCounter metric.Int64Counter + tasksClaimedCounter metric.Int64Counter + tasksCompletedCounter metric.Int64Counter + tasksFailedCounter metric.Int64Counter + tasksRetriedCounter metric.Int64Counter // Blocker counters - blockersDetectedCounter metric.Int64Counter - fixTasksCreatedCounter metric.Int64Counter + blockersDetectedCounter metric.Int64Counter + fixTasksCreatedCounter metric.Int64Counter // Agent counters - agentPromptsCounter metric.Int64Counter - agentToolCallsCounter metric.Int64Counter - agentErrorsCounter metric.Int64Counter + agentPromptsCounter metric.Int64Counter + agentToolCallsCounter metric.Int64Counter + agentErrorsCounter metric.Int64Counter // Sync counters - syncCompletedCounter metric.Int64Counter - syncFailedCounter metric.Int64Counter - syncBytesCounter metric.Int64Counter + syncCompletedCounter metric.Int64Counter + syncFailedCounter metric.Int64Counter + syncBytesCounter metric.Int64Counter ) // Gauge instruments var ( - tasksActiveGauge metric.Int64ObservableGauge - workersActiveGauge metric.Int64ObservableGauge - tasksPendingGauge metric.Int64ObservableGauge - worktreesActiveGauge metric.Int64ObservableGauge - worktreesSyncingGauge metric.Int64ObservableGauge + tasksActiveGauge metric.Int64ObservableGauge + workersActiveGauge metric.Int64ObservableGauge + tasksPendingGauge metric.Int64ObservableGauge + worktreesActiveGauge metric.Int64ObservableGauge + worktreesSyncingGauge metric.Int64ObservableGauge ) // Histogram instruments var ( - taskDurationHistogram metric.Float64Histogram - agentDurationHistogram metric.Float64Histogram - claimLatencyHistogram metric.Float64Histogram - worktreeSetupHistogram metric.Float64Histogram - syncDurationHistogram metric.Float64Histogram + taskDurationHistogram metric.Float64Histogram + agentDurationHistogram metric.Float64Histogram + claimLatencyHistogram metric.Float64Histogram + worktreeSetupHistogram metric.Float64Histogram + syncDurationHistogram metric.Float64Histogram ) // initMetrics initializes all metric instruments diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index f8d03b9..d405382 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -7,12 +7,12 @@ import ( "os" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" - sdkmetric "go.opentelemetry.io/otel/sdk/metric" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" ) diff --git a/pkg/telemetry/tracer.go b/pkg/telemetry/tracer.go index 03f16a6..0e2b38e 100644 --- a/pkg/telemetry/tracer.go +++ b/pkg/telemetry/tracer.go @@ -29,9 +29,9 @@ const ( SpanTaskRetry = "drover.task.retry" // Worker spans - SpanWorkerRun = "drover.worker.run" - SpanWorkerPoll = "drover.worker.poll" - SpanWorkerLoop = "drover.worker.loop" + SpanWorkerRun = "drover.worker.run" + SpanWorkerPoll = "drover.worker.poll" + SpanWorkerLoop = "drover.worker.loop" // Worktree spans SpanWorktreeCreate = "drover.worktree.create" @@ -39,9 +39,9 @@ const ( SpanWorktreeCleanup = "drover.worktree.cleanup" // Agent spans - SpanAgentExecute = "drover.agent.execute" - SpanAgentPrompt = "drover.agent.prompt" - SpanAgentToolCall = "drover.agent.tool_call" + SpanAgentExecute = "drover.agent.execute" + SpanAgentPrompt = "drover.agent.prompt" + SpanAgentToolCall = "drover.agent.tool_call" // Blocker spans SpanBlockerDetect = "drover.blocker.detect" @@ -49,9 +49,9 @@ const ( SpanBlockerCreateFix = "drover.blocker.create_fix_task" // Git spans - SpanGitCommit = "drover.git.commit" - SpanGitPush = "drover.git.push" - SpanGitMerge = "drover.git.merge" + SpanGitCommit = "drover.git.commit" + SpanGitPush = "drover.git.push" + SpanGitMerge = "drover.git.merge" ) // StartWorkflowSpan starts a span for workflow execution diff --git a/pkg/types/task.go b/pkg/types/task.go index 45ef3b2..7de2868 100644 --- a/pkg/types/task.go +++ b/pkg/types/task.go @@ -30,23 +30,23 @@ const ( // Task represents a unit of work for an AI agent type Task struct { - ID string `json:"id" db:"id"` - Title string `json:"title" db:"title"` - Description string `json:"description" db:"description"` - EpicID string `json:"epic_id" db:"epic_id"` - ParentID string `json:"parent_id,omitempty" db:"parent_id"` // Parent task ID for sub-tasks - SequenceNumber int `json:"sequence_number,omitempty" db:"sequence_number"` // Position among siblings (1-indexed) - Type TaskType `json:"type" db:"type"` // Task type (feature, bug, etc.) - Priority int `json:"priority" db:"priority"` - Status TaskStatus `json:"status" db:"status"` - Attempts int `json:"attempts" db:"attempts"` - MaxAttempts int `json:"max_attempts" db:"max_attempts"` - LastError string `json:"last_error" db:"last_error"` - ClaimedBy string `json:"claimed_by" db:"claimed_by"` - ClaimedAt *int64 `json:"claimed_at" db:"claimed_at"` - Operator string `json:"operator" db:"operator"` // The operator/user who created or claimed this task - CreatedAt int64 `json:"created_at" db:"created_at"` - UpdatedAt int64 `json:"updated_at" db:"updated_at"` + ID string `json:"id" db:"id"` + Title string `json:"title" db:"title"` + Description string `json:"description" db:"description"` + EpicID string `json:"epic_id" db:"epic_id"` + ParentID string `json:"parent_id,omitempty" db:"parent_id"` // Parent task ID for sub-tasks + SequenceNumber int `json:"sequence_number,omitempty" db:"sequence_number"` // Position among siblings (1-indexed) + Type TaskType `json:"type" db:"type"` // Task type (feature, bug, etc.) + Priority int `json:"priority" db:"priority"` + Status TaskStatus `json:"status" db:"status"` + Attempts int `json:"attempts" db:"attempts"` + MaxAttempts int `json:"max_attempts" db:"max_attempts"` + LastError string `json:"last_error" db:"last_error"` + ClaimedBy string `json:"claimed_by" db:"claimed_by"` + ClaimedAt *int64 `json:"claimed_at" db:"claimed_at"` + Operator string `json:"operator" db:"operator"` // The operator/user who created or claimed this task + CreatedAt int64 `json:"created_at" db:"created_at"` + UpdatedAt int64 `json:"updated_at" db:"updated_at"` // ExecutionContext is not persisted in DB - it's set at runtime for execution ExecutionContext *TaskExecutionContext `json:"-" db:"-"` // Runtime execution context (guidance, worktree path, etc.) } @@ -85,26 +85,26 @@ type GuidanceMessage struct { // TaskExecutionContext provides additional context for task execution type TaskExecutionContext struct { - Guidance []*GuidanceMessage `json:"guidance,omitempty"` // Pending guidance messages - WorktreePath string `json:"worktree_path,omitempty"` // Path to the worktree + Guidance []*GuidanceMessage `json:"guidance,omitempty"` // Pending guidance messages + WorktreePath string `json:"worktree_path,omitempty"` // Path to the worktree } // ProjectStatus summarizes the current state of all tasks type ProjectStatus struct { - Total int `json:"total"` - Ready int `json:"ready"` - Claimed int `json:"claimed"` - InProgress int `json:"in_progress"` - Blocked int `json:"blocked"` - Completed int `json:"completed"` - Failed int `json:"failed"` - Epics []EpicStatus `json:"epics"` + Total int `json:"total"` + Ready int `json:"ready"` + Claimed int `json:"claimed"` + InProgress int `json:"in_progress"` + Blocked int `json:"blocked"` + Completed int `json:"completed"` + Failed int `json:"failed"` + Epics []EpicStatus `json:"epics"` } // EpicStatusSummary summarizes a single epic type EpicStatusSummary struct { - Epic Epic `json:"epic"` - TotalTasks int `json:"total_tasks"` - Completed int `json:"completed"` - Progress float64 `json:"progress"` + Epic Epic `json:"epic"` + TotalTasks int `json:"total_tasks"` + Completed int `json:"completed"` + Progress float64 `json:"progress"` }