Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

pinto is a lightweight, local-first Scrum backlog and Kanban board for the terminal. It keeps Product Backlog Items (PBIs), Sprints, and workflow state in plain text so that the board remains easy to inspect, version, and recover with Git.

The project deliberately has a small vocabulary:

  • Product Backlog — the ordered list of work.
  • Sprint — a time-boxed selection of PBIs with a goal.
  • Kanban workflow — the columns that describe a PBI’s current state.

pinto does not require a server, account, or database service. A new board can be initialized in the directory where the work is kept, and the same CLI can manage it from the first item through completion.

This book is the task-oriented guide for users and contributors. Detailed design decisions, JSON contracts, and migration notes remain in the repository reference documents:

Installation

Using a released package

Install the latest published 0.2.0 binary with Cargo:

cargo install pinto-cli --version 0.2.0
pinto --version

The crates.io package is named pinto-cli; it installs the pinto binary.

Rust 1.89 or newer is required to build the current project. If a release is not yet available on crates.io, install from a checkout instead.

Installing from source

Clone the repository and install the binary from its workspace:

git clone https://github.com/moriturus/pinto
cd pinto
cargo install --path . --locked
pinto --version

The source install uses the committed Cargo.lock; --locked makes dependency resolution fail instead of silently changing that lockfile.

Contributor setup

Contributors use mise to install the Rust toolchain, mdBook, and the project quality tools:

mise install
mise run check

The check task runs the Rust tests, lint, Rust API documentation, the mdBook build, and formatting checks. To preview this book while editing it, run:

mdbook serve

The generated site is written to target/book/, which is a build artifact and is not part of the source tree.

Quick start

The following workflow creates a board, adds a PBI, inspects it, and moves it through the default workflow. Run it from the directory that should own the board.

1. Initialize a board

pinto init

Initialization is idempotent. It creates the local .pinto/ board and its default workflow when they do not exist.

2. Add a PBI

pinto add "Implement the Markdown parser" --points 3 --label backend

The command prints the newly assigned ID, for example T-1. Keep that ID for the following commands.

3. Inspect the backlog

pinto list
pinto show T-1

Use --long for status, points, assignee, and timestamps, or --json when a script needs machine-readable output:

pinto list --status todo --long
pinto show T-1 --json

4. Move the work

The last operand to move is the destination column:

pinto move T-1 in-progress
pinto move T-1 review
pinto move T-1 done

The configured workflow determines which columns are valid. The default columns are todo, in-progress, review, and done.

5. View the board

pinto board

For an interactive view, use pinto kanban. The Kanban view uses the same board and services as the non-interactive commands, so changes remain visible to both interfaces.

CLI reference

Run pinto --help or pinto <command> --help for the complete, versioned option list. The commands below cover the normal Scrum workflow.

Board and PBI commands

CommandPurpose
pinto initInitialize a board in the current directory.
pinto add <title>Add a PBI; optionally set points, labels, Sprint, body, or a template.
pinto listList PBIs, with status, label, Sprint, search, root-only, long, and JSON filters.
pinto show <id>...Display one or more PBI details.
pinto move <id>... <status>Transition one or more PBIs to a workflow column.
pinto reorder <id>Reorder a PBI within its sibling group (same parent and column).
pinto edit <id>Update PBI fields or open the configured editor.
pinto remove <id>...Archive PBIs; use the rm alias and --force only for permanent removal.
pinto boardRender PBIs grouped by workflow column, optionally showing root PBIs only.
pinto kanbanOpen the interactive Kanban board.

Examples:

pinto list --status todo in-progress --long
pinto list --label backend frontend --all-labels
pinto list --search "parser"
pinto list --roots-only --status todo --json
pinto board --status in-progress review
pinto board --roots-only --status todo --long
pinto reorder T-1 --top
pinto edit T-1 --title "Implement the Markdown parser"

Display order

Priority is hierarchical. Every view — list, board, kanban, and their --json output — flattens the same parent/child forest in one canonical order:

  1. Top-level PBIs come first, in ascending rank (with a (prefix, number) ID tie-break so equal ranks never reorder between views).
  2. Each parent is immediately followed by its whole subtree; a parent’s children are ordered among themselves by rank.

