Infirmary checks a fleet of machines and agent lanes for stalls and gives advice. It is advice-only, and repair is off by default; live repair has never run on a live fleet. MIT licensed, see LICENSE.
Two commands: stall-explain (explains a quiet lane) and infirmary (detects the snags a fleet of agent lanes runs into).
When a fleet of long-running agent lanes goes quiet, most of the alarms are not stuck workers. They are
a lane that finished and waits for whoever supervises it, a long test that is queued behind another, a
declared wait, a model call that is thinking. stall-explain answers "why is this lane quiet?" with one
line and the evidence behind it, from local files and /proc, in well under a second:
$ stall-explain lane-finished
waiting-on-supervisor · lane-finished · finished 30m ago; the next move is the supervisor's
$ stall-explain lane-busy -v
running-long · lane-busy · agent process tree is burning 1.0 cores
declarations: newest event: working 2h00m ago "started"
process tree: 1.0 cores over the last sample, 0 descendants
recommendation: no action
took 411 ms (budget 1000 ms)
$ stall-explain lane-quiet --idle-s 3600; echo $?
wedged · lane-quiet · idle 60m, nothing running or declared
30
What it is. A deterministic explainer: a fixed rule order over evidence a source collected, one
verdict, an exit code per verdict, --json for machines, and a --replay mode that scores the rules
against recorded alarms before anyone relies on them.
What it is not. Not a monitor, not a restarter, not an alert router, no model calls. stall-explain only
explains. infirmary check detects and advises by default (advice-only); once an operator runs infirmary live on
for a home it runs a short, closed list of safe repairs on that home (live-repair), and only by calling the owner's own
command (below).
Status. Version 0.4.2. 0.4.0 was the first public release. This is early software, written against one fleet's incidents and maintained by one person.
stall-explainandinfirmary checkrun on Linux and WSL2. macOS and Windows are partial: many rows there reportunknownor are designed but not yet run, and a Mac was not measured. Do not runscripts/teston a Mac (Platforms).- It is advice-only by default. Live repair has never been run on a live fleet; it has only run against synthetic homes and shim owners, and it is off unless an operator turns it on.
- The 88.5% replay accuracy comes from one fleet's alarms, which the rules were written against. Read it as a description, not a forecast (Measured accuracy).
- The hosted CI matrix across Linux, macOS and Windows is a manual workflow. Its first run (2026-10-16) passed on Ubuntu and failed 3 checks each on macOS and Windows. Those were test expectations that only hold where
/procexists (and a path-format and a temp-path assumption), not product bugs; the tests were corrected and a second run the same day passed all five jobs (Ubuntu 3.8 and 3.13, macOS 3.13, Windows 3.8 and 3.13). In 0.4.0 a lane that is gone or silent on macOS or Windows was reportedunknown, notdeadorwedged, because the process evidence came only from/proc; 0.4.1 reads the process table there too (pson macOS,Win32_Processthrough PowerShell on Windows), and the hosted matrix has not yet been run on that change (Platforms). - It ships no adapter for a specific tool, only the example in
examples/adapter/files-home/.
Issues are the useful feedback: a verdict that looks wrong, a check that reads unknown on your machine, a platform it fails on, or a first run that confused you (SUPPORT.md).
Intent. Most alarms from long-running agent lanes are not stuck workers. Infirmary answers "why is this lane quiet?" with one line and the evidence behind it. It is deterministic, makes no model calls and reads local records. It advises, and it does not act unless an operator opts in. It is deliberately not a monitor, not a restarter and not an alert router.
What is open today, in one line each. The status above says the same in more words.
- Live repair has never been run on a live fleet; it has only run against synthetic homes and shim owners (docs/safety.md).
- The macOS process reader (
ps) has been run read-only on a Mac and against synthetic output, butscripts/testhas not run it on a Mac; the Windows reader has run underscripts/teston a Windows 11 PC (Platforms). - On Windows each process read starts PowerShell: about 0.6 s, and 1.3 s for the read that also samples CPU, on a Windows 11 PC. A verdict there is over the 1 s target, and on a slower machine it can pass the 2 s cap and read
unknown("time budget timeout"); each slot-holder pid a lane records adds a read (docs/verdicts.md). - Where the process table cannot be read (no reader for the platform, the tool missing or timing out, output it cannot parse, a command line Windows will not show without elevation), a lane that is gone or silent reads
unknown(exit 50) and says why, notdeadorwedged(docs/verdicts.md). - The macOS LaunchAgent install has been run only against a
launchctlshim on Linux, not on a real Mac (Platforms). scripts/testmust not be run on your own Mac: it can write real LaunchAgent plists into~/Library/LaunchAgents(Platforms).- The 88.5% replay accuracy is measured on alarms the rules were written against, so it is not a held-out figure (docs/accuracy.md).
- It ships no adapter for a specific tool, only the example in
examples/adapter/files-home/; the scheduled advise run and the incident rules do nothing for you until you write one (docs/sources.md). - Python 3.9 and 3.10 have not been run, and macOS with Python 3.8 is not in the CI matrix (Platforms).
Needs Python 3.8 or newer. No install, no dependencies. Two words you will meet: a lane is one long-running agent session (one worker, with its own state files and processes); an adapter is a small directory of code that teaches the Infirmary where one supervising tool keeps its lanes, so it can read them, schedule the advise run and apply the incident rules for that tool.
Clone this repository (or unzip the package), cd into it, and run these from there. Every command is bin/..., so nothing has to be on
your PATH (to type plain infirmary instead, run export PATH="$PWD/bin:$PATH" once in that shell):
bin/stall-explain --demo # replays 15 invented alarms, every verdict, with the accuracy gate
python3 examples/demo-fleet/make_demo.py /tmp/demo # five real little lanes (two child processes, gone after 60 s; add --stop DIR to end them now)
bin/stall-explain --set files.lanes=/tmp/demo/lanes.json lane-busy -v
bin/stall-explain --set files.lanes=/tmp/demo/lanes.json lane-quiet --idle-s 3600
bin/infirmary check # the catalogue: disk, memory and swap, orphaned servers, ... on this machine
bin/infirmary check --json --no-write # the same as `doctor.json`, writing nothing
bin/infirmary status # kill switch, host identity, the host beat, the last check, breakers, plans
bin/infirmary live # is live repair on for this home? (off unless you turned it on)
bin/infirmary plans # tier-2 repairs wait as plans until a person approves one
bin/infirmary advise --no-write # the advice-only pass: the check plus stall-explain over the day's recorded alarms
python3 scripts/test # the fixture and smoke tests (repository only, not in the package; Linux/WSL2 only, see Platforms)
Not in the block above on purpose, because they change state or need something of yours: bin/infirmary live on and live off (the
operator's one switch for live repair; leave it off while you are looking around), bin/infirmary approve PLAN_ID (needs a plan from
plans), and bin/infirmary install --home HOME --adapter NAME --advice-only --dry-run (schedules the advise pass for one home with one crontab
line; uninstall undoes it; HOME must be a real directory your adapter knows). To see it work with nothing of your own, use the example adapter
over a folder that holds a lanes.json (the demo's does):
bin/infirmary install --home /tmp/demo --adapter files-home --advice-only --dry-run --set 'adapter_paths=["'$PWD'/examples/adapter"]'
What works with nothing else. stall-explain over your own lanes.json (docs/sources.md), --demo, --replay,
and infirmary check, the generic host checks (most rows say skip with the reason until you configure what they need).
What needs an adapter for your own tool. The scheduled advise run (install, advise over a home's recorded alarms) and the incident
rules read a supervising tool's own records, so they need an adapter that knows that tool. This repository ships no adapter for a specific tool:
examples/adapter/files-home/ is a small generic one over plain files (a lanes.json and an alarms.jsonl) with one check, one
incident rule and one repair action, to run as it is or copy (its README). To write your own, copy that folder,
replace the file reads in source.py (and the check, rule and action in doctor.py) with reads of your tool's records, keep it in a directory listed in
adapter_paths and select it with --adapter NAME (docs/sources.md, CONTRIBUTING.md); until you do, those parts do nothing for you. Live repair has never been run on a live fleet by the project; it has only run against synthetic homes and shim owners, and
it is off unless an operator turns it on (docs/safety.md).
A first infirmary check can read CRITICAL, exit 30, on an ordinary machine. The free-space defaults come from one fleet's incidents
(warn under 40 GB free, critical under 15 GB; 15% and 7% of the volume also apply), so a small disk or a RAM-backed /tmp or home is called
critical and sets the drain flag, which only records the word and stops nothing. Read the row; if it is the disk, set the thresholds for
your machine (--set doctor.disk.warn_gb=2 --set doctor.disk.crit_gb=1). See docs/troubleshooting.md.
To point it at your own lanes, write a lanes.json (docs/sources.md) or use an
adapter. Settings live in infirmary.json (docs/verdicts.md lists every threshold).
Ten snags (and, for Windows hosts, three more: a hung WSL, leaked farm processes, capability drift; and for the Windows platform under WSL lanes, five more: virtualization off in firmware, hypervisor launch failures, a pending restart, Windows Update restarts, the update policy) that cost real hours on one fleet, each detected from files, /proc and read-only git, each reported as a
finding with its evidence, a plain explanation, the owning script's dry-run command as the "how", and a recommendation:
$ infirmary check
infirmary 0.4.0 · host box (wsl) · mode advice-only · 17 checks in 86 ms
[ok ] 1 Remote job workers 1 worker tree(s) for ~/.state/remote-job, code identity matches
[CRIT] 2 Other hosts (hung or out of memory) 1 finding(s); win-box: 884 MB free of 32189 MB, commit 88%
- win-box: win-box is short of memory
only 884 MB of 32189 MB free
how: ... the Infirmary sets the drain flag and stops nothing.
[WARN] 4 Review-gate refs 1 finding(s); 7 gated lane(s) compared, 6 in step with their gate
- lane-7: the review gate holds work the lane does not
gate 1111aaaa is not an ancestor of lane 2222bbbb and the lane is not an ancestor of the gate ...
how: dry run (in /path/to/worktree): gate-tool sync --check; ...
...
CRITICAL · 1 critical, 1 warning · exit 30
It also writes doctor.json (findings, per-host drain/hung/disk_critical flags, its own cost) and, with --push,
publishes the findings as cards on a Lookout board. The catalogue, its thresholds and
what each check needs: docs/catalogue.md. check --card DIR or infirmary card also produce one small
status card for a dashboard that reads them (docs/findings.md); nothing is written unless asked. What the Infirmary could not decide is never blank on any of these surfaces: each distinct unknown is one card with its exact reason, and the status card never says all clear beside one (the unknown vocabulary). Detection of items needing a specific tool lives in that
tool's adapter. One check is about the operator: ssh-check recognises an ssh login that stops at an additional-check prompt
(Tailscale SSH) and reports it with its login URL, because steers and pushes over that alias then wait silently until the operator
signs in (docs/ssh-check.md, with the check-period setting).
Some failures are not snags to list but things that have actually happened: a worker's agent died while its window stayed open, the
supervision watcher went down, a mirror stopped pushing, a machine stopped answering, a project clone has no git identity, a pipeline gate is
parked and nobody drives it. For each, the Infirmary writes one append-only row in plain words: what happened, since when, what it blocks,
whether work is at risk, the records it rests on with their ages, and the one safe next step as text (for a worker whose agent is gone: the
relaunch command that keeps the worktree). Rows go to incidents.jsonl in the state directory (never rotated, never edited; what is open now is
incidents.json), appear under their findings in check, first in advice.txt, in the status card and, with --push, on the board. An input a
rule cannot read is an unknown row that says what could not be read, never silence; a healthy look-alike stays quiet. Advice-only: nothing
acts on a row. A gone agent is carded after 3 minutes of silence; install --every 2 makes it a row within 3 to 5
minutes. The six rules, their look-alikes, the row format and the cost are in docs/incidents.md.
The Infirmary never signals a process, deletes a file, moves a branch or edits a record itself. A repair calls the owner's own command with the arguments the owner's dry run proposed:
- Tier 1 (run unattended while live repair is on for the home) only where the owner itself proves its target safe and nothing touches a lane, a session or a pool slot, e.g. "stop job workers whose code root is gone" through the owner's reap script. Each run passes the gates (kill switch, a host circuit breaker when 4 or more findings are open or the host is critical, a per-action breaker after 2 unverified runs, flap detection, rate limits of 3 per host, 2 per action and 1 per finding per hour, one repair at a time), takes two readings of the owner's dry run and of the finding (the second immediately before acting, with the same targets required), acts once, and counts as done only when the check no longer finds the snag.
- Tier 2 (everything riskier) is an inert plan file.
infirmary approve PLAN_IDre-verifies it from scratch and runs exactly its argv once; a hand-edited, expired, used or outdated plan is refused. Plans an owner must carry out (a lane's worker moving its own branch) are only handed over.
Live repair is one explicit switch, per home, off by default. Until an operator runs infirmary live on the Infirmary is
advice-only: findings, explanations and plans, and it never acts on the home. live on records who turned it on and when
in the home's own state directory (live-repair.json); infirmary live off turns it off again, at once, even for a
repair in flight; infirmary live and infirmary status say which mode it is in (advice-only or live-repair, also
in doctor.json and on the Lookout source label). Nothing turns it on for you: no configuration key, environment variable,
default, installer or adapter does, and mode: repair is refused as a setting. With it on, every gate still applies
unchanged, and the kill switch wins over it (infirmary off, INFIRMARY=off). infirmary approve is a person's
own act and works in either mode. See docs/safety.md.
Every run and refusal is a line in actions.jsonl with its reason and both readings, and a Lookout card. Details, the
gates and their numbers, and how to add an action: docs/repairs.md. The example adapter ships one action
(examples/adapter/files-home/); an adapter you write adds its own.
infirmary install --home HOME --adapter NAME --advice-only schedules infirmary advise for one home every 10 minutes. It
adds one line to the user's crontab (one LaunchAgent on macOS) and nothing system-wide, restarts nothing and edits no shared
configuration. infirmary uninstall --home HOME takes it out again and leaves the crontab as it was. infirmary installed --home HOME says whether it is in place and its advice fresh. Each run does the catalogue check, never repairing, and explains
every alarm the home recorded in the last 24 h. It leaves advice.txt and advice.json in the home for a supervisor to read at
wake time, plus a lookout.card 1.0 card: how many alarms were explained, by which explanation, and how many were real. The
install has no repair option, and the scheduled run repairs nothing even if live repair is turned on later. Formats, the card and
the install: docs/advice.md.
Judged alarms. For every alarm the advise run explains it appends one row to judged.jsonl in the state directory (append-only, never edited):
the verdict it gave, the sources it rests on and how old each state source was. Once 30 minutes have passed, a deterministic follow-up appends a
new row naming it: what the lane did next (relaunched, gone, finished, answered, moved, stuck, or quiet) from the records the run already read,
and whether the verdict was wrong, by a fixed table: a benign verdict followed by a stall that persisted or by a relaunch is wrong-benign; a real
verdict where the lane moved on by itself with no relaunch and no instruction recorded is wrong-real; anything not yet decidable or lacking
evidence is undecided, with its reason, and never counted as agreed. infirmary judged prints the counts for a window (explained, real,
wrong-benign, wrong-real, undecided). Nothing acts on a row. The table, what the records cannot know and the format: docs/judged.md.
| Verdict | Exit | Meaning |
|---|---|---|
queued, running-long, running-remote, thinking, declared-wait, torn-down |
0 | explained; nothing to do |
waiting-on-supervisor |
10 | the worker is done or asked a question; the supervisor's move |
waiting-on-human |
20 | a prompt needs a person |
wedged |
30 | alive, idle, nothing behind it |
dead |
40 | nothing there |
unknown |
50 | the evidence does not decide; never a guess |
A benign verdict needs positive evidence that was seen (a live job, a queue ticket, process or file
activity, a declaration). Silence alone is unknown. Details and thresholds: docs/verdicts.md.
Replayed over 54 real stale alarms from one fleet's day, against a hand classification: both real stalls flagged, 46 of the other 52 benign or supervisor-owed (88.5%; gate 80%). The rules were written against this same data, so treat it as a description, not a forecast. Full table, the six misses and the method: docs/accuracy.md.
One verdict takes about 5 ms from files and 0.4 s when it also samples a process tree; RSS about 27 MB;
the 63-alarm replay runs in about 50 ms. The hard cap is 2 s, after which the answer is unknown. The incident rules run inside the Infirmary's existing 2 s local budget; the fold into rows and the ledger append are
cost.incidents_ms in doctor.json, under 1 ms; a row is about 1.7 KB. The judged ledger adds no process: its rows are appended from what the advise run already read, 1 to 2 ms
a run on the fixtures' synthetic home (21 lanes, 105 alarms; the whole advise run about 20 ms), a verdict row about 0.7 KB and an outcome row about
0.4 KB (100 alarms a day is about 110 KB a day), the ledger read once per run at about 11 ms for each MiB; the one extra read is the names and times
of the files in a lane's own inbox (docs/judged.md).
The advise run keeps its own cost down: a wall-time budget (30 s), a CPU budget (40 CPU-seconds an hour) and a back-off when the machine's 5-minute load per
core is above 0.8 or it is on battery below 30%, in which case it says skipped on purpose: load 0.90 per core (limit 0.80) instead of running (docs/advice.md).
Measured on a Linux/WSL2 machine (24 cores): 2.2 s wall, 0.28 CPU-seconds and 35 MB for one run over a real home with 13 alarms, 0.02 s and 0.01 to 0.02 CPU-seconds
over the fixtures' synthetic home. On a Mac (8 cores, a run every 120 s, from an install's own log) a full run used 1.6 CPU-seconds on average and 2.5 at the
95th percentile, so the default CPU budget of 40 an hour allows about 25 full runs of the 30 an hour, at most about 1.2% of one core (docs/advice.md).
| Linux / WSL2 | macOS (Apple Silicon) | Windows 11 | |
|---|---|---|---|
Verdicts from files, --replay, --demo, --json |
run | designed, not yet run | designed, not yet run |
| Process tree, CPU, process identity | run (/proc) |
read from ps (read-only): the parser runs against synthetic output on every platform, and the reader was run read-only on a Mac (its own process, a gone pid); not run by scripts/test on a Mac; "unavailable", not guessed, when ps fails |
run from Win32_Process through PowerShell (read-only, fixed script) by scripts/test on a Windows 11 PC and against live processes; "unavailable", not guessed, when PowerShell fails |
| Infirmary: disk, memory and swap, orphaned servers | run | parsers run against recorded or documented ps/vm_stat/sysctl output; not run on a Mac |
reports unknown for process and memory checks, never "fine" |
| Infirmary: scratch and RAM-backed volumes, WSL interop (catalogue 19 and 20) | run on Linux/WSL2 (fixtures and read-only on a live fleet host) | scratch by command lines only; what needs /proc says skip or unknown; not run on a Mac |
reports unknown or skip, never "fine" |
| Infirmary: the Windows platform under the lanes: firmware virtualization, hypervisor launch failures, pending restart, Windows Update restarts, update policy (catalogue 23 to 27) | run from a WSL2 guest through powershell.exe (fixtures, and read-only on a live fleet host); the same script rides on the ssh probe of a Windows host (fixtures) |
skip (no Windows side) | same code natively, not run on Windows |
| Infirmary: ssh additional-check prompts on each configured alias (catalogue 28) | run (fixtures, and read-only on this machine's aliases) | same code, not run on a Mac | reports unknown, never "fine" |
| Infirmary: probing another host (ssh), including a Windows host's memory, its WSL VM, leaked farm processes and capabilities | run (Linux/WSL to a Windows host over ssh, read-only) | same code, not run | same code from Windows, not run |
advise, and the per-home install (install, installed, uninstall) |
run: advise on synthetic homes and read-only (--no-write) on a live home; the install against a crontab shim |
the LaunchAgent install is run against a launchctl shim on Linux only; not run on a Mac |
no scheduler entry; advise runs by hand |
Infirmary: repairs, plans, approve, breakers, kill switch, the live-repair toggle |
run against synthetic homes and shim owners only; the toggle is off by default and no repair has been run on a live fleet by the project | same code; a repair that stops processes is always a plan there (no pidfd); not run | lock via msvcrt; an owner is a POSIX script in the example adapter; not run |
"Run" means exercised by scripts/test (or, for a probe of another machine, run against the real machine) on that
platform during development (Linux/WSL2 with Python 3.14). scripts/test
here runs the core, the generic checks and the example adapter. scripts/test passes on Python 3.8, 3.11, 3.12, 3.13 and 3.14 (Linux/WSL2); 3.9 and 3.10 have not been run. scripts/test is the only suite, and it is
run to a pass on Linux and WSL2 during development, and on GitHub's hosted macOS and Windows runners on 2026-10-16 (below). Do not run it on a Mac. It starts the example adapter's scheduler fixtures, and on
macOS the default scheduler is launchd, so a run there wrote real LaunchAgent plists into your ~/Library/LaunchAgents (the install scenario now asks for cron
explicitly, which has not been confirmed on a Mac), and a run on a Mac failed 13 of its checks (that was a local run before the install scenario asked for cron; the hosted macOS runner, which is disposable, passes). On Windows the scenarios that use POSIX shims are skipped by design; the suite passes on a Windows 11 PC and on the hosted Windows runner. .github/workflows/ci.yml runs the suite on all three systems, by hand only
(workflow_dispatch), so the matrix is a way to find out, not a record of a pass. The first hosted run (2026-10-16) failed 3 checks each on macOS (3.13) and Windows (3.8 and 3.13): one scheduler-install scenario (macOS), the lane-gone exit 40, the one-hour-silence wedged exit 30, and a self-audit path-format check (Windows). None was a product bug: the macOS and Windows checks expected Linux-only process evidence (the product correctly said unknown, exit 50, "process unavailable", and the tests then asserted exactly that; from 0.4.1 those systems read their own process table and the checks expect dead and wedged there), the Windows self-audit check looked for a / path separator, and the macOS install fixture compared a temp path that is a symlink there. A second run after the correction passed all five jobs: Ubuntu 3.8 and 3.13, macOS 3.13, Windows 3.8 and 3.13. The macOS 3.8 cell is not in the matrix, so macOS with Python 3.8 has not been run.
The dashboard repository's docs/honesty.md sets the rules for everything that feeds a board; this section is what they mean here.
- What each output is. A verdict is derived: a fixed rule order over evidence a source measured (files,
/proc, read-only git), and the first rule with positive evidence wins (docs/verdicts.md,src/infirmary/explain/engine.py). A finding is measured facts plus a plain explanation, the evidence it rests on listed beside it (docs/findings.md). An incident row is derived from the records it names, with their ages (docs/incidents.md). The accuracy figure is a measured replay of one fleet's alarms, and says it was not held out (docs/accuracy.md). - What it cannot know. Only what its sources recorded. Silence alone proves nothing, so a lane with no positive evidence is
unknown, not benign (docs/verdicts.md). It cannot see inside an agent's model call, work no record shows, or a Mac or Windows host it has not run on (Platforms). - Unknown and stale are shown as such. The
unknownverdict (exit 50) is never a guess; a rule that cannot read its input writes anunknownincident row naming what it could not read, and a record with no time reads "age unknown" (docs/incidents.md,src/infirmary/explain/engine.py). A declaration older than the last boot or the lane's session does not count (docs/evidence.md). Nothing is called fine because nothing was seen. - What it never claims. A cause it was not given, an accuracy beyond the replay above, or a number about its own value: the judged ledger counts what a verdict was followed by, and calls what it cannot decide
undecided(docs/judged.md). It makes no model call (What it will never do). - Backing off is said, never silent. An advise run that steps aside because the machine is loaded, on a low battery or past its CPU budget writes
skipped on purpose: <reason>as the first line ofadvice.txtand as the card's headline (statusunknown, neverok); a reading it could not take (load, power) is "not read", never zero (docs/advice.md). Version gaps are the dashboard's own words, never the Infirmary's comparison: a machine whose version the dashboard cannot read is "unknown", not "old" (docs/catalogue.md, item 35). - Advice-only takes no action. Until an operator runs
infirmary live onfor a home,checkandadviseonly detect, explain and advise; nothing turns live repair on for you, and the scheduledadviserun never repairs (docs/safety.md,src/infirmary/core/live.py). Live repair, when on, calls only an owner's own command, and every run and refusal is a line inactions.jsonl(docs/repairs.md).
- Act while live repair is off (the default, advice-only): no signal, restart, file change or command outside its own state directory.
- Turn live repair on by itself. Only the operator's
infirmary live ondoes, per home; no setting, default, installer or adapter can. - Act, even with live repair on, except through an owner's own command, from a closed list in code, after re-checking right before acting; signal a process itself; force anything; delete what an owner's dry run did not list; discard unlanded work; let configuration add or widen a repair.
- Open a network connection except a Lookout push you configure, with the token read from a file or an environment variable, never from a command line, and the read-only probes of the hosts you list.
- Change a firmware, boot, update-policy, service or scheduled-task setting on any machine, or install anything; on Windows it only reads.
- Read or print a secret, send telemetry, or call a model.
- Call anything "fine" because it saw nothing. A rule that cannot read its input writes an unknown incident row naming what it could not read; a declaration that predates the last boot or the lane's session counts for nothing; a record with no time is shown as "age unknown", never as fresh.
- Rewrite, trim or delete an incident row, or act on one: the ledger is append-only, and the next step in a row is text for a person.
See docs/safety.md. Layout: src/infirmary/ (core, explain, doctor), examples/ (with examples/adapter/files-home/, an adapter to copy; adapters for a specific tool
live outside this repository, in a directory named by adapter_paths), docs/, scripts/ (self-audit,
build-package, test, merged-check). Licence: MIT (LICENSE). Contributing: CONTRIBUTING.md.
Security: SECURITY.md. Support: SUPPORT.md. Conduct: CODE_OF_CONDUCT.md. Changes: CHANGELOG.md.