Wits tracks what a cannabis patient was dispensed, what they have used, and what is left.
It is a ledger. Everything it shows is derived by replaying an append-only
log, so nothing is edited in place: a mistake is corrected by recording a
correction, the way git revert adds a commit rather than rewriting one. The
data is a multi-year medical record, and that is how a medical record should
behave.
Grams move through four accounts. Every entry is a transfer between two of them, so nothing is lost between the pharmacy and the ashes.
pharmacy / prescription
│ buy
▼
┌───────────┐ grind ┌───────────┐
│ STORAGE │ ────────────────────▶ │ STASH │
│ sealed, │ │ ground, │
│ per product │ one per product
└───────────┘ └─────┬─────┘
│ sesh · device + temperature
▼
┌───────────┐
│ CONSUMED │
└─────┬─────┘
│ weighed after collection
▼
┌───────────┐
│ AVB │ ──────▶ edibles / tincture
└───────────┘
A cycle is one prescription fill: it opens with the purchase and closes when its own jars are empty — not a calendar month, and not the arrival of the next fill. A fill lasting six weeks, a month with two fills, a month with none, and a remainder outliving its successors all work without special cases; cycles simply overlap, each keeping account of what it dispensed until the last of it is ground.
go install github.com/TheDonDope/wits/cmd/wits@latestFrom a checkout, make install-wits installs the working tree to the same
place instead — see Development.
A repository is created the way a git one is, and every command finds it by walking up from the working directory.
mkdir -p ~/wits && cd ~/wits
wits init .Record a fill, then use it:
$ wits buy "Enua 22/1 Wedding Cake" 20g
New product Enua 22/1 Wedding Cake — refer to it as wcake-221
[029efe8] purchase 20.00g wcake-221 into storageEvery product gets a short handle: three to five characters from the cultivar,
then the THC/CBD ratio. The ratio is there because the same cultivar from the
same maker at two strengths is two prescriptions — wcake-221 and wcake-251
are legible as a pair, and adding their grams together would be wrong. Pass
--slug lemon to choose your own, which is taken as written. A handle is
settled when the product first appears and never changes, because it is the
name every later entry refers to.
wits grind wcake 0.75
wits sesh wcake-221 0.3 --device volcano --temp 185
wits statusReferences resolve by prefix, so wcake is enough while it is unambiguous.
Tab completion offers the handles, with the full name and how much is left
beside each — and only the ones the command can act on, so sesh offers what is
in a stash rather than everything ever dispensed:
$ wits grind <TAB>
lcook-281 10.00 g · Cannamedical 28/1 Lemon Cookie
mac1-251 15.00 g · Cantourage 25/1 MAC1+
wcake-221 18.00 g · Enua 22/1 Wedding CakeCompletion is a script the shell sources, written by wits completion bash
(or zsh, fish). Bash only runs it through the bash-completion package,
which is not always preinstalled — without it the script loads silently and
does nothing. Install that first, give it the directory it reads user
completions from, and write the script there:
sudo pacman -S bash-completion # or apt/dnf install bash-completion,
# brew install bash-completion@2
mkdir -p ~/.local/share/bash-completion/completions
wits completion bash > ~/.local/share/bash-completion/completions/witsA new shell picks it up from there; nothing needs sourcing in .bashrc, and
no sudo was involved. To try it in the current shell without writing
anything: source <(wits completion bash). Zsh wants the script on its
fpath as _wits, fish in its own completions directory:
wits completion zsh > "${fpath[1]}/_wits" # zsh
wits completion fish > ~/.config/fish/completions/wits.fish # fishAnything can be backdated with --date 2026-07-29, which is what makes a
forgotten evening loggable the next morning without pretending it was entered
then.
$ wits status
On cycle 29, opened 2026-07-09 (day 26)
PRODUCT STORAGE STASH AVB LEFT
420-evolution-ice-cream-cake-271 16.90g 3.10g 0.00g 84%
enua-citrus-slap-361 17.09g 2.91g 0.00g 85%
cantourage-mac1-251 17.23g 47.77g 0.00g 86%
total 51.22g 85%
51.22g of 60.00g left over 23 days, 11 of them with an entry
15.82g more in 7 older jars, 6 earlier cycles still open
1.73g per active day, 1.37g median, 0.83g per elapsed day
About 30 days left at that rateEvery gram in storage stands on the account of the cycle that dispensed it, so the total is the fill's own: 51.22 g of the 60 g this prescription brought. What older cycles still hold gets its own line — those cycles are simply still open. A jar refilled before it was empty holds two cycles' grams, and grinding draws the oldest first, the way inventory leaves a shelf.
wits on its own opens the interface: a dashboard of cards — storage, stash,
sessions, devices, two rhythm calendars and a projection of the supply's
decline to its empty day — then the journal, an analysis view scoping from the
current cycle out to the whole history, the storage, the stash, the sessions,
the devices — and the Séance. Entries can be recorded there too — b for a
fill, g to grind, s for a session, r to weigh.
The analysis view draws the daily amounts as a braille area chart with a seven-day average riding over it, and the longer scopes as a calendar heatmap — one cell per day, colour carrying the amount — so a year of habit reads the way a contribution graph does: the heavy weeks, the pauses, whether weekends differ.
The Séance is where the ledger is summoned back. It replays any stretch of the
record — the whole ledger, one prescription cycle, or a date window picked by
hand: f turns the frame, d picks the dates, and the transport is the same
one every replay uses — p plays, ←/→ steps, +/- retunes. Each event
takes the table as a playing card wearing a stylized figurine for its action;
x flips the card to the record on its back — both timestamps, the accounts
the grams moved between, the hash chaining it to the entry before. Beneath the
table the storage jars and stash tins fill and drain as the replay walks the
history.
wits init [dir] |
Create a repository |
wits buy <product> <amount> |
Record a prescription fill, --slug to name it |
wits grind <product> <amount> |
Move product from storage into its stash |
wits sesh <product> <amount> |
Record a session, drawing on the stash |
wits status |
What is left, and how long it will last |
wits log |
The journal, newest first |
wits revert <entry> |
Undo an entry by recording a correction |
wits reconcile [account] [product] [weight] |
Make an account agree with the scale; interactive with no arguments |
wits device add <name> |
Register a vaporizer |
wits temps <celsius> |
What a temperature is hot enough to release |
wits import <file.xlsx> |
Import a tracking spreadsheet |
wits export |
Markdown, for reading or publishing |
wits bundle |
The whole repository as one compact file |
wits restore <file> |
Rebuild a repository from a bundle |
Every command takes --help. import writes nothing unless given --commit.
wits import reads a tracking workbook and turns each worksheet into a
prescription fill and the grinds that followed it. The default is a dry run: it
reports what it would record, and anything about the spreadsheet that does not
add up, so years of history can be checked before any of it is written.
Products are resolved by position, through the bindings in the running balance formulas, rather than by the label in the strain column — those labels are dropdown values that were not always renamed as products changed. On the records this was written for, trusting the labels misplaces 1116.97 g.
Nothing is edited in place. An entry is undone by recording a correction that moves the same grams back the way they came, so both stay in the log:
wits log --oneline -n 1
wits revert 8297238 --reason "misread the scale"The storage screen is two tables: what still holds something, and the history
of every jar weighed down to zero, newest first. The stash screen drills into
the ground product the same way — the stashes holding something above, and
under them every stash worked down to nothing, grouped under the day it was
consumed. The sessions screen tells the other half of the story: how much came
out of the stash, when, through which device and how hot, drawn with the same
charts the analysis view uses. Space ticks jars in either
table and r weighs the ticked ones together; e corrects a name; c records
stale stash remainders from earlier cycles as consumed, which is what four
imported years of grind-only records leave behind. Product names are never
abbreviated — the name column takes what the longest name needs. In the
journal, e amends an amount and d undoes an entry. The log shows what
currently stands; v reveals the corrections behind it.
Ledgers drift. A little is spilled, a session goes unlogged, a scale is read wrong. The past is not edited to hide it, because nobody knows which entry was wrong — instead the difference is recorded, and the account agrees with the jar again:
$ wits reconcile storage wcake-221 17.6 --dry-run
storage holds 18.00g by the ledger and 17.60g on the scale: -0.40g
$ wits reconcile storage wcake-221 17.6 --reason "spilled on the desk"
[21ee6ae] adjusted 0.40g out of storage of wcake-221, now 17.60gFor weighing day, wits reconcile on its own is interactive: pick storage or
the stash, tick the jars to weigh — all of them by default — and each is asked
for in turn, with the ledger's figure beside the prompt. A blank reading skips
a jar; wits reconcile stash skips the first question. In the interface this
is r, on any screen — it weighs the jars ticked on the storage screen, the
one under the cursor, or otherwise the fullest one, since that is the one
worth checking.
.wits/
config.yml # settings
products.yml # the catalog
devices.yml # vaporizers and their temperature ranges
journal.ndjson # append-only, one entry per line, never rewritten
index/ # reserved for a cached fold; nothing writes it yet
The journal is only ever appended to, and each entry is chained to the one before it with a SHA-256 hash, so an edit made outside Wits is detectable. Appending takes an advisory file lock as well as a mutex, because it is a read-then-write and two processes reading the same tip would fork the chain.
The directory is created 0700 and its files 0600. Nothing is transmitted
anywhere; the application makes no network calls at all.
wits bundle writes the catalogs and every entry to a single file that
wits restore reads back, reproducing the journal exactly, hash chain
included. That is what makes it worth trusting as a backup.
It is plain text, so the record stays legible with nothing but a text editor and diffs cleanly in git. Small, too, because most of what the journal stores is derivable and is left out: sequence numbers, account pairs and the whole hash chain are recomputed on restore.
Nearly three years of real history, 1369 entries across 50 products:
| bytes | ||
|---|---|---|
| journal | 506,205 | |
| bundle | 27,968 | 18× |
| bundle, gzipped | 6,399 | 79× |
Wits knows the boiling point of every cannabinoid and terpene, so a number on a dial reads as what it actually does — including the point at which it starts producing benzene.
$ wits temps 210
COMPOUND BOILS AT EFFECTS
THCA 120°C anti-inflammatory, anti-epileptic, anti-proliferic
CBDA 130°C anti-inflammatory, anti-proliferic
β-Caryophyllene 130°C anti-malarial, cytoprotective, anti-inflammatory
…
⚠️ 210°C is at or above the 205°C boiling point of Benzene.The devices screen shows the same for whichever device is selected, at its default setting.
One Go module, several commands, and a web interface beside them. Not a module per component: the domain is shared, and a boundary between a server and the ledger it serves would mean versioning the ledger against itself.
wits/ module github.com/TheDonDope/wits
cmd/
wits/ the terminal interface and the commands
witsnap/ the camera and the tap: screens as text, the fold as JSON
wits-server/ the REST API (planned)
pkg/
journal/ the append-only log
ledger/ the fold: balances, cycles, statistics
repo/ finding and creating a .wits directory
workspace/ opening a repository and holding its state
catalog/ products and devices
record/ applying entries, with the checks that guard them
bundle/ the portable archive format
importer/ reading the tracking spreadsheet
cannabis/ cannabinoids, terpenes and their boiling points
tui/ the screens
version/ the build stamp every binary reports from
wits-ui/ the web interface, in Angular (planned)
pkg/journal depends on nothing else here and everything above depends downwards
only, so a server is another caller of pkg/workspace, not a new layer.
- bubbletea — the TUI framework 🏗
- bubbles — components 🫧
- huh — forms and prompts 🤷
- lipgloss — layout and style 👄
- vhs — the recording above 📼
- cobra — the command line
All on the v2 line, which lives under charm.land/… rather than
github.com/charmbracelet/….
The charts are drawn in-tree. The terminal charting libraries still target Bubble Tea v1, and mixing the majors puts two renderers and two colour-profile detectors in one binary, which shows up on screen as inconsistent colour.
make build # build ./bin/wits
make run # build it and run it
make install-wits # install the working tree as `wits`, replacing a release
make test # test with coverage
make cover # coverage as HTML
make vet
make preflight # everything CI and the Codacy gate will say, said here firstmake build writes to ./bin/wits, which is fine for a session in the
checkout and wrong for daily use — the shell keeps finding whatever wits is
on the PATH. make install-wits installs the working tree where
go install …@latest puts a release: $GOBIN, or $GOPATH/bin when GOBIN is
unset (go env GOPATH says which). It goes through the same ldflags as
make build, so wits --version names the tag and commit it was built from;
a bare go install ./cmd/wits skips the stamp and reports
unknown (built from source).
./preflight.sh watch <pr> polls a pull request's checks and reads the Codacy
delta the way the gate does, so a red X never comes as a surprise.
make snap builds witsnap, the camera and the tap: witsnap screens renders
any screen as plain text — --press p,tick,tick photographs a replay mid-run —
and witsnap json writes the whole derived state as JSON, which is the seam a
new client or a new visualisation starts from.
make build-windows cross-compiles bin/wits.exe. make render-tapes re-records
every GIF in assets/ from the *.tape files; it needs vhs, gum and ttyd
(make install covers the first two). The tapes seed a throwaway repository
with demo-seed.sh first, because wits reads a .wits directory and a source
checkout has none.
coverage.out and coverage.html are ignored from source control.
Features, bugs and refactorings are tracked in ROADMAP.md rather than GitHub Issues, so the plan and the code stay in one place. A detailed changelog is in CHANGELOG.md.




