lane

package
v0.5.2 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package lane resolves the board's columns.

A board is its lane directory: the lane files under <root>/.jaira/lanes/ are the board, in the order of the order file beside them, and nothing is added underneath. The shipped lanes are compiled into the binary as the catalogue's standing offer, together with the files under ~/.jaira/lanes; a board opened for the first time gets its files written from the default board's selection or from the shipped lanes, and from then on 'jaira lanes add' and 'remove' are what change it. Both kinds of file go through this same parser: a shipped lane is not a special case, it is a lane that happens to ship inside the executable.

A board's file that shares a shipped lane's id is that lane — the user owns the pipeline and nothing about it is off limits. The loader's one reservation is a protection the shipped lane carried and the file drops; that is reported, because it is exactly what the protection guards against. See Load.

Index

Constants

View Source
const (
	ContextFresh  = "fresh"
	ContextShared = "shared"
)

The two values a lane's context: field takes. See Lane.Context.

View Source
const Template = `` /* 2835-byte string literal not displayed */

Template is the skeleton a fresh lane file starts from: every field parse() reads, commented, plus a prompt body — a file this parses without error, so the template cannot rot away from the parser it feeds.

Shared by 'jaira lanes template' and the lane settings screen's 'n' key, so there is one skeleton to keep in sync with parse(), not two.

Variables

This section is empty.

Functions

func Add

func Add(root string, set *Set, id string) (string, string, []string, error)

Add brings a built-in or catalogue lane onto this board: the lane's file is written into the board's lane directory and its id placed where its after: field says it belongs. It refuses a lane already part of set — "already in this project" — rather than re-exporting it. The returned warnings say what the placement had to assume, the same way Load reports an anchor it could not resolve.

The second return is the id the lane now follows, empty when it went to the front. Placement is the whole point of the call and the one thing a caller cannot see: a chain resolved through uninstalled lanes lands 'jaira lanes add testing' between in-progress and human, and silently, because warning on the path the board itself advertises would be noise. So the neighbour is handed back to be said out loud instead.

func Bytes

func Bytes(l *Lane) ([]byte, error)

Bytes returns a lane's file content verbatim: from the embedded binary for a built-in, from disk for anything else. Callers that need to copy a lane (Export, Publish, Materialise) go through this rather than re-serialising the parsed struct, so unknown fields, comments and field order all survive — the file format is the API.

func ContextInvalidMessage added in v0.3.7

func ContextInvalidMessage(l *Lane) string

ContextInvalidMessage says what is wrong with a lane whose context: value is neither fresh nor shared, and what the board does with it meanwhile.

func DefaultBoardPath

func DefaultBoardPath() string

DefaultBoardPath is the per-user file that decides a new board's lanes and which ticket Options start ticked. $JAIRA_DEFAULT_BOARD overrides it — an explicit override rather than a sibling of UserLanesDir, because a test needs to point at a temp file and deriving a sibling path from an already-overridable directory breaks the day someone points JAIRA_LANES_DIR at something that is not <something>/lanes.

func Export

func Export(l *Lane, dstDir string, overwrite bool) (string, error)

Export copies a lane's file, verbatim, from wherever it was loaded from into dstDir — the catalogue-to-project move. It refuses to overwrite an existing file unless overwrite is true, so the caller (the lane settings screen) decides rather than silently clobbering a project's own copy.

func LoadOrder

func LoadOrder(root string) ([]string, error)

LoadOrder reads a project's column order file. An absent file is not an error: it means the project has not customised its column order, and callers fall back to today's after:-anchor-derived order.

func Materialise

func Materialise(root string, set *Set, ids []string) (written []string, warnings []string, err error)

Materialise writes the lanes named by ids — resolved against set, which is the offer Load("") returns — into <root>/.jaira/lanes/ as files, and writes the order file in that same order. Load calls it the first time a board is opened with no lanes of its own; 'jaira init' therefore needs nothing more than to load the board it just created. An id set does not have is a warning, not a failure: one unknown name in a default board must not leave a board without lanes.

