Skip to content

Releases: thousandflowers/canary

v1.3.1

Choose a tag to compare

@github-actions github-actions released this 07 Sep 14:12
9141990

The Claude Code bird ages only while you work, and the day turns at your midnight.

The minutes are the ones you worked

The README's first promise is that a gap longer than five minutes is a break
and does not count. It was true above a shell prompt and false in Claude Code,
where the session JSON offers only a wall clock: a window left open all
afternoon aged the bird as if you had typed all afternoon. The status line now
reads the transcript's own timestamps, turn by turn and tool call by tool call,
and keeps a small ledger per session (~/.canary/active) of the minutes that
were actually work. A repaint credits nothing. The first refresh after this
upgrade starts that count from what the transcript's tail shows, so a long
session may show fewer minutes than it did a moment before; they are the true
ones.

The day is yours, not Greenwich's

Daily peaks, the night streak and "the seventh session of a day" were keyed on
UTC days. In Rome the day turned at 02:00, so a session from 23:00 to 03:00
was two days, and its peak came back as debt onto itself after two. Days now
turn at local midnight.

A night past the limit means past it

The history counted a day that peaked at exactly 90 as a night past your
limit, while 90 scores as worn, the band that means stop, and dead starts at
91. A streak could begin on nights that were never dead. And tonight's count
extended the streak on the score after debt, while the file remembered the
raw peak: "✕ 3 nights" tonight became two by morning. Both count the same
thing now, the raw peak past 90.

The body clock credits the right hour

In Claude Code the awake-hours histogram was told the hour had turned by a
repaint, credited 14:00 with turns typed at 13:xx, and had nothing left for
the first real turn of the afternoon. It follows the turns now, and credits
the hour they happened in.

The installer signs off on what it did

A source build that failed still returned success and reported the status
line wired, with the old binary in place. It now says the build failed and
exits non-zero. Every path builds or downloads beside the destination and
renames into place, so a bird running mid-install gets the old one or the new
one, never half. And sh install.sh from inside the checkout builds from
source, as the README said; it used to download, because a bare script name
has no slash in it.

The plugin

Claude Code keeps its copy of the plugin without .git, so a build from it
said canary dev. The installer now takes the version from the plugin
manifest there, which release CI holds equal to the tag. The plugin's hook
keeps the binary at the installer's own path (~/.local/bin/canary) at the
plugin's version, so /plugin update moves the bird along; one from Homebrew
or go install lives elsewhere and is never touched. The hook fetches the
release binary rather than building one, because a go build inside Claude
Code's start-up behind a slow module proxy is a stall with no visible cause.
And it says so when it leaves a settings.json alone, instead of staying
quiet about a status line that never appeared.

v1.3.0

Choose a tag to compare

@github-actions github-actions released this 07 Sep 13:01
3effafb

The Claude Code bird, installed the way Claude Code installs things.

As a plugin

/plugin marketplace add thousandflowers/canary
/plugin install canary@canary

That is the whole install for anyone who only ever wanted the status line
bird. The next session start finds the binary, or builds it from source when
Go is around and fetches the release binary otherwise, and wires the status
line. The shell rc is never touched.

A plugin cannot declare a status line: Claude Code's plugin manifest has no
field for one. So the plugin is one SessionStart hook that runs canary settings install, which refuses to wire itself twice, and every session after
the first is a no-op that says nothing. The hook adds nothing to the model's
context. canary is the marketplace and canary is the plugin, hence
canary@canary.

/plugin uninstall canary@canary and canary settings remove take it back out.

Why this exists

Run the one-liner a second time without CANARY_CLAUDE_ONLY=1 and the shell
bird comes back, because the installer does what it is told. The plugin is the
install that cannot make that mistake: there is no shell half to wire.

Also

Release CI now refuses a tag whose version the plugin manifest does not agree
with. The plugin installs straight from the repository, so a manifest left
behind is what its users would see.

v1.2.3

Choose a tag to compare

@github-actions github-actions released this 06 Sep 21:29
5cc2136

An installer that no longer signs off on work it did not do.

The rc it could not write

A read-only .bashrc produced this:

canary: added the PATH line to ~/.bashrc
canary: added the hook to ~/.bashrc

open a new shell (or: . ~/.bashrc) to meet it.

Nothing had been written. The bird never appeared, the install said it had
worked, and there was nothing to go on.

The installer now checks the append, names the file it could not write, prints
the line to add by hand, and exits non-zero. The binary still installs, because
downloading it did work. There is a test for it, and it compares the rc byte
for byte afterwards.

