# Kilter tools for agents

Kilter for Mac: https://atris.ai/kilter. Agent setup guide: https://atris.ai/kilter/agents.

Kilter provides a small local tool pack for inspecting windows, previewing deterministic arrangements, applying them with strict verification, undoing them, and opening installed apps. Any model or automation framework can call it through one persistent process. Deterministic zone requests use no language model.

MCP hosts can use `Contents/Resources/integrations/windowpilot_mcp.py` from the installed app, with the separately installed official MCP Python dependency (the setup guide at https://atris.ai/kilter/agents shows a uv command that prepares it). The adapter exposes the same tools over stdio and identifies itself as Kilter. It keeps one native process alive and reports transport loss instead of silently recreating token/Undo history. The app also bundles this guide as `Contents/Resources/Kilter-Agent.md`. Configuring an LLM host and granting access remain separate from installing the app.

```sh
/Applications/Kilter.app/Contents/MacOS/WindowPilot --agent
```

Write one JSON object per line to stdin. Read one JSON response per line from stdout. Requests run sequentially; there is no port, HTTP service, socket, shell-command endpoint, or arbitrary UI-action endpoint. The process does not open the Kilter panel or stop a running GUI instance.

Keep the same process alive between Preview, Apply and Undo. Tokens and Undo history belong to that process. Agent and GUI instances use the same engine implementation, **not shared histories**. Restarting discards pending tokens and agent Undo history; applied positions remain in place. Closing stdin asks the owned local model worker to stop; if it ignores the request, its supervisor attempts a forced stop. If Kilter cannot confirm cleanup, the agent exits with an error and reports it when possible; check Activity Monitor before retrying. If the supervisor is forcibly killed, or the worker launches descendants, cleanup is not guaranteed. Before exit, the agent retries pending accessibility recovery. If accessibility or window recovery remains incomplete, it reports each outstanding problem and exits with an error. Its process-local window Undo history is lost on exit, so inspect and arrange any affected windows manually. If a caller stops reading stdout, the agent stops accepting requests after a 10-second response deadline and attempts recovery before exiting with an error. A response may be partial or may arrive after that deadline, so inspect before retrying. Error output is best effort and is suppressed when stdout and stderr refer to the same stream. Recovery itself has no fixed deadline.

## Discover capabilities

```json
{"id":"inspect-1","op":"inspect"}
```

The response contains `apiVersion`, `capabilities`, `limits`, Accessibility availability, model availability, windows, displays and `undoAvailable`. Each window includes an opaque `windowId`, owner PID, app, bundle identifier, title, display and frame. Window handles track AX identity and remain usable while the window exists in this process. They do not refer to an enumeration index. Display IDs identify connected displays. All frames use Accessibility global top-left coordinates in points, including negative coordinates across displays.

Inspection works without Accessibility access, returning availability and displays; the window list remains empty until permission is granted. `open_app` also works without Accessibility. Previewing window geometry, Apply and Undo require existing permission. This interface never grants, resets, or requests permission automatically.

Responses have this shape:

```json
{"id":"inspect-1","ok":true,"elapsedSeconds":0.01,"result":{}}
```

Failures use `ok:false` and `error:{"code":"...","message":"..."}`. Caller IDs may be strings or integers; malformed JSON and oversized input return a null ID. Timing covers operation handling, excluding time spent waiting for input. Errors do not end the loop. Each line is limited to 64 KiB; command text is limited to 500 characters and 4,096 UTF-8 bytes. Unexpected request fields are rejected.

## Preview a saved setup

Use `list_setups` to discover setup IDs, names, apps and window counts. With all saved windows open and unminimized, send `{"id":"setup-1","op":"preview_setup","setupId":"SETUP-ID"}`. The preview shows before and after frames and returns an ordinary Apply token. Kilter requires exact, unique window titles for agent previews even if the setup allows looser matching in the app. It does not open missing apps, folders or links. If the setup, windows or displays change, the Apply attempt fails and a new preview is required. Undo belongs to this agent process.

## Preview, then apply

Select a window intentionally from inspection. Substitute its opaque handle:

```json
{"id":"preview-1","op":"preview","windowId":"OPAQUE-WINDOW-ID","zone":"leftHalf"}
```

Optional `displayId` chooses a connected display; omission keeps the window on its current display. Supported zones are returned by `inspect`: halves, quarters, thirds, two-thirds, `maximize`, `center`, and `larger`. `maximize` fills the usable display area, keeping the menu bar and Dock accessible. `center` keeps size where it fits; `larger` grows by 25% and centers. Both clamp dimensions to the destination's usable bounds.

A successful preview changes no geometry. It returns an opaque `token` and placements with `before` and `after` frames. Tokens expire after five minutes; at most 32 can remain pending.

For a custom size, replace `zone` with a percentage specification. Provide exactly one of `zone`, `customSize` or `command`:

```json
{"id":"custom-1","op":"preview","windowId":"OPAQUE-WINDOW-ID","customSize":{"widthPercent":60,"heightPercent":75,"centered":true}}
```

Percentages are finite numbers from 10 through 100 of the destination's inset usable display area. `centered` defaults to true; false keeps the current top-left where it fits and clamps it inside the display. A `windowId` is required and optional `displayId` chooses another connected display. These requests use no model. Results identify `zone:"customSize"`, return the settings and a readable label, and use the same Apply token and Undo safeguards. Extra fields and sizes below 80 points are rejected. An app's own larger minimum size can still prevent Apply; the engine verifies the result and attempts to restore previous positions on failure.

```json
{"id":"apply-1","op":"apply","token":"OPAQUE-PREVIEW-TOKEN"}
{"id":"undo-1","op":"undo"}
```

An Apply attempt consumes its token, including attempts rejected because a window or display changed. Preview again after a rejection. The engine checks the captured AX identity, original frame and display before writing, verifies the resulting geometry, and attempts to restore previous positions after failed writes. If a window has moved to an unexpected position, Kilter leaves it there rather than overwrite a possible independent change. Undo retains up to ten arrangements in this agent process. A rollback or Undo failure reports that restoration could not be completed; it is not reported as success. If window restoration remains incomplete, `inspect` reports `windowRecoveryAvailable` and lists affected app names in `windowRecoveryApps`. These are separate from ordinary `undoAvailable` history and belong to the current agent process. Inspect the affected windows before retrying `undo`. Undo follows the history stack, so newer successful arrangements are undone first. If a window cannot be recovered, arrange the affected windows manually and then close the agent; unresolved history is discarded and the agent exits with an error. If an app's temporary enhanced accessibility setting cannot be restored, `inspect` reports `accessibilityRecoveryAvailable`. Use `retry_accessibility_recovery` in the same agent process; it retries that setting without using window Undo. If the app remains unavailable, restarting that app also ends the pending recovery.

Undo checks each window's position before restoring it. If it sees a different position, Undo leaves that position in place and reports the conflict. An unresolved failed-write recovery remains pending if its window is in an unexpected position. Another process can still move a window during an accessibility write, after that check.

Exact commands `left`, `left half`, `right`, `right half`, `top half`, `bottom half`, `top left`, `top right`, `bottom left`, `bottom right`, `center`, `larger`, `maximize`, and `fill display` use the selected or focused window and its current display without a model. These exact phrases also work: `move this window left`, `move this window to the left`, `put this window on the left`, `put this window on the left half`, and the corresponding four right-side phrases; `center this window`, `make this window bigger`, `make this window larger`, `make this window bigger and center it`, `make this window larger and center it`, and `maximize this window`. Case, surrounding whitespace, and one final period are ignored. Extra instructions are not ignored. These commands use the same Preview/Apply/Undo safeguards even when a backend is supplied. Other commands need the selected model.

For local language interpretation, provide `command` instead of `zone`:

```json
{"id":"preview-2","op":"preview","windowId":"OPAQUE-WINDOW-ID","command":"Make this window bigger and center it","backend":"atris"}
```

`backend` is `apple` by default, or `atris` when its existing offline runtime and verified weights are installed. A command can name open apps and displays, with up to eight target windows. A supplied window handle selects “this window”; otherwise the captured focused window is used. Destinations belong in the command. `displayId` is accepted only for deterministic zone requests. The model selects a preset; Swift resolves identities and validates geometry.

## Open an app

```json
{"id":"open-1","op":"open_app","bundleId":"com.apple.calculator"}
```

Only an installed application's exact bundle identifier is accepted. LaunchServices lookup and the resolved bundle are checked before launch; the launched app's identifier is checked again. Paths, URLs, scripts and terminal commands are not inputs. Opening activates the chosen app and does not mint a layout token or an Undo entry.

## Connect to a model's tool calls

Expose `inspect`, `list_setups`, `preview_setup`, `preview`, `apply`, `undo`, `retry_accessibility_recovery` and `open_app` in the model's tool schema. Forward validated function arguments with `op` and a caller ID to this persistent process. The following minimal Python bridge is synchronous; call it from a worker thread when embedding it in an async application:

```python
import json
import subprocess
import threading
import uuid

process = subprocess.Popen(
    ["/Applications/Kilter.app/Contents/MacOS/WindowPilot", "--agent"],
    stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1,
)
lock = threading.Lock()

def call_kilter(operation, **arguments):
    request_id = str(uuid.uuid4())
    with lock:
        process.stdin.write(json.dumps({"op": operation, "id": request_id, **arguments}) + "\n")
        process.stdin.flush()
        response = json.loads(process.stdout.readline())
    if response["id"] != request_id:
        raise RuntimeError("Kilter response ID mismatch")
    return response

# Inspect first; select a deliberate windowId before previewing any change.
state = call_kilter("inspect")
# On shutdown: process.stdin.close(); process.wait()
```

Supported operations are intentionally bounded to the published capabilities. Browser interactions, clicking arbitrary controls, scripts, swarms and general autonomous computer use are not implemented by this interface.

## Verify the pack

The integration entry point is:

```sh
/Applications/Kilter.app/Contents/MacOS/WindowPilot --agent-smoke-test
```

It creates its own fixture child and separate agent process, selects fixtures by owner PID, checks malformed/oversized recovery and operation IDs, rejects stale previews and token reuse, applies and undoes a deterministic layout, checks every original captured frame, and verifies a clean exit on stdin EOF. The app-opening check uses only a nonexistent bundle identifier. It does not launch working apps or use production setup storage.
