Pinax
Pinax is a Git-native tracker for software delivery work. It stores an append-only JSONL event log in the repository, derives the current board from that log, and commits the generated Markdown board alongside the code.
The tracker gives a team one durable record of items, dependencies, ownership, status, and completion briefings. Git carries the record between clones and branches. There is no service to host, database to operate, or account to create.
Install
Pinax requires Python 3.10 or later and Git.
pipx install pinax-tracker
pinax --help
The same package installs with pip:
python -m pip install pinax-tracker
To work from a checkout:
git clone https://github.com/antikas/pinax-tracker.git
cd pinax-tracker
python -m pip install -e .
Start tracking a repository
Run pinax init at the root of the repository you want to track. It creates
.ergon/, records the initial events, configures the log's merge attributes,
and installs a pre-commit verification hook when Git allows it.
pinax init --actor alex@laptop
pinax add --title "Ship the widget" --actor alex@laptop
pinax claim <item-id> --actor alex@laptop
pinax done <item-id> --briefing completion.md --actor alex@laptop
pinax status
completion.md is a short work record. It stays with the item in the
generated projection.
Everyday commands
pinax add --title TEXT
pinax claim ITEM_ID
pinax done ITEM_ID --briefing FILE
pinax block ITEM_ID --gate scope|decision|destructive|proposal
pinax park ITEM_ID --reason TEXT
pinax dep add FROM_ID --blocks TO_ID
pinax ready [--under ITEM_ID]
pinax next [--under ITEM_ID]
pinax status [--json] [--under ITEM_ID]
pinax board [--json]
pinax report [--json]
pinax release ITEM_ID --reason TEXT
pinax policy claim-expiry --hours N
pinax verify [--fix]
pinax replay --at GIT_REF
pinax verify checks that every physical parsed event has a valid event hash
unless it is covered by a valid tombstone, then compares the generated board
and item pages with the committed projection. --fix regenerates only a
drifted projection. It refuses invalid event history without changing the log
or projection.
Publishing a state change
Every command above that appends an event (add, claim, done, block, park,
priority, dep, note, annul) runs the same publish sequence. It fetches the
remote named origin, folds the union of the local log and the remote
default branch's committed shards, appends the event, regenerates the
board and item pages, commits the shard and the projection with the
repository's hooks running, and pushes the remote default branch when that
branch is checked out. This is what makes a claim or a completion visible
to every other clone as soon as the command returns.
The commit carries the resolved tracker root in its environment, as
PINAX_ROOT. The sequence gives a hook no arguments, so a hook that calls
Pinax back, such as the pre-commit hook pinax init installs and the
pinax verify it runs, checks the tracker the command resolved rather
than whatever root the surrounding environment happened to name.
Every one of these commands also refuses an actor that is not written as
role@host, before it appends anything.
All of them except claim accept --offline, or the PINAX_OFFLINE=1
environment variable, to skip the fetch and the push and commit locally
only. A repository with no reachable origin, or no published default
branch, behaves the same way on its own: the command commits locally and
prints a warning naming the remote, instead of failing. A directory that
is not a git repository at all falls back the same way, appending and
regenerating the projection with a warning and no commit attempted, since
there is nothing to commit into. claim is the one exception. Its whole
purpose is telling every other machine an item is taken, so it always
needs the remote: without a reachable origin, or without a git
repository to fetch from, it ends without appending anything at all,
rather than recording a claim nobody else can see.
The exit codes these commands share are:
0 the event is committed, and published when the remote default
branch is checked out
2 the actor is not written as role@host; nothing was appended
4 claim needed the remote and could not reach or read it, or any
command's remote answered but its committed events could not be
read
5 the push was rejected on every attempt
7 the shard and the projection could not be staged, or the commit
was refused; the appended event stays uncommitted either way
A hook that refuses the commit is never worked around. The command prints
the hook's output, names the shard and the event id, leaves the appended
event uncommitted, and ends with exit 7; fix what the hook objected to and
commit the shard and the projection. A .gitignore rule that swallows
.ergon/log/ is one of these refusals: the installed hook's pinax verify
fails while the log is ignored, so a state-changing command ends with exit
7 instead of committing an event the repository would silently keep to
itself. Run pinax init again to reinstall the .ergon/.gitignore
negation, or fix the repository's own .gitignore, then commit the shard.
registry, reconcile, and init do not run this sequence. They keep
their own local behaviour: registry records and lists remote
repositories for pinax overview --remote, reconcile imports
already-completed offline work from a text file into ordinary events, and
init creates .ergon/ itself. None of the three needs cross-machine
visibility the moment it runs, so none of them fetches or pushes.
pinax doctor reports two separate things a git-native tracker can be
behind on: shards committed to HEAD but not yet on the remote default
branch (unsynced, a normal state for offline work waiting to be pushed),
and events appended to the working tree but not yet committed at all
(uncommitted, an orphaned trail worth investigating). They are listed in
distinct sections because they call for different next steps: push one,
commit the other.
Storage model
.ergon/
log/*.jsonl append-only event shards
board.md generated project board
items/<id>.md generated item pages
Each event has a content-derived identifier. The fold sorts events by
(seq, ts, actor, id), deduplicates by identifier, and applies the resulting
stream deterministically. Git's union merge driver preserves concurrent JSONL
appends; a duplicated line is a no-op in the fold.
Claims resolve during the fold. If two claims name one item, the earliest
(ts, actor, id) claim wins and the other becomes a reported supersession.
Dependency edges drive ready and next.
A live claim also makes its item ineligible for ready and next, on
every clone, from the moment the claim reaches that clone's log - a claim
and a status change are two different events, so a claimed item can still
show status queued. pinax status renders such an item in its building
list with the owner and how many hours ago the claim was made, labelled by
the claim itself rather than the item's creation time, even before its
status field ever moves to building. A held claim ends when it is
released with pinax release ITEM_ID --reason TEXT, or when it expires:
the policy in force is the last pinax policy claim-expiry --hours N
event at or before the claim, and twenty-four hours by default when the
log carries none before it. A quiet log - one that records nothing after
the claim - never expires it, so a claim only ends when the log itself
records the release or a later event past the deadline.
--under ITEM_ID on ready, next, and status restricts the view to
the transitive parent-child descendants of that item - the ancestor's
own row is never included, only its descendants; an id that names no item
in the log is refused, nothing is appended. status --under requires
repo scope; it is refused together with --portfolio and with the
two-argument setter form. It scopes the building/shipped/parked lists and
the next/queue_depth figures; the repo-wide claim-reconciliation notice
count is unaffected.
next --json and status --json were already JSON objects, so both
simply gain an under key: the ancestor id, or null when --under is
not given. ready --json was a bare array of item ids, and a JSON array
cannot carry a key, so it keeps that exact shape when --under is not
given; with --under it becomes {"ready": [...], "under": "<id>"}.
ready --json --all-branches was already an object before --under
existed, so it also just gains the under key.
An item with at least one parent-child child carries a derived
rollup value beside its own status - done, building, blocked,
or queued - summarising its children without ever changing the
item's own status or its eligibility for ready/next. pinax board
and pinax status render it alongside the status wherever it is
present; the rule the fold applies is in DESIGN.md.
The predecessor field is a local consistency check. It can report a dangling predecessor reference, but version 1 does not hash the predecessor field or provide remote anchoring, signatures, or hostile-writer authentication.
The event envelope, integrity rules, and projection model are described in DESIGN.md. The executable event handlers and renderers are the authoritative implementation details. Architecture decisions are in docs/decisions.
Licence
Pinax is available under the MIT License.
Release files for pinax-tracker 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pinax_tracker-0.2.0.tar.gz | 307.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pinax_tracker-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size:472.7 kB
Release files / pinax_tracker-0.2.0.tar.gz
| Download URL | pinax_tracker-0.2.0.tar.gz |
|---|---|
| Size | 307.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9ed83799daaed4cd68ab2bc075bd64a574eb69e4786874bf0d21206a7ef6e31a
|
|
BLAKE2b-256 checksum How to use checksums |
77e41f8eacd60d1b579224ae8e9d0ecb4954dedaa5e51ff2509f52673348a424
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.
Transparency logRelease files / pinax_tracker-0.2.0-py3-none-any.whl
| Download URL | pinax_tracker-0.2.0-py3-none-any.whl |
|---|---|
| Size | 164.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
63aa5f799437c6fbd324edb5dc31cc49b8dd6913407b9852bad941037810b4db
|
|
BLAKE2b-256 checksum How to use checksums |
36dbb2dd9591b1257c8128814d64e950fe56f6bec18e3ae0169a10167a1ecf6a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.
Transparency log