func Move

func Move(ids []string, id string, delta int) []string

Move returns a copy of ids with id shifted delta positions — -1 is one step toward the front, +1 one step toward the back. It is the user's model exactly: moving a lane one step swaps it with its neighbour. Moving past either end is a no-op, not an error and not a wrap-around: asking to move further than the board allows is not the same as asking for a mistake.

func MoveLane

func MoveLane(root string, set *Set, id string, delta int) error

MoveLane shifts id one step in this project's column order and writes the order file — the one implementation the CLI's 'lanes move' and the settings screen's H/L keys both call, so "move a lane" never has two bodies to drift apart.

func ProjectLanesActive

func ProjectLanesActive(root string) bool

ProjectLanesActive reports whether root's project lane directory is currently authoritative per D-03: present and holding at least one lane file. Load and 'jaira lanes path' both need this same test, so it lives here once rather than as two copies that could drift apart.

func ProjectLanesDir

func ProjectLanesDir(root string) string

ProjectLanesDir is where a project scopes itself down to the lanes it actually uses. Per D-03 it is authoritative when present and non-empty: it replaces the catalogue as this project's second lane source rather than adding to it, so a project directory holding only three lane files means a three-lane board, not the catalogue plus three overrides.

func Publish

func Publish(l *Lane, dstDir, who string, overwrite bool) (string, error)

Publish copies a lane's file into dstDir (normally Store.SharedDir()/identity.Slug(who)), stamping creator: with who when the file does not already declare one. It refuses to overwrite an existing file unless overwrite is true.

func RefreshDrift

func RefreshDrift(d DriftEntry) error

RefreshDrift pulls the catalogue copy into the project, in the direction the user asked for — nothing syncs on its own, per D-02.

func Remove

func Remove(root string, set *Set, store *ticket.Store, id string) (string, error)

Remove takes a lane off this board — never out of the catalogue — refusing when any ticket currently sits in it, naming them: a lane that vanishes under a ticket leaves it in a lane nothing knows. The board is its lane directory, so removing is deleting the file and its line in the order file; a shipped lane stays on offer in the catalogue and 'jaira lanes add' brings it back.

func ResolveOptions

func ResolveOptions(set *Set, board *DefaultBoard) []ticket.BodyOption

ResolveOptions turns a default board's option choices into the ticked/ unticked list a new ticket's Options checklist needs, against the options this set's installed lanes actually declare. An option the board names that no lane requires is silently dropped here — 'jaira lanes' reporting that typo is Validate's job, not this function's.

func SaveDefaultBoard

func SaveDefaultBoard(b *DefaultBoard) error

SaveDefaultBoard writes b to b.Path with lanes: and options: in a fixed order, so a saved file diffs predictably. The prose body is preserved verbatim if the file already had one.

func SaveOrder

func SaveOrder(root string, ids []string) error

SaveOrder writes a project's column order file, one id per line.

func Settle added in v0.1.1

func Settle(s *ticket.Store, l *Lane, folder, moved string, prepare func(*ticket.Ticket) error) (trimmed []ticket.Trimmed, filed bool, err error)

Settle applies a lane's file rules after a move landed a ticket there — the one place that knows which rule wins. A doorway (logbook-on-entry) files the whole lane into the logbook folder, prepare running on each ticket first (the callers stamp commits there); a capped lane (holds) trims the oldest beyond the newest Holds, never the just-moved ticket. filed reports which rule ran, so a caller can word its message. Both UIs call this instead of carrying the switch themselves — four status write-sites were one too many places for the same decision.

func UserLanesDir

func UserLanesDir() string

UserLanesDir is where custom lane files live. This is the catalogue: every lane a user has built or adopted, independent of any one project.

func Validate