Error messages that name the real cause

Three different broken settings.json reported the same sentence, about
comments: a directory in place of the file, a file with no read permission, and
valid JSON that is not an object. Somebody with a permission bit set wrong went
looking for a comment that was never there.

canary: ~/.claude/settings.json cannot be read (is a directory) — left untouched.
canary: ~/.claude/settings.json is JSON, but not an object — left untouched.
canary: ~/.claude/settings.json is not plain JSON (comments?) — left untouched.

What was already fine

The same pass threw hostile input at the rest of it: malformed and 2 MB JSON
payloads, a binary transcript, an unwritable ~/.canary, twenty concurrent
writers, COLUMNS of 0 and 1, a second uninstall, fish. All of it held.

v1.2.2

Choose a tag to compare

@github-actions github-actions released this 05 Sep 20:34
b865160

Two promises the pre-launch checks caught canary breaking.

The uninstall now leaves nothing

install.sh writes each block as a blank line, a marker comment, and the line
itself. uninstall.sh took the two lines back and left the blank. Install and
uninstall four times and the rc grew four blank lines: an uninstall that claims
to take back what it added, leaving a fingerprint anyway.

The blank that belongs to a marker now goes with it, and only that one. A run
of blanks before a marker keeps all but the last, because the others were your
own spacing. The installer test suite now compares the rc byte for byte across
three full rounds.

canary demo pipes cleanly

canary demo | tee log wrote the screen-clears and cursor-ups into the file.
With no terminal on the other end the demo now prints one block per thing that
actually changed: no escape codes, no sleeping, the same words.

The stat row is left out of that comparison, so the bird is not reprinted
seventy times to show a minute counter moving.

Nothing else changed. Same bird, same numbers, same corpus.

v1.2.1

Choose a tag to compare

@github-actions github-actions released this 05 Sep 20:00
10675c2

The three install paths now agree on what version they installed.

go install github.com/thousandflowers/canary/cmd/canary@latest printed
canary dev while Homebrew and the one-liner printed the release number. The
version is stamped through ldflags at release time, and a plain go install
has no flags of ours to stamp with.

The module version was inside the binary the whole time. canary version now
falls back to debug.ReadBuildInfo().Main.Version when nothing was stamped, so
a go-installed binary reports the release it was built from. A local go build
still says dev, because its build info says (devel) and that is true.

Nothing else changed: no new behaviour, no new flags, same bird.

v1.2.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 19:16
fe5f442

One bird, and a status line that survives the next upgrade.

What you get

The status line keeps working after brew upgrade. canary settings install wrote the path Homebrew resolves to, which points inside
Cellar/<version>/. The next brew cleanup deletes that directory and the
bird vanishes from Claude Code with no error anyone sees. It now writes the
symlink, which is the half that survives. The shell hook was never affected:
canary init runs again on every shell start and healed itself.

If you installed with Homebrew before this release, run canary settings install once more, or check the path in your settings.json.

One bird per install, not two. The bird that sang through the install
stayed on screen while a second one was drawn underneath it to sign off. Two
birds read as two installs. The singing bird's rows are now wiped when the last
step lands, the step log takes that space, and the bird that signs off is the
same bird, done singing.

--claude-only. canary is two birds from one binary, and the installer
wired both. Anyone who wanted only the Claude Code status line had to edit
their rc back by hand afterwards.

sh install.sh --claude-only
curl -fsSL .../install.sh | CANARY_CLAUDE_ONLY=1 sh

Homebrew never had this problem: it installs the binary and stops. canary settings install on its own is still the shortest path to the Claude Code half.

The installer stopped saying things that were not true. It promised a
backup of settings.json even when there was no file to back up, and it
reported "added the hook" twice because the PATH line and the hook share one
function. It now names each of them, and only names a backup that exists.

Also

The README opens with the gif, all three install paths are in the first screen
instead of four hundred lines down, and there is a second gif of the installer
itself. Both are recorded from tapes in the repo, so they can be re-rendered
rather than re-staged.

Upgrading

brew upgrade canary
canary settings install   # rewrites the status line command, once

v1.1.0

Choose a tag to compare

@github-actions github-actions released this 04 Sep 19:14
7961997

The installer sings.

curl | sh used to install canary in silence and sign off with a bird drawn by
printf. canary is a pixel-art fatigue bird, and its own install was the one
place it did not appear.

What you get