So rank orders siblings, and the tree decides the overall priority: a child never floats above an unrelated, higher-priority PBI just because its raw rank string happens to be lower. Deprioritise a parent and its entire subtree moves with it.

  • pinto list flattens the whole forest. A filtered-out or absent parent promotes its children to the top level, so the tree is cut cleanly at the filter boundary.
  • pinto board and pinto kanban build the same forest per column. A child whose parent lives in another column is shown at the top level of its own column (positioned by its own rank).
  • The completion column (done_column in config.toml) orders its top-level and sibling groups by completion time (done_at) descending by default, so the most recently finished PBI leads; the subtree grouping still applies.
  • pinto board --sort rank | done | created sets the root/sibling order explicitly (add --reverse to invert it); the hierarchy is always preserved. pinto kanban uses the defaults and has no sort toggle.

Because rank is sibling-local, pinto show and the Kanban details popup print it as a sibling ordinal: #2 under <parent-id> for a child (2nd among that parent’s children) or #2 for a top-level PBI.

Root-only views

Use --roots-only with list or board to show only PBIs whose persisted parent field is unset. Child PBIs are omitted, while root PBIs with or without children remain visible. Without the option, the existing hierarchical output is unchanged.

The option composes with compatible filters and output modes, for example:

pinto list --roots-only --status todo --label backend --search parser --json
pinto board --roots-only --status todo --sort rank --reverse --long

The check uses the stored parent link, not just the current result set. Thus a child is still omitted when its parent is hidden by a status, Sprint, label, or search filter.

The parent-child demo contains a reproducible hierarchy for trying these commands.

pinto reorder (and Kanban K / J) moves a PBI only within its sibling group--top / --bottom go to the front/back of that group, and --before / --after take a sibling as reference. Reordering relative to a non-sibling is refused; move a PBI between groups with edit --parent. Moving a parent carries its whole subtree.

Relations and Sprints

Use dependency commands to record ordering constraints between PBIs:

pinto dep add T-2 T-1
pinto dep rm T-2 T-1

Git commit links are managed separately:

pinto link add T-1 abc1234
pinto link sync

The Sprint commands create and manage time-boxed work:

pinto sprint new S-1 "Sprint 1" --goal "Ship the parser" --start 2026-07-01 --end 2026-07-14
pinto sprint edit S-1 --goal "Ship the parser" --start 2026-07-01 --end 2026-07-14
pinto sprint start S-1
pinto sprint add S-1 T-1
pinto sprint add S-1 --status todo --limit 3
pinto sprint add S-1 --status todo             # omit --limit to assign all matches
pinto sprint list
pinto sprint close S-1
pinto sprint remove S-1

Reports include pinto sprint burndown, pinto sprint velocity, pinto sprint capacity, and pinto cycletime.

Use pinto sprint edit to add a goal or change a planned period before starting a Sprint. Removing a Sprint releases its assigned PBIs without deleting them. Assign new PBIs only to planned or active Sprints; use pinto sprint unassign to correct an assignment that remains after a Sprint closes.

Definition of Done

A single Definition of Done is shared by every PBI. Display, set, or clear it:

pinto dod                          # show the current shared DoD
pinto dod set "- [ ] Tests pass and docs updated"
pinto dod clear

The DoD body is stored verbatim, so pass a multi-line checklist with a real newline in the quoted string. Because the text often starts with a hyphen, it is taken as a literal value rather than an option.

Maintenance

These commands keep storage tidy and are not part of the daily loop:

pinto rebalance --dry-run          # preview oversized sibling scopes and shorter ranks
pinto rebalance                    # rewrite only scopes that need it
pinto migrate --to git             # switch the storage backend

Automation and shell integration

automate accepts a validated JSON plan. Preview a plan before applying any writes, and use JSON output when another tool needs execution results:

pinto automate --plan plan.json --dry-run --json
pinto automate --plan plan.json --json

Plans can be supplied inline, from a file, or from standard input. pinto shell starts an interactive command shell, and pinto completion <shell> generates completion scripts for supported shells.

The dry-run snapshot holds the board write lock, so a concurrent writer cannot be mixed into the preview. It works from both normal repositories and linked worktrees: only .pinto is copied, and a temporary owner-private Git repository is initialized when the source project has Git metadata. The source .git object store is never copied, and the temporary workspace is cleaned up after success or failure.

