Status model¶
Every run reconciles the exit code (rc) against the parser into exactly one
status. pass is the only green state.
| rc | parser | status | meaning |
|---|---|---|---|
| 0 | confident, no findings, gates ok | pass |
green |
| 0 | gate failed (e.g. coverage < fail_under) |
fail |
tool happy, policy not |
| ≠ 0 | findings extracted | fail |
normal red + evidence |
| ≠ 0 | no findings, evidence expected | error |
infra / collection — fix the run |
| ≠ 0 | could not interpret output | error |
same, with log tail |
| 0 | findings anyway / unreadable | parse_mismatch |
green untrusted |
Invariants (enforced by ckdn.reconcile, covered by contract tests):
- Text never upgrades a nonzero exit code to green.
- A zero exit code never survives contradicting evidence.
- A confused parser sets
parser_ok=false→ louderror/parse_mismatch, never a silent clean.
Exit-code contract¶
ckdn run exits with the original command's code (clamped 1–255), so it drops
into any hook or CI slot where the raw command used to be. One extra rule:
rc == 0 with a non-green status exits 1.
When ckdn owns the failure it uses conventional synthetic codes — 124
timeout, 126 blocked by command policy, 127 command not found, 130
interrupted (Ctrl-C) — each also reconciling to a non-green status with
evidence.
How a run ends¶
timed_out and interrupted describe why the process stopped; they are
not results of their own, so the status stays inside the four-value model:
| ending | rc | flags | status |
|---|---|---|---|
| timeout | 124 |
timed_out: true |
error |
| Ctrl-C | 130 |
interrupted: true |
error |
A run that was cut short outranks every other signal, whether by Ctrl-C or
by its own timeout. It produced partial evidence, and partial evidence is
never a verdict: a half-written report does not become fail, and an
unreadable one does not become parse_mismatch — both are error. A killed
tool's findings describe the moment it died, not the code. Consumers that
predate these fields simply see error; an absent field means false.
An alias or --all series stops at an interrupted member rather than starting
the next one. Its aggregate carries interrupted: true and exits 130 — the
interruption outranks an earlier red member's exit code, which would otherwise
report the series' verdict and hide that the rest never ran.
What "terminated" guarantees¶
The child is detached into its own process group (POSIX) or held in a job
object (Windows), and the whole tree — not just the direct child — is
stopped: asked to finish, given a grace period, then terminated
outright. That is SIGTERM → grace → SIGKILL over the process group on
POSIX, and CTRL_BREAK → grace → the job object on Windows. The group or job
is what holds a wrapper's children when the wrapper itself exits first. The
tree is stopped on every path, including a clean exit, so a check cannot leave
a background process appending to a log whose digest is sealed.
What the "ask" buys differs by platform, and it is worth being exact. On POSIX
a tool that ignores SIGTERM keeps running until the grace expires. On
Windows the default CTRL_BREAK handler exits the process at once, so only
a tool that installs its own handler gets a shutdown window — for everything
else the ask and the kill look the same. The grace is an opportunity, not a
promise that a report will be finished.
On Windows the tree is held by a job object rather than by parentage, so a
grandchild whose parent already exited is still caught. Both halves are best
effort: if the job cannot be created the run falls back to taskkill, which
is blind to re-parenting.
Limits, stated rather than papered over:
- POSIX: a check that detaches into a new session of its own leaves ckdn's group and outlives the run. Nothing portable prevents that. A job is harder to leave — breaking out of one takes a flag the job does not grant.
- POSIX:
kill -9on ckdn runs no cleanup, so its tree survives. On Windows the job dies with ckdn's handle, so that case is covered when a job was created; where it fell back totaskkill, nothing runs either. - Windows: descendants the child creates before it is placed in the job are outside it. The window is milliseconds and cannot be closed from Python — a plain subprocess cannot be started suspended.
After any of these, the next run of that check reclaims the lock and says so in its notes. ckdn never kills anything it cannot prove it owns: only its own pid is recorded, and pids get recycled.
The log streams straight to full.log, so an interrupted run still leaves the
output it managed to produce, and meta.json records the sha256 of those
bytes exactly as they sit on disk.
For an alias, the exit code is the aggregate rc; see
Aliases & aggregates.
Determinism¶
digest.json is deterministic: given identical tool output and an identical
run-directory path, ckdn writes byte-identical JSON (keys sorted, no
timestamps or durations — those live in meta.json). Paths are normalized to
forward slashes so a digest is byte-stable across operating systems. This is
guarded by tests, so a regression fails CI.