The reference
A harness is named by one string, onkapso build --harness, kapso deploy --harness, the brief’s Harness line, kapso.load_harness() and every kapso harness subcommand:
A git reference is cloned once into
<harness.store>/<host>-<owner>-<repo>@<rev>/ with a source.json beside the clone; a branch or HEAD moves on kapso harness update, a tag or a commit never does. A string that matches no form, or a directory that does not exist, raises HarnessReferenceError listing the forms.
harness.yaml
The modules are
setup, ideation, implementation, verification, feedback, learning and deployment; build runs the first five, deploy the last, learn the sixth. An unknown key anywhere is a failure. Lists are complete; nothing is inherited between modules.
Files
${PLUGIN_ROOT} in a server’s or a hook’s command or arguments is the harness directory, expanded at staging; a path under it must exist. An env or headers value that is a ${NAME} reference is read from the environment or .env beside the run; a literal where a reference is expected, or a value that looks like a credential, is a failure.
The web. The engine’s search_strategy.params.ideation.web and implementation.web switches are the permission boundary on both hosts: off, Claude Code’s WebSearch and WebFetch leave the allow-list and are disallowed, and a Codex session runs without --search; the ensemble members, the selector and the lens planner follow ideation’s switch. A harness whose protocol forbids the web on a module states the rule as a hook on that module, so the session meets the reason whatever the config says: the trading example’s deny-web denies the two tools on implementation (PreToolUse, matcher WebSearch|WebFetch), and its deny-web-fetch denies curl, wget and aria2c through the shell on every module. Codex’s native web search is not a tool call its hooks see; there the switch alone holds.
check
kapso harness check <ref> and load_harness() run the same checks. The report prints a summary line, check ok (7 skills, 2 agents, 2 servers, 7 hooks) or check failed (...), then one line per finding: fail, warn or needs.
Failures, each with the file and, when it has one, the line:
- an unsupported
schema_version; a missingname,versionordescription; a name that does not match the form; an unknown module or field - a listed skill, agent, server or hook that does not exist; a skill or agent without front matter, with a
nameother than its directory’s or file’s, or without a description mcp.jsonthat is not{"mcpServers": {...}}; a server with neithercommandnorurl, with an unknown key, a wrong type, a bad URL or a bad name; anenvorheadersliteral, or a credential-shaped valuehook.jsonthat is not{"hooks": {...}}; an unknown event; an entry, matcher or command of the wrong shape; a command with no program; a${PLUGIN_ROOT}path that does not existenvironment.pythonnaming a file that does not exist; no page at all
guardrails.md when the module has no page of its own); a page over harness.page_warning_lines lines (every line counts, blank lines and headings included); a long opaque value in a server’s env or headers.
Needs, each tagged with the modules that need it: a program a hook or a server runs, or environment.commands names, that is not on PATH; a requirements line (pip’s form: name, name==v, name>=v, a marker; # comments, blank lines and - option lines skipped, an unparsable line stops check with pip’s own parse error) that python3 on PATH does not satisfy — a bare name needs the package installed, a specifier the installed version inside it — with the python3 -m pip install -r line (the harness’s servers, hooks and scripts run on that interpreter, so its requirements are checked there, not in the engine’s); a ${NAME} reference that is set nowhere, tagged with the modules that mount the server; a GPU. check exits 1 on a failure and 0 otherwise; a need never fails check or add.
Staging
Each session of a module gets, in its own directory:
The prompt carries the guidance block: a
## Domain guidance (harness <name> <version>) heading, guardrails.md whole, the module’s page whole, then ### Skills, ### Delegates and ### Tool servers listing the module’s attachments with their descriptions and how to reach them, then ### Paths with the task statement’s path, when the campaign has one, and each prepared resource’s. The learning crews are staged the same way: the mining lead at the bundle root, where the staging goes with the session and the bundle stays as harvested; the update lead and its codex exec workers in the update run’s directory, the workers reading the guidance from harness-guidance.md beside their launcher and running with the staged servers, delegates and hook trust. An ideation session’s stream goes where the ideation artifacts live, .kapso/ideation/iter<n>/, and its session.json names that path under stream.
Staging is idempotent and refuses a path the campaign’s repository tracks, by name. The harness is hashed again at each staging; a hash other than the launch’s fails the session naming both. The campaign’s repository ignores .claude/, .agents/, .codex/, kapso_task.md and prepared.
Per session, under .kapso/sessions/<session>/: session.json, the record (module, host, harness identity or null without one, session_dir, pages, skills, mcp_servers, agents, hooks, prepared, and runs, one entry per run the host made), and stream.jsonl, the host’s event stream. A session without a harness has the record too, with nothing staged. Session names: harness_setup; ideation/iter<n>/lens_planner, ideation/iter<n>/<cli>_<model>, ideation/iter<n>/selector; <branch> for an implementation; verification/<branch>; feedback/<branch>. A learning run records lead and workers under its own directory’s .kapso/sessions/ (learning/mining/<trajectory>/ for a mining run, learning/runs/lr_<id>/ for an update), since a bundle takes no writes. A harvest leaves .claude/, .agents/ and .codex/ out of the bundle, at every level: the session records say what each session ran with, and the files are the harness’s, recoverable from the hash the manifest records.
The task statement
kapso_task.md at the campaign root, composed by the engine and never by a session, with the sections Brief (the brief’s text, whole), Budget (minutes, experiments, usd, reserve_minutes, as the campaign resolved them, none where no limit is set), Evaluation, Data, Domain statement and Prepared resources. The setup session writes the domain statement to .kapso/harness_setup/statement.md and resources under prepared/<name>/; the engine composes the statement again after it, verifies that kapso_evaluation/ and kapso_datasets/ are unchanged, and records harness_setup (done, prepared, cost_usd) in the checkpoint. The session’s timeout is harness.setup_minutes, clamped by the budget.
Verification
For each attempt with a score, when the harness has verification content, under.kapso/verification/<branch>/: diff.patch against the campaign’s main branch, evaluation_output.txt, and verdict.json written by the session, which runs in a detached worktree at the attempt’s commit with a timeout of harness.verification_minutes.
verdict is valid or void; summary is a non-empty string; findings is a list of {rule, evidence}, at least one on a void. Anything else raises HarnessError naming the file. The session runs on the implementation CLI, the attempt’s own host; a session that writes no file runs again, harness.verification_attempts sessions in all, each clamped afresh; then the attempt is recorded {"verdict": "unverified", ...}. SearchNode.verification holds the verdict; a void clears the score after the judge, counts as rejected, and the attempt is neither a parent nor delivered; an unverified attempt keeps its score and may be a parent but is never delivered. The judge’s prompt renders the verdict beside the evaluation.
Records
hash is the SHA-256 over every file of the harness directory, paths and bytes; it is not part of the config fingerprint a resume compares.
Config
Python
load_harness raises HarnessReferenceError on a reference of no form, ManifestError with the file and line on a manifest with a finding, the YAML or JSON parser’s own error on a file that does not parse, and HarnessError on a check failure. A verb raises HarnessError on an unmet need of its modules and on a deploy or learn whose loaded harness has another name than the record’s, or is loaded over a campaign built without one; a resume with a harness that is not the launch’s raises the checkpoint’s RunCheckpointIncompatibleError, naming both identities and the way out.
Related pages: What a harness is · Use a harness · Write a harness · CLI reference · Python API · Brief format
Kapso is an open-source framework by Leeroo that builds software toward measurable goals through experiment campaigns. Source code: github.com/Leeroo-AI/kapso · Install: pip install leeroo-kapso · Every page as plain text: llms.txt.