Machine-readable output

Read commands support --json:

pinto list --json
pinto show T-1 T-2 --json
pinto board --json
pinto sprint list --json

Prefer this format over parsing human-oriented tables. IDs, statuses, ranks, relations, and timestamps keep the same meaning as the regular output; timestamps are RFC 3339 values in UTC.

Cookbook

This chapter collects goal-oriented recipes for everyday pinto work. Every recipe states its prerequisites, the exact command, and how to verify the result. All of them run in a clean temporary directory, so you can replay the whole chapter without touching an existing board:

mkdir -p /tmp/pinto-cookbook && cd /tmp/pinto-cookbook
pinto init

The recipes call the installed pinto binary. Inside the repository you can substitute cargo run -- for pinto, as described in Dogfooding. A ready-made board for the pipeline recipes lives in demos/single/cookbook.

Backlog basics

Seed a small backlog

Prerequisites: an initialized board (pinto init).

pinto add "Design the login form" --points 3 --label ui --label auth
pinto add "Implement the login API" --points 5 --label api --label auth
pinto add "Write onboarding docs" --points 2 --label docs
pinto add "Fix the session timeout bug" --points 1 --label bug --label auth
pinto add "Refactor the storage layer" --points 8 --label refactor

Verify: each command prints the assigned ID (Created T-1 … through T-5). The remaining recipes assume these five PBIs.

List and inspect

Prerequisites: the seeded backlog above.

pinto list --status todo
pinto show T-1
pinto list --long
pinto list --json

Verify: pinto list prints one line per PBI — ID, status, title, points in parentheses, labels in brackets. --long adds dates and other columns; its noninteractive output has no header row. --json emits machine-readable output for scripts.

Move work through the workflow

Prerequisites: the seeded backlog above.

pinto move T-1 in-progress
pinto move T-2 review
pinto board

Verify: pinto board shows T-1 under in-progress and T-2 under review. The last operand to pinto move is the destination column, exactly like Unix mv.

Sprint recipes

Create a sprint and assign work in bulk

Prerequisites: the seeded backlog with T-3, T-4, and T-5 still in todo.

pinto sprint new S-1 "Sprint 1" --goal "Ship the login flow" \
  --start 2026-07-13 --end 2026-07-27
pinto sprint add S-1 --status todo --limit 2
pinto sprint start S-1

Verify: the bulk assignment picks the two highest-ranked todo PBIs (Assigned T-3 to sprint S-1, Assigned T-4 to sprint S-1). Omit --limit to assign every match, or pass a single ID (pinto sprint add S-1 T-5) instead of --status. pinto sprint list now reports S-1 as active.

The close-out recipe moves the sprint PBIs to done before closing the Sprint, then runs the reports; see Close out and report.

Unix text-stream recipes

pinto prints plain text on purpose, so the standard Unix toolbox composes with it. Two properties make the default pinto list output easy to process:

  • columns are separated by runs of spaces, so tr -s ' ' normalizes a line to single-space-separated fields;
  • the ID is always the first field and the status the second, while the title may contain spaces.

All recipes below stick to POSIX options and behave the same with the GNU and BSD userlands, including the BSD tools shipped with macOS. Portability notes are called out per recipe — for example, in-place editing differs between GNU sed -i and BSD sed -i '', so the recipes always write to standard output instead. They assume the board built in the previous sections.

1. Extract IDs with cut

Prerequisites: the seeded backlog.

pinto list --status todo | cut -d' ' -f1

Verify: only the ID column remains:

T-3
T-4
T-5

The ID never contains a space, so cutting the first space-delimited field is safe even though later columns are padded with multiple spaces.

2. Filter by label with grep

Prerequisites: the seeded backlog.

pinto list | grep -E '\[[^]]*auth'

Verify: only the three PBIs labeled auth are printed. The pattern anchors on the label list in brackets, so a title that merely mentions “auth” does not match. grep -E (extended regular expressions) is POSIX and works with both GNU and BSD grep.

3. Count PBIs per status with sort and uniq

Prerequisites: the seeded backlog.

pinto list | tr -s ' ' | cut -d' ' -f2 | sort | uniq -c

Verify: a frequency table of the status column:

   1 in-progress
   1 review
   3 todo

uniq only merges adjacent lines, so the sort before it is required.

