Skip to content

Repository files navigation

ticketsplease

git-native parallel-work ticketing — weave parallel work threads into one fabric

ticketsplease (short alias tkt) manages development work as git-versioned markdown tickets carrying dependency and affected-area metadata, and computes conflict-aware parallel-work recommendations so multiple workers — primarily AI coding agents, humans secondarily — can judge what to dispatch from declared scope and claim context. No server, no database: GitHub stays git-only.

It's driven from the command line and built to be scripted: every command speaks JSON, exit codes are the API, and output is deterministic. Ticket detail commands include complete comment threads; collection commands carry a comment count so discussion is never silently hidden.

The two commands that matter

tkt tracks --format json          # recommended batches + live/stale claim context
tkt guard <branch> --format json  # exit 6 iff a branch's actual diff escapes its
                                  # ticket's declared scope (an overlap with an open
                                  # ticket is a non-failing WARN by default)

init / create / set / link / show / list / ready / next / lint / delete / rename are the convenience surface around those two; list --where filters with boolean expressions and view saves them as named views; rollup aggregates an initiative (counts, % done, ready frontier, blocked set) and graph / path export the dependency DAG (Graphviz DOT, critical path); tracks --max-overlap / lanes tune how parallel you go (tolerate benign overlap, or sequence conflicts onto per-worker lanes instead of idling); claim / release / claims / next --claim provide race-safe pull-based dispatch; status --all-branches, reconcile, watch, comment, and events --watch give an orchestrator a live view of — and an append-only, conflict-free annotation channel for — workers running on their own branches (reconcile flags where the board has drifted from the actual branches/worktrees); and run executes named recipes — typed, parameterized procedures over these same commands (e.g. run supersede --arg id=X --arg with=A,B re-points X's dependents onto A,B and closes X), defined in [recipe.<name>] or .ticketsplease/recipes/ and invoked explicitly (not a [workflow] state machine, and no event triggers). New to the model? tkt guide prints it in one screen, and tkt doctor verifies setup.

Install

From source (works today):

cargo install --git https://github.com/moderately-ai/ticketsplease --locked ticketsplease-cli

This installs the ticketsplease binary. Symlink a short alias if you like: ln -s "$(command -v ticketsplease)" ~/.local/bin/tkt.

Prebuilt binary via the installer (once a release is tagged):

curl -fsSL https://raw.githubusercontent.com/moderately-ai/ticketsplease/main/install.sh | sh

The installer detects your platform, downloads the matching static binary, verifies its SHA256, installs to ~/.local/bin, and symlinks tkt. Pin a version with TICKETSPLEASE_VERSION=v0.1.0, or change the directory with BIN_DIR=….

Quickstart

tkt init                              # scaffold tickets/ + ticketsplease.toml + the Claude skill
tkt guide                             # the conceptual model in one screen
# edit ticketsplease.toml to define your scopes (see below)
tkt create --title "Add vector index" --priority p1 --scope query/planner
tkt create --id build-index-trait --title "Build the index trait" --scope core
tkt create --from backlog.toml          # batch JSON/TOML; transactional, dry-run shows final ids
tkt link add-vector-index --depends-on build-index-trait
tkt ready                             # what's dispatchable now
tkt list --where 'priority:p0 AND NOT status:done'   # boolean filter (AND/OR/NOT, parens)
tkt view save epic 'tag:epic AND NOT status:done'    # save a reusable named view, then: tkt list --view epic
tkt tracks                            # recommended parallel batches + claim context
tkt next --parallel 4                 # four disjoint picks for four agents
tkt guard my-branch                   # gate a branch before merge (exit 6 = conflict)
tkt status --all-branches             # each worker's tip status across tkt/* branches
tkt show add-vector-index             # detail + comments from this tree and tkt/add-vector-index
tkt watch add-vector-index --until review --timeout 600  # block until a worker is ready (exit 7 on timeout)
tkt lint                              # validate schema, links, and cycles

The model

A ticket is a markdown file under tickets/ with YAML frontmatter:

---
id: add-vector-index          # slug; equals the filename
title: Add vector index
status: todo                  # built-in: todo|ready|in-progress|blocked|review|done|closed (or your [workflow.states])
priority: p1                  # p0 (highest) .. p3
dependencies: [build-index-trait]   # hard, scheduling-blocking; cycle-checked
related: []                   # soft "see also"; ignored by scheduling
scopes: [query/planner]       # exclusive (rewrite) area claims
shared_scopes: []             # additive (append) claims — co-edit freely
paths: []                     # extra explicit globs
tags: []
# any custom keys you add are preserved verbatim
---
Free-form markdown body.

Edits are round-trip-safe: ticketsplease rewrites only the field it changes and leaves unknown keys, key order, comments, and the body byte-for-byte. Hand-editing is fully supported.

Terminal states — done vs closed. Both take a ticket out of scheduling, but they mean different things. done = completed: it satisfies dependents, so they become ready. closed = terminated without completing (won't-do, duplicate, obsolete, superseded, cancelled): it does not satisfy dependents — instead they surface as orphaned (rollup lists them, lint fails on them, and claim refuses with a pointed message) so you re-point, waive, or close them rather than silently building on abandoned work. tkt close <id> --reason <duplicate|wontdo|obsolete|superseded|cancelled> --note <text> records an optional resolution; tkt reopen <id> returns it to an active status and clears the reason in the same write. The reason is queryable (list --where 'reason:duplicate').

A scope is a stable abstract name for an area of the codebase. Tickets reference scopes; ticketsplease.toml maps them to file globs (and, for Rust repos, to crates). Scheduling uses scope access intent, configured overlap budgets, and current claim leases to build a recommended front; the guard separately fails a branch that touches a scope its ticket did not declare.

Access intent. A scope can be claimed exclusively (scopes — a rewrite) or shared/additively (shared_scopes — append/extend). Two shared claims on a scope have zero conflict cost; a shared claim still costs against an exclusive one. tracks/next/lanes take --max-overlap K, a per-pair tolerance budget (0 strict … any). Their JSON includes claim_overlaps and stale_claims; live overlaps beyond the budget are left out of the recommended front, stale leases remain advisory, and --ignore-claims requests the unfiltered view without discarding the context. tracks --width reports recommended additional capacity, while lanes plans ordered per-worker queues. [scope_policy] weights a scope's clash cost (0 = free hub). The guard reports shared-by-both collisions as non-gating.

Configuration — ticketsplease.toml

schema_version = 1
tickets_dir = "tickets"
default_base = "main"          # base ref for `guard`

[defaults]
shared_scopes = ["project/tickets"] # merged into create/claim; explicit exclusive wins

[output]
comments = "auto"              # detail=full thread, collections=count; full|summary|none override
comment_source = "all"         # union worktree + tkt/<id>; or worktree|ticket-branch

[language]
backend = "rust"               # "none" (path globs only) or "rust" (also use the cargo crate graph)

[scopes]                       # scope name -> path globs
"query/planner" = ["crates/query/src/planner/**"]
"core"          = ["crates/core/**"]

[scope_crates]                 # scope -> owning crate, so the guard expands reverse-dependents
"core" = "my-core-crate"

[external_scopes]              # name a forked dep (pinned via git=…rev=…) as a scope
"sqlparser-fork" = { repo = "tomsanbear/sqlparser", paths = [] }

[scope_policy]                 # per-scope clash cost for tracks/next --max-overlap
"core" = { weight = 0 }        # weight 0 = a free-to-co-edit hub; higher = riskier (default 1)

[workflow]                     # custom lifecycle states (optional; omit for the built-in set)
enforce_transitions = false    # opt-in state machine; default off (any state -> any state)
[workflow.states.todo]
category = "dispatchable"       # dispatchable | open | parked | terminal — the engine contract
[workflow.states.qa]
category = "open"              # occupies its scopes for the guard; excluded from `ready`
[workflow.states.shipped]
category = "terminal"
satisfies_dependents = true    # a completed terminal state — unblocks dependents
[workflow.states.wontfix]
category = "terminal"
satisfies_dependents = false   # a dropped/cancelled terminal state — orphans dependents
[workflow.transitions]         # only consulted when enforce_transitions = true
todo = ["qa"]
qa   = ["shipped", "wontfix"]
"*"  = ["wontfix"]            # escape hatch: cancel from any state

[defaults].shared_scopes removes repetitive declaration work for additive areas such as tickets/**. create writes the defaults into new frontmatter and claim backfills older tickets atomically; the declaration remains visible to scheduling and guard audits. Unknown defaults are lint findings and claim-time errors.

When backend = "rust", the guard maps a branch's changed files to crates and walks the cargo reverse-dependency graph: a change to a leaf crate is flagged against every crate that depends on it. This needs cargo on PATH (always true inside a Rust repo). Each collision is tagged cause: "direct" (a real file/crate overlap) or "transitive" (reached only via the reverse-dep walk — safe for an additive change), and a per-scope affected_causes map lets a consumer triage which under-declarations and collisions are real rather than hand-diffing. Pass guard --direct-only (alias --no-reverse-deps) to gate on direct overlap only and skip the expansion entirely.

[external_scopes] extends the guard beyond this repo: a branch that bumps a pinned git = … rev = … dependency (matched by repo against the changed manifest lines) — or edits an in-tree fork paths glob — is flagged against tickets declaring that external scope. Because external scopes are ordinary scope names, tracks already keeps two tickets touching the same fork in separate batches.

Custom workflow states. With no [workflow] table a repo uses the built-in states (todo, ready, in-progress, blocked, review, done, closed). Define [workflow.states] to declare your own — each state's name is yours to choose, but it must be pinned to one engine category the scheduler/guard/rollup reason about: dispatchable (pickable), open (occupies its scopes for the guard, blocks conflicting parallel work), parked (held but not finished, like blocked), or terminal (finished, excluded from scheduling). A terminal state's satisfies_dependents bit is the done-vs-closed distinction — true unblocks dependents, false orphans them. tkt states lists the effective registry; tkt lint rejects a config with no dispatchable or terminal state; tkt migrate --remap old=new moves tickets stranded by a renamed/removed state. Because the engine keys on the category (never the name), renaming a state while keeping its category never breaks scheduling.

Enforced transitions (opt-in). Set enforce_transitions = true and declare a [workflow.transitions] adjacency map (from = [to, …]) to make set/close/reopen reject any move that isn't a listed edge — for a QA gate or a no-skip-review rule. It is off by default (any-to-any): the engine's real invariants ride on categories, not edges, so the transition graph is a convenience constraint, not a requirement — add it only when you have evidence you need it. A "*" source is an escape hatch (e.g. close-from-anywhere), --force overrides a single move (recorded as forced on the event), and claim/release are engine transitions exempt from the graph. Bulk set --where advances the legal matches and reports the rest as skipped rather than aborting. tkt lint flags transition edges that name undefined states and non-terminal dead-ends (a state with no way out).

Maintenance advisories (interactive only). In an interactive human session the CLI may print a stderr hint after a command — an update is available, the repo has drifted (run tkt migrate), or the board has lint findings. They are gated off for every non-interactive caller (--format json, no TTY, or CI set), so agents and pipelines never see them and stdout stays a clean data channel. [maintenance] tunes them: update_check (default true; probes the latest release at most once per check_interval_hours, default 24) and auto_migrate (default false — when true, an interactive session auto-applies the drift repair instead of only nudging; the --auto-doctor flag does the same for one command). TICKETSPLEASE_NO_ADVISORIES=1 disables all of them.

Comment visibility. Global --comments auto|full|summary|none and --comment-source all|worktree|ticket-branch override [output] for one invocation. auto shows full threads for show/comment list, adds comment_count and comment_sources to ticket collections, and leaves unrelated results alone. all deduplicates by comment id across the current worktree and matching tkt/<id> branch while retaining sorted source provenance. --ref remains an exact-source detail read.

Tool-managed state — .ticketsplease/

Saved views (and bundled body templates) live under .ticketsplease/ at the repo root. Unlike most tool dot-dirs this is meant to be committed — a saved view like "the open p0/p1 epic" or a shared ticket-body template is a project artifact, not local state. Don't add it to .gitignore.

The contract

  • --format json on every command yields a stable, versioned payload (schema_version: 1), deterministically ordered (sorted keys, no timestamps) — safe to diff and cache.
  • Comment fields are additive within the existing schema versions: summaries use comment_count/comment_sources; full comment objects also carry sources.
  • Exit codes are the API: 0 ok · 2 usage · 3 invalid/dirty · 4 not found · 5 dependency cycle · 6 conflict · 7 watch timeout.
  • Every command takes --repo <path>; everything is offline and atomic.

The agent skill

tkt init (and tkt skill install) wire the bundled skill into your agent harness's skills directory — Claude Code by default (.claude/skills/ticketsplease/). It teaches an agent the orchestration loop — tracks to fan out disjoint work, guard to gate each branch before merge.

The skill contains SKILL.md, progressive-disclosure references/, and Codex-facing agents/openai.yaml metadata. The major coding agents consume the same bundle, so --harness selects where it installs: claude.claude/skills, codex.agents/skills (the cross-tool Agent Skills standard directory, also read by opencode and Pi), opencode.opencode/skills, pi-agent.pi/skills. --global installs into the harness's user-global dir instead of the repo.

The skill is embedded in the binary, but instead of a frozen per-repo copy it lives once at a canonical per-user path (~/.local/share/ticketsplease/skill) and each install is a symlink to it. The installer runs tkt skill sync after every install/self-update, so the canonical copy — and therefore every linked project — always matches your binary; tkt doctor warns if it drifts and tkt migrate repairs a stale link. A project link is local, so init gitignores it; use tkt skill install --copy if you'd rather commit a real copy.

Dogfooding

ticketsplease tracks its own development. See tickets/ and ticketsplease.toml in this repo: tkt tracks reports which of the remaining work items can proceed in parallel.

Status

v0.1, pre-release. The core is in place and tested: round-trip frontmatter editing, scheduling (ready/tracks/next), the conflict guard (path-glob + cargo crate-graph), and the bundled skill. Release binaries, self-update, and the migration engine are in progress (tracked as tickets in this repo).

License

MIT — see LICENSE.

About

git-native parallel-work ticketing — conflict-free parallel work assignment over git-versioned markdown tickets (Rust CLI)

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages