Pluggable personal issue tracker that hands tickets off to coding agents.
File a ticket in the browser, then from its kebab menu: copy a composed agent
prompt, open a terminal (Windows Terminal, Tabby, …) already running
claude on the ticket, or AI-enrich the ticket with full undo history.
| Package | What |
|---|---|
@aylith/tickets |
Everything in one — both CLIs (tickets, tickets-tui) plus the libraries below re-exported as subpaths (. = core, /client, /ui, /server) |
@aylith/tickets-core |
Types, markdown ticket format, storage adapters (git data branch / plain folder), prompt composer |
@aylith/tickets-server |
tickets CLI (init, serve, tui, list, migrate, converge, rename, adopt) — Hono daemon: REST API, SSE, terminal launch, AI enrich, media pipeline |
@aylith/tickets-ui |
Framework-agnostic Lit web components (<ay-ticket-list>, <ay-ticket-card>, …), themeable via CSS custom properties |
@aylith/tickets-tui |
Terminal UI (tickets-tui / tickets tui) — browse and act on tickets across all projects, Ink + React |
apps/web |
Central UI served by the daemon — all projects at /, per-project at /<project> |
npm i @aylith/tickets # everything — then import '@aylith/tickets/ui', '/client', '/server'
npm i @aylith/tickets-ui # lean: just the web components (no ink/react/hono)Each package builds to dist/ via tsup (JS) + tsc --emitDeclarationOnly
(types). In-repo, the daemon/tests/web-build resolve packages from src
through the bun export condition, so no build step is needed to run locally.
bun run build # build all packages to dist/
bun run publish:all # build + bun publish each package (needs npm auth)CI (.github/workflows/): ci.yml runs tests/typecheck/lint/build on every
push and PR; publish.yml publishes the packages to npm on a published
GitHub release (auth via the NPM_TOKEN repo secret). landing-pages.yml builds the static
homepage in landing/ (its example prompt comes from the real composer) and
deploys it to GitHub Pages at https://tickets.aylith.com/ and /home/.
Tickets are markdown files (tickets/<id>.md, YAML frontmatter + body). The
default setup keeps them on a dedicated orphan tickets branch of the project's
own repo, checked out as a worktree under ~/.config/aylith-tickets/worktrees/.
Every mutation is one commit — title/description history and undo come from git,
not a bespoke store.
Each project points at its store through a StoreLocation, and the setups
coexist: repo-git (the default above), repo-folder (<repo>/.tickets),
central-git (one shared repo) and central-folder, both under
~/.config/aylith-tickets/store/. Pick one at tickets init --into <setup>;
migrate and converge move a project between them, and rename never moves
data. Every store is self-describing — a committed .tickets-store.json marker
carries a stable id — so renames, moves and reclones never orphan the data.
For the browser app, use the standalone v0.1.3 release.
Windows x64 users can download tickets-windows-x64.exe and run it directly or
rename it to tickets.exe on their own PATH. The shell installer below is for
Linux/macOS. No separate Bun install is needed for the standalone binary.
The default Git-backed init needs Git 2.42 or later: it uses
git worktree add --orphan, introduced in 2.42.
Git 2.35.1 rejects that command. The folder alternative below avoids that feature,
but does not provide Git-backed revision history or synchronization.
The npm0.1.3 archives contain libraries/CLIs but omit the browser application
assets. They are not a complete tickets serve browser install. This source
checkout includes later features; they are not automatically in release0.1.3.
Current local0.1.4 builds include dist/web inside the server package and check
those assets before packing. A fresh Windows npm consumer has verified browser
creation, narrow/keyboard recovery, external file updates and records surviving
a daemon restart. On 24 September 2026, a fresh isolated Linux npm consumer
of all five locally packed0.1.4 archives also verified the installed browser UI:
folder-backed creation, title/description editing, API retrieval, a daemon
restart using the umbrella Bun export, and the same edited ticket opening by
deep link. The source was 55496af6; retained local prepare/browser receipts are
under /home/stevenp/tickets-proof.vYtzfb/tickets-package-proof-VS8Dav/.
The installed server and umbrella archive SHA-256 hashes were
29d30907898f698fd546e909916cdf0e2766d970a2b22a125858b10c1a5f0a37
and 58c33a776e75ec3d99ada16e9a3c3eab511fa7f818326d5cf760efee152439d1.
The Linux proof uses a fresh task-owned profile/store and system Chromium; it is
not a published-registry installation, Steve's existing store, real-user
onboarding, the TUI, Git storage, remote access or a security assessment. The
daemon remains auth-less and should stay local. These repairs have not been published. Build with
bun run build before packing; bun run build:web also refreshes both the
checkout and packaged browser assets without a Unix cp dependency.
The same Linux checkout passed all 385 tests (2,789 assertions) with a task-owned
TMPDIR and bun run typecheck; the default /tmp on this host produced EDQUOT
and is not a product-test failure.
The source release gate now packs all five npm archives, verifies their exact dependency versions and browser-asset hashes, then publishes those same checked tarballs in dependency order. This gate has not published 0.1.4; public npm 0.1.3 still lacks the browser assets.
The repeatable local archive proof is scripts/package-first-use-proof.mjs:
prepare --bun <absolute-bun> --npm <absolute-npm> --git <absolute-git>, then
browser --root <root printed by prepare>. Set TICKETS_PROOF_BASE to a writable
task-owned directory outside the repo; on Linux, set TICKETS_PROOF_CHROMIUM
to a supported Chromium executable and use a short TMPDIR for its Unix socket.
Video recording is optional (TICKETS_PROOF_RECORD_VIDEO=1) and needs the
Playwright FFmpeg helper. Neither phase publishes or contacts an owner store.
Standalone binaries (no runtime needed) — tickets (daemon + CLI) and tickets-tui:
curl -fsSL https://raw.githubusercontent.com/aylith-labs/tickets/main/install.sh | bashmise:
mise use -g "ubi:aylith-labs/tickets[exe=tickets]" # release binary
mise use -g npm:@aylith/tickets # npm (needs Bun)npm (needs Bun to run — the daemon uses Bun APIs):
npm i -g @aylith/tickets # both CLIs: tickets + tickets-tuiFrom source:
git clone https://github.com/aylith-labs/tickets && cd tickets && bun install
bun run build:bin # → dist-bin/tickets, dist-bin/tickets-tuiChoose one storage mode in your local repository. The default example needs Git2.42+ and initializes a separate tickets worktree:
cd ~/projects/some-repo
tickets init # creates the orphan branch + worktree, registers the project
tickets serve # starts the daemon (all registered projects) + web UI
tickets tui # browse and act on tickets across all projectsFor the released0.1.3 plain-folder alternative:
cd path/to/your/repo
tickets init --adapter folder
tickets serveOpen the local URL printed by the daemon. Create a ticket, open it, choose Edit, save, then reload to check the result. A clean Windows x64 folder installation was verified through creation, title/description/status editing and retrieval after process restart. This does not verify TUI, the Git adapter, agents, providers or other operating systems. The release's390px create form overflows, and folder startup can warn about its not-yet-created storage folder; those release defects remain open. The daemon has no user authentication: keep it on a trusted local machine, not an untrusted network.
For an existing registered project, run the normal CLI in the foreground:
bun packages/server/src/cli.ts serve --local --project-id <exact-project-id> --port 6320Repeat --project-id <id> to select more than one project. This mode requires
exact stable IDs, binds only 127.0.0.1, skips registry reconciliation and shared
store discovery, and disables the selected Git adapters' automatic pushes in
memory. It does not rewrite configuration, mint IDs, repair/move stores or start
agents. Selected stores must already have matching version-1 markers; missing,
ambiguous, unavailable or mismatched selections fail before listening.
Git stores must resolve to the exact configured worktree root (the central
store root for central Git) and match their configured branch when specified.
Ticket edits still write to the selected stores and Git stores still commit
locally. Local mode makes terminal launch, enrichment, media publishing and
status hooks unavailable because they can execute or publish outside the
selected stores. No HTTP route can register/remove projects or change settings;
the running selection stays fixed until restart. This is not a read-only mode
or an authentication change. Plain serve retains its all-project
reconciliation and configured actions/push behavior. This option is not yet in
published packages.
Local mode also checks the actual listener host/port and the browser's exact origin before routing a request. Other local apps, opaque origins and foreign websites cannot read or mutate that daemon through cross-origin API requests; forwarded headers do not grant access. Following a link into the top-level UI still works. Native local clients without browser headers remain supported, so this is not user/tenant authentication or isolation from other local processes. Named reverse-proxy hosting is not enabled by this flag; do not alias another app or fixture server to make a hostname appear available.
The UI reports media upload availability separately from existing attachments. Local mode and unconfigured publication show a neutral explanation; older servers without capability metadata show unknown availability. No upload picker is offered until the server explicitly declares support. Existing evidence and ordinary ticket editing remain available.
Embedded hosts may pass requestObservation: createRequestObservation(sink) to
createApp. The synchronous sink accepts a frozen version1 request observation
and returns a boolean queue acknowledgement; it must not perform blocking work.
Only five finite labels are emitted: tickets-projects (GET), tickets-list
(GET), tickets-detail (GET), tickets-create (POST), tickets-update (PATCH).
Payload fields are event ID, epoch-ms occurrence, label, method, HTTP status and
bounded elapsed milliseconds. No project/ticket identity, URL/query, body, header,
exception or response content is emitted. Remaining encoded path aliases,
unknown routes, SSE, media and agent/triage operations are excluded. Runtime URL
normalization before middleware is not reversible or an access-control boundary.
snapshot() reports accepted/dropped counts; close() disables current/future
observations without cancelling product requests. Sink exceptions, rejected or
misused async acknowledgements cannot change the product response. The host owns
the separate bounded sender, current ingest grant, exact source/tenant mapping
and shutdown. This does not add identity to the auth-less daemon. Default startup
is unchanged: no automatic network delivery, process, monitoring or owner setup.
Local native browser proof connects this source to the private Hub collector, including lost-ack deduplication and independent Tickets/Hub restarts. Unexpected folder-list failures now surface as errors instead of empty successful lists; the UI offers keyboard Retry, preserves saved drafts and does not present an unread list as zero tickets. Missing/uninitialized folders remain empty. This is source verification with task-owned data, not published-package or owner parity.
createApp(context, {local: true, incidentTriage: options}) can register the
version1 GET|POST /api/incident-triage/incidents/<uuid> capability. It is absent
by default and not automatically activated by serve --local. Options require
an exact existing project ID, its actual FolderAdapter, matching marked
dataDir, one allowedScope, exact local hubOrigin and a current
authority(token) callback. Read/create roles are separate; empty, revoked,
expired, remapped and other-scope requests fail closed. Keep the existing local
host/Origin guard around the handler. This is not authentication for the rest of
the auth-less personal daemon, an owner account mapping or a named-host setup.
POST accepts {schemaVersion:1,incident:{id,scope,eventId,fingerprint,firstSeen,lastSeen}}.
It creates one ordinary Markdown ticket with provenance in the same file. A
complete staged file is exclusively hard-linked to a deterministic15-digit ID;
an occupied path must match the full source provenance, never be overwritten.
Retries, concurrent imports, lost acknowledgements and process restart return
the same work item and preserve edits/archive. Unsupported source versions and
collisions are explicit errors. This requires local filesystem hard-link support;
Git adapters are deliberately rejected. Host power loss/directory-journal
durability and changes by a hostile local filesystem writer are not claimed.
The minimal response contains version1, projectId and ticket {id,title,status,created}
(or null for an absent read), plus duplicate on POST. There is no Hub status
mirroring. The UI's source-incident link is validated against the exact project
and opens the Hub without credentials/referrer; Hub still requires its own read
grant. Unknown provenance survives ordinary editing but is not used as a link.
No external agent, provider, publication or registry repair is performed.
Every ticket exposes GET /api/tickets/<project>/<id>/prompt (plain text). The
terminal launch spawns your configured terminal running
claude "$(curl -fsS $PROMPT_URL)" in the project repo, flips the ticket to
in_progress, and the prompt instructs the agent to upload before/after media
and PATCH the ticket status when done.