4. Count matches with wc

Prerequisites: the seeded backlog.

pinto list --status todo | wc -l

Verify: prints 3. BSD wc pads the number with leading spaces; pipe through tr -d ' ' if a script needs the bare digits.

5. Take the top of the backlog with head

Prerequisites: the seeded backlog.

pinto list --status todo | head -n 2

Verify: the two highest-ranked todo PBIs, in backlog rank order — the same two that pinto sprint add S-1 --status todo --limit 2 would assign.

6. Take the last records with tail

Prerequisites: the seeded backlog.

pinto list --long | tail -n 2

Verify: only the last two data rows (T-4 and T-5 with the seed data) remain. Because pinto’s noninteractive --long output is data-only, tail -n 2 selects records rather than skipping a header. When an upstream command does emit a header, tail -n +2 (“start at line 2”) is POSIX and portable, unlike the historical tail +2 form.

7. Normalize aligned columns with tr

Prerequisites: the seeded backlog.

pinto list | tr -s ' '

Verify: every run of spaces collapses to a single space:

T-1 in-progress Design the login form (3) [ui, auth]

This is the standard first step before cut, join, or any tool that expects a single-character field delimiter.

8. Render a Markdown checklist with sed

Prerequisites: the seeded backlog.

pinto list --status todo | sed -E 's/^(T-[0-9]+)[[:space:]]+[[:alnum:]-]+[[:space:]]+/- [ ] \1 /'

Verify: a paste-ready checklist for a standup note:

- [ ] T-3 Write onboarding docs  (2)  [docs]
- [ ] T-4 Fix the session timeout bug  (1)  [bug, auth]
- [ ] T-5 Refactor the storage layer  (8)  [refactor]

sed -E is supported by both GNU and BSD sed. Avoid sed -i in shared scripts: GNU accepts sed -i, BSD requires sed -i ''.

9. Collect IDs onto one line with paste

Prerequisites: the seeded backlog.

pinto list --status todo | cut -d' ' -f1 | paste -sd' ' -

Verify: prints T-3 T-4 T-5 on a single line, ready to splice into another command. The trailing - operand is required by BSD paste to read standard input; GNU paste accepts it too, so always write it.

10. Join sprint assignments with statuses using join

Prerequisites: the sprint recipes above (T-3 and T-4 assigned to S-1).

pinto list | tr -s ' ' | cut -d' ' -f1,2 | sort > status.txt
pinto list --sprint S-1 | cut -d' ' -f1 | sort | join - status.txt

Verify: each sprint item paired with its current board status:

T-3 todo
T-4 todo

join needs both inputs sorted on the join field; the - reads the sprint IDs from standard input while status.txt supplies the second column.

11. Feed a pipeline back into pinto

Prerequisites: the seeded backlog, with T-3 and T-4 still in todo.

pinto move $(pinto list --status todo | head -n 2 | cut -d' ' -f1) in-progress

Verify: pinto confirms each transition (Moved T-3 to in-progress, Moved T-4 to in-progress), and pinto list --status in-progress shows the moved PBIs. This composes recipes 1 and 5: the pipeline selects the top of the backlog and the command substitution feeds the IDs back into pinto move.

12. Sum story points with paste and bc

Prerequisites: every listed PBI has story points; bc (POSIX) is installed.

pinto list --sprint S-1 | tr -s ' ' | sed -E 's/.*\(([0-9]+)\).*/\1/' | paste -sd+ - | bc

Verify: prints the total committed points for S-1 (3 with the seed data: 2 + 1). sed isolates the points, paste -sd+ - folds them into an arithmetic expression (2+1), and bc evaluates it.

Close out and report

Prerequisites: the sprint recipes above; recipe 11 already moved T-3 and T-4 to in-progress.

pinto move T-3 T-4 done
pinto sprint close S-1
pinto sprint velocity
pinto sprint burndown S-1
pinto cycletime --sprint S-1

Verify: pinto sprint velocity reports completed points per sprint (3 points for S-1 with the seed data), burndown draws a chart over the planned period, and cycletime lists lead and cycle times for the completed PBIs.

Kanban (TUI)

pinto kanban opens an interactive board in the terminal. It reads and writes the same .pinto/ board as the non-interactive commands, so a move made in the TUI is immediately visible to pinto list and pinto board, and vice versa.

