English · Português
House Party Protocol — by Rushar Labs

Harness manual · v2.11.4 · Claude Code and Codex CLI

From spec to proof.
Without a leap of trust.

An operating guide to the harness: how hpp init installs, what each map projects, how the loop advances, what the policy blocks, and what counts as proof. Every claim below carries the command that reproduces it. The outputs quoted were measured over several releases, not against one: the first on 2026-09-23 against 2.5.8, from a clone of this repository at the v2.5.8 tag and from a pip install of the same version (the hpp init blocks say which), and later ones up to 2.10.1. A measured line taken on a named version or tree names it — Measured on the 2.10.1 source tree, Measured from a checkout of main — and a line that names none sits in a block that carries the command to measure it again on your version. What is marked new in 2.6.0 had its outputs measured from a checkout of main before that release.

01 · model

Harness first; modules after.

The product is layered, and the layers are not interchangeable. The harness is the surface you operate; the protocol is what it enforces; the modules are what it installs; the distribution is how each host receives them.

Harness

python -m hpp — doctor, init, event log, attestation, maps, WorkGraph, policy, routing, context, eval and benchmark — and, new in 2.6.0, decision records, evidence bundles, a retrieval ruler and a citation check — and, new in 2.7.0, House Session deliberation records — and, new in 2.8.0, work coverage and review findings — and, new in 2.9.0, commands that act on a sealed verdict — and, new in 2.10.0, optional SSH signatures, a hash-chained event log, a project policy file and doctor --report — and, new in 2.11.0, Mermaid output for the maps and the WorkGraph. A Python package with no runtime dependencies; nothing runs in the background.

Protocol

hpp.manifest.json — protocol 2.1: four invariants, three roles (maker, checker, human-gate), five loop transitions with a named gate each, exit codes, host coverage per module, declared monitors and bundles.

Modules

Versioned directories, each installable on its own. Coverage per host is declared as native, explicit-command or unsupported; no adapter pretends otherwise.

Claude Code

Native plugin channel (/plugin marketplace add and /plugin install) and lifecycle hooks once you paste the wiring. .claude/settings.local.json — hooks and statusLine — stays a human gate.

Codex CLI

Its own plugin channel for the modules that carry skills (codex plugin marketplace add and codex plugin add; skills only), plus a verified copy per module through the module installer: skills to .agents/skills, runtime to .agents/hpp/<module>, AGENTS.md read by the host. Codex has lifecycle hooks of its own, but HPP's are written for Claude Code and are not loaded there; the same capabilities are explicit commands.

The four invariants

  • A verified outcome has recorded evidence and an explicit human gate.
  • A checker is read-only relative to the maker's workspace.
  • A loop advances only through a recorded event.
  • An approval is invalid when its bound spec, commit, or repository snapshot changes.

Source: hpp.manifest.json, key invariants.

02 · start

Diagnose before you install.

python -m hpp doctor
python -m hpp init --target ../your-repo
python -m hpp init --target ../your-repo --apply
python -m hpp benchmark -k 3

hpp doctor validates the manifest. In this repository it prints HPP doctor: ok · modules=10 · hosts=claude-code, codex · hooks=18 (permission gates=9 · llm egress=0), and because marketplace.json sits beside the manifest it also cross-checks every module path, version and plugin manifest; that result is visible only with --json, where distribution reads {"checked": true, "codex_marketplace": {"checked": true, "plugins": 7}, "modules": 10, "status": "ok"} — the seven being the modules of the Codex plugin channel. From a pip install the one-line output is the same and the field reads {"checked": false, "status": "source-contract"} — the command says what it could not verify instead of reporting a check it did not run.

hpp doctor --report (new in 2.10.0) prints the same line, then the link to the feedback form with its title filled in, the link to the support page, and an hpp.doctor-report/v1 object to paste into the form: the hpp and protocol versions, Python, the platform, the module count, the hosts, the hook counts by capability and the distribution status — no path from your machine. It makes no network call; nothing is sent until you open the link yourself. With --json it prints one object with feedback_url, report and support_url. Measured on the 2.10.1 source tree: the title reads [feedback] hpp 2.10.1 on Windows AMD64, exit 0.

hpp install --bundle reliable-coding --host codex --target ../your-repo prints a receipt with "mode": "plan-only" and copies nothing. The copy is done by the module installer, which is part of this repository at installers/kit-forge-1.5.4/kit_doctor.py, next to the module directories it installs from (a pip install carries neither): python installers/kit-forge-1.5.4/kit_doctor.py install --kit <module-path> --host <host> --target ../your-repo prints a plan, and the same command with --apply performs it. hpp init prints these lines for you in its wire block.

Requirements: Python 3.10 or newer (pyproject.toml), git on PATH for attestation and for the commit count in detect; no third-party package.

03 · init

Six fixed stages. A plan before any write.

hpp init fulfils the six stages of the installation contract. Without --apply it prints the plan and writes nothing; with --apply it writes exactly one file, .hpp/profile.json, inside the target. Host wiring is never written — it is a block you paste.

StageWhat it measuresWhat can stop it
detectClassifies the target as greenfield, in-progress or re-run from .claude/settings*.json (hooks or statusLine already set), AGENTS.md, .agents/, .hpp/events.jsonl, a previous .hpp/profile.json and the git commit count. Lists what already exists and is kept.A corrupt event log or profile is reported as a warning with the file to inspect.
prereqsPython at or above 3.10; the manifest contract; the distribution when marketplace.json sits beside the manifest; git on PATH.Python below the floor or a manifest/marketplace divergence halts the run (exit 2). A missing git is a warning with the install hint for your platform.
profileFour answers — host, bundle, policy mode and an optional decision advisor (new in 2.6.0; default off, never counted as a pending default) — taken from flags, a --profile JSON file, the prompt (only on a TTY) or the default, and the source of each is recorded. Plan reports would-write; --apply writes; the same answers again are a no-op.A recorded profile with different answers is a conflict: nothing is overwritten, the differing keys are named.
configureThe module plan for the chosen host, with native or explicit-command per module, and a CHECKSUMS.txt verification for every module directory present in the tree.A module unsupported on the host, or a checksum mismatch, halts the run (exit 2).
wire-suggestThe block to paste for the host. For Claude Code: the plugin channel lines, then module-installer lines for its explicit-command modules. For Codex CLI: the Codex plugin channel lines first (codex plugin marketplace add and one codex plugin add per module that carries skills; skills only), then one module-installer line per module, the verified copy that installs runtime, scripts and templates. Then the policy command as configured. Before the block it prints a HOOK CAPABILITIES table: each hook the chosen modules declare, with its events, its exit policy (observe, warn or block) and its capability groups. Reports 0 files written and the number of hooks declaring capabilities.Nothing; it never writes.
smokeFour controls: the policy classifier (rm -rf must be BLOCK, pytest must be ALLOW), a non-empty capability graph, the event log projecting onto the loop, and the benchmark at k=1.A failed control sets exit 1 and the plan says which one; --no-benchmark reports that control as not verified instead of skipping it silently.

Readiness is counted, not estimated

Eleven items, each verified, not verified or failed, each with the command that reproduces it. What was not measured appears as not verified — never as zero and never as one hundred. This is the boot sequence and the readiness line measured on an empty target from a clone of this repository, which carries the module directories, their CHECKSUMS.txt and marketplace.json:

> detecting host...           ✓ greenfield · 0 existing item(s) preserved
> checking prerequisites...   ✓ python 3.14.3 · protocol 2.1
> mounting profile...         ✓ would-write · host=claude-code · bundle=reliable-coding · policy=audit · 3 default(s)
> loading modules...          ✓ 6 modules · reliable-coding · claude-code · 6/6 checksums verified
> wiring suggestions...       ✓ 7 commands to paste · 0 files written · 17 hooks declaring capabilities
> verifying evidence...       ✓ policy · graph · events · benchmark
> protocol online.

  READINESS  every line is a check that ran; the command below it reproduces it
  ████████████████░░░░  9/11 verified · 2 not verified · 0 failed

