pm

package
v0.0.5 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package pm — chain pipeline helpers. BuildChainPrompt prepends the output from the previous pipeline step so the AI has full context when executing a chained task.

Package pm — deadline and SLA helpers.

Package pm implements the AI product manager mode. In PM mode, the goal is first decomposed into concrete tasks, then each task is executed and verified one at a time.

Index

Constants

View Source
const (
	CommitRequireCommitted = "committed"
	CommitRequirePushed    = "pushed"
	CommitRequireOff       = "off"
)

The values `cloop run --require-committed=<value>` and `cloop require-committed <value>` accept.

View Source
const (
	// ReviewModeFix sends the reviewer's findings back to the agent — in the
	// same conversation when the provider can resume one — and reviews the
	// result again, up to MaxFixRounds times. Work the reviewer still does not
	// approve after that is blocked, exactly as in ReviewModeBlock.
	ReviewModeFix = "fix"
	// ReviewModeBlock fails the task at once. Its changes stay where the agent
	// left them and are neither pushed nor merged.
	ReviewModeBlock = "block"
	// ReviewModeAdvisory records the review and publishes regardless.
	ReviewModeAdvisory = "advisory"
)

Review gate modes: what a review that does not approve does to the task.

View Source
const (
	// DefaultReviewFixRounds is how many times a fix-mode gate sends a task
	// back when MaxFixRounds is unset.
	DefaultReviewFixRounds = 2
	// MaxReviewFixRounds caps MaxFixRounds. Each round is a full agent turn
	// plus a review; past a handful the reviewer and the agent are arguing,
	// and a person should look.
	MaxReviewFixRounds = 5
	// MaxReviewInstructionsBytes caps the operator's extra review criteria,
	// which are sent with every review.
	MaxReviewInstructionsBytes = 4000
)

Bounds on the review gate settings.

View Source
const (
	// PublishPushed: cloop pushed it after the reviewer approved.
	PublishPushed = "pushed"
	// PublishWithheld: the reviewer did not approve, so it was not pushed.
	PublishWithheld = "withheld"
	// PublishRefused: approved, but the push would have sent commits the
	// reviewer never saw, so cloop did not send it.
	PublishRefused = "refused"
	// PublishFailed: cloop tried to push it and git failed.
	PublishFailed = "failed"
)

Outcomes of a held push.

View Source
const DefaultSnapshotRetention = 50

DefaultSnapshotRetention is how many plan snapshots SaveSnapshot keeps on disk (Task 20229).

Every mutation of the plan writes a full copy of it, so an unbounded history grows without limit in proportion to how much work a project does — not to how much state it holds. This repository's own history reached 3,983 files and 2.0 GB, which is 40% of its .cloop directory and larger than the state database it describes.

The bound is enforced here, at write time, rather than only by the periodic janitor, because a busy project can write thousands of snapshots between two janitor passes. A write-time bound makes the directory's size a function of the policy alone; the janitor then only has to catch up existing installs and re-apply a lowered keep-count.

50 is deliberately more generous than `cloop compact`'s KeepSnapshots of 10 — this runs unattended, so it should destroy less than the tool an operator invokes on purpose — while still bounding the directory at roughly 25 MB at this repository's average snapshot size. Commands that reach far back into history (`cloop diff`, `cloop scope creep`, `cloop rollback`) address snapshots by version, and a pruned version reports "not found" rather than returning wrong data.

View Source
const (
	MaxReviewFindings = 20
)

Bounds on a stored review record. The record is on every task row the dashboard renders, so it is a summary; the full reviewer output is in the provider call log.

View Source
const NodeKillerThreshold = 2

NodeKillerThreshold is how many distinct executors must have gone unreachable while a task was running before it is suspected of taking them down. One lost node is a flaky node; two different ones under the same task are a pattern.

View Source
const QuarantineNodeKiller = "node_killer"

QuarantineNodeKiller is TaskQuarantine.Kind for a task two or more distinct executors went unreachable under.

View Source
const StopDiskLimit = "disk_limit"

StopDiskLimit is TaskStop.Cause for an execution its executor stopped because the workspace grew past its disk limit.

Variables

CommitRequirements lists the accepted values, in the order help text shows them.

ReviewModes lists the valid modes, in the order the UI offers them.

Functions

func AbortAppliesTo

func AbortAppliesTo(a *TaskAbort, result string) bool

AbortAppliesTo reports whether a stored record still describes this task's current summary. A record that does not is stale and must be re-derived.

func AdaptiveReplanPrompt

func AdaptiveReplanPrompt(goal, instructions string, plan *Plan, failedTask *Task, failureReason string) string

AdaptiveReplanPrompt builds a prompt to re-plan remaining tasks after failures.

func AddAnnotation

func AddAnnotation(task *Task, author, text string)

AddAnnotation appends a new annotation to the task.

func ApplyCalibrationFactor

func ApplyCalibrationFactor(plan *Plan, factor float64)

ApplyCalibrationFactor scales the EstimatedMinutes of every task in the plan by factor. Values <= 0 or == 1.0 are no-ops. Values > 1 increase estimates (AI historically underestimates), values < 1 decrease them. Each task's estimate is rounded to the nearest whole minute and clamped to 1 minimum.

func BuildChainPrompt

func BuildChainPrompt(task *Task, previousOutput string) string

BuildChainPrompt returns a formatted section that prepends the previous pipeline step's output to the task prompt. The returned string is inserted directly into ExecuteTaskPrompt when task.ChainInput is non-empty.

func ClonePrompt

func ClonePrompt(original *Task, adaptContext string) string

ClonePrompt builds a prompt asking the AI to adapt a task's title and description for a new context. adaptContext describes what should change (e.g. "for module Y instead of module X").

func CollectRelevantContext

func CollectRelevantContext(workDir string, task *Task, maxTokens int) string

CollectRelevantContext scans the working directory for files relevant to the given task, collects up to maxTokens worth of the most relevant snippets, and formats them as a fenced code block section for inclusion in a task prompt.

Relevance is determined by keyword overlap between the task title/description and file paths, with git diff --stat used to boost recently-modified files. The top-N files are read and truncated to fit within the token budget.

Returns an empty string when workDir is empty, no relevant files are found, or maxTokens is zero.

func DecomposePrompt

func DecomposePrompt(goal, instructions, clarifyContext string) string

DecomposePrompt builds the prompt for decomposing a goal into tasks. clarifyContext is optional structured Q&A context from the goal clarification step; pass "" to omit it.

func DedupReason

func DedupReason(output string) string

DedupReason returns the AI's stated reason for dropping tasks. It is a lightweight wrapper that re-runs parseDedupResponse on cached output — used for logging.

func DescribeNodeLosses added in v0.0.5

func DescribeNodeLosses(losses []NodeLoss) string

DescribeNodeLosses renders losses as "sgx (unreachable 2026-10-06T12:00:01Z), edge-2 (unreachable 2026-10-06T12:03:10Z)", oldest first — the form every reason, journal row and listing uses, so an operator can grep one for the others.

func DistinctNodes added in v0.0.5

func DistinctNodes(losses []NodeLoss) []string

DistinctNodes returns the distinct executors in losses, in the order each first went down.

