board

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: May 4, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package board is the shared logic layer of focus.

Both the CLI (internal/cli) and the MCP server (internal/mcp) are thin wrappers over this package. Card mutations, status transitions, next_id allocation, and index updates all live here so the two surfaces never drift — see designs/focus-issue-001.md §"CLI/MCP shared logic placement".

Operations that mutate state acquire the .focus/.lock flock for the duration of the read-modify-write cycle. The Open() entry point returns a *Board which carries the .focus/ path for the resolved board; callers ask the Board to do things rather than wiring paths through every call.

Index

Constants

View Source
const CardFileName = "INDEX.md"

CardFileName is the required markdown file inside a card directory. Optional artifacts (designs, screenshots, logs) sit alongside it.

View Source
const CardsDirName = "cards"

CardsDirName is the per-board directory under .focus/ that holds each card folder. Always relative to .focus/.

View Source
const ConfigFileName = "config.yaml"

ConfigFileName is the per-board config file under .focus/. v0.1.0 writes it as an empty placeholder; future features fill it in.

View Source
const DefaultWIPLimit = 3

DefaultWIPLimit is the WIP cap applied when no config override is present. 3 is the v1 default and matches the kanban literature on solo developers; experimentally it's "you can only really focus on 3 things in flight at once".

View Source
const FocusDirName = ".focus"

FocusDirName is the per-project board directory that focus walks for, the way git walks for ".git". See designs/focus-v2.md §"Project-local boards".

Variables

View Source
var ErrNotInBoard = errors.New("not in a focus board (no .focus/ found in this directory or any ancestor); run `focus init`")

ErrNotInBoard is returned by Open when no .focus/ directory is found in $PWD or any ancestor. CLI prints a helpful message; MCP surfaces it as a tool error.

View Source
var ErrWIPLimit = errors.New("WIP limit reached")

ErrWIPLimit is returned by Activate when activating would exceed the board's WIP limit and force=false. CLI prints a hint about --force; MCP surfaces it as a tool error.

Functions

This section is empty.

Types

type Board

type Board struct {
	// Root is the project root — the directory that contains .focus/.
	Root string
	// Dir is the absolute path to .focus/ itself.
	Dir string
}

Board is a resolved focus board. Holds the absolute path to the .focus/ directory. Cheap to construct; safe to pass around.

func Init

func Init(root string) (*Board, error)

Init creates a .focus/ directory at root with the bare-minimum layout: empty config.yaml + empty cards/ dir. Per designs/focus-issue-001.md §"`focus init` minimal state", we deliberately do NOT create index.json (first `focus new` writes it), .lock (created on demand), starter cards, or a README.

Idempotent: running Init on an existing board is a no-op that returns the existing Board.

func Open

func Open(startDir string) (*Board, error)

Open walks from startDir up the directory tree looking for a .focus/ directory. Returns ErrNotInBoard if none is found before hitting the filesystem root.

startDir may be relative; we resolve to an absolute path first so the walk terminates predictably.

func (*Board) Activate

func (b *Board) Activate(id int, force bool) (*card.Card, error)

Activate transitions backlog → active. Enforces the board's WIP limit unless force=true. The check counts active cards (excluding the one being activated) under the same lock as the transition so two concurrent `focus activate` calls can't both squeak past the limit.

func (*Board) Board

func (b *Board) Board() (*BoardView, error)

Board returns the default board view: active cards, backlog cards, and epics. Read-only.

func (*Board) CardDir

func (b *Board) CardDir(id int, slug string) string

CardDir returns the absolute path to a card's directory given its id and slug. The slug is folder-only signage and is taken verbatim; the caller is responsible for having normalized it via card.Slugify.

func (*Board) CardFile

func (b *Board) CardFile(dirName string) string

CardFile returns the absolute path to a card's INDEX.md given the already-known directory name. Used by handlers that have already looked up the dir from the index.

func (*Board) CardsDir

func (b *Board) CardsDir() string

CardsDir returns the absolute path to <board>/.focus/cards/.

func (*Board) Done

func (b *Board) Done(id int, force bool) (*card.Card, error)

Done transitions active → done. Contract enforcement is the caller's job (the CLI prompts on tty; the MCP just transitions); pass force=true to skip validation entirely.

func (*Board) EpicAdd

func (b *Board) EpicAdd(epicID, cardID int, force bool) error

EpicAdd sets the epic field on a card to point at the given epic id. Validates that epicID names an existing card with type:epic unless force is true.

func (*Board) EpicList

func (b *Board) EpicList() ([]EpicProgress, error)

EpicList returns all epics in the board ordered by id, regardless of status. Useful for `focus epic list`.

func (*Board) EpicShow

func (b *Board) EpicShow(id int) (*EpicProgress, error)

