Architecture

naoru harness architecture

The durable reference for the naoru product surface: the orchestration loop, the leaf format, the mission→gate model, and the host contract. (Engine primitives — oracle, fix worker, gates, worktree, executor/outcome seams — are documented at their own modules; this doc covers the product layer composed on top of them.)

The loop (fix_engine.orchestrator)

run_session(leaves, *, executor, safety, mission, repo_root, llm_config=None, llm_config_factory=None, ...) resolves the mission's gate once and drives each enabled leaf through run_leaf:

  1. run — build an InvocationSpec from the leaf's command/cwd/env/timeout_s and launch it via the Executor (the system-under-test / check command).
  2. judge — evaluate the RunResult with the leaf's Predicate.
  3. green? — if the check already passes, return (green) with no fix. (A metric leaf is the exception: a single run only proves the metric is extractable, so it always enters the loop so the comparative perf gate can decide.)
  4. fix loop — up to the per-leaf cap, each attempt isolated in a LeafWorktree (a git worktree under host().runtime_dir() / "worktrees"): resolve LLM config only when the first fix attempt needs it, run the fix worker (run_fix_worker) with a generic, tech-stack-agnostic prompt (prompts.build_fix_prompt); re-run the check in the worktree; re-judge. Green and disabled leaves do not resolve LLM config.
  5. gate — on a candidate-GREEN attempt, hand a GateContext to the mission's gate, which returns a GateDecision (fixed / proposed / rejected).

The language-agnostic correctness signal handed to the oracle is gates_passed = (fix-worker validation_outcome.overall == "ran").

Leaf format (fix_engine.leaves)

A declarative .toml/.json document with a top-level leaves array (or a bare list). Each leaf:

fieldmeaning
idunique identifier (required)
commandargv for the check (required; any ecosystem)
predicateRED/GREEN judgement (required; see below)
cwd / env / timeout_scheck invocation context (optional)
fix_focusfree-form guidance to the fix worker
description / tagsmetadata
enabledskip when false (default true)
max_fix_loopsper-leaf fix-attempt cap (default 2; overridable at run time)

Predicate kinds (fix_engine.outcome.PredicateKind): exit_code (expected_exit), output_substring (required/forbidden, is_regex), traceback_signature / contract_violation (signature that must be ABSENT once fixed), non_crash, and metric (metric_pattern regex with one float group, metric_direction lower|higher, min_improvement fraction).

Leaf quality is separate from leaf syntax. A high-signal leaf is one narrow, deterministic, machine-checkable command plus a predicate that accurately judges the command output. If a raw project command is not a trustworthy RED/GREEN signal, the target repo should provide a project-local wrapper command that normalizes that command's behavior into a reliable exit code or output contract. Naoru remains tech-stack-agnostic: wrappers own project-specific command interpretation, while leaves declare only argv, context, and predicates.

Mission → gate model (fix_engine.mission)

A Mission selects the commit gate (resolve_gate); GateSpec records its CommitMode:

MissionGateCommit mode
test-and-fix-bugsoracle — red-first N-of-M (oracle.run_oracle) + gates_passedAUTO_COMMIT
improve-performancemetric — before/after improvement + no other-leaf regressionAUTO_COMMIT
improve-ux, general-improvementpropose-only — store the worktree diff for reviewPROPOSE_ONLY

The propose-only gate has no commit path, so subjective missions cannot auto-commit regardless of the safety profile (e.g. --unsafe). Commits flow through LeafWorktree.commit_attemptcommit_to_main; a pre-commit-hook rejection or empty diff surfaces as rejected, never a crash. A rejected verdict means the candidate patch was not committed by the gate. It does not necessarily mean the target repo remains red after other accepted fixes; shared root causes can be fixed by a different leaf and verified by a later no-fix run.

Host contract (fix_engine.host)

The engine core imports zero host-product modules; environment-coupled services come from a host installed once via configure_host. HostServices provides invoke (LLM dispatch), resolve_llm_config (inherits the host's configured provider/model — naoru hardcodes none), launch_llm_config (the naoru config picker), discover_tooling, runtime_dir, and is_codex_backed_provider.

Config (fix_engine.config)

Naoru owns a generic role-policy config independent of any host product. The packaged fix_engine/default_config.toml defines analysis, fix, target_orchestrator, and target_implementation, each with tier, optional provider/model/auth pins, reasoning_level, min_reasoning_level, and allow_below_min_reasoning.

load_naoru_config(...) applies: packaged defaults, user config, project .naoru/config.toml, selected profile, session overlay, environment, and CLI overrides. NAORU_CONFIG selects a config file, NAORU_PROFILE selects a profile, and role env vars use NAORU_ROLE_<ROLE>_<FIELD>. naoru config --show-resolved prints the effective JSON without opening the host picker.

Reasoning levels stay provider-neutral (medium, extra high, etc.). The host maps them to provider-specific effort values. If a requested level is below min_reasoning_level and bypass is false, Naoru raises the effective level and records floor_applied.

Discovery (discover_and_configure_host) resolves a provider without naming a host package: an explicit override or NAORU_HOST env (module.path:callable), else a single entry point in the naoru.host group. The default host extra registers obra.naoru_host:install; private host implementations are not part of the public Naoru host contract. A standalone host can supply its own HostServices implementation and config UI later.

Public proposed patches default to host().runtime_dir() / "naoru" / "patches". Host products may pass explicit paths for their own internal harness state.