ajhahn.de
← eeco
Go 92 lines
// Package workflow is eeco's workflow registry, runner, and scaffolder.
//
// A workflow inspects a repository and either passes cleanly, reports a
// finding, blocks (a required tool is missing), or defers an AI pass.
// The exit-code contract is fixed and shared with the CLI:
//
//	0  clean
//	1  finding / failure
//	2  blocked (a required tool is missing)
//	3  AI pass deferred (no --ai)
//
// Builtin workflows are implemented natively in Go and registered in the
// default registry. User workflows are scaffolded into the gitignored
// workspace as a directory with a runnable entry plus a one-line README
// and executed by the script runner, which honours the same contract.
//
// Every workflow is read-only with respect to the tracked tree: it
// writes only inside the workspace and uses the queue for any decision.
package workflow

import (
	"io"

	"github.com/ajhahnde/eeco/internal/ai"
	"github.com/ajhahnde/eeco/internal/config"
)

// Exit codes shared by every workflow and surfaced as the process exit
// code by `eeco run`.
const (
	CodeClean      = 0
	CodeFinding    = 1
	CodeBlocked    = 2
	CodeAIDeferred = 3
)

// Finding is one located issue. Line is 1-based; 0 means the finding is
// file-scoped rather than tied to a specific line.
type Finding struct {
	Path string
	Line int
	Msg  string
}

// Result is the outcome of a single workflow run. Code is one of the
// Code* constants. Summary is a one-line headline for the report;
// Findings carries the detail lines.
type Result struct {
	Code     int
	Summary  string
	Findings []Finding
}

// Env is the execution context handed to a workflow. The repository
// root is cfg.RepoRoot; a workflow treats it as its working directory
// and must never write outside cfg.Workspace.
type Env struct {
	Config *config.Config
	// AI reports whether the operator opted this run into a gated,
	// budget-capped AI pass (`--ai`). It mirrors Gate.Consent and is kept
	// for read-only builtins that only need the boolean.
	AI bool
	// Gate is the shared, single-invocation AI gate (consent + budget +
	// prompt-parking). A workflow that wants an AI pass calls Gate.Run
	// and falls back to its non-AI path when the Outcome is Skipped. Nil
	// is tolerated: a workflow then behaves as if no pass was consented.
	Gate *ai.Gate
	// Out is an optional sink for progress lines. Nil is fine.
	Out io.Writer
}

// Workflow is a named, runnable check. Run must be side-effect-free on
// the tracked tree and must return a Code from the contract.
type Workflow interface {
	Name() string
	// Summary is the one-line description shown in listings.
	Summary() string
	Run(env Env) (Result, error)
}

// normalizeCode clamps an arbitrary integer to the contract. Anything
// outside {0,1,2,3} is treated as a failure (1) so a misbehaving
// workflow can never masquerade as clean.
func normalizeCode(c int) int {
	switch c {
	case CodeClean, CodeFinding, CodeBlocked, CodeAIDeferred:
		return c
	default:
		return CodeFinding
	}
}