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:
- 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.
- 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:
- Locate code with
aft_search or grep.
- Inspect the implementation with
aft_zoom.
- Attempt
edit({ patch: "..." }) in hashline mode.
- 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:
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
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:
aft_search -> aft_outline -> aft_zoom, butaft_zoom,grep,aft_search, and conflict/search snippets do not mint hashline snapshots. Only a successful taggedread/aft_readcall (and accepted AFTcat/head/tailrewrites) currently produces a[path#TAG]that hashlineeditcan address.editslot survives, but does not require the AFTreadslot to survive. Withdisabled_tools: ["read"], hashlineeditcan remain active while OpenCode falls back to its native untaggedreadimplementation.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:
aft_searchorgrep.aft_zoom.edit({ patch: "..." })in hashline mode.[path#TAG]snapshot exists.aft_zoomreturns the full source of a symbol with line numbers, so it looks like an edit-ready read to the model, butcrates/aft/src/commands/zoom.rsnever 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 AFTreadregistration.readavailable.openCodeHashlineEffective()andopenCodeHashlineEditRegistered()only check whethereditsurvives, so the hashline{ patch }edit schema can remain active.readreturns 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, notsurface.hoistEdit && surface.hoistRead.Normal default hoisting is not the problem: when
hoist_builtin_toolsis enabled, AFT registersread,write,edit, andapply_patchunder the native names and overrides the host implementations.hoist_builtin_tools: falsealready disables effective hashline edit. The problematic case is a partially filtered surface, especially disabling onlyread.Expected behavior
A session must never expose hashline
editunless 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
editslot and the AFT tagged-read slot to survive final registration:registeredTools.has("edit")andregisteredTools.has("read")are true.surface.hoistEditandsurface.hoistReadare true.hashline_downgradedwarning with a reason such astagged_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:
The guidance should be conditional so legacy edit users and remote-document
aft_zoomusers are not shown irrelevant instructions.It may also help to make
aft_zoomoutput explicitly sayre-read with read before hashline editwhen the current session uses hashline mode.3. Consider making selected
aft_zoomresults edit-addressableA longer-term enhancement could let local, single-file
aft_zoomresults publish hashline snapshots and return a tag. This would make the recommendedsearch -> outline -> zoom -> editworkflow directly composable.This needs explicit semantics rather than treating every source snippet as a tagged read:
An alternative is an explicit
editable: true/tagged: truezoom 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
readreturns[path#TAG]and a subsequent hashline edit succeeds.disabled_tools: ["read"], hashline mode downgrades instead of exposing{ patch }edit beside native untaggedread.aft_zoom/search snippets do not mint tags.zoom -> editrejection followed byread -> editsuccess, or an enhanced zoom test covers the new tag-producing behavior if that option is implemented.Relevant code
packages/opencode-plugin/src/tool-registration.tspackages/opencode-plugin/src/tools/hoisted.tspackages/opencode-plugin/src/workflow-hints.tspackages/pi-plugin/src/index.tspackages/pi-plugin/src/tool-registration.tscrates/aft/src/commands/read.rscrates/aft/src/commands/zoom.rscrates/aft/src/hashline/integration/schema.rscrates/aft/src/hashline/snapshot/mod.rs