The two not verified are the two that only a later action can prove: the profile (plan only; --apply writes it) and the host wiring (a paste you do yourself). Command: python -m hpp init --target <empty-dir> --non-interactive --no-animation; the target directory held 0 files afterwards.

From a pip install of the same version, which carries the harness, the manifest and the benchmark suite but no module directories and no marketplace.json, the same command on the same target reads:

> detecting host...           ✓ greenfield · 0 existing item(s) preserved
> checking prerequisites...   ✓ python 3.14.3 · protocol 2.1
> mounting profile...         ✓ would-write · host=claude-code · bundle=reliable-coding · policy=audit · 3 default(s)
> loading modules...          ✓ 6 modules · reliable-coding · claude-code
> wiring suggestions...       ✓ 7 commands to paste · 0 files written · 17 hooks declaring capabilities
> verifying evidence...       ✓ policy · graph · events · benchmark
> protocol online.

  READINESS  every line is a check that ran; the command below it reproduces it
  █████████████░░░░░░░  7/11 verified · 4 not verified · 0 failed

The two extra items not verified there — distribution integrity and module checksums — have nothing to be measured against in a wheel, so they are reported as not verified, never as passed.

Flags

  • --host, --bundle, --policy-mode audit|enforce — answer the three required questions; --modules a,b replaces the bundle with an explicit list.
  • --decision-advisor off|typesafe|openrouter|compatible — the optional fourth question (default off; new in 2.6.0): records a typed-decision advisor you declare and prints how to integrate it. hpp never calls it and stores no key.
  • --profile answers.json — the same answers from a file (keys host, bundle, policy_mode, modules and, new in 2.6.0, decision_advisor; an unknown key is a usage error).
  • --yes, --non-interactive, --json, or CI set in the environment — no prompt is reached; unanswered questions take their defaults and the report says so.
  • --no-animation — plain output; NO_COLOR is honoured; without a TTY the output is complete and uncoloured.
  • --marketplace — the slug used in the Claude Code wire block, for forks.

Exit codes of init: 0 ok · 1 a warning or a non-blocking failure (git missing, a smoke control failed) · 2 halted at a blocking stage · 3 the invocation itself was wrong (target is not a directory, bad --profile file). A halted run never prints the closing brand line.

04 · maps

The state has more than one view.

Maps have no state of their own. Each is a projection of one or two inputs, sorted so that the same input yields the same output byte for byte. The Monitor Map does not probe anything and the Lane Map does not know your sessions: you supply last_signal and heartbeats, and --now is explicit so that no projection depends on the ambient clock.

MapInputQuestion it answersCommand
Capabilitymanifestwhich module provides which capability, on which host, in which bundlegraph --view capability
Operationalmanifest loopwhich event moves which state through which gategraph --view operational
Agentroles and modules; optional eventswho may make, check and approve; what has happened, in ordermap agent · graph --view agent
Evidencefixedhow a criterion, a record and a verdict relategraph --view evidence
Codemanifest componentswhich surfaces each module declares (not an AST)graph --view code
Lanelanes JSON, --now, thresholdswho owns what, who is alive, where live claims overlapmap lane
Contextcontext JSON, --budgetwhat entered the compiled context, what was omitted, with hashesmap context · context compile
Monitormonitors JSON, --now, --skew-tolerancewhat is observed, how fresh, and which gate consumes itmap monitor
Workspec JSONwhich units can run together and in which orderwork plan · work waves · work coverage

Each of these prints JSON by default. On graph, the four map views, work plan and work waves, --format mermaid prints a Mermaid flowchart of the same nodes and edges instead; work coverage and context compile stay JSON. In a flowchart the nodes are numbered in id order and the edges sorted, and a ", |, < or > in a label is written as #quot;, #124;, #lt; or #gt;. The bytes do not depend on the system: these commands write UTF-8 with \n line endings, on Windows too, so the same input gives the same hash everywhere.

Monitor Map: four states, and a clock that can lie

StateRule
healthynow − last_signal ≤ freshness
stalethe signal is older than the declared freshness
skewlast_signal > now + skew_tolerance — a timestamp in the future is not evidence of freshness; default tolerance 5 s, set with --skew-tolerance
unknownno last_signal supplied
python -m hpp map monitor examples/reliable-coding/monitors.json --now 1000
python -m hpp map monitor examples/reliable-coding/monitors.json --now 1000 --format mermaid

The monitor map has no graph of its own; in Mermaid each monitor is a node labelled <id> · <status>, with an observes edge to its target and a gates edge to its consumer gate.

Measured: service-health (signal 950, freshness 120) → healthy; data-freshness (signal 700, freshness 120) → stale. With a synthetic file, a signal at 1010 against --now 1000 projected as skew, and as healthy once --skew-tolerance 20 was passed; a monitor without last_signal projected as unknown.

Lane Map: liveness from heartbeats

A lane is alive when its heartbeat is at most --suspect-after seconds old (default 300), suspect up to --dead-after (default 900), and dead beyond that or when declared closed. A lane without a heartbeat is unknown. A dead lane never produces a collision; two live exclusive lanes whose territories overlap do. Command: python -m hpp map lane examples/reliable-coding/lanes.json --now 1000. In the JSON a collision is a record of the collisions field; with --format mermaid it is drawn as one collides:<paths> edge between the two lanes.

Competitions: choosing one of N is pass@N

New in 2.6.0

The lane-kit board (lane_board.py) runs a best-of-N: N lanes each build their own item for one task, compete declares them candidates, and select records one winner. The reviewer who selects must be on another lane and of another model family than every candidate's builders, and every candidate must be CHECKPOINT-READY with evidence, or VERIFIED. The losers become NOT-SELECTED, a terminal state; no candidate reaches MERGED before its task has a winner; select --checker-unavailable records DEFERRED, never a winner. Choosing one of N is pass@N, not reliability: the winner still needs its own VERIFIED before MERGED, and pass^k before anyone calls it reliable.

python multi-session/lane-kit-1.8.2/scripts/lane_board.py compete --task TASK-1 --items ITEM-A,ITEM-B --lane lane-a --model claude-opus-5-5
python multi-session/lane-kit-1.8.2/scripts/lane_board.py select --task TASK-1 --winner ITEM-B --lane lane-r --model gpt-5.6-sol --reason "same tests, half the diff"

Measured on 2026-09-27 from the source of lane-kit 1.6.0, with current model ids, on a scratch board where ITEM-A (lane lane-a, claude-opus-5-5) and ITEM-B (lane lane-b, claude-sonnet-5) were each claimed, built and CHECKPOINT-READY with evidence: compete → exit 0; select by claude-haiku-4-5-20251001 → maker≠checker violated: … SAME model family (claude), exit 1; select by gpt-5.6-sol → winner ITEM-B, not_selected ITEM-A, exit 0. Afterwards ITEM-A could not move (NOT-SELECTED -> UNDER-REVIEW refused) and ITEM-B could not go from CHECKPOINT-READY to MERGED, exit 1 each. The board exits 0 ok · 1 refused · 2 invalid usage.

Context compile: whole blocks under a budget

hpp context compile INPUTS --budget N reads a JSON array of blocks — source (unique), priority (an integer) and content — and fills a character budget in priority order. A block enters whole or not at all: one that does not fit is listed as omitted, never sliced, and the next block is still tried. The separator between two blocks (two newlines) is charged to the budget. Every block, included or omitted, carries its chars and its sha256; the result carries used, remaining and the compiled text. Secret-like content — a key, token, secret or password assignment, a PEM header, an sk- token — is refused before anything is compiled, and the refusal names the source.

python -m hpp context compile examples/reliable-coding/context.json --budget 100

