Skip to content

Latest commit

 

History

History
683 lines (484 loc) · 24.1 KB

File metadata and controls

683 lines (484 loc) · 24.1 KB

Kan Specification

A CLI tool for managing kanban boards where all data lives as plain files.

Overview

Kan is a personal project management tool that stores kanban boards as plain files. There is no database, no server, no external dependencies — just files. Works with any VCS (or none).

Why This Approach

  • File-based: Full VCS history and offline support come free (with any VCS)
  • No vendor lock-in: Your data is plain files you control
  • Portable and scriptable: Standard file formats, easy to automate
  • Data lives where work lives: Boards live alongside the code they track

Use Case

Personal project management — a kanban board per repo for your various projects, versioned alongside the code.


Data Model

Directory Structure

.kan/
  boards/
    <board-name>/
      config.toml           # board-specific configuration
      cards/
        <flexid>.json       # one file per card

The .kan/ directory is the default location. Users MAY specify a custom location via kan init --location <relative-path>, which is persisted in the global user config.

Global User Config

Location: ~/.config/kan/config.toml

# Global defaults
editor = "vim"  # Falls back to $EDITOR, then "vim"

# Known projects (lazily populated by any kan command)
[projects]
my-project = "/Users/name/src/my-project"
another = "/Users/name/src/another"

# Per-repo settings
[repos."/Users/name/src/my-project"]
default_board = "features"
data_location = "tools/kanban"  # if custom location was used

The projects registry enables future -p flag functionality for managing boards from anywhere.

Board Config

Location: .kan/boards/<board-name>/config.toml

# Board identity
id = "k7xQ2m"  # flexid, immutable after creation
name = "features"

# Columns (ordered)
[[columns]]
name = "backlog"
color = "#6b7280"

[[columns]]
name = "next"
color = "#3b82f6"

[[columns]]
name = "in-progress"
color = "#f59e0b"

[[columns]]
name = "done"
color = "#10b981"

# Default column for 'kan add'
default_column = "backlog"

# Labels
[[labels]]
name = "bug"
color = "#ef4444"
description = "Something is broken"

[[labels]]
name = "enhancement"
color = "#8b5cf6"

# Custom field schemas
[custom_fields.priority]
type = "enum"
values = ["low", "medium", "high"]

[custom_fields.assignee]
type = "string"

[custom_fields.due_date]
type = "date"

Default columns when creating a new board: backlog, next, in-progress, done.

Card Schema

Location: .kan/boards/<board-name>/cards/<flexid>.json

{
  "id": "k7xQ2m",
  "alias": "fix-login-bug",
  "alias_explicit": false,
  "title": "Fix login bug",
  "description": "Users are getting logged out randomly after 5 minutes.",
  "column": "in-progress",
  "labels": ["bug"],
  "parent": "m3Yp8n",
  "creator": "amterp",
  "created_at_millis": 1704307200000,
  "updated_at_millis": 1704393600000,
  "comments": [
    {
      "id": "c_9kL2x",
      "body": "Might be related to the session token changes from last week.",
      "author": "amterp",
      "created_at_millis": 1704310800000
    }
  ],
  "priority": "high"
}

Field Definitions

Field Required Description
id Yes Unique identifier generated via flexid
alias Yes Human-friendly slug, auto-generated from title by default
alias_explicit Yes Boolean. If false, alias updates when title changes. If true, alias is frozen until explicitly changed or cleared.
title Yes Card title
description No Detailed description (may contain markdown)
column Yes Current column (must reference a column defined in board config)
labels No Array of label names (must reference labels defined in board config)
parent No ID of parent card (for subtask relationships). MAY reference cards in other boards within the same repo.
creator Yes Username of card creator
created_at_millis Yes Creation timestamp in milliseconds since Unix epoch
updated_at_millis Yes Last update timestamp in milliseconds since Unix epoch
comments No Array of comment objects
(custom fields) No Any fields defined in board's custom_fields config

