executor

package module
v0.3.3 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 26 Imported by: 0

Documentation

Overview

Package executor defines the pluggable execution layer: how a claimed task actually runs.

Index

Examples

Constants

View Source
const (
	VerifyStageGofmt      = "gofmt"       // the gate's gofmt stage died
	VerifyStageTest       = "test"        // a go-test stage failed
	VerifyStageE2ETimeout = "e2e-timeout" // a timeout killed the run (go-test watchdog/panic or the task deadline)
	VerifyStageRun        = "run"         // the run as a whole: spawn failure, pre-test stage, foreign tooling
)

Verify-gate death classes stamped into FailureEvidence.VerifyStage, so a verify death's failing stage is queryable data instead of log forensics (row 144: 62 of the 93 gofmt-vendor deaths had their sidecar logs rotated, leaving the all-ok tail as the only witness, and one e2e kill was misread as gofmt until a docs cross-check).

View Source
const DefaultAgentBinary = "crush"

DefaultAgentBinary is used when Bin, $TQ_AGENT_BIN and $TQ_CRUSH_BIN are all empty.

View Source
const DefaultCloseoutPrompt = `` /* 960-byte string literal not displayed */

DefaultCloseoutPrompt is the second conversation turn every agent task runs when the pool enables --task-closeout: the same brutal self-review the owner uses interactively, answered by the agent that did the work, in the same session. {{TASK_ID}} resolves at execution time (report path + commit footer). The queue derives the run's commits and files from git trailers — the close-out no longer re-emits any result line.

View Source
const EvidenceTailBytes = 4096

EvidenceTailBytes is the ONE output-tail size every executor pins into FailureEvidence.Tail — agent, verify and command tails stay the same length so facts are comparable across executors (01:48 report f23: three independent 4096s, one drift waiting to happen).

View Source
const GoEnvExperiment = "GOEXPERIMENT=jsonv2"

GoEnvExperiment is the GOEXPERIMENT value minted Go verify commands export so the gate is identical inside and outside the flake devShell: without it, encoding/json/v2 imports die with "build constraints exclude all Go files" (the tq-agent-pool unit env carries no GOEXPERIMENT — the env lie that judged finished work on a broken gate).

View Source
const RedactMarker = "[REDACTED]"

RedactMarker replaces every provider-token-shaped match in tails, evidence facts and sidecar logs. Uniform on purpose: the tail's job is proving what failed, not identifying which credential leaked.

View Source
const ScopedGofmtStage = `test -z "$(gofmt -l . | git check-ignore --stdin -v --non-matching | grep '^::')"`

ScopedGofmtStage is the gitignore-aware gofmt stage minted Go verify commands carry: `gofmt -l .` walks the whole tree, gitignored vendor/ included (generated code an agent must not reformat), and `git check-ignore --stdin -v --non-matching` prints `::<TAB><path>` for exactly the paths git does NOT ignore, so the gate fails only on formatting defects in files that could be committed. Untracked files stay checked on purpose: agents write new files before committing them, so a tracked-only listing would open the exact hole the gate exists to close. Outside a git work tree the stage degrades to a pass (agent verifies always run in git clones). This kills the vendor-gofmt death class (P2) at the mint instead of classifying it after the fact.

View Source
const TaskTrailer = "Task-Queue-ID"

TaskTrailer is the git-footer key that attributes a commit to a queue task: agents end commit messages with `Task-Queue-ID: <id>` (the prompt contracts teach it; the executor resolves the placeholder at run time). Outcome derivation reads the same trailer the session-close bridge reads for `Crush-Session`.

View Source
const TaskTypeAgent = "agent"

TaskTypeAgent runs a headless AI coding agent (crush) in a repository. Register it under this type to make the queue agent-capable.

View Source
const TaskTypeDLQFix = "dlqfix"

TaskTypeDLQFix runs a DLQ autopsy: a headless agent diagnoses WHY a dead-lettered task exhausted its retries and either fixes the root cause (the sweeper then rescues the original) or rules it unfixable with a reasoned verdict (the sweeper then dismisses the original). Register it under this type to make a pool autopsy-capable; internal/dlqfix mints the tasks and performs the dispositions.

Both verdicts complete the task — a wontfix autopsy did its job; the disposition is mechanical, not the exit code. Only the output contract is enforced: no valid verdict line, or a wontfix without a reason, is a failed attempt.

View Source
const TaskTypeDepBump = "depbump"

TaskTypeDepBump runs a deterministic dependency bump (and optional release) in one repository: baseline, go get each module at an EXACT version, tidy/vendor/templ fixups, verify, commit, tag (and optionally push). It is the executor side of the depsweep pipeline: the sweeper (internal/depsweep) turns a project-dependency-graph plan into depbump tasks chained by the plan's own DAG. Register it under this type to make a pool upgrade-capable.

View Source
const TaskTypePrioritize = "prioritize"

TaskTypePrioritize runs a batch scorer agent over one repo's WORKING SET: a headless agent reads the open TODO_LIST items and returns a structured score/effort/confidence verdict per item. The sweeper (internal/prioritize) caches verdicts in the score cache and applies them as reprioritization facts. Register it under this type to make a pool prioritize-capable.

View Source
const TaskTypeReview = "review"

TaskTypeReview runs a reviewer agent over a COMPLETED agent task: a second headless agent inspects the change (item, commit, files) and returns a structured verdict. Register it under this type to make a pool review-capable.

Both verdicts complete the task — a review that requested changes did its job; the findings (in the completion fact detail) drive any follow-up work, not the exit code. Only the mechanical contract is enforced: output that is not a valid verdict JSON is a failed attempt, exactly like a failed verify.

View Source
const TaskTypeStatus = "status"

TaskTypeStatus runs the "done prompt": after a window of agent completions, a headless agent writes a full status report (docs/status/<timestamp>.md) and closes the loop by appending the next work items to TODO_LIST.md, where the next harvest tick picks them up. Register it under this type to make a pool status-capable; internal/status mints the tasks.

View Source
const VerifyGateEnvCode = "vendor-gofmt"

VerifyGateEnvCode is the machine-greppable reason code carried by every VerifyGateEnvironmental error text, so DLQ triage and rescue sweeps can classify the death as environmental without re-deriving it from evidence tails.

Variables

View Source
var (
	// ErrDepBumpEmptyPayload pins the empty-payload rejection.
	ErrDepBumpEmptyPayload = errors.New("depbump: empty payload, want {repo, bumps|release}")
	// ErrDepBumpNoWork rejects payloads with neither bumps nor a release.
	ErrDepBumpNoWork = errors.New("depbump: payload needs at least one bump or a release")
	// ErrDepBumpBadVersion rejects unparsable/empty bump target versions.
	ErrDepBumpBadVersion = errors.New("depbump: bump target version is empty or not stable semver")
)

DepBump contract sentinels: call sites match with errors.Is.

View Source
var (
	ErrPrioritizeEmptyPayload = errors.New("prioritize: empty payload, want {repo, items}")
	// ErrPrioritizeSparsePayload covers a decoded payload missing repo/items.
	ErrPrioritizeSparsePayload = errors.New("prioritize: payload needs non-empty repo and items")
	// ErrPrioritizeUnknownItem, ErrPrioritizeDuplicateVerdict, and
	// ErrPrioritizeMissingVerdict pin the exactly-once coverage rule.
	ErrPrioritizeUnknownItem      = errors.New("verdict for unknown item")
	ErrPrioritizeDuplicateVerdict = errors.New("duplicate verdict for item")
	ErrPrioritizeMissingVerdict   = errors.New("no verdict for item")
	// ErrPrioritizeScoreRange pins the 0-100 score domain.
	ErrPrioritizeScoreRange = errors.New("out of range 0-100")
)

