Documentation
¶
Overview ¶
Package task is memcode's autonomous Task: a reusable, declarative, auditable unit of work the agent can execute with no human present.
A Task is NOT inherently recurring. It is a runnable object first — `memcode task run <name>` is a first-class entry point, not a testing affordance — and a trigger is an optional attachment. That ordering is the whole point: cron is one way a Task becomes eligible, never what a Task is.
Three objects stay deliberately separate:
Task what to do, how, with what authority (YAML, hashable) Trigger when it becomes eligible (0..n, optional) Run one immutable execution (a DB row, package taskrun)
Every run records the Revision of the definition that produced it, so "why did this task push that branch?" stays answerable after the YAML has been edited five times. The autonomy package hashes delegation policies for the same reason and by the same method (see autonomy.CanonicalPolicy).
Index ¶
- Constants
- Variables
- func GlobalDir() (string, error)
- func Marshal(t Task) ([]byte, error)
- func ProjectDir(root string) string
- func Save(root string, t Task, scope Scope) (string, error)
- func ValidLevel(l Level) bool
- func ValidName(s string) bool
- type Autonomy
- type Capability
- type Concurrency
- type ConcurrencyPolicy
- type Coordination
- type Delivery
- type Execution
- type Git
- type Grant
- type Level
- type Limits
- type Missed
- type Notify
- type OnPush
- type Ownership
- type PRMode
- type Runtime
- type Scope
- type Task
- func (t *Task) ApplyDefaults()
- func (t Task) Capability() (Capability, error)
- func (t Task) CrossProject() bool
- func (t Task) Grants() ([]Grant, error)
- func (t Task) HasGrant(g Grant) bool
- func (t Task) IsEnabled() bool
- func (t Task) Manual() bool
- func (t Task) MayMutate() bool
- func (t Task) MayOpenPR() bool
- func (t Task) ReadOnly() bool
- func (t Task) ResolveProject(root string) (string, error)
- func (t Task) Revision() (string, error)
- func (t Task) Targets() []string
- func (t Task) TargetsFrom(anchor string) []string
- func (t Task) Timeout() time.Duration
- func (t Task) UsesWorktree() bool
- func (t Task) Validate(now time.Time) error
- type Trigger
- type Verify
- type Webhook
Constants ¶
const ( DefaultMaxCatchUp = 10 HardMaxCatchUp = 100 )
DefaultMaxCatchUp bounds catch_up when a trigger does not say. HardMaxCatchUp bounds what a trigger may ASK for: a YAML value is a ceiling request, not an override, and nothing should be able to schedule a thousand-run stampede.
const ConcurrencyKeyProject = "project"
ConcurrencyKeyProject is the default key: one mutating run per repo.
const DefaultBranchPattern = "auto/{name}-{date}"
DefaultBranchPattern names the branch an autonomous run pushes. {name} and {date} are substituted; the auto/ prefix makes provenance obvious in a branch list.
const DefaultTimeout = "45m"
DefaultTimeout bounds a run that does not set its own.
const DirName = "tasks"
DirName is the tasks subdirectory inside either root.
const ModelAuto = "auto"
ModelAuto means the task expressed no model preference.
const Version = 1
Version is the schema version every task file must declare. An explicit version on a user-editable file is what lets the format change later without guessing at the shape of what is on disk.
Variables ¶
var ErrNotFound = errors.New("task not found")
ErrNotFound is returned by Get when no task carries that name.
Functions ¶
func Marshal ¶
Marshal renders a task back to YAML, defaults applied. Used when memcode writes a task it proposed, so the file on disk is the complete definition rather than a sparse one whose behaviour shifts if a default ever changes.
func ProjectDir ¶
ProjectDir returns the repo-local task directory.
Types ¶
type Autonomy ¶
type Autonomy struct {
Level Level `yaml:"level,omitempty" json:"level,omitempty"`
Grants []Grant `yaml:"grants,omitempty" json:"grants,omitempty"`
}
Autonomy is the task's authority CEILING. Level is a friendly preset that expands to concrete grants; Grants adds named capabilities on top. The policy engine only ever reasons about grants.
A ceiling is not an override. Nothing written here can lift a task above memcode's hard floor — permissions.Decide still returns NeedPrompt for a catastrophic command in every mode, and an unknown grant is refused at parse time rather than interpreted generously.
type Capability ¶
type Capability struct {
// DenyTools are tool names or toolset names the child must not have.
DenyTools []string
// DenyCommands are shell command patterns the child may not run, matched
// against the AST (see permissions.DeniedBy) so wrappers cannot smuggle one.
DenyCommands []string
// ProtectProject means the run must not execute in the user's checkout.
// The runner satisfies it with a throwaway worktree, which is what makes
// "read-only" mean "cannot change YOUR project" rather than the impractical
// "performs no writes anywhere" — builds and tests write caches and temp
// files constantly, and forbidding that would forbid the work.
ProtectProject bool
// Mutating reports whether this run may change project state at all. Drives
// worktree retention and the concurrency lease.
Mutating bool
}
Capability is the projection of a task's grants onto what its run receives.
type Concurrency ¶
type Concurrency struct {
Key string `yaml:"key,omitempty" json:"key,omitempty"`
Policy ConcurrencyPolicy `yaml:"policy,omitempty" json:"policy,omitempty"`
}
Concurrency serializes runs that share a key. Key "project" (the default) resolves to the task's project root, so every mutating task in a repo takes the same lock while read-only tasks run freely.
type ConcurrencyPolicy ¶
type ConcurrencyPolicy string
ConcurrencyPolicy decides what happens when a task is due while an overlapping run is still going.
const ( // ConcurrencyQueue waits for the in-flight run. The default: two tasks // rewriting the same repo at once is a merge conflict with extra steps. ConcurrencyQueue ConcurrencyPolicy = "queue" // ConcurrencySkip drops this occurrence. ConcurrencySkip ConcurrencyPolicy = "skip" )
type Coordination ¶
type Coordination string
Coordination is the publication semantics of a multi-project run.
const ( // CoordIndependent publishes each project's work on its own merits. Right // when the projects merely share a chore ("keep dependencies current"): // repo B being stuck is no reason to withhold repo A's upgrade. CoordIndependent Coordination = "independent" // CoordCoordinated publishes all of it or none of it. Right when the // changes only make sense together, which is the whole reason the task is // cross-project rather than two tasks. A partial success is not a success; // it is a half-applied change to a system that was consistent before. CoordCoordinated Coordination = "coordinated" )
type Delivery ¶
type Delivery struct {
Desktop Notify `yaml:"desktop,omitempty" json:"desktop,omitempty"`
// Channel reuses the gateway's existing "channel:conversation" address when
// the user has one paired. Empty means inbox only.
Channel string `yaml:"channel,omitempty" json:"channel,omitempty"`
ChannelOn Notify `yaml:"channel_on,omitempty" json:"channel_on,omitempty"`
}
Delivery is where results go. The inbox is the durable ledger and is always on; everything else is notification layered over it.
type Execution ¶
type Execution struct {
// Steps are the mechanical part of the job, worked out when the task was
// SET UP rather than learned from it later. The design phase already runs
// the work once to validate it, and that run is where it becomes obvious
// which commands are deterministic — `go get -u ./...`, fetch, normalise,
// build — and which needed someone to think.
//
// A run does the mechanical part directly and pays no model for it. If the
// steps do everything the goal needed and nothing changed, the run is over
// and no agent starts at all: the common Monday costs nothing.
//
// They are an OPTIMISATION and never the definition. A step that fails is
// drift, not a verdict — the agent takes over and works out the current
// state. Nothing here can make a run succeed; only verification does that.
Steps []string `yaml:"steps,omitempty" json:"steps,omitempty"`
// KnownGood records an approach that WORKED once, with the date it worked.
// It is a hint and never the definition: a repository drifts, and a run
// that treats last quarter's steps as authoritative will confidently do the
// wrong thing. A run starts here and checks whether the assumptions still
// hold before relying on any of it.
KnownGood string `yaml:"known_good,omitempty" json:"known_good,omitempty"`
}
Execution selects the work strategy. Procedure names an entry in the repo's procedure store; the concept is deliberately not "shell script", because a procedure may later be an HTTP call, a tool sequence or a Go helper.
type Git ¶
type Git struct {
Worktree *bool `yaml:"worktree,omitempty" json:"worktree,omitempty"`
PullRequest PRMode `yaml:"pull_request,omitempty" json:"pull_request,omitempty"`
Branch string `yaml:"branch,omitempty" json:"branch,omitempty"`
// Remote is where a branch is published. Default origin.
Remote string `yaml:"remote,omitempty" json:"remote,omitempty"`
}
Git controls how code changes leave an unattended run. Worktree isolation is the default because an autonomous run must never disturb the branch a human is sitting on.
type Grant ¶
type Grant string
Grant is one named capability an autonomous run may exercise.
const ( // Read-only capabilities. GrantFilesystemRead Grant = "filesystem.read" GrantProcessExecReadOnly Grant = "process.execute_readonly" // Local mutation. GrantFilesystemMutate Grant = "filesystem.local_mutation" GrantProcessExec Grant = "process.execute" // Preparing a change for humans. GrantGitCreateBranch Grant = "git.create_branch" GrantGitCommit Grant = "git.commit" GrantGitPushBranch Grant = "git.push_branch" GrantGitHubOpenPR Grant = "github.open_pr" )
func ExpandGrants ¶
ExpandGrants resolves a task's authority to the concrete set the policy engine enforces: the level's preset plus any explicitly named additions, deduplicated and sorted. An unknown grant or level is an error — a typo must never silently widen or narrow authority.
func KnownGrants ¶
func KnownGrants() []Grant
KnownGrants lists every grant a task file may name, sorted.
type Level ¶
type Level string
Level is a preset bundle of grants.
const ( // LevelReadOnly inspects, runs read-only commands, and reports. It changes // nothing anywhere, including in a worktree. LevelReadOnly Level = "read_only" // LevelBranch is the default. It may edit an isolated worktree, run // commands, commit, push a NEW branch, and open a pull request — the full // shape of "prepare a change for review" and nothing beyond it. // // Opening a PR belongs here rather than in some higher tier. It does mutate // remote state, but a PR is a review artifact: it is precisely the act of // asking a human to decide. The invariant this whole feature is built // around is that autonomous work may PREPARE a change, never make it // authoritative. LevelBranch Level = "branch" )
type Limits ¶
type Limits struct {
Timeout string `yaml:"timeout,omitempty" json:"timeout,omitempty"`
MaxCostUSD float64 `yaml:"max_cost_usd,omitempty" json:"max_cost_usd,omitempty"`
}
Limits bound a single run. An unattended task that can spin forever is a bill, not a feature.
type Missed ¶
type Missed string
Missed is the catch-up policy for a trigger the daemon slept through. This matters more than it looks on a laptop: without it, "0 2 * * *" silently means "only if the machine happened to be awake at 2am", and a weekly job can go months without running while appearing healthy.
const ( // MissedRunOnce runs the task once on the next daemon start, then resumes // the normal cadence. The default, and the right answer for maintenance. MissedRunOnce Missed = "run_once" // MissedSkip forgets the occurrence entirely (the old schedule behaviour). MissedSkip Missed = "skip" // MissedCatchUp replays missed occurrences, up to Trigger.MaxCatchUp. Rarely // what anyone wants. MissedCatchUp Missed = "catch_up" )
type OnPush ¶
type OnPush struct {
Branch string `yaml:"branch,omitempty" json:"branch,omitempty"`
}
OnPush and Webhook are reserved trigger kinds, rejected by Validate today.
type Ownership ¶
type Ownership struct {
// Projects are the checkouts this responsibility spans. Empty means the
// task's own Project and nothing else, which is the common case.
Projects []string `yaml:"projects,omitempty" json:"projects,omitempty"`
// Coordination decides what happens when the work succeeds in some
// projects and not others. This is a real product decision, not a detail:
// "PR opened in repo A" while repo B could not be updated is a WORSE
// outcome than doing nothing, if the two changes only make sense together.
Coordination Coordination `yaml:"coordination,omitempty" json:"coordination,omitempty"`
// Responsibility states what this task is responsible FOR, independently of
// today's file layout. It is what a run judges its own share against when
// the repository has moved under it.
//
// It is NOT a licence to go looking for more projects. The approved project
// set is fixed at creation and changes only when the user changes the
// automation. Drift is allowed WITHIN an approved boundary — paths, build
// commands, package managers, moved code, refactors, whatever the run finds
// there. Expanding the boundary is not drift; it is new authority, and
// authority comes from a person.
//
// So if the responsibility grows into a repository nobody approved, the run
// stops and says so. Discovery is not authorization, the same way a login
// found in another tool's files is not consent to use it.
Responsibility string `yaml:"responsibility,omitempty" json:"responsibility,omitempty"`
}
Ownership answers "whose responsibility is this?", which is not the same question as "where was the conversation happening?".
A task created while someone happened to be sitting in one checkout can easily belong to something larger. memcode itself is the example: the model catalog is one responsibility implemented in two repositories, and a task that updated only whichever one was open would be quietly wrong forever.
So a task names the projects its responsibility spans, and how their work relates. Project is still where the task is ANCHORED — the repo it was created against, and the one whose worktree a single-project run uses. Projects widens that to the full set when the responsibility is genuinely cross-cutting.
type PRMode ¶
type PRMode string
PRMode decides whether a task that changed code opens a pull request.
const ( PRNever PRMode = "never" // PRWhenChanges opens a pull request when the run produced a commit. PRWhenChanges PRMode = "when_changes" // PRAlways means always FOR A PRODUCED COMMIT — never "manufacture an empty // one to satisfy the configuration". A run with no diff opens nothing under // either setting; the difference between them is reserved for future // conditions on an actual change, not for inventing artifacts. PRAlways PRMode = "always" )
type Runtime ¶
type Runtime struct {
Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"`
Allowed []string `yaml:"allowed,omitempty" json:"allowed,omitempty"`
Model string `yaml:"model,omitempty" json:"model,omitempty"`
Fallback []string `yaml:"fallback,omitempty" json:"fallback,omitempty"`
}
Runtime is where the task's inference runs. It is four separate decisions rather than one string, because "use my Claude subscription" quietly bundles availability, permission, capability and what-to-do-when-it-breaks, and those have different answers and different owners.
Strategy how to choose Allowed the user's ordered preference — order is obeyed, not optimized Model a pinned catalog model, or auto Fallback what may be tried when the RUNTIME fails (not when the task does)
A runtime being present on the machine authorizes nothing. See internal/runtimes.
type Scope ¶
type Scope string
Scope says where a definition was loaded from. Project scope wins over global on a name collision: a repo that ships its own task means it for that repo.
type Task ¶
type Task struct {
Version int `yaml:"version" json:"version"`
Name string `yaml:"name" json:"name"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
Project string `yaml:"project,omitempty" json:"project,omitempty"`
Ownership Ownership `yaml:"ownership,omitempty" json:"ownership"`
Triggers []Trigger `yaml:"triggers,omitempty" json:"triggers,omitempty"`
Instructions string `yaml:"instructions" json:"instructions"`
Execution Execution `yaml:"execution,omitempty" json:"execution"`
Runtime Runtime `yaml:"runtime,omitempty" json:"runtime"`
Autonomy Autonomy `yaml:"autonomy,omitempty" json:"autonomy"`
Git Git `yaml:"git,omitempty" json:"git"`
Verify Verify `yaml:"verify,omitempty" json:"verify"`
Delivery Delivery `yaml:"delivery,omitempty" json:"delivery"`
Limits Limits `yaml:"limits,omitempty" json:"limits"`
Concurrency Concurrency `yaml:"concurrency,omitempty" json:"concurrency"`
// Resolved by the loader, never authored and never hashed.
Path string `yaml:"-" json:"-"`
Scope Scope `yaml:"-" json:"-"`
}
Task is one task definition, as authored in YAML plus the resolved facts the loader attaches. Zero-value fields are filled by ApplyDefaults before validation or hashing, so a sparse file and its fully-written equivalent produce the same Revision.
func Load ¶
Load returns every task visible from root, project scope shadowing global on a name collision, sorted by name. Malformed files are returned separately so a caller can surface them without losing the tasks that did load.
func Parse ¶
Parse decodes one task file. Decoding is STRICT — an unknown key is an error rather than a silently ignored line, because a typo'd `autonmy:` that parses cleanly would hand a task the default authority while its author believed otherwise.
func (*Task) ApplyDefaults ¶
func (t *Task) ApplyDefaults()
ApplyDefaults fills every unset field with its documented default. It runs before validation and before hashing, so a sparse file and the fully-written equivalent are the same task with the same Revision.
func (Task) Capability ¶
func (t Task) Capability() (Capability, error)
Project returns the capability projection for a task.
func (Task) CrossProject ¶
CrossProject reports whether this responsibility spans more than one checkout.
func (Task) HasGrant ¶
HasGrant reports whether the task's expanded authority includes g. A resolution error means "no": authority questions fail closed.
func (Task) IsEnabled ¶
IsEnabled reports whether the task may run. Absent means enabled — a file someone wrote is meant to work.
func (Task) Manual ¶
Manual reports whether the task only ever runs by hand. A task with no triggers is manual — that is a normal, complete task, not an unfinished one.
func (Task) MayOpenPR ¶
MayOpenPR reports whether the task can push a branch AND open a pull request. Both are required: a PR with nothing pushed behind it is not a thing.
func (Task) ResolveProject ¶
ResolveProject returns the absolute, symlink-resolved directory a task runs in: its own project field when set, else the root it was loaded from.
func (Task) Revision ¶
Revision is the content hash of the normalized definition: sha256 over canonical JSON, sorted where order carries no meaning. Two files that differ only in key order or in omitted-but-defaulted fields hash identically; any semantic edit changes the hash.
Every run records this. It is what makes an execution auditable months later, after the YAML has moved on.
func (Task) Targets ¶
Targets is the set of checkouts a run must work through, anchor first.
Always non-empty for a valid task, so callers never special-case the single-project shape.
func (Task) TargetsFrom ¶
TargetsFrom is Targets with the anchor supplied by the caller.
A definition does not always carry its own project: a global task is anchored by the run, and a file authored without a `project:` key is anchored by where it was loaded from. The anchor is a property of the RUN in those cases, and pretending otherwise silently produces an empty target list — which for the lease means no lock at all.
func (Task) UsesWorktree ¶
UsesWorktree reports whether runs get an isolated worktree. Absent means yes.
func (Task) Validate ¶
Validate checks a task after ApplyDefaults. Every failure names the field and says what a working value looks like, because the reader is usually someone hand-editing YAML with no schema in front of them.
now is injectable so one-shot "at" triggers can be validated against a fixed clock in tests.
type Trigger ¶
type Trigger struct {
Cron string `yaml:"cron,omitempty" json:"cron,omitempty"`
Every string `yaml:"every,omitempty" json:"every,omitempty"`
At string `yaml:"at,omitempty" json:"at,omitempty"`
// TZ evaluates Cron in a named zone ("America/Los_Angeles"); empty = local.
TZ string `yaml:"tz,omitempty" json:"tz,omitempty"`
// Missed decides what happens when the daemon was not running at the moment
// this trigger was due. Meaningless for Manual.
Missed Missed `yaml:"missed,omitempty" json:"missed,omitempty"`
// MaxCatchUp bounds how many missed occurrences catch_up will actually run.
// Without a bound, a laptop returning after months with an hourly task would
// enqueue thousands of historical runs at once. Ignored unless Missed is
// catch_up; the excess is dropped newest-first-kept and surfaced on the run.
MaxCatchUp int `yaml:"max_catch_up,omitempty" json:"max_catch_up,omitempty"`
// Manual is an explicit "this trigger only fires by hand". A task with no
// triggers at all is already manual-only; this exists so a file can say so.
Manual bool `yaml:"manual,omitempty" json:"manual,omitempty"`
// Reserved kinds. Declared so an author who writes one gets a clear "not
// implemented yet" instead of an unknown-field parse error, and so the
// schema slot is taken.
OnPush *OnPush `yaml:"on_push,omitempty" json:"on_push,omitempty"`
Webhook *Webhook `yaml:"webhook,omitempty" json:"webhook,omitempty"`
}
Trigger is one way a task becomes eligible to run. Exactly one kind may be set. The list is plural from day one so adding a second kind later is not a schema migration; only the time forms and manual are implemented today.
func (Trigger) Validate ¶
Validate checks one trigger. The time forms are delegated to the gateway's shared spec validation rather than reparsed here: one validator means a cadence that parses on this surface parses on the scheduler, and a second parser is exactly how the previous two schedulers drifted apart.
type Verify ¶
type Verify struct {
Commands []string `yaml:"commands,omitempty" json:"commands,omitempty"`
// Across are checks that run ONCE, after every project has done its share,
// to test that the projects still agree with each other. Per-project checks
// cannot answer that: each side can be internally perfect and the pair
// still inconsistent, which on a cross-cutting change is precisely the
// failure worth catching. They run in the anchor project's working copy
// with MEMCODE_TASK_PROJECTS listing every project's working copy.
Across []string `yaml:"across,omitempty" json:"across,omitempty"`
}
Verify is how a run proves it succeeded. Without it "the agent stopped" gets mistaken for "the task worked", which is how autonomous systems quietly rot.