orchestration

package
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 3, 2026 License: MIT Imports: 47 Imported by: 0

Documentation

Index

Constants

View Source
const AgentJobTemplate = `` /* 662-byte string literal not displayed */

AgentJobTemplate is the template for agent jobs.

Variables

View Source
var BuiltinTemplates map[string]*JobTemplate

BuiltinTemplates stores templates loaded from embedded files.

View Source
var DefaultWorktreeConfig = WorktreeConfig{
	AutoCleanup:     true,
	CleanupAge:      24 * time.Hour,
	PreserveOnError: true,
	CleanupPrompt:   false,
}

Default configuration

Functions

func AddJob

func AddJob(plan *Plan, job *Job) (string, error)

AddJob adds a new job to the plan directory.

func AddJobsFromRecipe

func AddJobsFromRecipe(plan *Plan, recipe *Recipe, externalDeps []string, templateData interface{}) (newFiles []string, err error)

AddJobsFromRecipe adds jobs from a recipe into an existing plan. It handles re-numbering and dependency re-mapping in three passes: 1. In-memory creation & renaming 2. Dependency remapping 3. Writing to disk

func AddSummaryToJobFile

func AddSummaryToJobFile(job *Job, summary string) error

AddSummaryToJobFile updates a job's markdown file with a summary in its frontmatter.

func AppendAgentTranscript

func AppendAgentTranscript(job *Job, plan *Plan) error

AppendAgentTranscript finds the transcript for an agent job and appends it to the job's markdown file.

func ArchiveInteractiveSession

func ArchiveInteractiveSession(job *Job, plan *Plan) error

ArchiveInteractiveSession copies session metadata and the transcript to the plan's artifacts.

func BuildXMLPrompt

func BuildXMLPrompt(job *Job, plan *Plan, workDir string, contextFiles []string) (promptXML string, filesToUpload []string, err error)

BuildXMLPrompt assembles a structured XML prompt for oneshot and interactive_agent jobs. It returns the final XML string and a list of file paths that should be uploaded separately. contextFiles should include paths to .grove/context, CLAUDE.md, and other project context files.

func CopyProjectFilesToWorktree

func CopyProjectFilesToWorktree(worktreePath, gitRoot string) error

CopyProjectFilesToWorktree is a setup handler for workspace.Prepare that copies key project-level configuration files (like grove.yml and .cx) into a new worktree. This is necessary for files that are typically gitignored but are required for the Grove tooling to function correctly within the isolated worktree.

func CreateLockFile

func CreateLockFile(jobFilePath string, pid int) error

CreateLockFile creates a lock file for a job, writing the process ID to it.

func DetermineWorkingDirectory

func DetermineWorkingDirectory(plan *Plan, job *Job) (string, error)

DetermineWorkingDirectory determines the working directory for a job based on its worktree and repository configuration. This is the canonical logic used by both interactive agent execution and resume operations.

func ExtractFrontmatterString

func ExtractFrontmatterString(content []byte) (string, []byte, error)

ExtractFrontmatterString extracts the raw YAML string between delimiters.

func FindCodexPIDForPane

func FindCodexPIDForPane(targetPane string) (int, error)

FindCodexPIDForPane finds the PID of the 'codex' process running within a specific tmux pane by traversing the process tree from the pane's shell.

func FindContextFiles

func FindContextFiles(plan *Plan) []string

FindContextFiles looks for context files in multiple locations

func FindOpencodePIDForPane

func FindOpencodePIDForPane(targetPane string) (int, error)

FindOpencodePIDForPane finds the PID of the 'opencode' process running within a specific tmux pane

func FormatConversationXML

func FormatConversationXML(turns []*ChatTurn) string

FormatConversationXML converts parsed ChatTurns to structured XML format. This is used to send a clean, structured conversation to the LLM instead of raw markdown with HTML comment markers.

Input: []*ChatTurn - parsed conversation turns Output: XML string with <conversation><turn>...</turn></conversation> structure

The template attribute appears on assistant turns (indicating what persona/template was used to generate that response). The last user turn gets special attributes: - status="awaiting_response" to indicate this is the turn needing a response - respond_as="<template>" to indicate what persona/template should be used

Example output:

<conversation>
  <turn role="user">
    how about add fake syrup
  </turn>
  <turn role="assistant" template="chef" id="595424" timestamp="2026-01-08 05:34:09">
    *Sigh.* "Fake syrup"...
  </turn>
  <turn role="user" status="awaiting_response" respond_as="chef">
    respond to this
  </turn>
</conversation>

func GenerateGitChangesXML

func GenerateGitChangesXML(workDir string) (string, error)

GenerateGitChangesXML creates an XML block summarizing the git status of a worktree.

func GenerateJobFilename

func GenerateJobFilename(number int, title string) string

GenerateJobFilename creates a filename from job number and title.

func GenerateUniqueJobID

func GenerateUniqueJobID(plan *Plan, title string) string

GenerateUniqueJobID creates a globally unique job ID from a title string. This is the single source of truth for job ID generation across grove-flow.

func GetGitRootSafe

func GetGitRootSafe(planDir string) (string, error)

GetGitRootSafe attempts to find git root with multiple fallback strategies

func GetJobLogPath

func GetJobLogPath(plan *Plan, job *Job) (string, error)

GetJobLogPath returns the path to the log file for a given job. The log files are stored in <plan.Directory>/.artifacts/<job.ID>/job.log and the directory is created if it doesn't exist.

func GetNextJobNumber

func GetNextJobNumber(dir string) (int, error)

GetNextJobNumber scans the directory and returns the next available job number.

func GetProjectGitRoot

func GetProjectGitRoot(planDir string) (string, error)

GetProjectGitRoot returns the git root for the project associated with a plan. If the plan is inside a notebook, it returns the associated project's path. Otherwise, it falls back to GetGitRootSafe.

func GetProjectRoot

func GetProjectRoot() (string, error)

GetProjectRoot attempts to find the project root directory by searching upwards for a grove config file.

func GetProjectRootSafe

func GetProjectRootSafe(startPath string) string

GetProjectRootSafe returns the project root using the workspace model. It supports both Grove projects (with grove.yml) and non-Grove repos. Falls back to git root or current directory if workspace discovery fails.

