# editor

> A shared code workspace — open buffers, a file tree, unified diffs, fuzzy find and conflict-safe saves that an agent and a person see the same view of, plus a console editor page.

| field | value |
|-------|-------|
| version | 0.1.3 |
| type | binary |
| repo | https://github.com/iii-hq/workers |
| supported_targets | x86_64-apple-darwin, aarch64-apple-darwin, i686-pc-windows-msvc, x86_64-pc-windows-msvc, aarch64-pc-windows-msvc, x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-unknown-linux-musl, armv7-unknown-linux-gnueabihf |
| author | iii |

## installation

```sh
iii worker add editor@0.1.3
```

## configuration

```yaml
- workers:

```

## dependencies

- `shell` @ `^0.10.3`
- `state` @ `^0.21.3`
- `configuration` @ `^0.21.6`

## readme

# editor

<p align="center">
  <img alt="The editor page: an agent's turn on the left, the changes it made in the middle, and the diff of one of them on the right" src="https://raw.githubusercontent.com/iii-hq/workers/main/editor/assets/editor-changes.png" width="100%">
</p>

A code workspace that an agent and a person share. Open a folder, and the
buffers you have open, the folders you have expanded, and the version each
buffer was read at are one record on the bus — so the file an agent opens
appears in your tabs, and the file you open is one the agent can see.

The unit is a **folder**, not a repository. The tree, the tabs, the editor and
the finder all work in a plain directory; git adds a branch label and change
marks when the root happens to be a repo, and nothing else changes when it
is not.

When an agent is working, its edits land in the **changes** tab as they
happen: one group per turn with the files it touched and the lines it moved,
and the diff of any one of them a click away. Nothing is polled — the worker
observes every filesystem call the agent makes and pushes an `editor::changed`
event, so a write made by anything shows up, including tools that never call
this worker.

