lookaround

package
v0.44.0 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package lookaround collects facts about one project and derives what changed in a window (the feed) or where each of its tasks stands (the state). Collectors (host, tracker, local) fetch and interpret nothing; DeriveFeed and Derive are pure functions, facts in, rows out, and the only places a group or a stage is decided; the Board is the JSON every renderer reads, docs/board.md its spec and docs/board.schema.json its schema.

Index

Constants

View Source
const (
	ViewFeed  = "feed"
	ViewState = "state"
)

The two views a board is: what changed in the window, and where every task stands.

View Source
const (
	SourceHost    = "host"
	SourceTracker = "tracker"
	SourceLocal   = "local"
)

The three sources, in the order the board lists them.

View Source
const (
	StageAsksWaiting      = "asks-waiting"
	StageBlocked          = "blocked"
	StageReady            = "ready"
	StageInReview         = "in-review"
	StagePROpen           = "pr-open"
	StageImplementing     = "implementing"
	StagePlanned          = "planned"
	StageMergedUnfinished = "merged-unfinished"
	StageNext             = "next"
	StageDone             = "done"
)

The stages, in the order the board lists rows.

View Source
const (
	FromLocal   = "local"
	FromTrace   = "trace"
	FromHost    = "host"
	FromTracker = "tracker"
	FromGuessed = "guessed"
)

Where a stage's or a group's evidence came from: local state, the traces f10 left on the host, the host's certain facts (a merge, a creation), the tracker's list, or the host's facts read as a guess.

View Source
const (
	GroupStarted  = "started"
	GroupUpdated  = "updated"
	GroupReviewed = "reviewed"
	GroupFinished = "finished"
	GroupNewTasks = "new-tasks"
)

The feed's groups, in the order it lists them.

View Source
const (
	RelationPart     = "part"      // the two overlapped
	RelationFollowUp = "follow-up" // it merged before the later one opened
	RelationReplaced = "replaced"  // it closed unmerged
)

How a task's earlier request relates to the one its feed row leads with.

View Source
const Version = 3

Version is the Board JSON's version; a change to a stage rule or a field's meaning bumps it with the spec.

Variables

Groups is every feed group in listing order.

Stages is every stage in listing order.

Functions

func AgentFor

func AgentFor(id, branch, path string) string

AgentFor is the name start gave the task's agent in the worktree: the id and the suffix its branch carries, hashed with the path.

func Plural added in v0.39.0

func Plural(n int, word string) string

Plural is n and word, the word in its plural past one: "-sh" takes "es" ("2 force pushes"). Every renderer counting a word goes through it.

Types

type Board

type Board struct {
	Version     int       `json:"version"`
	GeneratedAt time.Time `json:"generated_at"`
	View        string    `json:"view"`
	Project     Project   `json:"project"`
	Window      Window    `json:"window"`
	Sources     []Source  `json:"sources"`
	Rows        []Row     `json:"rows"`           // the state view's; empty in the feed
	Feed        *Feed     `json:"feed,omitempty"` // the feed view's
}

A Board is one project's feed or rows, with where each fact came from.

func Derive

func Derive(f Facts, now, since time.Time) Board

Derive turns facts into the board: one row per task or taskless request, its stage decided by the rules docs/board.md lists in order, the first that matches winning, and each rule naming where its evidence came from. It reads no clock and does no I/O: now and since are the board's.

func DeriveFeed added in v0.39.0

func DeriveFeed(f Facts, now time.Time, w Window) Board

DeriveFeed turns facts into the feed over the window: one row per task whose requests had activity there, one per such request naming no task, each in the one group its activity reads as by the rules docs/board.md lists, one per task created there, and the open requests with none counted in the quiet line. Like Derive it reads no clock and does no I/O.

func Unreachable

func Unreachable(p Project, err error, now time.Time, w Window, view string) Board

Unreachable is the board of a project no collector could run for: a checkout whose instructions do not read, or a repository with no host CLI. Every source carries the error, so a reader of several boards sees the project failed rather than empty; a project with no checkout keeps its local source skipped, as a remote read does.

