Replace built-in edit tool with custom hashline edit tool#4
Open
IshanArya wants to merge 2 commits into
Open
Conversation
added 2 commits
May 11, 2026 14:31
aorizondo
added a commit
to aorizondo/opencode-hashline
that referenced
this pull request
May 14, 2026
…h-my-opentools patterns
## Problem
The hashline plugin v1 annotated reads with stable #HL refs (e.g.
`12#A3F#9BC`) but the edit workflow was broken: `translateHashlineEditArgs`
converted hashline operations to full-file `oldString`/`newString`, making edits
fragile — any file change between read and edit caused failure. The edit hook
also had a missing `.js` extension in `src/index.ts` that prevented ESM loading.
## Solution
Replace the entire edit mechanism with a properly registered `tool.edit`
custom tool that:
1. **Registers via `Hooks.tool.edit`** — overrides the native edit tool
through the standard SDK `tool()` API (zod schema + execute handler)
2. **Applies edits directly on file content** — reads the file, applies
splice-based operations (replace, range replace, append, prepend) on the
line array, and writes back. No fragile full-file `oldString`/`newString`
translation needed.
3. **Supports both hashline AND legacy modes** — accepts `operations[]` with
hashline refs for batch editing, plus `oldString`/`newString` fallback
for simple text replacements.
4. **File text canonicalization** — handles BOM stripping/restore and CRLF/LF
normalization before editing (ported from oh-my-opentools).
5. **HashlineMismatchError with context** — when refs become stale, shows a
`>>>`-marked diff of the ±2 lines around each mismatch with corrected refs.
6. **ctx.ask() permissions** — calls `context.ask({ permission: 'edit',
patterns: [filePath], always: ['*'] })` for proper OpenCode permission
integration.
7. **SHA1 hex + anchor hash (3-4 chars)** — uses the more collision-resistant
hash format from opencode-hashline v1 (4096-65536 values), NOT the 2-char
CID format (256 values) from oh-my-opentools.
## Structural changes
- Deleted `.opencode/` directory — all source moves to `src/`
- Deleted `tsconfig.build.json` — replaced by single `tsconfig.json`
- Deleted `src/codemap.md` — obsolete, described tool registration that
didn't exist
- New `tsconfig.json` — `strict: true`, `rootDir: ./src`, `sourceMap: true`
- `src/index.ts` — entry point registers `tool.edit` + 4 hooks (read
annotation, system transform, chat annotation, tool definitions)
- `src/hash.ts` — SHA1 hex hashing + anchor hash + computeFileRev
- `src/ref.ts` — ref parsing, formatting, validation
- `src/file-text.ts` — BOM/CRLF canonicalization
- `src/edit-ops.ts` — operations engine: primitives (set/replace/insert/
append/prepend) + orchestrator + dedup + overlap detection +
HashlineMismatchError
- `src/edit-executor.ts` — file I/O, ref resolution, ctx.ask(), metadata
- `src/edit-tool.ts` — `tool()` definition with zod schema
- `src/hooks.ts` — `tool.execute.after` for read annotation,
`experimental.chat.system.transform` for instruction injection,
`chat.message` for file annotation, `tool.definition` for descriptions
- `src/shared.ts` — config loading, LRU cache, format/strip helpers,
exclude patterns
## Tests
33 tests covering: hash functions, ref parsing/formatting, file-text
canonicalization, all edit operations (replace, range, append, prepend,
insert after/before, noop detection), mismatch errors with remaps,
format/strip round-trip, shouldExclude, cache, cache eviction, hooks
(read annotation, system transform, tool definitions), and full
integration tests of the edit executor with real file I/O.
## Comparison with PR AngDrew#4
This implementation differs from IshanArya's PR AngDrew#4 in several key ways:
- **Zero external dependencies**: no `diff` library needed
- **Canonicalization**: BOM + CRLF handling (PR AngDrew#4 lacks this)
- **Dual-mode API**: supports both `operations[]` and
`oldString`/`newString` (PR AngDrew#4 only supports operations[])
- **ctx.ask()**: proper OpenCode permission integration (PR AngDrew#4 omits this)
- **Deduplication**: identical replace edits at the same position are
collapsed (PR AngDrew#4 relies on core engine only)
- **Cleaner structure**: all source in `src/`, outputs to `dist/` with
`rootDir: ./src` (PR AngDrew#4 keeps `.opencode/`)
- **33 tests vs ~10**: broader coverage of all edge cases
Closes AngDrew#3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The hashline plugin annotates Read tool output with
#HLline refs (e.g.12#A3F#9BC) andREVtokens so the LLM can make precise, hash-verified edits. Previously, we tried to extend the native Edit tool's JSON Schema at runtime via thetool.definitionhook, addingoperations/fileRev/safeReapplyfields alongside the existingoldString/newString.This approach failed. Session export inspection confirmed that every edit call used
oldString/newString-- the schema extension either didn't propagate to the LLM or wasn't persuasive enough to override trained behavior. The hashline refs were generated but never consumed.Solution
Replace the built-in
edittool entirely with a custom tool (.opencode/tools/edit.ts) that only accepts hashline-style args. Per the OpenCode docs: "If a custom tool uses the same name as a built-in tool, the custom tool takes precedence."The custom tool schema has no
oldString/newString. The LLM must useoperations[]with hash-anchored refs from read output. This matches the approach used by oh-my-openagent, which successfully steers LLMs toward hash-anchored edits by removing the native fallback path.Changes
Created
.opencode/tools/edit.ts-- Custom edit tool with hashline-native schema (filePath,fileRev?,operations[]). CallsrunHashlineOperationsDetailedwithdryRun: falseto apply edits directly. Includes a concise tool description with workflow, rules, operation types, examples, and error recovery.Modified
.opencode/plugins/hashline-hooks.ts-- Removed ~190 lines of dead code:tool.definitionhook's edit schema extension blocktranslateHashlineEditArgsand all supporting functions (hasHashlineEditShape,toHashlineOperations,firstString,firstBoolean,isNativeEditTool)tool.execute.beforeto only strip hashline prefixes from content fields (write, patch, apply_patch still need this)mapOperationInput,runHashlineOperationsDetailed,HashlineOperationInput).opencode/plugins/hashline-routing.ts-- Removed"edit"from the known-tools set and removed all edit-specific snake_case-to-camelCase arg normalization (the custom tool defines its own schema)test/hashline-hardening.test.mjs-- Updated tests to verify the hook no longer modifies the edit tool's schema or descriptionRemoved
.opencode/tools/resolve-hash-edit.ts-- Deleted (backup intools_disabled/). This MCP tool was a workaround that translated hashline operations tooldString/newStringin a separate tool call. Redundant now that the edit tool handles hashline operations directly.What stays the same
tool.execute.afterhook to annotate output with#HLrefs. Not replaced.tool.execute.beforeexperimental.chat.system.transformhook still injects hashline workflow guidanceLINE#HASH#ANCHOR(3-4 hex chars), more collision-resistant than alternativesHow to verify
See
specs/verify-hashline-edit-schema.mdfor the full test procedure. Quick version:All edit calls should print
HASHLINE.reference: #3