func ListJobs

func ListJobs(dir string) ([]string, error)

ListJobs returns all job files in the directory sorted by filename.

func ParseFrontmatter

func ParseFrontmatter(content []byte) (map[string]interface{}, []byte, error)

ParseFrontmatter extracts YAML frontmatter from markdown content. Returns the parsed YAML as a map, the remaining content, and any error.

func ReadLockFile

func ReadLockFile(jobFilePath string) (int, error)

ReadLockFile reads the PID from a job's lock file.

func RebuildMarkdownWithFrontmatter

func RebuildMarkdownWithFrontmatter(frontmatter map[string]interface{}, body []byte) ([]byte, error)

RebuildMarkdownWithFrontmatter rebuilds a markdown file with new frontmatter data

func RemoveLockFile

func RemoveLockFile(jobFilePath string) error

RemoveLockFile deletes a job's lock file.

func RenameJob

func RenameJob(plan *Plan, jobToRename *Job, newTitle string) error

RenameJob renames a job file and updates its title and dependencies.

func ReplaceFrontmatter

func ReplaceFrontmatter(content []byte, newFrontmatter string) []byte

ReplaceFrontmatter replaces existing frontmatter with new YAML string.

func ResolveLogDirectory

func ResolveLogDirectory(plan *Plan, job *Job) string

ResolveLogDirectory determines where log files should be written

func ResolveProjectForSessionNaming

func ResolveProjectForSessionNaming(workDir string) (*workspace.WorkspaceNode, error)

ResolveProjectForSessionNaming resolves the appropriate project for tmux session naming. If workDir is inside a notebook, it returns the associated project. Otherwise, it returns the project at workDir.

func ResolvePromptSource

func ResolvePromptSource(source string, plan *Plan) (string, error)

ResolvePromptSource resolves a prompt source file with multiple strategies

func ResolveTemplate

func ResolveTemplate(templateName string, plan *Plan) (string, error)

ResolveTemplate resolves a template file path

func ResolveWorkingDirectory

func ResolveWorkingDirectory(plan *Plan) string

ResolveWorkingDirectory determines the appropriate working directory for command execution

func SavePlan

func SavePlan(dir string, plan *Plan) error

SavePlan saves a plan structure to disk (mainly used for tests).

func ScopeToSubProject

func ScopeToSubProject(workDir string, job *Job) string

ScopeToSubProject adjusts a working directory to point to a sub-project within an ecosystem worktree when job.Repository is specified. This ensures that context generation, command execution, and agent sessions all operate in the correct sub-project directory rather than the ecosystem root.

func SummarizeJobContent

func SummarizeJobContent(ctx context.Context, job *Job, plan *Plan, cfg SummaryConfig) (string, error)

SummarizeJobContent generates a summary for a completed job.

func SyncSkillsToWorktree

func SyncSkillsToWorktree(worktreePath string, node *workspace.WorkspaceNode) error

SyncSkillsToWorktree copies skills from all sources to a new worktree. Skills are collected from multiple sources with the following precedence (higher wins):

  1. User skills from ~/.config/grove/skills
  2. Ecosystem skills from the notebook (if project is part of an ecosystem)
  3. Project skills from the notebook

This delegates to grove-skills for the actual discovery and sync logic.

func UpdateFrontmatter

func UpdateFrontmatter(content []byte, updates map[string]interface{}) ([]byte, error)

UpdateFrontmatter updates specific fields in the frontmatter while preserving formatting.

func UpdateFrontmatterNode

func UpdateFrontmatterNode(yamlData []byte, updates map[string]interface{}) ([]byte, error)

UpdateFrontmatterNode updates YAML using the Node API to preserve formatting.

func UpdateJobDependencies

func UpdateJobDependencies(job *Job, newDeps []string) error

UpdateJobDependencies updates a job's depends_on field in its frontmatter.

func WriteBriefingFile

func WriteBriefingFile(plan *Plan, job *Job, content string, turnID string) (string, error)

WriteBriefingFile saves the provided content to a uniquely named .xml file in the plan's .artifacts/<job.ID> directory for auditing. For chat jobs, turnID should be the unique turn identifier. For other jobs, pass empty string.

Types

type AgentRunner

type AgentRunner interface {
	RunAgent(ctx context.Context, worktree string, prompt string) error
}

AgentRunner defines the interface for running agents.

type ChatDirective

type ChatDirective struct {
	ID       string                 `json:"id,omitempty"`
	Template string                 `json:"template,omitempty"`
	Model    string                 `json:"model,omitempty"`
	Type     string                 `json:"type,omitempty"` // Job type override for this turn
	Action   string                 `json:"action,omitempty"`
	Vars     map[string]interface{} `json:"vars,omitempty"`
}

ChatDirective represents the JSON payload in the user's comment

type ChatTurn

type ChatTurn struct {
	Speaker   string         // "user" or "llm"
	Content   string         // The markdown content of the turn
	Directive *ChatDirective // Parsed from the grove HTML comment
	Timestamp time.Time      // When the turn was recorded
}

ChatTurn represents a single entry in the conversation

func ParseChatFile

func ParseChatFile(content []byte) ([]*ChatTurn, error)

ParseChatFile parses a chat notebook file to determine the speaker of each turn. It returns a simplified list of turns for determining runnability.

type ClaudeAgentProvider

type ClaudeAgentProvider struct {
	// contains filtered or unexported fields
}

ClaudeAgentProvider implements InteractiveAgentProvider for Claude Code.

func NewClaudeAgentProvider

func NewClaudeAgentProvider() *ClaudeAgentProvider

func (*ClaudeAgentProvider) Launch

func (p *ClaudeAgentProvider) Launch(ctx context.Context, job *Job, plan *Plan, workDir string, agentArgs []string, briefingFilePath string) error

Launch implements the InteractiveAgentProvider interface for Claude. This contains the logic previously in executeHostMode.

type CodexAgentProvider

type CodexAgentProvider struct {
	// contains filtered or unexported fields
}

func NewCodexAgentProvider

func NewCodexAgentProvider() *CodexAgentProvider

func (*CodexAgentProvider) Launch