It opens no files itself. Reads, writes, moves, listings and `git` all go
through the [`shell`](https://github.com/iii-hq/workers/tree/main/shell) worker,
so shell's jail and denylist are the only filesystem boundary; the workspace
record lives in [`state`](https://github.com/iii-hq/workers/tree/main/state).
What `editor` adds is the model on top: diffing, ranking paths, refusing a
stale write, and keeping open buffers correct when a folder moves under them.

## Install

```bash
iii worker add editor
iii worker add shell   # required — editor has no filesystem access of its own
iii worker add state   # required — the workspace record lives here
```

### Companion workers

| Worker | Why |
|---|---|
| [`shell`](https://github.com/iii-hq/workers/tree/main/shell) | Required. Every read, write, move, listing (`coder::tree`) and `git` invocation. Its `fs.host_roots` jail governs which paths `editor` can reach. |
| [`state`](https://github.com/iii-hq/workers/tree/main/state) | Required. Holds the active root and one session per project (open buffers, expanded folders). |
| [`console`](https://github.com/iii-hq/workers/tree/main/console) | Optional. Renders the `#/ext/editor` page and the `editor::*` chat cards. |

## Quickstart

```rust
use iii_sdk::protocol::TriggerRequest;
use iii_sdk::{register_worker, InitOptions};
use serde_json::json;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let iii = register_worker("ws://localhost:49134", InitOptions::default());
    let call = |id: &str, payload| iii.trigger(TriggerRequest {
        function_id: id.into(), payload, action: None, timeout_ms: Some(30_000),
    });

    // Any folder. No repository required.
    call("editor::workspace::open", json!({ "root": "/srv/app" })).await?;

    let file = call("editor::open", json!({ "path": "src/main.rs" })).await?;
    let edited = file["content"].as_str().unwrap().replace("TODO", "done");

    // Show the change before making it.
    let preview = call("editor::diff", json!({
        "before": file["content"], "after": edited, "path": "src/main.rs",
    })).await?;
    println!("{}", preview["patch"].as_str().unwrap());

    // The version from the open is what makes this safe: if anything else
    // touched the file in between, nothing is written and the divergence
    // comes back as a patch. `expected_mtime` also works and is still
    // honoured, but it cannot see a write that landed inside the same second.
    let saved = call("editor::save", json!({
        "path": "src/main.rs", "content": edited, "expected_version": file["version"],
    })).await?;
    if saved["conflict"] == true {
        println!("refused:\n{}", saved["conflict_patch"].as_str().unwrap());
    }
    Ok(())
}
```

## Functions

| Function | Does |
|---|---|
| `editor::workspace::open` | Point the workspace at a folder. Returns the buffers and expanded folders remembered for it. |
| `editor::workspace::get` | The active root, open buffers, and expanded folders — what every surface sees. |
| `editor::tree` | List a folder, with the workspace's expansion state. The walk, the noise-folder excludes and the jail are shell's. |
| `editor::open` | Read a text file and record it as an open buffer, with its language id and the content version to save against. |
| `editor::save` | Whole-file write, refused when the file changed since the open it started from. The refusal carries the disk-vs-yours diff. |
| `editor::buffers::list` | Files currently open. |
| `editor::buffers::close` | Close one buffer. The file on disk is untouched. |
| `editor::move` | Move or rename, then rewrite every open buffer and expanded folder at or under the path. |
| `editor::create` | Create a file or folder, parents included. A file may be seeded with content. |
| `editor::delete` | Remove a path and close any buffer it held. |
| `editor::find` | Fuzzy file finder, ranked basename-first. Candidates from git in a repo, from the folder listing otherwise. |
| `editor::search` | Search file contents across the workspace, grouped by file. shell's recursive grep, shaped for a results panel. |
| `editor::diff` | Unified patch between two texts. Pure — no path is read, so it works on content that is not on disk yet. |
| `editor::git::status` | Branch, upstream, ahead/behind, and one typed row per changed path. |
| `editor::git::hunks` | What changed in one file: the rendered patch plus its line ranges. |
| `editor::git::show` | A file's contents at a revision, HEAD by default. Pair it with the working copy to render a diff without parsing a patch. |
| `editor::git::commit` | Stage and commit. `committed: false` when there was nothing staged. |
| `editor::git::sync` | Fetch, fast-forward pull, or push, with ahead/behind after. |
| `editor::git::stash` | Stash the working tree, or pop the most recent stash. |
| `editor::git::undo-commit` | `reset --soft HEAD~1`, returning the SHA and message undone. |

Pull is `--ff-only` on purpose: a merge under open buffers is how an editor
ends up showing a conflicted tree nobody asked for. A repository that needs
interactive credentials will hit `git_timeout_ms` rather than hang, because
`shell::exec` owns the process.

Two are worth calling out. `editor::diff` is the one an agent reaches for most:
it can show exactly what a write will change before making it. And `editor::move`
exists because `shell::fs::mv` alone leaves open buffers pointing at the old
path — the next save then writes them back there, silently recreating the folder
that was just moved.

## Custom trigger types

| Trigger type | Fires when | Payload |
|---|---|---|
| `editor::changed` | A file in the workspace changed, whoever changed it | `path`, `cause` (the function id that did it), `kind` (`created` \| `modified` \| `deleted` \| `unknown`), `added`, `removed`, `patch`, `truncated`, `root` |

The event is how a surface follows an agent without polling. It does not
require the agent to cooperate: the worker binds a `harness::hook::post-trigger`
hook on the `shell::*` and `coder::*` write paths, so an edit made by anything
becomes an event. The hook is advisory and fail-open — it never delays or
denies the write that produced it.

```rust
use iii_sdk::protocol::RegisterTriggerInput;

iii.register_trigger(RegisterTriggerInput {
    trigger_type: "editor::changed".to_string(),
    function_id: "my-worker::on-edit".to_string(),
    config: serde_json::json!({}),
    metadata: None,
})?;
```

Bindings take no config. Delivery is fire-and-forget: a slow or absent
subscriber is logged and skipped. `patch` is capped at 16 KiB with `truncated`
set — ask `editor::git::hunks` when you need the whole thing.

## Console page

`#/ext/editor` is a view over the same workspace: a collapsible file tree on
the left with a files/search switch, tabs on the right, and a save that
surfaces the conflict guard as a dialog. The open file has its own view strip
— `read`, `edit`, `preview` on a markdown file, `unsaved` while there is
something unsaved, and `head` for the diff against the last commit. A status
line under it carries the path, language, line count, git deltas, saved state
and the most recent observed edit. A git strip along the bottom does commit,
fetch, pull, push, stash and pop. Folder expansion round-trips through the
worker, so it survives a reload and both surfaces agree on it.

Nothing is polled: one read on mount seeds the git overlay, and after that the
page reacts to `editor::changed` and to the workspace's own `state` scope. So a
file an agent edits lights up as the edit lands and an open tab you have not
typed in reloads under you. A tab you *have* edited is never reloaded; it is
flagged, and the conflict guard decides the outcome.

`editor::*` calls also render as themselves in chat and traces: a diff as a
diff, a save as a file card with its line counts.

Files and diffs are drawn with [`@pierre/diffs`](https://www.npmjs.com/package/@pierre/diffs)
— real line numbers, a syntax theme, and its own add/delete colouring, all
inside its own shadow root. `editor::diff` and `editor::git::hunks` already
return unified patch text, which is exactly what it parses. Editing stays on
the console's shared Monaco `CodeEditor`: it is the one editing surface, and
the SOP forbids bundling a second editor to get chrome back. Bundling
`@pierre/diffs` as-is costs 10.3 MB — over the console's 8 MiB per-asset cap,
and almost all of it shiki's full grammar and theme catalogs — so `ui/build.mjs`
narrows those catalogs to what this worker opens and asserts both the size
budget and that highlighting still works.

## Configuration

Runtime config lives in the `configuration` worker under id `editor`, so the
console's Workers tab can edit it and every field hot-reloads — handlers read
the live snapshot per call, and nothing needs a restart. The block below is what
gets seeded when nothing is stored yet, and `--config <path>` takes a file in
that shape as an optional one-time seed that never overwrites a stored value.
The committed `config.yaml` is *not* that file: it is a bare engine config
(`workers: []`) for running this worker from source, and it would be rejected as
a worker seed.

If the configuration worker cannot be reached at boot, the worker retries, then
starts on the `--config` seed rather than exiting — with a warning naming which
numbers are in force — and keeps asking in the background until the
authoritative value lands. The built-in defaults are used only when nothing was
seeded either.

Every field is a bound. Nothing here grants access — that is `shell`'s config.

```yaml
max_diff_bytes: 2000000     # per side of editor::diff, bytes
diff_context_lines: 3       # editor::diff's default context; hunks defaults to 0
find_limit: 50              # rows returned by editor::find
max_find_candidates: 50000  # paths scanned per editor::find call
max_file_bytes: 2000000     # largest file editor::open will pull back
search_max_matches: 2000    # matching lines editor::search collects
git_timeout_ms: 15000       # per git invocation handed to shell::exec
```

## Local development & testing

```bash
iii -c config.yaml   # a bare engine (workers: []) so this worker runs from source
cargo run --release -- --url ws://127.0.0.1:49134
cargo test
```

`--url` also reads `III_URL`, which the worker manager injects — inside a
sandbox the engine is on the VM's gateway, never on its loopback.

The console assets are built from `ui/` by `build.rs`. For the hot-reload loop:

```bash
cd ui && pnpm install && pnpm watch    # esbuild --watch → dist/
III_EDITOR_UI_WATCH=1 cargo run        # re-registers each changed asset
```

## api reference

```json
{
  "functions": [
    {
      "description": "Close one open buffer. The file on disk is untouched.",
      "metadata": {},
      "name": "editor::buffers::close",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "path": {
            "description": "Root-relative path of the buffer to close.",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "title": "BufferCloseInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Buffer": {
            "description": "One open file. `mtime` is what the conflict guard compares against, so it is part of the shared record rather than per-surface bookkeeping — two surfaces editing one file must agree on which version they started from.",
            "properties": {
              "language": {
                "description": "Monaco language id for the path.",
                "type": "string"
              },
              "mtime": {
                "description": "Last-modified time this buffer was read at, Unix seconds.",
                "format": "int64",
                "type": "integer"
              },
              "path": {
                "description": "Path relative to the workspace root.",
                "type": "string"
              },
              "version": {
                "default": "",
                "description": "Opaque version of the content this buffer was read at — the same fact `mtime` carries, in the form that survives two writes inside one second. A surface saves against it by sending it as `expected_version`.\n\nDefaulted so a session persisted before this field existed still loads. Empty means \"unknown\": a surface holding an empty version has only the mtime to save against, which is the behaviour it had all along.",
                "type": "string"
              }
            },
            "required": [
              "language",
              "mtime",
              "path"
            ],
            "type": "object"
          }
        },
        "properties": {
          "buffers": {
            "items": {
              "$ref": "#/definitions/Buffer"
            },
            "type": "array"
          },
          "closed": {
            "description": "False when nothing was open at that path.",
            "type": "boolean"
          },
          "root": {
            "type": "string"
          }
        },
        "required": [
          "buffers",
          "closed",
          "root"
        ],
        "title": "BufferCloseOutput",
        "type": "object"
      }
    },
    {
      "description": "Files currently open in the workspace.",
      "metadata": {},
      "name": "editor::buffers::list",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "EmptyInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Buffer": {
            "description": "One open file. `mtime` is what the conflict guard compares against, so it is part of the shared record rather than per-surface bookkeeping — two surfaces editing one file must agree on which version they started from.",
            "properties": {
              "language": {
                "description": "Monaco language id for the path.",
                "type": "string"
              },
              "mtime": {
                "description": "Last-modified time this buffer was read at, Unix seconds.",
                "format": "int64",
                "type": "integer"
              },
              "path": {
                "description": "Path relative to the workspace root.",
                "type": "string"
              },
              "version": {
                "default": "",
                "description": "Opaque version of the content this buffer was read at — the same fact `mtime` carries, in the form that survives two writes inside one second. A surface saves against it by sending it as `expected_version`.\n\nDefaulted so a session persisted before this field existed still loads. Empty means \"unknown\": a surface holding an empty version has only the mtime to save against, which is the behaviour it had all along.",
                "type": "string"
              }
            },
            "required": [
              "language",
              "mtime",
              "path"
            ],
            "type": "object"
          }
        },
        "properties": {
          "buffers": {
            "items": {
              "$ref": "#/definitions/Buffer"
            },
            "type": "array"
          },
          "root": {
            "type": "string"
          }
        },
        "required": [
          "buffers",
          "root"
        ],
        "title": "BuffersOutput",
        "type": "object"
      }
    },
    {
      "description": "Recent file changes in the workspace, newest first, one entry per path. Recorded by the observer for every change however it was made, so it answers what happened while nothing was watching. Each entry carries the patch, the line counts, the function that performed the write, and the harness session and turn it happened in.",
      "metadata": {},
      "name": "editor::changes",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "EmptyInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "ChangeRecord": {
            "description": "One recorded change, as `editor::changes` returns it. Mirrors the `editor::changed` event: the log is those events, kept.",
            "properties": {
              "added": {
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "cause": {
                "description": "Function that performed the write, e.g. `shell::fs::write`.",
                "type": "string"
              },
              "kind": {
                "description": "`created`, `modified`, `deleted`, or `moved`.",
                "type": "string"
              },
              "patch": {
                "default": "",
                "description": "Unified patch for the change, empty when there was nothing to compare.",
                "type": "string"
              },
              "path": {
                "description": "Path relative to the workspace root the change was recorded against.",
                "type": "string"
              },
              "removed": {
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "root": {
                "default": "",
                "type": "string"
              },
              "session_id": {
                "description": "The harness session and turn the write happened in, absent when it happened outside a turn.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "truncated": {
                "default": false,
                "type": "boolean"
              },
              "turn_id": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "added",
              "cause",
              "kind",
              "path",
              "removed"
            ],
            "type": "object"
          }
        },
        "description": "Response of `editor::changes`.",
        "properties": {
          "changes": {
            "description": "Newest first, one entry per path.",
            "items": {
              "$ref": "#/definitions/ChangeRecord"
            },
            "type": "array"
          }
        },
        "required": [
          "changes"
        ],
        "title": "ChangesView",
        "type": "object"
      }
    },
    {
      "description": "Create a file or folder in the workspace, with parents as needed. A file may be seeded with content.",
      "metadata": {},
      "name": "editor::create",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "EntryKind": {
            "description": "Create a file or a folder.",
            "enum": [
              "file",
              "folder"
            ],
            "type": "string"
          }
        },
        "properties": {
          "content": {
            "default": null,
            "description": "Initial contents for a file. Ignored for a folder.",
            "type": [
              "string",
              "null"
            ]
          },
          "kind": {
            "allOf": [
              {
                "$ref": "#/definitions/EntryKind"
              }
            ],
            "default": "file",
            "description": "What to create. Defaults to a file."
          },
          "path": {
            "description": "Root-relative path to create. Missing parent folders are created.",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "title": "CreateInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "EntryKind": {
            "description": "Create a file or a folder.",
            "enum": [
              "file",
              "folder"
            ],
            "type": "string"
          }
        },
        "properties": {
          "created": {
            "description": "Always true on success; the call errors rather than reporting false.",
            "type": "boolean"
          },
          "kind": {
            "$ref": "#/definitions/EntryKind"
          },
          "path": {
            "type": "string"
          }
        },
        "required": [
          "created",
          "kind",
          "path"
        ],
        "title": "CreateOutput",
        "type": "object"
      }
    },
    {
      "description": "Remove a path and close any buffer it held. An open buffer for a deleted file would recreate it on the next save.",
      "metadata": {},
      "name": "editor::delete",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "path": {
            "description": "Root-relative path to remove.",
            "type": "string"
          },
          "recursive": {
            "default": false,
            "description": "Required to remove a non-empty folder.",
            "type": "boolean"
          }
        },
        "required": [
          "path"
        ],
        "title": "DeleteInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Buffer": {
            "description": "One open file. `mtime` is what the conflict guard compares against, so it is part of the shared record rather than per-surface bookkeeping — two surfaces editing one file must agree on which version they started from.",
            "properties": {
              "language": {
                "description": "Monaco language id for the path.",
                "type": "string"
              },
              "mtime": {
                "description": "Last-modified time this buffer was read at, Unix seconds.",
                "format": "int64",
                "type": "integer"
              },
              "path": {
                "description": "Path relative to the workspace root.",
                "type": "string"
              },
              "version": {
                "default": "",
                "description": "Opaque version of the content this buffer was read at — the same fact `mtime` carries, in the form that survives two writes inside one second. A surface saves against it by sending it as `expected_version`.\n\nDefaulted so a session persisted before this field existed still loads. Empty means \"unknown\": a surface holding an empty version has only the mtime to save against, which is the behaviour it had all along.",
                "type": "string"
              }
            },
            "required": [
              "language",
              "mtime",
              "path"
            ],
            "type": "object"
          }
        },
        "properties": {
          "buffers": {
            "items": {
              "$ref": "#/definitions/Buffer"
            },
            "type": "array"
          },
          "buffers_closed": {
            "description": "Buffers closed because their file is gone. Leaving them open would let the next save recreate a file the user just deleted.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "deleted": {
            "type": "boolean"
          },
          "path": {
            "type": "string"
          }
        },
        "required": [
          "buffers",
          "buffers_closed",
          "deleted",
          "path"
        ],
        "title": "DeleteOutput",
        "type": "object"
      }
    },
    {
      "description": "Unified diff between two texts. Pure: nothing is read from disk. Use it to show what an edit will do before writing it, or to explain what a write did.",
      "metadata": {},
      "name": "editor::diff",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "after": {
            "description": "The text as it will be.",
            "type": "string"
          },
          "before": {
            "description": "The text as it was.",
            "type": "string"
          },
          "context_lines": {
            "default": null,
            "description": "Unchanged lines kept around each hunk (`-U` of `git diff`). Defaults to the worker's `diff_context_lines`.",
            "format": "uint32",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "path": {
            "default": null,
            "description": "Path used to label the patch header. Nothing is read from disk — this is presentation only.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "after",
          "before"
        ],
        "title": "DiffInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Hunk": {
            "description": "One `@@` block: the line ranges it covers on each side, plus the counts a gutter needs without re-reading the patch body.",
            "properties": {
              "added": {
                "description": "Lines added within this hunk.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "new_lines": {
                "description": "Number of \"after\" lines the hunk spans.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "new_start": {
                "description": "First line of the hunk on the \"after\" side, 1-based.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "old_lines": {
                "description": "Number of \"before\" lines the hunk spans.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "old_start": {
                "description": "First line of the hunk on the \"before\" side, 1-based. `0` when the before side is empty (pure addition), matching unified-diff convention.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "removed": {
                "description": "Lines removed within this hunk.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              }
            },
            "required": [
              "added",
              "new_lines",
              "new_start",
              "old_lines",
              "old_start",
              "removed"
            ],
            "type": "object"
          }
        },
        "description": "A rendered patch plus the structured view of the same edits.",
        "properties": {
          "added": {
            "description": "Total lines added across every hunk.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "hunks": {
            "description": "One entry per `@@` block, in file order.",
            "items": {
              "$ref": "#/definitions/Hunk"
            },
            "type": "array"
          },
          "identical": {
            "description": "True when `before` and `after` are byte-identical.",
            "type": "boolean"
          },
          "patch": {
            "description": "Unified diff. Empty when the two sides are identical.",
            "type": "string"
          },
          "removed": {
            "description": "Total lines removed across every hunk.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "truncated": {
            "description": "True when either side exceeded `max_bytes` and the diff was skipped. `patch` and `hunks` are empty in that case — a caller that ignores this flag would read \"no changes\" from a file that was simply too big.",
            "type": "boolean"
          }
        },
        "required": [
          "added",
          "hunks",
          "identical",
          "patch",
          "removed",
          "truncated"
        ],
        "title": "DiffResult",
        "type": "object"
      }
    },
    {
      "description": "Fuzzy file finder over the workspace, ranked the way an editor's open-file palette ranks. Candidates come from git when the root is a repository and from the folder listing when it is not.",
      "metadata": {},
      "name": "editor::find",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "include_untracked": {
            "default": true,
            "description": "Include files git does not track yet (still honouring `.gitignore`). On by default — a file the agent just created is exactly the one you are looking for. Ignored outside a repository, where the workspace listing is the source of candidates.",
            "type": "boolean"
          },
          "limit": {
            "default": null,
            "description": "Rows to return. Defaults to the worker's `find_limit`.",
            "format": "uint32",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "query": {
            "description": "Fuzzy query. Matched as a subsequence against every tracked path, with basename and word-boundary hits ranked highest. Empty returns the first `limit` paths unranked.",
            "type": "string"
          }
        },
        "required": [
          "query"
        ],
        "title": "FindInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "FindMatch": {
            "properties": {
              "path": {
                "type": "string"
              },
              "positions": {
                "description": "Byte offsets into `path` that matched, in order — enough to highlight the match without re-running the matcher in the UI.",
                "items": {
                  "format": "uint32",
                  "minimum": 0,
                  "type": "integer"
                },
                "type": "array"
              },
              "score": {
                "description": "Higher is better. Comparable only within one response.",
                "format": "int32",
                "type": "integer"
              }
            },
            "required": [
              "path",
              "positions",
              "score"
            ],
            "type": "object"
          }
        },
        "properties": {
          "from_git": {
            "description": "True when candidates came from git's listing (so `.gitignore` was honoured), false when they came from the folder walk.",
            "type": "boolean"
          },
          "matches": {
            "items": {
              "$ref": "#/definitions/FindMatch"
            },
            "type": "array"
          },
          "scanned": {
            "description": "Paths considered. Compare against `truncated` to know whether the whole repo was ranked.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "truncated": {
            "description": "True when not every path in the workspace was ranked: it held more than `max_find_candidates`, or the folder walk stopped at its visit budget. Either way this ranking is over a prefix of the workspace.",
            "type": "boolean"
          }
        },
        "required": [
          "from_git",
          "matches",
          "scanned",
          "truncated"
        ],
        "title": "FindOutput",
        "type": "object"
      }
    },
    {
      "description": "Stage and commit. Returns the new SHA, or committed:false when there was nothing staged.",
      "metadata": {},
      "name": "editor::git::commit",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "cwd": {
            "default": null,
            "description": "Repository to run in. Defaults to the workspace root.",
            "type": [
              "string",
              "null"
            ]
          },
          "message": {
            "description": "Commit message. Passed as a single `-m` argument.",
            "type": "string"
          },
          "stage_all": {
            "default": true,
            "description": "Stage every change first (`git add -A`). On by default, matching what an editor's commit command does; pass false to commit only the index.",
            "type": "boolean"
          }
        },
        "required": [
          "message"
        ],
        "title": "GitCommitInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "committed": {
            "description": "False when there was nothing staged to commit.",
            "type": "boolean"
          },
          "sha": {
            "description": "Full SHA of the new commit, when one was made.",
            "type": [
              "string",
              "null"
            ]
          },
          "summary": {
            "description": "git's own summary line, verbatim.",
            "type": "string"
          }
        },
        "required": [
          "committed",
          "summary"
        ],
        "title": "GitCommitOutput",
        "type": "object"
      }
    },
    {
      "description": "What changed in one file: the rendered patch plus its line ranges. Compares the working tree against the index, the index against HEAD, or the working tree against HEAD — so it shows an edit made by anything, including an agent that never called this worker.",
      "metadata": {},
      "name": "editor::git::hunks",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Against": {
            "description": "Which copy of the file the working tree is compared against.",
            "oneOf": [
              {
                "description": "Unstaged edits: working tree vs the index.",
                "enum": [
                  "worktree"
                ],
                "type": "string"
              },
              {
                "description": "Staged edits: index vs HEAD.",
                "enum": [
                  "index"
                ],
                "type": "string"
              },
              {
                "description": "Everything since the last commit: working tree vs HEAD.",
                "enum": [
                  "head"
                ],
                "type": "string"
              },
              {
                "description": "Everything not yet pushed: working tree vs the branch's upstream (`@{upstream}`). Fails when the branch has no upstream configured, which is a real answer rather than an error to swallow.",
                "enum": [
                  "upstream"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "against": {
            "allOf": [
              {
                "$ref": "#/definitions/Against"
              }
            ],
            "default": "worktree",
            "description": "Which comparison to make. Defaults to `worktree`."
          },
          "context_lines": {
            "default": null,
            "description": "Unchanged lines kept around each hunk in `patch`. Defaults to 0, which keeps `hunks` exactly the lines that changed — the ranges a gutter paints. Pass 3 or so for a patch a person will read, and note that the reported ranges widen to include the context you asked for.",
            "format": "uint32",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "cwd": {
            "default": null,
            "description": "Directory to run git in. Defaults to the workspace root.",
            "type": [
              "string",
              "null"
            ]
          },
          "path": {
            "description": "Repository-relative path to inspect.",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "title": "GitHunksInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Against": {
            "description": "Which copy of the file the working tree is compared against.",
            "oneOf": [
              {
                "description": "Unstaged edits: working tree vs the index.",
                "enum": [
                  "worktree"
                ],
                "type": "string"
              },
              {
                "description": "Staged edits: index vs HEAD.",
                "enum": [
                  "index"
                ],
                "type": "string"
              },
              {
                "description": "Everything since the last commit: working tree vs HEAD.",
                "enum": [
                  "head"
                ],
                "type": "string"
              },
              {
                "description": "Everything not yet pushed: working tree vs the branch's upstream (`@{upstream}`). Fails when the branch has no upstream configured, which is a real answer rather than an error to swallow.",
                "enum": [
                  "upstream"
                ],
                "type": "string"
              }
            ]
          },
          "Hunk": {
            "description": "One `@@` block: the line ranges it covers on each side, plus the counts a gutter needs without re-reading the patch body.",
            "properties": {
              "added": {
                "description": "Lines added within this hunk.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "new_lines": {
                "description": "Number of \"after\" lines the hunk spans.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "new_start": {
                "description": "First line of the hunk on the \"after\" side, 1-based.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "old_lines": {
                "description": "Number of \"before\" lines the hunk spans.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "old_start": {
                "description": "First line of the hunk on the \"before\" side, 1-based. `0` when the before side is empty (pure addition), matching unified-diff convention.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              },
              "removed": {
                "description": "Lines removed within this hunk.",
                "format": "uint32",
                "minimum": 0,
                "type": "integer"
              }
            },
            "required": [
              "added",
              "new_lines",
              "new_start",
              "old_lines",
              "old_start",
              "removed"
            ],
            "type": "object"
          }
        },
        "properties": {
          "added": {
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "against": {
            "allOf": [
              {
                "$ref": "#/definitions/Against"
              }
            ],
            "description": "Echo of the comparison performed, so a cached response is self-describing."
          },
          "hunks": {
            "description": "Changed ranges, in file order. Empty when the file matches.",
            "items": {
              "$ref": "#/definitions/Hunk"
            },
            "type": "array"
          },
          "patch": {
            "description": "The rendered unified patch, for showing a person what changed. Empty when there is no difference, and capped by `max_diff_bytes` — a caller that only wants the ranges can ignore it.",
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "removed": {
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "untracked": {
            "description": "True when git reports the path as untracked, in which case there is nothing to compare against and `hunks` is empty.",
            "type": "boolean"
          }
        },
        "required": [
          "added",
          "against",
          "hunks",
          "patch",
          "path",
          "removed",
          "untracked"
        ],
        "title": "GitHunksOutput",
        "type": "object"
      }
    },
    {
      "description": "Read a file's contents at a revision (HEAD by default). Pair it with the working copy to render a real side-by-side or unified diff, rather than parsing a patch.",
      "metadata": {},
      "name": "editor::git::show",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "cwd": {
            "default": null,
            "description": "Repository to run in. Defaults to the workspace root.",
            "type": [
              "string",
              "null"
            ]
          },
          "path": {
            "description": "Root-relative path.",
            "type": "string"
          },
          "rev": {
            "default": null,
            "description": "Revision to read the file at. Defaults to `HEAD`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "path"
        ],
        "title": "GitShowInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "content": {
            "description": "The file's contents at that revision. Empty when the path did not exist there — which is what `exists: false` distinguishes from an empty file.",
            "type": "string"
          },
          "exists": {
            "description": "False when the path is absent at that revision (a file being added).",
            "type": "boolean"
          },
          "path": {
            "type": "string"
          },
          "rev": {
            "type": "string"
          }
        },
        "required": [
          "content",
          "exists",
          "path",
          "rev"
        ],
        "title": "GitShowOutput",
        "type": "object"
      }
    },
    {
      "description": "Stash the working tree, or pop the most recent stash.",
      "metadata": {},
      "name": "editor::git::stash",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "StashAction": {
            "enum": [
              "push",
              "pop"
            ],
            "type": "string"
          }
        },
        "properties": {
          "action": {
            "allOf": [
              {
                "$ref": "#/definitions/StashAction"
              }
            ],
            "description": "Stash the working tree, or restore the most recent stash."
          },
          "cwd": {
            "default": null,
            "description": "Repository to run in. Defaults to the workspace root.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "action"
        ],
        "title": "GitStashInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "StashAction": {
            "enum": [
              "push",
              "pop"
            ],
            "type": "string"
          }
        },
        "properties": {
          "action": {
            "$ref": "#/definitions/StashAction"
          },
          "ok": {
            "type": "boolean"
          },
          "summary": {
            "type": "string"
          }
        },
        "required": [
          "action",
          "ok",
          "summary"
        ],
        "title": "GitStashOutput",
        "type": "object"
      }
    },
    {
      "description": "Working-tree status as typed rows: branch, upstream, ahead/behind, and one entry per changed path. Fails when the root is not a repository — that is an absent overlay, not a broken workspace.",
      "metadata": {},
      "name": "editor::git::status",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "cwd": {
            "default": null,
            "description": "Directory to run git in. Defaults to the workspace root. Confined by shell's jail exactly like any other `shell::exec` call.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "GitStatusInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "StatusEntry": {
            "description": "A single changed path as git sees it.",
            "properties": {
              "index": {
                "description": "Status of the staged copy: `modified`, `added`, `deleted`, `renamed`, `copied`, `untracked`, `ignored`, `conflicted`, or `unchanged`.",
                "type": "string"
              },
              "path": {
                "description": "Path relative to the repository root.",
                "type": "string"
              },
              "renamed_from": {
                "description": "Original path for a rename or copy.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "staged": {
                "description": "True when the staged copy differs from HEAD.",
                "type": "boolean"
              },
              "worktree": {
                "description": "Status of the working-tree copy, same vocabulary as `index`.",
                "type": "string"
              }
            },
            "required": [
              "index",
              "path",
              "staged",
              "worktree"
            ],
            "type": "object"
          }
        },
        "description": "Branch header plus every changed path.",
        "properties": {
          "ahead": {
            "description": "Commits on this branch the upstream does not have.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "behind": {
            "description": "Commits on the upstream this branch does not have.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "branch": {
            "description": "Current branch, or `None` on a detached HEAD.",
            "type": [
              "string",
              "null"
            ]
          },
          "clean": {
            "description": "True when nothing is modified, staged, or untracked.",
            "type": "boolean"
          },
          "entries": {
            "description": "One row per changed path, in git's order.",
            "items": {
              "$ref": "#/definitions/StatusEntry"
            },
            "type": "array"
          },
          "upstream": {
            "description": "Configured upstream, e.g. `origin/main`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "ahead",
          "behind",
          "clean",
          "entries"
        ],
        "title": "StatusReport",
        "type": "object"
      }
    },
    {
      "description": "Fetch, fast-forward pull, or push. Pull is --ff-only on purpose: a merge under open buffers is how an editor ends up showing a conflicted tree it never asked for.",
      "metadata": {},
      "name": "editor::git::sync",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "SyncAction": {
            "description": "Which remote operation to run.",
            "oneOf": [
              {
                "enum": [
                  "fetch",
                  "push"
                ],
                "type": "string"
              },
              {
                "description": "Fast-forward only. A pull that would merge fails instead, so this can never produce a conflicted tree under open buffers.",
                "enum": [
                  "pull"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "action": {
            "allOf": [
              {
                "$ref": "#/definitions/SyncAction"
              }
            ],
            "description": "Which remote operation to run."
          },
          "cwd": {
            "default": null,
            "description": "Repository to run in. Defaults to the workspace root.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "action"
        ],
        "title": "GitSyncInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "SyncAction": {
            "description": "Which remote operation to run.",
            "oneOf": [
              {
                "enum": [
                  "fetch",
                  "push"
                ],
                "type": "string"
              },
              {
                "description": "Fast-forward only. A pull that would merge fails instead, so this can never produce a conflicted tree under open buffers.",
                "enum": [
                  "pull"
                ],
                "type": "string"
              }
            ]
          }
        },
        "properties": {
          "action": {
            "$ref": "#/definitions/SyncAction"
          },
          "ahead": {
            "description": "Ahead/behind after the operation, so a caller does not need a second round trip to find out whether it changed anything.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "behind": {
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "ok": {
            "type": "boolean"
          },
          "summary": {
            "description": "git's output, trimmed. Both streams: git reports progress on stderr.",
            "type": "string"
          }
        },
        "required": [
          "action",
          "ahead",
          "behind",
          "ok",
          "summary"
        ],
        "title": "GitSyncOutput",
        "type": "object"
      }
    },
    {
      "description": "Undo the last commit, keeping its changes staged (reset --soft HEAD~1). Returns the SHA and message that were undone.",
      "metadata": {},
      "name": "editor::git::undo-commit",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "cwd": {
            "default": null,
            "description": "Repository to run in. Defaults to the workspace root.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "GitUndoCommitInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "message": {
            "description": "Its message, so a caller can put it straight back in a commit box.",
            "type": "string"
          },
          "undone_sha": {
            "description": "SHA that was undone.",
            "type": "string"
          }
        },
        "required": [
          "message",
          "undone_sha"
        ],
        "title": "GitUndoCommitOutput",
        "type": "object"
      }
    },
    {
      "description": "Move or rename a path and rewrite every open buffer and expanded folder at or under it. Moving a folder with `shell::fs::mv` alone leaves buffers pointing at the old location, which silently recreates it on the next save.",
      "metadata": {},
      "name": "editor::move",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "from": {
            "description": "Existing root-relative path.",
            "type": "string"
          },
          "to": {
            "description": "Destination root-relative path.",
            "type": "string"
          }
        },
        "required": [
          "from",
          "to"
        ],
        "title": "MoveInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Buffer": {
            "description": "One open file. `mtime` is what the conflict guard compares against, so it is part of the shared record rather than per-surface bookkeeping — two surfaces editing one file must agree on which version they started from.",
            "properties": {
              "language": {
                "description": "Monaco language id for the path.",
                "type": "string"
              },
              "mtime": {
                "description": "Last-modified time this buffer was read at, Unix seconds.",
                "format": "int64",
                "type": "integer"
              },
              "path": {
                "description": "Path relative to the workspace root.",
                "type": "string"
              },
              "version": {
                "default": "",
                "description": "Opaque version of the content this buffer was read at — the same fact `mtime` carries, in the form that survives two writes inside one second. A surface saves against it by sending it as `expected_version`.\n\nDefaulted so a session persisted before this field existed still loads. Empty means \"unknown\": a surface holding an empty version has only the mtime to save against, which is the behaviour it had all along.",
                "type": "string"
              }
            },
            "required": [
              "language",
              "mtime",
              "path"
            ],
            "type": "object"
          }
        },
        "properties": {
          "buffers": {
            "items": {
              "$ref": "#/definitions/Buffer"
            },
            "type": "array"
          },
          "from": {
            "type": "string"
          },
          "remapped": {
            "description": "Open buffers and expanded folders rewritten to the new location. A folder move rewrites everything beneath it, which is the whole reason moves go through this function instead of `shell::fs::mv` directly.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "root": {
            "type": "string"
          },
          "to": {
            "type": "string"
          }
        },
        "required": [
          "buffers",
          "from",
          "remapped",
          "root",
          "to"
        ],
        "title": "MoveOutput",
        "type": "object"
      }
    },
    {
      "description": "Internal: hot-reload the editor's limits from the authoritative configuration when it changes.",
      "metadata": {
        "internal": true,
        "trace_hidden": true
      },
      "name": "editor::on-config-change",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Internal `editor::on-config-change` payload. The handler re-fetches the authoritative value, so this carries only the (advisory) id; a struct rather than a `Value` keeps the request schema concrete.",
        "properties": {
          "id": {
            "default": null,
            "description": "Configuration id that changed (advisory; the handler re-fetches).",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "OnConfigChangeEvent",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "required": [
          "ok"
        ],
        "title": "OnConfigChangeResponse",
        "type": "object"
      }
    },
    {
      "description": "Internal: turns a filesystem call made by anything into an editor::changed event. Observes only — always continues.",
      "metadata": {
        "internal": true,
        "trace_hidden": true
      },
      "name": "editor::on-file-change",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "HookCall": {
            "properties": {
              "arguments": {
                "default": null
              },
              "function_id": {
                "type": "string"
              }
            },
            "required": [
              "function_id"
            ],
            "type": "object"
          },
          "HookResult": {
            "description": "The part of the hook's result payload that says whether the call worked.",
            "properties": {
              "is_error": {
                "default": false,
                "type": "boolean"
              }
            },
            "type": "object"
          }
        },
        "description": "The subset of the harness hook payload this worker reads.\n\n`session_id` and `turn_id` come from the hook envelope, which is the only place the identity of the writer exists: the call itself says a file was written, not who was writing. Carrying them onto the event is what lets a surface say *this* agent session made *this* change, rather than reporting an anonymous edit. Both stay optional — a hook fired outside a turn (or by an operator reproducing one) has no session, and that is not an error.",
        "properties": {
          "call": {
            "anyOf": [
              {
                "$ref": "#/definitions/HookCall"
              },
              {
                "type": "null"
              }
            ]
          },
          "metadata": {
            "default": null
          },
          "result": {
            "anyOf": [
              {
                "$ref": "#/definitions/HookResult"
              },
              {
                "type": "null"
              }
            ],
            "description": "Outcome of the call this hook is reporting on. A write that failed did not change anything, and the hook fires either way."
          },
          "session_id": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          },
          "turn_id": {
            "default": null,
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "HookInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Always `continue`. This hook observes; it never decides.",
        "properties": {
          "decision": {
            "type": "string"
          }
        },
        "required": [
          "decision"
        ],
        "title": "HookOutput",
        "type": "object"
      }
    },
    {
      "description": "Read a text file and record it as an open buffer, with the metadata needed to write it back safely: its language id, and the mtime and content version to hand to editor::save.",
      "metadata": {},
      "name": "editor::open",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "path": {
            "description": "Jail-relative when `shell`'s `fs.host_roots` are set, else absolute — the same path vocabulary as `shell::fs::read`.",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "title": "OpenInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "content": {
            "description": "File contents as text. Binary files are refused rather than mangled.",
            "type": "string"
          },
          "language": {
            "description": "Monaco language id for the path, e.g. `rust`, `typescript`, `plaintext`.",
            "type": "string"
          },
          "mtime": {
            "description": "Last-modified time, Unix seconds. Pass it back as `expected_mtime` on `editor::save` to get the conflict guard.",
            "format": "int64",
            "type": "integer"
          },
          "path": {
            "type": "string"
          },
          "size": {
            "format": "uint64",
            "minimum": 0,
            "type": "integer"
          },
          "truncated": {
            "description": "True when the file exceeded `max_file_bytes` and `content` holds only its beginning. Saving a truncated buffer back would delete the rest of the file, so `editor::save` refuses one.",
            "type": "boolean"
          },
          "version": {
            "description": "Opaque version of the `content` above. Pass it back as `expected_version` on `editor::save` for a guard that does not depend on the filesystem's timestamp resolution: it catches a write that landed inside the same second, which `mtime` cannot. Compare it for equality only — the encoding is this worker's business and may change.",
            "type": "string"
          }
        },
        "required": [
          "content",
          "language",
          "mtime",
          "path",
          "size",
          "truncated",
          "version"
        ],
        "title": "OpenOutput",
        "type": "object"
      }
    },
    {
      "description": "Write a file, refusing the write when it changed underneath since the editor::open it started from. On refusal the divergence comes back as a patch.",
      "metadata": {},
      "name": "editor::save",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "content": {
            "description": "Full new contents. This is a whole-file write, not a patch.",
            "type": "string"
          },
          "expected_mtime": {
            "default": null,
            "description": "The `mtime` from the `editor::open` this edit started from. When it no longer matches the file on disk, the write is refused and the divergence comes back as a patch. Omit only when deliberately overwriting whatever is there.\n\nResolution is one second, which is all the filesystem reports. Two writes inside the same second are therefore indistinguishable to this field, and the second one wins silently — send `expected_version` instead to close that window.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "expected_version": {
            "default": null,
            "description": "The `version` from the `editor::open` — or from the previous `editor::save` — this edit started from. It is a version of the *content*, so it catches a write that landed inside the same filesystem second and it does not care whether a clock or a checkout moved the mtime.\n\nWhen this is present it is the guard and `expected_mtime` is ignored. When it is absent the `expected_mtime` comparison applies exactly as before, so a caller that has never heard of a version is unaffected.",
            "type": [
              "string",
              "null"
            ]
          },
          "path": {
            "type": "string"
          }
        },
        "required": [
          "content",
          "path"
        ],
        "title": "SaveInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "added": {
            "description": "Lines this save added relative to what was on disk before it.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "conflict": {
            "description": "True when the write was refused because the file changed underneath.",
            "type": "boolean"
          },
          "conflict_patch": {
            "description": "On conflict: a unified diff from the current disk contents to the contents you tried to write, so the divergence is reviewable without a second round trip.",
            "type": [
              "string",
              "null"
            ]
          },
          "created": {
            "description": "True when the file did not exist and was created.",
            "type": "boolean"
          },
          "disk_mtime": {
            "description": "What was on disk when a conflict was detected.",
            "format": "int64",
            "type": [
              "integer",
              "null"
            ]
          },
          "disk_version": {
            "description": "Version of the disk contents when a conflict was detected — the value to send as `expected_version` once you have reconciled with them.",
            "type": [
              "string",
              "null"
            ]
          },
          "mtime": {
            "description": "Last-modified time after the write. Feed it into the next save.",
            "format": "int64",
            "type": "integer"
          },
          "path": {
            "type": "string"
          },
          "removed": {
            "description": "Lines this save removed.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "saved": {
            "description": "True when the file was written.",
            "type": "boolean"
          },
          "version": {
            "description": "Version of the content this file now holds: what was written on success, what is on disk on a conflict. Feed it into the next save as `expected_version`.",
            "type": "string"
          }
        },
        "required": [
          "added",
          "conflict",
          "created",
          "mtime",
          "path",
          "removed",
          "saved",
          "version"
        ],
        "title": "SaveOutput",
        "type": "object"
      }
    },
    {
      "description": "Search file contents across the workspace, grouped by file — the shell worker's recursive grep, shaped into what a results panel renders.",
      "metadata": {},
      "name": "editor::search",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "ignore_case": {
            "default": false,
            "description": "Match case-insensitively.",
            "type": "boolean"
          },
          "include_glob": {
            "default": [],
            "description": "Glob filters restricting which files are searched, e.g. `[\"**/*.rs\"]`.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "max_matches": {
            "default": null,
            "description": "Stop after this many matching lines. Defaults to the worker's `search_max_matches`.",
            "format": "uint64",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "pattern": {
            "description": "Rust regex matched against each line.",
            "type": "string"
          }
        },
        "required": [
          "pattern"
        ],
        "title": "SearchInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "SearchFile": {
            "properties": {
              "hits": {
                "items": {
                  "$ref": "#/definitions/SearchHit"
                },
                "type": "array"
              },
              "path": {
                "type": "string"
              }
            },
            "required": [
              "hits",
              "path"
            ],
            "type": "object"
          },
          "SearchHit": {
            "properties": {
              "line": {
                "description": "1-based line number.",
                "format": "uint64",
                "minimum": 0,
                "type": "integer"
              },
              "text": {
                "description": "The matching line, as shell returned it.",
                "type": "string"
              }
            },
            "required": [
              "line",
              "text"
            ],
            "type": "object"
          }
        },
        "properties": {
          "files": {
            "description": "Matches grouped by file, in first-match order — the shape a result panel renders, rather than a flat list every caller has to group.",
            "items": {
              "$ref": "#/definitions/SearchFile"
            },
            "type": "array"
          },
          "total": {
            "description": "Total matching lines across every file.",
            "format": "uint32",
            "minimum": 0,
            "type": "integer"
          },
          "truncated": {
            "description": "True when the search stopped at `max_matches`.",
            "type": "boolean"
          }
        },
        "required": [
          "files",
          "total",
          "truncated"
        ],
        "title": "SearchOutput",
        "type": "object"
      }
    },
    {
      "description": "List a folder in the workspace, with the expansion state the workspace remembers. The walk, the noise-folder excludes and the jail are the shell worker's.",
      "metadata": {},
      "name": "editor::tree",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "collapse": {
            "default": [],
            "description": "Root-relative folders to collapse. Collapsing takes its descendants with it.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "expand": {
            "default": [],
            "description": "Root-relative folders to mark expanded before listing. Expansion is part of the shared workspace, so it survives a reload and both surfaces agree on it.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "max_depth": {
            "default": null,
            "description": "Levels to descend. Defaults to 4 — deep enough to navigate, shallow enough that one call does not walk a whole monorepo.",
            "format": "uint32",
            "minimum": 0,
            "type": [
              "integer",
              "null"
            ]
          },
          "path": {
            "default": null,
            "description": "Folder to list, relative to the workspace root. Defaults to the root.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "title": "TreeInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "expanded": {
            "description": "Folders the workspace has expanded, root-relative.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "path": {
            "description": "Canonical absolute path of the listed folder, as shell resolved it.",
            "type": "string"
          },
          "root": {
            "type": "string"
          },
          "tree": {
            "description": "The listing exactly as the shell worker returned it (nested `{name, kind, size, mtime, children}` nodes). Passed through rather than re-modelled so this worker does not pin shell's response shape."
          }
        },
        "required": [
          "expanded",
          "path",
          "root",
          "tree"
        ],
        "title": "TreeOutput",
        "type": "object"
      }
    },
    {
      "description": "Serve the editor worker's injected console UI assets (content function for its console:script / console:style triggers).",
      "metadata": {
        "internal": true
      },
      "name": "editor::ui-content",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Input of the content function: the console asks for one asset by path.",
        "properties": {
          "path": {
            "description": "The asset path from the trigger config (e.g. `state/page.js`).",
            "type": "string"
          }
        },
        "required": [
          "path"
        ],
        "title": "UiContentInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "description": "Output of the content function.",
        "properties": {
          "content": {
            "description": "The asset source, verbatim.",
            "type": "string"
          },
          "content_type": {
            "description": "MIME type the console should serve the asset with.",
            "type": "string"
          }
        },
        "required": [
          "content",
          "content_type"
        ],
        "title": "UiContentResult",
        "type": "object"
      }
    },
    {
      "description": "The active workspace: its root, the files open against it, and which folders are expanded. Shared by every surface, so this is what the agent and the console both see.",
      "metadata": {},
      "name": "editor::workspace::get",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "title": "EmptyInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Buffer": {
            "description": "One open file. `mtime` is what the conflict guard compares against, so it is part of the shared record rather than per-surface bookkeeping — two surfaces editing one file must agree on which version they started from.",
            "properties": {
              "language": {
                "description": "Monaco language id for the path.",
                "type": "string"
              },
              "mtime": {
                "description": "Last-modified time this buffer was read at, Unix seconds.",
                "format": "int64",
                "type": "integer"
              },
              "path": {
                "description": "Path relative to the workspace root.",
                "type": "string"
              },
              "version": {
                "default": "",
                "description": "Opaque version of the content this buffer was read at — the same fact `mtime` carries, in the form that survives two writes inside one second. A surface saves against it by sending it as `expected_version`.\n\nDefaulted so a session persisted before this field existed still loads. Empty means \"unknown\": a surface holding an empty version has only the mtime to save against, which is the behaviour it had all along.",
                "type": "string"
              }
            },
            "required": [
              "language",
              "mtime",
              "path"
            ],
            "type": "object"
          }
        },
        "properties": {
          "buffers": {
            "description": "Files currently open against this root, shared by every surface.",
            "items": {
              "$ref": "#/definitions/Buffer"
            },
            "type": "array"
          },
          "expanded": {
            "description": "Folders expanded in the tree, root-relative.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "root": {
            "description": "The active root every other path in this response is relative to.",
            "type": "string"
          }
        },
        "required": [
          "buffers",
          "expanded",
          "root"
        ],
        "title": "WorkspaceView",
        "type": "object"
      }
    },
    {
      "description": "Set the directory the editor works in. Any folder will do — a git repository is an overlay, not a requirement. Returns the buffers and expanded folders remembered for it.",
      "metadata": {},
      "name": "editor::workspace::open",
      "request_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "properties": {
          "root": {
            "description": "Directory to work in. Jail-relative when shell's `fs.host_roots` are set, else absolute. A plain folder is enough — a git repository is an overlay, never a requirement.",
            "type": "string"
          }
        },
        "required": [
          "root"
        ],
        "title": "WorkspaceOpenInput",
        "type": "object"
      },
      "response_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "definitions": {
          "Buffer": {
            "description": "One open file. `mtime` is what the conflict guard compares against, so it is part of the shared record rather than per-surface bookkeeping — two surfaces editing one file must agree on which version they started from.",
            "properties": {
              "language": {
                "description": "Monaco language id for the path.",
                "type": "string"
              },
              "mtime": {
                "description": "Last-modified time this buffer was read at, Unix seconds.",
                "format": "int64",
                "type": "integer"
              },
              "path": {
                "description": "Path relative to the workspace root.",
                "type": "string"
              },
              "version": {
                "default": "",
                "description": "Opaque version of the content this buffer was read at — the same fact `mtime` carries, in the form that survives two writes inside one second. A surface saves against it by sending it as `expected_version`.\n\nDefaulted so a session persisted before this field existed still loads. Empty means \"unknown\": a surface holding an empty version has only the mtime to save against, which is the behaviour it had all along.",
                "type": "string"
              }
            },
            "required": [
              "language",
              "mtime",
              "path"
            ],
            "type": "object"
          }
        },
        "properties": {
          "buffers": {
            "description": "Files currently open against this root, shared by every surface.",
            "items": {
              "$ref": "#/definitions/Buffer"
            },
            "type": "array"
          },
          "expanded": {
            "description": "Folders expanded in the tree, root-relative.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "root": {
            "description": "The active root every other path in this response is relative to.",
            "type": "string"
          }
        },
        "required": [
          "buffers",
          "expanded",
          "root"
        ],
        "title": "WorkspaceView",
        "type": "object"
      }
    }
  ],
  "triggers": [
    {
      "description": "Fires when a file in the workspace changes, whoever changed it — including an agent that never called this worker.",
      "invocation_schema": {},
      "metadata": {},
      "name": "editor::changed",
      "return_schema": {}
    }
  ]
}
```