func EstimateTokens

func EstimateTokens(text string) int

EstimateTokens estimates the token count of text using the rule 4 chars ≈ 1 token.

func EvolveDiscoverPrompt

func EvolveDiscoverPrompt(goal, instructions string, plan *Plan, evolveStep int, innovate bool) string

EvolveDiscoverPrompt builds a prompt asking the AI to discover new improvement tasks for auto-evolve mode after the current plan is complete.

func ExecuteTaskPrompt

func ExecuteTaskPrompt(goal, instructions, workDir string, plan *Plan, task *Task, noCodeInject bool, ctx ...*ProjectContext) string

ExecuteTaskPrompt builds the prompt for executing a specific task. workDir is the project root used to load prompt-stats, KB entries, and codebase context; pass "" to skip all workDir-based injections. noCodeInject disables the automatic codebase context snippet injection (equivalent to the --no-context-inject CLI flag). Pass a non-nil ProjectContext to include project state (git, file tree).

func FingerprintSummary

func FingerprintSummary(result string) string

FingerprintSummary fingerprints a task summary so a TaskAbort can be tied to the exact text it was derived from. Whitespace is normalised away because a summary that differs only in trailing newlines is the same summary; anything else counts as a new outcome that must be classified afresh.

func FormatCountdown

func FormatCountdown(d time.Duration) string

FormatCountdown formats a duration as a concise human-readable string.

Positive: "2d 3h", "4h 30m", "45m"
Zero / overdue: "OVERDUE (2h ago)"

func IsOverdue

func IsOverdue(t *Task) bool

IsOverdue returns true if the task has a deadline that has passed and the task is still pending or in-progress (done/skipped tasks are excluded).

func LessByExecutionOrder added in v0.0.2

func LessByExecutionOrder(a, b *Task) bool

LessByExecutionOrder reports whether a runs before b.

This is the single definition of "the order tasks run in", and the dashboard's Tasks tab renders pending rows through the same three keys so that the list a user is looking at *is* the run queue. Before Task 20299 there was no such definition: the sequential path compared Priority, the parallel path compared nothing at all (it dispatched in plan order, which is ID order after a load), and the dashboard floated pinned rows to the top — so the same plan had three different answers and dragging a row changed only one of them.

The keys, in order:

  1. Pinned. `cloop task pin` documents itself as marking a high-priority task to "always appear at top", and a queue whose top entry is not the next thing to run is not a queue. Pinning is therefore a scheduling decision, not a decoration.
  2. Priority ascending — 1 is the most urgent. This is what the drag-to-reorder endpoint rewrites, so a drag is expressed in the same units the scheduler already reads.
  3. ID ascending, to break ties by creation order. Without it the order of equal-priority tasks would depend on slice order, which differs between a freshly decomposed plan (append order) and one read back from SQLite (ORDER BY id) — the same plan would run in two different orders depending on whether the hub had restarted.

func MergePrompt

func MergePrompt(tasks []*Task) string

MergePrompt builds an AI prompt to synthesise a single merged task from multiple input tasks.

func NextTrigger

func NextTrigger(expr string, from time.Time) time.Time

NextTrigger computes the next time strictly after `from` that matches the cron expression. Advances minute by minute up to 4 years forward. Returns a zero Time if no match is found (should never happen for valid expressions).

func ParseCron

func ParseCron(expr string) error

ParseCron validates a standard 5-field cron expression. Format: "min hour dom mon dow" (e.g. "0 9 * * 1" = every Monday at 09:00). Supports: numbers, *, ranges (1-5), lists (1,2,3), step values (*/5, 1-10/2).

func ParseDeadline

func ParseDeadline(s string) (time.Time, error)

ParseDeadline parses a deadline string into an absolute time.Time. Accepted formats:

  • Relative: "2h", "30m", "3d", "1w" (hours, minutes, days, weeks from now)
  • RFC3339: "2025-12-31T23:59:00Z" or "2025-12-31T23:59:00+01:00"
  • Date only: "2025-12-31" (interpreted as midnight UTC on that date)

func PinnedCount

func PinnedCount(tasks []*Task) int

PinnedCount returns the number of pinned tasks in the slice.

func PruneToTokenBudget

func PruneToTokenBudget(steps []string, budgetTokens int) []string

PruneToTokenBudget prunes steps to fit within budgetTokens using the 4 chars ≈ 1 token estimate. It always retains steps[0] (the initial goal / first step) and the last two steps. Oldest intermediate steps (index 1 through n-3) are dropped from the front until the total fits within the budget. If len(steps) ≤ 3 or the total already fits, steps is returned unchanged. budgetTokens ≤ 0 disables pruning and returns steps unchanged.

func RepositoryAccessSection added in v0.0.3

func RepositoryAccessSection(getenv func(string) string) string

RepositoryAccessSection renders the prompt section describing the repositories this run may reach, from the environment the lease established. getenv is os.Getenv in production. It returns "" when no repository was granted, so a caller can append it unconditionally.

func ResetIfDue

func ResetIfDue(task *Task, now time.Time) bool

ResetIfDue resets a recurring task to pending and advances NextRunAt when the task's recurrence schedule is due (NextRunAt <= now) and the task is in a terminal state (done, skipped, or failed).

If NextRunAt is not yet set, it is computed and saved but the task is NOT reset yet — it will reset on the next call once the time arrives.

Returns true if the task was reset to pending.

func RoleSystemPrompt

func RoleSystemPrompt(role AgentRole) string

RoleSystemPrompt returns a role-specific system context prepended to task execution prompts. Returns empty string for unknown roles or empty role (generic execution).

func SaveSnapshot

func SaveSnapshot(workDir string, plan *Plan) error

SaveSnapshot serialises the current plan and appends a new snapshot to the history directory. It deduplicates: if the plan's tasks are identical to the most-recent snapshot it returns nil without writing. The plan's Version field is incremented before saving.

func SetSnapshotRetention

func SetSnapshotRetention(keep int)

SetSnapshotRetention sets how many snapshots SaveSnapshot keeps. A value of zero or less disables write-time pruning entirely.

func SetSnapshotRetentionResolver

func SetSnapshotRetentionResolver(fn func(workDir string) int)

SetSnapshotRetentionResolver installs a per-project keep-count lookup. Pass nil to fall back to the process-wide value set by SetSnapshotRetention.

The resolver is called on every snapshot write, so it must be cheap and must not call back into this package.

func SnapshotRetention

func SnapshotRetention() int

SnapshotRetention reports the process-wide write-time keep-count. Zero means pruning is disabled.

func SplitPrompt

func SplitPrompt(task *Task, reason string) string

SplitPrompt builds a prompt asking the AI to break a task into 2-5 concrete subtasks.

func SuspectedNodeKiller added in v0.0.5

func SuspectedNodeKiller(losses []NodeLoss) bool

SuspectedNodeKiller reports whether losses make a task a suspected node killer: NodeKillerThreshold or more distinct executors.

func TaskMatchesTags

func TaskMatchesTags(task *Task, filter []string) bool

TaskMatchesTags returns true when the task has at least one tag in filter, or when filter is empty (no filter active). Always returns true if the task has no tags and the filter is also empty.

func TimeUntilDeadline