func (p *CodexAgentProvider) Launch(ctx context.Context, job *Job, plan *Plan, workDir string, agentArgs []string, briefingFilePath string) error

type CommandLLMClient

type CommandLLMClient struct {
	// contains filtered or unexported fields
}

CommandLLMClient implements LLMClient using the llm command-line tool.

func NewCommandLLMClient

func NewCommandLLMClient(executor command.Executor) *CommandLLMClient

NewCommandLLMClient creates a new LLM client that executes the llm command.

func (*CommandLLMClient) Complete

func (c *CommandLLMClient) Complete(ctx context.Context, job *Job, plan *Plan, prompt string, opts LLMOptions, output io.Writer) (string, error)

Complete sends a prompt to the LLM and returns the response.

type ConceptInfo

type ConceptInfo struct {
	ID    string
	Title string
	Path  string
}

ConceptInfo represents basic information about a concept

type Config

type Config struct {
	OneshotModel         string
	TargetAgentContainer string
	PlansDirectory       string
	MaxConsecutiveSteps  int
}

Config holds orchestration-specific settings, decoupled from grove-core.

type DependencyGraph

type DependencyGraph struct {
	// contains filtered or unexported fields
}

DependencyGraph represents the job dependency relationships.

func BuildDependencyGraph

func BuildDependencyGraph(plan *Plan) (*DependencyGraph, error)

BuildDependencyGraph creates a dependency graph from all jobs in a plan.

func (*DependencyGraph) DetectCycles

func (dg *DependencyGraph) DetectCycles() ([]string, error)

DetectCycles uses DFS to detect circular dependencies.

func (*DependencyGraph) GetExecutionPlan

func (dg *DependencyGraph) GetExecutionPlan() (*ExecutionPlan, error)

GetExecutionPlan performs topological sort and groups jobs into parallel stages.

func (*DependencyGraph) GetRunnableJobs

func (dg *DependencyGraph) GetRunnableJobs() []*Job

GetRunnableJobs finds all jobs that can be run immediately.

func (*DependencyGraph) ToMermaid

func (dg *DependencyGraph) ToMermaid() string

ToMermaid generates a Mermaid diagram representation of the graph.

func (*DependencyGraph) UpdateJobStatus

func (dg *DependencyGraph) UpdateJobStatus(jobID string, status JobStatus)

UpdateJobStatus updates the status of a job in the dependency graph

func (*DependencyGraph) ValidateDependencies

func (dg *DependencyGraph) ValidateDependencies() error

ValidateDependencies checks for circular dependencies and missing references.

type DynamicRecipe

type DynamicRecipe struct {
	Description string            `json:"description"`
	Jobs        map[string]string `json:"jobs"` // filename -> content
}

DynamicRecipe represents the structure of a recipe from a dynamic source.

type ErrNotAJob

type ErrNotAJob struct {
	Reason string
}

ErrNotAJob is returned when a file is not a valid job file

func (ErrNotAJob) Error

func (e ErrNotAJob) Error() string

type ExecutionContext

type ExecutionContext struct {
	// PlanDirectory is where the plan files are stored
	PlanDirectory string

	// ProjectRoot is the root of the project (where grove.yml is located)
	ProjectRoot string

	// GitRoot is the root of the git repository
	GitRoot string

	// WorkingDirectory is where commands should be executed
	WorkingDirectory string

	// Config is the loaded grove configuration
	Config *config.Config
}

ExecutionContext provides context about where jobs are being executed from This helps resolve the disconnect between plan directories and project directories

func NewExecutionContext

func NewExecutionContext(planDir string, cfg *config.Config) (*ExecutionContext, error)

NewExecutionContext creates a new execution context

func (*ExecutionContext) LogDirectory

func (ctx *ExecutionContext) LogDirectory() string

LogDirectory returns where logs should be written

func (*ExecutionContext) ResolveContextFile

func (ctx *ExecutionContext) ResolveContextFile() []string

ResolveContextFile resolves a context file path

func (*ExecutionContext) ResolvePromptSource

func (ctx *ExecutionContext) ResolvePromptSource(source string) string

ResolvePromptSource resolves a prompt source file path

func (*ExecutionContext) WorktreeBaseDirectory

func (ctx *ExecutionContext) WorktreeBaseDirectory() string

WorktreeBaseDirectory returns where worktrees should be created

type ExecutionPlan

type ExecutionPlan struct {
	Stages [][]string // Each stage contains jobs that can run in parallel
}

ExecutionPlan contains stages of jobs that can run in parallel.

type Executor

type Executor interface {
	Execute(ctx context.Context, job *Job, plan *Plan) error
	Name() string
}

Executor is the interface for job executors.

type ExecutorConfig

type ExecutorConfig struct {
	MaxPromptLength int
	Timeout         time.Duration
	RetryCount      int
	Model           string
	ModelOverride   string // Override model from CLI
	SkipInteractive bool   // Skip interactive prompts
}

ExecutorConfig holds configuration for executors.

type ExecutorRegistry

type ExecutorRegistry struct {
	// contains filtered or unexported fields
}

ExecutorRegistry manages available executors.

func NewExecutorRegistry

func NewExecutorRegistry() *ExecutorRegistry

NewExecutorRegistry creates a new executor registry.

func (*ExecutorRegistry) ExecuteJob

func (r *ExecutorRegistry) ExecuteJob(ctx context.Context, job *Job, plan *Plan) error

ExecuteJob runs a job using the appropriate executor.

func (*ExecutorRegistry) Get

func (r *ExecutorRegistry) Get(jobType JobType) (Executor, error)

Get returns the executor for a given job type.

func (*ExecutorRegistry) Register

func (r *ExecutorRegistry) Register(jobType JobType, executor Executor)

Register adds an executor to the registry.

type FileLock

type FileLock struct {
	// contains filtered or unexported fields
}

FileLock represents a lock on a file.

func (*FileLock) Unlock

func (fl *FileLock) Unlock() error

Unlock releases the file lock.

type FrontmatterParser

type FrontmatterParser struct{}

FrontmatterParser provides methods for parsing and updating frontmatter.

func (*FrontmatterParser) ParseFrontmatter

