How the pieces of House Party Protocol fit together, where state lives, and what deliberately does not exist.
01
Four layers
┌──────────────────────────────────────────────────────────────────────────┐
│ HARNESS python -m hpp │
│ doctor · init · event · status · resume · attest · policy │
│ work · route · context · map · graph · eval · benchmark │
│ decide · evidence · retrieval · cite (new in 2.6.0) │
│ deliberate (new in 2.7.0) │
├──────────────────────────────────────────────────────────────────────────┤
│ PROTOCOL hpp.manifest.json │
│ roles · loop transitions and gates · exit codes │
│ host coverage · monitors · bundles · invariants │
├──────────────────────────────────────────────────────────────────────────┤
│ MODULES ten versioned directories, each installable on its own │
│ skills · hooks · commands · agents · rules · scripts │
├──────────────────────────────────────────────────────────────────────────┤
│ DISTRIBUTION Claude Code: marketplace.json + plugin per module │
│ Codex CLI: verified copy into .agents/ by the installer │
│ + plugin channel (skills only) for 7 modules │
└──────────────────────────────────────────────────────────────────────────┘
The harness is stdlib-only and executes no model. It reads files, runs the commands you name,
and writes to a small number of declared paths. decide, evidence, retrieval and cite are
new in 2.6.0; deliberate is new in 2.7.0.
02
The hpp/ package
| file | responsibility | surface |
|---|---|---|
cli.py | argument parsing, dispatch, the exit contract (2 for a refused input, 3 for an internal error) | every subcommand, --self-test |
manifest.py | locate, load and validate the manifest; cross-check marketplace.json and each module's plugin.json when present, and the Codex marketplace (.agents/plugins/marketplace.json) with each listed module's .codex-plugin/plugin.json | doctor |
state.py | append-only, hash-chained event log (each line links to the sha256 of the one before it; a broken link names its first divergent step, a log written before the chain reads as legacy); projection of events onto the loop; refusal of invalid transitions before the write | event append, event verify, status, resume |
workgraph.py | validate a spec, reject cycles with the path named, order units into waves; link each acceptance criterion to the tests that cite it and, with a JUnit XML report, to the tests that ran it; the plan and the waves as JSON or Mermaid, the coverage as JSON | work plan, work waves, work coverage |
findings.py | check the hpp.findings/v1 document a review lens answers with (stable code, severity, file and line, evidence, the universe inspected), refuse a key the contract does not define, and derive the verdict | findings check (new in 2.8.0) |
policy.py | classify a command as ALLOW, MANUAL or BLOCK; apply a project's .hpp/policy.json, which can only add rules or raise a class, never lower one; map to exit code by mode | policy check |
attest.py | bind a verdict to repo identity, base commit, spec hash and a full file snapshot; verify later; sign the record with an SSH key on request (--sign-key) and demand that signature on verify (--allowed-signers) | attest create, attest verify |
signing.py | sign the canonical bytes of a record with ssh-keygen -Y sign (namespace hpp) and check them with ssh-keygen -Y verify against an allowed-signers file; without the binary nothing is signed and nothing is reported verified | used by attest, evidence |
hosts.py | the host x module x channel matrix derived from hosts and components in the manifest, rendered for the documents | doctor --matrix |
context.py | fit whole blocks under a character budget with provenance hashes; refuse secret-like input | context compile, map context |
routing.py | choose a tier and a provider id from declared risk, complexity, context size and stage; fall back only upward | route |
maps.py | Lane Map, Agent Map, Context Map and Monitor Map as sorted, data-only projections; JSON or Mermaid | map lane, map agent, map context, map monitor |
graph.py | capability, operational, agent, evidence and code views from the manifest; JSON or Mermaid | graph |
evals.py | pass@k / pass^k runner over a suite of cases with three runner kinds | eval run, benchmark |
decision.py | validate an hpp.decision/v1 record made outside the harness (advisory, raise-only, abstention and instrument failure as outcomes); measure a declared decider with selective metrics. Calls no model | decide validate, decide eval (new in 2.6.0) |
evidence.py | run a declared criterion command (argv, no shell, its own process group), measure its exit code, hash the artifacts it declared and write a self-hashed record; re-derive a record later. Refuses a secret-like command line. The self-hash makes an edit visible; it is not a signature — --sign-key adds one, and verify --allowed-signers demands it. Drives no browser | evidence run, evidence verify (new in 2.6.0); evidence mutate runs the criterion on a copy of the workspace, clean and once per mutant, and names the mutants it let through (new in 2.7.0) |
retrieval.py | score a retriever you declare as a command against the ids a suite labels relevant: hit@k, recall@k, precision@k, MRR, nDCG@k, with instrument failures counted apart. Runs no index | retrieval eval (new in 2.6.0) |
citations.py | check that every citation marker in a text resolves to one id of the context it was written from, and flag a quantitative sentence with no marker. Does not judge whether the source supports the sentence | cite check (new in 2.6.0) |
deliberation.py | House Session: validate a panel of pinned deciders (two model families, a judge outside the participants' lanes, the roles its session type needs, a declared budget), count its turns (grounded and ungrounded votes, abstentions, seats not judged, moves without new evidence; with declared evidence, only references that resolve ground a fact), stop it by rule, and seal it with the judge's decision and, over grounded dissent, the judge's steelman as a self-hashed record that re-derives from its own turns; project the panel as one hpp.decision/v1. Calls no model | deliberate plan, validate, tally, stop, record, verify (new in 2.7.0) |
controls.py | the ten executable controls the benchmark runs, each with a positive and a negative case | benchmark, --self-test |
install.py | plan-only installation receipt for a bundle on a host; refuses unsupported coverage | install |
wizard.py | hpp init: six stages, readiness, plan versus apply, wire block | init |
term.py | colour tier detection, ANSI output, ASCII glyph fallback, no-TTY behaviour | used by init |
brand.py | palette, block wordmark and closing lines for the terminal | used by init |
policy.py holds eight fixed rules; the first match wins and every BLOCK rule is tried before
any MANUAL rule. BLOCK: recursive-delete, force-push, main-push, pipe-to-shell,
destructive-sql. MANUAL: external-push, external-send, decision-advisor (new in
2.6.0). Anything else is ALLOW. The mode
changes only the exit code: audit exits 0 for every verdict, enforce exits 2 on BLOCK and 1
on MANUAL. What each rule matches is in CONCEPTS.md; check one with
python -m hpp policy check --mode enforce --command "rm -rf src" (exit 2).
A project hardens the classifier with .hpp/policy.json (hpp.policy/v1, read by policy check
from the current directory, or named with --policy FILE): rules of its own with a pattern, a class
(BLOCK or MANUAL) and a reason, and built-in MANUAL rules raised to BLOCK. A file that would
exempt anything — a rule with action ALLOW, a raise that keeps or lowers a class, a rule that
reuses a built-in id — is refused as a whole, and a refused policy refuses the check (exit 2, no
verdict printed) instead of falling back to the built-ins. Every verdict says its source
(built-in or policy).
Sizes are small by design; the whole package is readable in one sitting. Nothing imports outside the standard library.
03
The flow
spec (JSON)
│ hpp work plan
▼
WorkGraph ──── waves ───▶ wave 1 ──▶ wave 2 ──▶ ... (barrier between waves)
│ │
│ hpp route │ per unit: tier → provider id (never a model call)
│ ▼
│ lane claim ◀── hpp map lane (liveness from heartbeat)
│ │
│ ▼
│ work_started ─▶ evidence_recorded ─▶ check_passed
│ │ ▲ │
│ │ hpp context compile │
│ │ (what the maker saw, hashed) ▼
│ │ hpp attest create
│ │ (verdict bound to bytes)
▼ ▼ │
hpp map monitor .hpp/events.jsonl ◀── human_approved ◀─────┘
(up · fresh · skew) │
▼
verified ── hpp status / hpp resume derive the next step
Every arrow that changes state is an appended event. Every box that decides something reads files and writes at most one declared file.
04
Where state lives
| path | written by | content | lifetime |
|---|---|---|---|
.hpp/events.jsonl | hpp event append | one JSON object per line: seq, id, type, data and prev_sha256, the sha256 of the line before it (the first links to the hash of nothing); append-only and hash-chained — hpp event verify names the first divergent step, and lines written before the chain read as legacy | the workspace's loop history |
.hpp/policy.json | the operator, by hand | hpp.policy/v1: rules that add BLOCK or MANUAL, and built-in MANUAL rules raised to BLOCK; read by hpp policy check, refused as a whole if it would exempt anything | until the operator changes it |
.hpp/profile.json | hpp init --apply | host, bundle, modules, policy mode, protocol and product version; decision_advisor only when one was declared | until the operator removes it |
.hpp/attestation.json | hpp attest create --output | the bound verdict; the path is yours to choose | until the bytes it describes change |
.hpp/evidence/<id>-<UTC>.json | hpp evidence run (new in 2.6.0) | one record per run, created exclusively: the command, base commit, exit code, verdict, byte count and sha256 of stdout and stderr (never the text), the sha256 of every declared artifact, and the record's own hash; --out picks another directory inside the workspace | until the operator removes it; evidence verify blocks it once an artifact it names changes |
hpp.manifest.json | the project | the protocol; found by walking up from the current directory, then next to the source package, then the copy shipped inside the installed package | versioned with the product |
module CHECKSUMS.txt | the forge | sha256 per distributed file | versioned with each emitted module |
Everything else the harness consumes is an input you pass on the command line: specs, lanes,
monitors, providers, routing requests, context blocks, evaluation suites. There is no hidden
cache, no user-level state directory and no environment variable that changes a verdict; the
only environment the harness reads is NO_COLOR, FORCE_COLOR, TERM, COLORTERM, CI and the
Windows terminal session flag, and those affect rendering only.
The event log is the recovery boundary. Reading it validates that seq is contiguous and that
id equals event:<seq>; a corrupt line stops the projection with the line number instead of
skipping it. Appending validates the transition against the manifest before opening the file, so
a refused event leaves no trace and cannot create the log.
05
The manifest as executable contract
hpp.manifest.json declares the protocol version, the hosts, the modules with their paths,
versions, capabilities, relations and per-host coverage, the bundles, the roles, the exit codes,
the routing tiers and rule, the map names, the installer path, the monitors and the loop.
hpp doctor rejects a manifest that violates any of: unique hosts, modules, capabilities, paths
and monitor ids; relative module paths; coverage values outside native / explicit-command /
unsupported; unknown relations; cycles in requires; bundles naming unknown modules or
capabilities; exit codes other than 0/1/2/3; routing tiers other than
economy/balanced/frontier; monitors with non-positive cadence or freshness.
When marketplace.json sits beside the manifest, the doctor also checks that the product version
matches, that the module set matches, that each module's source and version match, that the
module directory exists inside the product root, and that its .claude-plugin/plugin.json agrees
on name and version. In a source checkout without a marketplace the doctor says so
(distribution: source-contract) instead of reporting a check it could not run.
06
Maps
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.
| map | input | question it answers |
|---|---|---|
| capability | manifest | which module provides which capability, on which host, in which bundle |
| operational | manifest loop | which event moves which state through which gate |
| agent | manifest roles and modules, optional events | who may make, check and approve; what has happened in order |
| evidence | fixed | how a criterion, a record and a verdict relate |
| code | manifest components | which surfaces each module declares (not an AST) |
| lane | lanes JSON, --now, thresholds | who owns what, who is alive, where live claims overlap |
| context | context JSON, --budget | what entered the compiled context, what was omitted, with hashes |
| monitor | monitors JSON, --now, --skew-tolerance | what is observed, how fresh, and which gate consumes it |
| work | spec JSON | which units can run together and in which order |
The Context Map is provenance for one compilation; it is not retrieval and not a knowledge base. The Code Map inventories declared components; it is not a call graph.
07
Two hosts
| capability | Claude Code | Codex CLI |
|---|---|---|
| skills and instructions | plugin, native | copied into .agents/skills, namespaced |
lifecycle hooks (Stop, PreToolUse, SessionStart, ...) | native once wired by a person | the host has its own; HPP's are not loaded there, and the same scripts run as explicit commands |
| command policy | hook plus CLI | CLI or preflight |
| event log, attestation, maps, WorkGraph, eval | CLI | CLI |
| install | marketplace plugin, or installer copy for explicit-command modules | installer copy into .agents/hpp/<module> |
The host seam is a small table: settings path and manual gates for Claude Code; AGENTS.md,
skills path and runtime path for Codex CLI. hpp init uses it to name the same paths the module
installer will touch, and touches none of them. Adding a host means adding a row and a coverage
value per module, not changing the six stages.
Hosts and channels
One row per module, rendered from hosts and components in the manifest by hpp doctor --matrix
(hpp/hosts.py); tests/test_host_matrix.py keeps this table equal to that render.
| module | Claude Code | Codex CLI |
|---|---|---|
operator-kit 1.8.4 | native · plugin + hooks after the wiring is pasted | explicit-command · plugin (skills only) + verified copy |
kit-forge 1.5.4 | explicit-command · plugin | explicit-command · verified copy |
lane-kit 1.8.2 | native · plugin + hooks after the wiring is pasted | explicit-command · plugin (skills only) + verified copy |
continuity-kit 1.5.3 | native · plugin + hooks after the wiring is pasted | explicit-command · plugin (skills only) + verified copy |
health-kit 1.4.4 | explicit-command · plugin | explicit-command · plugin (skills only) + verified copy |
claude-dev-kit 1.3.8 | native · plugin | unsupported · — |
supabase-pack 1.2.3 | explicit-command · plugin | explicit-command · plugin (skills only) + verified copy |
agent-framework-wizard 1.2.5 | explicit-command · plugin | explicit-command · verified copy |
dev-squad-kit 1.2.2 | native · plugin | explicit-command · plugin (skills only) + verified copy |
gotcha-memory 1.1.4 | native · plugin + hooks after the wiring is pasted | explicit-command · plugin (skills only) + verified copy |
On Claude Code every module is a plugin of the marketplace, and a module that declares hooks arms
them once the wiring is pasted. On Codex CLI the plugin channel is the product's own marketplace,
.agents/plugins/marketplace.json (codex plugin marketplace add rusharlabs/house-party-protocol,
then codex plugin add <module>@house-party-protocol): it lists the modules that carry skills and
are supported there, and it installs skills only — the hooks are Claude Code's, and the verified copy
(kit_doctor.py install --host codex) is what installs runtime, scripts and templates. Each module's
.codex-plugin/plugin.json carries an empty hooks object on purpose: with no hooks key, Codex
loads the module's hooks/hooks.json by default.
08
Distribution
The published tree is emitted by the forge from module sources. Each module directory carries
CHECKSUMS.txt (sha256 per file) and a .zip with the same bytes; marketplace.json lists the
modules with source and version; .agents/plugins/marketplace.json lists, in the form Codex CLI
reads, the modules of the Codex plugin channel, each carrying a .codex-plugin/plugin.json rendered
from its Claude one, and hpp doctor cross-checks the two channels against the manifest
(distribution.codex_marketplace); the installer (installers/kit-forge-<version>/kit_doctor.py)
runs the six install stages, verifies checksums and executes the module's declared smokes.
sources ──forge──▶ module dir + CHECKSUMS.txt + .zip ──▶ marketplace.json
│ │
└── kit_doctor.py verify ◀────────────┘── hpp doctor (cross-check)
This repository is that published tree: the module directories with their CHECKSUMS.txt, the
installer and marketplace.json sit beside the harness, so hpp init verifies distribution
integrity and module checksums here (9 of 11 readiness items). Two other layouts carry less and
say so: the pip wheel ships the harness, the manifest and the benchmark suite but no module
directories, and the harness source tree has none of the emitted artefacts; in both, the doctor,
the benchmark and the suite run, and distribution integrity and module checksums are reported as
not verified rather than assumed.
09
Exit contract
| code | meaning | where |
|---|---|---|
0 | ok; in audit mode, every verdict (a refused policy file still exits 2) | every command |
1 | warn or manual gate; an eval gate that failed | policy check (MANUAL in enforce), eval run, benchmark, init with warnings, decide eval when its gate fails (new in 2.6.0), evidence run when the bundle did not pass, evidence verify on an intact record of a run that did not pass, retrieval eval when its gate fails, cite check with a warning (TOO_MANY, UNCITED_CLAIM) (new in 2.6.0), deliberate record when the verdict is blocked or the judge failed, evidence mutate with a blind spot, an incomplete run or no mutant applied (new in 2.7.0), work coverage when a criterion is not covered or, with --junit, not executed, findings check when a lens found something (new in 2.8.0) |
2 | block; a refused input (bad manifest, bad spec, corrupt log, invalid attestation) | policy check (BLOCK), attest, decide validate and decide eval on a record or suite that breaks the contract (new in 2.6.0), evidence run on a refused request or an event it could not append, evidence verify on a record that was edited or contradicts itself or whose artifact changed or went missing, retrieval eval on a refused suite or argument, cite check on UNKNOWN_ID, RANGE or EMPTY_MARKER or a refused input (new in 2.6.0), deliberate on a refused panel, turn or judge, a session that has not stopped, or a record that does not verify, evidence mutate when the criterion does not pass on the clean copy (no-control) or a mutant is refused (new in 2.7.0), work coverage on a refused report or a test path with no Python source, findings check on a document that breaks the contract (new in 2.8.0), event verify and status on a broken hash chain, policy check on a refused policy file, attest verify and evidence verify with --allowed-signers on a record whose signature is missing, not accepted or over changed bytes, every validation error |
3 | usage or internal error | init usage errors, unexpected exceptions |
hpp init reports the code it will return inside its JSON report (exit_code) and halts the
remaining stages when a stage fails with a blocking hint.
10
Tests and CI
The suite under tests/ is stdlib-only and runs without network. Each test file carries at least
one test named CONTROLE that proves the file can fail. In this repository one test skips by
design (the emitted copy is covered by the next test); the harness source tree, which has no
marketplace.json, skips the two tests that need it. CI runs the suite,
hpp doctor and hpp benchmark -k 3 on Linux, macOS and Windows across Python 3.10 to 3.13, and
a lint job runs ruff check hpp tests scripts against the rule set in pyproject.toml, with
ruff installed from a hash-pinned requirements file; every job has read-only permissions and no
step is allowed to fail silently.
python -m pytest tests -q
python -m hpp doctor
python -m hpp benchmark -k 3
11
What deliberately does not exist
| absent | why it is a choice |
|---|---|
| daemon or background service | a check that did not run was not run; a resident process would be a second thing to verify and would hold state the log cannot see |
| server, API or control plane | the harness is operated from the repository it protects; a remote plane would move the verdict away from the bytes it is about |
| scheduler or queue | cadence in the Monitor Map is declared for the consumer to honour; the harness does not wake itself up |
| graph database | every map is re-derivable from files; a stored graph would drift from its sources and require its own doctor |
| model execution | routing returns a tier and a provider id; calling a model would make verdicts depend on something the harness cannot reproduce or afford to hold credentials for |
| credential storage | there is none to leak; secret-like input to the context compiler is refused, and the failure memory redacts by shape |
| telemetry | nothing leaves the machine; git push, curl/wget to a URL and the example decision adapter are classified MANUAL (a transfer by another tool, such as scp, matches no rule) |
| automatic settings or hook wiring | enabling a hook changes what runs on every future tool call; that is a human action, printed for pasting |
| token counting | the budget is in characters, which every host measures the same way; a token count would tie the harness to a tokenizer |
A layer is added only when there is a consumer for it and a benchmark that shows what it costs. Until then, the absence is the feature.