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-xis 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* @fnin one section,PUT <1 @fnin another). - The whole call is validated before anything is written.
- Tolerant parsing: an optional
*** Begin/End Patchenvelope and markdown fences are stripped, andPUT 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 (limitabove 200 is capped); the footer gives theoffsetto continue.grep: at most 200 matches per call, grouped under[PATH#TAG]headers. Passoffsetfor the next page (… call grep again with offset=200).glob: at most 200 paths per call, with the sameoffsetcursor.
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 |
edit (hashline) |
Inline scripts that can touch files, processes or the network (node -e "require('fs')…", node --input-type=module --eval "import …", python3 -c "open(…)", ruby -e "File.write…", deno eval, bun -e, php -r…), or scripts piped in through stdin (… | node, node - < f) |
read/grep/edit |
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/nullis fine. - Filters on piped output keep working (
npm test 2>&1 | tail -5,ps aux | grep node). - Computation-only one-liners:
node -e "console.log(1+1)",node -p "2**10",python3 -c "print(sum(range(9)))",ruby -e 'puts 1+1'. Running a file is a normal command (node --test test/,node build.mjs).
How inline scripts are judged: the guard works out which interpreter runs (also through env, sudo and absolute paths) and pulls out its program from -e/-p/--eval/--print/--eval=/-c/-r/eval. The program is allowed only if it contains none of the identifiers that start any I/O path, for example:
- JavaScript:
require,import,process,globalThis,eval,Function,constructor,fetch,Deno,Bun, or bracket access with a string (x['y']); - Python:
import,__import__,open,os,subprocess, dunder attributes; - Ruby/Perl:
File,IO,system,open, backticks.
Banning the identifiers that start an I/O path covers obfuscation too: fs['write'+'File'] still needs require or import to get fs. A program the guard can't read (stdin, -) is denied.
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 would otherwise show raw patch JSON; instead they render with the same chrome as DSH's own tool rows, built from DSH's primitives (DisclosureRow, TextShimmer, Tooltip, its icons) with CSS copied from the stock ToolRow, CodeCard and DiffBlock modules:
- the standard 24px row: edit icon,
Edittitle, a dot separator, the file path as a dotted link that opens the file, and a small monospace+N -Mthat turns green/red on hover or when expanded. Rows start collapsed and follow the turn's expand/collapse state, like every other tool; - a failed edit shows its first error line in red, and expanding it shows the standard Output card;
- the expanded diff is the stock code card: a language header with the wrap and copy icon buttons, 22px-padded rows with the inset +/- bar, and the head/tail fold after 9 rows;
- one addition: a line-number gutter with the old and new file line of every row.
Colors come only from DSH theme tokens, so the view follows the active theme.
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:
modelVariants(substring ofprovider/model→ mode), first match wins.mode.- With
hashlineandstrict: false, model families oh-my-pi considers unreliable at hashline fall back tofallbackMode(defaultreplace). The list isfallbackModels, defaultkimi, 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:
editis replaced outright. Nothing of DSH'seditis used.readandgrepdelegate 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(orgrep) a file before editing it. This isfs-observation-policy. - Guarded writes: writes go through
ctx.fswith 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 with DSH's native tool-row and diff-card chrome |
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.