func (fp *FrontmatterParser) ParseFrontmatter(content []byte) (map[string]interface{}, []byte, error)

ParseFrontmatter parses YAML frontmatter from content.

func (*FrontmatterParser) WriteFrontmatter

func (fp *FrontmatterParser) WriteFrontmatter(w io.Writer, frontmatter map[string]interface{}) error

WriteFrontmatter writes frontmatter and body to a writer.

type GenerateRecipeExecutor

type GenerateRecipeExecutor struct {
	// contains filtered or unexported fields
}

GenerateRecipeExecutor handles generate-recipe jobs

func NewGenerateRecipeExecutor

func NewGenerateRecipeExecutor(config *ExecutorConfig) *GenerateRecipeExecutor

NewGenerateRecipeExecutor creates a new generate recipe executor

func (*GenerateRecipeExecutor) Execute

func (e *GenerateRecipeExecutor) Execute(ctx context.Context, job *Job, plan *Plan) error

Execute runs a generate-recipe job

func (*GenerateRecipeExecutor) Name

func (e *GenerateRecipeExecutor) Name() string

Name returns the executor name

type GitClient

type GitClient interface {
	WorktreeAdd(path, branch string) error
	WorktreeList() ([]Worktree, error)
	WorktreeRemove(name string, force bool) error
	CreateBranch(name string) error
	CurrentBranch() (string, error)
	IsGitRepo() bool
}

GitClient interface for git operations.

type GitClientAdapter

type GitClientAdapter struct {
	// contains filtered or unexported fields
}

GitClientAdapter adapts the git.WorktreeManager to the GitClient interface.

func NewGitClientAdapter

func NewGitClientAdapter(repoPath string) *GitClientAdapter

NewGitClientAdapter creates a new git client adapter.

func (*GitClientAdapter) CreateBranch

func (g *GitClientAdapter) CreateBranch(name string) error

CreateBranch creates a new branch.

func (*GitClientAdapter) CurrentBranch

func (g *GitClientAdapter) CurrentBranch() (string, error)

CurrentBranch returns the current branch name.

func (*GitClientAdapter) IsGitRepo

func (g *GitClientAdapter) IsGitRepo() bool

IsGitRepo checks if the path is a git repository.

func (*GitClientAdapter) WorktreeAdd

func (g *GitClientAdapter) WorktreeAdd(path, branch string) error

WorktreeAdd adds a new worktree.

func (*GitClientAdapter) WorktreeList

func (g *GitClientAdapter) WorktreeList() ([]Worktree, error)

WorktreeList lists all worktrees.

func (*GitClientAdapter) WorktreeRemove

func (g *GitClientAdapter) WorktreeRemove(name string, force bool) error

WorktreeRemove removes a worktree.

type HeadlessAgentExecutor

type HeadlessAgentExecutor struct {
	// contains filtered or unexported fields
}

HeadlessAgentExecutor executes headless agent jobs in isolated git worktrees.

func NewHeadlessAgentExecutor

func NewHeadlessAgentExecutor(llmClient LLMClient, config *ExecutorConfig) *HeadlessAgentExecutor

NewHeadlessAgentExecutor creates a new headless agent executor.

func (*HeadlessAgentExecutor) Execute

func (e *HeadlessAgentExecutor) Execute(ctx context.Context, job *Job, plan *Plan) error

Execute runs an agent job in a worktree.

func (*HeadlessAgentExecutor) Name

func (e *HeadlessAgentExecutor) Name() string

Name returns the executor name.

type InitAction

type InitAction struct {
	Type        string                 `yaml:"type"` // "shell" or "docker_compose"
	Description string                 `yaml:"description,omitempty"`
	Repo        string                 `yaml:"repo,omitempty"`         // Optional sub-repo for ecosystem worktrees
	Command     string                 `yaml:"command,omitempty"`      // For 'shell' type
	Files       []string               `yaml:"files,omitempty"`        // For 'docker_compose': list of user's compose files
	ProjectName string                 `yaml:"project_name,omitempty"` // For 'docker_compose' --project-name flag
	Overlay     map[string]interface{} `yaml:"overlay,omitempty"`      // For 'docker_compose': the generic overlay
}

InitAction defines a single action to be performed during plan initialization. These are defined in a recipe's `workspace_init.yml`.

type InlineCategory

type InlineCategory string

InlineCategory represents a category of files that can be inlined.

const (
	InlineDependencies InlineCategory = "dependencies" // Output from upstream jobs in the pipeline
	InlineInclude      InlineCategory = "include"      // Files specified in include: frontmatter
	InlineContext      InlineCategory = "context"      // cx-generated context file (.grove/context)
)

type InlineConfig

type InlineConfig struct {
	Categories []InlineCategory
}

InlineConfig controls which file types are embedded directly in the prompt vs uploaded as attachments. It can be specified as: - An array of categories: ["dependencies", "include", "context"] - A shorthand string: "none" (default), "all", or a single category like "dependencies"

func (InlineConfig) IsEmpty

func (ic InlineConfig) IsEmpty() bool

IsEmpty returns true if no categories are configured.

func (InlineConfig) MarshalYAML

func (ic InlineConfig) MarshalYAML() (interface{}, error)

MarshalYAML implements custom YAML marshaling.

func (*InlineConfig) UnmarshalYAML

func (ic *InlineConfig) UnmarshalYAML(unmarshal func(interface{}) error) error

UnmarshalYAML implements custom YAML unmarshaling to support both array and string syntax.

type InteractiveAgentExecutor

type InteractiveAgentExecutor struct {
	// contains filtered or unexported fields
}

InteractiveAgentExecutor executes interactive agent jobs in tmux sessions.

func NewInteractiveAgentExecutor

func NewInteractiveAgentExecutor(llmClient LLMClient, geminiRunner *gemini.RequestRunner, skipInteractive bool) *InteractiveAgentExecutor

NewInteractiveAgentExecutor creates a new interactive agent executor.

func (*InteractiveAgentExecutor) Execute

func (e *InteractiveAgentExecutor) Execute(ctx context.Context, job *Job, plan *Plan) error

Execute runs an interactive agent job in a tmux session and blocks until completion. The output writer is ignored for interactive agents as they run in a separate tmux session.

