Skip to content

Replace built-in edit tool with custom hashline edit tool#4

Open
IshanArya wants to merge 2 commits into
AngDrew:mainfrom
MyForking:main
Open

Replace built-in edit tool with custom hashline edit tool#4
IshanArya wants to merge 2 commits into
AngDrew:mainfrom
MyForking:main

Conversation

@IshanArya
Copy link
Copy Markdown

Problem

The hashline plugin annotates Read tool output with #HL line refs (e.g. 12#A3F#9BC) and REV tokens so the LLM can make precise, hash-verified edits. Previously, we tried to extend the native Edit tool's JSON Schema at runtime via the tool.definition hook, adding operations/fileRev/safeReapply fields alongside the existing oldString/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 edit tool 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 use operations[] 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[]). Calls runHashlineOperationsDetailed with dryRun: false to 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:
    • Removed the tool.definition hook's edit schema extension block
    • Removed translateHashlineEditArgs and all supporting functions (hasHashlineEditShape, toHashlineOperations, firstString, firstBoolean, isNativeEditTool)
    • Simplified tool.execute.before to only strip hashline prefixes from content fields (write, patch, apply_patch still need this)
    • Cleaned up unused imports (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 description

Removed

  • .opencode/tools/resolve-hash-edit.ts -- Deleted (backup in tools_disabled/). This MCP tool was a workaround that translated hashline operations to oldString/newString in a separate tool call. Redundant now that the edit tool handles hashline operations directly.

What stays the same

  • Read tool -- Still uses the tool.execute.after hook to annotate output with #HL refs. Not replaced.
  • Write/patch/apply_patch tools -- Still native, still get hashline prefix stripping via tool.execute.before
  • System prompt injection -- experimental.chat.system.transform hook still injects hashline workflow guidance
  • hashline-core.ts -- Core engine unchanged
  • Ref format -- Still LINE#HASH#ANCHOR (3-4 hex chars), more collision-resistant than alternatives

How to verify

See specs/verify-hashline-edit-schema.md for the full test procedure. Quick version:

  1. Start a new OpenCode session
  2. Ask the LLM to read a file, then edit it
  3. Export the session and inspect edit tool calls:
opencode export <sessionID> > session.json
node -e "
const d = require('./session.json');
for (const m of d.messages) {
  for (const p of (m.parts || [])) {
    if (p.type === 'tool' && p.tool === 'edit') {
      const input = p.state?.input || {};
      const style = 'operations' in input ? 'HASHLINE' : 'NATIVE';
      console.log(style, Object.keys(input));
    }
  }
}
"

All edit calls should print HASHLINE.

reference: #3

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant