Everything a consuming repository needs is in this directory. Two files get copied; the rest are examples.
| File | What it is |
|---|---|
plumbline-gate.sh |
Copy this. The runner. Reads the pin, resolves the harness, runs the gate, exits with the gate's code. |
plumbline.pin.example |
Copy this as plumbline.pin and edit it. The single file that records which harness commit gates your repository. |
Makefile.example |
How local tooling calls the same runner. |
github-actions.example.yml |
How CI calls the same runner. |
your-repo/
plumbline.pin # repo = ..., ref = <40-char commit>, config = ...
plumbline-gate.sh # copied from here, committed
plumbline/
target.toml # your suites and floors
bundle/ # your evidence bundle
baseline.json # optional, the bar you are holding
./plumbline-gate.sh # locally
./plumbline-gate.sh # in CI — the identical commandThree properties, and each one is a thing that goes wrong otherwise.
One file, both callers. A laptop and a CI runner read the same
plumbline.pin. "Works locally, fails in CI" and its worse twin, "passes in
CI, fails locally", both come from two places recording two versions of the
tool. There is one place.
An exact commit, not a range. ref must be a full 40-character commit
hash; the runner rejects a branch or a tag. A moving ref means a green gate
today can quietly mean something else tomorrow, which is the opposite of what
an audit record is for. Bumping the pin is a reviewed diff, like any other
dependency upgrade.
Resolved at run time, not installed. The harness is not in your
requirements.txt or your lockfile. It is fetched into a cache directory when
the gate runs and verified to be at the pinned commit. The thing auditing your
repository is not a thing your repository's own dependency resolution can
quietly move.
Every way this can go wrong exits 4 with a reason on stderr:
- no pin file, or a pin file missing
repo,reforconfig - a
refthat is not an exact commit hash gitor Python missing- the harness repository unreachable, or the pinned commit absent from it
- a resolved checkout that is not at the pinned commit
- a resolved checkout with no
src/ - a model judge configured with
mode = "live"— the gate does not make network calls. Record the judgments withplumbline audit, commit the judgment cache, and gate against it offline.
There is no path through the runner that skips the gate or reports success without having run it. If the harness cannot be reached, the job fails. A gate that could not run is not a gate that passed, and a build that treats those as the same thing is a build with no gate.
| Code | Meaning | What a CI job should do |
|---|---|---|
| 0 | Every enabled suite passed | merge |
| 1 | At least one suite failed | block; read the named suites |
| 2 | Usage error | fix the command |
| 3 | Integrity refusal — the evidence bundle did not verify, nothing was scored | block; the evidence is untrustworthy, which is not the same problem as a regression |
| 4 | Configuration or environment error, including an unresolvable harness | block; the gate did not run |
| 5 | Internal error — the harness crashed; nothing was measured | block; report the bug. This is deliberately not 1: exit 1 is a verdict, and no verdict was produced |
The separation of 1, 3, 4 and 5 is the point. "The target got worse", "the evidence is untrustworthy", "the gate was misconfigured" and "the instrument broke" need four different humans to do four different things, and a single non-zero exit tells you which one only if you read the log. What they have in common is that all of them block.
PLUMBLINE_SRC=/path/to/plumbline/src ./plumbline-gate.sh bypasses resolution
and runs the working tree. It prints two loud lines to stderr saying the run is
not pinned and not reproducible. It is for developing the harness. CI must
never set it.
| Variable | Default | Purpose |
|---|---|---|
PLUMBLINE_PIN_FILE |
plumbline.pin |
Read the pin from somewhere else. |
PLUMBLINE_CACHE_DIR |
.plumbline-cache |
Where resolved harness checkouts live. Cache it in CI keyed on the pin file. |
PLUMBLINE_PYTHON |
python3 |
Interpreter to run the harness with (3.11+). |
Arguments given to plumbline-gate.sh are passed through to the harness, so
./plumbline-gate.sh --summary-file "$GITHUB_STEP_SUMMARY" works.