Alias Behavior

  • Auto-generated by slugifying the title (lowercase, spaces to hyphens, etc.)
  • On collision, auto-suffix with incrementing number: fix-bug, fix-bug-2, fix-bug-3
  • Frontends SHOULD allow users to explicitly set an alias, which sets alias_explicit: true
  • Frontends SHOULD allow users to clear an explicit alias, reverting to auto-generation
  • Card references in data (e.g., parent field) MUST use the card ID, not alias

Comment Schema

Field Required Description
id Yes Unique identifier (flexid, prefixed with c_)
body Yes Comment text
author Yes Username of comment author
created_at_millis Yes Creation timestamp in milliseconds since Unix epoch

CLI Commands

Written in Go using ra for command-line parsing.

First Take (MVP)

kan init

Initialize Kan in the current directory.

kan init [--location <relative-path>]
  • Creates .kan/ directory (or custom location if specified)
  • Creates a default board named main with default columns
  • Registers the project in global user config

If --location is specified and the path already exists with valid Kan data, registers the existing project without re-initializing.

kan board

Manage boards.

kan board create <name>    # Create a new board with default columns
kan board list             # List all boards in the repo

kan add

Add a new card.

kan add <title> [description] [flags]

Positional arguments:

  • title (required): Card title
  • description (optional): Card description