type Branch

type Branch struct {
	Task   string   // the id the branch is linked to, else what its name spells
	Riders []string // a bundle's other members
	After  string   // the base task recorded on it
	Model  string   // the agent's model, where start recorded it
}

A Branch is what the local config records on a branch.

type Facts

type Facts struct {
	Project Project
	Prefix  string // the task id prefix, for reading ids off branches and titles
	Tasks   []Task
	PRs     []PR
	Local   *Local // nil when the local collector was skipped or failed
	People  []Person
	Sources []Source

	// the feed's: what the host did in the window, each started task's
	// other requests by id, and a local-only checkout's branch tips
	Activity *forge.Activity
	Earlier  map[string][]forge.PR
	Tips     []Tip
}

Facts is what the collectors found, before any interpretation.

func Collect

func Collect(ctx context.Context, in Inputs) Facts

Collect runs every collector and returns what each found, guarded: a collector that errors marks its source failed and contributes nothing, and every other one still runs.

type Feed added in v0.39.0

type Feed struct {
	Rows  []FeedRow `json:"rows"`
	Quiet Quiet     `json:"quiet"`
}

A Feed is what changed in the window: one row per task whose requests were touched there, per touched request naming no task, and per task created there, and the open requests nobody touched, counted.

type FeedRow added in v0.39.0

type FeedRow struct {
	Group string `json:"group"`
	Key   string `json:"key"` // the task id, else `#<number>`, else the branch
	Task  *Task  `json:"task,omitempty"`
	PR    *PRRef `json:"pr,omitempty"`
	From  string `json:"from"`
	Who   Who    `json:"who"`
	// Owes is who the next move waits on: the author after a review, a
	// requested reviewer before one.
	Owes      string      `json:"owes,omitempty"`
	Abandoned bool        `json:"abandoned,omitempty"` // closed without merging, nothing later
	Related   []RelatedPR `json:"related,omitempty"`   // the task's requests opened before PR
	Summary   string      `json:"summary,omitempty"`   // what happened, one sentence
	At        time.Time   `json:"at"`                  // the latest event in the window
}

A FeedRow is one task whose requests were touched in the window, one request naming no task, or one task created there.

type Findings

type Findings struct {
	Round int
	Open  int // findings with no verdict, or a verdict of ask
	At    time.Time
}

Findings is a task's latest review round as its findings file holds it.

type Inputs

type Inputs struct {
	Project Project
	Prefix  string
	// Since is where reading starts: the state view's done window, or the
	// earliest span the feed may show.
	Since time.Time

	// Feed reads what changed since Since instead of where every task
	// stands: the host's activity, the tracker's list, and nothing local
	// unless the checkout is local only.
	Feed bool

	// LocalOnly is a checkout with no origin remote: the host is skipped,
	// and the tracker too unless a driver answers it (Tasks set).
	LocalOnly bool

	Host      *forge.Host
	HostError string // why there is no host, for the source's error

	Tasks func(ctx context.Context) ([]Task, error)

	// RemoteOpen reports whether the project's declared remote reviewer
	// left a review on the request nobody handled; nil declares none. An
	// error is a note on the host source, the request kept without it.
	RemoteOpen func(ctx context.Context, number int) (bool, error)

	Remote bool
	Local  *LocalInputs

	// People is the user's map of one person's identities across the
	// sources; nil groups rows by whatever identity each source gives.
	People []Person

	// Now is the clock; nil is time.Now. A test fixes it so timings are
	// stable.
	Now func() time.Time
}

Inputs binds the collectors to one project. A nil Host, Tasks or Local is that source absent; Remote skips the local collector on purpose.

type Local

type Local struct {
	Runs       []state.Run
	Worktrees  []Worktree
	Branches   map[string]Branch    // by branch name
	Plans      map[string]time.Time // task id to the plan file's mtime
	Archived   map[string]bool      // task ids whose plan is archived
	Findings   map[string]Findings  // task id to its latest round
	Verdicts   map[string]string    // task id to its latest verdict
	Finished   map[string]bool      // task ids the ledger holds task_finished for
	Workspaces map[string]Workspace // worktree path to the Herdr workspace on it
}