func (*InteractiveAgentExecutor) Name

func (e *InteractiveAgentExecutor) Name() string

Name returns the executor name.

type InteractiveAgentProvider

type InteractiveAgentProvider interface {
	Launch(ctx context.Context, job *Job, plan *Plan, workDir string, agentArgs []string, briefingFilePath string) error
}

InteractiveAgentProvider defines the interface for launching an interactive agent.

type Job

type Job struct {
	// From frontmatter
	ID                   string        `yaml:"id" json:"id"`
	Title                string        `yaml:"title" json:"title"`
	Status               JobStatus     `yaml:"status" json:"status"`
	Type                 JobType       `yaml:"type" json:"type"`
	Model                string        `yaml:"model,omitempty" json:"model,omitempty"`
	DependsOn            []string      `yaml:"depends_on,omitempty" json:"depends_on,omitempty"`
	Include              []string      `yaml:"include,omitempty" json:"include,omitempty"`
	SourceBlock          string        `yaml:"source_block,omitempty" json:"source_block,omitempty"`
	Template             string        `yaml:"template,omitempty" json:"template,omitempty"`
	Repository           string        `yaml:"repository,omitempty" json:"repository,omitempty"`
	Branch               string        `yaml:"branch,omitempty" json:"branch,omitempty"`
	Worktree             string        `yaml:"worktree" json:"worktree,omitempty"`
	TargetAgentContainer string        `yaml:"target_agent_container,omitempty" json:"target_agent_container,omitempty"`
	Inline               InlineConfig  `yaml:"inline,omitempty" json:"inline,omitempty"`                             // New field: controls which file types are inlined vs uploaded
	PrependDependencies  bool          `yaml:"prepend_dependencies,omitempty" json:"prepend_dependencies,omitempty"` // Deprecated: use inline: [dependencies] instead
	OnCompleteStatus     string        `yaml:"on_complete_status,omitempty" json:"on_complete_status,omitempty"`
	CreatedAt            time.Time     `yaml:"created_at,omitempty" json:"created_at,omitempty"`
	UpdatedAt            time.Time     `yaml:"updated_at,omitempty" json:"updated_at,omitempty"`
	CompletedAt          time.Time     `yaml:"completed_at,omitempty" json:"completed_at,omitempty"`
	Duration             time.Duration `yaml:"duration,omitempty" json:"duration,omitempty"`
	Summary              string        `yaml:"summary,omitempty" json:"summary,omitempty"`
	SourcePlan           string        `yaml:"source_plan,omitempty" json:"source_plan,omitempty"`
	RecipeName           string        `yaml:"recipe_name,omitempty" json:"recipe_name,omitempty"`
	GeneratePlanFrom     bool          `yaml:"generate_plan_from,omitempty" json:"generate_plan_from,omitempty"`
	GitChanges           bool          `yaml:"git_changes,omitempty" json:"git_changes,omitempty"`
	GatherConceptNotes   bool          `yaml:"gather_concept_notes,omitempty" json:"gather_concept_notes,omitempty"`
	GatherConceptPlans   bool          `yaml:"gather_concept_plans,omitempty" json:"gather_concept_plans,omitempty"`
	RulesFile            string        `yaml:"rules_file,omitempty" json:"rules_file,omitempty"`
	NoteRef              string        `yaml:"note_ref,omitempty" json:"note_ref,omitempty"`
	SourceFile           string        `yaml:"source_file,omitempty" json:"source_file,omitempty"` // Origin file path (e.g., Claude plan file)

	// Derived fields
	Filename     string      `json:"filename,omitempty"`   // The markdown filename
	FilePath     string      `json:"file_path,omitempty"`  // Full path to the file
	PromptBody   string      `json:"-"`                    // Content after frontmatter
	Dependencies []*Job      `json:"-"`                    // Resolved job references
	StartTime    time.Time   `json:"start_time,omitempty"` // When job started
	EndTime      time.Time   `json:"end_time,omitempty"`   // When job completed
	Metadata     JobMetadata `json:"metadata,omitempty"`
}

Job represents a single orchestration job.

func CreateJobFromTemplate

func CreateJobFromTemplate(jobType JobType, title string, opts JobOptions) *Job

CreateJobFromTemplate creates a new job with default values.

func LoadJob

func LoadJob(filepath string) (*Job, error)

LoadJob loads a single job from a markdown file.

func (*Job) AppendOutput

func (j *Job) AppendOutput(sp *StatePersister, output string) error

AppendOutput appends output to the job file.

func (*Job) CanBeRetried

func (j *Job) CanBeRetried() bool

CanBeRetried checks if a failed job can be manually retried. This is used when a user explicitly targets a failed job for re-execution.

func (*Job) IsRunnable

func (j *Job) IsRunnable() bool

IsRunnable checks if a job can be executed.

func (*Job) ShouldInline

func (j *Job) ShouldInline(category InlineCategory) bool

ShouldInline checks if a specific category should be inlined in the prompt. It first checks the new Inline field, then falls back to PrependDependencies for backwards compatibility.

func (*Job) UpdateStatus

func (j *Job) UpdateStatus(sp *StatePersister, newStatus JobStatus) error

UpdateStatus updates the job status using the state persister.

type JobMetadata

type JobMetadata struct {
	ExecutionTime time.Duration `yaml:"execution_time"`
	RetryCount    int           `yaml:"retry_count"`
	LastError     string        `yaml:"last_error"`
}

JobMetadata holds additional job metadata.

type JobOptions

type JobOptions struct {
	DependsOn           []string
	Include             []string
	Worktree            string
	Prompt              string
	Inline              InlineConfig // New field: controls which file types are inlined
	PrependDependencies bool         // Deprecated: use Inline instead
}

JobOptions contains options for creating a new job.

type JobStatus

type JobStatus string

JobStatus represents the current state of a job.