pinto kanban

Start with a focused view

Startup flags narrow what the board shows without changing stored data:

pinto kanban --column in-progress review   # show only these columns
pinto kanban --maximize --column review     # open maximized on one column
pinto kanban --search parser                # filter cards by substring
pinto kanban --search '^T-1\d' --regex      # filter cards by regular expression

Explicit --column values override the [tui] hidden_columns setting for that run. --regex requires --search.

The board separates selecting a card from moving it: lowercase keys move the cursor, uppercase (Shift) keys move the selected item. Defaults are:

ActionKeys
Select column / rowh j k l or arrow keys
Move item across columnsH / L (Shift+Left / Shift+Right)
Reorder item within a columnK / J (Shift+Up / Shift+Down)
Expand or collapse a parentSpace / Enter
Add a PBIa
Edit the selected PBIe
Add / remove a dependencyd / D
Set or clear the parentp
Open the details popupv
Substring / regex search/ / Ctrl+?
Clear an active filterEsc
Toggle a maximized columnm
Reload the boardr
Help window?
Quitq or Esc
Quit into the shellQ

Press ? inside the board to open the built-in help window, which always lists the bindings that are actually in effect.

Cards follow the same hierarchical display order as pinto list and pinto board: top-level cards by rank, each parent followed by its subtree, with siblings ordered by rank. Expanding a parent reveals its children directly beneath it, so a child may sit ahead of a standalone card that outranks it — that is the point, since the parent’s priority carries its whole subtree. The completion column leads with the most recently finished card (done_at descending).

Customize behavior

The [tui] section of .pinto/config.toml adjusts the interactive board:

[tui]
confirm_quit = true                 # ask before leaving the board
hidden_columns = ["done"]           # hide columns unless --column overrides

Unknown column names in hidden_columns are rejected at load time, so a typo surfaces immediately rather than silently hiding nothing.

Rebind keys

[tui.key_bindings] overrides the keys for individual actions. Each action takes an array of one or more key expressions, and an action may keep several bindings at once:

[tui.key_bindings]
quit = ["q", "Esc"]                 # keep the defaults
add = ["a", "n"]                    # add a second key for "add"
move_left = ["Shift+Left"]          # replace the default for this action
help = ["?", "F1"]

Only the actions you list are overridden; every other action keeps its default keys. The action names are the snake_case forms shown by the built-in help window and the [tui.key_bindings] documentation (quit, shell, select_left, move_left, reorder_up, add, edit, dependency_add, parent, maximize, search, regex_search, details, help, and so on).

A key expression is a key name, optionally prefixed with +-separated modifiers:

  • Printable keys are the character itself: q, /, ?. Use an uppercase letter (H) rather than Shift+h for shifted letters.
  • Named keys: Enter, Esc, Tab, Backspace, Delete, Insert, Home, End, PageUp, PageDown, the arrows Left / Right / Up / Down, and function keys F1F12.
  • Modifiers: Ctrl, Alt, Shift, Cmd, Meta, and Hyper — for example Ctrl+a or Alt+Shift+Left. Write the literal plus key as Plus.

Invalid expressions (an empty name, an unknown modifier, or Shift+ on a printable character) are reported when the board configuration loads, so a bad binding is caught before the TUI starts rather than failing silently.

Configuration

pinto init writes .pinto/config.toml with the defaults below. Every setting is optional to change; a fresh board works without editing anything. Edit the file directly and keep the change small and reviewable — it is the one file in .pinto/ that is meant to be hand-edited.

columns = ["todo", "in-progress", "review", "done"]
done_column = "done"

[project]
name = "pinto"
key = "T"

[tui]
confirm_quit = true

[storage]
backend = "file"

[wip]
enabled = true

[display]
markdown = true
timezone = "local"

[points]
aggregate_children = false

Workflow columns

columns is the ordered list of Kanban states, left to right. done_column names the completion column; the board sorts that column by completion time and records a done_at timestamp when a PBI enters it. done_column must be one of columns, and an unknown value is rejected when the config loads.

Configuration uses a strict schema: unknown keys are rejected with the TOML table and field path, so a typo such as [display].timezome does not silently fall back to a default. columns must contain at least one non-blank, unique name. Values in done_column, [tui].hidden_columns, and [wip.limits] must refer to configured columns.