Local is what this machine knows: live runs, worktrees, branches, plans, findings, verdicts, what the ledger closed, and Herdr's view.

type LocalInputs

type LocalInputs struct {
	Main       string // the main checkout
	StateDir   string // run state files
	TTL        time.Duration
	SharedRoot string // the storage root every checkout shares; "" reads each checkout's .f10
	LedgerPath string // "" reads no ledger
	ProjectID  string // the ledger's project id
	Herdr      bool   // Herdr is reachable: read its workspaces
}

LocalInputs is where this machine keeps what the local collector reads.

type Next

type Next struct {
	Command string `json:"command,omitempty"`
	Text    string `json:"text,omitempty"`
}

Next is the row's next action: a command where one exists, else a sentence.

type PR

type PR struct {
	forge.PR

	Block      *trace.State   `json:"block,omitempty"`
	Trailers   []trace.Parsed `json:"trailers,omitempty"`
	HeadExists *bool          `json:"head_exists,omitempty"` // read for merged requests only
	RemoteOpen bool           `json:"remote_open,omitempty"` // the declared remote reviewer left one unhandled
}

A PR is a request with what the host says around it.

type PRRef

type PRRef struct {
	Number    int    `json:"number"`
	Title     string `json:"title,omitempty"`
	URL       string `json:"url,omitempty"`
	State     string `json:"state"` // open, merged or closed
	Draft     bool   `json:"draft,omitempty"`
	Head      string `json:"head,omitempty"`
	Base      string `json:"base,omitempty"`
	Mergeable string `json:"mergeable,omitempty"` // clean, conflicts or blocked
	Checks    string `json:"checks,omitempty"`    // green, red or pending
}

A PRRef is the request a row hangs on.

type Person

type Person struct {
	Name    string
	Tracker string
	Host    string
	Emails  []string
}

A Person joins the identities one person has across the sources: the tracker's name for them, their host login, the emails their commits carry. Every field but Name is optional.

type Project

type Project struct {
	ID       string `json:"id"`                 // the normalised remote, `github.com/org/repo`
	Name     string `json:"name"`               // the main checkout's basename
	Host     string `json:"host,omitempty"`     // gh or glab
	Checkout string `json:"checkout,omitempty"` // the main checkout; empty with no checkout read
	Remote   bool   `json:"remote"`             // local collectors skipped
	// LocalOnly is a checkout with no origin remote: no host to read, so
	// the board is this machine's alone.
	LocalOnly bool `json:"local_only,omitempty"`
}

Project names the project the board is about.

type Quiet added in v0.39.0

type Quiet struct {
	Count  int       `json:"count"`
	Oldest time.Time `json:"oldest,omitzero"` // the oldest one's creation time
}

Quiet is the open requests with no row in the window: no commits, reviews or status changes there.

type RelatedPR added in v0.44.0

type RelatedPR struct {
	Number   int    `json:"number"`
	URL      string `json:"url,omitempty"`
	State    string `json:"state"` // open, merged or closed
	Relation string `json:"relation"`
}

A RelatedPR is one of a task's earlier requests, beside the row's own.

type ReviewFacts

type ReviewFacts struct {
	Step             string `json:"step,omitempty"` // the step the state block names
	Round            int    `json:"round,omitempty"`
	Open             int    `json:"open,omitempty"`
	Asks             int    `json:"asks,omitempty"`
	Approvals        int    `json:"approvals,omitempty"`
	ChangesRequested int    `json:"changes_requested,omitempty"`
	RemoteOpen       bool   `json:"remote_open,omitempty"` // the declared remote reviewer left one unhandled
}

ReviewFacts is what the row's reviews add up to.

type Row

