Files
omp-edit/README.md
T

10 KiB

omp-edit

oh-my-pi-style editing for the DeepSeek Harness (DSH). It replaces DSH's built-in edit tool with oh-my-pi's hashline patch language. It also wraps read and grep so their output carries the content-hash tags the patches anchor on.

The model points at line numbers of a tagged snapshot instead of retyping the old text. That avoids whitespace-mismatch "string not found" loops and saves output tokens. A stale snapshot is detected before anything is written, and is rebased automatically when the anchored lines survived.

What the agent sees

read output carries a [PATH#TAG] header and LINE:TEXT rows (the 4 hex characters are a hash of the whole file):

<path>[src/app.py#1A2B]</path>
1:def greet(name):
2:    print("Hello, " + name)

grep groups matches under a tagged header per file. A grep hit counts as having read the file, so it can be edited straight away:

[src/app.py#1A2B]
2:    print("Hello, " + name)

edit takes one input string with one or more file sections:

[src/app.py#1A2B]
PUT 2.=2:
+    print(f"Hi, {name}")

It returns the fresh tag plus a compact numbered preview of the changed region, so the agent can chain edits without re-reading. The Web UI shows the usual diff card.

Hashline operations

Form Effect
PUT N.=M: + +rows Replace original lines N..M (N.=N for one line)
PUT N*: + +rows Replace the syntactic block opening at line N (function/class/object, Markdown heading section, Python/YAML indented block; leading decorators included)
PUT <N: / PUT >N: Insert before / after line N (<1 = file head, >$ = file tail)
PUT >N*: Insert after the block opening at N
CUT N.=M [@name] / CUT N* [@name] Delete and capture (anonymous register, or a named one)
PUT <N [@name] / PUT >N [@name] Paste a register at a gap
PUT N.=M @name / PUT N* @name Paste a named register over a range/block
REM Delete the file
MV DEST Move the file after the section's edits

The rules match oh-my-pi:

  • Body rows are +TEXT; a lone + is a blank line; literal -x is written +-x.
  • All numbers refer to the tagged snapshot, never shifted by earlier hunks.
  • Overlapping ranges are rejected.
  • Named registers persist for the session, and cross-file moves work (CUT 1* @fn in one section, PUT <1 @fn in another).
  • The whole call is validated before anything is written.
  • Tolerant parsing: an optional *** Begin/End Patch envelope and markdown fences are stripped, and PUT 3-5: / PUT 3: are accepted with a warning.

Stale tags

Every tagged read/grep/edit records a snapshot. When the file changed since the tag (another tool, a formatter, the user):

  • if every anchored line still exists unchanged, with a neighbour that moved by the same offset, the patch is remapped onto the current file and applied. A warning says so.
  • otherwise the edit is rejected without writing, with a message to re-read.

A tag that was never issued for that file (e.g. copied from another file) is rejected with a hint.

Paging: read, grep and glob

Like oh-my-pi, outputs are cut into pages so the model never pulls a whole file or huge search into context at once:

  • read: at most 200 lines per call (limit above 200 is capped); the footer gives the offset to continue.
  • grep: at most 200 matches per call, grouped under [PATH#TAG] headers. Pass offset for the next page (… call grep again with offset=200).
  • glob: at most 200 paths per call, with the same offset cursor.

Change the page size with pageLines.

Guard rule

A tools/pre-execute listener makes the agent use these tools instead of working around them through the shell. Denied calls get a short reason that names the right tool.

Denied Use instead
sed -i, perl -i, ed/ex, tee, >/>> into files, heredocs, truncate/dd, python -c/node -e… edit (hashline)
cat/head/tail/less/bat/nl FILE, sed -n, awk read (paged)
grep/rg/ag/ack over files grep (paged, tagged)
find/fd/locate, ls -R glob
str_replace_editor, apply_patch tools; write on an existing file edit (write stays allowed for brand-new files)

What stays allowed:

  • Text inside quotes is ignored (git commit -m "fix > bug").
  • Redirection to /dev/null is fine.
  • Filters on piped output keep working (npm test 2>&1 | tail -5, ps aux | grep node).

If the guard can't check whether a path exists, write is denied rather than allowed (it fails closed).

Turn the guard off with guard: false, or change the list of blocked tools with guardTools.

Diff cards in the Web UI

The plugin ships a web client (client.js) that registers an edit tool view. Hashline, sloppy and apply_patch calls render as a diff card instead of raw patch text:

  • green added rows, red removed rows and gray context, using only DSH's theme tokens (--dsw-alias-code-diff-added/-deleted, --dsw-alias-state-success/error-primary, the code-block font), so it follows the active theme like the stock diff view;
  • a line-number gutter with the old and new file line of every row;
  • +N -M counts in the header, plus Wrap and Copy buttons.

The host attaches oldStart, newStart and rows to every hunk in meta.diffs, next to the standard path/oldText/newText fields. Older results without them still render, just without numbers.

Edit modes

mode picks the wire format of the replacement edit tool:

Mode Input Notes
hashline (default) input Described above
replace file_path, old_string, new_string, replace_all, or edits: [{old_string,new_string,replace_all}] Fuzzy whitespace matching: exact → indentation/trailing-space-insensitive → whitespace-collapsed → smart quotes/dashes → ≥95% similarity. The file's indentation is carried over. Batched edits are atomic
sloppy input *** Edit File: / *** Find / *** Replace · *** Insert Before · *** Insert After, … captures, all suffix
apply_patch input Codex format: *** Add/Delete/Update File, *** Move to, @@ scoped hunks, *** End of File

Per-model selection, like oh-my-pi:

  1. modelVariants (substring of provider/model → mode), first match wins.
  2. mode.
  3. With hashline and strict: false, model families oh-my-pi considers unreliable at hashline fall back to fallbackMode (default replace). The list is fallbackModels, default kimi, mimo, minimax, deepseek, stepfun, codex-spark, glm-5.3-flash.

The tool schema follows the model the agent actually uses and switches mid-session when you change models.

How it replaces the built-ins

DSH registers edit/read/grep in each agent preset. Tools registered in an agent's own scope shadow those, so this plugin listens for agent/created and registers into every agent's scope:

  • edit is replaced outright. Nothing of DSH's edit is used.
  • read and grep delegate to DSH's implementations (sandbox, limits, ripgrep, spill files all unchanged) and only add tags to the output.

DSH's safety rules are kept:

  • Read first: you still need to read (or grep) a file before editing it. This is fs-observation-policy.
  • Guarded writes: writes go through ctx.fs with the version guard.
  • Sandbox: the session's sandbox policy applies to writes and REM.
  • Line endings: CRLF files stay CRLF.

To use DSH's original tools again, disable this plugin (Settings → Plugins → OMP Edit) or set replaceEdit/replaceRead/replaceGrep: false.

Installation

dsh plugin --profile web add git+https://git.rizzy.dev/DSH-Plugins/omp-edit.git

Or in the Web UI, go to Settings → Plugins → Install and paste git+https://git.rizzy.dev/DSH-Plugins/omp-edit.git. You can also ask the agent to install it with plugin_manager → install_bundle.

For development:

git clone https://git.rizzy.dev/DSH-Plugins/omp-edit.git ~/dsh-plugins/omp-edit
dsh plugin --profile web add link:$HOME/dsh-plugins/omp-edit

Then restart DSH (systemctl --user restart dsh or re-run dsh web). New sessions get the new edit tool. Sessions that already exist pick it up once they're re-created, e.g. after the restart.

No dependencies: everything is plain Node.js (≥ 20).

Configuration

Set options in the config of the row in ~/.dsh/profiles/web/cordis.patch.yml:

- id: omp-edit
  config:
    mode: hashline          # hashline | replace | sloppy | apply_patch
    strict: false           # true: never fall back from hashline
    fallbackMode: replace
    fallbackModels: [kimi, mimo, minimax, deepseek, stepfun, codex-spark, glm-5.3-flash]
    modelVariants:          # substring of "provider/model" -> mode
      gpt-5-codex: apply_patch
    replaceEdit: true
    replaceRead: true
    replaceGrep: true
    fuzzy: true             # replace mode fuzzy matching
    replaceGlob: true
    pageLines: 200          # read/grep/glob page size
    guard: true             # deny shell edits/reads/searches and non-hashline write tools
    guardTools: [write, str_replace_editor, apply_patch]   # write: only existing files are denied

Tests

npm test     # node --test test/*.test.mjs — 34 tests (engine + integration on the real DSH fs-local backend)

Files

File Role
index.js DSH plugin: per-agent tool registration, mode selection, fs policy, paging, guard rule
client.js Web client: edit tool view rendering hashline/patch results as the native diff card
lib/hashline.js Hashline parser and applier
lib/block.js N* block resolution (brackets, indentation, Markdown, decorators)
lib/store.js Session snapshot store, Myers line diff, stale-tag rebase
lib/replace.js Fuzzy replace mode
lib/sloppy.js sloppy mode
lib/apply_patch.js apply_patch mode
lib/diff.js Hunk diffs for the UI diff card
lib/prompts.js Model-facing tool descriptions (adapted from oh-my-pi's crates/pi-edit/prompts)

Pairs well with omp-lsp, which appends language-server diagnostics to every edit.