EpicShow returns the epic itself plus a child-status histogram. Errors if id doesn't refer to an existing epic.

func (*Board) FindCardDir

func (b *Board) FindCardDir(id int) (string, error)

FindCardDir locates a card's directory on disk by id. The directory name is "<padded-id>-<slug>" but the slug is unknown to the caller (we don't store it in frontmatter), so we glob for the prefix.

Returns the directory name (e.g. "0142-ship-the-feature") relative to the cards/ dir, suitable for passing to CardFile.

func (*Board) Kill

func (b *Board) Kill(id int, _ bool) (*card.Card, error)

Kill transitions any status → archived. The "force" flag is ignored because kill is always allowed by design.

func (*Board) List

func (b *Board) List(opts ListOpts) ([]index.Entry, error)

List returns the cards matching opts, ordered by id. Read-only; callers do not need to hold the lock.

func (*Board) LoadCard

func (b *Board) LoadCard(id int) (*card.Card, string, error)

LoadCard reads a card by id from disk, including its body. Used by `focus show`, `focus edit`, and any MCP tool that needs body content. Read-only — no lock taken.

func (*Board) LoadConfig

func (b *Board) LoadConfig() (Config, error)

LoadConfig reads .focus/config.yaml. An empty or missing file returns a zero-value Config — that's the supported "no override" state, not an error.

func (*Board) NewCard

func (b *Board) NewCard(title string, opts NewCardOpts) (*card.Card, string, error)

NewCard creates a new card on disk and updates the index. Acquires the .focus/.lock for the duration of allocate-id → write-card → write-index so concurrent `focus new` (or MCP equivalent) calls can't double-allocate or corrupt the index.

Returns the in-memory Card with id and uuid populated, plus the directory name relative to .focus/cards/.

func (*Board) Park

func (b *Board) Park(id int, force bool) (*card.Card, error)

Park transitions active → backlog.

func (*Board) Reindex

func (b *Board) Reindex() (*index.Index, error)

Reindex walks .focus/cards/ and rewrites index.json from scratch. Use after hand-edits, git merges, or any state-bypassing operation. Preserves the previous next_id high-water mark per designs/focus-v2.md §"Recovery".

func (*Board) Revive

func (b *Board) Revive(id int, force bool) (*card.Card, error)

Revive transitions archived → backlog.

func (*Board) SetBody

func (b *Board) SetBody(id int, body string) error

SetBody replaces a card's markdown body, leaving frontmatter untouched. Used by the MCP focus_edit_body tool — the CLI doesn't expose this directly because `focus edit` opens $EDITOR for interactive editing.

type BoardView

type BoardView struct {
	Active  []index.Entry
	Backlog []index.Entry
	Epics   []index.Entry
}

BoardView is the active+backlog snapshot used by `focus board` and the TUI's default view. Done and archived are excluded by design.

type Config

type Config struct {
	WIPLimit int `yaml:"wip_limit"`
}

Config holds per-board configuration loaded from .focus/config.yaml. Empty file → zero-value Config which the rest of the package reads as "use defaults". v0.1.0 only supports wip_limit; future fields (default project, theme override, etc.) get added here.

func (Config) EffectiveWIPLimit

func (c Config) EffectiveWIPLimit() int

EffectiveWIPLimit returns the WIP limit for this board: the config override if positive, else DefaultWIPLimit.

type EpicProgress

type EpicProgress struct {
	Epic    index.Entry
	Active  int
	Backlog int
	Done    int
	Archive int
}

EpicProgress is the progress summary for one epic: how many child cards are in each status. Used by `focus epic <id>`.

func (EpicProgress) Total

func (p EpicProgress) Total() int

Total returns the combined child-card count.

type ListOpts

type ListOpts struct {
	Status   card.Status
	Project  string
	Priority card.Priority
	Epic     *int
	Owner    string
	Tag      string
	Type     card.Type
}

ListOpts narrows a List call. Empty fields apply no filter; the CLI translates --project, --priority, etc. into these fields.

type NewCardOpts

type NewCardOpts struct {
	Project  string
	Priority card.Priority
	Type     card.Type
	Epic     *int
	Slug     string
}

NewCardOpts captures the optional fields callers may set when creating a card. Empty fields fall back to the defaults: priority p2, type card, status backlog, project = board's parent dir name.

Directories

Path Synopsis
Package card implements the card data model for focus.
Package card implements the card data model for focus.
Package index manages the .focus/index.json derived cache.
Package index manages the .focus/index.json derived cache.
Package lock wraps gofrs/flock to provide an advisory file lock on .focus/.lock for the duration of a mutating CLI/MCP operation.
Package lock wraps gofrs/flock to provide an advisory file lock on .focus/.lock for the duration of a mutating CLI/MCP operation.

Jump to

Keyboard shortcuts

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