type Row struct {
	Key          string       `json:"key"` // the task id, else `#<number>`
	Task         *Task        `json:"task,omitempty"`
	Stage        string       `json:"stage"`
	StageFrom    string       `json:"stage_from"`
	Who          Who          `json:"who"`
	Where        Where        `json:"where"`
	Review       *ReviewFacts `json:"review,omitempty"`
	BlockedBy    []string     `json:"blocked_by,omitempty"`
	LastActivity time.Time    `json:"last_activity,omitzero"`
	Next         Next         `json:"next"`
}

A Row is one task, or one request naming no task.

type Source

type Source struct {
	Name    string   `json:"name"`
	OK      bool     `json:"ok"`
	Skipped bool     `json:"skipped,omitempty"`
	Error   string   `json:"error,omitempty"`
	Notes   []string `json:"notes,omitempty"`
	TookMS  int64    `json:"took_ms"`
}

A Source is one collector's result: ok with its facts in, failed with its error and nothing from it, or skipped on purpose.

type Task

type Task struct {
	ID       string `json:"id"`
	Title    string `json:"title,omitempty"`
	URL      string `json:"url,omitempty"`
	Status   string `json:"status,omitempty"`
	Priority string `json:"priority,omitempty"`
	Assignee string `json:"assignee,omitempty"`
	After    string `json:"after,omitempty"` // the base its body names
	// Created is when the tracker created it, where the tracker says.
	Created time.Time `json:"created,omitzero"`
}

A Task is a tracker row.

type Tip added in v0.39.0

type Tip struct {
	Branch string
	At     time.Time
}

A Tip is a local branch's last commit, what a checkout with no host shows in its feed.

type Where

type Where struct {
	PR        *PRRef `json:"pr,omitempty"`
	Branch    string `json:"branch,omitempty"`
	Worktree  string `json:"worktree,omitempty"`
	Workspace string `json:"workspace,omitempty"`
	// BranchLeft marks a merged row whose branch outlived its worktree
	BranchLeft bool `json:"branch_left,omitempty"`
}

Where the work is.

type Who

type Who struct {
	Agent       string   `json:"agent,omitempty"`        // the Herdr agent in its worktree
	AgentStatus string   `json:"agent_status,omitempty"` // idle, working, done
	Model       string   `json:"model,omitempty"`
	Author      string   `json:"author,omitempty"` // the request's author
	Assignees   []string `json:"assignees,omitempty"`
	Reviewers   []string `json:"reviewers,omitempty"`  // requested and not yet heard from
	Committers  []string `json:"committers,omitempty"` // the author emails on the row's commits
	// Person is the one name the row groups under: the tracker assignee's,
	// else the author's, else the first committer's, each through the
	// people map where one matches, else as the source spelled it.
	Person string `json:"person,omitempty"`
}

Who is working on the row, and who it waits on.

type Window

type Window struct {
	Since time.Time `json:"since"`
	// End closes the feed's span, exclusive; zero in the state view.
	End time.Time `json:"end,omitzero"`
	// Chosen is how the feed's span was picked: the window named
	// (`today`, `last-week`, a date) or the default's pick.
	Chosen string `json:"chosen,omitempty"`
	// Fallback is the default falling back to the last working day
	// because today has no activity yet.
	Fallback bool `json:"fallback,omitempty"`
}

Window is the period the board covers: the feed's span, or the time since which the state view's done rows count.

func ChooseWindow added in v0.39.0

func ChooseWindow(today, lastWorkday Window, facts ...Facts) Window

ChooseWindow is the default's span: today when the facts give today's feed a row, else the last working day, marked as the fallback. Activity that makes no row (a review comment) keeps the fallback. The facts are read from the last working day's start, so one collection answers both.

type Workspace

type Workspace struct {
	ID          string
	Label       string
	AgentStatus string
}

A Workspace is a Herdr workspace open on a worktree.

type Worktree

type Worktree struct {
	Path   string
	Branch string
	Ahead  int      // commits past its base
	Emails []string // the distinct author emails on those commits
}

A Worktree is one checkout of the repo.

Jump to

Keyboard shortcuts

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