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:
- run — build an
InvocationSpecfrom the leaf'scommand/cwd/env/timeout_sand launch it via theExecutor(the system-under-test / check command). - judge — evaluate the
RunResultwith the leaf'sPredicate. - green? — if the check already passes, return (
green) with no fix. (Ametricleaf 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.) - fix loop — up to the per-leaf cap, each attempt isolated in a
LeafWorktree(a git worktree underhost().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. - gate — on a candidate-GREEN attempt, hand a
GateContextto the mission's gate, which returns aGateDecision(fixed/proposed/rejected).
The language-agnostic correctness signal handed to the oracle is gates_passed = (fix-worker validation_outcome.overall == "ran").
Deterministic schedule plans (fix_engine.scheduler)
Not in a release yet: contracts, plan and runtime are in the source tree but not in the published naoru 0.1.4 wheel. This section describes the next release.
The generic scheduler packet is deliberately separate from the current loop. It defines a closed task declaration in fix_engine.scheduler.contracts, compiles it in fix_engine.scheduler.plan, and offers explicit execution in fix_engine.scheduler.runtime. Nothing in orchestrator.run_session(...) opts into the runtime; the public leaf loop stays serial.
Each TaskSpec has a unique id, integer priority, Dependency edges with a DependencyCondition (success, failure, or completion), and opaque counted ResourceClaim values. compile_schedule(...) defaults to ScheduleMode.SERIAL; serial plans contain one task per stage. ScheduleMode.PARALLEL may put dependency-ready tasks in the same stage only while their summed resource claims remain within the supplied capacities.
Static stages do not erase conditional dependency semantics. SchedulePlan.admit(...) takes a closed mapping of completed task IDs to TaskOutcome (success or failure) and returns only the first stage's runnable IDs. The mapping is causally validated: every reported task must have completed dependencies whose conditions matched. A success edge admits only a successful upstream task, a failure edge only a failed task, and a completion edge either result; mutually exclusive children are therefore never admitted together. A reported outcome also cannot bypass an incomplete eligible earlier stage.
The compiler rejects duplicate task IDs, missing dependency targets, dependency cycles, claims for undeclared resources, and claims larger than a resource's capacity. Within a ready set, it orders tasks by descending priority and then task ID. SchedulePlan.canonical_json() is schema-versioned and independent of input container iteration, and its SHA-256 digest binds the mode, capacities, declarations, dependencies, resource claims, and compiled stages.
Opt-in execution
run_schedule(plan, runner, ...) is a host-neutral coordinator over the existing TaskSpec and SchedulePlan contracts. The injected runner receives (TaskSpec, CancellationToken) and returns TaskOutcome.SUCCESS or TaskOutcome.FAILURE. An ordinary runner exception becomes a failure outcome, including its error type and message, so a failure or completion dependency can open. TaskCancelled is a cooperative cancellation result without a success/failure outcome, so its dependents are blocked.
ScheduleRuntimeConfig carries the independent worker bound, deterministic aging interval, and shutdown bound. Serial plans always execute one task at a time. Parallel plans may use up to max_concurrency workers, but admission also acquires the task's exact counted resource claims. The lease is released exactly once after success, failure, exception, or cooperative cancellation. The coordinator uses SchedulePlan.admit(...) as the stage authority: when one task in the open stage finishes, another eligible member of that same stage starts immediately if a worker and its leases are available, even while peers keep running. A later compiled stage never opens until every task in the earlier stage has completed or its dependency condition has made it unreachable. Priority, stable task ID order, resource reservation, and admission-epoch aging apply only within the open stage, so fairness cannot bypass the compiled plan.
CancellationToken.stop_admission() stops new work and gracefully drains admitted tasks without starting a timeout. cancel() additionally signals admitted runners, which can use wait() or raise_if_cancelled(), and starts the configured cooperative shutdown bound. Timeout values must be finite, within threading.TIMEOUT_MAX, and positive for shutdown (or non-negative for an orphan wait). If a runner ignores that signal past shutdown_timeout_s, the runtime never kills it. Instead, ScheduleShutdownTimeout.orphans exposes live OrphanedTask handles for observation or a bounded wait(). Their daemon-backed execution does not keep interpreter shutdown open; each task retains its lease until it eventually returns.
ScheduleRunResult.tasks is always in plan.task_order, independent of worker completion order, and includes every declared task exactly once as success, failure, cancelled, condition-blocked, or not admitted. An optional on_event callback receives immutable, monotonically sequenced ScheduleEvent values through serial, daemon-backed invocations isolated from the coordinator. Per-task lifecycle events are declared, ready, resource-leased, admitted, and started before the runner thread can observe execution, followed by its terminal event. If cancellation's bound expires while a callback is still running, the callback is never killed: the result records one timeout failure and suppresses later callback delivery. ScheduleRunResult.to_dict() still includes the complete coordinator event stream for an exact neutral adapter input. Callback errors are captured in the result and do not change task execution; the callback is an observation seam only, with no persistence owned by the scheduler.
Leaf format (fix_engine.leaves)
A declarative .toml/.json document with a top-level leaves array (or a bare list). Each leaf:
| field | meaning |
|---|---|
id | unique identifier (required) |
command | argv for the check (required; any ecosystem) |
predicate | RED/GREEN judgement (required; see below) |
cwd / env / timeout_s | check invocation context (optional) |
fix_focus | free-form guidance to the fix worker |
description / tags | metadata |
enabled | skip when false (default true) |
max_fix_loops | per-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:
| Mission | Gate | Commit mode |
|---|---|---|
test-and-fix-bugs | oracle — red-first N-of-M (oracle.run_oracle) + gates_passed | AUTO_COMMIT |
improve-performance | metric — before/after improvement + no other-leaf regression | AUTO_COMMIT |
improve-ux, general-improvement | propose-only — store the worktree diff for review | PROPOSE_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_attempt → commit_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; that extra stays defined but does not resolve today (the obra distribution publishes no release files), so the documented install is bare naoru plus a host of your own until obra republishes. 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.