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
- Variables
- type Board
- func (b *Board) Activate(id int, force bool) (*card.Card, error)
- func (b *Board) Board() (*BoardView, error)
- func (b *Board) CardDir(id int, slug string) string
- func (b *Board) CardFile(dirName string) string
- func (b *Board) CardsDir() string
- func (b *Board) Done(id int, force bool) (*card.Card, error)
- func (b *Board) EpicAdd(epicID, cardID int, force bool) error
- func (b *Board) EpicList() ([]EpicProgress, error)
- func (b *Board) EpicShow(id int) (*EpicProgress, error)
- func (b *Board) FindCardDir(id int) (string, error)
- func (b *Board) Kill(id int, _ bool) (*card.Card, error)
- func (b *Board) List(opts ListOpts) ([]index.Entry, error)
- func (b *Board) LoadCard(id int) (*card.Card, string, error)
- func (b *Board) LoadConfig() (Config, error)
- func (b *Board) NewCard(title string, opts NewCardOpts) (*card.Card, string, error)
- func (b *Board) Park(id int, force bool) (*card.Card, error)
- func (b *Board) Reindex() (*index.Index, error)
- func (b *Board) Revive(id int, force bool) (*card.Card, error)
- func (b *Board) SetBody(id int, body string) error
- type BoardView
- type Config
- type EpicProgress
- type ListOpts
- type NewCardOpts
Constants ¶
const CardFileName = "INDEX.md"
CardFileName is the required markdown file inside a card directory. Optional artifacts (designs, screenshots, logs) sit alongside it.
const CardsDirName = "cards"
CardsDirName is the per-board directory under .focus/ that holds each card folder. Always relative to .focus/.
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.
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".
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
Board returns the default board view: active cards, backlog cards, and epics. Read-only.
func (*Board) CardDir ¶
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 ¶
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) Done ¶
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 ¶
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 ¶
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 ¶
Kill transitions any status → archived. The "force" flag is ignored because kill is always allowed by design.
func (*Board) List ¶
List returns the cards matching opts, ordered by id. Read-only; callers do not need to hold the lock.
func (*Board) LoadCard ¶
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 ¶
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 ¶
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) Reindex ¶
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".
type BoardView ¶
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 ¶
EffectiveWIPLimit returns the WIP limit for this board: the config override if positive, else DefaultWIPLimit.
type EpicProgress ¶
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. |