plandb

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Index

Constants

View Source
const (
	RolePlan  = "plan"
	RoleWork  = "work"
	RoleCheck = "check"
	RoleProbe = "probe"
)

The four words a task's seat may carry. Plan and work are the seats a task's shape gives it — a coordinator with children is plan, a leaf is work unless it declared otherwise — while check and probe are declared at add for the review round and the discriminating unknown. They are the store's spelling of the crew's seats, and the supervisor resolves the word to a model through the crew when it launches the task.

View Source
const (
	NoteFromWorker = "worker"
	NoteFromPerson = "person"
)

A NOTE'S AUTHOR IS ONE OF TWO HANDS: a worker leaving a handoff for the next worker, or the person steering the run. The store records which, and nothing else about the note changes with it.

View Source
const NoteAgentChat = "chat"

NoteAgentChat is the agent name a note carries when the CONVERSATION left it rather than a worker or the person. It is a worker-side note by the column above and deliberately so — the person's voice is the one thing on a run that may move what the work is judged by, and a model writing in it could grant itself permissions nobody gave (internal/session's relayToTask states the same law about the same hazard). The name is a constant here, in the package both the writer and every reader import, so the one hand that is neither the person nor a worker is spelled one way wherever it is drawn.

View Source
const RunEnv = "PLANDB_RUN"

RunEnv names the run a worker belongs to, by its root task's id. The door that seats a run worker exports it beside PLANDB_DB, and a store found at that path whose root is ANOTHER run's is refused rather than written: a path says where a run's store was, and only the root says which run it is.

Variables

View Source
var ErrClosed = errors.New("plan store is closed")

ErrClosed is what every method that can refuse answers once Close has released the store: each write, and each read whose answer the database holds rather than this handle's memory. IT IS THE SEAM A WORKER THAT OUTLIVED ITS RUN LANDS ON — the spend row, the trajectory ending and the completion such a worker still owes come back as a refusal a best-effort writer drops, rather than as a nil handle to dereference.

Functions

func CheckFiles

func CheckFiles(check string) []string

CheckFiles returns the literal file-shaped words of a check command because #1604 showed that treating a future package or shell expression as a missing file can refuse valid work. The command is not evaluated.

func Main

func Main(argv []string) int

Main runs one plandb command line and answers the process exit code. It is the one runner both doors call, and the only place argv is interpreted.

func NamedFiles

func NamedFiles(text string) []string

NamedFiles returns file-shaped words from a task's prose because a title or work order may put backticks and sentence punctuation around a real name. CheckFiles keeps its narrower command parsing for the #1604 compatibility case.

func PlaceholderSiblings

func PlaceholderSiblings(file, dir string, texts []string) []string

PlaceholderSiblings finds the numbered names that make a missing trailing underscore a likely lost number, as #1573's issue_.go did. An existing file is intentional; siblings may be on disk or in a task's prose work order.

func TaskDir

func TaskDir(storeDir, id string) string

TaskDir is the folder one task's record lives in beside the store: the trajectory the run's worker appends its steps to and the transcript and spill files the session seat leaves both land there, so one task's page is one folder a person can open. The store's own path is the one root both roads derive it from — the session seat reads it off the store path it was given, the run off the store it drives — and a second spelling of the layout would be two answers to where a task's record is.

Types

type BlockedCount

type BlockedCount struct {
	Task       *Task `json:"task"`
	Downstream int   `json:"downstream"`
}

BlockedCount is one bottleneck row: the task and how much it holds up.

type BlockedTask

type BlockedTask struct {
	Task    *Task    `json:"task"`
	Reasons []string `json:"reasons"`
}

type ContextEntry