Renaming or removing a column that still holds PBIs strands those items in a status the workflow no longer recognizes, so move work out of a column before retiring it.

Project identity

The [project] table sets the display name and the PBI ID prefix key. With key = "T", new items are numbered T-1, T-2, and so on. Changing key affects only IDs assigned afterward; existing IDs keep their original prefix. The key must contain only ASCII letters. Digits and - are reserved for the numeric ID portion and separator; _ is not accepted. The project name must not be empty or whitespace-only. Invalid settings stop the command before the selected storage backend is opened; fix the reported field in config.toml and retry.

Storage backend

[storage] backend selects where the board is persisted:

  • file (default) — one Markdown file per PBI under .pinto/.
  • git — the file layout plus one automatic commit for each complete write operation; pre-existing Git changes are kept out of that commit.
  • sqlite — a single .pinto/board.sqlite3 database, available only in builds with the optional sqlite feature.

All backends expose the same CLI. Use pinto migrate --to <backend> to move an existing board between them.

Write commands wait up to five seconds for another pinto process by default. The lock remains held through a Git-backed commit so one service operation stays atomic. For a slow filesystem or Git hook, set the process environment variable PINTO_LOCK_TIMEOUT_SECS to a larger non-negative integer before running the command.

WIP limits

[wip] enforces work-in-progress limits per column. It is enabled by default with no limits set, so nothing is restricted until you add one:

[wip]
enabled = true

[wip.limits]
in-progress = 3
review = 2

Exceeding a limit on pinto move prints a warning. Pass --no-wip-check to skip the check for a single move, or set enabled = false to disable the check for the whole board.

Display

[display] controls how PBI bodies and timestamps are shown by pinto show and the Kanban details popup:

  • markdown = true renders bodies as styled Markdown; set false for raw text.
  • timezone formats human-readable timestamps. Use local, UTC, or a fixed ±HH:MM offset such as +09:00. This affects display only — stored and JSON timestamps stay in UTC.

Parent PBI points (opt-in)

[points].aggregate_children is false by default. Set it to true when parent PBIs should display the sum of their active descendant leaves:

[points]
aggregate_children = true

When enabled, a parent’s stored points are replaced in read-only views while it has children. A nested parent is counted through its descendants only once, and an item in done_column contributes no points. Active descendants below a completed intermediate item remain eligible. If an active descendant leaf has no points, the affected parent is shown as unestimated (-) rather than using an incomplete sum. The stored Markdown frontmatter is never rewritten by this calculation.

Interactive Kanban

The [tui] table configures the interactive board — exit confirmation, hidden columns, and key bindings. See Kanban (TUI) for the full set of options and the key-binding syntax.

Data format

Board layout

The default file backend stores the board below .pinto/ in the repository where pinto is run. The directory contains the configuration, individual PBI Markdown files, Sprint data, templates, and the issued_ids history. Each PBI is a separate file so that a Git diff shows the change to one item clearly.

The board is local-first: no account, server, or database service is required. The optional Git and SQLite backends expose the same pinto operations while keeping the CLI contract consistent.

PBI files

A PBI file combines TOML frontmatter with a Markdown body:

+++
id = "T-1"
title = "Implement the parser"
status = "todo"
rank = "i"
created = "2026-01-01T00:00:00Z"
updated = "2026-01-01T00:00:00Z"
+++

Acceptance criteria and planning notes belong here.

The frontmatter carries structured fields such as the ID, title, status, rank, labels, relations, timestamps, and optional Sprint information. The body is user-authored Markdown and is preserved when the display locale changes.

The filename stem is part of the record identity: tasks/T-1.md and archive/T-1.md must both contain id = "T-1". File reads validate active and archived items, as well as Sprint filenames, and stop on filename mismatches or duplicate logical IDs before a write or migration can overwrite existing data.

Statuses must be columns in the configured workflow. The rank is a fractional index used to keep ordering changes small. Completion and start timestamps are recorded when a PBI crosses the configured workflow boundaries.

Configuration

.pinto/config.toml controls the workflow and presentation settings. The default workflow is:

columns = ["todo", "in-progress", "review", "done"]
done_column = "done"