func Validate(board *DefaultBoard, set *Set) []string

Validate checks a default board against the currently installed lanes, returning warnings in the same shape core/lane's own loader already uses: an unknown lane id, an unknown option, or (carried over from LoadDefaultBoard) an unparseable file. There is one warning channel, already surfaced by 'jaira lanes', --json, the TUI warnings block and every command through loadEnv — a second reporting path for the same class of problem would be the mistake here.

board.Warnings is included as-is rather than re-derived: LoadDefaultBoard is the only place that knows whether the file failed to parse.

Types

type DefaultBoard

type DefaultBoard struct {
	Lanes    []string
	Options  []string
	Path     string
	Warnings []string
	// Body is the prose below the frontmatter, preserved across a save so
	// SaveDefaultBoard never erases a note the user left themselves.
	Body string
}

DefaultBoard is a per-user setting: which lanes a freshly initialised board gets, and which ticket Options start ticked. An absent file, an absent lanes: field, or an empty list all mean the same thing — the built-ins — which is easy to get backwards, so it is stated here rather than only in the file format.

func LoadDefaultBoard

func LoadDefaultBoard() (*DefaultBoard, error)

LoadDefaultBoard reads DefaultBoardPath(). No file at all is the normal state, not a missing-configuration error, and returns a zero board. A file that exists but does not parse is warned about and treated the same as absent, rather than taking 'jaira init' or 'jaira lanes' down over one bad file — a teammate's shared default board naming their own lane, or a typo, must not break your board.

type DriftEntry

type DriftEntry struct {
	ID            string
	ProjectPath   string
	CataloguePath string
}

DriftEntry names one lane whose project copy no longer matches its catalogue copy of the same id.

func Drift

func Drift(root string, set *Set) ([]DriftEntry, error)

Drift implements D-02: it compares every lane this project's directory supplies against a catalogue file of the same id, and reports the ones whose bytes differ. Editing a lane writes through to the catalogue immediately, so it is the project's copy that goes stale, not the other way round — nothing here fixes that automatically; see RefreshDrift.

Checked only when the caller asks (the lane settings screen, on open), per the decided design: checking on every command would mean every invocation of the loader pays for a comparison most callers never look at.

type Lane

type Lane struct {
	ID          string
	Name        string
	Description string

	// After anchors ordering to another lane's ID. Anchoring is used instead of
	// a numeric position because two independently written lane files would
	// otherwise both claim the same slot.
	After string

	// Precedence orders lanes by how far along the work is, and is what a merge
	// consults so concurrent edits never revert forward progress. It is
	// deliberately separate from display order: Blocked appears last on the
	// board but must not outrank active work in a merge.
	Precedence int

	// Agentic marks a lane whose work is performed by a subagent.
	Agentic   bool
	Terminal  bool
	ModelTier string

	// Context says whether the agent working this lane may be the one that
	// worked the lanes before it. "fresh" means it may not: the lane is handed
	// to a new agent that has seen nothing of the ticket's work, because a
	// judge who wrote what it judges is not a judge. "shared" says the
	// opposite out loud, and empty means the file does not say. A single
	// agent carries a ticket through every other agentic lane itself.
	//
	// Load resolves the empty case for the whole board: when no lane declares
	// a context at all, the last agentic lane before the first lane a person
	// owns is fresh, with ContextImplied set — so a board written before the
	// field existed keeps a fresh review without a migration. See
	// impliedFresh.
	Context        string
	ContextImplied bool
	// ContextInvalid holds a context: value that is neither fresh nor shared —
	// a typo like "Fresh". Such a lane still loads and runs fresh, but it does
	// not count as having declared a context: the board's default still
	// applies when no lane carries a valid value, so a typo can add a fresh
	// lane and never take one away. Load warns about it and 'jaira validate'
	// fails on it.
	ContextInvalid string

	// Holds caps how many tickets the lane keeps. After a move lands a ticket
	// here, the oldest beyond the newest Holds leave for the logbook — reported,
	// never silent. 0 or absent means unlimited. Declared in the lane file so the
	// one rule that moves files can be read, changed or switched off there.
	Holds int

	// LogbookOnEntry files a ticket straight into the logbook the moment a
	// move lands it here — commits stamped first, reported, never silent. The
	// lane is a doorway, not a shelf: the logbook is the record of finished
	// work, so it gets everything immediately. Terminal lanes only.
	LogbookOnEntry bool

	// InputRequires names the ticket fields assembled into a subagent's bounded
	// input. OutputProduces names the fields it must return.
	InputRequires  []string
	OutputProduces []string

	RequiresQuestion       bool
	RequiresOutcome        bool
	RequiresNonModelSignal bool

	// RequiresBlockedReason means a ticket may not be parked here without saying
	// what it is waiting on. Parking is cheap and forgetting is silent, so this
	// is the one place the board charges for it.
	RequiresBlockedReason bool

	// RequiresCommits means a ticket cannot enter this lane without naming the
	// commits that carry its change. It sits on the done lane rather than on
	// the implementing lane's output contract, so work can move through review
	// uncommitted — but nothing is accepted that cannot be checked.
	RequiresCommits bool

	// RequiresHumanExit means no agent may move a ticket on out of this lane —
	// back to an earlier lane is allowed, since that accepts nothing. It is
	// the difference between a review step that is conventionally respected and
	// one that actually stops: an agent that decides its own work passed review
	// would make the step decorative.
	RequiresHumanExit bool

	// RequiresOption names an entry in a ticket's Options checklist. When set,
	// this lane is only part of a ticket's path if that option is ticked — which
	// is how one ticket is planned and reviewed while another goes straight to
	// implementation, without either lane being uninstalled.
	RequiresOption string

	// RejectsTo names the lanes a ticket goes back to when this lane finds the
	// work wanting: the loop's back edges, written down. Moving backwards has
	// always been allowed — the gate only checks a move that advances (see
	// core/gate) — but nothing declared where back was, so the route lived in
	// the prompt's prose and an agent reading the board could not see it.
	//
	// More than one is allowed because a rejection is not one kind of thing: a
	// critique that found a flaw sends the work back to be implemented, while a
	// critique that found a decision to make hands it to a person instead. Both
	// are the same lane's back edge and a reader has to be able to see both.
	//
	// It is documentation the tool validates, not a rail: nothing forces a
	// rejection to go here, exactly as nothing forces a lane's prompt to be
	// followed.
	RejectsTo []string

	// RequiresSpecified marks the first lane, in precedence order, that sets
	// this: the boundary between thinking about a ticket and working on it.
	// Everything before it is a place a half-formed ticket may sit; nothing at
	// or after it may hold a ticket that is missing its promotion fields.
	RequiresSpecified bool

	// Prompt is the markdown body: the instruction given to the subagent.
	Prompt string

	// Creator names who wrote this lane, for provenance once a lane has been
	// adopted or copied between machines. A built-in with no creator: field
	// defaults to "jaira" rather than having the field written into all nine
	// shipped files; a custom lane with no creator: field is left empty, since
	// "shipped by the tool" and "author unknown" are different facts.
	Creator string

	// Builtin distinguishes shipped lanes from user-installed ones: it says
	// the lane travels inside the binary, nothing more.
	Builtin bool
	// Default says the lane belongs to a fresh board's selection. It is the
	// second half of what Builtin used to carry alone: a shipped lane that is
	// installable without the network but must not be forced on every board —
	// critique, optimize, testing — ships with "default-board: false" and is
	// built-in without being default.
	Default bool
	// Source is the path a custom lane came from, for error messages.
	Source string
	// Overrides names the built-in this lane displaced, empty if it does not
	// override anything. Set by Load, not by parse: overriding is a fact about
	// resolution, not about the file itself.
	Overrides string
	// Unknown marks a synthetic passthrough lane created for a ticket whose
	// status names a lane this installation does not have.
	Unknown bool
}