const (
	JobStatusPending     JobStatus = "pending"
	JobStatusRunning     JobStatus = "running"
	JobStatusCompleted   JobStatus = "completed"
	JobStatusFailed      JobStatus = "failed"
	JobStatusBlocked     JobStatus = "blocked"
	JobStatusNeedsReview JobStatus = "needs_review"
	JobStatusPendingUser JobStatus = "pending_user"
	JobStatusPendingLLM  JobStatus = "pending_llm"
	JobStatusHold        JobStatus = "hold"
	JobStatusTodo        JobStatus = "todo"
	JobStatusAbandoned   JobStatus = "abandoned"
	JobStatusIdle        JobStatus = "idle" // Agent finished responding, waiting for next input
)

type JobTemplate

type JobTemplate struct {
	Name        string                 `json:"name"`
	Path        string                 `json:"path"`
	Source      string                 `json:"source"`           // "project", "user", "builtin"
	Domain      string                 `json:"domain,omitempty"` // "generic" or "grove"
	Type        string                 `json:"type,omitempty"`   // "agent" or "oneshot"
	Description string                 `json:"description,omitempty" yaml:"description,omitempty"`
	Frontmatter map[string]interface{} `json:"frontmatter,omitempty"`
	Prompt      string                 `json:"prompt,omitempty"`
}

JobTemplate represents a predefined job structure.

func (*JobTemplate) Render

func (t *JobTemplate) Render(data interface{}) (string, error)

Render applies data to a template's prompt.

type JobType

type JobType string

JobType represents the type of job execution.

const (
	JobTypeOneshot          JobType = "oneshot"
	JobTypeAgent            JobType = "agent"
	JobTypeHeadlessAgent    JobType = "headless_agent"
	JobTypeShell            JobType = "shell"
	JobTypeChat             JobType = "chat"
	JobTypeInteractiveAgent JobType = "interactive_agent"
	JobTypeGenerateRecipe   JobType = "generate-recipe"
	JobTypeFile             JobType = "file" // Non-executable job for storing context/reference content
)

type LLMClient

type LLMClient interface {
	Complete(ctx context.Context, job *Job, plan *Plan, prompt string, opts LLMOptions, output io.Writer) (string, error)
}

LLMClient defines the interface for LLM interactions.

func NewMockLLMClient

func NewMockLLMClient() LLMClient

NewMockLLMClient creates a new mock LLM client.

type LLMOptions

type LLMOptions struct {
	Model        string
	SchemaPath   string   // Path to JSON schema file for structured output
	WorkingDir   string   // Working directory for the LLM command
	ContextFiles []string // Paths to context files (.grove/context, CLAUDE.md)
	IncludeFiles []string // Paths to include files from job configuration
}

LLMOptions defines configuration for an LLM completion request.

type Logger

type Logger interface {
	Info(msg string, keysAndValues ...interface{})
	Error(msg string, keysAndValues ...interface{})
	Debug(msg string, keysAndValues ...interface{})
}

Logger defines the logging interface.

func NewDefaultLogger

func NewDefaultLogger() Logger

type MockLLMClient

type MockLLMClient struct {
	// contains filtered or unexported fields
}

MockLLMClient implements a mock LLM client for testing.

func (*MockLLMClient) Complete

func (m *MockLLMClient) Complete(ctx context.Context, job *Job, plan *Plan, prompt string, opts LLMOptions, output io.Writer) (string, error)

Complete implements the LLMClient interface for mocking.

type OneShotExecutor

type OneShotExecutor struct {
	// contains filtered or unexported fields
}

OneShotExecutor executes oneshot jobs.

func NewOneShotExecutor

func NewOneShotExecutor(llmClient LLMClient, config *ExecutorConfig) *OneShotExecutor

NewOneShotExecutor creates a new oneshot executor.

func (*OneShotExecutor) Execute

func (e *OneShotExecutor) Execute(ctx context.Context, job *Job, plan *Plan) error

Execute runs a oneshot job.

func (*OneShotExecutor) Name

func (e *OneShotExecutor) Name() string

Name returns the executor name.

type OpencodeAgentProvider

type OpencodeAgentProvider struct {
	// contains filtered or unexported fields
}

OpencodeAgentProvider implements InteractiveAgentProvider for the opencode agent.

func NewOpencodeAgentProvider

func NewOpencodeAgentProvider() *OpencodeAgentProvider

func (*OpencodeAgentProvider) Launch

func (p *OpencodeAgentProvider) Launch(ctx context.Context, job *Job, plan *Plan, workDir string, agentArgs []string, briefingFilePath string) error

Launch implements the InteractiveAgentProvider interface for opencode.

type Orchestrator

type Orchestrator struct {
	Plan *Plan
	// contains filtered or unexported fields
}

Orchestrator coordinates job execution and manages state.

func NewOrchestrator

func NewOrchestrator(plan *Plan, config *OrchestratorConfig) (*Orchestrator, error)

NewOrchestrator creates a new orchestrator instance.

func (*Orchestrator) ExecuteJobWithWriter

func (o *Orchestrator) ExecuteJobWithWriter(ctx context.Context, job *Job, output io.Writer) error

ExecuteJobWithWriter runs a single job and streams its output to the provided writer. This is primarily for TUI integration where output needs to be captured and displayed.

func (*Orchestrator) GetStatus

func (o *Orchestrator) GetStatus() *PlanStatus

GetStatus returns the current plan status.

func (*Orchestrator) RunAll

func (o *Orchestrator) RunAll(ctx context.Context) error

RunAll executes all jobs in the plan.

func (*Orchestrator) RunJob

func (o *Orchestrator) RunJob(ctx context.Context, jobFile string) error

RunJob executes a specific job by filename.

func (*Orchestrator) RunNext

func (o *Orchestrator) RunNext(ctx context.Context) error

RunNext executes all currently runnable jobs.

func (*Orchestrator) SetLogger

func (o *Orchestrator) SetLogger(logger Logger)

SetLogger sets a custom logger.

func (*Orchestrator) UpdateJobMetadata

func (o *Orchestrator) UpdateJobMetadata(job *Job, meta JobMetadata) error

UpdateJobMetadata updates a job's metadata.

func (*Orchestrator) UpdateJobStatus

func (o *Orchestrator) UpdateJobStatus(job *Job, status JobStatus) error

UpdateJobStatus updates a job's status with proper synchronization.

func (*Orchestrator) ValidatePrerequisites

func (o *Orchestrator) ValidatePrerequisites() error

