Documentation
¶
Overview ¶
Package store is worklog's persistence layer: a per-project SQLite database (`.worklog/tasks.db`) holding the task tree, its blocking edges, and the durable record of what was decided and done across agent sessions.
Index ¶
- Constants
- Variables
- func AppendToSectionInBody(body, path, content string) (string, error)
- func DeleteSectionInBody(body, path string) (string, error)
- func DetectKind(url string) string
- func GetSectionFromBody(body, path string) (string, bool)
- func InsertSectionInBody(body, path, heading, sectionBody, position string) (string, error)
- func MoveSectionInBody(body, path, before, after string) (string, error)
- func ReplaceInBodyText(body, old, new string) (string, error)
- func Resolve(dir string) (string, error)
- func SetSectionInBody(body, path, content string) (string, bool)
- func ValidStatusName(s string) bool
- type ChildRef
- type Config
- func (c *Config) DefaultStatus() string
- func (c *Config) Has(name string) bool
- func (c *Config) IsActionable(name string) bool
- func (c *Config) IsBlocked(name string) bool
- func (c *Config) IsClosed(name string) bool
- func (c *Config) Kind(name string) (kind StatusKind, ok bool)
- func (c *Config) Names() []string
- func (c *Config) NamesOfKind(kinds ...StatusKind) []string
- func (c *Config) Validate() error
- type CreateTaskInput
- type Decision
- type FindOpts
- type FindResult
- type JournalEntry
- type Link
- type ListOpts
- type Section
- type Session
- type StatusDef
- type StatusKind
- type Store
- func (s *Store) ActiveBlockers(taskID int64) ([]Task, error)
- func (s *Store) AddDecision(slug, decision, rationale string) (*Decision, error)
- func (s *Store) AddDep(slug, blockerSlug string) error
- func (s *Store) AddDeps(slug string, blockers []string) error
- func (s *Store) AddJournal(slug, text string) (*JournalEntry, error)
- func (s *Store) AddLink(slug, url, kind, label string) (*Link, error)
- func (s *Store) AppendSection(slug, path, content string) (*Task, error)
- func (s *Store) Close() error
- func (s *Store) Config() *Config
- func (s *Store) CreateTask(in CreateTaskInput) (*Task, error)
- func (s *Store) CurrentSessionID() int64
- func (s *Store) Decisions(taskID int64) ([]Decision, error)
- func (s *Store) DeleteRecord(kind string, id int64) error
- func (s *Store) DeleteSection(slug, path string) (*Task, error)
- func (s *Store) Detail(slug string) (*TaskDetail, error)
- func (s *Store) EditRecord(kind string, id int64, field, content string) error
- func (s *Store) EndCurrentSession() error
- func (s *Store) EndLatestOpenSession(summary string) (*Session, error)
- func (s *Store) EndSession(id int64, summary string) (*Session, error)
- func (s *Store) Find(o FindOpts) (*FindResult, error)
- func (s *Store) GetSection(slug, path string) (string, error)
- func (s *Store) GetSession(id int64) (*Session, error)
- func (s *Store) GetTask(slug string) (*Task, error)
- func (s *Store) InsertSection(slug, path, heading, sectionBody, position string) (*Task, error)
- func (s *Store) Links(taskID int64) ([]Link, error)
- func (s *Store) ListTasks(o ListOpts) ([]TaskView, error)
- func (s *Store) MoveSection(slug, path, before, after string) (*Task, error)
- func (s *Store) NextLine() (string, error)
- func (s *Store) NextTask() (*TaskView, error)
- func (s *Store) RecentJournal(limit int) ([]JournalEntry, error)
- func (s *Store) RemoveDep(slug, blockerSlug string) error
- func (s *Store) RemoveDeps(slug string, blockers []string) error
- func (s *Store) ReplaceInBody(slug, old, new string) (*Task, error)
- func (s *Store) SetConfig(cfg *Config) error
- func (s *Store) SetSection(slug, path, content string) (*Task, error)
- func (s *Store) StartSession(agent string) (*Session, error)
- func (s *Store) TOC(slug string) ([]Section, error)
- func (s *Store) TaskJournal(taskID int64, limit int) ([]JournalEntry, error)
- func (s *Store) Tree(rootSlug string) ([]TreeNode, error)
- func (s *Store) UpdateTask(in UpdateTaskInput) (*Task, error)
- func (s *Store) WarmContext(journalLimit int) (string, error)
- type Task
- type TaskDetail
- type TaskView
- type TreeNode
- type UpdateTaskInput
Constants ¶
const ConfigFileName = "config.jsonc"
ConfigFileName is the optional per-project config file, kept beside the database in DirName. Its format is JSON with comments and trailing commas (JSONC).
const DirName = ".worklog"
DirName is the per-project directory that holds the database.
const FileName = "tasks.db"
FileName is the database file within DirName.
const StatusNamePattern = `^[a-z][a-z0-9]*(_[a-z0-9]+)*$`
StatusNamePattern is the regular expression every status name must match: lowercase words of letters and digits joined by single underscores, the first starting with a letter. No leading, trailing or doubled underscore is allowed, so each name maps to one distinct heading (see statusHeading).
Variables ¶
var ErrAmbiguousMatch = errors.New("ambiguous match")
ErrAmbiguousMatch is returned by a body-replace when the target text occurs more than once, so no single occurrence can be chosen unambiguously.
var ErrInvalidSectionOp = errors.New("invalid section operation")
ErrInvalidSectionOp is returned when a section operation is malformed — a move with the wrong number of anchors, a move relative to itself or into its own descendant, or an unknown insert position.
var ErrNotFound = errors.New("not found")
ErrNotFound is returned when a task or other record does not exist.
Functions ¶
func AppendToSectionInBody ¶ added in v0.6.0
AppendToSectionInBody inserts content as a block at the section's end line — after the section's own content and all its subsections, before the next section — with a single blank-line seam on each side.
func DeleteSectionInBody ¶ added in v0.6.0
DeleteSectionInBody removes a section's entire span — its heading, body, and all subsections — and re-seams the surrounding blocks.
func DetectKind ¶
DetectKind infers a link kind from a URL so callers can just pass the URL.
func GetSectionFromBody ¶
GetSectionFromBody returns the content under a heading path (its subsections included), excluding the heading line itself.
func InsertSectionInBody ¶ added in v0.6.0
InsertSectionInBody inserts a new heading (and optional body) relative to an existing heading path, or — when path is empty — at document scope. position is one of before, after, firstChild, lastChild. The new heading's depth is the target's level for before/after, the target's level+1 for first/lastChild, and the document's shallowest level (or 2) at document scope. A whitespace-only sectionBody yields a heading-only block.
func MoveSectionInBody ¶ added in v0.6.0
MoveSectionInBody moves a section (with its subsections) before or after another heading path. Exactly one of before/after must be set. The moved block keeps its original heading levels verbatim — no depth rewriting.
func ReplaceInBodyText ¶ added in v0.6.0
ReplaceInBodyText replaces one exact, unique substring anywhere in the body with new (which may be "" to remove it). It is a verbatim splice — no seam normalization, so it may touch heading lines or any other text. An empty or absent old is ErrNotFound; more than one occurrence is ErrAmbiguousMatch.
func Resolve ¶
Resolve returns the database path for the project containing dir, walking up from dir to find an existing DirName; if none is found it returns the path dir/DirName/FileName so a fresh database is created in place.
func SetSectionInBody ¶
SetSectionInBody replaces the content under a heading path, preserving the heading line, and returns the new body.
func ValidStatusName ¶ added in v0.7.0
ValidStatusName reports whether s is a well-formed status name (see StatusNamePattern).
Types ¶
type ChildRef ¶
type ChildRef struct {
Slug string `json:"slug"`
Title string `json:"title"`
Status string `json:"status"`
}
ChildRef is a lightweight reference to a child task.
type Config ¶ added in v0.7.0
type Config struct {
Statuses []StatusDef `json:"statuses"`
}
Config is a project's worklog configuration. Statuses is the complete, ordered list of statuses a task may be set to.
func DefaultConfig ¶ added in v0.7.0
func DefaultConfig() *Config
DefaultConfig returns a fresh copy of the configuration used when a project has no config file: pending (default), in_progress, blocked, done, dropped.
func LoadConfig ¶ added in v0.7.0
LoadConfig reads ConfigFileName from dir and parses it with ParseConfigFile. An absent file yields DefaultConfig; a file that exists but holds no JSON value is an error (see ParseConfig). A read error is returned as the os package reports it (e.g. "open <path>: permission denied"); parse and validation errors are prefixed with the file path, and all but the no-content error carry the path:line:col of the offending input (see ParseConfig).
func ParseConfig ¶ added in v0.7.0
ParseConfig decodes and validates JSONC config data. An object that omits "statuses" (such as {}) yields DefaultConfig. A document with no JSON value at all (empty, or only whitespace, a byte-order mark and comments) is an error, errNoContent: that is what an editor saving in place exposes for a moment, so it must never be read as the defaults. Keys are matched exactly (case-sensitive); an unknown or duplicate key, or a null document, "statuses" list, entry or status field, is an error. Syntax, type and key errors are prefixed with line:col, pointing at the offending character (for a type error, the first character of the offending value). So are the status-list rule violations Validate reports: a rule about one status points at its offending field's value (or at the entry's opening brace when that field is absent), and a rule about the list as a whole (it is empty, has no actionable or no closed status, or no default) at the list's opening bracket. Key errors echo the key, and rule errors a status name or kind, as written in the file (see sourceText).
func ParseConfigFile ¶ added in v0.7.0
ParseConfigFile is ParseConfig for data read from the file at path: every error is prefixed with path (path:line:col: for positioned errors). It lets a caller that has already read the file parse exactly those bytes, rather than re-reading a file that may have changed since.
func ParseConfigFileDefaults ¶ added in v0.7.0
ParseConfigFileDefaults is ParseConfigFile that also reports whether the result is the defaults a document without "statuses" (such as {}) stands for (defaults true), rather than a list the file gives, even one that matches the defaults. defaults is false whenever err is non-nil.
func (*Config) DefaultStatus ¶ added in v0.7.0
DefaultStatus returns the status new tasks receive.
func (*Config) IsActionable ¶ added in v0.7.0
IsActionable reports whether name is a configured status of kind open or active. A status absent from the config is not actionable.
func (*Config) IsBlocked ¶ added in v0.7.0
IsBlocked reports whether name is a configured status of kind blocked. A status absent from the config is not blocked.
func (*Config) IsClosed ¶ added in v0.7.0
IsClosed reports whether name is a configured status of kind closed. A status absent from the config is not closed.
func (*Config) Kind ¶ added in v0.7.0
func (c *Config) Kind(name string) (kind StatusKind, ok bool)
Kind returns the kind of name; ok is false when name is not configured.
func (*Config) NamesOfKind ¶ added in v0.7.0
func (c *Config) NamesOfKind(kinds ...StatusKind) []string
NamesOfKind returns, in config order, the names of statuses whose kind is any of kinds.
func (*Config) Validate ¶ added in v0.7.0
Validate checks the status list: at least one status; every name well-formed and unique; every kind known; no open- or active-kind status (the kinds that get their own WarmContext briefing section) whose heading would equal a fixed section heading of the briefing; exactly one default, of kind open or active; at least one actionable (open or active) status; and at least one closed status. Blocked- and closed-kind statuses get no section of their own, so any name is allowed for them. Names and kinds are echoed Go-quoted; its errors carry no position (ParseConfig adds one for a config read from input).
type CreateTaskInput ¶
type CreateTaskInput struct {
Title string
Slug string // optional; derived from Title when empty
Parent string // optional parent slug
Body string
Status string // optional; defaults to the configured default status
Priority int // optional; defaults to 3
BlockedBy []string
}
CreateTaskInput carries the fields for a new task.
type Decision ¶
type Decision struct {
ID int64 `json:"id"`
TaskSlug string `json:"task_slug,omitempty"`
SessionID int64 `json:"session_id,omitempty"`
Ts string `json:"ts"`
Decision string `json:"decision_md"`
Rationale string `json:"rationale_md,omitempty"`
}
Decision is a choice made while executing a task — the "why we did it this way" record that turns a finished task into something you can learn from later. Stored as rows (queryable) but rendered as a "## Decisions" section when a task is shown.
type FindOpts ¶
type FindOpts struct {
Query string // free text matched against titles, bodies, decisions, journal, link URLs
Status string // restrict matched tasks to this status, configured or not
Since string // RFC3339 lower bound on decision/journal timestamps
HasDecisions bool // only tasks that carry at least one decision
}
FindOpts constrains a work-find search. Any subset may be set.
type FindResult ¶
type FindResult struct {
Tasks []TaskView `json:"tasks,omitempty"`
Decisions []Decision `json:"decisions,omitempty"`
Journal []JournalEntry `json:"journal,omitempty"`
}
FindResult is what a search turned up, kept in three buckets so "what was decided" and "what was done" are answerable, not just "what's open".
type JournalEntry ¶
type JournalEntry struct {
ID int64 `json:"id"`
TaskSlug string `json:"task_slug,omitempty"`
SessionID int64 `json:"session_id,omitempty"`
Ts string `json:"ts"`
Kind string `json:"kind"`
Text string `json:"text_md"`
}
JournalEntry is one line in the running log of what happened — the trail a fresh session reads to reconstruct where the last one left off.
type Link ¶
type Link struct {
ID int64 `json:"id"`
Kind string `json:"kind"`
URL string `json:"url"`
Label string `json:"label,omitempty"`
CreatedAt string `json:"created_at"`
}
Link ties a task to an external artifact — most importantly a GitHub issue or PR by its full URL, so "which task touched issue #412" is a lookup, not a grep through prose.
type ListOpts ¶
type ListOpts struct {
Status string // exact status filter, configured or not; empty for any
Parent string // parent slug; "" for any parent
IncludeClosed bool // when false (and Status is empty), closed-kind tasks are omitted
}
ListOpts filters ListTasks.
type Section ¶
type Section struct {
Path string `json:"path"`
Level int `json:"level"`
Title string `json:"title"`
}
Section identifies one markdown heading within a task body. Path is the slash-joined chain of ancestor headings ("Design/Storage"), so nested sections are addressable without ambiguity.
func TOCFromBody ¶
TOCFromBody lists the sections in a body.
type Session ¶
type Session struct {
ID int64 `json:"id"`
StartedAt string `json:"started_at"`
EndedAt string `json:"ended_at,omitempty"`
Agent string `json:"agent"`
Summary string `json:"summary_md,omitempty"`
}
Session is one agent working session: the unit worklog attributes decisions and journal entries to, so a later session can see who did what and when.
type StatusDef ¶ added in v0.7.0
type StatusDef struct {
Name string `json:"name"`
Kind StatusKind `json:"kind"`
Default bool `json:"default"`
}
StatusDef is one configured task status.
type StatusKind ¶ added in v0.7.0
type StatusKind string
StatusKind classifies a configured status by how the rest of the system treats tasks holding it.
const ( // KindOpen is actionable work not yet started. KindOpen StatusKind = "open" // KindActive is actionable work underway. KindActive StatusKind = "active" // KindBlocked is a manual hold: neither actionable nor closed. KindBlocked StatusKind = "blocked" // KindClosed is terminal: a closed task no longer blocks its dependents. KindClosed StatusKind = "closed" )
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a handle to one project's worklog database.
func Open ¶
Open opens (creating if needed) the database at path, applies the schema, and enables WAL so concurrent agent sessions sharing the tree do not clobber each other. The status configuration is read from ConfigFileName beside the database (see LoadConfig); a config error fails the open. Pass ":memory:" for an ephemeral database (tests), which uses DefaultConfig.
func OpenWithConfig ¶ added in v0.7.0
OpenWithConfig is Open with an explicit status configuration instead of the one beside the database. cfg is validated and copied, so later changes to it do not affect the store; nil means DefaultConfig.
func (*Store) ActiveBlockers ¶
ActiveBlockers returns the tasks still blocking the given task — those dependency targets whose status is not of kind closed. A closed dependency no longer blocks and is omitted; one holding a status absent from the config still blocks.
func (*Store) AddDecision ¶
AddDecision records a decision against a task, attributed to the current session, and journals it.
func (*Store) AddDeps ¶ added in v0.6.0
AddDeps records that task (slug) is blocked by each named task, applying the whole batch in one transaction: a single unresolvable, self-blocking, or cycle-forming slug rolls back every edge in the call, so the edge set never lands half-applied.
func (*Store) AddJournal ¶
func (s *Store) AddJournal(slug, text string) (*JournalEntry, error)
AddJournal appends a freeform note, optionally against a task (slug "" records a project-level note).
func (*Store) AddLink ¶
AddLink attaches a link to a task. When kind is empty it is detected from the URL. The addition is journaled.
func (*Store) AppendSection ¶ added in v0.6.0
AppendSection appends a block to the end of a task-body section.
func (*Store) Config ¶ added in v0.7.0
Config returns a copy of the status configuration the store runs with.
func (*Store) CreateTask ¶
func (s *Store) CreateTask(in CreateTaskInput) (*Task, error)
CreateTask inserts a task, optionally under a parent and with blocking edges, and records a "created" journal entry. Its status must be a configured one.
func (*Store) CurrentSessionID ¶
CurrentSessionID returns the session writes are attributed to, or 0 if none.
func (*Store) DeleteRecord ¶ added in v0.6.0
DeleteRecord removes a decision or journal row by id. Like EditRecord it is silent — no journal entry is appended for the deletion.
func (*Store) DeleteSection ¶ added in v0.6.0
DeleteSection removes a task-body section and its subsections.
func (*Store) Detail ¶
func (s *Store) Detail(slug string) (*TaskDetail, error)
Detail assembles the full record for one task.
func (*Store) EditRecord ¶ added in v0.6.0
EditRecord replaces one markdown field of an existing decision or journal row, in place. It is deliberately silent — no journal entry is appended and the row's ts is untouched — because an in-place correction of a formatting slip is not itself an event worth logging.
func (*Store) EndCurrentSession ¶
EndCurrentSession closes the in-process session (set by StartSession), if any. Safe to call more than once.
func (*Store) EndLatestOpenSession ¶
EndLatestOpenSession closes the most recent session that has no end time, recording summary when non-empty. It is the entry point a Stop hook uses, so it need not know the session id. A no-op when nothing is open.
func (*Store) EndSession ¶
EndSession records summary against the session and stamps its end time. When summary is empty the existing summary is left untouched.
func (*Store) Find ¶
func (s *Store) Find(o FindOpts) (*FindResult, error)
Find searches across tasks, decisions, and the journal.
func (*Store) GetSection ¶
GetSection returns one section of a task's body.
func (*Store) GetSession ¶
GetSession returns one session by id.
func (*Store) InsertSection ¶ added in v0.6.0
InsertSection inserts a new section into a task body relative to a heading path (or at document scope when path is empty).
func (*Store) MoveSection ¶ added in v0.6.0
MoveSection moves a task-body section before or after another heading path.
func (*Store) NextLine ¶ added in v0.3.0
NextLine renders the single highest-priority actionable task as a one-line notice, for a visible session-start message. It returns "" when nothing is actionable, so a caller emits nothing rather than an empty banner.
func (*Store) NextTask ¶
NextTask returns the highest-priority actionable task — one whose status is of kind open or active, with no active blockers — or nil when nothing is actionable.
func (*Store) RecentJournal ¶
func (s *Store) RecentJournal(limit int) ([]JournalEntry, error)
RecentJournal returns the most recent entries across the whole project, newest first, joined to their task slug when they have one.
func (*Store) RemoveDeps ¶ added in v0.6.0
RemoveDeps drops each named blocking edge in one transaction, so an unresolvable slug mid-batch leaves the whole edge set untouched.
func (*Store) ReplaceInBody ¶ added in v0.6.0
ReplaceInBody replaces one exact, unique substring in a task body.
func (*Store) SetConfig ¶ added in v0.7.0
SetConfig replaces the store's status configuration, so a long-running process (the MCP server) can follow edits to ConfigFileName without reopening the database. cfg is validated and copied exactly as OpenWithConfig does; nil means DefaultConfig. On a validation error the store keeps its current configuration.
SetConfig is not safe to call concurrently with any other Store method: the status set is read without synchronisation by every query and write. The MCP server calls it from its single request loop, between tool calls.
func (*Store) SetSection ¶
SetSection replaces one section of a task's body, leaving the rest untouched.
func (*Store) StartSession ¶
StartSession opens a new session and marks it current, so subsequent writes (decisions, journal) are attributed to it. agent is a free label, typically the client name from the MCP handshake.
func (*Store) TaskJournal ¶
func (s *Store) TaskJournal(taskID int64, limit int) ([]JournalEntry, error)
TaskJournal returns a task's own journal entries, newest first.
func (*Store) Tree ¶
Tree returns the forest of top-level tasks (rootSlug == "") or the subtree rooted at a given slug. Every node carries closed tasks too, so the full history of a branch stays visible.
func (*Store) UpdateTask ¶
func (s *Store) UpdateTask(in UpdateTaskInput) (*Task, error)
UpdateTask applies a partial update. A new status must be a configured one; setting a closed-kind status stamps closed_at, setting any other clears it, and a change of status is journaled. A task whose saved status is no longer configured keeps it, and its other fields stay editable.
func (*Store) WarmContext ¶
WarmContext renders the "where was I" briefing a new session opens with: the next actionable task, then the open work grouped by status, then the tail of the journal so the last session's trail is visible immediately.
Open work is grouped into one section per active-kind status and then one per open-kind status (each in config order), then "Blocked" (tasks with a blocked-kind status or an unclosed dependency), then one section per status absent from the config that an unclosed task still holds (sorted by name). A dependency-blocked task appears both under its status and under Blocked. A leftover section's heading carries the suffix " (not in config)".
type Task ¶
type Task struct {
ID int64 `json:"id"`
Slug string `json:"slug"`
ParentID int64 `json:"parent_id,omitempty"`
Title string `json:"title"`
Body string `json:"body_md,omitempty"`
Status string `json:"status"`
Priority int `json:"priority"`
Position int `json:"position"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ClosedAt string `json:"closed_at,omitempty"`
}
Task is one node in the work tree.
type TaskDetail ¶
type TaskDetail struct {
Task
Blockers []string `json:"blocked_by,omitempty"`
Children []ChildRef `json:"children,omitempty"`
Links []Link `json:"links,omitempty"`
Decisions []Decision `json:"decisions,omitempty"`
Journal []JournalEntry `json:"journal,omitempty"`
}
TaskDetail is the full picture of one task: its body, what still blocks it, its children, and — the point of worklog — the links, decisions, and journal that record what was done and why.
func (*TaskDetail) Markdown ¶
func (d *TaskDetail) Markdown() string
Markdown renders a task detail as human-readable markdown, overlaying the stored links and decisions as their own sections beneath the authored body.
type TaskView ¶
type TaskView struct {
Task
Blockers []string `json:"blocked_by,omitempty"`
ChildCount int `json:"child_count,omitempty"`
Actionable bool `json:"actionable"`
}
TaskView is a task plus the derived facts a reader needs to decide what to do next: its still-active blockers and whether it has children.
type UpdateTaskInput ¶
type UpdateTaskInput struct {
Slug string
NewSlug *string // rename the task's slug; normalized like a created slug
Title *string
Status *string
Priority *int
Position *int
Parent *string // "" detaches to root
Body *string
}
UpdateTaskInput carries an update; nil pointers leave a field unchanged.