Octonote AI
Make your own →

02 · Code structure

Sunil Tiwari· Oct 27, 2026 · 48 min

02 · Code structure

Harness² is one Python package, harness2, split into four subpackages: core (the method), substrate (the agent frameworks that run a solver), benchmarks (the three benchmark adapters) and cli (the command-line entry points), plus a few top-level helper modules. The whole design rests on keeping three axes apart: method, substrate and benchmark. Each axis talks to the others only through a small set of typed seams, almost all defined in core/spec.py and substrate/base.py. This chapter maps the packages, explains every type that makes up those seams (what it holds, what it checks, why it exists), and spells out the import rules that keep core free of benchmark and substrate code. It also keeps the judge, the only code that ever sees a score, out of the method. It ends with a trace of the bundled offline demo through these types.

All code links point at commit 1cfe254e8d25b15f37048c3730134686abe60335.


1. Packages at a glance

1.1 The file tree

The Python sources at the pinned commit, with line counts. Everything under third_party/ (runner overlays, patches, licences) holds benchmark-specific material that the adapters load at run time. None of it is part of the importable package.

PathLinesRole
harness2/init.py15Docstring only: "Harness²: Recursive Agent Harnessing for an Open World."
harness2/config.py189Deployment settings, model ids, dependency pins, paths. Reads .env; shell values win. Imports nothing from the package.
harness2/install.py319Installs benchmark tools and runner overlays (PATCHES, missing_runners, codex_config_error).
harness2/preflight.py160Health checks before money is spent (check, require_ready).
harness2/demo.py163Offline demo: DemoAdapter and DemoProposer.
harness2/core/"Substrate- and benchmark-independent method code" (core/init.py)
core/spec.py277The benchmark seam. ChannelSpec, BenchPrompts, BenchSpec, RunView, SubstrateLike, BenchmarkAdapter.
core/tree.py427The c1..c8 component tree: SurfaceSpec, read/validate/write/diff.
core/ledger.py132Budget accounting: R = 4k rollouts, P = 3k improvement sessions.
core/selection.py505Blind, answer-free selection of one final, then reflection.
core/recursion.py578The two recursion modes (parallel = agg, sequential = seq).
core/schedule.py850Namespacing (Namespace, ns), stream order, drivers run_task / run_domain / run_sweep.
core/proposer.py534Proposer: runs improver/selector sessions on one channel, with an audit trail and a cache.
core/context.py546Builds the on-disk evidence tree a proposer reads, plus leak and grader-verdict guards.
core/library.py222The per-domain experience library.
core/prompts.py1163Prompt text: propose, aggregate, compose_round, select, experience.
core/runmark.py115Completion markers and harness fingerprints for idempotent rollouts.
core/judge.py155Scores the run that selection chose, after it chose it.
core/report.py165Turns judged rows into the figure each benchmark reports.
harness2/substrate/"The two solver substrates behind one interface" (substrate/init.py)
substrate/base.py83Install, RunRequest, Launcher, Substrate.
substrate/opencode.py395OPENCODE_SURFACE, OpencodeSubstrate (name "oc"), event renderer.
substrate/codex.py480CODEX_SURFACE, CodexSubstrate (name "cx"), event renderer.
harness2/benchmarks/"Benchmark task discovery, execution, and grading."
benchmarks/lab.py1005LabAdapter, lab_spec, LAB prompt tables.
benchmarks/workbuddy.py1029WbAdapter, spec_for, WorkBuddy subsets and prompt tables.
benchmarks/wb_render.py577Renders WorkBuddy deliverables (e.g. pptx) into readable evidence.
benchmarks/jobbench.py1175JobBenchAdapter, make_spec, JobBench prompt tables.
benchmarks/jb_opencode_runner.py281Stand-alone JobBench runner script for OpenCode (no package imports).
benchmarks/tasks.py88discover / select: task ids and split files.
harness2/cli/"Command-line adaptation, judging, and reporting."
cli/_common.py144make_adapter and shared argparse helpers.
cli/run_sweep.py239harness2 entry point.
cli/judge.py204harness2-judge.
cli/report.py114harness2-report.
cli/setup.py, check.py, demo.py, bridge.py120 / 49 / 58 / 108harness2-setup, -check, -demo, -bridge.

The console scripts are declared in pyproject.toml: harness2 = harness2.cli.run_sweep:main, plus harness2-judge, -report, -setup, -check, -demo and -bridge, each pointing at the matching cli/*.py:main.

1.2 Package and import diagram

Arrows point from importer to imported. The diagram was built from every import line in harness2/**/*.py at the pinned commit. The dashed edges are imports made inside a function (lazy imports).

flowchart TB
  subgraph CLI ["cli"]
    runsweep["run_sweep.py"]
    clijudge["judge.py"]
    clireport["report.py"]
    common["_common.py"]
    clidemo["demo.py"]
    clicheck["check.py / setup.py / bridge.py"]
  end
  subgraph BENCH ["benchmarks"]
    lab["lab.py"]
    wb["workbuddy.py + wb_render.py"]
    jb["jobbench.py"]
    tasks["tasks.py"]
  end
  subgraph SUB ["substrate"]
    sbase["base.py"]
    soc["opencode.py"]
    scx["codex.py"]
  end
  subgraph CORE ["core (the method)"]
    schedule["schedule.py"]
    recursion["recursion.py"]
    selection["selection.py"]
    library["library.py"]
    context["context.py"]
    prompts["prompts.py"]
    proposer["proposer.py"]
    ledger["ledger.py"]
    spec["spec.py"]
    tree["tree.py"]
    runmark["runmark.py"]
    judge["judge.py"]
    report["report.py"]
  end
  subgraph TOP ["top level"]
    config["config.py"]
    install["install.py"]
    preflight["preflight.py"]
    demo["demo.py"]
  end
  runsweep --> schedule
  runsweep --> proposer
  runsweep --> common
  runsweep -.-> preflight
  common --> library
  common --> schedule
  common --> spec
  common -.-> lab
  common -.-> wb
  common -.-> jb
  clijudge --> judge
  clireport --> report
  clireport --> clijudge
  clidemo --> schedule
  clidemo --> demo
  clijudge --> common
  clireport --> common
  clireport --> schedule
  clicheck --> preflight
  clicheck --> install
  clicheck --> common
  lab --> spec
  lab --> tree
  lab --> runmark
  lab --> sbase
  lab --> soc
  lab --> scx
  wb --> spec
  wb --> tree
  wb --> runmark
  wb --> selection
  wb --> sbase
  wb --> soc
  wb --> scx
  jb --> spec
  jb --> tree
  jb --> runmark
  jb --> install
  jb --> sbase
  jb --> soc
  jb --> scx
  lab --> tasks
  wb --> tasks
  jb --> tasks
  soc --> tree
  soc --> runmark
  soc --> sbase
  scx --> tree
  scx --> runmark
  scx --> sbase
  sbase --> tree
  schedule --> recursion
  schedule --> selection
  schedule --> library
  schedule --> runmark
  recursion --> context
  recursion --> prompts
  recursion --> ledger
  recursion --> proposer
  selection --> context
  selection --> prompts
  selection --> proposer
  library --> context
  library --> tree
  context --> spec
  context --> tree
  prompts --> spec
  prompts --> tree
  proposer --> spec
  spec --> tree
  spec --> config
  report --> judge
  report --> schedule
  judge --> spec
  demo --> spec
  demo --> tree
  demo --> proposer
  demo --> soc
  preflight --> install
  preflight --> soc
  click spec href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/spec.py" "core/spec.py"
  click tree href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/tree.py" "core/tree.py"
  click ledger href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/ledger.py" "core/ledger.py"
  click selection href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/selection.py" "core/selection.py"
  click recursion href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/recursion.py" "core/recursion.py"
  click schedule href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/schedule.py" "core/schedule.py"
  click proposer href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/proposer.py" "core/proposer.py"
  click context href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/context.py" "core/context.py"
  click library href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/library.py" "core/library.py"
  click prompts href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/prompts.py" "core/prompts.py"
  click runmark href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/runmark.py" "core/runmark.py"
  click judge href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/judge.py" "core/judge.py"
  click report href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/report.py" "core/report.py"
  click sbase href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/substrate/base.py" "substrate/base.py"
  click soc href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/substrate/opencode.py" "substrate/opencode.py"
  click scx href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/substrate/codex.py" "substrate/codex.py"
  click lab href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/benchmarks/lab.py" "benchmarks/lab.py"
  click wb href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/benchmarks/workbuddy.py" "benchmarks/workbuddy.py"
  click jb href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/benchmarks/jobbench.py" "benchmarks/jobbench.py"
  click tasks href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/benchmarks/tasks.py" "benchmarks/tasks.py"
  click common href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/cli/_common.py" "cli/_common.py"
  click runsweep href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/cli/run_sweep.py" "cli/run_sweep.py"
  click clijudge href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/cli/judge.py" "cli/judge.py"
  click clireport href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/cli/report.py" "cli/report.py"
  click clidemo href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/cli/demo.py" "cli/demo.py"
  click config href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/config.py" "config.py"
  click demo href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/demo.py" "demo.py"
  click preflight href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/preflight.py" "preflight.py"
  click install href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/install.py" "install.py"

