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
| Command | Purpose |
|---|---|
pinto init | Initialize a board in the current directory. |
pinto add <title> | Add a PBI; optionally set points, labels, Sprint, body, or a template. |
pinto list | List 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 board | Render PBIs grouped by workflow column, optionally showing root PBIs only. |
pinto kanban | Open 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:
- Top-level PBIs come first, in ascending
rank(with a(prefix, number)ID tie-break so equal ranks never reorder between views). - 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 listflattens 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 boardandpinto kanbanbuild 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 ownrank).- The completion column (
done_columninconfig.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 | createdsets the root/sibling order explicitly (add--reverseto invert it); the hierarchy is always preserved.pinto kanbanuses 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.
Navigate and edit
The board separates selecting a card from moving it: lowercase keys move the cursor, uppercase (Shift) keys move the selected item. Defaults are:
| Action | Keys |
|---|---|
| Select column / row | h j k l or arrow keys |
| Move item across columns | H / L (Shift+Left / Shift+Right) |
| Reorder item within a column | K / J (Shift+Up / Shift+Down) |
| Expand or collapse a parent | Space / Enter |
| Add a PBI | a |
| Edit the selected PBI | e |
| Add / remove a dependency | d / D |
| Set or clear the parent | p |
| Open the details popup | v |
| Substring / regex search | / / Ctrl+? |
| Clear an active filter | Esc |
| Toggle a maximized column | m |
| Reload the board | r |
| Help window | ? |
| Quit | q or Esc |
| Quit into the shell | Q |
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 thanShift+hfor shifted letters. - Named keys:
Enter,Esc,Tab,Backspace,Delete,Insert,Home,End,PageUp,PageDown, the arrowsLeft/Right/Up/Down, and function keysF1–F12. - Modifiers:
Ctrl,Alt,Shift,Cmd,Meta, andHyper— for exampleCtrl+aorAlt+Shift+Left. Write the literal plus key asPlus.
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.sqlite3database, available only in builds with the optionalsqlitefeature.
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 = truerenders bodies as styled Markdown; setfalsefor raw text.timezoneformats human-readable timestamps. Uselocal,UTC, or a fixed±HH:MMoffset 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:
- Red — write a focused test that fails for the missing behavior.
- Green — implement the smallest change that makes the test pass.
- 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:
| Job | Toolchain | Scope |
|---|---|---|
msrv | Rust 1.89.0 | Default and all-feature build/test compatibility |
check | Pinned Rust 1.97.0 | Full mise run check quality gate on each primary OS |
current-stable | Latest stable channel | Forward-compatibility test suite with all features |
release | Pinned Rust 1.97.0 | Release 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