Skip to content

hashline patch grammar is undocumented on the tool surface: first-use failures across PUT/CUT/REM/MV (v0.51.2) #251

Description

@Qiiks

Summary

edit_mode: "hashline" (v0.51.2) ships a rich patch language whose grammar lives only in crates/aft/src/hashline/syntax/mod.rs (and its unit tests). The tool description, README, and docs/config.md all say only "PUT/CUT/REM/MV operations with [path#TAG] sections" — so a fresh agent assembling its first patch cannot succeed without reading Rust source. In practice I hit 5 distinct failures in one round-trip session, several of which the tool surface actively invites (it prints tags that are then rejected).

What worked (after reading the parser)

Round-trip on a test file: PUT 25: (text) → CUT 13.=14 @fragPUT >$ @fragMV name.mdREM. All five ops function; tags rotate per mutation.

Evidence: first-use failures (v0.51.2, exact responses)

  1. Text PUT needs a trailing colon that is never stated. PUT 25 + body rows fails with "PUT body rows must begin with +; unexpected content at line 3". Reason (from parser): PUT 25 is the register-copy form (anonymous register) which accepts no body; the text form is the subtly different PUT 25: (colon). The colon is a one-ASCII-char rune no surface documents.
  2. Trailing newline in the patch breaks parsing. PUT 25: + +row → error "PUT body rows must begin with +; unexpected content at line 4" when the patch string ends with \n — the empty final line is treated as a body row. Empty lines inside a body must be written + alone; this is only discoverable from source.
  3. PUT <line> REPLACES the line rather than inserting. PUT 25: deleted the existing line 25 and substituted the body (data loss of the old line). Insert-before/after require gap forms <25 / >25. A plain line address is a replacement — nowhere documented, and the semantic is surprising for a PUT.
  4. Tags printed by edit responses are unusable. After CUT, the response printed [path#D0E7] — reusing that tag for the next edit fails with "gap boundary line 23 was not retained by the tagged read". Only tags from an actual tagged read retain eligibility; the response tag is a dead end, yet it looks like the same currency.
  5. steering never surfaces; stale rejection is opaque. Rejection structs carry a steering hint ("perform a ranged tagged re-read because required boundary context changed", "re-read the current tagged content before editing"…), but the tool response returns only the message — e.g. a stale known tag yields "addressed content no longer matches the Phase-1 baseline" with no tag name and no "re-read" hint.
  6. REM/MV right after MV is rejected. The MV response again showed [path#47BA] as the destination tag, but REM with it fails with "REM and MV require a tagged read that retained the whole source file" — needs yet another read.

Suggested fixes (any subset, in priority order)

  • Document the grammar in the tool description (primary ask — the agent sees nothing else). Include at minimum:
    • Address forms: 0 (BOF), <N / >N (gap before/after), N (span), ranges N.=M (aliases N..=M/N..M), blocks N* / >N*, $ / $-K (EOF-relative).
    • PUT: text form PUT <addr>: + +-prefixed body rows (empty rows as bare +); register form PUT <addr> @name (no body). Register-name rules (@ + ascii alnum/_/-).
    • CUT: CUT <addr> [@name]; REM: bare REM (whole file); MV: MV "<dest path>" (must be the only op in the section).
    • Semantics: plain line address = replace; </> = gap inserts; ops apply per section; *** Begin Patch/*** End Patch envelope accepted.
    • "The tag must come from a tagged read, not from an edit response."
  • Surface steering in the tool response (or at least tag + "re-read" hint) — it already exists and is being discarded.
  • Stop printing a [path#TAG] for post-mutation state in edit responses (or label it clearly as un-eligible), since those tags cannot be used in the next patch.
  • Accept a tolerated trailing newline at patch end (or document the empty-body-row rule) — a model naturally emits a final \n.

Happy to contribute: the grammar summary above can be distilled into a docs/hashline.md + an excerpt in the tool description. (Test env: OpenCode plugin build 0.51.2, edit_mode: "hashline".)

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