Files
omp-edit/README.md
T

12 KiB
Raw Blame History

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, glob and bash

Outputs are cut into pages so the model never pulls a whole file, huge search, or giant shell stream into context at once:

  • read: at most 200 lines per call (limit above 200 is capped); the footer gives the offset to continue. The UI row indicates the line range being read (Read lines A–B).
  • 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.
  • bash: output is capped at 200 lines per stream (stdout / stderr). The complete, uncut output is saved to a temporary file (/tmp/omp-edit-output/...txt), and the footer instructs the model how to continue reading with read (read file_path="..." offset=201).

Change the page size with pageLines.

Guard rule

A tools/pre-execute listener ensures file modifications go through appropriate tools while lifting restrictions on reading files via shell. Writing through shell is blocked.

Denied Use instead
sed -i, perl -i, ed/ex, tee, >/>> into files, heredocs, truncate/dd edit (hashline)
Inline scripts that write files (node -e "require('fs').writeFileSync…", python3 -c "open(…,'w')", ruby -e "File.write…", awk writes) edit / write
find -delete, find -fprint edit / file tools
str_replace_editor, apply_patch tools; write on an existing file edit (write stays allowed for brand-new files)

What stays allowed:

  • Reading files and searching via shell: cat, head, tail, less, more, bat, nl, sed -n, grep, rg, ag, find, ls -R, read-only awk are all permitted. Their output is automatically paged to 200 lines by the bash wrapper.
  • Text inside quotes is ignored (git commit -m "fix > bug").
  • Redirection to /dev/null, /dev/stderr, /dev/stdout is 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/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, Edit title, a dot separator, the file path as a dotted link that opens the file, and a small monospace +N -M that 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:

  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 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.