Several modules also import config (core/recursion.py, core/proposer.py, both substrates, every benchmark, several CLIs). Those edges are left out to keep the picture readable. config imports nothing from the package, so it is a leaf.

Reading the diagram from the bottom up:

  • core/tree.py** is the root type module.** It imports nothing from the package. SurfaceSpec lives here rather than in substrate/ so that both core and substrate can depend on it without core ever importing substrate.
  • core/spec.py** depends only on** tree** and** config. Everything else in core depends on spec.
  • substrate/** imports only** core.tree, core.runmark** and** config. It never imports the method modules (recursion, selection, schedule) and never imports a benchmark.
  • benchmarks/** imports** core** value types plus helpers (spec,** tree, runmark, and selection.order_graded_first** in WorkBuddy), the substrates and** install.
  • cli/** sits on top.** It is the only place that picks a concrete benchmark, and it does so lazily inside make_adapter.
  • core/judge.py* is imported only by** core/report.py** (for the** Scored** row type),** cli/judge.py** and the test suite.** No module in the method's run path (schedule, recursion, selection, context, library, proposer, prompts) imports it. The module docstring states it outright: *"Nothing in the method imports this module" (core/judge.py#L15-L19).

2. The three axes and their seams

The docstring of core/spec.py gives the organising principle:

Three axes stay separate -- method (recursion, prompts, selection, library), substrate (the agent framework a solver rollout runs under) and benchmark (a BenchSpec value plus an adapter). ChannelSpec describes the proposer/selector channel, which is independent of both.

So there are really four independent concerns. Each one has its own seam:

AxisOwnsSeam type(s)Where defined
MethodHow harnesses are proposed, composed, chosen and remembered(consumer of the seams below)core/recursion.py, core/selection.py, core/library.py, core/prompts.py, core/schedule.py
BenchmarkTasks, execution, deliverable extraction, gradingBenchmarkAdapter protocol plus a BenchSpec valuecore/spec.py
SubstrateThe agent framework's editable surface, staging, launching, trajectory formatSubstrateLike (what core needs) and Substrate (the full interface)core/spec.py, substrate/base.py
ChannelWhich model the improver/selector talks to, and howChannelSpec, consumed by Proposercore/spec.py, core/proposer.py

The key structural move: the method never sees a substrate directly. It reaches the substrate only through adapter.substrate, and asks it for one thing only, surface(), which returns a SurfaceSpec value. Staging, launching and trajectory rendering all happen inside the adapter's execute and trajectory. The sequence below shows one rollout crossing all the seams. It follows recursion._roll calling LabAdapter.execute.

sequenceDiagram
  participant R as recursion._roll
  participant T as core.tree
  participant A as LabAdapter
  participant S as OpencodeSubstrate
  participant L as launcher _launch_oc
  R->>A: substrate.surface()
  A-->>R: SurfaceSpec (OPENCODE_SURFACE)
  R->>T: write_tree(surface, tree, work_dir/harnesses/label)
  T-->>R: harness_dir
  R->>A: execute(task_id, harness_dir, run_id)
  A->>A: runmark.is_complete? reuse : re-roll
  A->>S: materialise(harness_dir, spec, ctx)
  S-->>A: Install(staging_dir, fingerprint, extras)
  A->>S: execute(task_id, install, [run_id], timeout)
  S->>L: launcher(RunRequest, Install)
  L-->>S: list of run dirs
  S-->>A: list of run dirs
  A->>A: runmark.mark_complete(out)
  A-->>R: run_dir
  R->>A: delivered(run_dir)
  A-->>R: bool
  R->>R: RunView(label, run_id, run_dir, ok, delivered)

Code for each step: _roll, write_tree, LabAdapter.execute, LabAdapter._install, OpencodeSubstrate.materialise, OpencodeSubstrate.execute.

Notice the inversion in the middle. The substrate owns the call shape execute(task_ref, install, run_ids, timeout), but the benchmark supplies the body (the launcher) when it builds the substrate. That is why OpencodeSubstrate.__init__ takes launcher: Launcher | None, and why execute raises if none was given (see section 6).


3. Values versus behaviour

The first question this chapter answers is: which types are values and which hold behaviour? The code is consistent. Configuration and records are dataclasses, many of them frozen=True. Behaviour lives in protocols, their concrete implementations, and the module-level functions in core.

TypeKindFrozen?Holds behaviour?Notes
ChannelSpecdataclassyesvalidation onlyOne model on one channel
BenchPromptsdataclassyesvalidation onlyBenchmark wording for shared prompts
BenchSpecdataclassyesvalidation only"A value, not a code path."
RunViewdataclassnononeOne finished rollout. "Never carries a score."
SurfaceSpecdataclassyesnone (rules are data, logic is in tree.py functions)One agent framework's editable surface
InstalldataclassyesnoneA staged harness
RunRequestdataclassyesnoneOne launch request
Namespacedataclassyesnaming only<bench>-<substrate>-<mode><k>[-s<seed>][-<tag>]
EntrydataclassnononeOne ledger line
Ledgerdataclassnoaccounting and the budget assertionMutable, append-only by convention
SelectiondataclassnononeRecord of one selection
Stage, ModeResult, TaskResult, ContextTree, Reply, Scored, Summarydataclassesnoderived properties and to_dict onlyRecords produced by method stages
Librarydataclassnoyes: file I/OA handle on one on-disk directory
Proposerdataclassnoyes: subprocesses, cache, auditRuns model sessions
BenchmarkAdapterProtocolyes (implemented by adapters)The benchmark seam
SubstrateLike, SubstrateProtocolyes (implemented by substrates)The substrate seam
LabAdapter, WbAdapter, JobBenchAdapter, DemoAdapterplain classesyesSatisfy BenchmarkAdapter structurally; none inherits from it
OpencodeSubstrate, CodexSubstrateplain classesyesSatisfy Substrate structurally

Two design consequences follow:

  1. A benchmark cell** is data plus one object.** A BenchSpec (frozen) plus an adapter instance fully describe a (benchmark, substrate) cell. Building a new cell means writing a new value and a new adapter, never editing method code.
  2. Validation happens at construction. ChannelSpec, BenchPrompts and BenchSpec all validate in __post_init__. A misconfigured cell fails when it is built, before any rollout costs money.

4. core/spec.py: the benchmark seam

4.1 Module constants

NameValuePurpose
ModelIdstr aliasDocuments that a field holds a model id
CHANNELS("claude_cli", "opencode")The only two proposer/selector channels
INSTALL_MODES("workspace_agents_md", "home_dir_jinja", "workspace_config", "codex_root")How a substrate stages a harness. The first three belong to OpenCode, codex_root to Codex.

4.2 ChannelSpec

ChannelSpec is "How to reach one model, on one proposer/selector channel."

FieldTypeDefaultMeaning
kindstr(required)One of CHANNELS
modelModelId(required)Model id. Its format depends on kind (see resolve_channel_model).
effortstr"high"Reasoning effort passed to the session
stepsint120Step budget for the session
timeoutint3600Wall-clock seconds per session

__post_init__ rejects an unknown kind. Because the dataclass is frozen, it is hashable and comparable. _selector_for relies on that equality: when the selector ChannelSpec equals the proposer's, the same Proposer object is reused (which keeps fake proposers such as DemoProposer in place). Otherwise a fresh Proposer is built for the selector channel, with its audit trail under <audit_dir>/selector.

4.3 resolve_channel_model

resolve_channel_model(kind, model, *, effort, steps, timeout) is "The one channel->model mapping; a lone override of either raises."

spec.py#L60-L65

use_default = kind is None and model is None
if (kind is None) != (model is None):
    raise ValueError(
        "channel and model must be overridden TOGETHER (a bare-id/provider-prefixed "
        f"mismatch fails silently downstream); got kind={kind!r}, model={model!r}"
    )

The rules, in order:

  1. Override both or neither. A lone override raises, because the two id formats differ (see rule 5) and a mismatch "fails silently downstream".
  2. No override: kind = config.DEFAULT_PROPOSER_CHANNEL (which is "opencode", config.py#L174) and model = config.PROPOSER_MODEL_BY_CHANNEL[kind] (config.py#L175). Both channels map to IMPROVER_MODEL, which comes from HARNESS2_IMPROVER_MODEL and falls back to HARNESS2_SOLVER_MODEL.
  3. An empty model is allowed only on the default path. It returns a ChannelSpec with model="", so specs can be built before the environment is configured. The error is deferred to Proposer.__post_init__, which raises "Set HARNESS2_IMPROVER_MODEL before starting model sessions". An explicit empty override raises at once.
  4. opencode needs a provider-prefixed id (it must contain /).
  5. claude_cli needs a bare Anthropic id (no /).

All three benchmark spec builders call it with (None, None): lab_spec, spec_for and make_spec.

4.4 BenchPrompts

BenchPrompts is "The benchmark-shaped text the shared prompts interpolate." core/prompts.py holds one shared set of prompts. A benchmark specialises them only through these fields:

FieldDefaultUsed for
work(required)One phrase describing the work (e.g. the demo's "reconcile signed amounts in a transaction ledger")
domain_word"domain"What a grouping unit is called ("practice area", "profession", "role"…)
inv_levers""Hints for the INV (invariant) proposal dimension
dom_levers""Hints for the DOM (domain) proposal dimension
yardstick""What "good" looks like for the work
deliverable_word"deliverable"What the output is called
domain_guidance""Extra domain guidance
selection_checks""What the selector should check
selection_lead""Lead-in text for the selector prompt
selection_conformance"full"Conformance wording for selection
prompt_pack"adapt""adapt" or "tts". Picks between the two prompt families in prompts.py (the _tts_* builders).

How each field is spliced into the prompt text belongs to chapter 06 Budget selection and judging. What matters structurally is the anti-leak check in __post_init__:

spec.py#L111-L126

visible = {
    "work": self.work,
    "inv_levers": self.inv_levers,
    "dom_levers": self.dom_levers,
    "yardstick": self.yardstick,
    "domain_guidance": self.domain_guidance,
    "selection_checks": self.selection_checks,
}
for field_name, value in visible.items():
    low = value.lower()
    for bad in ("benchmark", "evaluation", "grader", "rubric", "score"):
        if bad in low:
            raise ValueError(
                f"BenchPrompts.{field_name} must describe the WORK, not its "
                f"measurement (found {bad!r}). Say what the practitioner needs."
            )

So a benchmark author cannot even build a prompt profile that mentions the benchmark, its evaluation, grader, rubric or score in the six fields models see. This is one of the first lines of the evidence-and-safety story (chapter 05). Note that domain_word, deliverable_word, selection_lead and selection_conformance are not scanned. The code does not say why.

4.5 BenchSpec

BenchSpec is "Static description of one (benchmark, substrate) cell. A value, not a code path."

FieldTypeDefaultMeaning and who reads it
namestr(required)Benchmark name ("lab", "demo"…). The first segment of every namespace.
runs_rootPath(required)Root for runs, the scheduler state (<runs_root>/adapt, runs_root_for) and the library (<runs_root>/library, library_root_for)
install_modestr(required)One of INSTALL_MODES. Read by Substrate.materialise.
promptsBenchPrompts(required)Default prompt profile. An adapter may override it per task with prompts_for.
channelsdict[str, ChannelSpec](required)Must contain "proposer" and "selector"
domain_wordstr"domain"Set by every adapter, but no module reads spec.domain_word at this commit. The prompts read BenchPrompts.domain_word instead.
leak_globstuple[str, ...]()Globs that must never appear in an evidence tree (e.g. LAB ("task.json",), the demo ("reference_answer.txt",)). Enforced by context.assert_no_leak_paths.
allow_subagentsboolFalsePassed to tree.validate by _accept_tree
reserved_scriptsfrozenset[str]frozenset()Script basenames that the runtime provides, so a harness may call them without shipping them. Unioned with tree.RESERVED_SCRIPTS.
proposer_deliverablesstr"contents""contents" or "names": whether proposers see deliverable file contents or only their names (context.py#L144)
state_deliverableboolFalseIf no candidate produced a file deliverable, selection may fall back to showing trajectories (selection.py#L134)
obs_truncint50_000Per-observation truncation when rendering trajectories

__post_init__ (spec.py#L150-L173) enforces five invariants:

  • install_mode is in INSTALL_MODES.
  • proposer_deliverables is "contents" or "names".
  • channels defines both "proposer" and "selector".
  • obs_trunc >= 10_000. The message calls smaller values "the range that fabricates defects": an observation cut too short looks to the improver like a solver failure. context.mission makes the same point to the model (context.py#L534-L537).
  • reserved_scripts holds bare .py basenames, not paths.

4.6 RunView

RunView is "What the method knows about one finished rollout. Never carries a score."

FieldTypeDefaultMeaning
labelstr(required)Stage label: base1, inv1, dom1, agg1, c1…
run_idstr(required)Namespace.run_id(task_id, label), e.g. demo-oc-agg2/ledger-1/base1
run_dirPath(required)Where the adapter left the rollout
okbool(required)The run directory exists (the adapter returned without raising)
deliveredbool(required)adapter.delivered(run_dir): the requested artifact exists
cost_usdfloat0.0Rollout cost, if known
wall_sfloat0.0Wall time
errorstr""Why a rollout is dead

This is the only view the method has of a rollout. With no score field, recursion and selection cannot condition on a grade even by accident. That is the structural half of the "blind selection" claim. The procedural half is context.assert_no_grader_verdicts, which scans every evidence tree for grader output.

4.7 SubstrateLike

SubstrateLike is a @runtime_checkable Protocol with one attribute and one method:

  • name: str: "oc" or "cx". It feeds namespaces. ns refuses an empty substrate name, because otherwise "the two substrates' caches [would] collide on every label".
  • surface() -> SurfaceSpec: the editable c1..c8 tree schema.

It is deliberately a subset of Substrate. Its own docstring calls it "What core needs of a substrate", and Substrate's says "core.spec.SubstrateLike* is the subset core needs."* Defining it in core lets core type adapter.substrate without importing substrate/.

4.8 BenchmarkAdapter

BenchmarkAdapter is "Implement these and the whole method works. Nothing here is method logic." It is a @runtime_checkable Protocol with two attributes and twelve methods.

MemberSignatureContract (from the docstrings)Main callers in core
specBenchSpecThe cell's static descriptioneverywhere
substrateSubstrateLikeProvides name and surface()recursion, schedule, library
tasks(split="all") -> list[str]Task ids for a split. "Stable order -- library state must be reproducible."schedule / CLI
domain_of(task_id) -> str"The grouping unit: practice area / occupation / role / sector."run_task, streams
task_prompt(task_id) -> strThe task as the agent received it, "with criteria and expected output stripped"context.build, run_selection
task_files(task_id) -> Path or NoneOptional source materialcontext.copy_source (via getattr)
execute(task_id, harness_dir, run_id, *, timeout=None) -> PathRun one rollout, "idempotently by run_id". harness_dir=None means the unmodified base harness.recursion._roll, schedule._roll_bases
delivered(run_dir) -> boolDid the rollout produce the requested artifact?_roll, _roll_bases
trajectory(run_dir, *, obs_trunc) -> strReadable trajectory in the shared [ACTION]/[OBSERVATION]/[ERROR] grammarcontext
deliverable_files(run_dir) -> dict[str, str]Optional. Filename to extracted text.context (via getattr)
deliverable_text(run_dir) -> strThe artifact as text, "extracted from agent-authored bytes only"selection._render_pool
base_harness() -> PathDirectory holding the unmodified c1..c8 treerun_task, reflect_adoption
validate_tree(tree) -> NoneBenchmark-specific invariants. Raise ValueError listing all problems in actionable prose, because "the message is fed back verbatim as retry feedback."recursion._accept_tree
score(run_dir, task_id) -> dict or NoneJudge one run. None means the judge failed: "not a zero and is never cached as one. Called only by core.judge, only after selection committed."only core.judge.judge_one

The score contract is the boundary between method and measurement. Everything above score in the table is answer-free. score is the one place a grade enters the program, and only core/judge.py calls it. judge_one wraps it: an exception or an empty result becomes Scored.error, never a zero.

Optional hooks (duck-typed with getattr)

Beyond the protocol, core probes for extra adapter methods with getattr(adapter, name, None). They are optional: core falls back to defaults when they are missing. Every probe at this commit:

HookProbed atEffectImplemented by
prompts_for(task_id) -> BenchPromptsresolve_promptsPer-task prompt profile. A non-BenchPrompts return raises TypeError.Lab, WorkBuddy, JobBench
reference_domain_order() -> list[str]schedule.py#L175Reference ordering of domains for the task streamLab, WorkBuddy, JobBench
artifacts_intact(run_dir)schedule.py#L404Checked when resuming a cached taskLab, JobBench
execute_many(task_id, None, run_ids)schedule.py#L437Batch the k base rollouts nativelyWorkBuddy
deliverable_listing(run_dir)context.py#L339Names-only deliverable viewWorkBuddy
deliverable_images(run_dir)context.py#L380Images copied into evidenceLab, JobBench
deliverable_assets(run_dir)context.py#L261, selection.py#L195Rendered evidence files per candidate. The adapter names them explicitly, so grader output in the run dir is never swept in.WorkBuddy
deliverable_assets_note, deliverable_assets_line, deliverable_assets_reference_onlyselection.py#L170, #L177, #L308Override the wording about assets in the selector promptnone of the shipped adapters (defaults are used)
task_files, deliverable_filescontext.py#L193, #L236Protocol members, but marked optional and probed the same wayall four

preflight() is not probed by core. It exists on all three real adapters and is called by the CLI and preflight.py.

4.9 resolve_prompts

resolve_prompts(adapter, task_id) returns adapter.prompts_for(task_id) when that method exists, else adapter.spec.prompts, and checks the type. Recursion, selection and reflection all call it, so per-task prompt profiles (e.g. LAB's practice area plus work type in LabAdapter.prompts_for) reach every prompt the same way.


5. core/tree.py: the component tree and SurfaceSpec

The module docstring (tree.py#L15-L21) sets the pattern. "Everything that differs between the two agent frameworks is data on SurfaceSpec… the shared logic exists once. The edit contract is omitted-is-inherited: a proposal emits only what it changes."

5.1 Types and constants

  • Tree is dict[str, str], mapping a relative path to file content. Every harness in memory is a Tree.
  • RESERVED_SCRIPTS is frozenset({"read_doc.py", "pack.py", "unpack.py"}). Runtime-provided helpers a harness may invoke without shipping them.
  • _NAME is [a-z0-9][a-z0-9_\-]*, the naming rule for skills, sub-agents and plugins.

5.2 SurfaceSpec

SurfaceSpec is "The editable surface of one agent framework: paths, caps and per-surface rules." The table lists each field with the two concrete values from OPENCODE_SURFACE and CODEX_SURFACE.

FieldMeaningOpenCodeCodex
nameSurface name"opencode""codex"
always_onFiles injected into every turnsystemprompt.md, guardrails.md, LongTermMEMORY.md, tool_descriptions/tool_guidance.mdAGENTS.md
always_on_maxCap on total always-on chars24_0008_000
always_on_cap_reasonProse appended to the over-cap erroroutput-budget competitioninjected every turn, silently truncated at 32,768 bytes
skill_dirSkills rootskills.agents/skills
skill_name_ruleFrontmatter name: policy"must_match""overrides_dir"
trigger_hintFix-it text for an untriggered skill"add a trigger line … to systemprompt.md or guardrails.md""add a trigger line … to AGENTS.md"
autotrigger_fileWhere autotrigger appends linesguardrails.mdAGENTS.md
trigger_lineTemplate for a trigger line- When {desc}, load the {name} skill.- When {desc}, read .agents/skills/{name}/SKILL.md and follow it.
subagent_requires_modeSub-agent needs mode: subagentTrueFalse
plugin_dirPlugin directory (c8)"plugin"None
hooks_fileHooks file (c8)None.codex/hooks.json
hook_eventsAllowed hook eventsfrozenset()10 events (PreToolUse, PostToolUse, PermissionRequest, PreCompact, PostCompact, SessionStart, SubagentStart, SubagentStop, UserPromptSubmit, Stop)
compile_skill_scriptsAlso compile .py under skillsFalseTrue
reject_empty_always_onEmpty always-on text is an errorFalseTrue
stubs(path, body) README stubs describing c5..c8scripts/, sub_agents/, plugin/, skills/ READMEsscripts/, sub_agents/, .codex/, .agents/skills/ READMEs

What the c1..c8 components mean is chapter 04 The harness tree. Structurally, the point is that substrate differences are data. tree.py contains no if surface.name == "codex" branch anywhere. It reads the fields.

5.3 Path rules

  • _res and _rx compile, once per surface (lru_cache(maxsize=8)), the regexes for skill (<skill_dir>/<name>/SKILL.md), skill_script, subagent (sub_agents/<name>/AGENT.md), script (scripts/<lowercase>.py), script_ref (a python3 …scripts/x.py invocation inside text) and, when plugin_dir is set, plugin (<plugin_dir>/<name>.ts).
  • normalize_rel(rel) converts backslashes, strips leading ./ and outer /, and raises on absolute paths or any .. segment: "path escapes the harness". write_tree calls it on every key before joining it to the destination.
  • is_allowed(surface, rel) is a predicate that "never raises". A path is on the surface if it is an always-on file, the hooks file, or matches one of the skill, skill-script, sub-agent, script or plugin patterns.

tree.py#L92-L110

def is_allowed(surface: SurfaceSpec, rel: str) -> bool:
    """Whether this path is on the editable surface. A predicate: it never raises."""
    try:
        rel = normalize_rel(rel)
    except ValueError:
        return False
    if rel in surface.always_on:
        return True
    if surface.hooks_file is not None and rel == surface.hooks_file:
        return True
    r = _rx(surface)
    if (
        r["skill"].fullmatch(rel)
        or r["skill_script"].fullmatch(rel)
        or r["subagent"].fullmatch(rel)
        or r["script"].fullmatch(rel)
    ):
        return True
    return "plugin" in r and bool(r["plugin"].fullmatch(rel))

5.4 Read, write, inspect

  • read_tree(surface, d, *, rejected=None) returns every on-surface file under d, skipping __pycache__ and stub READMEs. Pass a list as rejected to collect the paths that exist but are off the surface. _run_stage uses that list to tell the proposer which files were ignored (recursion.py#L211-L224). A missing directory returns {} instead of raising.
  • is_tree(surface, d) is true if at least one always-on file exists. Both substrates' materialise use it as a guard.
  • write_tree(surface, tree, dest, *, stubs=True) materialises an exact tree. It first deletes every on-surface file (and stub) already at dest, so a file removed from the Tree is really removed. Off-surface files are preserved. It creates empty always-on files if the tree lacks them, writes the tree, and adds stubs if asked. Library.add passes stubs=False, so library entries contain only real components.
  • always_on_text(surface, tree) joins the always-on files with blank lines. This is the text the cap applies to.
  • autotrigger(surface, tree, *, always_on_max=None) is a repair, not a check. For each skill not named in the always-on text, it builds a trigger line from the skill's description: frontmatter and surface.trigger_line, then appends it to surface.autotrigger_file, unless that would exceed the cap. It mutates tree in place and returns the names it fixed. _accept_tree calls it before validation.
  • diff_summary(surface, before, after) returns added / removed / modified path lists and always_on_char_delta. "Kept base" means all three lists are empty (recursion.py#L119-L120, _tree_is_base).
  • diversity(trees) computes the pairwise line-Jaccard over candidate trees (lines keyed rel:line), plus the count of distinct trees, "so collapse to one body is visible". run_parallel stores it as ModeResult.similarity for the k aggregates.

5.5 validate

validate(surface, tree, *, allow_subagents=True, always_on_max=None, reserved_scripts=RESERVED_SCRIPTS) "Raise[s] ValueError listing every problem; return[s] a census of the validated tree." It collects every problem before raising and joins them with " | ". One rejected proposal therefore gets complete feedback on its retry, not one error at a time.

The checks, in order:

  1. Always-on text over the cap, with always_on_cap_reason appended.
  2. If reject_empty_always_on (Codex): an empty always-on body ("a harness that does not exist").
  3. _check_skills: every skill needs description: (else it is "dropped SILENTLY at load"). Under must_match, name: must equal the directory. Under overrides_dir, a mismatching name: is flagged because it would break trigger lines. Every skill must be named in the always-on text, else "an untriggered skill is dead weight".
  4. _check_subagents: sub-agents present when allow_subagents is false is an error. mode: subagent is required when subagent_requires_mode. description: is required.
  5. _py_compiles on every scripts/*.py, and on skill scripts when compile_skill_scripts. It calls the built-in compile() only. Nothing is executed.
  6. check_hooks if the hooks file is present: valid JSON, a non-empty top-level hooks object, known PascalCase events, list-of-entries shape, type: "command" only, and a non-empty command. The docstring explains why: a malformed hooks file "never fires rather than erroring", so the shape has to be enforced where the proposer can see the error.
  7. _check_script_refs: every python3 scripts/x.py invoked from a skill or always-on file must exist in the tree, unless its basename is reserved.

On success it returns a census: always_on_chars, n_files, skills, sub_agents, scripts, plus plugins or hooks depending on the surface.


6. substrate/base.py: the substrate seam

substrate/base.py is 83 lines and depends only on core.tree.SurfaceSpec.

base.py#L26-L44

@dataclass(frozen=True)
class Install:
    """A materialised harness ready for a launcher; `fingerprint` digests the SOURCE tree."""

    staging_dir: Path
    fingerprint: str
    extras: Mapping[str, str] = field(default_factory=dict)


@dataclass(frozen=True)
class RunRequest:
    """One execution request handed to a benchmark-supplied launcher body."""

    task_ref: str
    run_ids: tuple[str, ...]
    timeout: int


Launcher = Callable[[RunRequest, Install], list[Path]]

6.1 Install

Install:

FieldMeaning
staging_dirA temporary directory holding the substrate-native files
fingerprintDigest of the source tree, from runmark.fingerprint: first 16 hex chars of a SHA-1 over sorted relative paths and bytes, ignoring __pycache__, .pyc, .DS_Store and the completion marker. None (base) digests to "base".
extrasFree-form strings. Substrates put install_mode here. Adapters add their own keys (LAB adds "harness": "base" or "staged", which decides whether the staging dir is deleted after the run, lab.py#L609-L610).

Fingerprinting the source tree, not the staged output, means a rollout's identity does not depend on how a substrate renders it. Combined with runmark.is_complete, adapters reuse a cached rollout only if it was produced by the same harness. This is what makes BenchmarkAdapter.execute "idempotent by run_id" in practice.

6.2 RunRequest and Launcher

RunRequest has task_ref, run_ids (a tuple, so the frozen value stays hashable) and timeout. Launcher is the type of the benchmark-supplied body: (RunRequest, Install) -> list[Path], one run dir per run id.

6.3 Substrate

Substrate is the full, @runtime_checkable protocol:

MethodContract
name: str"oc" / "cx"
surface()The SurfaceSpec
materialise(tree_dir, spec, ctx)Stage the tree as native files per spec.install_mode. ctx carries benchmark-owned values: {{token}} slots and file:<rel> entries for integration files.
execute(task_ref, install, run_ids, timeout)Run via the launcher. One run dir per run id.
read_trajectory(stream_path, *, obs_trunc, max_chars, args_trunc=400)Render the native capture format into the shared grammar. args_trunc defaults to 400 for OpenCode and 2000 for Codex.
preflight()Health checks. Raise RuntimeError with the remediation command. "Never starts any part of the model chain."

The read_trajectory docstring says obs_trunc and max_chars "are carried on the cell's BenchSpec". At this commit BenchSpec has obs_trunc but no max_chars field. Each adapter passes its own literal: WorkBuddy TRAJ_MAX_CHARS = 2_000_000 (workbuddy.py#L872), JobBench 8_000_000 (jobbench.py#L880), LAB 10**9 (lab.py#L766).

6.4 The two implementations

OpencodeSubstrateCodexSubstrate
name"oc""cx"
surface()OPENCODE_SURFACECODEX_SURFACE
Install modesworkspace_agents_md, home_dir_jinja, workspace_configcodex_root only
materialiseCopies the tree, writes file: ctx entries, and for home_dir_jinja composes a prompt.j2 (checked by assert_jinja_safe)Copies only AGENTS.md, .agents, scripts, .codex. No render step, "because the CLI injects AGENTS.md itself". Non-file: ctx goes into extras.
Reference argv/envrun_argv, run_env (permission fence, output-token ceiling)exec_argv, exec_env
preflightOpenCode checkout and bun presentEndpoint env vars, URL shape, binary present, TCP reachability

Both execute methods are identical in shape:

opencode.py#L287-L298

def execute(
    self, task_ref: str, install: Install, run_ids: list[str], timeout: int
) -> list[Path]:
    if self._launcher is None:
        raise RuntimeError(
            "OpencodeSubstrate was constructed without a launcher body; the benchmark "
            "adapter must supply one (it owns runner choice, env wiring and read-back "
            "locations)"
        )
    return self._launcher(
        RunRequest(task_ref=task_ref, run_ids=tuple(run_ids), timeout=int(timeout)), install
    )

The adapter wires its own bound method in as the launcher when it builds the substrate:

lab.py#L520-L527

self.spec = lab_spec(substrate)
if substrate == "oc":
    self.substrate = OpencodeSubstrate(launcher=self._launch_oc)
    self.model = model or config.SOLVER_MODEL_OC
    self._results_root = config.RUNS / "lab" / "results" / "oc"
else:
    self.substrate = CodexSubstrate(launcher=self._launch_cx)
    self.model = model or config.SOLVER_MODEL_CX

That is the cleanest statement of the seam. The substrate knows how to stage files for its framework, and the benchmark knows which runner, which environment and where results land. Neither imports the other's internals: substrate modules import no benchmark, and the benchmark uses only the substrate's public constructor and methods. The detail of each executor is chapter 07 Executors and benchmarks.


7. core/ledger.py: budget accounting as a type

The module docstring (ledger.py#L15-L19) gives the budget: R = 4k rollouts and P = 3k improvement stages. "Base executions count toward each mode. Retries are separate invocation counts; selection and reflection are recorded outside this ledger."

NameWhat it is
expected_rollouts(k)4*k: "k shared base + 2k candidates + k finals"
expected_sessions(k)3*k: "k INV + k DOM + k compositions (parallel: agg1..aggk; sequential: c1..ck)"
BudgetErrorSubclass of AssertionError: the ledger missed the identity
Entrykind ("rollout"/"session"), label, usd=0.0, cached=False, attempts=1
Ledgermode, task_id, k, entries: list[Entry]

Ledger methods:

Method/propertyBehaviour
rollout(label, usd=0.0, *, cached=False)Append a rollout entry
session(label, usd=0.0, *, cached=False, attempts=1)Append a session entry. attempts keeps retries visible without counting them as extra sessions.
R / PCount of rollout / session entries
usd, rollout_usd, session_usdCost sums rounded to 6 places
labels(kind)Labels of one kind, in recorded order
assert_identity()Raise BudgetError unless R=4k, P=3k and no duplicate label
to_dict()Serialisable record, including session_attempts and both expected values

ledger.py#L89-L100

def assert_identity(self) -> None:
    """Raise unless the recorded entries show exactly R=4k and P=3k with no duplicate
    label. Duplicates are checked before the counts, because a duplicate can satisfy them.
    """
    want_r, want_p = expected_rollouts(self.k), expected_sessions(self.k)
    rl, sl = self.labels("rollout"), self.labels("session")
    dupes = sorted(
        {label for label in rl if rl.count(label) > 1}
        | {label for label in sl if sl.count(label) > 1}
    )
    if not dupes and self.R == want_r and self.P == want_p:
        return

Why it is shaped this way:

  • Recording, not budgeting. The ledger does not stop spending. It records what happened, and both recursion modes call assert_identity() at the very end (recursion.py#L448, #L565). A mode that cut a corner (or ran extra work) fails loudly, so the two modes are always compared at an identical budget.
  • Duplicates first. Two stages writing the same run id would overwrite each other while still producing the right count. The error message spells this out: "a candidate the ledger counts was never actually produced".
  • Dead rollouts count. _roll records a rollout even when execute raised. Fallback stages are still recorded as sessions. Budget spent is budget spent.
  • Deterministic order under concurrency. _roll_many and _record_sessions write ledger entries after a thread pool drains, in the caller's order, "so a concurrent run records the same sequence of labels as a serial one".

8. core/selection.py: blind choice and reflection

The module docstring: "Choose an existing anonymized final output, then reflect on its refinement evidence." The full selection flow is chapter 06. Here the focus is on the types and the structural guarantees.

8.1 Module-level names

NamePurpose
_CHOICE, _WHY, _EXP, _EDITSRegexes that parse a selector reply: a fenced choice block, <why>, <experience>, <edits>
order_graded_first(items, graded)Stable sort, graded items first, nothing discarded. Not used inside core. WorkBuddy uses it to rank trial directories (workbuddy.py#L736).
SelectionThe record of one selection (table below)
assign_ids(labels, *, task_id, k, mode)Anonymous id to label, by canonical sort and then a shuffle seeded from the first 12 hex digits of a SHA-1 over the task id, k and mode (joined by vertical bars). The sweep's --seed plays no part
_anon_id(index)Spreadsheet-style ids: A..Z, AA..AZ, BA…
_is_base(label)label.startswith("base")
selection_pool(mode, k, rollouts, base)"The one home for selection-pool construction: exactly the k finals of mode."
_render_poolDrops non-answers (not ok, not delivered, render failure, empty text) before anonymising, and records each in dropped. With spec.state_deliverable, falls back to trajectories as evidence.
_assets_note, _assets_line, _evidence_blockWording about rendered assets (adapter-overridable). The default texts are _DEFAULT_ASSETS_NOTE and _DEFAULT_ASSETS_LINE.
_write_candidatesOne untrimmed <anon>.md per candidate, plus explicitly named asset files
_fallbackNeutral stand-in after a selector failure: a delivered base, else (finals pool) the first final by sorted label
_persistWrites selection.json. A disk failure is recorded on the Selection instead of being raised.
run_selectionThe selector session
trivial_selectionk=1: deliver the single final without a selector session
reflect_adoptionBuilds a reflection evidence tree after adoption and distils a lesson
_distilThe experience session. Returns {cue, lesson, evidence, confidence[, edits]} or {}.

8.2 Selection fields

FieldDefaultMeaning
task_id, mode, k(required)Identity
chosen_id""Anonymous id chosen ("A"…)
chosen_label""The real label (agg1, c2…)
chosen_run_id""Run id of the chosen rollout
chosen_is_baseFalseWhether the chosen label is a base
why""Selector's reasoning (up to 2000 chars)
id_map{}Anonymous id to label
dropped[]Candidates removed before anonymising, with causes
n_candidates0Pool size after drops
okFalseThe selector made a valid, parsed choice. A fallback pick leaves it False
fallbackFalseThe neutral fallback was used
error""Failure detail
usd0.0Cost of selection plus reflection
experience{}Distilled lesson, if any
trivialFalsek=1 path
evidence"deliverable""deliverable" or "trajectory"

8.3 The pool is the finals, by construction

selection.py#L105-L111

del base
labels = (
    [f"agg{i}" for i in range(1, int(k) + 1)]
    if mode == "agg"
    else [f"c{t}" for t in range(1, int(k) + 1)]
)
return {lbl: rollouts[lbl] for lbl in labels if lbl in rollouts}

base is accepted "and deliberately unused", so the exclusion is visible at the call site. Neither base rollouts nor intermediate candidates (inv*, dom*) can be selected. The docstring calls this "no abstention by construction".

8.4 Failure semantics

run_selection docstring: "A selector failure falls back to a delivered base and never raises; an adapter bug does raise, because it is a bug and not a selection outcome." In the code, only ProposerError and ValueError (no choice block, or a choice outside the id map) are caught, around the prop.call (selection.py#L332-L350). An exception from adapter.task_prompt or context.copy_source propagates. _render_pool does catch exceptions from deliverable_text and trajectory, but records them as drops.

One consequence of the finals-only pool: _fallback looks for a delivered base first, but selection_pool never puts bases in the pool. With pool_kind="finals" (which _run_one_mode always passes), the fallback in practice chooses sorted(pool)[0], the lowest-sorted final. _fallback never sets ok, so that selection stays ok=False: the task ends partial, and the pick is not reflected, not added to the library and not judged (chapter 06).

Before the selector session starts, context.assert_no_grader_verdicts(root) scans the selection directory for grader markers such as "pass_rate" or deterministic_eval_summary (context.py#L419-L427).


9. Neighbouring types the seams depend on

These live outside the five files in the brief, but the class diagrams below need them.

9.1 Proposer, Reply, ProposerError (core/proposer.py)

  • Proposer, "Runs agentic sessions on one channel, with an audit trail and a cache." Fields: spec: ChannelSpec, audit_dir=None, attempts=2, project and region (Vertex, from config) and a private _calls: list[Reply]. env() strips Anthropic credentials and sets Vertex variables per channel. call(mission, context_dir, label, *, system, timeout, max_attempts, harvest) copies context_dir into a fresh temp dir as context/ and runs the channel CLI. It retries with a 10·(n−1) s back-off. With harvest set (e.g. "context/harness"), it copies that subpath out before the temp dir is deleted, which is how an edited tree comes back. Results are cached by a fingerprint of label, mission, system prompt and context (_call_fingerprint).
  • Reply: label, text, ok, cost_usd, wall_s, attempts, cached, channel, error, audit, workspace. __bool__ returns ok.
  • ProposerError: "Never swallowed into an empty string." It carries attempts and cost_usd, so callers still bill failed sessions (see sel.usd += exc.cost_usd).

Proposer is a dataclass, so it can be subclassed. DemoProposer overrides only call.

9.2 Namespace and ns (core/schedule.py)

The module docstring: "ns()* is the only function that builds a run namespace;* Namespace* is the only thing that globs over one. Scheme:* <bench>-<substrate>-<mode><k>[-s<seed>][-<tag>]."

  • Namespace (frozen): benchmark, substrate, mode, k, seed=None, tag="". __str__ delegates to ns. run_id(task_id, label) returns f"{self}/{task_id}/{label}". task_dir(root, task_id) returns root/<ns>/<task_id with / → __>. glob(...) returns the two patterns that match bare, seeded and tagged forms.
  • ns(...) validates the mode (must be in MODES = ("agg", "seq")), a non-empty substrate and the tag (validate_tag rejects tags that look like a seed segment such as s3, so seeded and tagged namespaces can never collide).
  • mode_token maps parallel/sequential to the on-disk tokens. MODE_NAMES maps back.

Run ids are what adapters cache on, so a single naming function means two settings can never collide on disk. The substrate name is in every run id, so OpenCode and Codex rollouts never share a cache entry.

9.3 Library (core/library.py)

Library is "One (run, domain) library: read before each task, appended after." Fields: root, domain, surface: SurfaceSpec. It needs the surface because entries are stored as real component trees (read_tree/write_tree). Its methods are dir, index, entry_dir, exists, context_dir (None until the domain has at least one EXPERIENCE.md), entries, tree_of, forget, retain, add (refuses an entry without cue, lesson and evidence, and scans for grader verdicts before writing) and rebuild_index. How entries are written, trimmed and replayed is covered in chapters 03 and 06.

9.4 Records: ContextTree, Stage, ModeResult, TaskResult, Scored, Summary

  • ContextTree is what context.build actually wrote: root, rollouts, deliverables, harness file count, library flag, pool, failures. Its layout property lists only what exists, for mission().
  • Stage: one improvement stage: label, dim (INV/DOM/AGG/COMPOSE), tree, kept_base, fell_back, error, usd, attempts, cached, notes.
  • ModeResult: one mode on one task: stages, trees, rollouts: dict[str, RunView], finals, ledger: Ledger, similarity, errors. ok means finals exist and there are no errors.
  • TaskResult: the summary.json record for one task across modes.
  • Scored: one judged selection. graded means "the judge succeeded, not whether the run did well". frac is None when ungraded.
  • Summary: the aggregate one mode reports.

10. Class diagrams

10.1 The seam types

The types from the brief and how they depend on each other. Protocols are marked <<Protocol>>. Dashed realisation arrows mean "satisfies structurally": no class inherits from a protocol.

classDiagram
  direction LR
  class BenchmarkAdapter {
    <<Protocol>>
    +spec: BenchSpec
    +substrate: SubstrateLike
    +tasks(split) list
    +domain_of(task_id) str
    +task_prompt(task_id) str
    +task_files(task_id) Path
    +execute(task_id, harness_dir, run_id) Path
    +delivered(run_dir) bool
    +trajectory(run_dir, obs_trunc) str
    +deliverable_files(run_dir) dict
    +deliverable_text(run_dir) str
    +base_harness() Path
    +validate_tree(tree) None
    +score(run_dir, task_id) dict
  }
  class BenchSpec {
    <<frozen>>
    +name: str
    +runs_root: Path
    +install_mode: str
    +prompts: BenchPrompts
    +channels: dict
    +leak_globs: tuple
    +allow_subagents: bool
    +reserved_scripts: frozenset
    +proposer_deliverables: str
    +state_deliverable: bool
    +obs_trunc: int
  }
  class BenchPrompts {
    <<frozen>>
    +work: str
    +domain_word: str
    +inv_levers: str
    +dom_levers: str
    +yardstick: str
    +selection_checks: str
    +prompt_pack: str
  }
  class ChannelSpec {
    <<frozen>>
    +kind: str
    +model: str
    +effort: str
    +steps: int
    +timeout: int
  }
  class RunView {
    +label: str
    +run_id: str
    +run_dir: Path
    +ok: bool
    +delivered: bool
    +cost_usd: float
    +error: str
  }
  class SubstrateLike {
    <<Protocol>>
    +name: str
    +surface() SurfaceSpec
  }
  class Substrate {
    <<Protocol>>
    +materialise(tree_dir, spec, ctx) Install
    +execute(task_ref, install, run_ids, timeout) list
    +read_trajectory(stream_path, obs_trunc, max_chars) str
    +preflight() None
  }
  class SurfaceSpec {
    <<frozen>>
    +name: str
    +always_on: tuple
    +always_on_max: int
    +skill_dir: str
    +plugin_dir: str
    +hooks_file: str
    +stubs: tuple
  }
  class Install {
    <<frozen>>
    +staging_dir: Path
    +fingerprint: str
    +extras: Mapping
  }
  class RunRequest {
    <<frozen>>
    +task_ref: str
    +run_ids: tuple
    +timeout: int
  }
  class OpencodeSubstrate {
    +name = oc
    -_launcher: Launcher
  }
  class CodexSubstrate {
    +name = cx
    -_launcher: Launcher
  }
  class Ledger {
    +mode: str
    +task_id: str
    +k: int
    +entries: list
    +rollout(label, usd)
    +session(label, usd, attempts)
    +R() int
    +P() int
    +assert_identity()
  }
  class Entry {
    +kind: str
    +label: str
    +usd: float
    +cached: bool
    +attempts: int
  }
  class Proposer {
    +spec: ChannelSpec
    +audit_dir: Path
    +attempts: int
    +call(mission, context_dir, label) Reply
  }
  class Reply {
    +label: str
    +text: str
    +ok: bool
    +cost_usd: float
    +workspace: Path
  }
  class Namespace {
    <<frozen>>
    +benchmark: str
    +substrate: str
    +mode: str
    +k: int
    +seed: int
    +tag: str
    +run_id(task_id, label) str
    +task_dir(root, task_id) Path
  }
  class LabAdapter
  class WbAdapter
  class JobBenchAdapter
  class DemoAdapter
  BenchmarkAdapter --> BenchSpec : spec
  BenchmarkAdapter --> SubstrateLike : substrate
  BenchmarkAdapter ..> RunView : run_dir becomes
  BenchSpec *-- BenchPrompts : prompts
  BenchSpec *-- ChannelSpec : channels
  SubstrateLike <|-- Substrate : superset
  SubstrateLike ..> SurfaceSpec : surface()
  Substrate ..> Install : materialise
  Substrate ..> RunRequest : execute
  OpencodeSubstrate ..|> Substrate
  CodexSubstrate ..|> Substrate
  LabAdapter ..|> BenchmarkAdapter
  WbAdapter ..|> BenchmarkAdapter
  JobBenchAdapter ..|> BenchmarkAdapter
  DemoAdapter ..|> BenchmarkAdapter
  LabAdapter --> OpencodeSubstrate : launcher
  LabAdapter --> CodexSubstrate : launcher
  Ledger *-- Entry
  Proposer --> ChannelSpec : spec
  Proposer ..> Reply : call
  Namespace ..> RunView : run_id
  click BenchmarkAdapter href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/spec.py#L200-L250" "BenchmarkAdapter"
  click BenchSpec href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/spec.py#L132-L173" "BenchSpec"
  click BenchPrompts href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/spec.py#L89-L126" "BenchPrompts"
  click ChannelSpec href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/spec.py#L36-L48" "ChannelSpec"
  click RunView href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/spec.py#L176-L187" "RunView"
  click SubstrateLike href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/spec.py#L190-L197" "SubstrateLike"
  click Substrate href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/substrate/base.py#L47-L80" "Substrate"
  click SurfaceSpec href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/tree.py#L40-L60" "SurfaceSpec"
  click Install href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/substrate/base.py#L26-L32" "Install"
  click RunRequest href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/substrate/base.py#L35-L41" "RunRequest"
  click OpencodeSubstrate href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/substrate/opencode.py#L173-L320" "OpencodeSubstrate"
  click CodexSubstrate href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/substrate/codex.py#L109-L228" "CodexSubstrate"
  click Ledger href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/ledger.py#L49-L132" "Ledger"
  click Entry href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/ledger.py#L40-L46" "Entry"
  click Proposer href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/proposer.py#L90-L387" "Proposer"
  click Reply href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/proposer.py#L70-L87" "Reply"
  click Namespace href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/schedule.py#L84-L108" "Namespace"
  click LabAdapter href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/benchmarks/lab.py#L498-L1002" "LabAdapter"
  click WbAdapter href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/benchmarks/workbuddy.py#L671-L1004" "WbAdapter"
  click JobBenchAdapter href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/benchmarks/jobbench.py#L622-L1162" "JobBenchAdapter"
  click DemoAdapter href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/demo.py#L41-L117" "DemoAdapter"

10.2 The method's run-time records

What the method produces while it runs one task, and which record owns which.

classDiagram
  direction TB
  class TaskResult {
    +task_id: str
    +domain: str
    +k: int
    +status: str
    +modes: dict
    +selections: dict
    +base: list
  }
  class ModeResult {
    +mode: str
    +k: int
    +stages: list
    +trees: dict
    +rollouts: dict
    +finals: list
    +ledger: Ledger
    +similarity: dict
    +ok() bool
  }
  class Stage {
    +label: str
    +dim: str
    +tree: dict
    +kept_base: bool
    +fell_back: bool
    +notes: str
  }
  class Ledger {
    +entries: list
    +assert_identity()
  }
  class RunView {
    +label: str
    +ok: bool
    +delivered: bool
  }
  class Selection {
    +chosen_id: str
    +chosen_label: str
    +id_map: dict
    +dropped: list
    +fallback: bool
    +experience: dict
  }
  class ContextTree {
    +root: Path
    +rollouts: list
    +pool: list
    +layout() str
  }
  class Library {
    +root: Path
    +domain: str
    +surface: SurfaceSpec
    +context_dir() Path
    +add(task_id, harness)
  }
  class Scored {
    +label: str
    +n_passed: int
    +n_criteria: int
    +graded() bool
  }
  class Summary {
    +mode: str
    +k: int
    +score: float
  }
  TaskResult ..> ModeResult : modes = to_dict()
  TaskResult ..> Selection : selections = to_dict()
  ModeResult *-- Stage
  ModeResult *-- Ledger
  ModeResult o-- RunView : rollouts
  Selection ..> RunView : picks from finals
  Stage ..> ContextTree : built per stage
  Selection ..> Library : experience added
  Scored ..> Selection : reads selection.json
  Summary ..> Scored : aggregates
  click TaskResult href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/schedule.py#L239-L262" "TaskResult"
  click ModeResult href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/recursion.py#L58-L105" "ModeResult"
  click Stage href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/recursion.py#L41-L55" "Stage"
  click Ledger href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/ledger.py#L49-L132" "Ledger"
  click RunView href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/spec.py#L176-L187" "RunView"
  click Selection href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/selection.py#L46-L68" "Selection"
  click ContextTree href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/context.py#L42-L102" "ContextTree"
  click Library href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/library.py#L57-L181" "Library"
  click Scored href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/judge.py#L32-L53" "Scored"
  click Summary href "https://github.com/google-research/harness2/blob/1cfe254e8d25b15f37048c3730134686abe60335/harness2/core/report.py#L31-L65" "Summary"

Scored and Summary sit at the far end. They are produced by a separate process (harness2-judge, harness2-report) that reads selection.json from disk after the run. That boundary is a file, not an import.


11. Seams and import rules

11.1 Where the seams are

SeamInterfaceOne sideOther sideWhat crosses
Method ↔ benchmarkBenchmarkAdapter + BenchSpeccore/*benchmarks/*, demo.pyTask ids, prompts, run dirs, text, RunView
Method ↔ substrateSubstrateLike.surface() → SurfaceSpeccore/tree.py, core/recursion.py, core/library.pysubstrate/*One frozen value
Benchmark ↔ substrateSubstrate + Install + RunRequest + Launcherbenchmarks/*substrate/*Staged dirs, fingerprints, launch requests. The benchmark injects the launcher.
Method ↔ modelChannelSpec → Proposer.call → Replycore/recursion.py, core/selection.pythe channel CLIsMission text, a context dir, harvested files
Method ↔ measurementBenchmarkAdapter.score via core/judge.pycli/judge.pyadaptersScores, after selection, through selection.json on disk
CLI ↔ everythingmake_adaptercli/*benchmarks/*(--bench, --subset, --substrate) → adapter

11.2 What may never import what

The code states these as rules in docstrings, and the import graph in section 1.2 confirms each one at this commit:

  1. core* never imports** substrate, benchmarks** or** cli. Checked: no `core/.pyhas such an import.SubstrateLikeandSurfaceSpecexist incore` precisely so this holds.
  2. No method module imports core/judge.py. Its importers are core/report.py, cli/judge.py and tests/test_workflow.py. The method can't read a score because it never imports the code that produces one.
  3. substrate** never imports** benchmarks. It imports core.tree, core.runmark and config only. Benchmark specifics reach it as a launcher callable and ctx mappings.
  4. benchmarks** never import method logic.** They use core value types and utilities (spec, tree, runmark, selection.order_graded_first), never recursion, schedule, context or prompts.
  5. Only the CLI chooses a benchmark, lazily, in make_adapter. So import harness2.cli._common does not pull in all three benchmark modules.
  6. config** imports nothing internal.** Every layer can read settings without creating a cycle.

The repo has no import-linter and no test that enforces these rules mechanically. They hold because of how the modules are written. The test suite (tests/test_workflow.py) imports core.judge directly, which is fine for a test.

11.3 Why it matters

The paper's comparisons depend on these boundaries. "Blind selection" holds because the selection code cannot reach a score. "Identical budget" holds because both modes record into the same Ledger type and assert the same identity. "Substrate-independent method" holds because core sees only SurfaceSpec values. Swapping OpenCode for Codex changes a value, not a code path.


12. Worked example: the offline demo through these types

harness2-demo (cli/demo.py) runs the whole method with no model service and no benchmark judge. It is the smallest complete use of every seam. The trace below follows the default --k 2 --mode both, task ledger-1.

1. Building the cell. DemoAdapter.init builds:

  • substrate = OpencodeSubstrate(), with no launcher, because the demo's execute runs a local script itself and never calls substrate.execute. The substrate is used only for name == "oc" and surface() == OPENCODE_SURFACE.
  • channel = ChannelSpec("opencode", "local/deterministic"). __post_init__ accepts kind. The model contains /, which resolve_channel_model would require for opencode, but the demo builds the ChannelSpec directly.
  • spec = BenchSpec(name="demo", runs_root=root/"runs", install_mode="workspace_config", prompts=BenchPrompts(work="reconcile signed amounts in a transaction ledger"), channels={"proposer": channel, "selector": channel}, leak_globs=("reference_answer.txt",)). All BenchSpec checks pass: valid install mode, both roles present, obs_trunc 50,000. BenchPrompts passes the anti-leak scan (no "score", "rubric"…).
  • A base tree written with tree.write_tree(surface, base, root/"base", stubs=False): four always-on files (only systemprompt.md non-empty: "Reconcile the ledger using python scripts/summarize.py.") plus scripts/summarize.py, which sums only positive amounts (BASE_SCRIPT). That filter is the bug.
  • Source files: ledger-1 = 100.25, -20.10, 5.00. The correct total is 85.15. The base script prints Total: 105.25.

DemoProposer(adapter.spec.channels["proposer"]) passes Proposer.__post_init__ because the model is non-empty. _selector_for returns the same object, since the selector ChannelSpec equals the proposer's.

2. Namespaces and bases. run_task builds Namespace("demo", "oc", "agg", 2), which stringifies to demo-oc-agg2. Base labels are base1, base2, with run ids demo-oc-agg2/ledger-1/base1 and …/base2. Both modes share these bases: they are rolled once, in the agg namespace. DemoAdapter.execute(…, None, run_id) runs the base script and writes answer.txt = Total: 105.25. Each becomes RunView(label="base1", ok=True, delivered=True, …).

3. Parallel mode (agg). run_parallel creates Ledger(mode="agg", task_id="ledger-1", k=2) and records the two bases as rollouts. Proposal stages are created in the order inv1, dom1, inv2, dom2 (recursion.py#L368). For each one, _run_stage builds a ContextTree under context/<label> and calls prop.call(mission, tree.root, "inv1.a1", harvest="context/harness"). DemoProposer (the else branch) deletes if amount > 0 from context/harness/scripts/summarize.py and returns workspace=context/harness. read_tree(OPENCODE_SURFACE, workspace, rejected=[]) gets the tree back. _accept_tree runs autotrigger (no skills, nothing to do), diff_summary (one modified file, so kept_base=False) and validate, which passes: always-on text well under 24,000 chars, scripts/summarize.py compiles, and the python scripts/summarize.py reference in systemprompt.md matches a shipped file. Then DemoAdapter.validate_tree (scripts/summarize.py must exist) and the grader-verdict scan run. Four sessions are recorded. Four rollouts follow, each now printing Total: 85.15. Two AGG stages (agg1, agg2) do the same edit on the base and roll once each.

Ledger at the end: rollouts base1, base2, inv1, dom1, inv2, dom2, agg1, agg2 gives R = 8 = 4k. Sessions inv1, dom1, inv2, dom2, agg1, agg2 gives P = 6 = 3k. assert_identity() passes.

4. Selection. selection_pool("agg", 2, …) returns {agg1, agg2}. _render_pool keeps both. assign_ids(["agg1", "agg2"], task_id="ledger-1", k=2, mode="agg") seeds random.Random with the first 12 hex digits of sha1("ledger-1|2|agg"), which gives {"A": "agg1", "B": "agg2"}. (Computed by re-running the same three lines of logic outside the repo. For ledger-2 the same call gives {"A": "agg2", "B": "agg1"}, showing that the shuffle really depends on the task.) The selector sees only context/candidates/A.md and B.md. DemoProposer (select. branch) recomputes the expected total from context/source/ledger.csv and picks the first candidate file containing Total: 85.15. That is A, and the reply contains a choice block. Selection(chosen_id="A", chosen_label="agg1", ok=True), persisted to selection/agg2/selection.json.

5. Reflection and library. reflect_adoption builds a reflection tree with harness/ (agg1's tree) and base_harness/. DemoProposer (experience. branch) sees the files differ and returns <edits> and <experience> blocks with cue, lesson, evidence and confidence. _distil parses them. Library.add stores agg1's full tree under library/k2/agg/accounting/harnesses/ledger-1/ with EXPERIENCE.md, and rebuilds INDEX.md.

6. Sequential mode (seq). In namespace demo-oc-seq2, round 1 runs inv1, dom1, then compose c1 on the base. Round 2 runs inv2, dom2, c2 on c1's tree. In round 2 the demo's edit is a no-op, because the filter is already gone, so those stages come back with kept_base=True (identical to their fallback, c1's tree) yet still pass validation. Ledger: 2 bases + 3 rollouts per round × 2 = R = 8, and 3 sessions per round × 2 = P = 6. The identity holds again. The pool is {c1, c2}.

7. Next task. run_domain calls lib.retain(task_ids[:1]) before ledger-2. Each mode's Library.context_dir() now returns the domain directory (it contains an EXPERIENCE.md), so ledger-2's improvers find context/library/INDEX.md in their evidence.

At no point did any code path call adapter.score: DemoAdapter does not even define it. The demo runs end to end without a judge, which is the strongest proof that selection is answer-free. (Running it is chapter 08 Setup and running.)


13. What the code does not settle

  • BenchSpec.domain_word** is never read.** Every adapter sets it, but only BenchPrompts.domain_word reaches the prompts. The code does not say whether this is vestigial or reserved.
  • max_chars** is described as carried on** BenchSpec** but isn't.** See section 6.3. The value is a literal in each adapter.
  • The base-first branch of _fallback** cannot fire under the finals-only pool.** The bases are never in the pool. The code keeps it, and the pool_kind="mixed" default of run_selection, but no caller in this release passes a mixed pool.
  • deliverable_assets_note** /** _line** /** _reference_only** hooks have no implementer** among the shipped adapters.
  • Import rules are conventions, not checks. No tool or test fails if someone adds from ..benchmarks import … to core.
  • Why some BenchPrompts** fields are exempt from the anti-leak scan** (domain_word, deliverable_word, selection_lead, selection_conformance) is not explained by the code.

Key takeaways

  • Four concerns, small seams: method (core), benchmark (BenchmarkAdapter + BenchSpec), substrate (Substrate, seen by core only as SubstrateLike.surface()), and channel (ChannelSpec → Proposer).
  • Values are frozen dataclasses that validate on construction (ChannelSpec, BenchPrompts, BenchSpec, SurfaceSpec, Install, RunRequest, Namespace). Behaviour lives in protocols, their implementations, Proposer, Library and core's module functions.
  • Substrate differences are data. tree.py has no per-framework branches. Everything is a SurfaceSpec field.
  • The benchmark injects the launcher into the substrate. The substrate owns staging, and the benchmark owns runners, environment and result locations.
  • RunView** has no score, and nothing in the method's run path imports** core/judge.py. BenchmarkAdapter.score is called only by the judge, after selection, across a file boundary.
  • Ledger.assert_identity()** makes the R=4k / P=3k budget a checked invariant**, duplicates first, with dead rollouts and fallback stages still counted.
  • Naming is centralised in ns()** /** Namespace. Substrate, mode, k, seed and tag are all in every run id, so caches never collide.
  • tree.validate** reports every problem at once**, because its message is the proposer's retry feedback.

Where to go next

Was this useful?
Make a page like this. Free.Notes and diagrams side by side, written by you or by Claude Code and Codex. Publish it in one click.
Make your own, free