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

Harness manual · v2.6.5 · 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 on 2026-09-23 against version 2.5.8, from a clone of this repository at the v2.5.8 tag and from a pip install of the same version, and each block says which. 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. 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

Verified copy per module through the module installer: skills to .agents/skills, runtime to .agents/hpp/<module>, AGENTS.md read by the host. There are no lifecycle hooks; 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, "modules": 10, "status": "ok"}. 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 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.4.2/kit_doctor.py, next to the module directories it installs from (a pip install carries neither): python installers/kit-forge-1.4.2/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: plugin channel lines for Claude Code, module-installer lines for Codex CLI and for explicit-command modules, and 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

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

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.

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.4.1/scripts/lane_board.py compete --task TASK-1 --items ITEM-A,ITEM-B --lane lane-a --model claude-opus-4-8
python multi-session/lane-kit-1.4.1/scripts/lane_board.py select --task TASK-1 --winner ITEM-B --lane lane-r --model gpt-5.6 --reason "same tests, half the diff"

Measured from a checkout of main on a scratch board where ITEM-A (lane lane-a, claude-opus-4-8) and ITEM-B (lane lane-b, claude-sonnet-4-6) were each claimed, built and CHECKPOINT-READY with evidence: compete → exit 0; select by claude-haiku-4-5 → maker≠checker violated: … SAME model family (claude), exit 1; select by gpt-5.6 → 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 · 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. 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 and names the next step; 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

Measured in an empty directory: 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.

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

Measured on the example: wave 1 spec · wave 2 build, docs · wave 3 verify; tier counts economy 2 · balanced 1 · frontier 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.

06 · 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 the exit code is always 0 and the verdict is only recorded; in enforce the verdict becomes the exit code.

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.

07 · 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.

See examples/typed-decisions for the adapter, its risks and how to measure it before trusting it.

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 self-hash makes an edited record visible; it is not a signature — anyone who can write the file can rewrite it — so a checker that must not trust the maker re-runs the command from its own lane instead of relying on verify alone.

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 verify RECORD — the record_path that run printed. 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.

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.

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.

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.

See the claim matrix and the benchmark contract.

08 · hosts

Same contract, visible differences.

Claude CodeCodex CLI
Distributionplugin marketplace (/plugin marketplace add rusharlabs/house-party-protocol, then /plugin install <module>@house-party-protocol)verified copy per module through kit_doctor.py install --host codex
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)none; the same capabilities are explicit commands
Coverage in the manifestsix modules native, four explicit-commandnine explicit-command; 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.

09 · exit codes

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

Command0123
policy check --mode enforceALLOWMANUALBLOCK—
policy check --mode auditalways———
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-evidenceblockedinternal 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
attest create · attest verifyapproved · valid—revise/blocked · invalid · refused—
initok · no-opwarning, non-blocking failurehaltedusage error
event append · status · maps · route · context compileok—invalid transition, corrupt log, 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.