A bird that sings while it installs. In a terminal the installer cycles
render.Frames(Fresh) — rises, hesitates, falls — in the same slot the prompt
bird speaks from, redrawn in place rather than scrolled.

 ▗███▖
▐ O ▌>  ♪·· fetching the binary

In the status row those notes move only during a real break, so that nothing
pretty there can be earned by grinding. An install is not the status row: it
happens once, it is already an interruption, and the bird has something to be
pleased about.

It says what it found before it touches anything. Platform, shell, the rc
file it is about to edit, where the binary is going. curl | sh runs code
sight-unseen; naming the machine it landed on is the least it can do.

The last word belongs to the binary, not the installer. It runs what it just
installed and lets it draw itself with a line from the corpus compiled into it —
which also means a binary that cannot run cannot sign off.

Into a pipe it is words, not cursor movement. A CI log, a redirect, or
NO_COLOR / CANARY_NO_ANIM set: the same lines with no escape codes. Build
output is collected and printed under the bird once it has settled, so a go build never scrolls through the animation.

Also

brew install thousandflowers/tap/canary is the one tap now. The old
thousandflowers/homebrew-canary carries a tap_migrations.json and moves
anyone who installed from it across on their next brew update — no reinstall,
and the old install command still resolves. It had quietly served the 0.7.0
shell scripts for months after canary became a binary, which is the argument
against a second tap in one line.

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 31 Aug 13:07

canary is one binary now.

Until 0.7 it was three shell scripts — canary.sh, canary.fish and
canary-statusline.sh — three implementations of one formula, kept honest by a
test suite whose whole job was checking they still agreed with each other. One
implementation cannot disagree with itself. That is most of what 1.0 is.

What you get

The bird belongs to you, not to a terminal. Three windows open used to mean
three independent birds, each ignorant of the others, while the file the Claude
Code status row read was overwritten by whichever of them ran a command last —
which is why the numbers on the status row jumped around. One state file now,
shared, with an idle rule so a window you are not typing in accrues nothing.

It talks. The phrase corpus that the tiers, the shuffle and the state
triggers were all waiting for is here, and it ships inside the binary rather
than being copied into ~/.canary by the packaging. Lines are drawn on a state
transition, never on the clock: the bird says something when it changes, holds
it long enough to be read, then goes quiet instead of finding something new to
say. canary lint checks a corpus against the rules in VOICE.md.

canary demo wilts the bird through all five states in about twenty seconds,
so you can see what you are installing without having to earn it.

jq is no longer required to wire the status line. canary settings install
does it.

New: the circadian curve now points at your body clock

The shape of the time-of-day curve is population-wide and well evidenced — the
nadir at 02:00–06:00, the genuine post-lunch dip, the evening wake-maintenance
zone where you are sharper than you think. Its phase is not. Chronotype shifts
by hours between people, and for one person it shifts with the season and with
whatever they are in the middle of. A curve nailed to the wall clock punishes a
04:00 sleeper exactly when they are at their best and forgives them exactly when
they are falling over.

So canary measures the phase instead of assuming it. It keeps 24 counters, one
per hour, noting that you were awake in that hour, and halves all of them daily
— a half-life near eleven days, fast enough to follow a schedule that moves,
slow enough that one late night does not repaint it.

$ canary chrono
  ▇█▇▄▃▂    ▃▂▄▅▇██▄▃▃▃▅▅▆
  0     6     12    18
  hours you are awake in, decayed daily

Your day centres on 19:46 (R=0.26).
Offset +5h.
The deepest trough sits at 07:00-09:00 for you, not 02:00-04:00.

For that night owl, ten of the twenty-four hours change band: 02:00 stops being
scored as the bottom of the trough, and 08:00–11:00 starts being.

Three things worth knowing about it:

  • It is a circular mean, not the longest quiet stretch. Hunting for a run of
    idle hours is the obvious approach and it fails on real people — measured
    against a month of one night owl's actual activity the longest clean gap was
    four hours, because an irregular sleeper has no hour they are reliably asleep
    in. The mean uses all twenty-four at once, so a schedule that wobbles by three
    hours a night still has a stable centre.
  • It refuses rather than guesses. R is how concentrated your histogram is —
    1.0 is one hour, 0 is no schedule at all. Below 0.15, or before about forty
    recorded hours, canary keeps the textbook curve and says so.
  • canary chrono --bootstrap seeds the histogram from macOS's own
    screen-time history, so it works this week instead of next month. Nothing
    leaves your machine.

CANARY_CHRONO_OFFSET pins the rotation by hand if you would rather; 0 pins
the textbook curve, which turns the learning off.

There is deliberately no temperature sensor. powermetrics refuses SMC readings
to a non-root user on Apple Silicon, the keys are in neither ioreg nor
sysctl, and a status row that asks for sudo on every refresh is not a status
row. It would also be measuring the wrong thing: a hot laptop at 09:00 is a
render, not a tired person.

Upgrading from 0.x

Run install.sh again, or sh uninstall.sh first if you want to be thorough —
it recognises the old source line and takes it back.

Two things changed on purpose:

  • canary on / canary off are gone. A binary cannot change the
    environment of the shell that ran it. CANARY_DISABLED=1 still works.
  • The bird is per-person, not per-terminal — see above.
brew install thousandflowers/tap/canary

v0.7.0 — the bird speaks

Choose a tag to compare

@thousandflowers thousandflowers released this 27 Aug 21:39

The canary now says things. One line, on a state transition, and silence
otherwise — roughly two thirds of eligible transitions produce nothing,
which is the point.

▐ O ▌>  ♪                              silent, the default
▐ - ▌>  ⌐ sliding. slowly. audibly.    speaking
▐ x ▌v                                 dead, and done talking

What's in it

  • phrases/en/** — 22 plain .txt files, one phrase per line. Add a line from the GitHub web editor; there is no syntax to learn.
  • Rarity is gated on recovering — after a real break, on a rising note, while still fresh — and never on hours logged. A tool about fatigue must not pay you to keep working.
  • Category eligibility shrinks as the state worsens: tired drops worldly/, worn drops lore/ too, dead says its one line and goes quiet.
  • canary preview --state worn --note falling draws a candidate line in the bird, so a phrase PR can be judged without cloning.
  • VOICE.md — the voice rules and the whole corpus. CONTRIBUTING.md — how to add a line. A roadmap through v1.0 is in the README.

Knobs

CANARY_QUIET=1 no phrases · CANARY_ASCII=1 ASCII glyphs · CANARY_RESERVE_COLS space for whatever shares the row

No new dependency. Still pure shell, no network, no API calls.

brew install thousandflowers/tap/canary

v0.6.0 — evidence-based fatigue model, CI, four real bug fixes

Choose a tag to compare

@thousandflowers thousandflowers released this 27 Aug 02:02

Bands now map to the Karolinska Sleepiness Scale, time-on-task is a concave curve instead of a straight line, and the circadian penalty follows the real trough rather than a flat 22:00–07:00 window. The README gained a "why these numbers" section with sources.

Fixed

  • uninstall.sh destroyed symlinked rc files. If ~/.zshrc was a symlink into a dotfiles repo, mv replaced the link with a regular file — the dotfiles copy kept sourcing canary (a silent no-op uninstall) and the rc was detached from the repo for good. It also reset the mode 644→600.
  • The prompt bird leaked variables into your interactive shell — cmd, len, score, name, now, gap and more, on every prompt. Under bash the DEBUG trap parked the full text of your last command in $cmd.
  • The statusline exited 1 on every ordinary render, which leaked into the whole status line chain and would fail brew test.
  • canary never loaded in four of eight bash setups. install.sh wired .bash_profile when .bashrc was missing; which file bash reads depends on login vs non-login, and the old rule got it wrong half the time.

Model

bird KSS
fresh 1 extremely alert
chirpy 3 alert
tired 5 neither alert nor sleepy — not impaired
worn 7 fatigue-risk protocols call for a break here
dead 9 fighting sleep

Time: 15m→7 · 1h→26 · 3h→55 · 5h→72 · 8h→86. Five hours of active work lands on worn. The old linear minutes/3 pinned everything past 5h at 100, which made the dead bird wallpaper.

Circadian: nadir 02:00–06:00, post-lunch dip 13:00–16:00, nothing added 17:00–21:00 (the evening wake-maintenance zone). CANARY_NIGHT_START/CANARY_NIGHT_END are gone; CANARY_NIGHT_MULT is now the multiplier at the bottom of the trough (default 150), and 100 still disables it.

CANARY_MIN_SCORE=71 is the recommended quiet threshold now, not 46 — worn is the band worth acting on.

Packaging

The formula now ships canary.sh and canary.fish too. Without them install.sh fell back to curling them from GitHub, so a brew install left an installer that needed the network to finish and failed outright offline.

First release with CI

shellcheck (pinned) plus four test suites on Ubuntu and macOS, and a fish job.