It is the one file under .pinto/ intended for hand-editing. Beyond the workflow columns, it selects the storage backend, project identity, WIP limits, display and timezone options, and the interactive Kanban key bindings. See Configuration for every setting. Keep machine-readable JSON timestamps in UTC; the display timezone does not rewrite stored data.

Safe operations

Use pinto commands to add, transition, rank, edit, archive, and relate PBIs. The generated .pinto/issued_ids file preserves every issued item number so a permanently deleted ID is never assigned to a different PBI; do not remove it when changing storage backends. Do not maintain a second hand-edited backlog or edit task files as part of the normal workflow. Direct recovery is an exception for damaged data; validate the board with pinto list afterward.

For the full JSON contract and migration rationale, see JSON output and storage migration.

Dogfooding

pinto develops on a pinto board. When validating a change in this repository, run the current worktree through cargo run so the behavior under test is the behavior being developed.

Inspect the board

Use commands such as these from the repository root:

cargo run --quiet -- list --status todo --long --json
cargo run --quiet -- show <ID-from-list>
cargo run --quiet -- board

Replace <ID-from-list> and <ID> with IDs returned by the board commands.

Human-readable output is useful for a quick check; --json is useful when the result must be inspected without depending on table formatting.

These examples correspond to the installed pinto list, pinto show, and pinto board subcommands. The cargo run -- prefix is intentional while developing: it selects the executable built from the current checkout.

Update an item

Add, transition, rank, edit, and remove items through the CLI:

cargo run -- add "Document the workflow" --template default
cargo run -- move <ID> in-progress
cargo run -- reorder <ID> --top
cargo run -- show <ID>

For a transition, the installed form is pinto move <id> <status>; the dogfooding form above is cargo run -- move <id> <status>.

Use the default archive operation when an item was created by mistake. Reserve remove --force for an explicit permanent cleanup, and inspect the result with list, show, or board after every write.

For multiple planned writes, validate the plan first:

cargo run -- automate --plan plan.json --dry-run --json
cargo run -- automate --plan plan.json --json

