Skip to content

[Feature] Make AFT's hoisted edit composable with third-party engines (e.g. oh-my-openagent hashline_edit): guardrails + engine separation #178

Description

@Qiiks

Summary

Feature request: make AFT's hoisted edit tool composable with third-party edit engines, specifically oh-my-openagent's hashline_edit (LINE#ID-anchored engine). Goal: run AFT's guardrails (permission preview, external-directory checks, diff formatting) around the hashline engine instead of only around opencode's builtin edit.

Why this matters

AFT's edit is a wrapper, not an engine — it dispatches to whatever the harness resolves as edit and adds guardrails around that call:

// packages/opencode-plugin/src/tools/hoisted.ts (createEditTool)
const preview = await callToolCall(ctx, context, "edit", rawArgs, { preview: true })
// ...permission gating, external-directory check...
const data = await callToolCall(ctx, context, "edit", rawArgs)

opencode's ToolRegistry emits [...builtin, ...plugin tools] with no dedupe, and the harness resolves one implementation per tool id (last-registered wins). So today the user must choose one:

  • hashline_edit: true in OMO → OMO's edit shadows AFT's wrapper entirely; guardrails are lost, and the model gets only the LINE#ID engine.
  • hashline_edit off (default) → AFT's wrapper runs over opencode's builtin edit; hashline precision is unavailable.

Both tools have value: hashline's hash-verified line anchors make wrong-line edits nearly impossible; AFT's guardrails add preview/permission/external-dir safety that hashline lacks. Users of both plugins should not have to pick one.

Proposed design (AFT side)

  1. Add a configurable edit-engine target (e.g. edit_engine in aft.jsonc, or extend hoist_builtin_tools semantics) that lets AFT's wrapper target a named tool instead of edit:
    • default: "edit" (current behavior — wraps builtin / whatever owns the name)
    • optional: "hashline_edit" — AFT's wrapper dispatches to that name
  2. Arg-schema adaptation: when the target engine is not opencode's builtin schema, translate AFT's edits[] ({oldString,newString} / {startLine,endLine,content}) into the target's schema. For hashline this means producing {op: replace|append|prepend, pos/end: "LINE#ID", lines} operations — which requires the caller to know the LINE#ID anchors, so:
  3. Guardrail ordering: keep permission-preview + external-dir checks on AFT's side (they're schema-agnostic), and let the engine-specific translation live in a small adapter module (or accept hashline-shaped args pass-through when targeting hashline_edit).

Simplest viable slice: when edit_engine: "hashline_edit", AFT passes hashline-shaped args through unchanged (the model reads LINE#ID anchors via the read tool anyway) and keeps its guardrails on top. Full translation of builtin-shaped → hashline-shaped args can be a follow-up.

Enabling prerequisite (OMO side, referenced for coordination)

  • oh-my-openagent registers hashline under a stable distinct tool id (hashline_edit) rather than only clobbering edit, so another plugin can target it without a name collision. See code-yeongyu/oh-my-openagent packages/omo-opencode/src/tools/hashline-edit/ (tools.ts registers { edit: createHashlineEditTool(ctx) } when config.hashline_edit).
  • Document the LINE#ID schema ({line}#{hash} anchors, ops replace/append/prepend) as the engine contract.

Acceptance criteria

  • With both AFT and OMO installed and edit_engine: "hashline_edit" set, the model's edit calls run hashline's engine and AFT's guardrails (preview + permission prompt appear; external-directory edits blocked).
  • Default config (edit_engine unset) behaves exactly as today.
  • No tool-id collision: hashline remains reachable as a distinct name even when not the wrapped engine.

Related

Happy to prototype the adapter if the direction is approved.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions