audit 2026-09-05 (wave 2): artefact-keyed freshness, start sentinel, tz-aware time, anchored regex - #6
Merged
Merged
Conversation
A log filename carries no offset, so a naive strptime of "2026-01-15-0000" compared against a naive datetime.now() skewed every age by the gap between the producer's clock and the checker's. A UTC-stamping job read from a UTC+10 host reported ten hours fresher than it was -- the wrong direction for a tool whose whole job is catching what stopped. Adds an optional top-level `timezone` key naming the clock that writes the log stamps: "local" (default), "UTC", a fixed offset like "+10:00", or an IANA zone name. Both sides of the age subtraction are now aware. An unresolvable zone raises a ConfigError rather than falling back silently to the wrong clock. A caller passing a naive `now` still works -- it is read in the configured zone -- so existing embedders and the fixed-clock tests are unaffected. Enables ruff's DTZ rules so the naive-datetime shape cannot come back, with the test module exempted where its fixed naive clocks are the fixture. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
check_task tested `task.sentinel in body`, so a log that only mentions the string reported FRESH. A run quoting its own last failure, a summary line naming the sentinel it went looking for, a config pasted into the output -- each passed the switch. That is a false FRESH, strictly worse than the false finding it avoids, and the same bug applied to failure_sentinel in reverse. Both tests now anchor to the start of a line, tolerating the indentation, backticks, asterisks and underscores a real sentinel line picks up on the way into a log, and stripping a leading byte order mark first. A word boundary stops MY_JOB_OK matching MY_JOB_OK_PENDING, and is dropped for sentinels that do not end in a word character. Ported from the same fix made to this pattern's origin implementation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…INEL The state machine could not tell a job the scheduler skipped from a job that fired and wedged halfway. Both produced a log with no success string, so both reported NO_SENTINEL, and those are different problems with different fixes. Adds an optional per-task `start_sentinel` the job writes as its first act. A log carrying the start string but neither success nor failure now reports HUNG, with the detail line saying how long ago the run began. Optional `max_runtime_hours` is the allowance before that counts as hung -- inside it the task reports RUNNING, which passes, so checking while a long job is genuinely mid-flight raises nothing. Staleness still short-circuits, so an unfinished start past max_age_hours is STALE as before. Tasks that configure neither key behave exactly as they did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…duces A log line only exists if the job ran through whatever wrapper writes the log. Run the same job another way -- by hand, from another host, through an agent rather than its cron entry -- and the log stays put while the real work happens, so the switch reports a permanent false-stale on a healthy task. The lane this pattern came from sat at CRITICAL for ten days that way. Adds an optional per-task `artefact` object naming what the job actually produces. `format: "mtime"` takes the file's modification time; `format: "jsonl"` takes the newest `timestamp_field` across the records of a newline-delimited JSON file. `match` filters those records by field regex, so an unrelated append to a shared file cannot report the lane fresh. When the artefact is the newer signal it decides the state and the older log's sentinel is not scored, because the artefact already shows the job produced its output. When the log is newer nothing changes, and a task with no `artefact` behaves exactly as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four fixes from the 2026-09-05 audit pass, all in
deadmans.py. Two correct states the checker was reporting wrongly, two add optional config keys that stay inert unless you set them. Existing configs behave exactly as they did.Tests: 121 pass (was 60),
ruff checkandruff format --checkclean,deadmans.py selftestpasses.DTZis now in the ruff select set so the naive-datetime shape cannot come back.Read log stamps and the clock in a configurable timezone (
1f0f52ff)A log filename carries no offset, so
strptimeon2026-01-15-0000compared against a naivedatetime.now()skewed every age by the gap between the producer's clock and the checker's. A UTC-stamping job read from a UTC+10 host reported ten hours fresher than it was, which is the wrong direction for a tool whose job is catching what stopped.New optional top-level
timezonekey naming the clock that writes the stamps:local(default),UTC, a fixed offset like+10:00, or an IANA zone name. Both sides of the subtraction are now aware, and an unresolvable zone is a config error rather than a silent fall back to the wrong clock. A caller passing a naivenowstill works, read in the configured zone.Require the sentinel to open a line (
8b24219f)check_tasktestedtask.sentinel in body, so a log that only mentioned the string reportedFRESH. A run quoting its own last failure, a summary line naming the sentinel it went looking for, a config pasted into the output: each passed the switch. That is a falseFRESH, strictly worse than the false finding it avoids, and the same bug applied tofailure_sentinelin reverse.Both tests now anchor to the start of a line, tolerating the indentation, backticks, asterisks and underscores a real sentinel line picks up, and stripping a leading byte order mark first. A word boundary stops
MY_JOB_OKmatchingMY_JOB_OK_PENDING, dropped for sentinels that do not end in a word character.Optional start sentinel, so a hung run reports
HUNG(c31eb5a4)The state machine could not tell a job the scheduler skipped from a job that fired and wedged halfway. Both produced a log with no success string, so both reported
NO_SENTINEL, and those are different problems with different fixes.New optional per-task
start_sentinelthe job writes as its first act. A log carrying the start string but neither success nor failure reportsHUNG, with the detail line saying how long ago the run began. Optionalmax_runtime_hoursis the allowance before that counts as hung; inside it the task reportsRUNNING, which passes, so checking while a long job is genuinely mid-flight raises nothing. Staleness still short-circuits, so an unfinished start pastmax_age_hoursisSTALEas before.Optional artefact source, so freshness can key on what a job produces (
41d7ecff)A log line only exists if the job ran through whatever wrapper writes the log. Run the same job another way and the log stays put while the real work happens, so the switch reports a permanent false-stale on a healthy task. A permanent false-stale trains you to ignore the switch, which is the one outcome worse than not having it.
New optional per-task
artefactobject naming what the job produces.format: "mtime"takes the file's modification time;format: "jsonl"takes the newesttimestamp_fieldacross the records of a newline-delimited JSON file.matchfilters those records by field regex, so an unrelated append to a shared file cannot report the lane fresh. When the artefact is the newer signal it decides the state and the older log's sentinel is not scored, because the artefact already shows the job produced its output.Notes
timezone,start_sentinelandmax_runtime_hoursadded todeadmans.example.json.#3(PyPI packaging) also touches README.md, in the Install section above Quickstart. This branch only edits from Config reference down, so the two should merge cleanly in either order.🤖 Generated with Claude Code