v0.55.0
[0.55.0] - 2026-08-26
This release removes places where the harness reported success while checking
nothing. Three of its four changes are the same shape: a gate or a metric that
produced a green, confident answer without the input it claimed to be reading.
Nothing here is a new capability — verification_plan is the only addition, and
even it makes verification CONSISTENT rather than stronger: it aligns the local
gates to CI, and a project whose CI is weak now reproduces that weakness
faithfully.
Known and deliberately not fixed in this release: ci.yml's Pytest step carries
no -m "not advisory" filter while the measurement comment above it does, so
advisory tests run as blocking; test_runners plan's advice still names the
filter that step does not use; the 147 historically polluted ledger rows remain
on disk, neutralised by the exclusions file rather than purged; and
verification_plan reads GitHub Actions only, degrading elsewhere to the
example commands that were the status quo.
Removed
- BREAKING for a harness not re-rendered since 0.53.0:
lens_coverage checkno
longer accepts--diff-files/--rev. ADR-004 of PLAN-probe-envelope-contract
set 0.55.0 as the sunset when it absorbed them, and this is that release — the
expiry is enforced by the suite rather than by anyone remembering, so cutting this
version is what removed them. The flags were dead on arrival: the repo-access probe
they fed was deleted, and they were kept only so an older renderedreview.mdwould
not hit anargparseexit 2 at/hm:reviewStep 3 and lose the review. Rendered
commands stopped passing them when the probe went, so a harness re-rendered at any
point since is unaffected. One that has not been re-rendered since 0.53.0 will now
see that exit 2 — run/harness-maker:make --update.
Added
hm verification_plan {show,commands}— the gate commands are now DERIVED from the
project's CI, not guessed./hm:verifyand/hm:wrapupused to ship EXAMPLE commands
(mypy --strict src/,ruff check src/ tests/), which are a guess about someone else's
repository; the dangerous direction of that guess is NARROWING, because a local pass that
checks a subset produces exactly the same green output as one that checks everything. This
harness's own stages checkedsrc/while its CI checkedsrc tests, so every type error
in test code was structurally unreachable locally — two consecutive commits shipped a red
CI through the gap. The reader keepspush/pull_requestworkflows, reports one blocking
command per gate kind, carriescontinue-on-errorthrough so an advisory CI step never
becomes a blocking local gate, lists both the commands it did not select and the ones it
could not classify, and degrades EXPLICITLY with a reason so an empty plan cannot read as
"this project has no gates". GitHub Actions only; anything else falls back to the previous
examples, which is the status quo rather than a regression.
Fixed
-
The Production
find-unboundgate can now fail. It never had.--collect-only
prints node ids at exactly verbosity -1, and_pytest_collect_nodeidspassed its own
-qon top of this repo'saddopts = "-q"— landing on -2, where pytest prints
per-file counts (tests/e2e/foo.py: 2) instead. Nothing in that form contains"::",
so the parse always yielded nothing, every AC looked un-collectable, andfind-unbound
reported OK over genuinely unbound work. The helper now resetsaddoptsto pin the
verbosity it parses, and treats rc 0 with zero parsed node ids as could-not-adjudicate
rather than as an empty suite — pytest signals a real empty suite with rc 5, so rc 0
with unreadable stdout is our own read failure. That second half is independent of the
first and holds for any future output-format change. Every existing collect test built
a sandbox with nopyproject.toml, so none of them could reproduce the composition
that killed the gate; the new one builds a repo that carries it, and asserts as a
precondition that the naive parse still finds nothing there. -
CLAUDE.md's second-opinion loss-rate guidance now names the shipped reader
(hm verifier_discrimination report+.claude/observability/.ledger-exclusions.json)
instead of prescribing a hand formula. A hand calculation over the raw ledger reported
codex at 61.3% where the reader reports 2.15% — a 30x error, because 147 of 375 rows were
pytest fixture writes the exclusions file already names. A differential test runs the
documented command as a subprocess over a fixture ledger and compares its per-model
loss_rateto the shipped reader's own output. -
worktree._cli_span_endnow copiesgit_branch/task_slugfrom the span's own start
when the caller has a session id. Every one of the 109endrows in the live ledger had
nulled both fields, so a slug-keyed join overstage-spans.jsonlalways returned zero spans. -
wrapup_receipt._confinedaccepts an absolute path iff it resolves inside the repo root,
instead of rejecting every absolute path outright (it was firing on truthful delegate
receipts reporting documents written from inside a.worktrees/<slug>/checkout).
WrapupReceiptgained two fields observed in production delegate receipts, and the
stage-delegate template now states the exact key set and path form expected in return. -
tests/unit/test_run_classify.py's fixture no longer hardcodes an absolute date. The old
fixture pinned timestamps to 2026-07-26 while_cmd_boundariesfalls back to a 30-day
window, so on 2026-08-26 every turn fell outside the window and all seven tests failed —
a time-bomb that was green on the day it was written and silently failed a month later.
Changed
CLAUDE.mdshrunk from 624 to 392 lines (Production ceiling is 500) by relocating two
procedural blocks verbatim intodocs/reference/implementation-patterns.mdand
docs/reference/pre-change-checklist.md, with a resolving pointer left in their place.
Nothing was compressed or reworded.
Boundary tests (advisory)
Layer 1 of PLAN-test-fidelity-gap. Pure parser checks; advisory only.
Status: ✅ PASS
============================= test session starts ==============================
platform linux -- Python 3.12.14, pytest-9.0.3, pluggy-1.6.0
rootdir: /home/runner/work/harness-maker/harness-maker
configfile: pyproject.toml
plugins: anyio-4.13.0, asyncio-1.3.0, hypothesis-6.155.7, respx-0.23.1, xdist-3.8.0
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 50 items
tests/integration/test_boundary_codex_toml.py ............ [ 24%]
tests/integration/test_boundary_cursor_mdc.py ....... [ 38%]
tests/integration/test_boundary_harness_yaml.py ........ [ 54%]
tests/integration/test_boundary_hooks_json.py ......... [ 72%]
tests/integration/test_boundary_meta.py ....... [ 86%]
tests/integration/test_boundary_settings_json.py ....... [100%]
============================== 50 passed in 5.52s ==============================