type ContextEntry struct {
	ID        string    `json:"id"`
	TaskID    string    `json:"task_id,omitempty"`
	Kind      string    `json:"kind"`
	Content   string    `json:"content"`
	Project   string    `json:"project,omitempty"`
	Chat      string    `json:"chat,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

type DepKind

type DepKind string
const (
	DepFeedsInto DepKind = "feeds_into"
	DepBlocks    DepKind = "blocks"
	DepSuggests  DepKind = "suggests"
)

type Dependency

type Dependency struct {
	TaskID string  `json:"task_id"`
	Kind   DepKind `json:"kind,omitempty"`
}

type Effect

type Effect string
const (
	EffectObserve         Effect = "observe"
	EffectReversibleWrite Effect = "reversible_write"
	EffectExternalAction  Effect = "external_action"
	EffectIrreversible    Effect = "irreversible"
	EffectMixed           Effect = "mixed"
)

type Filter

type Filter struct {
	Project string
	Chat    string
}

Filter narrows a reading verb to the rows carrying one project tag and one chat tag. THE ZERO VALUE FILTERS NOTHING: an empty field is a wildcard, so a caller that names neither gets every row — the answer the store gave before the tags existed.

type LiveStep

type LiveStep struct {
	Step    int
	Command string
	Since   time.Time
}

LiveStep is the step a task has in flight: the number the worker gave it (counted from one, the same number the trajectory will record), the command it asked the belt to run, and the moment the command started. The zero value is the honest answer for a task that is running nothing, which is why a reader draws nothing rather than a zero step.

func (LiveStep) Empty

func (l LiveStep) Empty() bool

Empty answers whether there is no live step at all — the zero value, and the one question a surface asks before it draws a line. It is a method so the emptiness law has one spelling: a live step with no number is no live step, whatever a zero Command and Since might tempt a reader to draw.

type Note

type Note struct {
	ID     string `json:"id"`
	TaskID string `json:"task_id"`
	Agent  string `json:"agent,omitempty"`
	// From is who left the note: a worker's own handoff, or the person
	// steering the run. It defaults to worker, so every note written before
	// the column — and every one a worker leaves — reads as a worker's.
	From    string    `json:"from,omitempty"`
	Body    string    `json:"body"`
	Project string    `json:"project,omitempty"`
	Chat    string    `json:"chat,omitempty"`
	At      time.Time `json:"at"`
}

Note is a task-scoped message one worker leaves for the others working around the same task — the CLI's `task note`/`task notes`, which the earlier port did not carry. Context (below) is the project-wide cousin; the two stay separate because a note is about one task and a context entry is about the run.

type ReadySet

type ReadySet struct {
	Runnable []*Task       `json:"runnable"`
	Blocked  []BlockedTask `json:"blocked,omitempty"`
}

type ResourceClaim

type ResourceClaim struct {
	URI  string `json:"uri"`
	Mode string `json:"mode"`
}

type RootPreview

type RootPreview struct {
	Title       string
	Description string
	CreatedAt   time.Time
}

RootPreview is the original request behind a saved run. Listing a conversation needs these words, not a replay or a writable handle to the task graph.

func ReadRootPreview

func ReadRootPreview(path string) (RootPreview, error)

ReadRootPreview reads only the committed root of an existing store. It never creates a database, migrates its schema or settles a task left running by a process that exited, so a conversation picker can safely read live work too.

type SearchResult

type SearchResult struct {
	Kind   string `json:"kind"`
	ID     string `json:"id"`
	TaskID string `json:"task_id,omitempty"`
	Title  string `json:"title,omitempty"`
	Detail string `json:"detail"`
	Score  int    `json:"-"`
}

SearchResult is one ranked answer: what kind of thing it is, where it lives, the line that matched, and the score that ranked it.

type SpendLine

type SpendLine struct {
	Key   string  `json:"key"`
	Model string  `json:"model,omitempty"`
	Role  string  `json:"role,omitempty"`
	USD   float64 `json:"usd"`
	In    int     `json:"in"`
	Out   int     `json:"out"`
	Calls int     `json:"calls"`
}

SpendLine is one row of a spend rollup: the key the ledger was grouped under — a chat, a project, a seat, a model, or the task a charge was made against — and the dollars, tokens and calls the rows under that key carry.

MODEL AND ROLE MIRROR THE KEY ON THE MODEL AND SEAT AXES, so a reader asking for a model's or a seat's spend can read the word by name; on the entity axes — chat, project, task — a group spans several models and seats, so both stay empty rather than naming one of many.

type SpendSummary

type SpendSummary struct {
	ByRole  map[string]SpendTotal `json:"by_role,omitempty"`
	ByModel map[string]SpendTotal `json:"by_model,omitempty"`
}

SpendSummary is the ledger read back by the two groupings the seats care about — what each role spent and what each model spent — with the dollars and the calls under each. It is the reading the crew's per-role and per-model fronts are fed from, so a seat's cost is a row from every run and not from a bench cell alone.

type SpendTotal

type SpendTotal struct {
	USD   float64 `json:"usd"`
	Calls int     `json:"calls"`
}

SpendTotal is one project's or one chat's share of the ledger: the dollars spent under that tag and the number of calls that spent them.

type Status

type Status string
const (
	StatusPending   Status = "pending"
	StatusReady     Status = "ready"
	StatusClaimed   Status = "claimed"
	StatusRunning   Status = "running"
	StatusDone      Status = "done"
	StatusFailed    Status = "failed"
	StatusCancelled Status = "cancelled"
)

type Store

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

Store is the plan: one SQLite database, a mutex for this process, and a transaction per write that every other process serializes on. Its method set is the earlier port's, kept because it already answers the CLI's questions: the graph laws (validateGraphs below) are the port's own and were correct there.

WHAT THE ADAPTATION TOOK OUT, deliberately, is written at the functions that changed: the earlier store doubled as a governance gate — validateSpec required a role, deliverables and acceptance on every task, and Claim refused a task whose effect was unresolved or that claimed no resources. The CLI has no flags for any of that, so every `plandb add` the doctrine teaches would have been refused by the port's own gates. The gates are gone; the graph laws stay.

func Open

func Open(path, project, rootID, rootTitle, rootDescription string, chat ...string) (*Store, error)

Open loads the plan at path, or creates one when the database does not exist.

THE ROOT IS THE RUN: `plandb init` makes a project and the runtime runs it by seeding a root task for the work it was given. A store that exists but belongs to a different run is a refusal, not a merge: two sessions sharing one store by accident would each dispatch the other's children.

The optional chat names the conversation the run was seeded in; it tags the root task, and every task, note and context entry made under the root inherits it. The parameter is optional so every call site that names no chat — a worker's own reading open, a reopen — keeps its argument list.

func (*Store) AddContext

func (s *Store) AddContext(taskID, kind, content string) (ContextEntry, error)

AddContext records a run-wide fact. Kinds are freeform — the doctrine says `--kind decision` and the store takes the word at face value.

func (*Store) AddDep

func (s *Store) AddDep(downstream, upstream string, kind DepKind) (*Task, error)

AddDep adds one edge between two tasks. It is the CLI's `task add-dep`, and the graph laws are asked of the whole result: a hard edge between a task and its own ancestor or descendant, or an edge that closes a cycle, refuses the edge rather than corrupting the plan. A hard edge between two branches of the containment tree is allowed.

func (*Store) AddMany

func (s *Store) AddMany(specs []TaskSpec) ([]*Task, error)

AddMany admits a batch of specs and answers the tasks as they now stand. The batch is validated whole before any of it is written, so a split with a bad third part creates nothing — which is the property the CLI's split answer and the runtime's dispatch both rest on.

THE ID IS THE CALLER'S. The runtime mints ids it can match to nodes; the CLI mints short random ones — `t-` + six base-36 characters — and honours `--as` names. Both roads end here.

func (*Store) AddNote

func (s *Store) AddNote(taskID, agent, body string) (Note, error)

AddNote leaves a task-scoped message. The note is public to every worker on the run — the CLI's notes listing prints all of them — and the author is recorded so a reader can tell an owner's handoff from a bystander's observation.

func (*Store) AddPersonNote

func (s *Store) AddPersonNote(taskID, body string) (Note, error)

AddPersonNote leaves a note in the person's own voice. It is AddNote with the author taken to be the person and no agent name; the CLI prints it with a `person:` prefix, and it is the same store row, because a note's home is the task either way.

func (*Store) AddReviewCheck

func (s *Store) AddReviewCheck(spec TaskSpec) (*Task, error)

AddReviewCheck seats a review check beneath its parent WHATEVER THE PARENT HAS WRITTEN ABOUT ITSELF MEANWHILE. A finished piece of work is reviewed when its worker's return reaches the run, and that can be after the task above it has written its own done: a worker still in its turn may finish the moment its children's rows read done, and the store admits that because every child it has IS finished. The review then has to go beneath a task that reads done, which Store.AddMany refuses for every child, and rightly.

A TASK IS NOT DONE UNTIL THE REVIEWS BENEATH IT HAVE LANDED, whoever wrote its ending. So this one transaction adds the check and moves every done ancestor back to waiting on it, each keeping the result it earned. The root is completed again by the run once the tree is whole (Store.CompleteRoot); a task between closes the way any composite nobody is working closes, when its children are all terminal ([promote]). NOTHING BUT A CHECK COMES IN THIS WAY: every other child of a terminal task continues to be refused by AddMany.

func (*Store) AddSpend

func (s *Store) AddSpend(taskID, model, role string, usd float64, inTokens, outTokens int) error

AddSpend records one charge against a task: the model that spent it, the role it played, the dollars and the token counts. The ledger is append-only — the summary reads it back and nothing here rewrites it — so the row lives outside the whole-plan rewrite every other writer performs, and its own transaction is all it needs. Nothing in the runtime calls this yet; the wiring that spends is a later change.

func (*Store) Amend

func (s *Store) Amend(id, text string) (*Task, error)

Amend prepends text to a task's description — the doctrine's "annotate future work" verb, and one of the two ways a plan learns while it runs.

func (*Store) Archive

func (s *Store) Archive(olderThan time.Duration) ([]*Task, error)

Archive moves whole finished subtrees out of the live plan and into the archive: a task and every task under it, when each one has been terminal — done, cancelled or failed — for longer than the window. The moved tasks leave Tasks, ReadySet and Search whole, so nothing that reads the plan sees them again, and Archived reads the rows back whole. Two things are kept honest: a subtree a still-live task depends on stays in the plan, because an edge to a task that is gone would break the next open, and every surviving parent's composite flag is recomputed, because a parent whose last child left is no longer composite. The root is never archived: it is the run.

func (*Store) Archived

func (s *Store) Archived() ([]*Task, error)

Archived lists the tasks the archive holds, in admission order, each carrying the moment it was archived. It is the read behind the CLI's `list --archived`.

func (*Store) Bottlenecks

func (s *Store) Bottlenecks(limit int) []BlockedCount

Bottlenecks answers the unfinished tasks that hold up the most work right now, most first, bounded by the caller's limit. The count is the tasks that hard-depend on it DIRECTLY — the work one completion unblocks at once — not the transitive reach: a task three removes downstream is not waiting on this one, it is waiting on the task in between. A task nothing hard-depends on holds up nothing and is left out, and equal counts order by descending id.

func (*Store) CanFinalize

func (s *Store) CanFinalize() (bool, string)

CanFinalize says whether the run is over: the root's descendants all terminal, and a plain-word reason naming the open ones when they are not.

func (*Store) CanFinish

func (s *Store) CanFinish(id string) (bool, string)

CanFinish reports whether a task may be completed now, and when it may not, the reason naming the first task in the way: a child that is not terminal, or a hard dependency that is not done. `suggests` does not block — it is advice, not a gate — and the child scan walks the plan in the order it is carried, so the reason a refusal names is the same one on every call. Done refuses with this same reason.

func (*Store) Cancel

func (s *Store) Cancel(id, reason string) (*Task, error)

Cancel ends a task, its descendants, and the work that hard-depends on it. The cascade is the port's own law and runs unconditionally: a cancelled dependency is a cancelled dependent, because nothing in this store can resolve a hard edge whose upstream will never answer.

func (*Store) Changed

func (s *Store) Changed(since time.Time) []string

Changed answers the ids of the tasks that moved since a moment: the task row itself, a note left on it, or a context entry scoped to it. It is what a waiting worker's wake reads for its one early return — the child or dependency that ended failed or cancelled since the park (internal/run's waitMoved), not the whole wake: a move that is not a landing is not a reason to come back. One read over the three places a task's state lives, and it names which tasks to look at again, never what changed about them. The ids are the store's bare spelling, in admission order, and a task created after the moment counts as changed because its own row is newer than the moment.

func (*Store) Claim

func (s *Store) Claim(id, agent string, owner ...string) (*Task, error)

Claim hands a ready leaf to an agent. THE AGENT NAME IS THE RUNTIME'S NAMING TRICK: the supervisor claims with the task's own id, so the worker that later finishes "as" the task can only be the worker the task was handed to. Ownership in Done and Fail is enforced against exactly this name.

THE OWNER IS THE PROCESS, AND IT IS OPTIONAL. Dispatch is per process, so the run supervisor names the process that holds the claim — "<hostname>:<pid>" — in an argument beside the agent, and every pass touches that claim's seen-at stamp. A claim made without an owner (the CLI's own `go`, the session graph's dispatch) has no process behind it, and the claim's agent stands in for one so the seen-at stamp is never left empty.

func (*Store) ClaimNext

func (s *Store) ClaimNext(agent string) (*Task, error)

ClaimNext claims the highest-priority ready leaf for an agent, or answers nil when nothing is ready. It is the `go` verb's whole body, and it is one transaction rather than a read plus a claim because two agents asking at once must not both be handed the same task — the read and the write happen inside the same transaction on the database, which is the only shape that guarantees it.

func (*Store) ClaimWake

func (s *Store) ClaimWake(id, agent string, owner ...string) (*Task, error)

ClaimWake restores ownership to a ready composite the supervisor is waking. Ordinary Claim remains leaf-only; this narrow road exists so a coordinator that released its claim with Wait can use Wait or Done on its next turn.

func (*Store) ClearLive

func (s *Store) ClearLive(taskID string) error

ClearLive forgets a task's live step. It is the deletion every ending runs — the step's own end line, the cap, the wall, an error — so a stopped task stops claiming a present. Clearing a task that has no live row is not an error: the honest answer to "clear it" is the same whether the row was there or already gone.

func (*Store) Close

func (s *Store) Close() error

Close releases the store's database handles and marks the handle closed against every later call. A store opened for one pass — the runtime opens one per pulse — must be closed when the pass is done, or every pass would leave a connection and a file descriptor behind.

A CLOSED STORE REFUSES; IT NEVER PANICS. Close is what a caller does the moment a run is over, and the run's own workers are the last writers to reach for the handle: every method that carries an error — each write, through the one transaction opener, and each read the database answers rather than this handle's memory (Show, RoleOf, Resolve, Archived) — answers ErrClosed afterwards, and the reads that carry none (Tasks, Notes, the summaries and rollups) answer the plan the handle last held instead of reaching for a handle that is gone. Closing a store twice is not an error: a caller that closes in a defer and again on the ending road says the same thing both times.

func (*Store) CompleteRoot

func (s *Store) CompleteRoot(result string) error

CompleteRoot ends the run. Only the runtime calls it, and only when nothing is open; the word it writes is the run's own account of itself. One transaction, like every writer: the database's write lock, a fresh load, the change, the commit. See AddMany for why.

func (*Store) Contexts

func (s *Store) Contexts(taskID, kind string, limit int, filters ...Filter) []ContextEntry

Contexts answers the run's context entries, newest first, bounded and filterable the way the CLI's `contexts --kind` filters.

func (*Store) CriticalPath

func (s *Store) CriticalPath() []*Task

CriticalPath answers the longest chain of hard dependencies between unfinished tasks, upstream first. Empty is the honest answer for a plan with no such chain — one task, or all parallel — and the doctrine teaches it as "prioritize this", which a missing answer must never fake.

func (*Store) Done

func (s *Store) Done(id, agent, result string, artifacts, evidence []string) (*Task, error)

Done completes a task its agent owns. The root is the runtime's, exactly as the earlier port had it: a worker cannot finish the run, only its own task.

THE ONE EXCEPTION IS THE ROOT'S OWN WORKER. The run's root is handed to a worker like every other task (internal/run's supervisor launches it), and on this belt that worker finishes by naming its task's id — which for the root is `root`. It is the same act every other worker performs on its own task, and refusing it would leave the root's worker with nothing to end the loop on. So the root may be completed by an agent whose name IS the root id, and by nothing else: the run's own completion stays Store.CompleteRoot's, and a worker that is not the root's own is still refused.

func (*Store) EndRoot

func (s *Store) EndRoot(reason string) error

EndRoot ends the run on an ending of its OWN that is not its tree's completion: a limit its person set was reached, or the run's own worker failed. Only the runtime calls it, the way only the runtime calls Store.StopRoot and Store.CompleteRoot. The run's own task is FAILED with the reason, every task still open is cancelled with the same reason, and every task that had already ended keeps the ending it has.

IT IS Store.StopRoot's WRITE WITH ONE WORD CHANGED, AND THE WORD IS THE POINT. A cancelled run's task is a person's stop and reads as one; a run that hit a limit or whose own worker failed was stopped by nobody, and a store that said cancelled over it would put a person's hand on an ending no person made.

A RUN LEFT OPEN IS A RUN THE NEXT HAND-OFF ADOPTS, which is why these endings have to be written at all: until this verb only a person's stop wrote an ending on the run's own task, so a run that ended on its dollar limit stayed `running` in its store and the next request in the same place read that store's brief as its own. Two calls are one ending, and a run that has already ended is left as it ended.

func (*Store) Fail

func (s *Store) Fail(id, agent, message string) (*Task, error)

Fail marks a task failed by its owner, with a reason the next reader sees.

func (*Store) FailRoot

func (s *Store) FailRoot(reason string) error

FailRoot ends the run because the run's own task failed: its worker came home with an error and nothing of the run is still working. Only the runtime calls it, the way only the runtime calls Store.CompleteRoot and Store.StopRoot. The run's task is failed with the reason, and every other task still open is cancelled with it, in one transaction; a task that had already ended keeps its ending. No result is written: a result is what a finished run delivers, and a failed worker's account is not one.

A FAILED RUN WAS LEFT OPEN, AND AN OPEN RUN READS AS RUNNING. Nothing wrote the ending of a run whose own worker failed, so its store said `running` for ever: the task's page drew `running` and offered `stop it` over a program that had ended forty minutes earlier, and a door that adopts open stores would have taken the dead run up as live work (Store.StopRoot). A run that already ended is left as it ended.

func (*Store) FailRootAt

func (s *Store) FailRootAt(reason string, at time.Time) error

FailRootAt is Store.FailRoot with the instant the run ended named rather than read off the clock: the zero time is now, which is FailRoot itself.

A RUN WHOSE PROCESS WENT AWAY ENDED WHEN IT WAS LAST SEEN, NOT WHEN SOMEBODY NOTICED. A program's run that codeaf was closed under is ended by the next process that finds its store open, which can be hours later; written at that moment, the run's page counted every hour the machine sat idle as time the program had worked. The caller names the run's last evidence of life instead (its last model call, its last charge, the store's own last write), and the ending is written there.

The instant is held inside what can be true of the run: never before its own task was made, because a run cannot end before it began, and never after now, because an ending in the future would read as a run still going.

func (*Store) LastSpendAt

func (s *Store) LastSpendAt() time.Time

LastSpendAt answers when the ledger's latest charge was written, and the zero time for a ledger with none or a store that is closed. It is one of the three readings a run's last evidence of life is taken from, beside its last model call and the store's own last write (Store.FailRootAt says why that instant matters): a charge is written the moment a call was paid for, so it is the latest moment the run was certainly still spending.

THE LATEST IS FOUND IN GO, not with MAX() in the query, for the reason SpendBy gives: `at` is RFC3339Nano text, whose fractional digits vary, so a text comparison would misorder a whole second against its own fraction.

func (*Store) Live

func (s *Store) Live(taskID string) LiveStep

Live answers one task's live step, and the zero value when the task is running nothing. The read is narrowed to one id; a caller drawing a whole pane reads [LiveSteps] once instead of one row at a time.

func (*Store) LiveSteps

func (s *Store) LiveSteps() map[string]LiveStep

LiveSteps answers every task's live step, keyed by the store's own bare id — what a pane reads in one pass before it draws a column of rows. It reads through the store's read handle, so it answers the last committed live rows — another process's worker included — and runs beside a writer rather than behind it. A store already closed answers no steps rather than reaching for a handle that is gone, the way the other no-error reads do.

func (*Store) NextID

func (s *Store) NextID() string

NextID mints one short id the store has never used: `t-` + six base-36 characters, drawn from crypto/rand. Collision is retried, not mapped around — six characters is over two billion spellings and a plan is bounded far below that.

func (*Store) Notes

func (s *Store) Notes(taskID string, limit int) []Note

Notes answers one task's notes, newest last, with a bound so a task that accumulated a transcript's worth cannot become one.

func (*Store) OpenWaits

func (s *Store) OpenWaits(id string) []string

OpenWaits names what a task is still waiting on, from the store's own current state: every dependency whose upstream is not done, and every child that is not terminal. It is the wait's other half — a parked task's wait is OVER exactly when this answers nothing — and the runtime reads it there rather than re-deriving the rule (internal/run's waitMoved). Sorted, so the reason a refusal names is the same on every call.

func (*Store) Path

func (s *Store) Path() string

func (*Store) Pause

func (s *Store) Pause(id string) (*Task, error)

Pause holds a task, and by inheritance everything under it, out of the ready frontier without changing a single status: the flag is what readiness reads, not a rung of the ladder. It is the runtime's and a person's hold, never a worker's — the bare lifecycle verb stays refused to workers — and the root, which is the run itself, is nobody's to pause.

func (*Store) Project

func (s *Store) Project() string

func (*Store) Prune

func (s *Store) Prune(id string) error

Prune removes one context entry by id. It is the CLI's own verb over its own store, and the id it names is the id AddContext answered.

func (*Store) ReadyLeaves

func (s *Store) ReadyLeaves() []*Task

ReadyLeaves answers the tasks a worker may start now: ready, not composite, nothing else running they conflict with.

func (*Store) ReadySet

func (s *Store) ReadySet(filters ...Filter) ReadySet

ReadySet is ReadyLeaves with the reasons: what can run and, for each task that cannot, why not. The doctrine's `list --status ready` and the runtime's dispatch both read it.

func (*Store) Release

func (s *Store) Release(id, agent string) (*Task, error)

Release puts a claimed task back to pending, for an owner that is not going to finish it. Promotion runs again so a task whose blocker cleared while it was held comes back ready.

func (*Store) RemoveDep

func (s *Store) RemoveDep(downstream, upstream string) (*Task, error)

RemoveDep removes one hard edge between two tasks — the insert verb's rewire, which replaces a direct edge with a path through a new task. It is a graph law like AddDep: the whole plan is asked after the edge is gone, and readiness is recomputed, because lifting an edge can make the downstream task runnable. Removing an edge that is not there is not an error: a rewire asked twice is the same plan.

func (*Store) Resolve

func (s *Store) Resolve(word string) (*Task, error)

Resolve answers one task for a word the model may have written loosely: an exact id first, then `t-` + the word, then a unique prefix of either. Ids fuzzy-match because the doctrine leans on that; a prefix that fits more than one task is a question the caller must not guess at.

func (*Store) Resume

func (s *Store) Resume(id string) (*Task, error)

Resume releases a hold Pause set. Resuming a task that was not paused changes nothing and reports no error, so a caller may call it without asking first.

func (*Store) Retry

func (s *Store) Retry(id string) (*Task, error)

Retry reopens a failed task. The runtime uses it when a node's ending says the work, not the store, is worth another run.

func (*Store) Revise

func (s *Store) Revise(id string, patch TaskPatch) (*Task, error)

Revise patches a task's contract before it runs. After it starts, the contract is frozen: a worker mid-flight answering a spec nobody wrote is the failure the revision gate exists to prevent.

func (*Store) RoleOf

func (s *Store) RoleOf(id string) (string, error)

RoleOf answers the seat a task's shape gives it at the moment it is asked, never the seat it was born with: a task with children is a coordinator and answers plan, and a leaf answers the role it was declared with, which is work unless add or split was told otherwise. Reading the shape rather than a stored guess is what lets a leaf that splits move up to the plan seat without anyone configuring it, and fall back to its own role once the archive has taken its children away.

func (*Store) RootID

func (s *Store) RootID() string

func (*Store) Search

func (s *Store) Search(query string, limit int, filters ...Filter) []SearchResult

Search answers the tasks, notes and context entries whose words match the query, best first. The ranking is simple term overlap — the CLI contract is "ranked results", and what ranks them is the store's own choice so long as the same query answers the same order.

func (*Store) SetLive

func (s *Store) SetLive(taskID string, step int, command string) error

SetLive records the step a task has in flight: its number, the command, and the moment the command began. It is written the way a spend row is — one BEGIN IMMEDIATE transaction under the store's own lock — so a live step and the ledger that accounts for the same call are published by one discipline, and a second begin for the same task rewrites the row rather than stacking one behind another: there is one live step per task, and a new one replaces the last.

The moment is the store's own clock, the same one a spend row is stamped with, so two readings of one run's present cannot disagree about now.

func (*Store) SetVerdictBasis

func (s *Store) SetVerdictBasis(id string, basis VerdictBasis) (*Task, error)

SetVerdictBasis records HOW a task's verdict was earned on the task itself. Done writes it by the same gate that judged the verdict; this store method is the seam a reader with a basis it already holds writes through, and the basis persists with the row.

func (*Store) Show

func (s *Store) Show(id string) (*Task, error)

func (*Store) SpendBy

func (s *Store) SpendBy(axis string, since time.Time) []SpendLine

SpendBy answers the ledger rolled up under one axis, heaviest key first, over the charges written since a moment — the zero time means the whole ledger. Each line is the exact sum of the rows under its key, and a key with no rows under it is absent, never a zero line.

chat and project read the tags of the task each charge names, so a charge whose task the store does not hold has no tag to be counted under and is left out, exactly as spendTotals does. seat, model and task read the ledger's own columns, so a charge the store cannot place still answers under the word it carries. An axis that is not one of spendAxes answers nothing.

func (*Store) SpendSummary

func (s *Store) SpendSummary() SpendSummary

SpendSummary answers the ledger grouped by role and by model: the dollars spent and the calls that spent them under each seat and each model. The ledger is append-only and read here whole — one query, two groupings — so a seat's cost is a row drawn from every run, and a charge whose role or model the run never named still appears under the empty word rather than being dropped.

func (*Store) StaleClaims

func (s *Store) StaleClaims(olderThan time.Duration) []Task

StaleClaims answers the tasks a process holds without touching them: a claimed or running task whose owner was last seen longer ago than the window, so a take-over knows which claims a dead process left behind. THE ROOT IS NOT ONE OF THEM — it is the run itself, claimed by the runtime and never a process's to take over — and a task that is not currently held is not stale either, however long ago it was last touched.

func (*Store) StopRoot

func (s *Store) StopRoot(reason string) error

StopRoot ends the run on a person's word. Only the runtime calls it, the way only the runtime calls Store.CompleteRoot, and it is the one ending of the run's own task that does not wait for the work under it: the run's task and every task still open are cancelled in one transaction, and every task that had already ended keeps the ending it has.

EVERY OPEN TASK IS ENDED, NOT ONLY THE ONES A CASCADE WOULD REACH. Every task in a store is the run's, so the walk is over the store and not down the tree: a cascade that follows cancelled parents stops at a parent that ended earlier and would leave the open work under it to be offered to the next worker.

A RUN LEFT OPEN READS AS RUNNING, which is why a stop has to be written here and cannot only be a context somebody cut: a store whose run task is still open is drawn as work going, and a door that adopts open stores (the headless errand's, the carry-on door) picks it up again, stopped work included. Two presses are one stop, and a run that ended by itself is left as it ended.

func (*Store) Summary

func (s *Store) Summary() Summary

func (*Store) Task

func (s *Store) Task(id string) *Task

Task returns a copy of one task by exact id, for callers that already know the id (the runtime does; a store-born node's id is the plan id).

func (*Store) Tasks

func (s *Store) Tasks(filters ...Filter) []*Task

Tasks answers every task in admission order, copies, narrowed to the tags a filter names. The reading verbs — overview, status, list — render from this.

func (*Store) TouchClaims

func (s *Store) TouchClaims(owner string) (int, error)

TouchClaims refreshes the seen-at stamp of every task an owner holds, so a live process's claims never read as stale. It is the supervisor's heartbeat — called once per pass — and it touches the seen-at column alone: the UpdatedAt moment every other write moves is what a waiting reader's Changed reads, and a heartbeat is not work the plan did, so refreshing a live claim must not wake anybody. An owner holding nothing writes nothing.

func (*Store) Wait

func (s *Store) Wait(id, agent string) (*Task, error)

Wait parks a task whose worker cannot go on: the claim is released, the task stays open and NOT done, and the runtime launches it again through its wake road when its wait is over — once every dependency is done and every child has finished, or at once if one of them failed or was cancelled. THE PARK IS A WAIT ON SOMETHING, so a task with nothing open to wait on — no dependency that is not done, no child that is not terminal — is refused, and a worker cannot park forever on nothing. A parked task keeps a non-terminal status, so it counts as open for every finish law: a parent cannot complete while a parked child stands, and a dependent cannot complete while a parked dependency stands.

func (*Store) Wake

func (s *Store) Wake(id string) (*Task, error)

Wake clears a parked task's wait flag, the other half of Store.Wait: the runtime calls it when the task's wait is over — nothing it waited on is open any more, or one of them failed or was cancelled — immediately before it launches the task's worker again. It moves NOTHING ELSE — not the status, and it does not promote. A leaf keeps the status [Wait] left it (Ready where its hard dependencies are done, so the launch's claim answers the ownership check, and Pending where one is still open, so the launch is dropped and the ordinary frontier brings the task back once it clears). A composite is launched without a claim and needs no status; promoting here would let the store auto-complete a composite the instant its last child landed, before the worker it is being woken for can integrate them.

type Summary

type Summary struct {
	Project   string `json:"project"`
	RootID    string `json:"root_id"`
	Total     int    `json:"total"`
	Pending   int    `json:"pending"`
	Ready     int    `json:"ready"`
	Running   int    `json:"running"`
	Done      int    `json:"done"`
	Failed    int    `json:"failed"`
	Cancelled int    `json:"cancelled"`
	// ProjectSpend and ChatSpend are the ledger's per-project and per-chat
	// totals, read from the spend table — the dollars spent and the calls
	// that spent them. A store that has never been charged carries neither,
	// and --json omits both.
	ProjectSpend map[string]SpendTotal `json:"project_spend,omitempty"`
	ChatSpend    map[string]SpendTotal `json:"chat_spend,omitempty"`
}

type Task

type Task struct {
	TaskSpec
	Status    Status `json:"status"`
	Composite bool   `json:"composite"`
	// Paused is the status-independent hold on a task and everything under it.
	// A paused task keeps the status it had — ready stays ready — and leaves
	// the ready frontier whole while the flag is set; Resume clears it. It is
	// not a rung of the status ladder, which is why it lives here beside the
	// containment flag and not among the statuses.
	Paused bool `json:"paused,omitempty"`
	// Waiting is the parked flag: a worker that could not proceed called
	// `plandb wait`, the store released its claim, and the task stays open and
	// not done until a dependency or a child moves. It is not a rung of the
	// status ladder either — a parked task keeps a non-terminal status — so it
	// lives here beside Paused. WaitedAt is the moment it parked, which is the
	// moment Changed is asked from when the runtime looks for what moved.
	// ReadySet leaves a waiting task off the frontier: the runtime launches it
	// again on the wake road, not on the ordinary ready dispatch.
	Waiting  bool      `json:"waiting,omitempty"`
	WaitedAt time.Time `json:"waited_at,omitempty"`
	// Project and Chat are the row's tags: the run it belongs to and the
	// conversation it was made in. They are not the caller's to set — a task
	// inherits them from its parent — so they live beside the status ladder
	// and not on the spec. A row made before the tags existed carries the
	// empty string.
	Project   string `json:"project,omitempty"`
	Chat      string `json:"chat,omitempty"`
	ClaimedBy string `json:"claimed_by,omitempty"`
	// Owner is the process that holds the claim — "<hostname>:<pid>" — and
	// ClaimedBy stays the worker's identity, the name its finish command
	// answers to. Dispatch is per process, so a claim names the process
	// answerable for it; a process that dies leaves claims nobody touches, and
	// their Owner is how a take-over names them. It is empty on a task made
	// before the column existed.
	Owner string `json:"owner,omitempty"`
	// SeenAt is when the claim's owner was last seen alive: Claim stamps it,
	// every pass of the owning process touches it, and StaleClaims reads it. A
	// task with no claim carries the zero time.
	SeenAt    time.Time `json:"seen_at,omitempty"`
	Result    string    `json:"result,omitempty"`
	Error     string    `json:"error,omitempty"`
	Artifacts []string  `json:"artifacts,omitempty"`
	Evidence  []string  `json:"evidence,omitempty"`
	// VerdictBasis is HOW this task's verdict was earned: whether a check on it
	// read the work or ran the declared proof, and the recorded exit of every
	// run. It is written when the verdict lands, by the same gate that judged
	// it, and it persists so a later reader never has to reopen a trajectory.
	// A task with no verdict carries the zero value.
	VerdictBasis VerdictBasis `json:"verdict_basis,omitempty"`
	CreatedAt    time.Time    `json:"created_at"`
	UpdatedAt    time.Time    `json:"updated_at"`
	CompletedAt  time.Time    `json:"completed_at,omitempty"`
	// ArchivedAt is set only on the tasks Store.Archived reads back — the
	// moment the archive took the row out of the live plan. A live task
	// carries the zero time, because the tasks table has no such column.
	ArchivedAt time.Time `json:"archived_at,omitempty"`
}

type TaskPatch

type TaskPatch struct {
	Title                *string
	Description          *string
	Question             *string
	Kind                 *string
	Priority             *int
	Capabilities         *[]string
	Resources            *[]ResourceClaim
	Effect               *Effect
	Parallel             *string
	Isolation            *string
	Role                 *string
	ContextInputs        *[]string
	Deliverables         *[]string
	EvidenceRequirements *[]string
	Agent                *string
	Acceptance           *string
	Checks               *[]string
}

TaskPatch is the contract half of a task that may be revised before it executes. Parent and dependency rewrites stay graph operations on purpose: a revision that could rewrite the ready frontier silently would make "ready" a word that means different things a minute apart.

type TaskSpec

type TaskSpec struct {
	ID           string          `json:"id"`
	Title        string          `json:"title"`
	Description  string          `json:"description,omitempty"`
	Question     string          `json:"question,omitempty"`
	Kind         string          `json:"kind,omitempty"`
	ParentID     string          `json:"parent_id,omitempty"`
	Dependencies []Dependency    `json:"dependencies,omitempty"`
	Priority     int             `json:"priority,omitempty"`
	Capabilities []string        `json:"capabilities,omitempty"`
	Resources    []ResourceClaim `json:"resources,omitempty"`
	Effect       Effect          `json:"effect,omitempty"`
	// parallel and isolation say how two running tasks may share the machine,
	// and their DEFAULT IS PARALLEL UNLESS DECLARED OTHERWISE. The earlier port
	// defaulted serial, which would have made every store-driven dispatch one
	// at a time and quietly unmade the loop the belt is measuring.
	Parallel  string `json:"parallel,omitempty"`
	Isolation string `json:"isolation,omitempty"`
	// Role is the seat the task occupies, and it is the harness's rather than
	// the work's: the supervisor reads it when the task's next turn is composed
	// and resolves it to a model through the crew. It is one of the four words
	// above, and it matters only for a leaf — a task with children answers plan
	// by its shape alone — so `add` and `split` default a new task to work.
	Role string `json:"role,omitempty"`
	// The contract fields below stay on the type because the store persists
	// them and its laws read them — an evidence requirement makes `done` answer
	// with evidence — while no CLI verb requires any of them. An empty field is
	// the ordinary case.
	ContextInputs        []string `json:"context_inputs,omitempty"`
	Deliverables         []string `json:"deliverables,omitempty"`
	EvidenceRequirements []string `json:"evidence_requirements,omitempty"`
	Agent                string   `json:"agent,omitempty"`
	Acceptance           string   `json:"acceptance,omitempty"`
	Checks               []string `json:"checks,omitempty"`
}

type VerdictBasis

type VerdictBasis struct {
	// Kind is "run" when the declared checks were what the verdict rested on,
	// and "reading" when the checker judged from what it could see. There are
	// exactly those two, and a reader needs nothing else: the pair answers
	// whether a verdict claims a re-run.
	Kind string `json:"kind"`
	// Runs is one entry per declared command that has a recorded run, with the
	// exit the check's own record shows. Only a run basis carries it; a
	// reading basis carries none.
	Runs []VerdictRun `json:"runs,omitempty"`

	// Unobserved names the declared checks a reading verdict did not observe
	// run, which happens for a record written before command exits were
	// recorded. It is set only on a reading basis, so a reader does not mistake
	// an old record's holds for one earned by running the declared checks.
	Unobserved []string `json:"unobserved,omitempty"`
}

THE VERDICT BASIS IS HOW ONE VERDICT WAS EARNED, written down so that a reader can tell whether the checker read the work or ran the declared proof, and, when it ran, the recorded exit from every run, without reopening the trajectory. It is part of the task record (Task.VerdictBasis) and rides every surface reading that record: cli.go's cliTaskObject, the session fit record, and the tasks view.

type VerdictRun

type VerdictRun struct {
	Command  string `json:"command"`
	ExitCode int    `json:"exitCode"`
}

VerdictRun is one declared check's recorded run: the command, and the exit it actually produced in the checker's own trajectory.

Jump to

Keyboard shortcuts

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