Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes

Burin compass: choose safer edit tools

Harn ships a set of AST-precise edit primitives — see the structured refactorings cookbook. They pay off when structural addressing, parse validation, or semantic-neighbor updates prevent a real failure mode. For an exact localized change, a hash-guarded text patch can be simpler and equally safe.

The burin compass helps specialized coding agents make that choice. It is a built-in system-reminder provider, compass_ast_edits, that injects a standing reminder at session start (and on resume):

Choose the simplest safe edit mechanism for each change. Use the AST-precise tools when structural addressing or semantic reach materially reduces risk; use edit_safe_text_patch for exact localized changes even when grammar support is available. Preview risky or multi-operation plans with edit_dry_run.

Both the reminder and routing layer ship opt-in. Unlike the other canonical providers — which are conditional and stay silent until they have something to say (project facts, a workspace anchor, token pressure) — a steer toward code edits is only wanted in code-editing sessions, not in every sub-agent or one-shot loop. So coding-agent personas and configs turn it on explicitly; once enabled it reaches every agent surface that runs the Harn agent loop — TUI, IDE, cloud-supervised. The reminder is marked preserve_on_compact, so the guidance survives a context compaction and keeps steering through a long session.

The active routing layer

The reminder is the steer; the tool-rewrite router is the active half of the compass (#2612). It is a per-tool-call hook in the agent loop that observes a freeform edit before it runs and acts on it. It sits at the single chokepoint every agent surface funnels through — after permission and pre-tool hooks, before the tool is dispatched — so it reaches the TUI, the IDE, and cloud-supervised loops with no per-surface wiring.

It recognises a freeform / whole-file edit on a parseable source file:

  • a str_replace-shape call ({path, old_text, new_text} or a hunks array),
  • a whole-file write_file / create_file,
  • a single-hunk edit on a rename-capable language that reads like a symbol rename.

For anything else — a structural call, a file with no tree-sitter grammar, an arg shape it doesn't understand — the router is inert and the call dispatches unchanged. It is conservative by construction: it never touches a call it cannot reason about.

It runs in one of two modes:

  • suggest (the default after enabling Compass) — advisory. The router injects a one-turn system reminder naming the structural primitive the edit maps to (edit_apply_node for a node, edit_rename_symbol for a rename, or the hash-guarded edit_safe_text_patch), then dispatches the original call unchanged. The model stays in control; nothing is rewritten.
  • rewrite — silent substitution, but only when the structural form is provably equivalent to the freeform call. The one substitution the router can prove without reading the file is a raw text replace → edit_safe_text_patch: identical old_text → new_text matcher, but with a stale-base hash guard and staged-fs atomicity. A rename or a whole-file write is not byte-equivalent (a project-wide rename touches other files; a whole-file write rewrites untouched bytes), so the router falls back to a suggestion rather than guess.

Observability

Every decision increments a harn.compass.* counter via the standard counter(...) instrument, tagged with harn.compass.persona, harn.compass.tool (the freeform tool), and harn.compass.target (the structural tool):

  • harn.compass.suggested — an advisory routing decision fired.
  • harn.compass.rewritten — a call was silently substituted.
  • harn.compass.fell_backrewrite mode considered a substitution but could not prove equivalence, so the original freeform call ran.

The router also emits a live compass_routing_decision agent event before dispatch. ACP surfaces it on _harn/agentEvent with toolCallId, mode, action (suggested, rewritten, or fell_back), persona, originalTool, routedTool, targetTool, and path when the edit call named a file. The event carries routing metadata only; it does not include old or new file contents.

These surface in eval dashboards as the agent-loop edit-reliability signal.

Why a reminder, not a hard rewrite

In suggest mode the compass steers rather than rewrites. A reminder keeps the model in control: it can choose an exact text patch when that is the simplest safe fit, and the router never silently changes the bytes a tool call would produce. That makes the behaviour predictable and auditable — the reminder is visible in the transcript like any other system reminder. rewrite mode is opt-in for exactly this reason: it only ever substitutes a provably-equivalent call.

Turning it on

The compass is a normal reminder provider, so the standard controls apply:

  • Enable it for a session or persona via the reminder config: reminders.providers.compass_ast_edits = true. It is registered as a canonical provider but ships default_enabled: false, so this opt-in is what activates the steer.
  • Inspect it alongside the other canonical providers — compass_ast_edits appears in the provider metadata listing with the purpose of helping coding agents choose the safest edit primitive for each change.

Configuring the router (escape hatches)

The router is off by default. Enable and configure it with the compass option passed alongside the agent-loop tools:

  • omitted, compass: false, or compass: null — off. Edit calls dispatch with no observation, reminder, or counter.
  • compass: true — enable advisory suggest mode.
  • compass: {enabled: false} or compass: {mode: "off"} — equivalent off switch in dict form.
  • compass: {mode: "suggest"} — advisory reminders only.
  • compass: {mode: "rewrite"} — silent substitution of provably-equivalent calls, with a fall-back-to-suggest safety net.
  • compass: {prefer: ["edit_apply_node", ...]} — a persona's ordering hint. The router consumes the edit_strategy.prefer list a persona already declares (e.g. personas/fixer/manifest.harn), so a persona that prefers node-level editing nudges a plain hunk edit toward edit_apply_node in its suggestion. When no compass.prefer override is set the router reads edit_strategy.prefer directly.

The model can always ignore an advisory suggestion. A suggest reminder never blocks or alters the call, and even a rewrite only ever swaps in a behaviourally identical patch — so a deliberate localized text edit, unsupported file, or one-off raw write is always available.

Composes with