The way of working that the harness enforces. A team can adopt it in a day, because every
practice ends in a command that either passes or reports what it measured. The commands below
use the harness CLI (python -m hpp) and, where a module is involved, the scripts that module
installs. Module paths are shown relative to the emitted distribution.
01
1. Open a unit of work
Work starts as a spec, not as a conversation. A spec is a list of units, each with an id,
its depends_on, at least one acceptance criterion written as something a command can decide,
and a tier that states how much risk it carries. The harness compiles the spec into a WorkGraph
and derives waves from the dependencies. Units in the same wave may run in parallel; a unit whose
dependencies have not closed does not start.
Before touching files in a shared repository, the session claims a lane: an id, the paths it will own, and a heartbeat it will keep updating. Two live lanes may not overlap on exclusive territory. A lane that stops heartbeating loses its claim by time, not by someone deleting a lock.
Then the first event is recorded, and the loop leaves planned.
python -m hpp work plan SPEC.json
python -m hpp work waves SPEC.json
python multi-session/lane-kit-1.8.2/scripts/lane_board.py claim --help
python -m hpp event append --type work_started --data '{"work":"ITEM-1","actor":"maker-a"}'
02
2. Declare done
"Done" is a state the harness enters, not a word the agent says. It requires the acceptance
criteria to run as commands and exit 0, in the current checkout, now. The done gate takes the
criteria as arguments, runs each one, and prints DONE-GATE: DONE only when all pass. When some
criterion cannot pass yet, the correct output is a declared partial: the gate accepts
--declare-partial "<what is missing>" and reports PARCIAL-DECLARADO with that text and a
non-zero exit. Without the declaration it reports NOT-DONE. Silence is not a state; an
undeclared partial is a failure.
Once the criteria pass, the evidence is recorded in the event log with a reference to where the
output lives. The primary way is hpp evidence run --record-event (new in
2.6.0): it runs the criterion itself, measures the exit
code, hashes the files declared with --artifact, writes a record under .hpp/evidence/, and
appends evidence_recorded only when that bundle passed. It needs at least one --artifact,
because an exit code alone leaves nothing to re-derive later. On a release without it, the same
event is appended by hand with hpp event append, and it then carries only what the person wrote.
The loop moves to evidenced. It cannot move to verified without at least one such record.
python frameworks/operator-kit-1.8.4/scripts/done_gate.py "python -m pytest -q" "python -m py_compile app.py"
python frameworks/operator-kit-1.8.4/scripts/done_gate.py "python -m pytest -q" --declare-partial "external target not yet validated"
python -m hpp evidence run --id ITEM-1 --artifact artifacts/pytest.xml --record-event -- python -m pytest -q --junitxml=artifacts/pytest.xml
python -m hpp event append --type evidence_recorded --data '{"work":"ITEM-1","ref":"artifacts/pytest.txt"}'
03
3. What counts as proof
A proof is reproducible by someone who was not there. It carries the command, the output or exit code, the version or commit it ran against, the scope it covered, and the time it ran. Any claim that lacks one of the five is a report of a belief.
Two further rules separate proof from a green light. A number is published with the instrument that produced it, so that the next person can re-derive it instead of trusting it. And an instrument is trusted only after a control: pointed at a case known to be good and a case known to be bad, it must answer differently. A checker that would say "zero problems" about a broken tree cannot be used to say "zero problems" about a good one.
The same control applies to the instruments an agent leans on (all new in 2.6.0). A retriever is
trusted only after hpp retrieval eval has measured it on labelled queries, and a decider only
after hpp decide eval has, each with instrument failures counted apart. A report written from
sources marks each claim with the id of its source, and hpp cite check proves that every marker
resolves to a source the report was given and flags a number with no marker; it does not prove
that the source says what the sentence says.
When a checker approves, the approval is bound to bytes. The attestation records the spec hash,
the base commit and a snapshot of every tracked and untracked file. If the checkout changes
afterwards, the approval is blocked on verification and must be given again on the new bytes.
The evidence therefore comes first and the attestation last. In your own project, list .hpp/ in
.gitignore: the event log and the evidence records are per-checkout state, and the snapshot
covers every untracked file that is not ignored, so without that line an event or a record
written after attest create blocks attest verify.
python -m hpp cite check --text examples/citations/answer.md --context examples/citations/context.json
python -m hpp retrieval eval examples/retrieval/suite.json \
--retriever-command '["python", "examples/retrieval/keyword_retriever.py"]'
python -m hpp decide eval examples/typed-decisions/gotcha-family-suite.json \
--decider-command '["python", "examples/typed-decisions/baseline_decider.py"]'
python -m hpp attest create --repo . --spec SPEC.md \
--maker maker-a --checker checker-b --session review:001 \
--verdict approved --output .hpp/attestation.json
python -m hpp attest verify .hpp/attestation.json --repo .
04
4. Review without a pen
The checker is a different actor from the maker, preferably on a different provider, and holds no
file-editing tools: the host enforces the absence of Write and Edit. Bash remains available,
so read-only is a promise, verified by comparing the working tree before and after the review —
git status --porcelain captured on both sides must match, and a checker that changed the tree
invalidates its own findings. Without a pen, the checker cannot "fix it and move on"; it has to
point, with severity, file and line, and the maker has to answer. That friction is the mechanism:
it is what makes a process defect visible instead of silently patched.
The checker's report keeps the same criteria across rounds, names findings by a stable code rather than an adjective, and never asks the maker something it could verify itself by running a command. When no checker from a different provider is available, the review is recorded as not done, not as done by the maker.
The checker's pass is an event. The loop moves from evidenced to checked.
python multi-session/lane-kit-1.8.2/scripts/checker_router.py --maker claude --require
python -m hpp event append --type check_passed --data '{"work":"ITEM-1","checker":"checker-b"}'
05
5. When the budget runs out
Every loop declares its budget before the first iteration: iterations, time, and whatever the host meters. Stopping on budget is one of three legitimate ends, next to "the criterion passed" and "the circuit breaker tripped". It is not a failure and it is not a reason to try one more time.
The loop driver in the operator module reads a charter with the criteria and a maximum number of iterations. A completion tag in the model's output does not release the loop; the done gate is re-run by the hook, outside the model's reach, and only its exit code counts. When the budget is reached, the driver stops, reports where it stopped and what remains, and waits for a person to extend the budget or close the work. Extending the budget is a human decision, never something the loop grants itself.
python frameworks/operator-kit-1.8.4/hooks/loop_gate.py start \
--charter "<objective>" --criteria "python -m pytest -q" --max-iterations 10
python frameworks/operator-kit-1.8.4/hooks/loop_gate.py status
python frameworks/operator-kit-1.8.4/hooks/loop_gate.py cancel
06
6. Resume after an interruption
The recovery boundary is the last recorded event, not the last thing anyone remembers. After a
crash, a context reset or a new day, the next step is derived from the event log: hpp status
projects the log onto the loop and names the state; hpp resume returns the next step as JSON.
Neither consults a summary and neither asks the model to reconstruct what happened.
A restored summary or handoff is reference material. Before re-running anything it describes, the session checks whether that step already produced its effect (a file, a commit, an event). A step that already ran is not run again. When a step was accepted but its confirmation was lost, it is marked as unknown together with the command that resolves the doubt, and it is inspected before it is retried.
python -m hpp status --json
python -m hpp resume
07
7. Every gate is born with a test that forces it to fail
A gate that has only been seen passing cannot be distinguished from a gate that is missing. So a gate enters the codebase with two proofs. The first is a test that feeds it the case it exists to stop and asserts that it stops; if the gate were removed, this test would fail. The second is the call site: evidence that the gate is actually invoked on the live path, not merely defined.
The same rule applies to bug fixes. The test is written first and run to see it fail for the right reason; then the fix; then the test again. A test written after the fix proves that the current code works, not that anything was fixed. When both sides of a fix produce the same result, the proof does not discriminate, and the usual cause is an earlier guard stopping the path before it reaches the defect.
The benchmark is this rule applied to the harness itself: ten controls, each with a positive and
a negative case, run k times, with pass^k = 1.00 required.
python -m pytest tests/test_policy.py -q
python -m hpp eval run examples/reliable-coding/benchmark-suite.json -k 3 --gate both
python -m hpp benchmark -k 3 --json
08
8. Plan before apply
Anything that writes shows its plan first and writes on a second, explicit invocation. The plan
lists what exists in the target and will be preserved, what would be written, and what stays a
human action. Neither hpp init nor the module installer blocks on a terminal prompt; the "yes"
is the re-invocation with --apply, which works the same for a person and for an agent acting
on their behalf.
Settings, hooks and AGENTS.md are never written by the harness. The wire block is printed for a
person to paste, and the doctor is run afterwards to confirm the result.
python -m hpp init --target ../your-repo
python -m hpp init --target ../your-repo --apply
python installers/kit-forge-1.5.4/kit_doctor.py install --kit <module-dir> --host codex --target ../your-repo
python installers/kit-forge-1.5.4/kit_doctor.py install --kit <module-dir> --host codex --target ../your-repo --apply
python -m hpp doctor
09
9. A human gate where the action is irreversible
The command policy classifies what an agent is about to run. Recursive deletion in any flag
order, force pushes, pushes to main or master, piping a download into a shell and destructive
SQL are BLOCK. Any push and any outbound transfer are MANUAL: the action may be right, and a
person confirms it. In enforce mode the classifier returns exit 2 and 1 respectively; in
audit mode it reports the verdict and exits 0, so a team can measure false positives before
switching.
In the loop, the human gate is an event. checked becomes approved only through
human_approved, and the final verified requires that step to have happened.
python -m hpp policy check --mode audit --command "git push origin feature"
python -m hpp policy check --mode enforce --command "rm -rf build"
python -m hpp event append --type human_approved --data '{"work":"ITEM-1","by":"operator"}'
python -m hpp event append --type verified --data '{"work":"ITEM-1"}'
10
10. Let the graph decide the order
Write a spec small enough to compile
A spec is a JSON object with a work list. Each unit has an id, a depends_on list (empty
when it depends on nothing), at least one acceptance criterion written as something a command
can decide, and a tier. A unit is small enough when one person can say, in one command, what
closes it; when a unit needs a paragraph, it is two units. The compiler does not measure size; it
measures shape. Dependencies are written, not remembered: an edge that lives only in the
conversation does not exist for the compiler. It refuses a spec with a duplicate id, an unknown
dependency, an empty acceptance list or an invalid tier, and says which unit.
python -m hpp work plan SPEC.json
Read the waves before starting
hpp work waves prints the waves in order with the units of each. Read it as the plan of the
day: everything in wave 1 may start now; nothing in wave 2 may start until every unit of wave 1
has passed its criteria. A wave with a single unit is a serial step, and there is nothing to do
about it except finish the unit. The order inside a wave is alphabetical, not a priority: pick
any order, or all at once. Two units in the same wave may still touch the same paths; the graph
does not know the files, so lanes are declared separately, and the Lane Map is what reports an
overlap between live, exclusive territories.
python -m hpp work waves SPEC.json
python -m hpp map lane LANES.json --now "$(date +%s)"
When the graph reports a cycle
dependency cycle: a -> c -> b -> a means the spec contradicts itself: each unit in the path
asks to run after the next one, and no order satisfies all of them. The harness names the path
and exits 2; it never drops an edge to make the spec compile. The fix is in the spec, and it is
one of three: an edge was declared backwards, two units are really one, or a unit does two
things and the second half belongs to a new unit that depends on the first. Change the spec and
recompile; the compiler is the test.
python -m hpp work waves SPEC.json
Deciding that a unit cannot run in parallel
A unit runs after another only when an edge says so, and the edge is worth declaring when the
later unit reads what the earlier one produces: a schema before the code that uses it, a build
before the check that runs it, a rename before the references to the new name. When the answer
is "they touch the same files", that is a lane question, not a dependency: give the two units
disjoint territories, or make one depend on the other and accept the serial step. Once declared,
the edge appears in edges, from the dependency to the unit that waits for it, with the relation
depends-on, and the later unit moves to a later wave; if it did not move, the edge is not the
one you meant.
python -m hpp work plan SPEC.json
11
The loop, end to end
work_started ─▶ evidence_recorded ─▶ check_passed ─▶ human_approved ─▶ verified
[scope] [fresh-evidence] [read-only-checker] [human] [closure]
Each arrow is an appended event. Each gate is a condition the harness checks before the append.
hpp status tells you where you are; hpp resume tells you what comes next.