Keyboard-driven terminal ticket explorer for a tree of markdown briefs. Linear-like TUI, zero deps beyond the Python stdlib + an optional markdown pager.
LANES STATE CTX
◐ P1 teams-error-mapping active 74K
○ P0 oauth-rotation-plan open —
● audit-logs-rollout done —
- The filesystem is the database. A ticket is a markdown file.
grep,git log, andlsall keep working. - No SaaS, no auth, no network. Runs entirely against a local tree.
- Linear-like keys.
j/k,/to filter,Enterto open,pto pick up into awtlane. - Pure reader. tix never writes
status:for you. If you want auto-status derivation, wire up your own preload hook (see below).
pipx install tix-cli
# or, in a venv:
pip install tix-cliInstalls one console script: tix.
git clone https://github.com/gitpancake/tix
cd tix
pipx install --editable .mkdir -p ~/.pi/agent/tickets/spikes
cp $(python -c 'import tix, pathlib; print(pathlib.Path(tix.__file__).parent / "templates" / "_TEMPLATE.md")') \
~/.pi/agent/tickets/spikes/my-first-ticket.md
tixOr point at a project tree:
tix my-project # browses ~/.pi/agent/tickets/my-project plus
# ~/.claude/tickets/my-project when both exist, and
# chdirs into ~/Documents/code/my-project so pickup
# (p) runs against the right repo
TIX_CODE_DIR=~/src tix my-project # override the code-repo lookup root
TICKETS_DIR=./docs/tickets tixFor zsh users who want per-project TICKETS_DIR to follow cd, add this chpwd hook to ~/.zshrc:
autoload -U add-zsh-hook
_tix_tickets_dir() {
local root base
root=$(git rev-parse --show-toplevel 2>/dev/null) || { unset TICKETS_DIR; return; }
base=${root:t}
if [[ -d "$HOME/.pi/agent/tickets/$base" ]]; then
export TICKETS_DIR="$HOME/.pi/agent/tickets/$base"
else
unset TICKETS_DIR
fi
}
add-zsh-hook chpwd _tix_tickets_dir
_tix_tickets_dirFor narrow sidecars (think tmux pane next to an editor):
tix --mini # flat reverse-chrono list, ≥20 cols
tix my-project --mini # same project resolution as `tix <project>`Mini surfaces a single signal — what was filed recently, can I pick one
up — and gets out of the way. Done/cancelled hidden; ↑/↓ move, Enter
opens in glow/$PAGER, p spawns wt, l sets/clears label:, q
quits. No filters, no groups, no rescope — use plain tix for that.
| Key | Action |
|---|---|
↑ ↓ / j k |
Move |
Ctrl-U Ctrl-D |
Half page |
g G |
Top / bottom |
Enter → |
Open in glow (or $PAGER) |
Esc ← h |
Collapse / back |
/ |
Filter |
e |
Edit in $EDITOR |
p |
Pickup → wt <slug> |
i |
Pin status active |
d |
Pin status done |
x |
Pin status cancelled |
l |
Set/clear label: |
m |
Move ticket to area |
y |
Copy slug to clipboard |
o |
Open Linear URL (if linear: set) |
r |
Reload (re-runs preload hook if set) |
? |
Help |
q |
Quit |
A ticket is a markdown file with YAML-ish line-based frontmatter:
---
status: open
priority: P1
label: backend
area: integrations
linear: PROJ-123
---
# teams-error-mapping
## Context
…
## Acceptance criteria
- [ ] …Full contract: docs/ticket-schema.md.
- Filename is the slug.
teams-error-mapping.md, neverPROJ-123.md. - Epic = folder. A directory containing
_epic.mdis an epic; numbered children (01-foo.md,02-bar.md) are its stories. - Status vocab is pinned:
active,open,draft,done,cancelled.
| Env | Default | Purpose |
|---|---|---|
TICKETS_DIR |
~/.pi/agent/tickets |
Primary ticket tree / write root |
TIX_EXTRA_TICKETS_DIRS |
(unset) | Extra read roots, separated by : on macOS/Linux |
TIX_PICKUP_AGENTS |
(unset) | Per-root agent launcher for p: <root>=<cmd> pairs, separated by :. When a picked-up ticket lives under <root>, wt runs with WT_AGENT_CMD=<cmd>. No match → wt's own default. Longest-ancestor root wins. An explicit WT_AGENT_CMD in the environment overrides the map. |
TIX_CODE_DIR |
~/Documents/code |
Lookup root for tix <project> — the repo it chdirs into so pickup works |
ACTIVE_LANES_FILE |
~/.claude/active-lanes.json |
Optional sidecar map: slug → {path, branch, repo, last_commit}. Read by the TUI; tix never writes it. |
LINEAR_WORKSPACE |
(unset) | Slug used to derive linear: URLs (o key) |
TIX_PRELOAD_HOOK |
(unset) | Shell command run before launch. See below. |
EDITOR |
vi |
Used by e |
PAGER |
less |
Fallback when glow is absent |
TICKETS_DIR resolves in this order: explicit env var → ~/.pi/agent/tickets. When TICKETS_DIR points at a project under either ~/.pi/agent/tickets/<project> or ~/.claude/tickets/<project>, tix also reads the matching other-side project tree if it exists, so Pi-created and Claude-created tickets both surface. tix <project> resolves the primary write root (centralized ~/.pi/agent/tickets/<project>/ first, legacy centralized ~/.claude/tickets/<project>/ second, then $TIX_CODE_DIR/<project>/.claude/tickets/, then ./<project>/.claude/tickets/ for legacy trees), adds any other existing project roots as extra read roots, and chdirs into the project's git repo under $TIX_CODE_DIR (default ~/Documents/code) so the pickup key (p) runs wt against the right repo. Or set TICKETS_DIR explicitly (e.g. via the chpwd hook above).
tix doesn't write status: — that's deliberate. If you want statuses derived from external signals (live worktrees, feature branches, merged PRs, calendar events, anything), put a script on disk and point at it:
export TIX_PRELOAD_HOOK=~/bin/my-status-sync
tixThe hook runs once before the TUI is drawn. Its stdout/stderr are discarded — curses is about to claim the screen, so the next render is the feedback, not the printed diff. The hook is best-effort: a missing or failing command never blocks launch.
The r key in the TUI re-runs the hook and reloads.
A reference implementation (filesystem + git + gh) lives in gitpancake/.dotfiles as claude/scripts/ticket-status-sync.py — it derives active from live worktrees and done from merged PRs. Copy it, fork it, replace it.
wt— if awtcommand is on PATH, thepkey suspends curses, runsgit fetch && git checkout main && git merge --ff-only && wt <slug>, then resumes.glow— preferred markdown pager for ticket preview. tix reflows hard-wrapped briefs and pipes them to glow word-wrapped at the terminal width, so authored line width never causes ragged re-wrapping. Falls back to$PAGER(defaultless).gh— used by some preload hooks (not by tix itself).
- No remote sync, no auth, no web UI.
- No mouse support.
- No notifications.
- No bundled status reconciler — wire your own via
TIX_PRELOAD_HOOK.
MIT.