Do not edit .pinto/tasks/*.md directly during normal backlog work. The configuration file may be edited when changing board settings, but the CLI is the source of truth for item operations.

Development verification

After implementation and dogfooding, run the same quality gate used by CI:

mise run check
mise run release-check

The release gate repeats the check as needed and also runs the all-features coverage threshold, dependency audit, and dependency-policy checks. Coverage has no source exclusions, so storage boundaries and TUI lifecycle paths remain measured.

Contributing

Read the repository’s AGENTS.base.md, CONTRIBUTING.md, and design guide before making a design decision. The project favors a small, fast, Scrum-focused tool with plain-text, Git-friendly storage.

Development loop

Install the managed tools and run the quality gate:

mise install
mise run check

Follow TDD for behavior changes:

  1. Red — write a focused test that fails for the missing behavior.
  2. Green — implement the smallest change that makes the test pass.
  3. Refactor — improve structure while keeping the tests green.

Domain behavior belongs in unit-testable modules under src/; CLI input and output belong in integration tests under tests/. Documentation changes should also build the book locally:

mise run book
mdbook serve

The repeatable unit, integration, doctest, and fuzzing commands are collected in Testing and fuzzing.

See Reproducible builds and releases for the pinned toolchain policy, CI job responsibilities, and locked package verification.

Before committing

Run mise run check after the final change. It runs all-feature tests, Clippy with warnings denied, Rust documentation with warnings denied, the mdBook build, and formatting checks. Review the complete diff for unrelated changes, keep dependencies minimal, and write actionable user-facing errors.

Backlog changes are part of the normal workflow: inspect and update the self-hosted .pinto/ board through pinto commands, then verify the result with pinto list or pinto board.

Pull requests

Use a focused branch and describe the motivation, implementation, tests, and documentation changes. Include a related issue or planning reference when one exists. Follow the pull request checklist and keep user-facing documentation in English; localized Fluent resources are the intentional exception.

Testing and fuzzing

Run the normal test layers from a checkout with the Rust toolchain selected by mise:

mise run test                 # unit, CLI, docs, i18n, and skill integration tests
cargo test --doc              # public API examples
cargo test --test cli         # CLI and pseudo-terminal smoke tests
mise run check                # tests, Clippy, Rust docs, mdBook, and fmt

mise run coverage writes coverage.xml in Cobertura format and then checks the artifact’s root Cobertura line-rate with scripts/check-coverage.sh. The 0.95 threshold is therefore applied to the same metric that CI uploads, rather than to the different denominator used by the LLVM text summary.

The macOS PTY lifecycle regression can be reproduced with:

cargo test --test cli kanban::pty_tests::shell_can_reenter_kanban_without_leaking_lifecycle_state -- --exact --nocapture

The CI failure observed on the macOS 26 arm64 runner occurred after the test had returned to the third pinto> prompt following two Kanban entries. The child process did not satisfy the test’s three-second exit deadline after Ctrl-D, while the same lifecycle passed on local macOS and Linux; the Windows check suite also passed. The test keeps the Ctrl-D and terminal-flag assertions, but uses the platform-specific SHELL_EXIT_WAIT deadline only for this final process-exit wait so PTY teardown latency is not mistaken for a lifecycle leak.

The Markdown frontmatter parser and automation-plan parser have libFuzzer targets under fuzz/. Install the fuzz runner once with nightly-compatible tooling:

rustup toolchain install nightly
cargo install cargo-fuzz --locked
cargo fuzz list
cargo fuzz run automation_plan_parse -- -max_total_time=300
cargo fuzz run markdown_frontmatter_parse -- -max_total_time=300

The weekly scheduled CI workflow runs both targets for five minutes and uploads failures from fuzz/artifacts. To reproduce a reported input locally, pass the uploaded crash file or corpus directory to the same target:

cargo fuzz run markdown_frontmatter_parse fuzz/artifacts/markdown_frontmatter_parse/crash-...

Keep the failing input when fixing a parser bug, then rerun the target with a short time limit and finish with mise run check. The fuzz targets treat parser errors as expected input outcomes; a panic or sanitizer failure is the failure signal.

Reproducible builds and releases

The repository commits Cargo.lock and treats it as part of the source and release contract. Cargo commands that build, test, document, package, or install pinto must use --locked; an intentional dependency update is made with cargo update, followed by review of the lockfile diff.

Toolchain roles

Development and release commands use Rust 1.97.0, pinned in mise.toml. Cargo.toml continues to declare Rust 1.89 as the minimum supported version. CI keeps the responsibilities separate:

JobToolchainScope
msrvRust 1.89.0Default and all-feature build/test compatibility
checkPinned Rust 1.97.0Full mise run check quality gate on each primary OS
current-stableLatest stable channelForward-compatibility test suite with all features
releasePinned Rust 1.97.0Release build, package, and source-install verification

The all-feature MSRV checks and the pinned quality gate intentionally cover different support contracts. The latest-stable job does only the forward compatibility probe, so a moving toolchain does not define release artifacts.

Clean-checkout verification

From a clean checkout, install the pinned tools and run the same gates used by CI:

mise install
mise run check
cargo build --release --all-features --locked
cargo package --all-features --locked
cargo install --path . --locked --root "$PWD/.tmp/pinto"

mise run release-check adds coverage, dependency audit, dependency policy, and the release build/package tasks to the quality gate.

Allowlisted package contents

The crate manifest uses root-anchored package.include entries for the manifest, source, locale resources, README, license, and the rank benchmark example. This allowlisted package excludes repository-only data such as .pinto, demos, tests, docs, and CI metadata.

Run ./scripts/verify-package.sh or mise run release-package to run cargo package --all-features --locked, compare the package file list with the committed release/package-files.txt, and run tests against the extracted packaged crate. A deliberate runtime-file addition must update the allowlist and its file-list baseline together. CI also runs cargo install --path . --locked from the clean checkout as the source-install check.

Publishing a release

For each release, update the package version in Cargo.toml and both committed lockfiles, move the relevant entries into a dated CHANGELOG.md heading, and update the published-version installation examples. For a breaking change while pinto remains in the 0.x series, increment the minor version as the 0.2.0 CLI rename demonstrates. Before publishing, run the complete local release gate and verify the package without uploading it:

mise run release-check
cargo publish --dry-run --all-features --locked

After the release commit has passed CI and has been fast-forwarded to main, create the repository’s version tag and push it together with main. Publish the same locked package to crates.io only after the tag points at that commit:

git tag 0.2.0
git push origin main 0.2.0
cargo publish --all-features --locked