func TimeUntilDeadline(t *Task) (time.Duration, bool)

TimeUntilDeadline returns the duration remaining until the task's deadline and whether the task has a deadline at all. A negative value means overdue.

func TimeUntilDeadlineD

func TimeUntilDeadlineD(t *Task) time.Duration

TimeUntilDeadlineD is a convenience wrapper that returns the raw duration (positive = time remaining, negative = overdue). Returns 0 if no deadline.

func ValidRoles

func ValidRoles() []string

ValidRoles returns all known role names as strings (for help text and validation).

func ValidVerdict added in v0.0.5

func ValidVerdict(v ReviewVerdict) bool

ValidVerdict reports whether v is a verdict this build knows.

func VerifySignal

func VerifySignal(output string) (pass bool, found bool)

VerifySignal parses the verifier's response and returns true if VERIFY_PASS was found.

func VerifyTask

func VerifyTask(ctx context.Context, p provider.Provider, goal, instructions, model string, timeout time.Duration, task *Task, executorOutput string) (bool, error)

VerifyTask calls the provider to verify whether a task was genuinely completed. Returns (true, nil) for pass, (false, nil) for fail, (false, err) on provider error.

An empty/whitespace verifier response is treated as a transient provider hiccup and surfaced as an error rather than silently passing. The orchestrator's verifyErr branch then logs the issue and falls back to "treat as pass" so a flaky verifier never blocks a real completion — but the empty response is no longer mis-attributed as a successful verification in the audit log.

func VerifyTaskPrompt

func VerifyTaskPrompt(goal, instructions string, task *Task, executorOutput string) string

VerifyTaskPrompt builds a prompt that asks the AI to verify whether a task was actually completed. The verifier reviews the task description and the executor's output and checks the real filesystem/code.

Types

type AgentRole

type AgentRole string

AgentRole defines specialized AI expertise for a task category. The role shapes the AI's perspective and focus during task execution.

const (
	RoleBackend  AgentRole = "backend"  // API, server, database, business logic
	RoleFrontend AgentRole = "frontend" // UI, UX, HTML, CSS, JavaScript/TypeScript
	RoleTesting  AgentRole = "testing"  // Unit, integration, e2e, and benchmark tests
	RoleSecurity AgentRole = "security" // Auth, permissions, hardening, threat modeling
	RoleDevOps   AgentRole = "devops"   // CI/CD, Docker, deployment, infrastructure
	RoleData     AgentRole = "data"     // Databases, migrations, data modeling, ETL
	RoleDocs     AgentRole = "docs"     // Documentation, README, API docs, comments
	RoleReview   AgentRole = "review"   // Code review, quality, refactoring
)

type Annotation

type Annotation struct {
	Timestamp time.Time `json:"timestamp"`
	Author    string    `json:"author"` // "user" or "ai"
	Text      string    `json:"text"`
}

Annotation is a timestamped note attached to a task. Author is either "user" (from CLI) or "ai" (added by the orchestrator).

type BackgroundState

type BackgroundState string

BackgroundState is the lifecycle of background work left by an agent.

const (
	// BackgroundWaiting means cloop is currently blocked on the work. A task
	// in this state is running, not finished, however complete its own output
	// claims to be.
	BackgroundWaiting BackgroundState = "waiting"
	// BackgroundDrained means the work finished on its own and the task's
	// result can be trusted.
	BackgroundDrained BackgroundState = "drained"
	// BackgroundAbandoned means the work outlived its budget. The task is not
	// complete: its output describes work that had not finished.
	BackgroundAbandoned BackgroundState = "abandoned"
)

type BackgroundWork

type BackgroundWork struct {
	// State is "waiting" while cloop is blocked on the work, "drained" once it
	// finished, and "abandoned" when it outlived its budget.
	State BackgroundState `json:"state"`
	// Detected is how many processes were still running.
	Detected int `json:"detected"`
	// Commands names them, for diagnosis.
	Commands []string `json:"commands,omitempty"`
	// WaitedSeconds is how long cloop blocked on the work.
	WaitedSeconds int `json:"waited_seconds,omitempty"`
	// Terminated is how many processes were killed after outliving the budget.
	Terminated int `json:"terminated,omitempty"`
	// DetectedAt is when the work was first seen.
	DetectedAt time.Time `json:"detected_at"`
}

BackgroundWork records processes an agent left running after it reported the task complete, and how that resolved.

It is persisted on the task rather than only logged because it answers two questions an operator asks later: why did this task take twenty minutes, and why was this task not marked done when its own output said TASK_DONE.

func (*BackgroundWork) Pending

func (b *BackgroundWork) Pending() bool

Pending reports whether the task is still blocked on background work.

type BranchActivation

type BranchActivation struct {
	TaskID    int
	Title     string
	Activated bool   // true = task kept/reset to pending; false = task skipped
	Branch    string // "on_success" or "on_failure"
}

BranchActivation records the result of evaluating one branch entry after a task completes.

func ResolveBranch

func ResolveBranch(plan *Plan, task *Task) []BranchActivation

ResolveBranch evaluates conditional branching after a task completes.

Rules:

  • outcome TaskDone → activate on_success tasks, skip on_failure tasks
  • outcome TaskFailed → activate on_failure tasks, skip on_success tasks
  • outcome TaskSkipped or other → no branch changes

"Activate" means: if the target task is pending leave it alone; if it was skipped (deactivated by a prior branch evaluation) reset it to pending. "Skip" means: if the target task is pending mark it skipped. Tasks that are already done/in_progress/failed are never touched.

type CommitPolicy added in v0.0.5

type CommitPolicy struct {
	// Enabled turns the check on. Pushed is kept while it is off, so switching
	// it off and on again does not lose the choice.
	Enabled bool `json:"enabled"`
	// Pushed also requires the commits a task made to be on the upstream of
	// the branch they are on.
	Pushed bool `json:"pushed,omitempty"`
}

CommitPolicy is what "done" requires of a task's git work. The zero value is off.

func ParseCommitRequirement added in v0.0.5

func ParseCommitRequirement(v string) (*CommitPolicy, error)

ParseCommitRequirement turns a command-line value into a policy. The empty string means "committed", which is what the bare flag asks for.

func (*CommitPolicy) Active added in v0.0.5

func (p *CommitPolicy) Active() bool

Active reports whether p requires anything. A nil policy is off.

func (*CommitPolicy) Clone added in v0.0.5

func (p *CommitPolicy) Clone() *CommitPolicy

Clone returns an independent copy (nil for nil).

func (*CommitPolicy) Describe added in v0.0.5

func (p *CommitPolicy) Describe() string

Describe says in a few words what "done" requires under p.

func (*CommitPolicy) Requirement added in v0.0.5

func (p *CommitPolicy) Requirement() string

Requirement is the command-line value that selects p: "committed", "pushed" or "off".

func (*CommitPolicy) RequiresPush added in v0.0.5

func (p *CommitPolicy) RequiresPush() bool

RequiresPush reports whether p requires the task's commits to be pushed.

type FieldChange

type FieldChange struct {
	Field    string
	OldValue string
	NewValue string
}

FieldChange records one field that changed between two task versions.