ValidatePrerequisites ensures all requirements are met before running jobs.

type OrchestratorConfig

type OrchestratorConfig struct {
	MaxParallelJobs     int
	CheckInterval       time.Duration
	StateFile           string
	ModelOverride       string           // Override model for all jobs
	MaxConsecutiveSteps int              // Maximum consecutive steps before halting
	SkipInteractive     bool             // Skip interactive agent jobs
	SummaryConfig       *SummaryConfig   // Configuration for job summarization
	CommandExecutor     command.Executor // For dependency injection
}

OrchestratorConfig holds configuration for the orchestrator.

type Plan

type Plan struct {
	Name          string            // Name of the plan (directory name)
	Directory     string            // Root directory of the plan
	Jobs          []*Job            // List of all jobs
	JobsByID      map[string]*Job   // Keyed by job ID
	SpecFile      string            // Path to spec.md if exists
	Orchestration *Config           // Orchestration configuration
	Context       *ExecutionContext // Execution context for the plan
	Config        *PlanConfig       // Plan-specific configuration from .grove-plan.yml
}

Plan represents a collection of orchestration jobs.

func LoadPlan

func LoadPlan(dir string) (*Plan, error)

LoadPlan loads all jobs from a plan directory.

func (*Plan) GetJobByFilename

func (p *Plan) GetJobByFilename(filename string) (*Job, bool)

GetJobByFilename returns a job by its filename.

func (*Plan) GetJobByID

func (p *Plan) GetJobByID(id string) (*Job, bool)

GetJobByID returns a job by its ID.

func (*Plan) GetJobsSortedByFilename

func (p *Plan) GetJobsSortedByFilename() []*Job

GetJobsSortedByFilename returns all jobs sorted by their filename.

func (*Plan) GetRunnableJobs

func (p *Plan) GetRunnableJobs() []*Job

GetRunnableJobs returns all jobs that can currently be executed.

func (*Plan) ResolveDependencies

func (p *Plan) ResolveDependencies() error

ResolveDependencies converts dependency IDs to Job pointers and checks for cycles.

type PlanConfig

type PlanConfig struct {
	Model                string            `yaml:"model,omitempty"`
	Worktree             string            `yaml:"worktree,omitempty"`
	TargetAgentContainer string            `yaml:"target_agent_container,omitempty"`
	Status               string            `yaml:"status,omitempty"`
	Repos                []string          `yaml:"repos,omitempty"`                // List of repos to include in ecosystem worktree
	Notes                string            `yaml:"notes,omitempty"`                // User notes/description for the plan
	Inline               InlineConfig      `yaml:"inline,omitempty"`               // New field: controls which file types are inlined by default
	PrependDependencies  bool              `yaml:"prepend_dependencies,omitempty"` // Deprecated: use inline instead
	Hooks                map[string]string `yaml:"hooks,omitempty"`
	Recipe               string            `yaml:"recipe,omitempty"` // Recipe used to create this plan
}

PlanConfig holds plan-specific default settings from .grove-plan.yml.

func (*PlanConfig) ShouldInline

func (pc *PlanConfig) ShouldInline(category InlineCategory) bool

ShouldInline checks if a specific category should be inlined by default for jobs in this plan. It first checks the new Inline field, then falls back to PrependDependencies for backwards compatibility.

type PlanStatus

type PlanStatus struct {
	Total     int
	Pending   int
	Running   int
	Completed int
	Failed    int
	Blocked   int
	Progress  float64
}

PlanStatus provides comprehensive status information.

type Recipe

type Recipe struct {
	Name              string                  `json:"name"`
	Description       string                  `json:"description"`
	Source            string                  `json:"source,omitempty"`  // [Built-in], [User], [Dynamic], or [Project]
	Domain            string                  `json:"domain,omitempty"`  // "generic" or "grove"
	DefaultNoteTarget string                  `json:"-"`                 // This will be populated from recipe.yml
	Jobs              map[string][]byte       `json:"-"`                 // Filename -> Content
	InitActions       []InitAction            `yaml:"init,omitempty"`    // Actions that run with --init flag
	NamedActions      map[string][]InitAction `yaml:"actions,omitempty"` // Named, on-demand action groups
}

func GetBuiltinRecipe

func GetBuiltinRecipe(name string) (*Recipe, error)

GetBuiltinRecipe finds and returns a built-in recipe.

func GetNotebookRecipe

func GetNotebookRecipe(name string) (*Recipe, error)

GetNotebookRecipe finds and returns a recipe from the current notebook context.

func GetProjectRecipe

func GetProjectRecipe(name string) (*Recipe, error)

GetProjectRecipe finds and returns a project-local recipe by searching upwards.

func GetRecipe

func GetRecipe(name string, getRecipeCmd string) (*Recipe, error)

GetRecipe finds and returns a recipe by name with precedence: Project > Notebook > User > Dynamic > Built-in.

func GetUserRecipe

func GetUserRecipe(name string) (*Recipe, error)

GetUserRecipe finds and returns a user-defined recipe.

func ListAllRecipes

func ListAllRecipes(getRecipeCmd string) ([]*Recipe, error)

ListAllRecipes lists all available recipes with precedence: Project > User > Dynamic > Built-in.

func ListBuiltinRecipes

func ListBuiltinRecipes() ([]*Recipe, error)

ListBuiltinRecipes lists all available built-in recipes.

func ListDynamicRecipes

func ListDynamicRecipes(getRecipeCmd string) ([]*Recipe, error)

ListDynamicRecipes loads recipes by executing an external command.

func ListNotebookRecipes

func ListNotebookRecipes() ([]*Recipe, error)

ListNotebookRecipes lists all recipes from the current notebook context.

func ListProjectRecipes

func ListProjectRecipes() ([]*Recipe, error)

ListProjectRecipes lists all project-local recipes from .grove/recipes, searching upwards.

func ListUserRecipes

func ListUserRecipes() ([]*Recipe, error)

ListUserRecipes lists all user-defined recipes from ~/.config/grove/recipes.

func (*Recipe) RenderJob

func (r *Recipe) RenderJob(filename string, data interface{}) ([]byte, error)

RenderJob renders a single job template from a recipe.

type ShellExecutor

