Releases: TGPSKI/catnip
Release list
catnip 0.4.0 — windows end on a day GitHub has finished counting
GitHub keeps adding counts to a day for well over a day after it closes, and
documents none of it. Every window catnip drew ended on days that were still
being counted, and the tannery agent that was supposed to notice reported
success without running anything.
Added
CATNIP_SETTLE_HOURS— how long after a day closes GitHub is still
adding to it. 36 is a measured floor: the variable raises the wait for an
account that settles slower and cannot lower it, because a short wait
costs nothing visible and publishes part of a day as all of it.catnip.runfiles— one reader for a run directory's raw payloads,
shared by the four traffic analyses. Each had its own copy of the
raw/repo_<slug>_<endpoint>.jsonconvention, and each copy inlined
config.slug_forwithout its sanitizing step: a repo whose name carries
any character outside[A-Za-z0-9_.-]resolved to a filename the fetcher
never wrote,is_file()returned False, and the repo left anomaly,
profile, funnel and correlation output with no error and no row. No repo
on this account triggers it today.meta.jsonrecordswindow_digest,settle_hoursand
store_latest_day.
Changed
- Windows move back. No window ends inside the settling wait, so at a
midday cron1dcovers the day before yesterday. The TUI header names the
day it is showing instead of calling it "last day", and the report's
provenance table carries asettled throughrow. - Correlation is more conservative. A repo whose third non-zero day was
an unsettled one no longer qualifies for the pair scan. - Upgrading is handled, not waited out.
meta.jsonchanged what
latest_daymeans: reports written before this recorded the store's
newest day, which is a fetch day and ahead of what those reports covered.
Comparing a settled day against it suppresses reports for exactly the
cycles the store is gaining settled days, so ameta.jsonwithout
settle_hoursis recognised as pre-settling and does not gate the next
report. - The tannery needs leather v0.5.3 or newer. Older builds parse
require_tool:as prompt text instead of enforcing it.
Fixed
- No window ends on a day GitHub has not finished counting. The last-day
view printed a column of zeros ranked alphabetically, because the newest
day in the store is the day the fetch ran and GitHub returns that day as a
flat zero. Re-reading one day out of four run directories, over the same
35 repos every time, it carried 33% of its final views and 47% of its
final clones at 12h after closing and was complete at 30h; days older than
that were identical in every read.derive.settled_dayends every window
settle_hours()behind the newest fetch — hours, not a day count, because
the same "two days back" means different amounts of settling depending on
what hour the timer fires.docs/metrics.mdcarries the measurement and
#5 tracks making it
self-checking. - The edge is never the last day carrying traffic. Trimming trailing
zeros was the tempting fix and is wrong: this account recorded ten
genuinely empty days in June, and reading a quiet day as an unfinished one
slides every window a day left without saying so. The previous code did
exactly that on the CSV path, where it had been dead for as long as a
store existed. - Trend arrows were biased downward.
totals.pysummedclones_7dover
a window ending on the fetch day whileclones_prior_7dwas complete, so
the comparison measured the shape of the collection. - The per-run analyses no longer score days GitHub is still counting. A
run filed aminor dipagainst a day eighteen hours old. The anomaly
detector, clone profiler and correlation analysis read settled days now,
which also lifts their baselines onto complete data. catnip reportno longer treats a fetch as new data, and no longer
freezes a day it got wrong. The interval guard compared the store's
newest day, which advances on every run whether or not GitHub counted
anything. It compares settled days now, and records awindow_digestof
the numbers each report stated so a day revised after publication reopens
the report. Without it, a report written before a day settled asserted a
third of that day's traffic forever, because the only question asked was
whether a new day had appeared.catnip prunekeeps runs whose days may still be revised. Ingested
means a run's numbers are in the store, not that they were the final
numbers. The run directory is the only record of what one fetch saw, and
history --rebuildreconstructs from surviving runs alone.- A tannery turn that must act can no longer end on prose. The collect
agent answered "catnip-run call succeeded" without calling it on
2026-08-08 and 2026-08-09, finished in 1.4s instead of 41 minutes, and
recorded an inventedlatest_day: 2024-01-15over an untouched store, as
success. Replayed against the same served model the tool was in scope
every time, and the model answered in prose in 4 of 16 samples. Every
acting turn declaresrequire_tool:, and record turns writeunmeasured
withaction: failedwhen a{{value}}arrives unsubstituted. - The tannery no longer does date arithmetic in the model.
catnip-store-statusemitssettled_day,settled_expectedand a
stale: yes|noverdict, and the collect agent's rule is "failed if stale
is yes". A model comparing dates to "yesterday" is arithmetic nobody
checks, and it returned a passing verdict on both failed nights.
catnip 0.3.0 — the tannery sizes its own hunt
Added
- An editor stands between assembly and the published prowl.md. Analyst
packages accumulate into a staged assembly; each recorded package hands
the assembly to an editor that merges same-phenomenon blocks, orders the
document to argue account-wide first, and cuts restatements. Its publish
is the only writer ofprowl.mdand is guarded twice: a document whose
tier counts differ from its own stamped counts is refused (the editor
arranges, it does not drop or invent findings), and one whose stamp
trails the cycle's record is skipped as superseded — a fresher edit is
already queued behind it. - Self-graded analysis depth. A meta-analyst reads the deterministic
report every third day, judges how much investigation the cycle deserves,
and seeds N analyst briefs — one per distinct phenomenon, N being its
call. Each brief runs as its own analyst; packages fan back in through the
writer, whose per-cycle files accumulate and whose publish dedupes, so
collation needs no join queue and a failed angle costs only itself. The
dispatch and recording ends are both single deterministic calls that parse
blocks themselves — no model ever counts its own fan-out.
Changed
-
The served chain is event-driven; only the fetch and the analysis are on
cron. 0.2.1 scheduled the report at 06:52 and sized the gap so a
41-minute collect would probably have finished — clock arithmetic standing
in for a dependency. The report now runs from a curing fed by collect's
output, and the prowl writer from a curing fed by the analysis, so a slow
fetch delays the report instead of losing it. Routing is one fact per
stage: the producer's intake URL and the consuming curing name the same
queue. Theleather ingesttargets remain for one-shot testing and agent
validation; the served chain never uses them. -
The prowl analysis is recorded by one deterministic call. The 0.2.1
writer made one tool call per FINDING/REFUTED block, which put a
data-dependent number of calls on the model: three calls against four
findings drops one silently — no error, just a shorter report.
catnip-prowl-recordreplacescatnip_prowl_finding,catnip_prowl_refuted
andcatnip_prowl_publish: it takes the whole analysis, parses the blocks
itself, files them all or fails naming the block it refused, and publishes.
Validation is unchanged — tier enum, evidence opening with the catnip tool
the claim rests on, publish refusing an empty cycle. -
Every measured value in a state file is now carried by an extract rule.
Coverage, repo count, schema version, the report's meta fields and the
recorder's counts reach their recording turns as{{...}}verbatim instead
of as a model's transcription — 0.2.1 wrote a Python repr of the coverage
ranges where the store's own JSON belonged. The model writes only judgment:
action,reason, a finding's prose. -
catnip-report's verdict rules moved into the turn that owns the
writer. The agent frontmatter is the system prompt and is resent every
turn, so rules aboutcatnip-report-writefired on turns that could not
reach the tool. Output rules now live with the tools that produce them. -
tannery/README.mdrewritten for someone deciding whether to run this,
in the shape of leather's example READMEs.
Fixed
- The prowl writer dead-lettered every scheduled cycle. The 0.2.1
lifecycle routed the analysis withoutput: type: queue, which builds a
queue item with no hide behind it; the curing loads the item's hide
unconditionally and failed each attempt. The route is nowtype: httpto
leather's/intake, which stores the hide first and enqueues against it.
This tannery had never successfully recorded a prowl analysis until now.
catnip 0.2.1 — a leather tannery, and a fix it surfaced
Added
-
tannery/— a leather tannery.catnip-collectruns the fetch daily
at 05:07,catnip-reportwrites the deterministic report an hour and
forty-five minutes behind it, andcatnip-prowlhunts every third day.
The gaps are headroom, not estimates: the fetch is serial and paginated so
its duration tracks repo count. Overrunning one is safe by construction —
the store has not advanced, the interval guard refuses, and the report
recordsskippedrather than describing yesterday as today. -
Per-turn tool scoping, which is the reason to use leather here at all.
Each agent is multi-turn and a turn replaces its tool scope rather than
extending it, so an agent reaches only what its current turn declares:catnip-collect catnip-pipeline -> catnip-inspect -> catnip-record catnip-report catnip-inspect -> catnip-report-write -> catnip-record catnip-prowl catnip-evidence -> catnip-file -> catnip-publishcatnip-collectcannot write state while the pipeline turn runs.
catnip-reportcannot reach the writer before reading the store's newest
day.catnip-prowlcannot file during the turn it gathers evidence and
cannot gather more once filing starts, so everything it files came from
evidence already in context. That replaced a paragraph asking the agent to
test before filing: there is now no turn in which it can do otherwise. -
Provenance as argument validation.
catnip_prowl_findingrejects any
tieroutsidemeasured|inferred|speculativeand anyevidencethat does
not name the catnip tool the claim rests on. No path through the toolset
records an untagged or unsourced claim. Refutations have their own tool,
because a cycle with none tested nothing, and publishing fails on an empty
cycle rather than writing an empty file. -
make smoke-toolsexecs every read-only tool's real argv straight from
shell-tools.json, so an argv or quoting regression surfaces before the
scheduler hits it at 05:07 with nobody watching. It is what caught
catnip view why. -
--why-view, andcatnip view why <view>in the dispatcher.
Changed
- Funnel
views/uniqrenamed topgviews/pguniq. They sum GitHub's
top ten paths and are not repo traffic, but wore names every other view
uses for something else. One repo read 10/10 where its real figures were 19
views from 1 unique visitor:pguniqcounted one person once per page, and
pgviewscaught ten of nineteen because ten paths is all the API returns.
Re-sourcing them from the store would have been worse — the store's window
and the paths snapshot genuinely disagree, so real windowed traffic beside
path-derived depth trades a naming problem for an arithmetic one. bin/catnipruns insidemain(). bash reads a script incrementally,
andcatnip runis a 41-minute job; rewriting the file in place mid-run
moves the bytes under bash's stored offset. Measured: an unwrapped script
loses the rest of its body, a wrapped one always completes. Not a total
fix — bash may still read garbage aftermainreturns and exit non-zero
having done all the work. Treat a trailing syntax error from a run that
otherwise reported success as this, and check the store, not the exit code.
Fixed
- Tannery timeouts are sized from a measurement instead of the first
draft's guess. The draft of this release boundedcatnip_runat 2100
seconds without measuring anything; a real run took 2459. Shipped as
drafted, shell-mcp would have SIGKILLed the pipeline 35 minutes into its
first scheduled cycle and the agent would have reported a timeout. Now
5400s/6000s/6600s, innermost-first, roughly twice the measurement. The
same draft told the agent to expect "10–20 minutes", which would have made
a normal 41-minute run read as anomalous; the prompt now states the
measured duration. catnip view why <view>had never worked. It took the view name from
--timeframe, which argparse restricts to1d/1w/2w/all/epoch, so the
name was rejected and a bareview whyasked derive for the derivation of
"2w". It is how both the README and thecatnip-prowlskill say to read
a derivation from a shell — the third instance of the same defect after
catnip reportshipping undispatched andcatnip config --jsoncolliding
with its own mutually-exclusive group.- Printing a derivation no longer requires a populated store. A
derivation is documentation, not data; it rode the same path as the views
and so failed on a fresh install — exactly when someone asks what a number
will mean before collecting any. attribution()returned different keys depending on whether the window
was empty, and the view read the missing one unconditionally, so an empty
window raisedKeyErrormid-render inside curses. The test asserts the two
returns carry identical key sets rather than the one key that was missing.- Config tests compared a resolved path to an unresolved tmpdir, which
fails only on macOS, where/varis a symlink to/private/var. Caught by
the cross-platform matrix, which runs onmainand on any PR carrying the
full-testlabel.
catnip 0.2.0 — every number from the durable store
GitHub keeps fourteen days of your traffic data. catnip keeps all of it.
Stdlib Python 3.10+ and gh. No service, no build step, no data leaving your machine.
git clone git@github.com:TGPSKI/catnip.git && cd catnip && make install
gh auth login --scopes repo
catnip init && catnip run && catnip tui
The change
Every windowed number is now computed from the permanent daily store rather than GitHub's rolling 14-day totals. The old views differenced two snapshots of a window that loses its oldest day every night, so they reported window artifacts as change — large negative clone deltas for repos where nothing had happened, and a timeframe selector several views ignored outright.
One derive.py holds every formula, shared by the TUI, catnip view and catnip report, so the three cannot disagree about the same day.
Added
- Ten views, eight new or rebuilt: traffic, audience, table, lang, attribution, deltas, anomaly, profile, correlation, funnel.
- Five detail screens behind
[space]— repo drilldown, attribution finding, momentum, account event, correlated pair, and a repo's actual pages. [?]derivation overlay on every view — formula, thresholds, inputs, and which components were withheld for want of evidence.catnip report— deterministic markdown from the store. Same store and timeframe produce byte-identical output; every figure taggedmeasured.- Three agent skills —
catnip-onboarding,catnip-triage, andcatnip-prowl, which runs the deterministic report and then forms and tests its own hypotheses, tagging each claimmeasured,inferredorspeculativein a separate file. - Store schema 3 ingests popular paths.
/traffic/popular/*rolls like the traffic endpoints, so paths that lived only in run directories were destroyed bycatnip pruneand could never be refetched.
The rule underneath
Absence is never rendered as zero. Withheld audience components are dropped and the weights renormalized. A repo too thin to classify is low-signal, not mixed. A day with no recorded push says no cause recorded in the store, which is not the claim that nothing shipped. unexplained is a first-class attribution tier, because most traffic has no visible cause and a tool that always names one is fitting noise.
Notes
The drawing layer is pane 0.2.0, vendored byte-identically. The demo recording uses generated data — only the API responses are synthetic; analyze, history, totals and derive all run for real on top of them.
226 tests, stdlib only.