Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,8 @@ jobs:

# gpui_linux (wayland + x11), font-kit's fontconfig, reqwest's default
# native-tls, and rodio's ALSA backend (`preview::playback`) are the only
# things that need system libraries.
# mold replaces the single-threaded default linker; macOS already ships a parallel one and
# Windows-MSVC cannot select rust-lld on stable.
# things that need system libraries. mold replaces the single-threaded
# default linker; macOS already ships a parallel one.
- name: Install Linux system libraries
if: runner.os == 'Linux'
run: |
Expand All @@ -91,6 +90,14 @@ jobs:
- name: Install cargo-nextest
uses: taiki-e/install-action@nextest

# rust-lld ships with Rust and accepts the MSVC linker interface, avoiding the slower link.exe
# without another installation step. This runs before rust-cache so its key includes the flag.
- name: Configure Windows linker
if: runner.os == 'Windows'
shell: pwsh
run: |
'RUSTFLAGS=-C linker=rust-lld' >> $env:GITHUB_ENV

- name: Cache cargo build
uses: Swatinem/rust-cache@v2

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ jobs:
build-windows:
needs: version
runs-on: windows-latest
# rust-lld ships with Rust and accepts the MSVC linker interface, avoiding the slower link.exe
# without another installation step.
env:
RUSTFLAGS: -C linker=rust-lld
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
Expand Down
157 changes: 18 additions & 139 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,152 +1,31 @@
# Agent instructions

For any agent runtime, not just one vendor. Two unrelated jobs live here — pick the one you are
actually doing.
- **Changing qrate code?** Read [CLAUDE.md](CLAUDE.md).
- **Reviewing the project open in qrate?** Use bridge protocol 2 below. The saved `.qrate` file can omit unsaved on-screen edits.

- **Changing qrate's code?** Read [CLAUDE.md](CLAUDE.md). It is the contributor guide: crate map,
build and test commands, code style, git workflow. It is not Claude-specific despite the name,
and nothing in it is repeated below.
- **Reading the metadata in a running qrate?** That is the rest of this file.
## Live qrate bridge

## Reading a running qrate
qrate publishes `agent-bridge.json` in the platform application-data directory. It contains `url`, a per-launch `token`, and `bridge_protocol: 2`. If it is missing, qrate is not running or Settings ▸ Agent is off. Reject any other protocol version.

qrate can hand you the metadata it currently holds in memory, including cell edits the archivist
has not saved yet. This is the only route to that state — the `.qrate` file on disk is the last
save, not what is on screen.
POST one JSON request to `url` with `Authorization: Bearer <token>`, `Content-Type: application/json`, and `X-Agent: codex`. The agent name is a visible label, not authority. Re-read the endpoint after `forbidden` because the token changes each launch.

### The bridge cannot change a cell, and that is the point
The bridge never changes a cell. `stage_findings` only replaces that agent's draft findings in the Problems panel and optionally offers whole-cell replacements in the Fixes menu.

Nothing in the protocol writes data. You cannot fix a row — you can only stage what you would
change and let the archivist decide, one click at a time, in their own app. Never tell the user you
have corrected something.
qrate's bundled Pi runs as a contained child process. Stop, Restart, panel teardown, and app exit terminate its process tree; do not rely on an older Pi process surviving a restart.

The archive is somebody's real collection. When you report a problem, quote the row index and the
column name so they can find it, and say what the data says rather than what you assume it means —
a date that looks wrong may be exactly what the source object is stamped with.
### Protocol 2 methods

### Connecting
- `overview`: compact project, column, selection, diagnostic-count, and revision information.
- `query`: bounded live rows or diagnostics. Sources are `all_rows`, `selected_rows`, `rows`, `search`, and filtered `diagnostics`. Operations include `select`, `where`, `distinct`, `group_by`, `order_by`, `limit`, and revision-bound `cursor`.
- `program_save`: validate and activate a confined Luau function without running it.
- `program_run`: run the saved function once against an immutable snapshot at an exact revision.
- `thumbnails`: return at most four qrate-generated 512-pixel PNG derivatives by source row/page.
- `stage_findings`: publish a complete advisory draft batch.

