pm

package
v0.0.4 Latest Latest
Warning

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

Go to latest
Published: Sep 27, 2026 License: MIT Imports: 22 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 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.

Variables

This section is empty.

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 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 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 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 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 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 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.
	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"`
}

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.

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 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.

Jump to

Keyboard shortcuts

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