Skip to content

Hashline edit can be exposed without a tag-producing read path #292

Description

@llc1123

Summary

In hashline mode, models often report that they "did not receive a hashline" even though they have already inspected the relevant source. There are two related causes:

  1. AFT's recommended exploration workflow strongly encourages aft_search -> aft_outline -> aft_zoom, but aft_zoom, grep, aft_search, and conflict/search snippets do not mint hashline snapshots. Only a successful tagged read/aft_read call (and accepted AFT cat/head/tail rewrites) currently produces a [path#TAG] that hashline edit can address.
  2. Hashline activation checks that the edit slot survives, but does not require the AFT read slot to survive. With disabled_tools: ["read"], hashline edit can remain active while OpenCode falls back to its native untagged read implementation.

The first case is an agent-facing contract ambiguity. The second is a real surface-consistency bug.

Current behavior

Common navigation workflow

A model can do the following:

  1. Locate code with aft_search or grep.
  2. Inspect the implementation with aft_zoom.
  3. Attempt edit({ patch: "..." }) in hashline mode.
  4. Fail because no current [path#TAG] snapshot exists.

aft_zoom returns the full source of a symbol with line numbers, so it looks like an edit-ready read to the model, but crates/aft/src/commands/zoom.rs never publishes into the hashline snapshot store. Search and conflict tools similarly return source snippets without producing addressable snapshots.

The hashline edit description currently says that the tag must come from a "current tagged read", but it does not explicitly identify which tools mint tags or state that aft_zoom/search output is non-addressable.

Configuration-dependent native read fallback

This configuration can expose an inconsistent surface:

{
  "edit_mode": "hashline",
  "disabled_tools": ["read"]
}

On OpenCode:

  • buildOpenCodeToolMap() removes the AFT read registration.
  • Omitting the plugin override leaves OpenCode's native read available.
  • openCodeHashlineEffective() and openCodeHashlineEditRegistered() only check whether edit survives, so the hashline { patch } edit schema can remain active.
  • Native read returns ordinary line-numbered content and cannot register a snapshot in the Rust hashline binding.

Pi has the same conceptual gap: hashline activation is based on surface.hoistEdit, not surface.hoistEdit && surface.hoistRead.

Normal default hoisting is not the problem: when hoist_builtin_tools is enabled, AFT registers read, write, edit, and apply_patch under the native names and overrides the host implementations. hoist_builtin_tools: false already disables effective hashline edit. The problematic case is a partially filtered surface, especially disabling only read.

Expected behavior

A session must never expose hashline edit unless it also exposes at least one clearly documented, agent-visible tool that can mint compatible tags for that session.

Models should also be told explicitly that inspecting source is not equivalent to establishing hashline edit evidence.

Possible solutions

1. Tighten hashline surface activation (recommended first fix)

Require both the hashline edit slot and the AFT tagged-read slot to survive final registration:

  • OpenCode: effective only when both registeredTools.has("edit") and registeredTools.has("read") are true.
  • Pi: effective only when both surface.hoistEdit and surface.hoistRead are true.
  • If either slot is absent, downgrade to the legacy edit arm and emit the existing hashline_downgraded warning with a reason such as tagged_read_unavailable.

This closes the native-read/hashline-edit mixed surface without changing snapshot semantics.

2. Add hashline-specific workflow guidance

When hashline mode is effective, inject a short routing rule into the tool guidance and/or hashline edit description:

Only read/aft_read and accepted AFT cat/head/tail rewrites mint hashline tags. aft_zoom, outline, grep, search, and conflict snippets do not. After navigation, call tagged read for every local file and range addressed by the patch.

The guidance should be conditional so legacy edit users and remote-document aft_zoom users are not shown irrelevant instructions.

It may also help to make aft_zoom output explicitly say re-read with read before hashline edit when the current session uses hashline mode.

3. Consider making selected aft_zoom results edit-addressable

A longer-term enhancement could let local, single-file aft_zoom results publish hashline snapshots and return a tag. This would make the recommended search -> outline -> zoom -> edit workflow directly composable.

This needs explicit semantics rather than treating every source snippet as a tagged read:

  • Use the resolved definition path, not necessarily the originally requested barrel/re-export path.
  • Publish only the displayed symbol and context lines as seen rows.
  • Preserve partial-coverage rules for addressed rows and gap boundaries.
  • Define per-file carriers for multi-symbol and multi-target zoom responses.
  • Keep remote URL/cache-backed zoom results non-writable.
  • Avoid refreshing or replacing snapshots for ambiguous-symbol/member-menu responses that do not contain a concrete source region.

An alternative is an explicit editable: true/tagged: true zoom option, so snapshot publication is intentional and its response contract can expose resolved path, tag, and coverage without changing ordinary navigation calls.

Suggested acceptance criteria

  • With default hashline configuration, AFT read returns [path#TAG] and a subsequent hashline edit succeeds.
  • With disabled_tools: ["read"], hashline mode downgrades instead of exposing { patch } edit beside native untagged read.
  • The same invariant holds for OpenCode and Pi.
  • Hashline-mode guidance names the tag-producing tools and states that aft_zoom/search snippets do not mint tags.
  • A regression test covers zoom -> edit rejection followed by read -> edit success, or an enhanced zoom test covers the new tag-producing behavior if that option is implemented.

Relevant code

  • packages/opencode-plugin/src/tool-registration.ts
  • packages/opencode-plugin/src/tools/hoisted.ts
  • packages/opencode-plugin/src/workflow-hints.ts
  • packages/pi-plugin/src/index.ts
  • packages/pi-plugin/src/tool-registration.ts
  • crates/aft/src/commands/read.rs
  • crates/aft/src/commands/zoom.rs
  • crates/aft/src/hashline/integration/schema.rs
  • crates/aft/src/hashline/snapshot/mod.rs

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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