qrate listens by default, but the user can switch it off under Settings ▸ Agent. If the endpoint
file is missing, qrate is either not running or the bridge is off — say which you think it is and
ask the user to check, rather than guessing at data.
Queries default to 20 and allow at most 50 records. Collection results send one `fields` array and positional `items`, plus `returned`, `remaining`, `truncated`, and `next_cursor`. Ask for the smallest useful field set and follow a cursor only when more evidence could change the answer.

```bash
# Windows
ENDPOINT="$LOCALAPPDATA/qrate/agent-bridge.json"
# macOS: ~/Library/Application Support/qrate/agent-bridge.json
# Linux: ~/.local/share/qrate/agent-bridge.json
Programs have no network, filesystem, process, clock, randomness, plugin storage, UI, raw qrate objects, or staging access. Their only linked-file methods are qrate-resolved bounded UTF-8 reads and PDF text search. Original paths and bytes are never returned.

URL=$(sed -n 's/.*"url":"\([^"]*\)".*/\1/p' "$ENDPOINT")
TOKEN=$(sed -n 's/.*"token":"\([^"]*\)".*/\1/p' "$ENDPOINT")
Every staged finding needs the exact response `revision`, source `row`, `column`, severity, one-sentence message, and exact current whole-cell `expected`; `replacement` is optional and must be the whole proposed value. A stale cell is rejected. Never report that staging corrected data.

AGENT=your-runtime-name # see below — substitute yours, do not send this literally

ask() {
curl -s -X POST "$URL" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-H "X-Agent: $AGENT" -d "$1"
}
```

Send `X-Agent` on every call. It is what the archivist sees in qrate's Agent panel beside each of
your calls, and without it you appear as `unnamed agent`.

Name the runtime, not the task: `claude-code`, `pi`, `codex`. Add a suffix only to tell concurrent
sessions of the same runtime apart — `claude-code/review-2` — because the panel groups by this
string, so two sessions sharing a name read as one agent reconnecting. If your runtime came with a
qrate skill or plugin, that is where its name is written down; this file is vendor-neutral and
cannot name it for you.

It is a label, not a credential: qrate cannot check it, so never treat it as authorisation for
anything, and never send a name that is not yours.

`sed` rather than a JSON parser on purpose — the endpoint file is two flat fields, and a machine
running qrate is not guaranteed to have `python` or `jq`.

The endpoint also carries `"bridge_protocol": 1`. Callers should reject a different protocol
version rather than guessing at request semantics. The token is regenerated every launch. Re-read
the file if a call starts returning `forbidden`.

If your runtime cannot run a shell, any HTTP client will do: POST the request JSON to `url` with the
`Authorization: Bearer <token>` and `X-Agent` headers. The bridge ignores the method and path, so it
is only HTTP to the extent that an ordinary client can talk to it.

### The requests

One JSON object per call: `{"method": ..., "params": ...}`. `params` is omitted for the methods
that take none.

| Call | Returns |
|---|---|
| `{"method":"project_summary"}` | project name, row and column counts, whether a files folder is set |
| `{"method":"columns"}` | every column with its configured data type and notes |
| `{"method":"rows","params":{"rows":[0,1,2]}}` | those rows as column/value pairs — max 100 per call |
| `{"method":"search_rows","params":{"query":"1974","limit":20}}` | rows whose column name or value contains the query, case-insensitive |
| `{"method":"diagnostics"}` | what qrate's own validators already flagged, with severity and source |
| `{"method":"selected_rows"}` | source-row indices the archivist has selected right now |
| `{"method":"stage_findings","params":{…}}` | publishes your findings as drafts — see below |

Every response carries a `revision`. If it changes between calls, the archivist edited something
mid-review — re-read anything you are about to quote.

Row indices are **source** rows and ignore any active filter, so an index from `selected_rows` or
`diagnostics` can be passed straight to `rows`.

### Handing findings back