type ShellExecutor struct {
	// contains filtered or unexported fields
}

ShellExecutor executes shell commands as orchestration jobs.

func NewShellExecutor

func NewShellExecutor(config *ExecutorConfig) *ShellExecutor

NewShellExecutor creates a new shell executor.

func (*ShellExecutor) Execute

func (e *ShellExecutor) Execute(ctx context.Context, job *Job, plan *Plan) error

Execute runs a shell job.

func (*ShellExecutor) Name

func (e *ShellExecutor) Name() string

Name returns the executor name.

type StateManager

type StateManager struct {
	// contains filtered or unexported fields
}

StateManager handles persistence of job states.

func NewStateManager

func NewStateManager(planDir string) *StateManager

func (*StateManager) UpdateJobMetadata

func (sm *StateManager) UpdateJobMetadata(job *Job, meta JobMetadata) error

func (*StateManager) UpdateJobStatus

func (sm *StateManager) UpdateJobStatus(job *Job, status JobStatus) error

type StatePersister

type StatePersister struct {
	// contains filtered or unexported fields
}

StatePersister handles persistent state updates for jobs.

func NewStatePersister

func NewStatePersister() *StatePersister

NewStatePersister creates a new state persister.

func (*StatePersister) AppendJobOutput

func (sp *StatePersister) AppendJobOutput(job *Job, output string) error

AppendJobOutput appends output to a job's markdown file.

func (*StatePersister) UpdateJobMetadata

func (sp *StatePersister) UpdateJobMetadata(job *Job, meta JobMetadata) error

UpdateJobMetadata updates metadata fields for a job.

func (*StatePersister) UpdateJobStatus

func (sp *StatePersister) UpdateJobStatus(job *Job, newStatus JobStatus) error

UpdateJobStatus updates the status of a job in its markdown file.

func (*StatePersister) UpdateJobTemplate

func (sp *StatePersister) UpdateJobTemplate(job *Job, newTemplate string) error

UpdateJobTemplate updates the template of a job in its markdown file.

func (*StatePersister) UpdateJobType

func (sp *StatePersister) UpdateJobType(job *Job, newType JobType) error

UpdateJobType updates the type of a job in its markdown file.

func (*StatePersister) ValidateJobStates

func (sp *StatePersister) ValidateJobStates(plan *Plan) []error

ValidateJobStates validates all job states in a plan.

type SummaryConfig

type SummaryConfig struct {
	Enabled  bool
	Model    string
	Prompt   string
	MaxChars int
}

SummaryConfig holds configuration for job summarization.

type TemplateManager

type TemplateManager struct {
}

TemplateManager finds and loads job templates.

func NewTemplateManager

func NewTemplateManager() *TemplateManager

func (*TemplateManager) FindTemplate

func (tm *TemplateManager) FindTemplate(name string) (*JobTemplate, error)

FindTemplate searches for a template by name, traversing upwards to find it.

func (*TemplateManager) ListTemplates

func (tm *TemplateManager) ListTemplates() ([]*JobTemplate, error)

ListTemplates lists all discoverable templates by searching upwards.

func (*TemplateManager) LoadTemplate

func (tm *TemplateManager) LoadTemplate(path, name, source string) (*JobTemplate, error)

LoadTemplate loads a template from a given path.

type Worktree

type Worktree struct {
	Name      string
	Path      string
	Branch    string
	HEAD      string
	IsLocked  bool
	CreatedAt time.Time
}

Worktree represents a git worktree.

type WorktreeConfig

type WorktreeConfig struct {
	AutoCleanup     bool          // Whether to auto-remove worktrees after job completion
	CleanupAge      time.Duration // Age after which stale worktrees are cleaned
	PreserveOnError bool          // Keep worktree if job fails
	CleanupPrompt   bool          // Ask user before cleanup
}

WorktreeConfig defines worktree management behavior.

type WorktreeLock

type WorktreeLock struct {
	WorktreeName string
	JobID        string
	LockedAt     time.Time
	PID          int
}

WorktreeLock represents a lock on a worktree.

type WorktreeManager

type WorktreeManager struct {
	// contains filtered or unexported fields
}

WorktreeManager handles git worktree lifecycle for job execution.

func NewWorktreeManager

func NewWorktreeManager(baseDir string, gitClient GitClient, logger Logger) (*WorktreeManager, error)

NewWorktreeManager creates a new worktree manager.

func (*WorktreeManager) CleanupJobWorktree

func (wm *WorktreeManager) CleanupJobWorktree(job *Job) error

CleanupJobWorktree cleans up a worktree after job completion.

func (*WorktreeManager) CleanupStaleWorktrees

func (wm *WorktreeManager) CleanupStaleWorktrees(age time.Duration) error

CleanupStaleWorktrees removes worktrees older than the specified age.

func (*WorktreeManager) CreateWorktree

func (wm *WorktreeManager) CreateWorktree(name string, baseBranch string) (string, error)

CreateWorktree creates a new worktree.

func (*WorktreeManager) GetOrCreateWorktree

func (wm *WorktreeManager) GetOrCreateWorktree(name string) (string, error)

GetOrCreateWorktree gets an existing worktree or creates a new one.

func (*WorktreeManager) IsLocked

func (wm *WorktreeManager) IsLocked(name string) (bool, *WorktreeLock)

IsLocked checks if a worktree is locked.

func (*WorktreeManager) ListWorktrees

func (wm *WorktreeManager) ListWorktrees() ([]Worktree, error)

ListWorktrees returns all worktrees managed by this manager.

func (*WorktreeManager) LockWorktree

func (wm *WorktreeManager) LockWorktree(name string, jobID string) error

LockWorktree locks a worktree for exclusive use.

func (*WorktreeManager) RemoveWorktree

func (wm *WorktreeManager) RemoveWorktree(name string, force bool) error

RemoveWorktree removes a worktree.

func (*WorktreeManager) SetConfig

func (wm *WorktreeManager) SetConfig(config WorktreeConfig)

SetConfig updates the worktree configuration.

func (*WorktreeManager) UnlockWorktree

func (wm *WorktreeManager) UnlockWorktree(name string) error

UnlockWorktree removes the lock from a worktree.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL