11 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-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 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 -Mcounts 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:
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 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.