type Link struct {
	URL   string   `json:"url"`
	Label string   `json:"label,omitempty"`
	Kind  LinkKind `json:"kind"`
}

Link is an external URL associated with a task (ticket, PR, doc, artifact).

type LinkKind

type LinkKind string

LinkKind classifies an external link attached to a task.

const (
	LinkKindTicket   LinkKind = "ticket"   // bug tracker issue (e.g. GitHub Issues, Jira)
	LinkKindPR       LinkKind = "pr"       // pull / merge request
	LinkKindDoc      LinkKind = "doc"      // documentation page
	LinkKindArtifact LinkKind = "artifact" // build artifact, download, or generic URL
)

func DetectLinkKind

func DetectLinkKind(rawURL string) LinkKind

DetectLinkKind infers a LinkKind from a URL string using simple pattern matching. Callers can override the detected kind with an explicit flag.

type MergeOptions

type MergeOptions struct {
	// Provider options forwarded to the AI call.
	Provider provider.Options
}

MergeOptions controls the behaviour of Merge.

type MoveResult

type MoveResult struct {
	// Task is the task as it now exists in the destination plan (new ID).
	Task *Task
	// SrcTaskID is the original ID in the source plan.
	SrcTaskID int
	// DstTaskID is the newly assigned ID in the destination plan.
	DstTaskID int
}

MoveResult holds the outcome of a Move operation.

func Move

func Move(
	ctx context.Context,
	srcPlan *Plan,
	dstPlan *Plan,
	dstGoal string,
	taskID int,
	adapt bool,
	p provider.Provider,
	opts provider.Options,
) (*MoveResult, error)

Move relocates a task from one cloop project directory to another.

The source task is marked as skipped in the source plan (preserving history). A copy is appended to the destination plan with a new ID that does not conflict with any existing task IDs in that plan. Both plans are saved atomically (source first, then destination).

When adapt is true, the AI rewrites the task title and description to suit the destination project's goal. p and opts must be non-nil in that case.

LoadState and SaveState are callbacks so the caller (cmd layer) controls how state is loaded/saved — this keeps pkg/pm free of pkg/state imports.

type NodeLoss added in v0.0.5

type NodeLoss struct {
	// ExecutorID is the executor that went unreachable.
	ExecutorID string `json:"executor_id"`
	// LostAt is when it was found unreachable.
	LostAt time.Time `json:"lost_at"`
	// SessionID is the executor session that was lost with it.
	SessionID string `json:"session_id,omitempty"`
	// Attempt is that session's attempt number: 1 for the original dispatch.
	Attempt int `json:"attempt,omitempty"`
}

NodeLoss is one executor that went unreachable while a task was running on it.

func SortNodeLosses added in v0.0.5

func SortNodeLosses(losses []NodeLoss) []NodeLoss

SortNodeLosses returns a copy of losses ordered by when each node went down.

type OverdueResult

type OverdueResult struct {
	Task    *Task
	Overdue bool
	// OldPriority is the priority before boosting (0 if not boosted).
	OldPriority int
	// Boosted is true when the priority was changed.
	Boosted bool
}

OverdueResult holds the result of an overdue check on a single task.

func CheckAndBoostOverdue

func CheckAndBoostOverdue(plan *Plan) []OverdueResult

CheckAndBoostOverdue examines all tasks in the plan for missed deadlines. For each overdue task it:

  1. Boosts priority to 1 (highest) if not already P1.
  2. Returns the result so callers can fire notifications.

The plan is mutated in place; callers should persist state after calling this.

type Plan

type Plan struct {
	Goal    string  `json:"goal"`
	Tasks   []*Task `json:"tasks"`
	Version int     `json:"version"`
}

Plan is the full task plan for a goal.

func Decompose

func Decompose(ctx context.Context, p provider.Provider, goal, instructions, model string, timeout time.Duration, clarifyContext string) (*Plan, error)

Decompose calls the provider to decompose a goal into a task plan. clarifyContext is optional structured Q&A context from the clarification step; pass "" to omit it.

An empty/whitespace provider response is surfaced as an error rather than dropped into ParseTaskPlan, where it would produce an opaque "no JSON found in response" message that hid the real (transient) cause — matching the empty-output discipline applied across the orchestrator stack.

func NewPlan

func NewPlan(goal string) *Plan

NewPlan creates an empty plan for a goal.

func ParseTaskPlan

func ParseTaskPlan(goal, output string) (*Plan, error)

ParseTaskPlan extracts a Plan from the AI's JSON response.

func RestoreSnapshot

func RestoreSnapshot(workDir, snapshotID string) (*Plan, error)

RestoreSnapshot loads the snapshot identified by snapshotID (a version number as a string, e.g. "3") and returns its Plan. The caller is responsible for persisting the returned plan back to state.

func (*Plan) AbortedOutcomes

func (p *Plan) AbortedOutcomes() []*Task

AbortedOutcomes returns every task carrying an abort record, cleared or not, in plan order. This is the reporting view: the UI shows cleared entries too, annotated with why they were allowed to stand.

func (*Plan) CountByStatus

func (p *Plan) CountByStatus() (done, failed int)

CountByStatus returns the number of done (includes skipped) and failed tasks. Timed-out tasks are counted as failed.

func (*Plan) DepsReady

func (p *Plan) DepsReady(t *Task) bool

DepsReady returns true when all of this task's dependencies are done or skipped.

func (*Plan) IsComplete

func (p *Plan) IsComplete() bool

IsComplete returns true if all tasks are done or skipped, or if remaining pending tasks are permanently blocked (all their deps include a failed task).

A task recorded as done whose stored summary is an uncleared provider or harness refusal does not count (Task 20224). It never ran, so a plan resting on it is not finished — and answering otherwise is what let auto-evolve plan new work on top of features that were never written. The orchestrator's sweep reopens such tasks, but the guard lives here so that every caller asking whether the plan is done gets the same truthful answer.

func (*Plan) NextTask

func (p *Plan) NextTask() *Task

NextTask returns the pending task that runs next: the first of ReadyTasks in execution order. It agrees with GateTasks by construction — both take the head of the same sorted candidate set — which is what stops the CLI's "next task" readouts (cloop task next, cloop context, cloop prioritize) from naming a different task than the orchestrator actually picks.

func (*Plan) PermanentlyBlocked

func (p *Plan) PermanentlyBlocked(t *Task) bool

PermanentlyBlocked returns true if a task can never run because one of its dependencies has failed or timed out (and is not retried).

func (*Plan) ReadyTasks

func (p *Plan) ReadyTasks() []*Task

ReadyTasks returns all pending tasks whose dependencies are satisfied, in the order they run. In parallel mode all of these can run concurrently, but the order still decides which ones start first when the worker pool is smaller than the queue — so it is sorted here rather than left in plan order, which would make a reorder in the UI invisible to every capped parallel run.

func (*Plan) Summary

func (p *Plan) Summary() string

Summary returns a short summary of task completion.

func (*Plan) TaskByID

func (p *Plan) TaskByID(id int) *Task

TaskByID returns the task with the given ID, or nil if not found.

func (*Plan) UnverifiedAborts

func (p *Plan) UnverifiedAborts() []*Task