Prioritize contract sentinels: call sites match with errors.Is; the wrapped forms render exactly the messages the ad-hoc errors carried.

View Source
var ErrDLQFixSummaryMissing = errors.New(
	"verdict JSON has no summary (a diagnosis is required for either verdict)",
)

ErrDLQFixSummaryMissing pins the wontfix-needs-a-diagnosis rule: an unexplained dismissal is exactly the behavior being automated away.

View Source
var ErrUnknownType = errors.New("executor: unknown task type")

ErrUnknownType is returned by Registry.Lookup for unregistered task types.

Functions

func AgentVersion

func AgentVersion(ctx context.Context, bin string) (string, error)

AgentVersion probes the agent binary's --version line (first stdout line, trimmed). Pools call it at startup so a missing/stale binary is a warning, not a surprise mid-task.

func CommandFromPayload added in v0.3.0

func CommandFromPayload(payload []byte) string

CommandFromPayload is the read-only view of unwrapCommand for surfaces that DISPLAY a command without executing it (tq show, the web dashboard): same accepted shapes — raw shell line, JSON string, {"cmd": "..."} — and the same "true" default for an empty payload. It lives beside the executor so the display can never drift from what actually runs.

func DetectRateLimit added in v0.3.0

func DetectRateLimit(output string, now time.Time) (time.Duration, bool)

DetectRateLimit scans agent output for provider exhaustion and returns how long to wait before the next attempt. The delay is, in order of trustworthiness: the provider's reset timestamp (+ grace), a numeric retry-after hint (+ grace), then defaultRateLimitBackoff. now is injectable for tests.

func DetectVerify

func DetectVerify(repo string) string

DetectVerify is the exported form of autoDetectVerify for tools that prepare repos (tq bootstrap pins the detected command into .tq-verify).

func Excerpt added in v0.3.1

func Excerpt(text string) string

Excerpt reduces an item text, agent prompt, or session summary to its first line, bounded to excerptMaxLen bytes — a one-line pointer, never a transcript (the full text stays in the payload, read via `tq show`). Truncation is rune-safe: the byte cut backs off to a whole UTF-8 boundary so a multi-byte prompt never renders a broken rune. Trailing whitespace is trimmed before the ellipsis so a mid-line truncation never ends in a dangling space.

func ExtractResultPayload

func ExtractResultPayload(output string) ([]string, string, bool)

ExtractResultPayload parses the agent's structured self-report ({files_changed, commit_sha}) from its output — the legacy fallback for in-flight tasks; derivation is the primary source. Best-effort: no line, no problem — the fields simply stay empty in the result detail.

func ExtractSessionID

func ExtractSessionID(output string) string

ExtractSessionID pulls a crush session id out of agent output. Best-effort by design: crush's output format is not a stable contract.

func IsGateArtifactDeath added in v0.3.1

func IsGateArtifactDeath(ev FailureEvidence, lastError string) bool

IsGateArtifactDeath reports whether a DEAD task's stored failure names the unscoped-gofmt verify-gate artifact (the VerifyGateEnvironmental class): either the death was classified at run time (VerifyGateEnvCode in the recorded error text) or it is a LEGACY fact predating both stamps, where the all-ok evidence tail plus a gofmt stage in the recorded verify command is the witness (the minted gofmt stage swallows its own output, row 237). A real regression — any FAIL marker, a stamped non-environmental class, missing evidence — is never the artifact. dlqfix uses this to auto-dismiss shipped gate-artifact deaths instead of minting an autopsy (TODO row 145); the miss direction is always safe (an autopsy runs).

func IsStableSemver added in v0.3.1

func IsStableSemver(v string) bool

IsStableSemver reports whether v is a non-empty stable semver version ("v1.2.3" form, no pre-release/build suffixes). Bump targets must be stable: dev/-rc targets are exactly the class the sweeper refuses.

func Permanent

func Permanent(cause error) error

Permanent wraps cause in a *PermanentError; nil passes through unchanged. A cause that is already permanent is returned as-is, so defensive double wrapping stays a single class.

func QuestionPending added in v0.3.1

func QuestionPending(cause error, retryAfter time.Duration) error

QuestionPending wraps cause in a *QuestionPendingError; nil passes through unchanged. A cause that is already a *QuestionPendingError is returned as-is, so defensive double wrapping stays a single class.

func RateLimited added in v0.3.0

func RateLimited(cause error, retryAfter time.Duration) error

RateLimited wraps cause in a *RateLimitError; nil passes through unchanged. A cause that is already a *RateLimitError is returned as-is, so defensive double wrapping stays a single class.

func ReadTQVerify

func ReadTQVerify(repoDir string) string

