Skip to content
What is Tisket?

What is Tisket?

Tisket stores issues as markdown files in the git repository they describe.

Why files in git

Most issue trackers are web applications. The issue data is on another company’s server, reached through a browser or an API, and the data is unavailable when the network fails. Tisket puts an issue with the code it describes.

The issues and the code are in the same tree. One branch holds both the fix and the issue state change. git clone gives you the full issue history. Tisket needs no account, no API token, and no webhook. You can create, edit, and search issues offline. Git operations do the sync.

A coding agent works with issues as file operations. It reads a file and writes a file. It needs no API client and no authentication flow.

The file format

An issue file has three sections: YAML frontmatter, a markdown body, and an optional scratch notes section.

Frontmatter comes between two --- fences. It holds the structured fields: title, status, priority, assignee, due_date, labels, depends_on, children, created, and updated. Tisket reads these fields to list and filter issues. The labels, depends_on, and children fields are arrays. Tisket sets the created and updated timestamps itself. A tags mapping holds your own key and value pairs, and any other key you write survives an edit untouched.

Body is free-form markdown. It comes after the closing --- and before the scratch notes header, or the end of the file. Put the issue description, the acceptance criteria, and any context links here. A person or an agent reads the body to learn what the work is.

Scratch notes come below a ## Scratch Notes header at the end of the file. This section has its own read, write, append, and clear operations, and none of them changes the body.

A minimal issue file looks like:

---
title: Fix the widget
status: todo
priority: null
assignee: null
labels: []
depends_on: []
created: 2026-03-20T14:00:00Z
updated: 2026-03-20T14:00:00Z
---

The widget throws an error when given empty input.

## Scratch Notes

Reproduced locally — the bounds check is missing on line 42.

The status lifecycle

Active statuses (the issue is open):

  • discovery — The issue exists, but the scope is not clear enough to start work. Someone is still gathering the requirements, or the approach is undefined.
  • todo — Ready for pickup. The scope is clear enough to start the work.
  • in_progress — A person or an agent is working on the issue now.
  • blocked — The work cannot continue until an external dependency clears.
  • paused — The work started, then someone suspended it on purpose. This differs from blocked. Nothing external stops the work. The pause is a choice.

Terminal statuses (the issue is closed):

  • done — Someone completed the work.
  • cancelled — Someone abandoned the work. The work is unnecessary, or another issue replaced it.

todo, blocked, and paused are pickable. An agent workflow picks up only those. It rejects an issue in discovery, because the scope is not clear, and an issue in in_progress, because someone already works on it.

When you close an issue, the file moves from the project directory into a .closed/ subdirectory. When you reopen the issue, the file moves back. The directory structure records the open or closed state, not only the frontmatter field. You can therefore count the open issues with ls.

There is one legacy alias. Tisket parses backlog as todo.

The short ID system

Every issue gets a filename such as ab12-fix-the-widget.md. The filename has a 4-character random prefix, a hyphen, and a slug from the title.

Tisket makes the slug from the title. It converts the title to lowercase. It replaces each non-alphanumeric character with a hyphen. It then collapses repeated hyphens and removes any hyphen at the start or the end. “Fix the Widget!” becomes fix-the-widget. A slug must be unique across every project in the repo. If you create an issue with a duplicate slug, tisket returns an error. The prefix does not make a duplicate slug unique.

The prefix is 4 characters from the set [a-z0-9]. Tisket generates it at random and compares it against every existing prefix in the repo. The alphabet has 36 characters and the prefix has 4 positions. There are therefore about 1.7 million possible prefixes. A collision is unlikely, but tisket checks for one anyway.

The combined ID (ab12-fix-the-widget) is the filename stem and the canonical identifier. Tisket also accepts three shorter forms:

a. Full ID — an exact match against the filename stem. b. Short prefix — only ab12. Tisket scans every project for a file that starts with ab12-. If more than one issue has that prefix, tisket returns an error. c. Slug — only fix-the-widget. Tisket takes the slug part of every prefixed filename and compares it. If more than one issue matches, tisket returns an error.