Lane is one column of the board.

func Adopt

func Adopt(srcPath, dstDir string, overwrite bool) (*Lane, string, error)

Adopt parses srcPath first — an unparseable file must never land in the catalogue — then copies its bytes verbatim into dstDir under the parsed lane's own id, never the source filename. It refuses to overwrite an existing file unless overwrite is true.

func Builtins

func Builtins() ([]*Lane, error)

Builtins returns the seven lanes shipped in the binary.

func Installable

func Installable(set *Set) ([]*Lane, error)

Installable lists every built-in and catalogue lane not already part of set — the "add a lane to this project" catalogue, for both 'jaira lanes add' and the settings screen's '+' column. A file that fails to read or parse is skipped rather than failing the whole listing, matching Shared's treatment of the same class of problem.

func Parse added in v0.1.1

func Parse(b []byte, source string) (*Lane, error)

Parse reads one lane definition from bytes that did not come from a file on this machine — a marketplace download, say — naming source in errors. It is parse without the built-in flag: nothing arriving this way ships in the binary.

func Passthrough

func Passthrough(id string) *Lane

Passthrough synthesises a read-only column for a status no installed lane claims. Hiding such tickets would be a worse failure than showing an inert column: the ticket still exists and the user needs to know it does.

func (*Lane) OnPath added in v0.3.7

func (l *Lane) OnPath(t *ticket.Ticket) bool

OnPath reports whether a ticket walking forward would step into this lane: not a parking lane, not a question-only checkpoint, and not a lane whose option the ticket leaves unticked. It is the rule Next applies, so that anything else asking "would this ticket have gone through here" gives the same answer.

type Set

type Set struct {
	Lanes []*Lane

	// Warnings records non-fatal problems (a custom lane anchored to a lane that
	// is not installed, for instance) so the caller can surface them without
	// failing to draw a board.
	Warnings []string
	// contains filtered or unexported fields
}

Set is the resolved, ordered collection of lanes.

func Load

func Load(root string) (*Set, error)

Load resolves this board's lanes.

A board is its lane directory. When <root>/.jaira/lanes/ holds lane files, those files are the board — all of it, and nothing is added to them. The built-ins are compiled into the binary as the catalogue's standing offer (Installable lists them beside ~/.jaira/lanes); 'jaira lanes add' copies one onto a board, and nothing else does. The directory is per person and gitignored, so two people on one shared board may run two different boards.

A board whose lane directory is empty is set up on first load: the default board's selection, or the built-ins when there is none, are written into the directory as files with an order file — once, reported in Warnings. This is the one place a read writes. It was asked for: a board must never sit half-configured, and the next command reads files rather than a fallback. Boards from before this rule are brought over by migrateLegacy.

A lane file the board already holds is never rewritten from the built-ins. The single exception is a named, once-only, one-field correction of a defect a shipped lane carried, applied only to a file still recognisable as that shipped lane — see correction. applyCorrections runs those and says so on stderr rather than in Warnings, because a correction speaks once and most callers here never read Warnings.

An empty root, or a root with no .jaira/ at all, means no board is in hand — the launcher spans many, a test has none, 'jaira lanes template' needs the shape only — and Load returns the offer: built-ins plus the catalogue.

A file whose id matches a shipped lane is that lane, not an override of anything: nothing is on the board to override. It is still compared with the shipped lane of that name, for one reason — if it drops a protection the shipped lane carried (requires-human-exit, terminal, requires-outcome, requires-nonmodel-signal) a warning names which, because those exist to stop an agent accepting its own work, and losing one silently is exactly what they guard against.

func (*Set) Columns

func (s *Set) Columns(statuses []string) []*Lane

Columns returns the lanes to draw for a given set of ticket statuses, appending passthrough columns for any status with no matching lane. Unknown lanes are sorted alphabetically among themselves so the board is deterministic.

func (*Set) Default

func (s *Set) Default() *Lane

Default is the lane new tickets enter.

func (*Set) Facts added in v0.3.7

func (s *Set) Facts() []board.LaneFact

Facts reads a board's pipeline into the plain form core/board renders into the agent note. A nil set gives nil, so a caller without a board writes the note it wrote before lanes were in it.

The translation lives here rather than in core/board because that package is stdlib-only on purpose, and here rather than in the CLI and the TUI because both write the same note: two copies had already drifted once a lane fact was added to one of them and not the other.

func (*Set) Get

func (s *Set) Get(id string) (*Lane, bool)

Get returns a lane by ID.

func (*Set) IDs

func (s *Set) IDs() []string

IDs lists lane IDs in display order.

func (*Set) Index

func (s *Set) Index(id string) int

Index reports a lane's display position, or -1.

func (*Set) Next added in v0.1.1

func (s *Set) Next(t *ticket.Ticket) *Lane

Next reports the lane this ticket goes to when the step it is in is finished.

It exists because the route was only ever implicit. The column order carries it, and a ticket's Options decide which optional steps are part of that ticket's path at all — so an agent wanting to know "where does this go next" had to load the lane set, work out the order, and apply the ticket's Options itself. Every agent doing that separately is the same derivation written four times, each free to get it wrong.

Three kinds of lane are never the answer:

  • a lane this ticket opted out of (RequiresOption not ticked): it is not on this ticket's path, and moving into it is refused by the gate.
  • a parking lane (RequiresBlockedReason): being blocked is a thing that happens to a ticket, not the next step in its work.
  • a human checkpoint that only asks a question (RequiresQuestion): a ticket goes there because something stopped, not because it advanced.

A ticket that is itself parked or waiting on an answer also has no next lane. It resumes where it stopped, and nothing on the board records where that was — so walking forward from there would send work past the very steps that raised the question. Measured against a real board: a ticket in HITL reported "review" and would have skipped critique and optimize.

A ticket in a terminal lane, in an unknown lane, or in the last lane on its path has no next lane either, and nil says so. Nothing enforces the answer: 'jaira move' re-checks the gates whatever route the caller took.

func (*Set) Options

func (s *Set) Options() []string

Options lists the distinct RequiresOption values declared by the installed lanes, in display order. A ticket's Options checklist is derived from what is actually installed rather than a fixed list, so a user-written optional lane shows up in it too, without the loader needing to know its name.

func (*Set) Precedence

func (s *Set) Precedence(id string) int

Precedence returns a lane's merge precedence. An unknown lane sorts below everything known, so a merge never promotes a ticket into a lane this installation cannot reason about.

func (*Set) Terminal

func (s *Set) Terminal() *Lane

Terminal is the lane a signed-off ticket lands in — the first lane declaring itself terminal, which is where accepting work moves it to.

type SharedLane

type SharedLane struct {
	Lane   *Lane
	Folder string
	Path   string
}

SharedLane is one lane found under a project's .jaira/shared/ tree, alongside the folder (normally the publisher's identity.Slug) it came from.

func Shared

func Shared(root string) (lanes []SharedLane, warnings []string, err error)

Shared walks <root>/.jaira/shared/*/*.md and returns every lane it can parse, alongside the folder it came from and its path. A file that fails to read or parse is skipped and reported in warnings rather than failing the walk — one teammate's broken file must not hide everyone else's lanes.

Shared lanes are never loaded by Load: they are visible here, adoptable through Adopt, and otherwise inert. That separation is the whole security story for T-5-02 — a committed shared lane is untrusted prompt content that would otherwise run at whatever model-tier it declares — so nothing should ever wire this function's result into Load.

Jump to

Keyboard shortcuts

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