ReadTQVerify is the exported form of readTQVerify for tools that compose payloads (the harvester pins the repo's verify contract into tasks).

func RedactSecrets added in v0.3.1

func RedactSecrets(s string) string

RedactSecrets masks every provider-token-shaped match in s. Pure: no env, no I/O — surfaces that must redact unconditionally (and the audit) call it directly.

func RenderAgentPayload

func RenderAgentPayload(p AgentPayload) (jsontext.Value, error)

RenderAgentPayload marshals a payload for tasks of type agent.

func ResultLine

func ResultLine(output string) (jsontext.Value, error)

ResultLine extracts the raw JSON of the LAST TQ_RESULT line from agent output. Executors with a mechanical output contract (review, status) build their strict parsing on top of it; ExtractResultPayload is the lenient consumer for plain agent runs.

func SecretHits added in v0.3.1

func SecretHits(s string) int

SecretHits counts distinct provider-token-shaped locations in s — the detector half of the pass: tq audit --journal scans stored facts with it so tokens that landed in evidence BEFORE the redaction pass are findable without ever printing them. Pattern shapes overlap by design (the bearer and authorization-header shapes both match one "Authorization: Bearer …" line), so overlapping match spans are merged before counting — the hit count estimates secret LOCATIONS, not pattern matches.

func SetFailureEvidence

func SetFailureEvidence(ctx context.Context, stage string, err error, tail string)

SetFailureEvidence attaches forensics for a FAILING execution; a no-op when the context carries no sink. Mirrors SetResultDetail: executors call it on their failure paths, the worker hands it to Store.Fail so the task.failed fact carries the evidence.

func SetResultDetail

func SetResultDetail(ctx context.Context, detail jsontext.Value)

SetResultDetail attaches outcome detail to the current execution; a no-op when the context carries no sink (plain executors, tests).

func SetVerifyFailureEvidence added in v0.3.1

func SetVerifyFailureEvidence(ctx context.Context, verifyStage string, err error, tail string)

SetVerifyFailureEvidence attaches verify-gate forensics with the death class stamped: Stage "verify" plus VerifyStage (verifyDeathStage's gofmt / test / e2e-timeout / run), so DLQ triage can query the failing stage from the fact instead of re-deriving it from tails and rotated logs.

func StaleVerifyReasons added in v0.3.1

func StaleVerifyReasons(verify string) []string

StaleVerifyReasons reports why a verify pin matches a KNOWN-STALE minted verify form — the f46 audit class (09-39 report §e3): a task enqueued before the verify contract moved on still carries the old gate, and the doctor's --hygiene check must flag it as a gate, not an investigation. The predicates are REPO-INDEPENDENT (pure content matching), so the check works even where the repo directory is absent. Returns nil for a clean pin; each reason is one line of operator-facing text.

The known-stale forms are the historical Go verify mints, in order:

  1. "go build ./... && go test ./... -count=1" — root-module-only, so a multi-module repo verifies vacuously (the original f46 incident).
  2. the find -execdir walk — swallows the inner exit status (find reports only its own errors), so a red nested gate passed unnoticed.
  3. any Go gate without the GOEXPERIMENT=jsonv2 export — predates the env-self-contained mint and dies outside the flake devShell on repos importing encoding/json/v2 (the 2026-09-11 env-lie class).
  4. any gofmt stage without the gitignore scope — the unscoped `test -z "$(gofmt -l .)"` shape flags gitignored vendor/ trees and kills the gate on files the agent cannot even commit (the 2026-10-01 P2 class: 167 dead letters, every dispatched task killed after doing real work).

The current mint (goEnvPrelude + the explicit-exit `for f in $(find …)` walk + the gitignore-scoped gofmt stage) matches none of the predicates; non-Go gates (npm/make/cargo/nix) are out of scope for the Go-mint evolution and always come back clean.

func SweepSidecars

func SweepSidecars(dir string, maxAge time.Duration) (int, error)

SweepSidecars deletes agent-output sidecar logs older than maxAge from dir and returns how many were removed — the retention hatch behind --log-dir-max-age, so a long-running pool's sidecar directory cannot grow forever. Only *.log files directly inside dir are considered; other content is left alone. Errors on individual files are skipped (retention is best effort: a locked file must never fail the pool tick).

func SweepSidecarsByBytes

func SweepSidecarsByBytes(dir string, maxBytes int64) (int, error)

SweepSidecarsByBytes caps the total size of sidecar logs in dir: when the *.log files together exceed maxBytes, the OLDEST are deleted until the total fits — the retention hatch behind --log-dir-max-bytes (the round-6 retention item's open half). Age-based retention cannot bound a high-traffic dir that stays under --log-dir-max-age; a byte budget does. Only *.log files directly inside dir are considered; other content is left alone, and per-file errors are skipped (best effort, like the age sweep).

Types

type AgentExecutor

type AgentExecutor struct {
	// Bin is the agent executable. Default: $TQ_AGENT_BIN, $TQ_CRUSH_BIN or
	// "crush". Point it at a stub in tests.
	Bin string
	// ProjectsDir resolves relative Repo names in payloads ("demo" →
	// <ProjectsDir>/demo). Absolute Repo paths bypass it.
	ProjectsDir string
	// Yolo is the operator-level autonomy request (pool start). crush run
	// has no yolo flag; see AgentPayload.Yolo for how autonomy is actually
	// granted (repo-local crush config). Fail closed, never silently.
	Yolo bool
	// MaxConcurrent caps how many agent processes run at once MACHINE-WIDE
	// (flock'd slot files shared across every tq process on the host; 0 =
	// uncapped). Caps cost when several pools share a machine.
	MaxConcurrent int
	// CloseoutPrompt, when set, gives every agent task a SECOND conversation
	// turn: after the work turn ends, the same session receives this prompt
	// (the owner's brutal self-review + status report) before verify runs.
	// Empty = off (single turn, pinned argv contract unchanged).
	CloseoutPrompt string
	// ReresolveVerify enables claim-time verify re-resolution: the payload's
	// enqueue-time verify pin is IGNORED and the gate is resolved fresh
	// (.tq-verify file, then auto-detect) — a verify-contract change in the
	// repo can no longer fire a stale pinned command at a task queued before
	// the change (09-39 f3). Default false keeps the payload contract:
	// file > pin > detect.
	ReresolveVerify bool
	// contains filtered or unexported fields
}

AgentExecutor runs one headless AI coding agent per task, then enforces the payload's verify contract. It must be safe for concurrent use; the worker pool executes several tasks in parallel.

func NewAgentExecutor

func NewAgentExecutor(projectsDir string) *AgentExecutor

NewAgentExecutor builds an AgentExecutor for a projects directory.

func (*AgentExecutor) Execute

func (e *AgentExecutor) Execute(ctx context.Context, t task.Task) error

Execute guards the repo, runs the agent, then runs the verify command. Any miss is a failed attempt (the queue retries with backoff, then dead-letters). Input-contract misses (payload, repo, dirty tree, autonomy) are permanent: the identical retry would fail identically, and for agent tasks every retry is real money.

func (*AgentExecutor) WithoutCloseout added in v0.3.0

func (e *AgentExecutor) WithoutCloseout() *AgentExecutor

WithoutCloseout returns a clone with CloseoutPrompt cleared, for the review and status executors (they ARE the second opinion; giving them the work turn's self-review would double agent cost for no new signal). The clone carries every runtime setting but starts with a FRESH rate-limit gate: gates are per-executor-instance (re-arming from fresh provider evidence is one cheap refused run), and an armed atomic.Int64 must never be struct-copied (copylocks). The clone is marked secondOpinion: the owner-question channel stays work-turn-only (21-04 §f42).

type AgentPayload

type AgentPayload struct {
	// Repo is the repository the agent works in: a name resolved against the
	// executor's ProjectsDir, or an absolute path. Required.
	Repo string `json:"repo"`
	// Prompt is the full instruction for the agent run. Required.
	Prompt string `json:"prompt"`
	// Model optionally overrides the crush model ("provider/model").
	Model string `json:"model,omitempty"`
	// V is the payload contract version. Zero decodes
	// as v1; a version above what this binary understands fails fast as a
	// permanent error instead of misparsing newer fields.
	V int `json:"v,omitempty"`
	// Session optionally continues a previous crush session by ID.
	Session string `json:"session,omitempty"`
	// Dedup is the harvester's item key; purely informational, used to keep
	// TODO items and tasks 1:1 across harvest runs.
	Dedup string `json:"dedup,omitempty"`
	// Item is the raw TODO_LIST work item the prompt was rendered from;
	// purely informational. Status windows pin it so report excerpts show
	// the real work instead of the prompt template's first line. Empty for
	// tasks minted outside the harvester.
	Item string `json:"item,omitempty"`
	// Items carries the full member list when the task was minted as a
	// BATCH (harvest --batch-items > 1): one task, many work items, one
	// session. Item stays the FIRST member; purely informational.
	Items []string `json:"items,omitempty"`
	// Verify is a shell command that must exit 0 after the agent run for the
	// task to complete — the enforced quality gate. Empty means auto-detect:
	// Go repositories (go.mod present) run "go build ./... && go test ./...
	// -count=1", everything else runs nothing.
	Verify string `json:"verify,omitempty"`
	// RequireClean refuses to start unless the repo's git tree is clean, so
	// the pool never tramples human work-in-progress. Default true. Repos
	// without a .git directory skip the check (nothing to protect).
	RequireClean *bool `json:"require_clean,omitempty"`
	// TimeoutMinutes caps the whole task (agent run + verify). Default 30.
	// The worker's task timeout still applies as a hard ceiling above this.
	TimeoutMinutes int `json:"timeout_minutes,omitempty"`
	// Yolo marks this task's agent as autonomous. crush run has NO yolo
	// flag (verified against crush v0.92: "Unknown flag: --yolo"); autonomy
	// comes from the repo's own project-local crush config granting
	// permissions (`.crushrc`: `permissions allow view ls grep edit write bash`).
	// This field makes the executor fail fast with remediation guidance when
	// autonomy is requested but the repo has no such config, instead of
	// burning agent attempts on runs that stall on permission prompts.
	Yolo bool `json:"yolo,omitempty"`
	// Answered carries the owner's rulings for questions this task asked
	// (PapDashboard questions): the store injects them when the answer
	// unblocks the parked task, and the executor renders them into the
	// prompt so the resumed run honors the decisions instead of re-asking.
	Answered []queue.QuestionAnsweredDetail `json:"answered,omitempty"`
	// RejectedSHA is the commit a review-fix task was minted against (the
	// finding's cited commit, falling back to the reviewed run's commit).
	// Structured so the claim-time done-preflight can run the
	// rejected-SHA disposition (exists? superseded? anchor gone?) without
	// parsing prompt prose. Empty on non-fix tasks.
	RejectedSHA string `json:"rejected_sha,omitempty"`
	// Anchor is the verbatim text a review-fix finding attaches to (same
	// provenance as RejectedSHA): the disposition's no-anchor-left signal.
	// Empty when the finding carried no anchor.
	Anchor string `json:"anchor,omitempty"`
}

AgentPayload is the payload contract for "agent" tasks: run a headless AI coding agent in a repository, then prove the result.

type AgentResult

type AgentResult struct {

	// VerifyTail is the last lines of the verify command's output: the
	// proof the task completed on.
	VerifyTail string `json:"verify_tail,omitempty"`
	// Commits are the commits whose `Task-Queue-ID: <id>` footer names this
	// task, DERIVED from git after the run (oldest first) — the agent never
	// reports them. FilesChanged is the union of files those commits
	// touched; CommitSHA is the newest one (the review sweeper and web UI
	// read both). A legacy `TQ_RESULT` self-report still fills them when
	// derivation finds nothing (in-flight tasks minted before derivation).
	Commits      []Commit `json:"commits,omitempty"`
	FilesChanged []string `json:"files_changed,omitempty"`
	CommitSHA    string   `json:"commit_sha,omitempty"`
	// PreflightDone carries the done-preflight reason when the worker
	// completed the task WITHOUT an agent run because the work was provably
	// already done (footer commits exist, item ticked, fix cured…). A
	// non-empty value means no session ran and no review may be minted for
	// this completion (review.enqueueReview skips it).
	PreflightDone string `json:"preflight_done,omitempty"`
	// contains filtered or unexported fields
}

AgentResult is the structured outcome detail of one agent run, stored alongside the completion so `tq show` can answer "what did the agent actually do" without SSH-ing into logs.

type CommandExecutor

type CommandExecutor struct {
	Template string
	// MemoryLimitMB caps the shell process's virtual memory via
	// `ulimit -v` (POSIX sh; 0 = uncapped). Bounds runaway payloads on
	// shared machines — the queue keeps serving while the task dies fast.
	MemoryLimitMB int
	// Nice lowers scheduling priority via `nice -n` (0 = unchanged).
	// Interactive work wins CPU; queue work yields.
	Nice int
}

CommandExecutor runs a command per task.

If Template is empty, the task payload is used as the shell line verbatim — enqueue '{"cmd":"..."}' JSON or just the raw command text; JSON {"cmd":…} is unwrapped automatically.

If Template is set, it may contain {{ID}}, {{PROJECT}}, {{TYPE}}, {{PAYLOAD}} placeholders substituted per task.

func NewCommandExecutor

func NewCommandExecutor(template string) *CommandExecutor

NewCommandExecutor builds a CommandExecutor from an optional template.

func (*CommandExecutor) Execute

func (e *CommandExecutor) Execute(ctx context.Context, t task.Task) error

Execute renders the template (or unwraps the payload) and runs it.

art-dupl:accept exec wiring: one buffer aliased to both Stdout and Stderr is the os/exec idiom; a seam would outlive the three lines it saves.

type Commit added in v0.3.1

type Commit struct {
	SHA     string `json:"sha"`
	Subject string `json:"subject"`
}

Commit is one trailer-attributed commit (git footer `key: value`).

type DLQFixExecutor added in v0.3.0

type DLQFixExecutor struct {
	// Agent supplies the run mechanics and its configuration (Bin,
	// ProjectsDir, Yolo). nil selects a default AgentExecutor.
	Agent *AgentExecutor
}

DLQFixExecutor runs autopsy agents. It reuses the agent run mechanics (repo resolution, autonomy preflight, process-group handling) through its Agent executor; the only differences are the prompt and the strict output contract.

func (*DLQFixExecutor) Execute added in v0.3.0

func (e *DLQFixExecutor) Execute(ctx context.Context, t task.Task) error

Execute runs the autopsy and enforces the verdict contract. fixed and wontfix both succeed; malformed output (or a wontfix without a reason) fails the attempt — retryable, the model may comply on a retry. Input-contract misses are permanent.

type DLQFixPayload added in v0.3.0

type DLQFixPayload struct {
	// Repo is the repository the dead task ran in (same resolution rules as
	// AgentPayload.Repo). Required.
	Repo string `json:"repo"`
	// DeadTask is the dead task's queue id — lineage for `tq show` and the
	// sweeper's disposition target. Required.
	DeadTask string `json:"dead_task"`
	// DeadType is the dead task's type (the mint rule scopes autopsies to
	// agent tasks; the field rides along for the prompt and forensics).
	DeadType string `json:"dead_type,omitempty"`
	// Work is what the dead task was asked to do: the agent prompt, with
	// its {{TASK_ID}} placeholders already resolved the way the dead run
	// saw them. Required.
	Work string `json:"work"`
	// Failure is the structured forensics from the dead task's LAST
	// task.failed fact (stage, exit code, output tail). Zero when the
	// journal predates failure evidence.
	Failure FailureEvidence `json:"failure,omitempty"`
	// LastError is the dead task record's error text.
	LastError string `json:"last_error,omitempty"`
	// Attempts is how many attempts the dead task burned.
	Attempts int `json:"attempts,omitempty"`
	// Model optionally overrides the crush model for the autopsy.
	Model string `json:"model,omitempty"`
	// Yolo marks the autopsy as autonomous (same contract as
	// AgentPayload.Yolo: needs a repo-local crush config granting
	// permissions). Mints mirror the dead task's autonomy.
	Yolo bool `json:"yolo,omitempty"`
	// RequireClean defaults FALSE for autopsies, deliberately: a dead
	// agent's uncommitted partial work IS evidence, and a clean-tree
	// preflight would requeue the autopsy forever on exactly the cases the
	// feature exists for. The repo's .git absence skips the check as
	// usual. Minted payloads pin the false explicitly.
	RequireClean *bool `json:"require_clean,omitempty"`
	// TimeoutMinutes caps the autopsy run. Default 30 (a diagnosis is a
	// full work turn: read code, maybe reproduce, maybe fix).
	TimeoutMinutes int `json:"timeout_minutes,omitempty"`
}

DLQFixPayload is the payload contract for "dlqfix" tasks. Self-contained, like ReviewPayload: the evidence travels in the payload, so the executor stays a pure process runner.

type DLQFixResult added in v0.3.0

type DLQFixResult struct {
	Verdict DLQFixVerdict `json:"verdict"`
	// Summary is the diagnosis: for fixed, the root cause and the change;
	// for wontfix, the reason the task is unfixable from the repo.
	Summary string `json:"summary"`
	// CommitSHA is the fix commit the autopsy landed ("" for wontfix, or a
	// fixed diagnosis that needed no repo change).
	CommitSHA string `json:"commit_sha,omitempty"`
	// contains filtered or unexported fields
}

DLQFixResult is the structured outcome of one autopsy run, stored in the completion fact detail (the sink convention) for the sweeper and `tq show`.

func ParseDLQFixResult added in v0.3.0

func ParseDLQFixResult(output string) (DLQFixResult, error)

ParseDLQFixResult extracts and validates the verdict JSON from autopsy output. Strict where it matters: the verdict must be exactly fixed or wontfix (case-insensitive), the JSON must parse, and a wontfix must carry a non-empty summary (an unexplained dismissal is the behavior being automated away).

type DLQFixVerdict added in v0.3.0

type DLQFixVerdict string

DLQFixVerdict is the autopsy's decision. Only these two values are accepted; anything else is invalid output and a failed attempt.

const (
	// VerdictFixed means the root cause lived in the repo and has been
	// fixed (and proven) by the autopsy; the original task is rescuable.
	VerdictFixed DLQFixVerdict = "fixed"
	// VerdictWontFix means the root cause is outside the repo (or the task
	// is unworkable for a stated reason); the original is dismissed. The
	// summary IS the reason — a wontfix without one is invalid output.
	VerdictWontFix DLQFixVerdict = "wontfix"
)

type DepBump added in v0.3.1

type DepBump struct {
	Module  string `json:"module"`
	Version string `json:"version"`
}

DepBump is one exact module pin: go get <module>@<version>.

type DepBumpExecutor added in v0.3.1

type DepBumpExecutor struct {
	// ProjectsDir resolves relative Repo names in payloads ("demo" →
	// <ProjectsDir>/demo). Absolute Repo paths bypass it.
	ProjectsDir string
	// ExtraEnv is appended to every spawned go/git environment (e.g.
	// GOEXPERIMENT=jsonv2 — the pool unit env does not carry it).
	ExtraEnv []string
	// GoBin, GitBin, and TemplBin default to "go", "git", and "templ"
	// from PATH (templ absent is a soft skip; see regenerateTempl). The
	// overrides exist for tests and pinned deployments.
	GoBin    string
	GitBin   string
	TemplBin string
}

DepBumpExecutor applies DepBumpPayloads deterministically.

func NewDepBumpExecutor added in v0.3.1

func NewDepBumpExecutor(projectsDir string) *DepBumpExecutor

NewDepBumpExecutor returns a DepBumpExecutor resolving repos under projectsDir.

func (*DepBumpExecutor) Execute added in v0.3.1

func (e *DepBumpExecutor) Execute(ctx context.Context, t task.Task) error

Execute runs one bump task. Error classes follow the executor contract: payload/contract misses are Permanent; a dirty tree is a Preflight requeue (someone else holds the repo — retry later without burning an attempt); go failures are regular attempts (bounded retries, then DLQ). Any failure AFTER mutations rolls the touched paths back to HEAD so the repo is left clean for the retry.

type DepBumpPayload added in v0.3.1

type DepBumpPayload struct {
	// Repo is the repository to bump: a name resolved against the
	// executor's ProjectsDir, or an absolute path. Required.
	Repo string `json:"repo"`
	// RepoName is the display name (task listings, logs).
	RepoName string `json:"repo_name,omitempty"`
	// Bumps are the exact pins to apply, in order. May be empty when the
	// task is release-only (the stale-build case: deps are already
	// committed, only the tag is missing).
	Bumps []DepBump `json:"bumps,omitempty"`
	// Release optionally tags (and pushes) after verification.
	Release *DepBumpRelease `json:"release,omitempty"`
	// V is the payload contract version. Zero decodes as v1; a version
	// above what this binary understands fails fast as a permanent error.
	V int `json:"v,omitempty"`
	// RequireClean refuses to start unless the repo's git tree is clean,
	// so the bump never interleaves with an agent's or human's WIP.
	// Default true.
	RequireClean *bool `json:"require_clean,omitempty"`
	// TimeoutMinutes caps each go command. Default 10; the worker's task
	// timeout still applies as the hard ceiling above it.
	TimeoutMinutes int `json:"timeout_minutes,omitempty"`
}

DepBumpPayload is the payload contract for "depbump" tasks. Everything is deterministic: no model, no prompt — a fixed list of exact pins and an optional release. The mechanical gate is the executor's own verify (build + test) before anything is committed.

type DepBumpRelease added in v0.3.1

type DepBumpRelease struct {
	Version string `json:"version"`
	Push    bool   `json:"push,omitempty"`
}

DepBumpRelease cuts a release after the (optional) bumps verify: an annotated tag at the repo HEAD. Version is the exact tag ("v1.2.3"), resolved from the plan's nextVersion or patch-bumped by the sweeper — the executor never invents versions. Push additionally pushes master and the new tag, which is what makes the tag proxy-resolvable for consumers; it stays opt-in because pushes are the irreversible step.

type Executor

type Executor interface {
	Execute(ctx context.Context, t task.Task) error
}

Executor runs one claimed task. It must be safe for concurrent use. Returning nil marks the task completed; any error marks a failed attempt. Return a *PermanentError (see Permanent) when retrying can never help.

type FailureClass added in v0.3.1

type FailureClass string

FailureClass is the retry taxonomy for one failed attempt: WHY the run failed, in the vocabulary the retry ladder already acts on. The worker stamps it onto the failure evidence (and DLQ autopsies read it back) so forensics never re-derive the class from the output tail (paperclip's provider-failure classification, M12).

const (
	// FailureClassTransient: the identical retry may succeed (flake,
	// contention, kernel ETXTBSY). A plain retry with backoff — the
	// store's default failure treatment — is already the right ladder.
	FailureClassTransient FailureClass = "transient"
	// FailureClassPermanent: the identical retry fails identically (bad
	// payload, missing repo, an introduced verify failure). Dead-letter
	// material; a retry only burns money.
	FailureClassPermanent FailureClass = "permanent"
	// FailureClassProviderWindow: the model provider behind the run
	// refused (429 / usage window); the identical retry fails until the
	// window resets. Requeue WITHOUT burning an attempt, parked on the
	// parsed window.
	FailureClassProviderWindow FailureClass = "provider-window"
	// FailureClassEnvironment: the task's environment refused to run or
	// to prove the run (dirty tree, missing autonomy, dead or slow verify
	// gate). Requeue WITHOUT burning an attempt on the escalation ladder.
	FailureClassEnvironment FailureClass = "environment"
)

func ClassifyFailure added in v0.3.1

func ClassifyFailure(err error) FailureClass

ClassifyFailure maps a failed run's error onto the retry taxonomy. It lives here (not inline in the worker) so the taxonomy has ONE definition, but it is CALLED by the worker: only the worker sees the final error wrapper chain (a verify gate wraps a plain failure and changes its class). An unrecognized error classifies transient — a plain retry is already the default treatment, so a wrong guess degrades to today's behavior, never worse.

type FailureEvidence

type FailureEvidence struct {
	Stage    string `json:"stage"`               // "agent", "verify" or "command"
	ExitCode int    `json:"exit_code,omitempty"` // process exit code (0 when the error was not an exit)
	Tail     string `json:"tail,omitempty"`      // last lines of the failing output
	// VerifyStage names the failing stage OF THE VERIFY GATE on Stage
	// "verify" deaths: a VerifyStageGofmt / VerifyStageTest /
	// VerifyStageE2ETimeout / VerifyStageRun class. Stamped from the FULL
	// run output at the tail cut (verifyDeathStage), so the class stays
	// queryable after the evidence tail truncates and the sidecar logs
	// rotate. Empty for non-verify failures.
	VerifyStage string `json:"verify_stage,omitempty"`
	// Class is the retry taxonomy (FailureClass) stamped by the WORKER
	// after the run: only the worker sees the final error wrapper chain
	// (a verify gate wraps a plain failure and changes its class). Empty
	// on legacy facts; readers treat empty as "unclassified".
	Class string `json:"class,omitempty"`
}

FailureEvidence is the structured forensics attached to a task.failed fact: WHAT failed (which stage), the process exit code, and the tail of the output that proves it. The 21:40 window's two retry-path failures left empty {} detail — the why lived only in the error text.

type Func

type Func func(ctx context.Context, t task.Task) error

Func adapts a plain function to Executor.

func (Func) Execute

func (f Func) Execute(ctx context.Context, t task.Task) error

Execute calls f.

type GitLogScanner added in v0.3.1

type GitLogScanner struct {
	// Bin is the git binary; empty means "git" from PATH.
	Bin string
}

GitLogScanner attributes commits by scanning `git log` for the footer trailer. Commits come back oldest first. Requires git >= 2.15 (%(trailers) format); every supported environment clears that floor by years.

func (GitLogScanner) CommitsByTrailer added in v0.3.1

func (s GitLogScanner) CommitsByTrailer(ctx context.Context, repo, key, value string) ([]Commit, error)

CommitsByTrailer implements GitScanner. A repository without any commit yet (unborn HEAD) attributes nothing — that is an empty session, not an error.

type GitScanner added in v0.3.1

type GitScanner interface {
	CommitsByTrailer(ctx context.Context, repo, key, value string) ([]Commit, error)
}

GitScanner attributes commits: which commits in repo carry the git footer `key: value`. The interface keeps consumers (the session-close bridge, outcome derivation) testable; GitLogScanner is the real implementation.

type GitScannerFunc added in v0.3.1

type GitScannerFunc func(ctx context.Context, repo, key, value string) ([]Commit, error)

GitScannerFunc adapts a function to GitScanner.

func (GitScannerFunc) CommitsByTrailer added in v0.3.1

func (f GitScannerFunc) CommitsByTrailer(ctx context.Context, repo, key, value string) ([]Commit, error)

CommitsByTrailer implements GitScanner.

type HTTPExecutor

type HTTPExecutor struct {
	URL    string
	Client *http.Client
}

HTTPExecutor POSTs the task as JSON to a URL. Any 2xx completes the task; anything else (or transport error) fails the attempt.

func NewHTTPExecutor

func NewHTTPExecutor(url string) *HTTPExecutor

NewHTTPExecutor builds an HTTP executor with a connection-pooled default client (see the http* constants).

func (*HTTPExecutor) Execute

func (e *HTTPExecutor) Execute(ctx context.Context, t task.Task) error

Execute posts the task envelope. Classification: a malformed URL is permanent; a 429 is a *RateLimitError (same no-attempt-burn requeue as agent tasks — an HTTP 429 burns the retry budget exactly like the 2026-09-11 incident otherwise); transport errors and 408/5xx responses are transient; any other non-2xx (404 route, 401 auth, 400 body) is permanent — the same request would fail the same way again.

type LeaseLostError

type LeaseLostError struct{ Cause error }

LeaseLostError signals the worker lost its lease while executing and must not record a failure for the task.

func (*LeaseLostError) Error

func (e *LeaseLostError) Error() string

func (*LeaseLostError) Unwrap

func (e *LeaseLostError) Unwrap() error

type PermanentError

type PermanentError struct{ Cause error }

PermanentError marks a failure that retrying can never fix: the identical input will fail identically (bad payload, missing repo, dirty tree, missing autonomy). Workers must dead-letter the task after the first attempt instead of burning the retry budget — for agent tasks every retry is real money.

Detect it with errors.AsType / errors.Is; executors should return it via Permanent so wrapping is uniform.

func (*PermanentError) Error

func (e *PermanentError) Error() string

func (*PermanentError) Unwrap

func (e *PermanentError) Unwrap() error

type PreflightError

type PreflightError struct{ Cause error }

PreflightError signals the executor refused to START a task: the environment is not ready (dirty repo waiting on a human commit, autonomy config missing, verify tool absent) but neither the task nor the payload is wrong. Workers must requeue the task WITHOUT burning an attempt — it becomes claimable again after a delay, so the pool picks it up as soon as the environment is fixed.

func (*PreflightError) Error

func (e *PreflightError) Error() string

func (*PreflightError) Unwrap

func (e *PreflightError) Unwrap() error

type PrioritizeExecutor added in v0.3.0

type PrioritizeExecutor struct {
	// Agent supplies the run mechanics and its configuration. nil selects
	// a default AgentExecutor.
	Agent *AgentExecutor
}

PrioritizeExecutor runs batch scorer agents. Like ReviewExecutor it reuses the agent run mechanics and is closeout-free (the scorer IS a short-lived utility turn, not paid work needing a self-review).

func (*PrioritizeExecutor) Execute added in v0.3.0

func (e *PrioritizeExecutor) Execute(ctx context.Context, t task.Task) error

Execute runs the scorer and enforces the verdict contract: every input item must come back exactly once with a 0-100 score; anything else is a failed attempt (retryable — the model may comply on a retry), input misses are permanent, dirty trees are preflight requeues.

type PrioritizeItem added in v0.3.0

type PrioritizeItem struct {
	Key     string `json:"key"`
	Text    string `json:"text"`
	Heading string `json:"heading,omitempty"`
}

PrioritizeItem is one item the scorer must verdict, with the dedup key the verdict will be cached under (the sweeper derives it with the same harvest.ItemKey derivation the queue dedup uses).

type PrioritizePayload added in v0.3.0

type PrioritizePayload struct {
	// Repo is the repository whose TODO_LIST the items come from (same
	// resolution rules as AgentPayload.Repo). The scorer reads it for
	// context (AGENTS.md, code layout) but changes nothing. Required.
	Repo string `json:"repo"`
	// RepoName is the display name of the repo.
	RepoName string `json:"repo_name,omitempty"`
	// Items is the batch to score. Required, non-empty.
	Items []PrioritizeItem `json:"items"`
	// Model optionally overrides the crush model; empty reuses the repo's
	// .crushrc (the default and recommended setting — model+effort live
	// ONLY there, ADR/pool contract).
	Model string `json:"model,omitempty"`
	// Yolo marks the scorer as autonomous (same contract as
	// AgentPayload.Yolo).
	Yolo bool `json:"yolo,omitempty"`
	// RequireClean refuses to start on a dirty tree. Default true — the
	// scorer reads the repo for context and should see committed state.
	RequireClean *bool `json:"require_clean,omitempty"`
	// TimeoutMinutes caps the run. Default 10 (scoring reads; it does not
	// build).
	TimeoutMinutes int `json:"timeout_minutes,omitempty"`
}

PrioritizePayload is the payload contract for "prioritize" tasks: self-contained like ReviewPayload — the batch, the repo for context, and the autonomy/timeout knobs.

type PrioritizeResult added in v0.3.0

type PrioritizeResult struct {
	Verdicts []PrioritizeVerdict `json:"verdicts"`
	// contains filtered or unexported fields
}

PrioritizeResult is the structured outcome stored on the completion fact detail, same sink convention as ReviewResult.

func ParsePrioritizeResult added in v0.3.0

func ParsePrioritizeResult(output string, items []PrioritizeItem) (PrioritizeResult, error)

ParsePrioritizeResult extracts and validates the verdict batch: the TQ_RESULT line must parse, cover every input item exactly once, and carry in-range scores. Lenient on optional fields.

type PrioritizeVerdict added in v0.3.0

type PrioritizeVerdict struct {
	ItemKey       string `json:"item_key"`
	Score         int    `json:"score"`
	EffortMinutes int    `json:"effort_minutes,omitempty"`
	Confidence    int    `json:"confidence,omitempty"`
	Reasoning     string `json:"reasoning,omitempty"`
}

PrioritizeVerdict is one item's score.

type QuestionPendingError added in v0.3.1

type QuestionPendingError struct {
	Cause error
	// RetryAfter is how long to wait before the next attempt: until the
	// question expires. Always positive.
	RetryAfter time.Duration
	// ResumeCloseout marks a question asked during the CLOSE-OUT turn: the
	// work turn finished and its session is alive, so the re-claim resumes
	// at closeout instead of re-running the paid work turn.
	ResumeCloseout bool
}

QuestionPendingError signals that the agent parked its task on a question for the owner (PapDashboard questions): the run asked via `tq ask` and ended its turn without finishing the work. This is never the task's fault — the identical retry fails identically UNTIL the owner answers — so workers must requeue WITHOUT burning an attempt, delayed by RetryAfter (the question's expiry: the safety valve that re-enters the task if the answer never comes).

func (*QuestionPendingError) Error added in v0.3.1

func (e *QuestionPendingError) Error() string

func (*QuestionPendingError) Unwrap added in v0.3.1

func (e *QuestionPendingError) Unwrap() error

type RateLimitError added in v0.3.0

type RateLimitError struct {
	Cause error
	// RetryAfter is how long to wait before the next attempt. Always
	// positive; capped by maxRateLimitWait.
	RetryAfter time.Duration
	// Provider is the provider tag parsed from the failing output
	// (crush logs "provider=synthetic"), empty when the output carries
	// none. Observability only: the worker's requeue log surfaces it so
	// operators can see WHICH provider armed the gate (16-00 f35).
	Provider string
	// ResumeCloseout marks a rate-limited CLOSE-OUT turn: the work turn
	// finished and its session is alive, so the re-claim resumes at
	// closeout instead of re-running the paid work turn. The worker
	// mirrors the flag into the requeue fact's evidence (16-00 f31).
	ResumeCloseout bool
}

RateLimitError signals the model provider behind an agent run (Z.ai, synthetic.new, …) refused with an exhaustion response — HTTP 429 with a usage/rate-limit message. This is never the task's fault: the identical retry fails identically UNTIL the provider's limit window resets, and while it lasts every retry is a wasted (possibly billed) process spawn.

Workers must requeue the task WITHOUT burning an attempt, delayed by RetryAfter (the parsed reset time when the provider reports one, a conservative default otherwise).

func (*RateLimitError) Error added in v0.3.0

func (e *RateLimitError) Error() string

func (*RateLimitError) Unwrap added in v0.3.0

func (e *RateLimitError) Unwrap() error

type Registry

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

Registry maps task types to executors.

Example

Registering a custom executor type makes any task whose type matches run your Go code instead of a shell command.

package main

import (
	"context"
	"encoding/json/jsontext"
	"encoding/json/v2"
	"fmt"
	"log"

	"github.com/larsartmann/go-taskqueue/internal/executor"
	"github.com/larsartmann/go-taskqueue/internal/task"
)

func main() {
	reg := executor.NewRegistry()
	reg.RegisterFunc("greet", func(_ context.Context, t task.Task) error {
		var name string
		if err := json.Unmarshal(t.Payload, &name); err != nil {
			name = "stranger"
		}

		fmt.Printf("hello %s\n", name)

		return nil
	})

	ex, err := reg.Lookup("greet")
	if err != nil {
		log.Fatal(err)
	}

	err = ex.Execute(context.Background(), task.Task{
		ID:      task.ID("t1"),
		Type:    "greet",
		Payload: jsontext.Value(`"world"`),
	})
	if err != nil {
		log.Fatal(err)
	}

}
Output:
hello world

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry.

func (*Registry) Lookup

func (r *Registry) Lookup(taskType string) (Executor, error)

Lookup returns the executor for a task type.

func (*Registry) Register

func (r *Registry) Register(taskType string, e Executor)

Register binds a task type to an executor.

func (*Registry) RegisterFunc

func (r *Registry) RegisterFunc(taskType string, f Func)

RegisterFunc binds a task type to a function.

func (*Registry) Types

func (r *Registry) Types() []string

Types returns the registered task types.

type ReviewExecutor

type ReviewExecutor struct {
	// Agent supplies the run mechanics and its configuration (Bin,
	// ProjectsDir, Yolo). nil selects a default AgentExecutor.
	Agent *AgentExecutor
}

ReviewExecutor runs reviewer agents. It reuses the agent run mechanics (repo resolution, autonomy preflight, clean-tree guard, process-group handling) through its Agent executor; the only differences are the prompt and the strict output contract.

func (*ReviewExecutor) Execute

func (e *ReviewExecutor) Execute(ctx context.Context, t task.Task) error

Execute runs the reviewer and enforces the verdict contract. Approve and request_changes both succeed; malformed output fails the attempt (retryable — the model may comply on a retry), input-contract misses are permanent, dirty trees are preflight requeues.

type ReviewFinding

type ReviewFinding struct {
	// Title is the one-line summary; it keys the dedup of auto-minted fix
	// tasks, so keep findings stable in wording across re-reviews.
	Title string `json:"title"`
	// Severity is a display hint normalized to low/medium/high at parse
	// time (unknown or empty ratings degrade to medium — severity never
	// gates anything mechanically, so leniency here is safe).
	Severity string `json:"severity,omitempty"`
	// Detail is the free-form explanation and suggested direction.
	Detail string `json:"detail,omitempty"`
	// CommitSHA is the commit the finding is anchored to (what the reviewer
	// inspected). Empty in model output is backfilled from the review
	// payload's CommitSHA at parse time — every request_changes finding
	// ends up anchored to a commit even when the model omits it.
	CommitSHA string `json:"commit_sha,omitempty"`
	// Anchor is the short VERBATIM text the finding is attached to (a line
	// or hunk of the cited file AS IT READS at the anchored commit). A bare
	// line-range citation ("lines 12-18", "x.go:3") is rejected at parse
	// time: line numbers go stale on the first rebase, and a fix agent that
	// must re-derive the location is re-doing the review (the 2026-09-10
	// 05-58 stale-anchor incident cost a rework day).
	Anchor string `json:"anchor,omitempty"`
}

ReviewFinding is one concrete problem the reviewer wants addressed.

type ReviewPayload

type ReviewPayload struct {
	// Repo is the repository the change landed in (same resolution rules as
	// AgentPayload.Repo). Required.
	Repo string `json:"repo"`
	// ReviewedTask is the queue task ID under review — lineage for `tq show`
	// and the sweeper's loop guard. Required.
	ReviewedTask string `json:"reviewed_task"`
	// Item is the original instruction the reviewed agent was given: the bar
	// the change is judged against. Required.
	Item string `json:"item"`
	// CommitSHA is the commit the reviewed agent reported (from its
	// TQ_RESULT self-report). Empty when the agent landed no commit; the
	// reviewer then inspects the repo's latest state instead.
	CommitSHA string `json:"commit_sha,omitempty"`
	// FilesChanged lists the files the reviewed agent reported touching.
	FilesChanged []string `json:"files_changed,omitempty"`
	// Extra carries operator focus instructions appended to the prompt.
	Extra string `json:"extra,omitempty"`
	// Model optionally overrides the crush model for the reviewer.
	Model string `json:"model,omitempty"`
	// Yolo marks the reviewer as autonomous (same contract as
	// AgentPayload.Yolo: needs a repo-local crush config granting
	// permissions). Pools typically mirror the reviewed task's autonomy.
	Yolo bool `json:"yolo,omitempty"`
	// RequireClean refuses to start unless the repo tree is clean — a
	// reviewer must judge the committed state, not dirty work-in-progress.
	// Default true. Same escape hatch as AgentPayload.
	RequireClean *bool `json:"require_clean,omitempty"`
	// TimeoutMinutes caps the review run. Default 15 (reviews read; they do
	// not rebuild the world).
	TimeoutMinutes int `json:"timeout_minutes,omitempty"`
}

ReviewPayload is the payload contract for "review" tasks. It is self-contained: everything the reviewer needs to judge the change travels in the payload, so the executor stays a pure process runner.

type ReviewResult

type ReviewResult struct {
	Verdict  ReviewVerdict   `json:"verdict"`
	Summary  string          `json:"summary,omitempty"`
	Findings []ReviewFinding `json:"findings,omitempty"`
	// contains filtered or unexported fields
}

ReviewResult is the structured outcome of one review run, stored in the completion fact detail (the sink convention) for the sweeper and `tq show`.

func ParseResult

func ParseResult(output string) (ReviewResult, error)

ParseResult extracts and validates the verdict JSON from reviewer output. Strict where it matters, lenient where nothing depends on it: the verdict must be exactly approve or request_changes (case-insensitive) and the JSON must parse; findings with empty titles are dropped and severity ratings are normalized to low/medium/high.

type ReviewVerdict

type ReviewVerdict string

ReviewVerdict is the reviewer's decision. Only these two values are accepted; anything else is invalid output and a failed attempt.

const (
	// VerdictApprove means the change is correct and complete as it stands.
	VerdictApprove ReviewVerdict = "approve"
	// VerdictRequestChanges means the reviewer found concrete, actionable
	// problems; each finding is a candidate fix task.
	VerdictRequestChanges ReviewVerdict = "request_changes"
)

type Sink

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

Sink collects structured outcome detail for ONE task execution.

func NewSink

func NewSink(ctx context.Context) (context.Context, *Sink)

NewSink returns a context carrying the sink and the sink itself.

func (*Sink) Detail

func (s *Sink) Detail() jsontext.Value

Detail returns the recorded outcome detail, or nil.

func (*Sink) Failure

func (s *Sink) Failure() jsontext.Value

Failure returns the recorded failure evidence, or nil.

type StatusCompletion

type StatusCompletion struct {
	TaskID      string   `json:"task_id"`
	Item        string   `json:"item"`
	Commit      string   `json:"commit,omitempty"`
	Files       []string `json:"files,omitempty"`
	CompletedAt string   `json:"completed_at,omitempty"`
	// Report is the repo-relative path of the task's closeout report
	// (docs/status/tasks/<ts>_task-<id>.md; legacy root paths resolvable
	// too), resolved by the sweeper when it could find one — the done
	// prompt reads it directly instead of re-deriving it from the task
	// id. Empty when none was found.
	Report string `json:"report,omitempty"`
}

StatusCompletion is one completed agent task in the window a status report covers. The item is an excerpt of the agent's instruction (first line, bounded) — the full prompt stays in `tq show`.

type StatusExecutor

type StatusExecutor struct {
	// Agent supplies the run mechanics and its configuration. nil selects a
	// default AgentExecutor.
	Agent *AgentExecutor
}

StatusExecutor runs the done-prompt agent. It reuses the agent run mechanics through its Agent executor (same pattern as ReviewExecutor); the differences are the prompt and the output contract: the final TQ_RESULT line must name an existing, repo-relative report file.

func (*StatusExecutor) Execute

func (e *StatusExecutor) Execute(ctx context.Context, t task.Task) error

Execute runs the reporting agent and enforces two gates: the output must end with TQ_RESULT: {"report":"docs/status/...","next_items":N} naming an existing, repo-relative file, and the repo verify command must exit 0 (the reporter commits, so it can break the tree it reports on). Malformed output, a missing file or a failed verify is a retryable failure; payload misses are permanent; dirty trees are preflight requeues.

type StatusPayload

type StatusPayload struct {
	// Repo is the repository being reported on (same resolution rules as
	// AgentPayload.Repo). Required.
	Repo string `json:"repo"`
	// Project is the queue project (repo name) the window belongs to.
	// Required; also the sweeper's loop guard identity.
	Project string `json:"project"`
	// Completed lists the agent tasks in this report's window, oldest
	// first. Bounded by the sweeper; never empty for a minted task.
	Completed []StatusCompletion `json:"completed"`
	// Model optionally overrides the crush model for the reporting agent.
	Model string `json:"model,omitempty"`
	// Yolo marks the reporting agent as autonomous (same contract as
	// AgentPayload.Yolo). Pools typically mirror the window's autonomy.
	Yolo bool `json:"yolo,omitempty"`
	// RequireClean refuses to start unless the repo tree is clean — the
	// reporter must not trample work-in-progress, and commits its own
	// report afterwards. Default true. Same escape hatch as AgentPayload.
	RequireClean *bool `json:"require_clean,omitempty"`
	// TimeoutMinutes caps the report run. Default 15.
	TimeoutMinutes int `json:"timeout_minutes,omitempty"`
	// Verify is the repo quality gate that must exit 0 after the report
	// run (the reporter commits, so a broken tree is a real possibility).
	// Empty means the usual resolution: the repo's .tq-verify file, then
	// auto-detection — identical to AgentPayload.Verify.
	Verify string `json:"verify,omitempty"`
}

StatusPayload is the payload contract for "status" tasks. Self-contained, like ReviewPayload: everything the reporting agent needs travels in the payload, so the executor stays a pure process runner.

type StatusResult

type StatusResult struct {

	// Report is the repo-relative path of the written status report.
	Report string `json:"report"`
	// NextItems counts the next-work items the agent appended to
	// TODO_LIST.md (its own self-report, not re-verified).
	NextItems int `json:"next_items"`
	// contains filtered or unexported fields
}

StatusResult is the structured outcome of one status run, stored in the completion fact detail (the sink convention) for `tq show` and the web UI.

type VerifyGateClass added in v0.3.1

type VerifyGateClass string

VerifyGateClass says WHY a verify gate failed without judging the task:

  • VerifyGateDead: the gate fails at the PRE-attempt rev too, so the failure is pre-existing or environmental (boot-fragile host precondition, broken baseline) — retrying the task changes nothing.
  • VerifyGateSlow: the deadline killed the gate before a verdict (context deadline exceeded, no gate output judgement) — a retry on a warm cache may simply pass.
  • VerifyGateEnvironmental: the failure matches a BASELINED environmental signature (the vendor-gofmt class: every package stage passed and the gofmt stage flags only gitignored vendor/ files). The identical retry dies identically and each re-dispatch re-does finished work, so the worker DEAD-LETTERS immediately instead of riding the requeue ladder.

The first two classes requeue WITHOUT burning an attempt (the worker treats them like a PreflightError); the environmental class dead-letters on the first death with the VerifyGateEnvCode reason. Only an introduced failure — the gate passing at the pre-attempt rev and failing after the agent's work — counts against the task.

const (
	VerifyGateDead VerifyGateClass = "gate-dead"
	VerifyGateSlow VerifyGateClass = "gate-slow"

	// VerifyGateEnvironmental marks a proven-environmental verify death
	// (the row-135 vendor-gofmt class). See vendorGofmtSignature.
	VerifyGateEnvironmental VerifyGateClass = "gate-env"
)

type VerifyGateError added in v0.3.1

type VerifyGateError struct {
	Class  VerifyGateClass
	Verify string
	Cause  error
	Tail   string
}

VerifyGateError carries a gate-level (not task-level) verify failure.

func (*VerifyGateError) Error added in v0.3.1

func (e *VerifyGateError) Error() string

func (*VerifyGateError) Unwrap added in v0.3.1

func (e *VerifyGateError) Unwrap() error

Jump to

Keyboard shortcuts

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