Measured: spec and acceptance included, background omitted, used 86, remaining 14. A block whose content was an api_key= assignment → hpp: secret-like material refused from source: env, exit 2.

05 · lanes

Lanes coordinate through files, never through a live session.

Parallel sessions — Claude Code or Codex CLI — each work as a lane: its own worktree, a declared territory and a heartbeat. The lane-kit gives them four shared files and no messaging channel of its own. A file survives the session that wrote it, and both hosts can read it.

PieceFileWhat it guaranteesWritten by
Registry.claude/lanes/registry.jsonwho is alive, with role, model and worktree; a dead lane is evicted at the next register, and its uncommitted work is saved as a rescue patch firstlane_register.py on SessionStart, --heartbeat on PostToolUse
Board.claude/lanes/board.jsonl (append-only)who claimed which item, every verdict, every competitionlane_board.py
Mailbox.claude/lanes/mailbox/*.mdcontext the board cannot carry — a scope change in the middle of the workany lane; read by the lane it names
Effects.claude/lanes/effects.jsonwhether a verdict was only decided, or also delivered to the lane that has to act on itlane_effects.py, from the board

The mailbox: write once, read, archive

A message is a Markdown file in .claude/lanes/mailbox/. Its routing line names the target lane — ## To: <lane_id> (the Portuguese form ## Para: from earlier versions is still honoured). The target lane is told about it in its session context when it registers, and when a message arrives while it is already running, at its next heartbeat — once per message, not on every tool call. It reads the file and moves it to mailbox/_read/; a message is never edited in place. templates/REORIENT-MAILBOX.template.md carries the fields a reorientation needs: from, when, affected items, what changed, why, what the target must do, evidence.

cp multi-session/lane-kit-1.8.2/templates/REORIENT-MAILBOX.template.md .claude/lanes/mailbox/2026-09-27-scope.md
# fill the placeholders, keep "## To: exec-b" exactly as written
# exec-b sees: 1 unread mailbox message(s): .claude/lanes/mailbox/2026-09-27-scope.md — read them and archive in mailbox/_read/

Decided is not delivered

When the board records a verdict — VERIFIED, NEEDS-FIX, DEFERRED, NOT-SELECTED or WITHDRAWN — lane_effects.py reserves its delivery under a reservation_id derived from item, decision, target, effect and round: a retry reuses the reservation, a new round gets its own. Then the board delivers it itself: it writes the verdict into the mailbox of the lane that built the item — routed with ## To:, with the reason, the evidence and the round — and marks the reservation delivered, with the message's path as evidence. A Codex CLI lane is also rung with codex queue; Claude Code has no command that reaches a running session, so a Claude Code lane hears of the message at its next heartbeat. Delivery is on by default. With mailbox.notify_on_verdict: false in lanes.yaml the board writes nothing: the verdict stays pending — as it also does when the mailbox cannot be written — and lane_board.py render lists it under UNDELIVERED until whoever tells the lane closes it with lane_effects.py deliver.

python multi-session/lane-kit-1.8.2/scripts/lane_effects.py pending
python multi-session/lane-kit-1.8.2/scripts/lane_effects.py deliver <reservation_id> --evidence .claude/lanes/mailbox/_read/2026-09-27-scope.md

Two guards around shared files

lane_territory_guard.py warns when a lane edits inside another live lane's exclusive territory, naming the owner; it never blocks. lane_git_guard.py watches the git index the lanes share: while another lane is alive, it catches the commands that sweep it — add -A/--all/., commit -a, a commit without an explicit pathspec, stash, reset --hard, checkout ., commit --amend. It runs in warn mode by default and moves to block only after zero false positives in real use, set in lanes.yaml; a lane working alone is never stopped.

Measured on the 2.10.0 tree: evals/collision-2lanes.sh → 10/10 valid lines, 0 corruption; evals/collision-git-guard.sh and evals/collision-territory-guard.sh → 18/18 checks over 3 rounds each (pass^k = 1.00); the register self-test shows a message addressed to the lane appearing, and gone once archived.

Operator hand-offs: route a fix, open a review, approve a red item

Three board commands belong to the operator, not to a lane — contributed by @kleinelizeu together with the Lane Dashboard. release-fix moves a NEEDS-FIX item to FIX-QUEUED for a named executor that is registered, alive and allowed to build the item; only that lane may take it back to BUILDING, and a fix whose lane stopped beating can be routed again, with the reroute recorded. start-review moves a checkpoint, or a DEFERRED item, to UNDER-REVIEW for a named live reviewer that built no attempt at it. Both write a kickoff to the target's mailbox, routed with ## To:. approve records the human gate of a red item as APPROVED — not MERGED: the merge happens afterwards, in git, and is recorded afterwards.

python multi-session/lane-kit-1.8.2/scripts/lane_board.py release-fix ITEM-1 --target-lane exec-b
python multi-session/lane-kit-1.8.2/scripts/lane_board.py start-review ITEM-1 --target-lane rev-x
python multi-session/lane-kit-1.8.2/scripts/lane_board.py approve ITEM-1

Two rules of the verdict were tightened with them. The model family of a checker is compared with every lane that built the item, not only the one that claimed it, and OpenAI aliases such as codex-* count as gpt. The tag belongs to the item: once red, every later event is red, whatever --tag a caller passes.

The Lane Dashboard: the board as a page, every action a terminal command

scripts/lane_dashboard.py serves the board as a page on 127.0.0.1: its columns, the live lanes, the competitions, the mailbox of each lane (unread, announced, read), the deliveries with their doorbell outcome, the sessions it started, the backlog with its waves and — asked of git, not of the board — whether each reviewed item is integrated. It is optional. Every action on it is a terminal command that does exactly the same thing, and the page only calls that command.

The Lane Dashboard recorded from a live run: the metrics and a backlog of three example specs in two dependency waves; a terminal command moves EXAMPLE-1 from Building to Ready for review and the page follows by itself; the Approve merge dialog of the red item EXAMPLE-2 names lane_board.py approve EXAMPLE-2 and is cancelled; the language switch turns the page to Portuguese

Recorded from a live run by scripts/record_lane_dashboard.py (its command, the seed's commands and the story as measured are in assets/ui/lane-dashboard.gif.txt): a throwaway project built by scripts/seed_lane_dashboard_demo.py through lane-kit's own writers, served by lane_dashboard.py serve --no-orca --port 0. The terminal strip is the recorder's, holding the one command it ran and that command's stdout; the page moved the card 0.24 s after the command exited in this recording, because the server re-reads the board every 0.75 s and tells the page. The dialog was cancelled, and the page attempted 0 POSTs.

Page actionTerminal command
New spec · Start a wavelane_dashboard.py spec add · lane_dashboard.py wave start
Route the fix · Start review — to a live lanelane_board.py release-fix · lane_board.py start-review
Route the fix · Start review — with a new sessionlane_dashboard.py fix launch · lane_dashboard.py review launch
Approve merge · Record mergedlane_board.py approve · lane_dashboard.py merged
Ask for a brief · Integration reportlane_dashboard.py brief request · lane_dashboard.py integration report
Start the session again · Stop a sessionlane_dashboard.py session relaunch · lane_dashboard.py session cancel
python multi-session/lane-kit-1.8.2/scripts/lane_dashboard.py --project-dir . --open
python multi-session/lane-kit-1.8.2/scripts/lane_dashboard.py merged ITEM-1 --branch feat/item-1 --project-dir .

The same line starts it on macOS, Linux and Windows (with python3 or py where that is the interpreter's name); it prints the URL, and Ctrl+C stops it. merged writes MERGED only when git shows the builder's branch integrated in the target, measured at that moment, and the measurement becomes the event's evidence. Ancestry is asked first (git merge-base --is-ancestor), because it sees every commit, merges included; only a branch that is not an ancestor falls back to patch-id equivalence (git cherry), and a merge commit of the branch that the target does not contain keeps it pending, because equivalence cannot prove a merge.

Security model. The server is the bounded exception the manifesto makes for modules. The operator starts and stops it; it binds loopback and answers only a loopback Host; every action is a POST that carries a token issued for that run, as application/json; the page refuses to be framed; a GET writes nothing and starts nothing; every write goes through lane_board.py and _lane_io.py. An agent session starts only when the operator confirms an action, with the model declared in .claude/lanes/dashboard.json: maker ≠ checker compares the family of the model a session actually runs, so a CLI that serves several vendors is refused until its model is declared.

LaunchermacOSLinuxWindows
terminalTerminal.appthe desktop's terminal ($TERMINAL, then the usual ones)a PowerShell console
itermiTerm2, the session script passed to osascript as an argument——
tmuxa window in the tmux session the dashboard runs in, or in hpp-lanes—
orcaorca, POSIX commandorca-ide, POSIX command; never the GNOME screen readerorca, --shell powershell; older Orca is refused
externalany terminal declared in dashboard.json as an argv with {script}
headlessthe agent's non-interactive mode under a supervisor: a timeout, a stop and a capped log

ORCA_CLI_COMMAND overrides the CLI on every platform. A cut worktree list is flagged on the page and as orca_truncated in snapshot. See HPP inside Orca for the shared-worktree setup, project-scoped plugins and what has not been verified in the real host.

Measured on the 2.10.0 tree: the dashboard self-test → 77 of 77 checks in 9.3 s from a fresh copy of the module (the installer gives each self-test 30 s); tests/test_lane_dashboard.py and tests/test_lane_board_release.py → 66 tests, with controls beside the refusals; 65 passed and 1 skipped on Windows, the one that runs the session script under a POSIX shell.

06 · loop and waves

The loop advances only through a recorded event.

planned --work_started--> active --evidence_recorded--> evidenced --check_passed--> checked
        [scope]                   [fresh-evidence]                 [read-only-checker]

checked --human_approved--> approved --verified--> verified
        [human]                      [closure]

The event log is .hpp/events.jsonl in the workspace, append-only and, new in 2.10.0, hash-chained: every appended event carries prev_sha256, the sha256 of the line before it as written (the first links to the hash of nothing). An event that does not fit the current state is refused before anything is written, and verified requires at least one recorded evidence item. hpp status projects the log onto the machine above, names the next step and ends with the state of the chain; hpp resume returns the same answer as JSON. Neither asks a model to remember anything.

python -m hpp event append --type work_started
python -m hpp status
python -m hpp resume
python -m hpp event verify
ChainWhat it meansevent verify
intactevery line links to the one before itexit 0
legacysome lines were written before the chain existed and carry no link; they read as before, and status prints chain=legacy (N unchained)exit 0
brokena line's prev_sha256 does not match the line before it, or a line does not parse; the report names first_divergent, and status refuses to project the log (exit 2, naming the same step)exit 2
emptyno log yet, or no event in itexit 0

The chain alone cannot see an edit of its last line: the tail is anchored only by a copy of head_sha256 kept outside that line — the next append, which carries it as its prev_sha256, or a copy you keep elsewhere. An attestation does not record it; its snapshot binds the log only when .hpp/events.jsonl is not ignored by git. status --json carries the chain summary: status, chained, legacy and head_sha256.

Measured in an empty directory on the 2.10.1 source tree: hpp status → HPP status: planned · events=0 · next=start work · chain=empty. event append --type verified as the first event → hpp: invalid transition at event 1: planned --verified--> ?, exit 2, no file created. Then event append --type work_started → state active, hpp status prints HPP status: active · events=1 · next=record fresh evidence · chain=intact. After a second event, with the first line rewritten: event verify → broken, first_divergent event:2, exit 2, and hpp status → hpp: corrupt event log at line 2: hash chain broken at event:2 (prev_sha256 does not match line 1 as written), exit 2. A log whose first line carries no link → chain=legacy (1 unchained), and event verify → legacy, exit 0.

Parallelism follows the WorkGraph

A spec lists work units, each with dependencies, non-empty acceptance criteria and a tier (economy, balanced, frontier). The compiler rejects a dependency cycle as an error — never as an empty wave — and emits topological waves: each unit sits in the first wave after all of its dependencies. It launches nothing; the operator honours the barrier between waves.

python -m hpp work plan examples/reliable-coding/workgraph.json
python -m hpp work waves examples/reliable-coding/workgraph.json
python -m hpp work waves examples/reliable-coding/workgraph.json --format mermaid

Measured on the example: wave 1 spec · wave 2 build, docs · wave 3 verify; tier counts economy 2 · balanced 1 · frontier 1. With --format mermaid (the same diagram from work plan and work waves) the three waves are three subgraph blocks and each node reads id · tier, e.g. verify · frontier.

A criterion cited is not a criterion run

New in 2.8.0: hpp work coverage SPEC --tests PATH... [--junit REPORT...]. A test cites an acceptance criterion with [spec: capability/scenario] in its docstring; without a report the command answers whether every criterion is cited (hpp.spec-coverage/v1). With the JUnit XML report of the run it answers whether a citing test ran: a criterion is executed (a citing test passed and none failed), failed, cited_not_run (skipped, absent from the report, or cited by a module or class docstring), an orphan, or unknown (hpp.spec-execution/v1). Exit 0 only when every criterion is covered or, with a report, executed; 1 otherwise; 2 on a refused report — one declaring a DOCTYPE or an entity, or not UTF-8 — or on a test path with no Python source. matched_testcases says how many report cases were joined to a citing test. A report is tied to the files it measured by time: a citing test file modified after the oldest report is listed in stale_sources, and the answer is not complete (exit 1) until the run is repeated, so a report older than an edit cannot keep a criterion executed.

python -m hpp work coverage examples/cited-and-run/workgraph.json --tests examples/cited-and-run/check_prices.py
python -m pytest examples/cited-and-run/check_prices.py -q -p no:cacheprovider --junitxml out/cited-and-run.xml
python -m hpp work coverage examples/cited-and-run/workgraph.json --tests examples/cited-and-run/check_prices.py --junit out/cited-and-run.xml

Measured on the example: citation alone → three criteria covered, exit 0; pytest → 2 passed, 1 skipped; with the report → bulk-discount in cited_not_run (skipped), two executed, matched_testcases 3, exit 1.

Review lenses answer in one shape

New in 2.8.0: the operator module ships four review lenses with no file-editing tool (read-only is proved when they are seated through house_session.py) — lens-verification-gap, lens-partial-set, lens-deletion, lens-stale-evidence — each looking at a change for one kind of defect. Every lens answers with an hpp.findings/v1 document: findings with a stable code, a severity, file and line, the claim and its evidence, and inspected, what the lens looked at. hpp findings check FILE... [--subject CHANGE] refuses a document that names another change than CHANGE, a document with a key the contract does not define, an empty inspected or the same finding twice (exit 2), and derives the verdict: exit 0 when every review passed, 1 when a lens found something. It reads the answer, never the change.

python -m hpp findings check examples/review-lenses/deletion-clean.json
python -m hpp findings check examples/review-lenses/deletion-clean.json examples/review-lenses/partial-set.json

Measured on the example: the clean review → pass, exit 0; with the second → warn, one CALLER_NOT_UPDATED at checkout.py:18, exit 1.

Routing: a tier first, then a provider you declared

hpp route --request F --providers F [--policy economy|balanced|frontier], default balanced. The request declares stage, risk and complexity (low, medium or high) and context, a non-negative integer; each provider declares id, tiers, stages and max_context. High risk, high complexity or a context above 16000 requires frontier under any policy; medium, or a context above 8000, requires at least balanced; otherwise the policy's own tier applies. A provider is eligible when it declares the stage and its max_context covers the context. The route returns the tier and one of the provider ids you declared, ties broken by id. When no eligible provider offers the requested tier, it falls back upward only, to the nearest tier above, and says so in fallback; when nothing at or above the tier is eligible, it refuses (exit 2). It never names a model, reads a price or ranks vendors.

python -m hpp route --request examples/reliable-coding/route-request.json --providers examples/reliable-coding/providers.json

Measured: the output carries schema (hpp.route/v1), policy, requested_tier, selection (provider, tier), fallback, eligible_providers and rationale. On the example (low risk, low complexity, context 4000), balanced selects local-frontier at balanced, economy selects local-economy and frontier selects local-frontier at frontier. Against a synthetic list offering only balanced, an economy request selected it with fallback reason upgraded-above-requested-tier; a high-risk request against an economy-only list → hpp: no provider satisfies the frontier risk floor, exit 2.

07 · policy

Advisory does not dress up as a block.

The classifier returns one of three verdicts and never executes the command. The mode decides what the verdict costs: in audit every verdict exits 0 and is only recorded; in enforce the verdict becomes the exit code. A project policy file that is refused exits 2 in either mode, with no verdict printed (below).

ALLOW · exit 0

No rule matched

The command runs. The rule set is small and explicit; it does not claim to catch every destructive form.

MANUAL · exit 1 in enforce

Human gate

Any git push and any curl or wget to a URL — and, new in 2.6.0, running the example typed-decision adapter (examples/typed-decisions/decide.py; the 2.5.8 package returns ALLOW for it): external publication or transfer needs a person.

BLOCK · exit 2 in enforce

Refused

Recursive delete in any flag order (-rf, -fr, -r -f, --recursive --force, rmdir /s), force push, push to main or master, curl | sh, DROP/TRUNCATE.

python -m hpp policy check --mode audit --command "git push origin main"
python -m hpp policy check --mode enforce --command "git push origin main"
CommandVerdict · ruleauditenforce
git push origin mainBLOCK · main-pushexit 0exit 2
git push origin featureMANUAL · external-pushexit 0exit 1
rm -rf srcBLOCK · recursive-deleteexit 0exit 2
python -m pytest -qALLOWexit 0exit 0

All eight cells measured on this tree with python -m hpp policy check. rm file.txt, grep -rf patterns.txt and cp -rf a b are not blocks: the check reads the option set of each rm invocation, not a spelling.

A project policy only hardens

New in 2.10.0

A project adds to the built-in rules in .hpp/policy.json (hpp.policy/v1), which policy check reads from the current directory, or in the file named with --policy FILE. It holds rules of its own — an id, an action (BLOCK or MANUAL), a pattern (a regular expression, matched case-insensitively like the built-ins) and a reason — and, under raise, built-in MANUAL rules moved to BLOCK. Every BLOCK, built in or declared, is tried before any MANUAL, so a declared rule cannot soften a built-in block. The output names the file in policy, and every verdict says its source: built-in or policy.

{"schema": "hpp.policy/v1",
 "rules": [{"id": "no-prod-deploy", "action": "BLOCK", "pattern": "\\bdeploy\\b.*--prod\\b",
            "reason": "production deploys go through release review"}],
 "raise": {"external-push": "BLOCK"}}

A file that would exempt anything is refused as a whole: a rule with action ALLOW, a raise that keeps or lowers a class, a rule that reuses a built-in id, a key outside the contract (there is no allow list), a missing field or an invalid pattern. A refused file refuses the check — exit 2 in audit as in enforce, no verdict printed — instead of falling back to the built-ins, which would print ALLOW for the very command the project asked to block.

Measured on the 2.10.1 source tree with the file above in .hpp/: npm run deploy -- --prod → BLOCK, rule no-prod-deploy, source policy, exit 0 in audit and 2 in enforce; git push origin feature in enforce → BLOCK, rule external-push, source policy, exit 2. With a rule whose action is ALLOW → hpp: rule 'let-it-through': action must be BLOCK or MANUAL, not 'ALLOW'; a policy only hardens, exit 2 in both modes; with "raise": {"external-push": "ALLOW"} → hpp: raise: 'external-push' is MANUAL and ALLOW would lower it; a policy only hardens, exit 2 in audit; --policy naming a missing file → hpp: policy file not found: nope.json, exit 2.

08 · proof

Capability and reliability are not the same metric.

pass@k measures whether a case passed at least once in k runs. pass^k measures whether it passed every time. The capability gate requires pass@k ≥ 0.90; the regression gate requires pass^k = 1.0; --gate both requires both. A suite that passes the first and fails the second is flaky, and the report says by how much.

python -m hpp eval run examples/reliable-coding/benchmark-suite.json -k 3 --gate both
python -m hpp benchmark -k 3 --json

Measured: HPP benchmark: pass@k=1.00 · pass^k=1.00 · gate=PASS over ten controls (manifest contract, policy enforcement, WorkGraph waves, lane collision, monitor freshness, context provenance, routing floor, event/evidence gate, graph determinism, evidence attestation), three runs each. The JSON report carries the suite's sha256 and the platform it ran on. Exit 0 when the gate passes, 1 when it does not.

A decision made elsewhere is measured, never trusted by default

New in 2.6.0

The harness calls no model. A small decision taken outside it — by a rule, a person, a local model or a hosted typed-decision model — can be recorded as hpp.decision/v1: always advisory; with raise-only it may raise a declared value and never lower it; abstention and instrument-failure are outcomes that change nothing. hpp decide eval runs the decider you name as a command against labelled cases and reports coverage, selective accuracy, confident errors, abstentions and failures separately, with a coverage curve by confidence floor. Version 1 measures choice questions only.

python -m hpp decide validate decision.json
python -m hpp decide eval examples/typed-decisions/gotcha-family-suite.json --decider-command '["python", "examples/typed-decisions/baseline_decider.py"]'
  • decide eval SUITE — --decider-command takes the decider's argv as a JSON array of strings, run without a shell; omit it to replay the records stored in the suite's cases, where a case without a record counts as an instrument failure. --timeout (default 10.0) is the number of seconds per case before that case counts as an instrument failure. The gate: --min-selective-accuracy (default 0.9), --max-confident-errors (default 0; a confident error is a wrong answer at or above the suite's confidence_floor, 0.6 when the suite does not set one) and --max-failures (default 0). Exit 0 the gate passed · 1 the gate failed, or nothing was decided and there is no accuracy to measure · 2 an invalid record, suite or argv.
  • decide validate RECORD — prints valid and the effective value a consumer may act on, with its action (raised, kept, advised or none). Exit 0 valid · 2 refused, naming the first broken rule.

Measured from a checkout of main: the baseline decider on the example suite → 15 cases, 12 decided, 3 abstained, 0 instrument failures, coverage 0.8, selective accuracy 1.0, every threshold held, exit 0. The same suite without --decider-command holds no records → 15 instrument failures, no decision was made, exit 1. A record with "authority": "binding" → hpp: authority must be 'advisory': …, exit 2. The 2.5.8 package has no decide command: hpp decide there is an invalid choice, exit 2.

How it works end to end — declaring an advisor such as Jev, pinning its version, recording, validating and measuring it — is in chapter 09. The adapter and its risks are in examples/typed-decisions.

Evidence bundles: the exit code of a criterion, measured outside the model

New in 2.6.0

hpp drives no browser and calls no model. hpp evidence run runs the criterion command you declare — an end-to-end spec, a pytest suite, any script — as argv without a shell, measures its exit code, hashes every artifact file you declared, and writes .hpp/evidence/<id>-<UTC>.json (hpp.evidence/v1): the command, the base commit, the exit code, the verdict, stdout and stderr as byte counts and sha256 (never the text), the artifacts and a self-hash. The verdict is passed only when the command exited 0 and every declared pattern matched a file this run wrote (a file untouched since before the run is listed as unchanged and does not count); otherwise it is failed, missing-artifacts, timeout or could-not-start. hpp evidence verify re-derives a record from the files on disk. The record is self-hashed, and signed only with --sign-key. The self-hash makes an edited record visible; it is not a signature — anyone who can write the file can rewrite it and its hash — so a checker that must not trust the maker re-runs the command from its own lane instead of relying on verify alone, or asks for a signature and checks it with --allowed-signers (optional signature, below).

python -m hpp evidence run --id smoke-page --artifact out/report.html --artifact out/smoke.log -- python examples/evidence/smoke_page.py
python -m hpp evidence verify <record_path>
  • evidence run --id ID [--artifact GLOB]... -- COMMAND — --artifact is a glob relative to the workspace, repeatable; --timeout (default 600.0) is the number of seconds before the run counts as a timeout; --out is the record directory, relative to the workspace (default .hpp/evidence); --record-event appends evidence_recorded to the event log when, and only when, the bundle passed, and needs at least one --artifact. A secret-like argv, an artifact or --out path outside the workspace, a bad id or a non-positive timeout is refused before anything runs. Exit 0 passed · 1 not passed · 2 refused, or the event could not be appended.
  • evidence run ... --sign-key KEY --signer NAME (new in 2.10.0) — signs the record; --signer is required with --sign-key, and both are checked before the command runs.
  • evidence verify RECORD — the record_path that run printed; --allowed-signers FILE (new in 2.10.0) also demands a signature that file accepts. Exit 0 valid, an intact record of a run that passed · 1 not-evidence, an intact record of a run that did not pass · 2 blocked, the record was edited or contradicts itself, or an artifact changed or went missing, or — with --allowed-signers — the signature is missing, not accepted or over changed bytes.

Measured from a checkout of main, in a copy of the tree so that out/ and .hpp/ were written there: the demo → passed, exit 0, and verify on its record → valid, exit 0. With --break after the script, both artifacts were written and the verdict was failed, exit 1; verify on that record → not-evidence, exit 1, and on a copy edited to read passed → blocked, the record was edited after it was written, exit 2. The first record, verified again after the second run had rewritten out/ → blocked, 2 artifact(s) changed since the run, exit 2: end-to-end runners that clear their output directory at the start of each run do the same, so give artifacts that must stay verifiable a per-run directory. --record-event with no work_started in the log → exit 2, the event not appended; after event append --type work_started it moved the loop to evidenced. The 2.5.8 package has no evidence command: hpp evidence there is an invalid choice, exit 2.

New in 2.7.0: hpp evidence mutate --id ID (--mutants FILE | --generate FILE)... -- COMMAND asks whether the criterion would notice broken code. It runs the command on a copy of the workspace, clean (it must pass, or the verdict is no-control, exit 2, and no mutant runs), then once per mutant (it must fail). --generate mutates a Python file operator by operator (==↔!=, <↔<=, >↔>=, and↔or, True↔False), never inside strings or comments; --mutants reads declared {id, file, find, replace}. Exit 0 sensitive · 1 blind-spots, incomplete or no-mutants · 2 no-control or refused. Measured on examples/criterion-sensitivity: the strong check kills 3 of 3 mutants, exit 0; the weak check, just as green, lets 3 of 3 through and names them, exit 1.

See examples/evidence for the same pattern around a real end-to-end runner.

Retrieval ruler: the retriever, measured apart from the answer

New in 2.6.0

The harness runs no index and calls no model. A retriever is a command you declare: it reads {"query": …, "k": …} as JSON on stdin and prints ranked ids, best first. hpp retrieval eval scores the top k of each answer against the ids an hpp.retrieval-suite/v1 suite labels relevant — hit@k, recall@k, precision@k, MRR and nDCG@k — and counts instrument failures (could not start, timeout, non-zero exit, output that is not JSON, a duplicate id) apart: they are never scored, and the means cover measured cases only. With no measured case the metrics are null and the gate says why, never 0%. Whether a good answer can be generated from what was retrieved is a separate question, not asked here.

python -m hpp retrieval eval examples/retrieval/suite.json
python -m hpp retrieval eval examples/retrieval/suite.json --retriever-command '["python", "examples/retrieval/keyword_retriever.py"]'
  • retrieval eval SUITE — --retriever-command takes the retriever's argv as a JSON array of strings, run without a shell; omit it to replay the results stored in the suite's cases. -k overrides the suite's cut-off. --timeout (default 10.0) is the number of seconds per case before that case counts as an instrument failure. The gate: at least one measured case, --min-recall (default 0.8) for mean recall@k and --max-failures (default 0). Exit 0 the gate passed · 1 the gate failed · 2 a refused suite or argument.

Measured from a checkout of main: both commands → 7 cases measured, 0 instrument failures, mean recall@3 0.786, hit@3 0.857, MRR 0.786, nDCG@3 0.749, mean recall@k 0.786 < 0.8, exit 1 — the keyword baseline misses the paraphrased question entirely, a measured 0, and the default gate fails; with --min-recall 0.75 → every threshold held, exit 0. A retriever that exits 3 on every case → 7 instrument failures, metrics null, no case was measured (7 instrument failures); there is no recall to measure, exit 1. --retriever-command '"python"' → hpp: --retriever-command must be a JSON array of strings (argv, no shell), exit 2. The suite is synthetic and its baseline was written beside it: these numbers prove the ruler works, not that any retriever is good. The 2.5.8 package has no retrieval command: there it is an invalid choice, exit 2.

See examples/retrieval for the retriever contract and the suite format.

Citation check: every marker resolves, or the exit code says which does not

New in 2.6.0

An answer written from sources marks each claim with the id of the source it rests on — [ID:<id>] by default. hpp cite check reads the answer and the list of context items it was written from, and reports without a model: a marker whose id is not in the context (UNKNOWN_ID), a marker naming a range or a list (RANGE) and an empty marker (EMPTY_MARKER) block; more than --max-per-sentence markers in one sentence (TOO_MANY) and a sentence with a number, percentage, currency amount or date and no marker (UNCITED_CLAIM) warn. It proves that every marker resolves, not that the cited source supports the sentence: that judgement needs a reader. The sentence splitter and the number detector are heuristics, listed in the docstring of hpp/citations.py.

python -m hpp cite check --text examples/citations/answer.md --context examples/citations/context.json
  • cite check --text FILE --context FILE — --context is a JSON list of items with an id (and their text), or of bare ids; --max-per-sentence (default 4); --marker takes a regex with exactly one capture group, the id. Empty text, text that is only code, secret-like text or context and an unusable regex are refused. Exit 0 clean · 1 warn (TOO_MANY, UNCITED_CLAIM) · 2 block (UNKNOWN_ID, RANGE, EMPTY_MARKER) or refused input.

Measured from a checkout of main: the shipped answer → verdict ok, 7 sentences, 5 markers, 4 of the 5 context ids cited, exit 0. In copies of it: [ID:glossary] changed to [ID:glossary-v2] → UNKNOWN_ID, exit 2; to [ID:runbook-7,sla-2026] → RANGE, exit 2; the marker removed from the sentence with 4 engineers → UNCITED_CLAIM, exit 1. And the limit, measured: 42 minutes changed to 90 minutes, still cited to the source that says 42 → verdict ok, exit 0. The 2.5.8 package has no cite command: there it is an invalid choice, exit 2.

See examples/citations for another marker syntax and the report format.

House Session: a panel of pinned deciders, counted, stopped and sealed

New in 2.7.0

The harness calls no model: the seats and the judge answer outside it, in their own lanes. hpp deliberate checks what they said. A panel (hpp.panel/v1) does not start unless every seat names a pinned model (an alias such as -latest is refused), the participants come from at least two model families, exactly one judge sits in a lane no participant uses, and the roles fit the session_type — a review needs two examiners and a dissenting seat. Each turn (hpp.turn/v1) lists the turns its seat had read: a round-1 turn that read any is refused, so the blind round is checkable. A seat with no turn in a round is not judged, never a vote against, and it blocks the verdict. The session stops by rule — not-judged, grounded-convergence, paused-budget, no-new-evidence, max-rounds — and escalate sends grounded dissent that survived to a person. record seals the panel, the turns, the tally, the stop, the judge's hpp.decision/v1, the verdict and the dissent that lost into hpp.deliberation/v2 (the form sealed since 2.8.0; a v1 record is only verified); verify checks the seal and re-derives the tally, stop and verdict from the turns. The seal is not a signature: a consistent rewrite of a turn or of the judge's record, resealed, verifies, so keep the verbatim text and the judge's raw response, whose hashes the record carries. A session ends at the first round where a rule holds; turns of later rounds are refused, and the judge's record must come from the judge seat's provider and pinned model. The human decision is written afterwards, outside the seal.

New in 2.8.0: a panel may declare evidence.ids — plan --context FILE fills them from the context the seats were given, --evidence RECORD... adds each hpp.evidence/v1 record that verifies. Then a fact grounds a position only through a reference that resolves; any other reference is unsupported, listed per round and never counted as new evidence. When the verdict leaves grounded dissent standing, record needs --rationale, an hpp.rationale/v1 from the judge seat with one steelman per dissenting position and at least one would_change_if. The record is hpp.deliberation/v2; a v1 record sealed by 2.7.0 still verifies.

New in 2.8.0, in the lane module: house_session.py families says whether the host can seat two model families (one is DEFERRED, never a one-family panel), and house_session.py seat runs one seat's command in its own worktree, fingerprinting the worktree before and after — a seat that wrote gets no turn (exit 2), a seat that failed is not judged (exit 1). /deliberate in Claude Code and the house-session skill in Claude Code and Codex walk the whole session.

python -m hpp deliberate plan examples/house-session/review/panel.json
python -m hpp deliberate stop --panel examples/house-session/review/panel.json --turns examples/house-session/review/turns.json
python -m hpp deliberate record --panel examples/house-session/review/panel.json --turns examples/house-session/review/turns.json --judge examples/house-session/review/judge.json --rationale examples/house-session/review/rationale.json --out out/record.json
python -m hpp deliberate verify out/record.json
  • deliberate plan FILE [--context FILE] [--evidence RECORD...] [--out FILE] · deliberate validate FILE [--panel FILE] — validate reads a panel, a turn (with the panel it belongs to) or a record, which it verifies. Exit 0 valid · 2 refused, naming the first broken rule.
  • deliberate tally --panel FILE --turns FILE · deliberate stop --panel FILE --turns FILE — --turns is a JSON list of hpp.turn/v1. Exit 0 reported · 2 refused.
  • deliberate record --panel FILE --turns FILE [--judge FILE] [--rationale FILE] --out FILE — refuses a session that has not stopped, a verdict without a judge, a verdict over grounded dissent without a steelman, and a judge that answered another question, another state or from another model than the judge seat's. Exit 0 sealed with the judge's answer · 1 sealed, verdict blocked or judge failed · 2 refused.
  • deliberate verify RECORD — exit 0 intact · 2 broken: edited after sealing, or it does not re-derive from its own turns and judge.

New in 2.9.0: a sealed session feeds the commands that act on a verdict, and can only make the outcome stricter. hpp route --deliberation PLAN-RECORD --maker-family F lets a plan session raise the request's risk, never lower it. hpp attest create ... --deliberation GATE-RECORD --maker-family F is the release gate: a release-gate session whose facts rest only on hpp.evidence/v1 records; the attestation records the stricter verdict and the record hash, and a panel that did not decide sends it back for revision. lane_board.py select --deliberation DESIGN-RECORD decides a competition, with the judge as the reviewer of record. A judge of the maker's model family is refused everywhere.

Measured from a checkout of main: the review example → stop max-rounds, escalate: true; record → verdict high, dissent seat c at low, blind agreement 0.33, final agreement 0.67, 591 characters of 6000, exit 0; verify → intact, exit 0, and on a copy with the verdict edited → record_sha256 does not match, exit 2. record without --judge → a verdict needs the judge's decision record, exit 2. The panel replayed as one decider on the typed-decision suite → 15 sessions, 12 decided, 3 abstained, 0 instrument failures, exit 0. The sessions are synthetic: they prove the contract, not that a panel beats one agent.

See examples/house-session for the roles each session type needs and the full stop rule.

Attestation binds an approval to bytes

hpp attest create records a verdict together with the spec hash, the base commit, a digest of the full repository snapshot (tracked and untracked), the maker, the checker and the session. Maker and checker must differ, or the record is refused. hpp attest verify re-derives every binding and blocks when any of them moved.

python -m hpp attest create --repo . --spec spec.md --output att.json --maker a --checker b --session s1 --verdict approved
python -m hpp attest verify att.json --repo .

Measured in a scratch repository: --maker a --checker a → hpp: maker and checker must be different non-empty actors, exit 2. With different actors the record is approved and verify returns valid, exit 0; after one line of the spec changed, verify returns blocked with mismatches: spec_sha256, snapshot_digest, exit 2. Attestation requires git.

A stale approval refused, rendered from real output: an author refused as its own checker, an approval recorded and verified, one byte appended to a committed file, and the same approval blocked with exit 2

Rendered from a live run (scripts/render_terminal_svg.py --animate --session attest): in a repository whose one commit holds spec.md and app.py, one byte appended to app.py after the approval makes verify return blocked with mismatches: snapshot_digest alone, exit 2, while base_commit does not move: an uncommitted edit moves the snapshot, not the commit.

Optional signature: who gave the verdict, checked offline

New in 2.10.0

A self-hash shows that a record changed; it does not say who wrote it. Both records can carry an SSH signature. attest create ... --sign-key KEY [--signer NAME] signs the attestation — the signer defaults to the checker — and evidence run ... --sign-key KEY --signer NAME signs the evidence record. The harness never reads the key: it runs ssh-keygen -Y sign, namespace hpp, over the canonical bytes of the record — the signature itself never among them — and stores a signature block — scheme (sshsig), namespace, signer, the sha256 of the signed bytes and the armored signature. Without ssh-keygen on PATH the command refuses (exit 2) and nothing is written.

attest verify and evidence verify report the signature in their own signature block. Without --allowed-signers it is not-signed or not-verified (present, never claimed), and the verdict is decided exactly as before. With --allowed-signers FILE — the ssh-keygen allowed-signers file the verifier trusts — it is verified only when ssh-keygen -Y verify accepts the signature of exactly these bytes by a key the file lists for the signer named; anything else is refused, and the record is blocked, exit 2: unsigned, signed by another key, or changed after signing, however consistently its hash was recomputed.

python -m hpp attest create --repo . --spec spec.md --output att.json --maker a --checker b --session s1 --verdict approved --sign-key ~/.ssh/id_ed25519
python -m hpp attest verify att.json --repo . --allowed-signers allowed_signers
python -m hpp evidence run --id smoke --artifact out.txt --sign-key ~/.ssh/id_ed25519 --signer b -- python check.py
python -m hpp evidence verify <record_path> --allowed-signers allowed_signers

Measured on the 2.10.1 source tree in a scratch repository, with an ed25519 key and an allowed-signers file listing it for the checker, the signed attestation: verify without the file → valid, signature not-verified, exit 0; with the file → valid, signature verified, exit 0; with a file listing another key → blocked, mismatches: signature, exit 2. An unsigned attestation verified with the file → blocked, the record is not signed, exit 2. evidence run --sign-key without --signer → hpp: signing needs a signer: pass --signer with the principal your allowed-signers file lists, exit 2; with it → passed, and evidence verify answers not-verified, verified and blocked exactly as the attestation did.

See the claim matrix and the benchmark contract.

09 · decisions

An advisor is declared, measured, and never called by the harness.

Some decisions are small: pick one of a few options about a change — its risk, its route, whether it needs a person. They can come from outside the harness: a rule, a person, a local model, or a hosted typed-decision model such as Jev. hpp still calls no model and holds no key. What it adds is a record you can check and a ruler that measures the advisor before anyone trusts it.

PieceWhereCalls a model?
the record hpp.decision/v1hpp/decision.py · hpp decide validateno
the rulerhpp decide evalno — it runs the decider you name, as a command
a lexical baselineexamples/typed-decisions/baseline_decider.pyno
an HTTP adapterexamples/typed-decisions/decide.pyyes — only when you run it, with your own key

How it works with Jev

  1. Declare. hpp init --decision-advisor typesafe (or openrouter, or compatible) records which advisor you use and prints how to wire it. The default is off. The key stays in your shell — TYPESAFE_API_KEY for TypeSafe AI, which serves Jev; OPENROUTER_API_KEY for the same model through OpenRouter's decisions endpoint (alpha); --endpoint for a local or self-hosted server that speaks the same wire format.
  2. Pin. The adapter sends the question, its options and the state, to a pinned model version such as jev-1.13.0. An alias such as jev-latest is refused, even when the server answers under one, so the record can say who answered.
  3. Record. The answer becomes an hpp.decision/v1 record with one of three outcomes: recommendation (one of your options, with the confidence the decider reported, or null), abstention (it would not choose — changes nothing) or instrument-failure (timeout, HTTP error, an HTML page, a malformed answer — changes nothing, never a verdict). The record is always advisory; with --declared <value> it is raise-only: advice can raise the value you declared, never lower it.
  4. Validate. hpp decide validate record.json prints the effective value a consumer may act on and its action — raised, kept, advised or none.
  5. Measure before you trust. hpp decide eval runs the baseline and your advisor on the same labelled suite and reports coverage, selective accuracy, confident errors, correct and missed abstentions, instrument failures and, when confidences exist, a coverage curve at floors 0.5–0.9. A suite with no decision is reported as no sample, never as 0%.
export TYPESAFE_API_KEY=<your key>
python -m hpp decide eval examples/typed-decisions/gotcha-family-suite.json \
  --decider-command '["python", "examples/typed-decisions/baseline_decider.py"]'
python -m hpp decide eval examples/typed-decisions/gotcha-family-suite.json \
  --decider-command '["python", "examples/typed-decisions/decide.py", "--provider", "typesafe", "--model", "jev-1.13.0"]'

python examples/typed-decisions/decide.py --provider typesafe --model jev-1.13.0 \
  --question examples/typed-decisions/route-risk-question.json --state-file change.txt --declared low > decision.json
python -m hpp decide validate decision.json

What the adapter refuses, and why

  • A valid answer can be wrong. The schema guarantees that the answer is one of your options, not that it is correct; confidence is a formula over the answer distribution, not a probability of being right. Calibrate a floor per question, on your own labels.
  • Abstention is ours. The vendor has no abstention state; low confidence becomes abstention here.
  • Judged text can steer the answer. Instructions planted in the text can shift it — which is why raise-only exists.
  • Nothing is reproducible by asking again. The record keeps the pinned model that answered and the hash of the raw response, written beside it in .hpp/decisions/raw/.
  • Every call sends text off your machine. Never call an advisor from a hook; the adapter refuses secret-like text, makes one attempt, never retries, caps the body at 1 MiB, refuses redirects, and sends a key only over https or to a loopback address. hpp policy check classifies running it as MANUAL, like curl to a URL.

Version 1 measures choice questions only; a score or a probability of yes has no agreed definition of correct, so the contract refuses those kinds instead of reporting a vacuous 0%. The shipped suite is fifteen synthetic cases written by hand, and the baseline was written alongside it — its score proves the ruler works, not the baseline's quality. Build your suite from your own labelled cases before reading any number as quality.

Measured on the 2.10.0 tree: the baseline decider on the shipped suite → 15 cases, 12 decided, 3 abstained, 0 instrument failures, coverage 0.8, selective accuracy 1.0, exit 0.

10 · hosts

Same contract, visible differences.

Claude CodeCodex CLI
Distributionplugin marketplace (/plugin marketplace add rusharlabs/house-party-protocol, then /plugin install <module>@house-party-protocol)its own plugin marketplace for the modules that carry skills (codex plugin marketplace add rusharlabs/house-party-protocol, then codex plugin add <module>@house-party-protocol; skills only, read from .agents/plugins/marketplace.json), plus a verified copy per module through kit_doctor.py install --host codex for runtime, scripts and templates
Where it landsthe host's plugin store; .claude/settings.local.json for hooks and statusLine.agents/skills for skills, .agents/hpp/<module> for runtime; AGENTS.md is what the host reads
Lifecycle hooksnative, once the wiring is pasted (a human gate; hpp init lists the modules that declare hooks)the host has its own, but HPP loads none of its hooks there (each Codex plugin manifest carries an empty hooks object); the same capabilities are explicit commands
Coverage in the manifestsix modules native, four explicit-commandnine explicit-command, seven of them in the plugin channel; claude-dev-kit is unsupported, and hpp init halts rather than plan it there

Source: hosts in each module entry of hpp.manifest.json; the per-host paths are the ones hpp init prints in its wire block. hpp init --host codex plans the reliable-coding bundle with all six modules as explicit-command, opening with the plugin channel lines for the five that carry skills — measured on the 2.10.1 source tree, 12 commands to paste: one codex plugin marketplace add, five codex plugin add and six installer lines. The host × module × channel matrix, one row per module, is rendered from the manifest by hpp doctor --matrix and kept in Architecture · Hosts and channels; a test fails when the two diverge.

11 · exit codes

One contract: 0 ok · 1 warn · 2 block · 3 error.

Command0123
policy check --mode enforceALLOWMANUALBLOCK · refused policy file (new in 2.10.0)—
policy check --mode auditevery verdict—refused policy file (new in 2.10.0)—
eval run · benchmarkgate passedgate failedinvalid suite—
decide eval (new in 2.6.0)gate passedgate failed · nothing decidedinvalid record, suite or argv—
decide validate (new in 2.6.0)valid—refused—
evidence run (new in 2.6.0)passednot passedrefused · event not appendedinternal error
evidence verify (new in 2.6.0)validnot-evidenceblocked · with --allowed-signers, a signature missing or not acceptedinternal error
evidence mutate (new in 2.7.0)sensitiveblind spots · incomplete · no mutantsno control · refusedinternal error
work coverage (new in 2.8.0)completenot covered · not executedrefused report or test pathinternal error
findings check (new in 2.8.0)every review passeda lens found somethingdocument breaks the contractinternal error
retrieval eval (new in 2.6.0)gate passedgate failedrefused suite or argumentinternal error
cite check (new in 2.6.0)cleanwarnblock · refused inputinternal error
deliberate record (new in 2.7.0)sealedverdict blocked · judge failedrefusedinternal error
deliberate verify · plan · validate (new in 2.7.0)intact · valid—broken · refusedinternal error
attest create · attest verifyapproved · valid—revise/blocked · invalid · refused · with --allowed-signers, a signature missing or not accepted—
event verify (new in 2.10.0)intact · legacy · empty—brokeninternal error
initok · no-opwarning, non-blocking failurehaltedusage error
event append · status · maps · route · context compileok—invalid transition, corrupt log or broken hash chain, invalid input, no provider at the risk floor, secret-like contextinternal error

Source: exit_codes in the manifest, hpp/cli.py and the measurements quoted above. Any known error (manifest, install, state, eval, attestation, invalid JSON) exits 2 with a one-line message; an unexpected exception exits 3.