Flags:

  • -b, --board <name>: Target board
  • -c, --column <name>: Target column (defaults to board's default_column)
  • -l, --label <name>: Add label (repeatable)
  • -p, --parent <id|alias>: Set parent card
  • -I, --non-interactive: Fail instead of prompting for missing required fields

Behavior:

  • Interactive by default: if any required fields are missing, prompts the user
  • With -I: fails with an error if required fields are missing
  • Board selection:
    • If only one board exists, uses that board
    • If multiple boards exist and -b not specified, uses configured default_board
    • If multiple boards exist, no -b, and no default configured, prompts (or fails with -I)
  • Column defaults to the board's configured default_column (first column if not configured)
  • Creator is automatically set (from $KAN_USER, git config user.name, or $USER)

kan edit

Edit an existing card.

kan edit <id|alias> [OPTIONS]

Options:

  • -b, --board <name> — Target board
  • -t, --title <value> — Set card title
  • -d, --description <value> — Set card description
  • -c, --column <name> — Move card to column
  • -l, --label <name> — Set labels (repeatable, replaces existing labels)
  • -p, --parent <id|alias> — Set parent card
  • -a, --alias <value> — Set explicit alias
  • -f, --field <key=value> — Set custom field (repeatable)

Behavior:

  • If any flags are provided, applies the changes and exits (non-interactive).
  • If no flags are provided, opens an interactive menu to select which field to edit, then opens $EDITOR for that field.
  • Use -l "" to clear all labels.

Editor resolution order (for interactive mode):

  1. editor from global user config
  2. $EDITOR environment variable
  3. vim

kan show

Display card details.

kan show <id|alias>

Displays all card fields in a readable format.

kan list

List cards.

kan list [flags]

Flags:

  • -b, --board <name>: Filter by board
  • -c, --column <name>: Filter by column

Future Roadmap

The following are planned but NOT part of the first take:

CLI Extensions

  • kan move <id|alias> <column>: Move card to a different column
  • kan archive <id|alias>: Delete card from working tree (recoverable via VCS history)
  • kan comment <id|alias> [text]: Add comment (opens $EDITOR if text not provided)
  • kan config: Manage board configuration (columns, labels, custom fields)
  • -p, --project <fuzzy-name>: Global flag to operate on a different registered project from anywhere

Relationships

  • "Related to" links between cards (bidirectional)
  • "Blocked by" dependencies (directional)

Archival

  • kan archive search <query>: Search archived cards via VCS history

Frontends

Phase 1: Kan's file-based architecture means anyone can build frontends that read/write the .kan/ directory. The file format is the interface.

Phase 2: A web frontend will be embedded in the Go CLI binary and served via a kan command (e.g., kan web or kan serve). This provides a localhost browser interface out of the box.

Future: The same pattern could support additional embedded frontends (TUI, etc.) launched via CLI commands.


Design Decisions

One File Per Card

Decision: Each card is stored as a separate JSON file.

Rationale: VCS tools handle merges at the file level. With one file per card:

  • Adding cards rarely conflicts (different files)
  • Conflicts only occur when two people edit the same card — a real conflict worth surfacing
  • The alternative (JSONL with one card per line) causes spurious conflicts on concurrent additions

Trade-off: Directory with many files (hundreds to low thousands). Acceptable for tooling; ls becomes unwieldy but that's not the primary interface.

JSON for Data, TOML for Config

Decision: Card files are JSON. Configuration files are TOML.

Rationale:

  • JSON is structured and tooling-friendly for data that's primarily machine-read/written
  • TOML is more human-readable for configuration that users may hand-edit
  • Both are still editable in a pinch if needed

Flexid for Card IDs

Decision: Use flexid for generating card IDs.

Rationale: Short, low-collision identifiers that are reasonable to type in CLI commands. Not fully human-memorable, hence the alias system.

Alias as Derived Field

Decision: Aliases are auto-generated from titles but can be explicitly overridden.

Rationale: Provides human-friendly card references (fix-login-bug) without coupling filesystem structure to mutable data. The ID remains stable; the alias is a convenience layer.

Comments Embedded in Card

Decision: Comments are stored as an array within the card JSON, not as separate files.

Rationale: Simpler model. A card is the atomic unit of work. Comment volume per card is expected to be low (personal project management, not team collaboration).

Board as Directory

Decision: Each board is a subdirectory with its own config and cards. Boards have a flexid (stored in config) and a name (used as directory name).

Rationale: Clean isolation. Board-specific configuration lives with the board. Deleting a board is deleting a directory. Cross-board references are still possible via card IDs. The flexid provides a stable internal identifier if needed for future features.

Global Project Registry

Decision: Kan maintains a registry of known projects in ~/.config/kan/config.toml, lazily populated by any kan command run in a Kan-enabled project.

Rationale: Enables future -p flag for operating on projects from anywhere. No explicit registration step needed.

No Plugin Architecture

Decision: Frontends are not plugins. They're independent tools that read/write Kan's file format.

Rationale: The file format is the API. Anyone can build a TUI, web UI, or integration by reading .kan/ files. No need for Kan to define a plugin interface.


Card Fields Architecture Revamp

This section documents a planned architectural change to how card fields work. The current implementation (described above) treats labels as a first-class field. This revamp generalizes the approach.

Motivation

The trigger: Kan's default labels (feature, bug, enhancement, chore) are mutually exclusive—a card is a bug OR a feature, not both. But labels are multi-select by design. This revealed a category error:

  • Type (bug/feature/chore): "What kind of thing is this?" → single-select, categorical
  • Labels (blocked/urgent/needs-review): "What attributes does this have?" → multi-select, combinable

The defaults were steering users to misuse labels as types.

The deeper issue: we conflated "commonly used" with "architecturally special." Labels are common but not fundamentally different from any other multi-select enum field. Making labels first-class (dedicated CLI flag, special config section, hardcoded frontend rendering) means every future "commonly used" field faces the same question: "Is this special enough to be first-class?"

The solution: Shrink first-class fields to truly structural/system things. Everything else—including what labels currently is—becomes a custom field with a consistent schema. Board configuration controls how custom fields render on cards.

Field Taxonomy

After the revamp, fields fall into two categories:

Core Fields (First-Class)

These are structural or system fields that exist on every card:

Category Fields Description
Identity id, alias, alias_explicit Immutable identifiers and human-friendly references
Content title, description User-provided content, always present
Workflow column Structural—determines board position
Hierarchy parent Structural—enables subtask relationships
Discussion comments Nested objects, not a simple field type
System creator, created_at_millis, updated_at_millis, _v Auto-managed metadata

Core fields have dedicated schema, validation, and (where applicable) CLI flags. They are not configurable per-board.

Custom Fields

Everything else is a custom field defined in the board's configuration:

  • What the current labels field does becomes a custom field with type enum-set
  • Type distinction (bug/feature/task) becomes a custom field with type enum
  • Any user-defined fields (priority, assignee, due date, etc.) use the same system

Custom fields are:

  • Defined per-board in [custom_fields.*] config sections
  • Stored flattened at the top level of card JSON (not nested)
  • Validated against the board schema (undefined fields rejected, enum values checked)
  • Rendered according to [card_display] configuration

Custom Field Types

Type Description Example Values
string Free-form text "John Doe", "https://..."
enum Single-select from defined options "bug", "feature"
enum-set Multi-select from defined options ["blocked", "urgent"]
free-set Multi-value freeform text ["backend", "auth"]
date Date value "2024-03-15"

The enum-set type replaces what labels currently does. The naming convention (enum-set / free-set) makes the constrained vs unconstrained distinction explicit and uses "set" to convey no-duplicates semantics.

Custom Field Schema

Custom fields are defined in board config with type, options (for enum/enum-set), and per-value colors:

[custom_fields.type]
type = "enum"
options = [
  { value = "feature", color = "#16a34a" },
  { value = "bug", color = "#dc2626" },
  { value = "task", color = "#4b5563" },
  { value = "chore", color = "#8b5cf6" },
]

[custom_fields.labels]
type = "enum-set"
options = [
  { value = "blocked", color = "#dc2626" },
  { value = "needs-review", color = "#f59e0b" },
  { value = "urgent", color = "#f97316" },
]

[custom_fields.assignee]
type = "string"

[custom_fields.due_date]
type = "date"

Schema structure:

type CustomFieldSchema struct {
    Type    string              `toml:"type"`              // "string", "enum", "enum-set", "free-set", "date"
    Options []CustomFieldOption `toml:"options,omitempty"` // for enum/enum-set types
}

type CustomFieldOption struct {
    Value string `toml:"value"`
    Color string `toml:"color,omitempty"` // hex color, e.g. "#ef4444"
    // Icon string `toml:"icon,omitempty"` // future: icon identifier
}

Validation rules:

  • enum and enum-set fields require options to be defined
  • Values set on cards must exist in the field's options list
  • Field names cannot start with _ (reserved for internal use) or kan_ (reserved for Kan)
  • The x_ prefix is available as an escape hatch for user-defined fields that would otherwise conflict

Card Display Configuration

How cards render in the board view is configured at the board level, not per-field. This provides:

  1. Separation of concerns: Field schema defines what the field is. Card display defines how the board renders cards.
  2. No conflicts: Each display slot maps to specific field(s). Impossible for multiple fields to claim the same slot.
  3. Explicit at a glance: Reading [card_display] shows exactly how cards will appear.
  4. Easy reassignment: Change which field shows as the type indicator by editing one line.
[card_display]
type_indicator = "type"           # Single enum field shown as colored badge
tint = "priority"                 # Single enum field for card background color
badges = ["labels"]               # Array of set fields shown as colored chips
metadata = ["assignee"]           # Array of fields shown as small text

Schema structure:

type CardDisplayConfig struct {
    TypeIndicator string   `toml:"type_indicator,omitempty"` // single enum field
    Tint          string   `toml:"tint,omitempty"`           // single enum field -> card background color
    Badges        []string `toml:"badges,omitempty"`         // array of set fields (enum-set, free-set)
    Metadata      []string `toml:"metadata,omitempty"`       // array of any fields
}

Card Display Slots

Card display slots determine where custom field values appear on cards in the board view. Each slot has specific visual treatment.

Configurable Display Slots

Slot Cardinality Field Type Rendering
type_indicator Single field enum Small colored badge (e.g., "bug" pill) + left border accent
tint Single field enum Subtle background color wash on the entire card
badges Array of fields enum-set, free-set Colored chips, displayed in config order
metadata Array of fields Any Small text in card footer

Fields not assigned to any display slot are only visible in the card detail/edit view.

Type indicator rendering: For v1, type_indicator renders as a badge (similar visual treatment to badges, just single-value). Icons are deferred to a future version.

Badges ordering: When multiple fields are assigned to the badges slot, their values are displayed in the order the fields appear in the config array. Within each field, values appear in the order stored on the card.

System Display Indicators

Some display elements are non-configurable and always present:

Indicator Source Rendering
Has description description field non-empty 📝 icon on card
Has comments comments array non-empty 💬 icon on card

These are hardcoded to core fields and cannot be reassigned. They indicate presence, not values.

Frontend rendering logic:

1. SYSTEM INDICATORS (always, non-configurable):
   - If card.description is non-empty → show description icon
   - If card.comments is non-empty → show comments icon

2. CONFIGURABLE DISPLAY SLOTS (from [card_display]):
   - If type_indicator configured → render field value as badge + left border accent
   - If tint configured → render card with subtle background color from field's option color
   - If badges configured → render each field's values as chips (in order)
   - If metadata configured → render each field's value as small text

Card Visual Example

┌────────────────────────────────────┐
│ Fix login timeout                  │  ← title (core field)
│ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│ │   bug   │ │ blocked │ │ urgent │ │  ← type_indicator + badges
│ └─────────┘ └─────────┘ └────────┘ │
│ assignee: sarah              📝 💬 │  ← metadata + system indicators
└────────────────────────────────────┘

Validation Rules

Board config validation:

  • type_indicator must reference an existing enum custom field
  • tint must reference an existing enum custom field
  • Each entry in badges must reference an existing enum-set or free-set custom field
  • Each entry in metadata must reference an existing custom field (any type)
  • References to non-existent fields produce a validation error on board load

Card validation:

  • Custom field values must be defined in board schema (unknown fields rejected)
  • Enum/enum-set values must exist in the field's options list
  • Field names cannot use reserved prefixes (_, kan_)

Default Board Configuration

New boards are created with sensible defaults that demonstrate the pattern:

# Columns (unchanged from current)
[[columns]]
name = "backlog"
color = "#6b7280"

[[columns]]
name = "in-progress"
color = "#f59e0b"

[[columns]]
name = "done"
color = "#10b981"

default_column = "backlog"

# Custom fields
[custom_fields.type]
type = "enum"
options = [
  { value = "feature", color = "#16a34a" },
  { value = "bug", color = "#dc2626" },
  { value = "task", color = "#4b5563" },
]

[custom_fields.labels]
type = "enum-set"
options = [
  { value = "blocked", color = "#dc2626" },
  { value = "needs-review", color = "#f59e0b" },
]

# Card display configuration
[card_display]
type_indicator = "type"
badges = ["labels"]

Users see the pattern immediately and can modify, remove, or add fields as needed.

Migration from Current Schema

Existing boards with [[labels]] sections require migration:

  1. Board config: [[labels]] entries convert to [custom_fields.labels] with type = "enum-set" and corresponding options array
  2. Card data: No changes needed—labels array on cards remains the same, just sourced from a custom field now
  3. Card display: Auto-generated [card_display] with badges = ["labels"] to preserve current rendering

The migration is handled by kan migrate and schema version bumping (board schema v2).

CLI Changes

Removed:

  • -l, --label flag on kan add and kan edit

Unchanged:

  • -f, --field <key=value> for setting custom fields

Usage after revamp:

# Setting labels (now a custom field)
kan add "Fix bug" -f labels=blocked

# Setting type
kan add "New feature" -f type=feature

# Setting multiple on edit
kan edit abc123 -f type=bug -f labels=blocked,urgent

The CLI is primarily for scripts and agents; ergonomics of dedicated flags is less important than consistency. All custom fields use the same -f mechanism.

Deferred: CLI shortcuts configuration (e.g., [cli.shortcuts] to map -l to -f labels=...) may be added in a future version if demand exists.

Future Extensions

The architecture supports future enhancements without structural changes:

  • Icons on type_indicator: Add icon field to CustomFieldOption
  • Multiple badge rows: Add badges_top, badges_bottom display slots
  • Required fields with defaults: Add required and default to CustomFieldSchema
  • CLI shortcuts: Add [cli.shortcuts] for board-specific flag aliases
  • Additional field types: number, url, string-list (freeform multi-value)

Open Questions

None currently blocking. The following are explicitly deferred:

  • GitHub/GitLab issues sync: Likely a frontend/integration concern, not core Kan
  • Multi-repo boards: Explicitly out of scope. Boards are per-repo.