Use the short prefix by hand. Use the full ID in a script.

Scratch notes

Each issue file can have a ## Scratch Notes section below the body. This section is the working memory for an agent across sessions. At the end of a session, the agent writes what it learned into the scratch notes. At the start of the next session, an agent workflow loads those notes into the context.

The scratch notes have their own append, write, read, and clear operations. An agent updates the notes and does not change the body. The body describes the work. The scratch notes record the progress of the work.

Git-aware divergence detection

When tisket lists or shows an issue, it reads the file from the disk. It also reads the same file path from the tree of every other branch. This includes local branches and remote branches, but not the current HEAD. Tisket then parses each version and compares the content field by field.

A branch can hold a different version of the issue. The status, the title, the priority, the body, or the scratch content can differ. Tisket then marks the issue as divergent. The detailed view names each branch and each field that differs.

People and agents change issues on branches. An agent picks up an issue on main. The pickup sets the status to in_progress and creates a worktree, and the work then happens on the branch. Meanwhile someone can edit the priority on main, or another branch can change the title. Divergence detection shows you the conflict before the merge.

The comparison is structural, not textual. Tisket parses the frontmatter from each branch version. It compares the title, status, priority, assignee, due_date, labels, and depends_on fields. It also compares whether the body and the scratch content exist. A whitespace change therefore does not produce a false divergence.

Using tisket in an agent workflow

Tisket is a standalone tool. An agent harness can also use tisket as its issue source. The harness supplies the session behavior, and tisket supplies what that behavior reads.

At session start a harness reads the tisket state: the open issue count, and whether the current branch name resolves to an issue ID. If an issue matches, the harness puts the title, the body, and the scratch notes into the starting context. The agent runs no command to learn what its work is.

The pickable statuses and depends_on give the harness a gate. It picks up only a todo, blocked, or paused issue whose every dependency is closed, sets the status to in_progress, and starts the work. Tisket does not enforce that rule. It stores the status and the dependency list, and the harness decides what they allow.

The short ID is a stable key. A harness names the worktree and its branch after the issue ID. It then passes the branch name back to tisket issue show. That command resolves a full ID, a 4-character prefix, or a slug, and so finds the active issue for the session.

For CLI usage details, see the CLI reference. For the issue file format, see the file format reference. For day-to-day issue management, see the workflow guide.

Composed trackers

A tracker can declare the other trackers it reads. The declarations live in stores.yml, each under a local alias:

format: 2
stores:
  - alias: a
    path: ../project-a
  - alias: b
    git: https://example.com/org/project-b

An epic is an issue that lists its children. A child can live in another tracker: children: [a:x9k2, b:m4p1]. issue show gives the status of each child and the count that is done.

Containment is not blocking, so depends_on keeps its meaning. An entry in either field names another tracker only when the text before the first colon is a declared alias. An entry such as x: y therefore keeps the meaning that it always had.

The declarations set the direction. A user-level tracker declares the project trackers that it follows. A project tracker does not declare the user-level one. It cannot: the target does not exist for the other users who clone the project. A dependency tracker is read-only, so an edit runs from the tracker that owns the issue.

tisket store list shows the trackers. tisket store sync fetches the remote ones. tisket check reports a reference that names no issue and an unreachable tracker. It also reports a cycle in the children of an epic, and a declaration that other clones cannot follow.

Root resolution

Without --root, a command finds its tracker by one rule, identical in zettel and almanac: the nearest tisket.yml at or above the working directory wins. The walk requires a regular file. It stops at the first directory the invoking user does not own, so a marker planted in a shared ancestor captures nothing. With no tracker found, a read falls back to the root tracker set in ~/.config/tisket/config.yml and says so on stderr. A write never falls back. It fails and names --home. No environment variable participates: an env var is the one input an agent cannot see in a transcript, and a repository can set one through direnv or mise. The config path itself is fixed, and the home directory comes from the passwd database, not $HOME, for the same reason.

A write prints its resolved target on stderr unless the target is the working directory’s own tracker or --root named it. A read resolved by walking or by fallback prints its source.