UnverifiedAborts returns the tasks recorded as done whose stored outcome is an uncleared refusal — work the plan believes shipped and which demonstrably never ran.

type PlanDiff

type PlanDiff struct {
	Added   []*Task
	Removed []*Task
	Changed []TaskDiff
}

PlanDiff is the result of comparing two Plan snapshots.

func DiffPlans

func DiffPlans(a, b *Plan) PlanDiff

DiffPlans computes the diff between plan a (old) and plan b (new).

func (*PlanDiff) IsEmpty

func (d *PlanDiff) IsEmpty() bool

IsEmpty returns true if there are no differences.

type ProjectContext

type ProjectContext struct {
	FileTree  string
	GitStatus string
	RecentLog string
	WorkDir   string
}

ProjectContext holds a lightweight snapshot of the project's current state. It is injected into task prompts to give the AI situational awareness.

func BuildProjectContext

func BuildProjectContext(workdir string) *ProjectContext

BuildProjectContext collects project state from the filesystem and git. Failures are silently ignored — context is best-effort.

func (*ProjectContext) Format

func (c *ProjectContext) Format() string

Format returns a human-readable string of the project context for prompt injection. Returns empty string if context is entirely empty.

type PruneStats

type PruneStats struct {
	// Deleted counts snapshot files removed (or, for a dry run, that would
	// have been removed).
	Deleted int
	// BytesFreed sums their sizes.
	BytesFreed int64
	// Remaining counts the snapshots left behind.
	Remaining int
}

PruneStats reports what a snapshot prune removed.

func PruneSnapshots

func PruneSnapshots(workDir string, keep int, dryRun bool) (PruneStats, error)

PruneSnapshots deletes all but the `keep` newest snapshots in workDir's plan-history directory, newest by version number.

keep <= 0 is a no-op: it means "retention disabled", not "delete everything". Callers that want an empty history should remove the directory themselves, so that a mis-parsed or zero-valued config can never be the instruction that destroys a project's history.

Files that do not parse as snapshots are left strictly alone — including the `.corrupt-<unix>` siblings LoadSnapshot quarantines, which are forensic evidence that something wrote a bad snapshot and are not ours to reclaim.

type ReviewFinding added in v0.0.5

type ReviewFinding struct {
	// Severity is "blocker", "major", "minor" or "nit".
	Severity string `json:"severity"`
	File     string `json:"file,omitempty"`
	Line     int    `json:"line,omitempty"`
	Title    string `json:"title"`
	Detail   string `json:"detail,omitempty"`
}

ReviewFinding is one problem the reviewer reported.

type ReviewGate added in v0.0.5

type ReviewGate struct {
	// Enabled turns the gate on. Everything else is kept while it is off, so
	// switching it off and on again does not lose the reviewer's settings.
	Enabled bool `json:"enabled"`
	// Provider is the reviewer's provider; empty means the project's own.
	Provider string `json:"provider,omitempty"`
	// Model is the reviewer's model; empty means that provider's default —
	// or, when Provider is empty too, the model the project works with.
	Model string `json:"model,omitempty"`
	// Effort is the reviewer's reasoning effort (claudecode only).
	Effort string `json:"effort,omitempty"`
	// Mode is what a review that does not approve does: ReviewModeFix
	// (the default when empty), ReviewModeBlock or ReviewModeAdvisory.
	Mode string `json:"mode,omitempty"`
	// MaxFixRounds bounds how often ReviewModeFix sends a task back.
	// 0 means DefaultReviewFixRounds.
	MaxFixRounds int `json:"max_fix_rounds,omitempty"`
	// Instructions are extra review criteria from the operator, appended to
	// every review ("reject anything that touches the migrations").
	Instructions string `json:"instructions,omitempty"`
}

ReviewGate configures the reviewer that checks a task's changes before cloop publishes them. The zero value is a disabled gate.

func (*ReviewGate) Active added in v0.0.5

func (g *ReviewGate) Active() bool

Active reports whether g is a gate that reviews. A nil gate is off.

func (*ReviewGate) Clone added in v0.0.5

func (g *ReviewGate) Clone() *ReviewGate

Clone returns an independent copy (nil for nil).

func (*ReviewGate) EffectiveFixRounds added in v0.0.5

func (g *ReviewGate) EffectiveFixRounds() int

EffectiveFixRounds returns how many times a fix-mode gate sends a task back. It is 0 for the other modes, which never do.

func (*ReviewGate) EffectiveMode added in v0.0.5

func (g *ReviewGate) EffectiveMode() string

EffectiveMode returns the mode in force, applying the default.

func (*ReviewGate) Normalize added in v0.0.5

func (g *ReviewGate) Normalize()

Normalize trims and lower-cases the enumerated fields in place, so a value typed as " Anthropic " is stored the way it is compared.

func (*ReviewGate) Validate added in v0.0.5

func (g *ReviewGate) Validate() error

Validate reports the first setting that cannot be used. It checks settings even while the gate is disabled: a stored value is one enabling the gate will start using, and the moment to refuse it is when it is typed.

type ReviewPublish added in v0.0.5

type ReviewPublish struct {
	Repo    string `json:"repo"`
	Remote  string `json:"remote"`
	Ref     string `json:"ref"`
	Commit  string `json:"commit,omitempty"`
	Outcome string `json:"outcome"`
	Detail  string `json:"detail,omitempty"`
}

ReviewPublish is one push the gate held, and its fate.

type ReviewVerdict added in v0.0.5

type ReviewVerdict string

ReviewVerdict is the outcome of a task's review.

const (
	// ReviewApproved means the reviewer approved the changes.
	ReviewApproved ReviewVerdict = "approved"
	// ReviewChangesRequested means the reviewer found problems that should be
	// fixed before the changes are published.
	ReviewChangesRequested ReviewVerdict = "changes_requested"
	// ReviewUnavailable means no verdict could be reached: the reviewer could
	// not be built or called, or answered with something that is not a
	// verdict. The gate fails closed on it, as it does on a rejection.
	ReviewUnavailable ReviewVerdict = "unavailable"
	// ReviewNothing means the task changed nothing in any repository, so
	// there was nothing to review and nothing to publish.
	ReviewNothing ReviewVerdict = "no_changes"
)

type ReviewedRepo added in v0.0.5

type ReviewedRepo struct {
	// Path is the repository's directory relative to the project ("." for
	// the project's own repository).
	Path string `json:"path"`
	// Base and Head are the commits the diff was taken between; the working
	// tree's uncommitted changes were included on top of Head.
	Base string `json:"base,omitempty"`
	Head string `json:"head,omitempty"`
	// Files, Insertions and Deletions summarise the diff.
	Files      int `json:"files"`
	Insertions int `json:"insertions,omitempty"`
	Deletions  int `json:"deletions,omitempty"`
	// Commits is how many commits Base..Head holds.
	Commits int `json:"commits,omitempty"`
	// Uncommitted reports that the working tree held changes not in Head.
	Uncommitted bool `json:"uncommitted,omitempty"`
	// Truncated reports that the diff was cut to fit the review prompt.
	Truncated bool `json:"truncated,omitempty"`
}

ReviewedRepo describes the changes one repository contributed to a review.

type SLAStats

type SLAStats struct {
	// Total number of tasks that had a deadline.
	Total int
	// Met is the number of tasks completed (done/skipped) before or by their deadline.
	Met int
	// Missed is the number of tasks that failed or are overdue past their deadline.
	Missed int
	// ComplianceRatio is Met/Total (NaN when Total==0).
	ComplianceRatio float64
}

SLAStats computes deadline compliance statistics for a plan.

func ComputeSLAStats

func ComputeSLAStats(plan *Plan) SLAStats

ComputeSLAStats calculates SLA compliance for a plan.

type Snapshot

type Snapshot struct {
	Version   int       `json:"version"`
	Timestamp time.Time `json:"timestamp"`
	Plan      *Plan     `json:"plan"`
}

Snapshot is a versioned, timestamped copy of a Plan.

func LoadSnapshot

func LoadSnapshot(workDir string, version int) (*Snapshot, error)

LoadSnapshot loads the snapshot with the given version number. Returns an error if no snapshot with that version exists.

type SnapshotMeta

type SnapshotMeta struct {
	Version   int       `json:"version"`
	Timestamp time.Time `json:"timestamp"`
	Filename  string    `json:"filename"`
	TaskCount int       `json:"task_count"`
	Summary   string    `json:"summary"`
}

SnapshotMeta contains lightweight metadata about a snapshot (no full plan).

func ListSnapshots

func ListSnapshots(workDir string) ([]*SnapshotMeta, error)

ListSnapshots returns metadata for all saved snapshots, sorted by version ascending.

type Task

type Task struct {
	ID               int          `json:"id"`
	Title            string       `json:"title"`
	Description      string       `json:"description"`
	Priority         int          `json:"priority"` // 1 = highest
	Status           TaskStatus   `json:"status"`
	Role             AgentRole    `json:"role,omitempty"`       // specialized agent role for this task
	DependsOn        []int        `json:"depends_on,omitempty"` // IDs of tasks that must complete before this one
	Result           string       `json:"result,omitempty"`
	StartedAt        *time.Time   `json:"started_at,omitempty"`
	CompletedAt      *time.Time   `json:"completed_at,omitempty"`
	Deadline         *time.Time   `json:"deadline,omitempty"`          // optional SLA deadline; overdue tasks are auto-boosted to P1
	VerifyRetries    int          `json:"verify_retries,omitempty"`    // number of times task was re-queued by verifier
	GitHubIssue      int          `json:"github_issue,omitempty"`      // linked GitHub issue number (0 = none)
	EstimatedMinutes int          `json:"estimated_minutes,omitempty"` // AI-predicted duration
	ActualMinutes    int          `json:"actual_minutes,omitempty"`    // measured duration (set on completion)
	ArtifactPath     string       `json:"artifact_path,omitempty"`     // path to the full AI response artifact file
	FailureDiagnosis string       `json:"failure_diagnosis,omitempty"` // AI diagnosis of what went wrong on failure
	Tags             []string     `json:"tags,omitempty"`              // user-defined labels for filtering
	FailCount        int          `json:"fail_count,omitempty"`        // number of times this task has been marked failed
	HealAttempts     int          `json:"heal_attempts,omitempty"`     // number of auto-heal re-attempts made for this task
	Annotations      []Annotation `json:"annotations,omitempty"`       // timestamped notes from user or AI
	Condition        string       `json:"condition,omitempty"`         // optional gate: "$cmd" = shell check, else AI yes/no prompt
	Recurrence       string       `json:"recurrence,omitempty"`        // cron expression (5-field); auto-resets task when due
	NextRunAt        *time.Time   `json:"next_run_at,omitempty"`       // computed next trigger time for recurring tasks
	RequiresApproval bool         `json:"requires_approval,omitempty"` // task must be interactively approved before execution
	Approved         bool         `json:"approved,omitempty"`          // pre-approved via 'cloop task approve'; bypasses interactive gate
	MaxMinutes       int          `json:"max_minutes,omitempty"`       // per-task execution time budget in minutes (0 = no limit)
	Assignee         string       `json:"assignee,omitempty"`          // team member assigned to this task
	ExternalURL      string       `json:"external_url,omitempty"`      // URL of the external resource (e.g. GitHub PR)
	Links            []Link       `json:"links,omitempty"`             // external URLs/tickets/docs associated with this task
	Pinned           bool         `json:"pinned,omitempty"`            // pinned tasks always appear at the top of task lists
	TDDStatus        string       `json:"tdd_status,omitempty"`        // TDD verification outcome: "pass", "fail", or "skipped"
	TDDScore         int          `json:"tdd_score,omitempty"`         // TDD score 0-100 (percentage of acceptance criteria met)
	SprintID         int          `json:"sprint_id,omitempty"`         // sprint this task belongs to (0 = unassigned)
	ComplexitySize   string       `json:"complexity_size,omitempty"`   // T-shirt size: XS/S/M/L/XL (set by ai-complexity)
	StoryPoints      int          `json:"story_points,omitempty"`      // Fibonacci story points: 1/2/3/5/8/13 (set by ai-complexity)
	// ChainInput holds the previous task's output when this task is part of a pipeline
	// chain (tagged "chain:<uuid>"). It is runtime-only and never persisted: it is a
	// copy of up to 16 MiB of transcript, so the orchestrator derives it again from
	// the stored predecessor when it dispatches the task (ensureChainInput) rather
	// than writing it into every save.
	ChainInput string `json:"-"`
	// OnSuccess lists task IDs to activate (keep pending) when this task succeeds.
	// Tasks in OnFailure are skipped when the success branch is taken.
	OnSuccess []string `json:"on_success,omitempty"`
	// OnFailure lists task IDs to activate (keep pending) when this task fails.
	// Tasks in OnSuccess are skipped when the failure branch is taken.
	OnFailure []string `json:"on_failure,omitempty"`
	// RiskScore is the cached AI likelihood/risk score 1-10 (set by ai-risk-matrix).
	RiskScore int `json:"risk_score,omitempty"`
	// ImpactScore is the cached AI strategic impact score 1-10 (set by ai-risk-matrix or ai-impact).
	ImpactScore int `json:"impact_score,omitempty"`
	// RetryBudget caps the total number of provider attempts this task is
	// allowed to consume across all DoWithRetry calls (Task 20114). Zero
	// or unset means use provider.DefaultRetryBudget. Tasks known to be
	// expensive or experimental can set a tighter cap to bound the blast
	// radius of repeated provider failures.
	RetryBudget int `json:"retry_budget,omitempty"`
	// WriteBackBranch and WriteBackCommit record where an isolated executor
	// left this task's work (Task 20180). They are set only for a task that
	// ran somewhere the hub does not share a filesystem with: a local run's
	// changes are already in the working tree, so there is no branch to name
	// and nothing to point a reviewer at. When they are empty, the result was
	// produced in place; when they are set, the work is only reachable by
	// fetching the named branch.
	WriteBackBranch string `json:"write_back_branch,omitempty"`
	WriteBackCommit string `json:"write_back_commit,omitempty"`
	// ExecutorID, ExecutorKind and Isolation record *where* this task ran
	// (Task 20244). The hub's headline claim is that it never spawns a
	// harness on the host; without these the claim is unauditable after the
	// fact, because executor identity otherwise exists only in the
	// project_executors binding — which says where the project's *next* task
	// would go, not where this one went — and in audit payloads that are
	// scoped to a dispatch rather than to a task.
	//
	// They are stamped at task start, not at completion, so a task that is
	// still in flight is attributable too: "what is running on that edge
	// device right now" is the question an operator asks during an incident,
	// and a field that only appears at completion cannot answer it.
	//
	// Isolation is the boundary strength the executor advertised
	// ("none" | "container" | "vm" | "remote"), mirroring
	// executor.Capabilities.Isolation. Kind mirrors executor.Kind()
	// ("localprocess" | "container" | "remote" | "kubernetes"). Held as
	// strings rather than the executor package's named types to keep pm free
	// of a dependency on the execution layer — pm is the data model that the
	// CLI, the TUI and the exporters all load, and none of them place work.
	//
	// Isolation "none" or kind "localprocess" means the harness ran as a
	// child process of the hub with the host's filesystem and network. That
	// is the case the UI renders differently on purpose: it is legitimate for
	// a developer's own machine and is precisely what must never happen
	// silently on a shared hub.
	ExecutorID   string `json:"executor_id,omitempty"`
	ExecutorKind string `json:"executor_kind,omitempty"`
	Isolation    string `json:"isolation,omitempty"`
	// RunID names the execution this task was dispatched into (Task 20282).
	//
	// It is the join key between this task's audit rows and the secret-lease
	// rows the hub wrote when it started the run — "which leases did task 63
	// hold" has as many answers as task 63 has attempts, and only a per-run
	// token makes the question answerable exactly.
	//
	// Stamped at start alongside the executor fields, and deliberately *not*
	// cleared when a task is reset: the run id belongs to the attempt that is
	// ending, and the terminal audit row is written after the reset. It is
	// overwritten by the next attempt's dispatch.
	RunID string `json:"run_id,omitempty"`
	// Background records work the agent harness left running after it claimed
	// to be finished (Task 20205), so the UI can show why a task waited, or
	// why it was not accepted as done.
	Background *BackgroundWork `json:"background,omitempty"`
	// Abort records that this task's stored summary is a provider or harness
	// refusal rather than work (Task 20224). It is set by the orchestrator's
	// pre-completion sweep and is what stops a plan full of usage-limit
	// messages counting as finished. See abort.go for the lifecycle.
	Abort *TaskAbort `json:"abort,omitempty"`
	// Review records what the project's review gate decided about this task's
	// changes, and which of the pushes it held were published (Task 20357).
	// Nil when the gate was off. See review.go.
	Review *TaskReview `json:"review,omitempty"`
	// Quarantine marks a task the control plane suspects of taking down the
	// nodes that ran it (Task 20391): two or more distinct executors went
	// unreachable while it was running. It runs again only after an explicit
	// reset. See quarantine.go.
	//
	// Not a plan_tasks column: it lives in task_quarantine, written by the
	// failover that marks it and deleted by the reset that clears it, and by
	// nothing else. A run's save holds a copy of the plan loaded before the
	// mark existed, and must be able to neither write it back nor away.
	Quarantine *TaskQuarantine `json:"quarantine,omitempty"`
	// Stop says why this task's execution was ended from outside its run, set
	// by whoever settles the run for the one save that records the end — the
	// hub, for a workload its executor stopped at its disk limit (Task 20405).
	// It is what that save's task.finish row carries as structured fields.
	//
	// Never persisted, and not a plan_tasks column: the durable account is the
	// note written beside it, which the task keeps; this exists only so the
	// terminal audit row can say "disk_limit" and both sizes as fields an
	// auditor can query, rather than as prose.
	Stop *TaskStop `json:"-"`
}

Task is a single unit of work derived from the project goal.

func AdaptiveReplan

func AdaptiveReplan(ctx context.Context, p provider.Provider, goal, instructions, model string, timeout time.Duration, plan *Plan, failedTask *Task, failureReason string) ([]*Task, error)

AdaptiveReplan calls the provider to re-plan remaining tasks after a failure. It returns a new set of tasks to append/replace the pending tasks in the plan.

An empty/whitespace provider response is surfaced as an error rather than dropped into ParseTaskPlan, where it would produce an opaque "no JSON found in response" message that hid the real (transient) cause — matching the empty-output discipline applied across the orchestrator stack.

func Clone

func Clone(ctx context.Context, p provider.Provider, opts provider.Options, plan *Plan, taskID int, adaptContext string) (*Task, error)

Clone duplicates a task and optionally uses the AI to adapt its title/description for a new context (adaptContext). Without adaptContext, the task is copied verbatim with " (copy)" appended to the title.

The cloned task:

  • Gets the next available ID in the plan
  • Inherits all fields (priority, role, tags, deps) from the original
  • Has status reset to pending
  • Has timing fields (StartedAt, CompletedAt, etc.) cleared

The caller is responsible for saving state after Clone returns.

func DeduplicateTasks

func DeduplicateTasks(ctx context.Context, p provider.Provider, opts provider.Options, existing []*Task, candidates []*Task) ([]*Task, error)

DeduplicateTasks calls the provider to filter out candidates that duplicate existing tasks. It returns only the candidates the AI considers genuinely new. If the provider call fails or the response can't be parsed, all candidates are returned unchanged (fail-open: better to inject a duplicate than to silently drop novel work).

func Merge

func Merge(ctx context.Context, p provider.Provider, opts MergeOptions, plan *Plan, ids []string) (*Task, error)

Merge synthesises a new task from the identified input tasks using an AI provider:

  1. Validates all IDs exist in the plan.
  2. Calls the AI with MergePrompt to produce title/description/role.
  3. Builds the merged task with union deps/tags, highest priority, earliest deadline.
  4. Appends the merged task to the plan.
  5. Marks all input tasks as skipped.

The caller is responsible for saving state after Merge returns.

func ParseEvolveTasks

func ParseEvolveTasks(goal, output string, existing *Plan) ([]*Task, error)

ParseEvolveTasks parses newly discovered tasks from evolve mode output, ensuring IDs don't conflict with existing plan tasks.

func ParseSplitResponse

func ParseSplitResponse(response string, parentTask *Task, nextID int) ([]*Task, error)

ParseSplitResponse parses the AI's JSON response into subtasks. New IDs are assigned as parentID + suffix (e.g. task 5 becomes 5a, 5b, etc.). IDs are assigned sequentially starting from nextID.

func SortByExecutionOrder added in v0.0.2

func SortByExecutionOrder(tasks []*Task) []*Task

SortByExecutionOrder returns a shallow copy of tasks in the order they run. It does NOT mutate the input slice — callers hold the plan's own backing array, and reordering it underneath them would change which task a pending index refers to.

func SortPinnedFirst

func SortPinnedFirst(tasks []*Task) []*Task

SortPinnedFirst returns a shallow copy of tasks sorted so that pinned tasks come first, preserving original order within each group. It does NOT mutate the input slice.

func SplitTask

func SplitTask(ctx context.Context, p provider.Provider, opts provider.Options, plan *Plan, taskID int, reason string) ([]*Task, error)

