# omp-edit
[oh-my-pi](https://github.com/can1357/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):
```
[src/app.py#1A2B]
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:` | 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]` | 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.
## 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
```bash
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:
```bash
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`:
```yaml
- 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
```
## Tests
```bash
npm test # node --test test/*.test.mjs — 32 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, diff cards |
| `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](https://git.rizzy.dev/DSH-Plugins/omp-lsp), which appends language-server diagnostics to every edit.