`stage_findings` is how a review reaches the archivist. Each finding lands in qrate's Problems panel
beside its own validators' output, and a finding that carries a `replacement` also puts that value
in the cell's right-click **Fixes** menu — which the archivist has to open and click. Staging writes
nothing.

```json
{"method":"stage_findings","params":{
"revision": 12,
"findings": [{
"row": 41,
"column": "Date",
"severity": "warning",
"message": "the title says 1962 but this column reads 1926 — likely transposed digits",
"expected": "1926",
"replacement": "1962"
}]
}}
```

- `revision` is the one from the reads this batch is based on. Echo it; do not invent it.
- `expected` is the cell text you judged, copied exactly from what `rows` gave you. qrate drops any
finding whose cell no longer says that, and withholds the fix later if the cell changes before the
archivist opens the menu. This is the guard that stops you proposing an edit to text nobody read —
getting `expected` wrong silently loses the finding.
- `replacement` is the cell's **whole** new text, not a fragment. Omit it for an observation with no
fix to offer; most findings should omit it.
- `severity`: `error`, `warning`, or `note`. Use `note` freely and `error` only for something
demonstrably wrong.

The reply is `{"accepted": n, "stale": [indices]}`, where `stale` indexes into the batch you sent.
Re-read those rows and re-stage them if they still matter.

Each batch **replaces** the last one you staged. Send everything you stand by in one call, and send
an empty `findings` list to retract. Max 200 findings per batch, 512 characters per message —
the panel shows one line per finding, so write one sentence.

### Working order

1. `project_summary` and `columns` first — the column names and notes tell you what the collection
is and what each field is supposed to hold. Review nothing before reading them.
2. `diagnostics` next. qrate already found the malformed dates and misspellings; repeating them adds
nothing. Your value is what a validator cannot see — a title that contradicts its own date field,
a creator spelled two ways across rows, a description that belongs to a different object.
3. Then read rows. `selected_rows` when the ask is about "these", `search_rows` when it is about a
term, explicit indices when the diagnostics pointed at them.
4. Report by row index and column name. Separate "this is wrong" from "this looks inconsistent".
5. `stage_findings` last, once, with everything you stand by — so the archivist sees the review in
the app rather than only in your transcript. Tell them it is staged and still theirs to accept.

Errors come back as `{"error": ...}`: `project_unavailable` means no project is open,
`table_unavailable` means no table is loaded yet, and the rest are bad requests — check the row
count and query limits above before retrying.

The contract's types are `crates/ai/src/agent.rs`, the transport is `crates/app/src/agent_bridge.rs`,
the live-table adapter is `crates/table/src/agent.rs`, and the panel that shows the archivist what
you did is `crates/workspace/src/panels/agent.rs`. Change any of them and this file is part of the
change.
The contract is `crates/ai/src/agent.rs`, transport is `crates/app/src/agent_bridge.rs`, live adapter is `crates/table/src/agent.rs`, private runner is `crates/plugin-host/src/agent_program.rs`, and the audit panel is `crates/workspace/src/panels/agent.rs`. Keep this file synchronized when those surfaces change.
36 changes: 21 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ members = [
default-members = ["crates/app"]

[workspace.package]
version = "0.3.0-alpha.2"
version = "0.4.0-alpha.1"
edition = "2024"

# Dev runtime was crawling on the 2k-row table because unoptimized gpui/text-shaping
Expand Down
2 changes: 2 additions & 0 deletions crates/agent-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ gpui.workspace = true
log.workspace = true
settings = { path = "../settings" }

[target."cfg(windows)".dependencies]
windows = { version = "0.62.2", features = ["Win32_Foundation", "Win32_Security", "Win32_System_JobObjects", "Win32_System_Threading"] }
2 changes: 1 addition & 1 deletion crates/agent-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ mod runtime;
mod terminal;

pub use runtime::{AgentRuntime, init};
pub use terminal::{AgentTerminal, TerminalBackground, TerminalPalette, TerminalScreen};
pub use terminal::{AgentTerminal, TerminalPalette, TerminalScreen};
Loading
Loading