SplitTask orchestrates an AI-driven task split:

  1. Calls the AI with SplitPrompt
  2. Parses the response into subtasks
  3. Removes the original task from the plan
  4. Inserts subtasks at the same position with the same priority
  5. Remaps any tasks that depended on the original task to depend on the last subtask

Returns the new subtasks on success.

func (*Task) Quarantined added in v0.0.5

func (t *Task) Quarantined() bool

Quarantined reports whether t is held back until an explicit reset.

type TaskAbort

type TaskAbort struct {
	// Class is the orchestrator's AbortClass as a string ("usage_limit",
	// "quota_exceeded", "harness_refused", ...). It is kept as a plain string
	// because the classifier lives in pkg/orchestrator, which imports this
	// package; holding the typed value here would invert that dependency.
	Class string `json:"class"`
	// Reason is operator-facing prose naming what the provider said.
	Reason string `json:"reason"`
	// Evidence is the matched excerpt of the stored summary, so the record
	// shows what was actually seen rather than our paraphrase of it.
	Evidence string `json:"evidence,omitempty"`
	// DetectedAt is when the sweep first recognised this outcome.
	DetectedAt time.Time `json:"detected_at,omitempty"`
	// SummaryFingerprint binds this record to the exact summary it describes.
	// When the task runs again its summary changes, the fingerprint stops
	// matching, and the whole record — including any clearance below — ceases
	// to apply. It is what makes reusing the stored classification safe rather
	// than merely cheap: a stale verdict can never mask a fresh refusal.
	//
	// Not a credential despite being a hash: it digests the task's own summary,
	// which is already visible to anyone who can read the task, and it gates a
	// cache decision rather than access to anything.
	SummaryFingerprint string `json:"summary_fingerprint,omitempty"`

	// Cleared records a verdict that the work exists despite the refusal
	// summary — typically because a later task re-landed it. A cleared abort
	// stops blocking plan completion but stays visible, because the ledger
	// entry is still wrong and a reader of project history deserves to know
	// why it was allowed to stand.
	Cleared bool `json:"cleared,omitempty"`
	// ClearedBy names who made that call ("cli", "triage", a username).
	ClearedBy string `json:"cleared_by,omitempty"`
	// ClearedNote is the evidence for the clearance. Required in practice:
	// a clearance without a reason is indistinguishable from giving up.
	ClearedNote string     `json:"cleared_note,omitempty"`
	ClearedAt   *time.Time `json:"cleared_at,omitempty"`
}

TaskAbort records that a task's stored summary is a provider or harness refusal rather than a description of work done.

func (*TaskAbort) Blocks

func (a *TaskAbort) Blocks() bool

Blocks reports whether this abort should stop a plan counting as complete. A nil receiver does not block, so callers can ask the question of any task.

func (*TaskAbort) Clear

func (a *TaskAbort) Clear(by, note string)

Clear records the verdict that the work exists despite the refusal summary.

type TaskDiff

type TaskDiff struct {
	ID      int
	Title   string
	Changes []FieldChange
}

TaskDiff describes what changed in a specific task between two plan versions.

type TaskQuarantine added in v0.0.5

type TaskQuarantine struct {
	// Kind is what the task is suspected of; QuarantineNodeKiller.
	Kind string `json:"kind"`
	// Reason is one sentence naming the nodes.
	Reason string `json:"reason,omitempty"`
	// Nodes are the losses behind the mark, oldest first.
	Nodes []NodeLoss `json:"nodes,omitempty"`
	// MarkedAt is when the task was marked.
	MarkedAt time.Time `json:"marked_at"`
	// MarkedBy is what marked it, e.g. "failover".
	MarkedBy string `json:"marked_by,omitempty"`
}

TaskQuarantine marks a task that runs again only after an explicit reset.

type TaskReview added in v0.0.5

type TaskReview struct {
	// Verdict is the final outcome across every round.
	Verdict ReviewVerdict `json:"verdict"`
	// Mode is the gate's mode when the review ran.
	Mode string `json:"mode"`
	// Blocked reports that the gate stopped the task's changes from being
	// published: the verdict was not an approval and the mode was not
	// advisory.
	Blocked bool `json:"blocked,omitempty"`
	// Provider and Model name the reviewer.
	Provider string `json:"provider,omitempty"`
	Model    string `json:"model,omitempty"`
	// Rounds is how many reviews ran; FixRounds how often the agent was sent
	// back with findings.
	Rounds    int `json:"rounds"`
	FixRounds int `json:"fix_rounds,omitempty"`
	// Summary is the reviewer's one-paragraph assessment of the last round.
	Summary string `json:"summary,omitempty"`
	// Findings are the last round's findings, most severe first.
	Findings []ReviewFinding `json:"findings,omitempty"`
	// Error says why no verdict was reached (ReviewUnavailable).
	Error string `json:"error,omitempty"`
	// Repos describes what the last round reviewed, per repository.
	Repos []ReviewedRepo `json:"repos,omitempty"`
	// Published records each push the agent attempted while the gate held it,
	// and what became of it.
	Published []ReviewPublish `json:"published,omitempty"`
	// ReviewedAt is when the last round finished.
	ReviewedAt time.Time `json:"reviewed_at"`
}

TaskReview records what the review gate decided about a task, and what was published because of it.

func (*TaskReview) Approved added in v0.0.5

func (r *TaskReview) Approved() bool

Approved reports whether the review let the task's changes through: an approval, a task with nothing to review, or any verdict in advisory mode.

func (*TaskReview) Bound added in v0.0.5

func (r *TaskReview) Bound()

Bound trims the free-text fields to their stored sizes in place. The reviewer is a model and its output is untrusted in size.

func (*TaskReview) Clone added in v0.0.5

func (r *TaskReview) Clone() *TaskReview

Clone returns a deep copy (nil for nil).

func (*TaskReview) Diagnosis added in v0.0.5

func (r *TaskReview) Diagnosis() string

Diagnosis renders the review for Task.FailureDiagnosis when the gate blocked the task: what to fix, in the reviewer's words.

func (*TaskReview) Headline added in v0.0.5

func (r *TaskReview) Headline() string

Headline renders the review as one line for annotations, logs and events.

type TaskStatus

type TaskStatus string

TaskStatus represents the state of a task.

const (
	TaskPending    TaskStatus = "pending"
	TaskInProgress TaskStatus = "in_progress"
	TaskDone       TaskStatus = "done"
	TaskFailed     TaskStatus = "failed"
	TaskSkipped    TaskStatus = "skipped"
	TaskTimedOut   TaskStatus = "timed_out"
)

func CheckTaskSignal

func CheckTaskSignal(output string) TaskStatus

CheckTaskSignal looks for TASK_DONE, TASK_SKIPPED, TASK_FAILED in output.

type TaskStop added in v0.0.5

type TaskStop struct {
	// Cause is the machine-readable reason: StopDiskLimit.
	Cause string
	// Detail is the sentence naming it, with its numbers.
	Detail string
	// DiskUsedMB and DiskLimitMB are the measured workspace and the limit it
	// was held to, for a disk-limit stop.
	DiskUsedMB  int64
	DiskLimitMB int
}

TaskStop is why an execution was ended from outside its run.

Jump to

Keyboard shortcuts

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