A small, offline time tracker for one freelancer who bills by the hour. A tray
app and a tt command line are equal ways into one local SQLite file. The unit
is time; no money lives in the app.
The easiest way in: download the latest Linux or macOS app from the
releases page. Every merge to
main publishes a release, so it stays current.
To build from source instead, you need Node ≥ 22.5 — persistence is the
built-in node:sqlite, so there's no
native build step. The tray app needs Electron 35+ (its bundled Node must be
≥ 22.5); this repo pins Electron 42.
npm install
npm run build
npm run tt -- status # or: node packages/cli/dist/bin.js statusYour data is one SQLite file: $TT_DB if set, otherwise the per-OS app-data
directory (~/.local/share/stint/timetracker.sqlite on Linux,
~/Library/Application Support on macOS, %APPDATA% on Windows). Both surfaces
use the same path. Backup is copying the file. The app makes no network
connection except the optional, user-initiated update check (§17 R9), which
sends no data.
tt start "auth refactor" --client "Client A" --project API --tag deep
tt status # ▸ running 01:24:07 · "auth refactor" · Client A / API
tt stop
tt add "spec review" --from 13:00 --to 14:30 --client "Client A"
tt list --week
tt report --week --by client --round 15
tt export --month --csv -o june.csv
tt sleep ls # entries the machine slept through (detected by the GUI app while it runs)
tt sleep subtract 42 # exclude slept time (reversible)
tt status --json # --json on every read command, for scriptingTimes accept absolute (14:30, 2026-06-24T14:30) and relative (-90m,
-1h30m) forms. Read commands exit 0; refusals and errors exit non-zero.
npm run gui # needs an Electron binary (see Install)A tray timer counts up; one click starts or stops. The glyph itself says whether a
timer is running — two stacked bars idle, one fused block running — so the state is
visible at a glance on macOS and Linux alike, without hovering. Ctrl+Alt+T
toggles it from anywhere. The main window lays one week of entries on a calendar
grid — a month calendar picks the week, drag the grid to add, click a block to
edit, flags show in place — and builds reports with CSV/JSON export. Anything the
window does, tt does too.
One core, one file, two thin shells. A single SQLite file in WAL mode; all reads
and writes go through @stint/core; each write is one BEGIN IMMEDIATE
transaction with a busy timeout, so the CLI and the running app cooperate.
The keystone idea: a running timer is just the one entry whose end is null.
"Running" is a row state, not a process, and elapsed time is always derived
(now − start), never stored. That's why both surfaces can drive the live timer
without coordinating — they read and write the same row.
The repo is two halves, and the split is the whole point. The first is the specification — the product and process requirements, plus the acceptance criteria that say what must hold. The second is the rendering — the implementation and the generated evidence that those criteria do hold. The rendering is produced from the specification; the ghost-distribution goal below is exactly the claim that the second half can be regenerated from the first.
The specification — the source, the artifact worth keeping:
context/ The spec — concept, PRD, glossary, acceptance strategy, process, engineering.
features/ Gherkin acceptance criteria, run against BOTH surfaces (parity).
acceptance/
criteria/ What must hold — coverage matrix, schemas, JUDGE rubric, MANUAL runbook, parity matrix.
CLAUDE.md Repo guide and working instructions.
.claude/ The standing agent skills — requirements change, discovery audits, triage, backlog.
README.md This front door.
The rendering — generated from the specification above:
packages/
core/ @stint/core — schema, state transitions, invariants, reporting, rounding.
cli/ tt — the command line (commander), --json everywhere.
gui/ Electron tray app + window; renderer is an equal surface over IPC.
acceptance/
evidence/ Generated proof the criteria hold — CLI transcript, screenshots, recordings, judge report.
scripts/ Evidence generator and the no-network backstop.
The design lives in the styled HTML under context/. The
per-file inventory and read order live in CLAUDE.md's Files
table — one home, so it can't drift from this page.
npm run build
npm test # PROP · GOLD · BDD · integration · parity
npm run judge # GUI screenshots scored against the JUDGE rubric
npm run evidence # regenerates acceptance/evidence/cli-transcript.md
npm run tokens # regenerates the CSS token blocks from context/design.tokens.json
npm run icons # regenerates the app mark + tray glyphs from context/mark/ (needs Chromium)
npm run verify:no-network
npm run metrics # celebratory SLOC + documentation census (--out FILE to save)
npm run metrics:check # reconcile-only gate: fails if a file escapes categorizationnpm run tokens rewrites the CSS block between the STINT-TOKENS markers in
every mockup and in packages/gui/renderer/styles.css from
context/design.tokens.json; a guard test asserts the blocks match the tokens
file and that the contrast floors hold, so the block is never hand-edited.
npm run icons does the same job for the images the OS draws rather than the app:
it rasterizes the three SVG sources in context/mark/ into the app icon, the
running/idle tray glyph pair, and the Linux installer's icon ladder, injecting the
palette from the same token file. It is not on the build path — rasterizing needs
Chromium — so the PNGs are committed, and a guard test fails if a source ever renders
to nothing. The rules live in context/design.html §09.
npm run metrics walks every git-tracked file, buckets it
(implementation / tests / verification / requirements / design / AI context /
build / packaging / docs), and prints a reconciled report — code + comment + blank = total at every level, with no catch-all rule, so a new directory or
unforeseen file fails the count loudly until it is categorized. CI runs the
--check gate and uploads the rendered report as a codebase-metrics build
artifact, so the numbers can never go stale relative to the tree.
No single verification system or notation covers the whole PRD, so acceptance
uses five complementary methods (full map in
acceptance/criteria/COVERAGE.md):
| Method | Proves | Run |
|---|---|---|
| BDD (Gherkin) | User flows against both surfaces | npm run test:bdd |
| PROP (fast-check) | The money-affecting laws over many inputs | npm run test:prop |
| GOLD (snapshots + JSON-Schema) | The exact CLI/CSV/JSON contract | npm run test:gold |
| JUDGE (Playwright + rubric) | Subjective GUI qualities over real screenshots | npm run judge |
| MANUAL (runbook) | Sleep/wake, cadence, no-network, tray/hotkey | by hand |
The BDD suite runs each .feature against @stint/core and the built tt
binary, which proves full parity without a second copy of the spec.
Stint aims to be ghost-distributable — specified completely enough that the
software could be shipped as its requirements alone. The product and process
specs — context/, acceptance/criteria/,
CLAUDE.md, .claude/, and this README — are meant to
be complete enough that a capable agent harness and model could regenerate
extremely similar, functionally identical software from them alone. The code in
packages/ is one rendering of that specification; the specification is the
artifact worth keeping. Distributing only the specs, and letting an agent render
the software downstream, is "ghost distribution."
MIT — see LICENSE.