Catches automations that are fresh on the outside, stale inside.
Your monitoring checks whether the job ran. watchpost checks whether it produced — because the expensive failures don't throw errors. The backup writes a file: empty. The report arrives on time: with last week's numbers. The sync says done: having synced nothing.
2026-08-03, 16:52. A scheduled market digest on our own machine fired on time, wrote its output file, and passed every freshness check we had — while serving Friday's data under Monday's timestamp. An AI summary layer then narrated three-day-old prices in the present tense, confidently. Every monitor said healthy, because every monitor judged the file, not the content.
watchpost was written the same afternoon, and its first production run caught the incident's residue: the "fixed" digest had been refreshed to stdout only, while the file on disk still held the stale data. The tool flagged its own author's fix as incomplete. That is the job.
| Check | What it catches | Who else does it |
|---|---|---|
MISSING / EMPTY |
The output doesn't exist, or is 0 bytes — the backup that has "succeeded" nightly for months | everyone |
STALE-FILE |
Not written recently enough (weekend-aware, so Mondays don't cry wolf) | everyone |
STALE-CONTENT |
The newest date inside the file lags the write time — fresh timestamp over old data | nobody |
FROZEN |
Rewritten byte-identical N runs straight — a report nobody is feeding | nobody |
pip install . # stdlib only, no dependencies
watchpost watch.json # check everything, write the report
watchpost watch.json --out report.html # self-contained, client-sendable
watch.json:
{
"client": "Acme Co",
"watches": [
{"name": "nightly backup", "path": "backups/latest.sql", "max_age_hours": 26, "expect_dates": false},
{"name": "weekly report", "path": "reports/weekly.txt", "max_age_hours": 170},
{"name": "export feed", "path": "out/feed.csv"}
]
}Exit code is the worst result (0 OK, 1 warnings, 2 failures) so a
scheduler can gate on it. The HTML report is self-contained — no external
requests — and written to be forwarded to a non-technical owner as-is.
- Date recognition is ISO (
2026-08-03) and US (08/03/2026) only. Two-digit years are deliberately ignored:03/08/26is ambiguous three ways, and a guard that guesses manufactures exactly the false confidence it exists to kill. Files without recognizable dates should set"expect_dates": falseand rely on the other three checks. FROZENneeds the state file (--state) to persist between runs.- It watches outputs on a filesystem. A stack living entirely inside SaaS tools with no exportable output is not a fit.
- Fail closed, loudly. A malformed watch entry produces a FAIL result, never a silent skip — a guard that crashes on one entry and drops the rest is the failure mode this tool exists to end.
- Don't cry wolf. Weekend grace on file age, business-day arithmetic on content lag, and deliberately narrow date parsing. A guard that false-alarms every Monday is muted by the second week, and a muted guard is worse than none because it looks like coverage.
Sister project: flatline finds what's already quietly dead (entropy-dead signals, silent no-op jobs, unreachable code). watchpost is the standing guard you leave running after the audit.
full-history (the default) holds the commits this tool was actually built in.
master holds the single squashed snapshot it was first published as. They share
no common ancestor, which looks odd enough to be worth explaining rather than
leaving you to wonder: the original release was pushed from a staging directory
and carried no history at all — one commit, no failures, nothing to inspect. For
a tool built to catch software that reports success while doing nothing, handing
you an unexaminable repo was the wrong move.
Rather than force-push over the snapshot, the real history was added alongside it
and made the default. Nothing was destroyed; master is still exactly what it
was. One file is absent from full-history on purpose: watch-own-stack.json,
the manifest describing this machine's own jobs. It names real local paths, so it
was stripped from every commit before publishing. demo-report.html only ever
existed in the published snapshot and was carried across, so republishing took
nothing away either.
Five small tools, one idea: software reports success while doing nothing, and nobody notices for months. Each answers a different question, and each says plainly what it cannot see.
| tool | the question it answers | its blind spot |
|---|---|---|
| attest | did this job run, and did it produce what it claimed? | it sees declared outputs, not whether they are correct |
| flatline | is this data still carrying information? | it waits to be asked |
| canary | what is wrong in the file that just landed? | it never sees whether a job ran at all |
| custody | what did the AI actually do, and was it right? | it cannot see inside the model, and never claims an answer was true |
They share one hash chain and one signature implementation, imported rather than copied — two versions of a trust primitive diverge the first time only one gets fixed.
Why there are several of these, and when we will delete one — the rule each tool had to pass to exist, the one overlap that is real, and the date we have committed to settling it.
MIT. Built by Automated Workflow — the same engine behind our automation monitoring service.