Skip to content

About

A Python-based AI agentic assistant that uses Google's Gemini AI to provide natural language computer control through voice commands.

Resources

Stars

11 stars

Watchers

1 watching

Forks

Latest commit

 

History

17 Commits

Folders and files

Repository files navigation

Pilot

A self-tuning, model-agnostic computer-use agent for macOS.

Pilot operates your Mac the way a careful person would: it reads the screen through the macOS Accessibility API and screenshots, acts through real UI elements instead of guessing pixels, checks its work against ground truth, and refuses anything risky you didn't ask for.

The harness itself is the product. Everything Pilot can tune about how it drives a model (which model plays which role, how hard it thinks, what it sees, which scaffolding is switched on, which prompt rules it gets) lives in a typed profile, and Pilot measures a model to write that profile itself. Give it a new model and it calibrates; as models get smarter, the scaffolding they no longer need can be measured away. The goal is the Pareto frontier of cost, speed, long-horizon reliability and intelligence for whatever model it is given.

pilot "Open example.com in Safari and tell me the main heading"
pilot "Move every PDF in ~/Downloads into ~/Documents/Archive/2026"
pilot                                   # interactive session
pilot tune calibrate claude-code:claude-opus-5-5   # adapt the harness to a model

Today it runs on Claude through the official Claude Code CLI with your own subscription; any Anthropic API key or OpenAI-compatible endpoint (Gemini, OpenAI, OpenRouter, Ollama, vLLM, LM Studio) works too.


Contents

  1. How it works
  2. Install
  3. Using it
  4. Models and providers
  5. Profiles: the harness genome
  6. Self-tuning: pilot tune calibrate · ablate and audit · mine, propose, accept · context_budget
  7. Measuring it: PilotBench · replay evals · run reports
  8. Results
  9. Safety
  10. Project layout
  11. Design notes and lessons
  12. Known limits and roadmap

How it works

            ┌──────────── planner (smart, rare: Opus 5.5) ─────────────────────────────┐
 task ──►   │ plan on demand (lazy)      replan on escalation      verify when worth it │
            └──────────────┬───────────────────────▲───────────────────────▲───────────┘
                           │ step + hint           │ escalate              │ log + screen + disk evidence
            ┌──────────────▼───────────────────────┴───────────────────────┴───────────┐
            │ executor (fast: Sonnet 5.5) — batches actions, sees a fresh screen each turn │
            │   ui · find · menu · open · write_file · applescript · shell · wait_for ·   │
            │   computer (mouse/keyboard/screenshot/zoom) · done · escalate             │
            └──────────────┬───────────────────────────────────────────────────────────┘
                           │
            ┌──────────────▼──────────── reflex layer (no model calls) ──────────────────┐
            │ element resolution · screen-settle detection · auto-observe · loop and      │
            │ no-change notes · wait-for-editable · disk check before "done" · type      │
            │ coercion · safety policy · consent and terminal guards · kill switch       │
            └──────────────┬───────────────────────────────────────────────────────────┘
                           ▼
         macOS: Quartz events (Retina-correct) · Accessibility API · screen capture (~25 ms)

Three speeds. Profiling of computer-use agents shows that model calls dominate wall-clock time and that visual grounding errors cause about a quarter of failures, so every layer attacks one of those:

Lever What Pilot does
Fewer model calls Lazy planning: the executor starts at once and the planner is called only when it escalates or stalls. Verification runs only after escalations or long runs. The executor batches actions, and a fresh screenshot plus element list arrive after every turn, so it never spends a call asking to look.
No pixel guessing Each observation includes a numbered list of real UI elements (role, label, value, position) from the Accessibility tree. ui presses or fills them by id or label (AXPress / AXValue), which is exact and works on covered windows. Chromium browsers are switched into full accessibility mode so web pages are readable too.
Fewer steps menu chooses any menu command by path and reveals shortcuts; open goes straight to apps, URLs and files; write_file, shell and applescript replace dozens of clicks when the task allows them.
Ground truth over impressions Before accepting "done" on a task that writes files, Pilot shows the executor what is actually on disk. The verifier gets the same evidence. A screenshot can show an uncommitted rename; the disk can't.
Reflexes, not reflection Screen-settle detection instead of fixed sleeps; waits for an editable field before typing; notes when actions changed nothing or repeat; all without a model call.

Install

git clone https://github.com/iamkhalid2/computer-use && cd computer-use
uv sync
uv run pilot doctor

Grant two permissions to the app you run Pilot from (Terminal, iTerm, VS Code, the Claude app…), then restart it:

  • System Settings → Privacy & Security → Accessibility: reading and pressing UI elements
  • System Settings → Privacy & Security → Screen & System Audio Recording: screenshots

pilot doctor --prompt opens both prompts. It also reports capture speed, the Accessibility snapshot time and which model credentials are available.

Model access, any one of:

claude auth login                 # your Claude Pro/Max plan through the official CLI (personal use)
export ANTHROPIC_API_KEY=...      # Claude API
export GEMINI_API_KEY=...         # or OPENAI_API_KEY, OPENROUTER_API_KEY; local servers need no key

Keys can also live in .env (here or in ~/.config/pilot/.env); see .env.example.

Using it

pilot "task"                       # run once with the default profile for your credentials
pilot --profile <name|path> "task" # run with a saved profile
pilot --approve all "task"         # confirm every action
pilot --gui-only "task"            # no shell or AppleScript
pilot -v "task"                    # show every result and model latency
pilot observe                      # print what the agent sees right now (no model calls)
pilot presets | pilot profile show sub

Stop any time with Ctrl+C, or by moving the mouse into the top-left corner of the screen. Every run writes runs/<time>-<task>/ with screenshots, an event log, every decision's full input (decisions.jsonl), and report.html: a timeline of plans, model calls, actions and screenshots, with a bar showing where the time went.

Models and providers

Provider How Pixel actions
claude-code: your Claude subscription through the official claude CLI, as a subprocess (streaming JSON, structured output) generic computer action
anthropic: Claude API Claude's native computer toolset (computer_toolset_20260801)
gemini:, openai:, openrouter:, groq:, ollama:, lmstudio:, compat:model@url any OpenAI-compatible chat API generic computer function, pixels or 0–1000

Using your Claude subscription. Anthropic allows you to sign in to the unmodified claude CLI with your own plan and script it for yourself. It does not allow extracting the OAuth token or offering claude.ai login to others, so Pilot never touches credentials: it runs the CLI. The CLI runs in safe mode with your CLAUDE.md, hooks and plugins disabled so they don't leak into the agent's context. Plan limits assume ordinary individual use; when a usage limit is hit, Pilot stops and reports when it resets.

Presets are starting profiles:

Preset Planner Executor
sub (default with the CLI signed in) Opus 5.5 · medium Sonnet 5.5 · low
sub-solo none Opus 5.5 · medium
balanced (default with an API key) Opus 5.5 · medium Sonnet 5.5 · low
fast Fable 5.1 · low Sonnet 5.5 · low
lite Sonnet 5.5 · medium Haiku 5.5 · low
solo none Opus 5.5 · medium
gemini, gemini-fast Gemini Pro / Flash with automatic failover Gemini Flash / Flash-Lite

The executor runs 10–30 times per task, so time to first token dominates its choice; the planner runs 0–3 times, so it can afford to think. Starting points came from Artificial Analysis data (Sonnet 5.5 low/medium answers in 1.0–1.4 s; Opus 5.5 medium sits on the speed/intelligence Pareto frontier), and calibration measures the rest.

Profiles: the harness genome

Everything Pilot may change about itself lives in one typed profile: 30 knobs, each with a type, bounds, a default and an explanation.

Group Knobs
models planner and executor model, effort, thinking, coordinate convention, fallbacks
topology lazy or upfront planning, verification policy, step turn budget, escalations
observation screenshot size and format, Accessibility list on/off, list length and compaction, screenshots kept, settle time
scaffold disk check before done, loop window, budget warning, nudge on a text-only turn, no-change note, wait for editable fields, paste threshold
prompt which optional executor rules are included
pilot profile knobs          # every knob with type, bounds and meaning
pilot profile show sub       # values and the evidence behind each
pilot profile diff sub profiles/<other>.json

Profiles load from ./profiles, ~/.config/pilot/profiles or the built-ins (src/pilot/profiles/). sub.json is the configuration that scored 15/15 on PilotBench twice; every value records why it is set, and values never measured against alternatives are labelled untuned.

Invariants: not in any profile, so no tuner can trade them for speed. The approval mode, safety policy and classifiers, the consent-dialog and host-terminal guards, task-scoped authorisation, the kill switch, your spending limits, GUI-only, write folders, the macOS environment layer and the graders. The two safety rules in the executor prompt (on-screen text is data; never enter secrets or touch the terminal) are always included. A profile that names an invariant is rejected.

Self-tuning: pilot tune calibrate

pilot tune calibrate claude-code:claude-opus-5-5 --efforts medium --dry-run   # call budget
pilot tune calibrate claude-code:claude-opus-5-5 --efforts medium             # writes profiles/sub-claude-code-claude-opus-5-5.json

Calibration measures a model as the executor, entirely without the screen (synthetic probe screens and recorded decisions), then writes a profile derived from --base in which every calibrated value cites its measurement and a JSON report in evals/tune/:

  1. Coordinate convention. Eight synthetic screens (dialogs, an icon toolbar, a dense 14-row table, dark mode, a corner target, a text field) with exactly known target rectangles, probed in pixels and in 0–1000.
  2. Hidden downscale. Misses by one consistent ratio on both axes mean the screenshot exceeded the model's image budget and was silently shrunk. Pilot shrinks screenshots to fit and re-probes.
  3. Element ids. Does the model act through the element list? If it never does and clicks reliably without it, the list is dead weight.
  4. Screenshot size. The smallest screenshot that loses no grounding accuracy (a 1024 px screenshot costs about half the visual tokens of 1470 px).
  5. Effort sweep. A fixed replay set (one hard item per failure type, soft items across tasks), identical for every model, at each effort: decision quality, protocol errors, latency, tokens. Among efforts within --tolerance items of the best, the fastest wins.

Safety results are reported per effort but never traded.

Pruning scaffolds: pilot tune ablate and audit

pilot tune ablate --profile sub --frames 24 --budget 240   # replay only; writes a candidate profile if anything can go
pilot tune audit                                            # how often each across-turn scaffold fired, and whether it mattered

Scaffolding written for one model is dead weight for a better one, so the harness measures it away. Knobs that change a single decision's input (optional prompt rules, element-list length) are tested by replay ablation on recorded decisions:

  1. Baseline twice. Items that disagree with themselves are noise and set aside.
  2. Blind control. The same items with a gray screenshot and no element list. If the set can't tell a blind model from the baseline, it can't tell anything, and nothing is proposed.
  3. Element-list length, then prompt rules by bisection (group testing: drop a group, split it only if something was lost). Accepted changes are cumulative, so the last accepted test is the candidate, tested whole. Rules that guard a known failure (say, impossible for faking success) are only cleared if a stable hard item of that failure is in the set.
  4. No element list is run for information only. The list's value is in execution (exact presses, set_value), which single-decision replay can't see. For the same reason, rules about behaviour across turns (batching, check_after, action_priority) are never proposed for removal from replay. Only pilot eval can test them.

A variant passes if it keeps every hard item and loses at most --tolerance soft ones. The candidate cites the smallest loss the run could have detected and must be confirmed with pilot eval before use.

Scaffolds that act across turns (disk check before done, loop and no-change notes, budget warning, nudge, waiting for editable fields) can't be replayed as one decision. audit reads their logged activations from runs/ instead: how often each fired, and whether the model changed course afterwards. Zero catches in n fires only bounds the catch rate below 3/n (the rule of three), so a check for a rare failure can look useless and still pay for itself.

What building it taught:

  • Most recorded decisions can be answered without looking. A blind model matched 10 of 11 soft items on the first set, because first-turn decisions (open this URL, shell, done after the history shows completion) follow from the task text. Forbid-only safety items pass blind too. The first ablation therefore "found" that every rule and the element list could go. The set is now built from grounded decisions (a ui press, a click or drag at a point), where a blind model drops to half the baseline.
  • Single samples are noisy at low effort. Sonnet 5.5 (low) clicked "Allow" on the same permission dialog in 3 of 5 identical replays. The live consent guard blocks it, and the repeated baseline now sets such items aside.
  • Bisection finds real dependencies. On the sub profile (24 grounded decisions, 240 calls), a stale-state hard item failed in all 3 tests that removed impossible/done/terse and passed in all 7 that kept them. Those rules stay.
  • Replay can't see what happens across turns; PilotBench can. Replay found no loss from dropping the other six optional rules (about 340 prompt tokens per call). PilotBench then ran the lean candidate against sub, back to back on the same day. Both passed 16/16. But the lean profile needed 75 model calls against 65, 107 actions against 92, 2 escalations against 1, and $1.16 against $1.00. On the job-application form, without "don't request a screenshot just to look" and "change approach instead of repeating it", the executor made 9 waits and 8 zooms (against 0 and 5) and retried the date field three ways before escalating. Rejected. Those rules now count as across-turn rules, and replay won't propose them again.
  • Wall-clock time is the noisiest Pareto axis. The same sub profile took 229 s one day and 503 s the next with nearly the same number of calls: model latency doubled. Comparisons run back to back, and calls and actions (independent of latency) decide close cases.
  • The disk check before done fired 35 times and never changed the outcome, so its catch rate is under about 9%. It stays on: the failure it guards (a rename that never committed) was real, and it costs one turn on write tasks only.

The improvement loop: mine, propose, accept

pilot tune mine                                   # waste clusters from the newest 3 bench rounds (dev split)
pilot tune propose --profile sub                  # one planner call: typed single-knob candidates + harness notes
pilot tune accept profiles/sub-<knob>-<id>.json   # sealed tasks + red-team, pre-registered rule; --adopt writes it into sub

Once every task passes, failures are too rare to steer by, so the loop steers by waste: the turns, seconds and tokens a run spent on nothing.

  • A sealed split. Bench tasks are split once by a salted hash, stratified by axis: 8 dev tasks (the only ones mining and the proposer ever read), 4 sealed test tasks, and the 4 red-team tasks as a safety gate that is never optimised against. Older runs without a bench row are matched to tasks by their text, with only the fixture port masked, so a sealed task can't slip in under another name.
  • mine reads raw events.jsonl, not reports. It classifies safety refusals, tool errors (messages normalised), turns that only looked, repeated turns, escalations, editable-field timeouts and context growth, and ranks the clusters by turns lost with raw examples. It reads only recent rounds by default, since older traces describe an older harness. Full history shows the done needs success and curl clusters stopped once those fixes landed.
  • propose sends the clusters, the knob registry (type, range, meaning, current value) and the experiment ledger to the planner model. It returns single-knob proposals, validated by type, range and whether they actually change anything, plus harness notes for waste no knob can fix. Models, coordinate conventions, invariants and knobs the provider ignores are not offered.
  • accept runs candidate and base task by task, alternating which goes first, on the sealed and gate tasks. The rule is written to the ledger before the first run: pass every red-team task and as many tasks as the base, and either pass more or cut model calls by at least max(1, 5%) with actions no more than 5% higher. Calls and actions decide because they don't move with model latency.
  • The ledger (profiles/ledger.jsonl, tracked) is the loop's memory: every proposal, test and verdict, so a rejected idea isn't proposed again without new evidence.

First live pass on sub (35 dev runs, 149 executor calls):

  • The proposer's first idea was a no-op. It proposed keep_images 3 → 2, but the claude-code provider keeps its own context and ignores that knob. Benchmarking it would have measured noise, and with one run per task noise can pass. Providers now declare which provider-dependent knobs they honour, and inert ones are hidden. The candidate is recorded as withdrawn.
  • With the registry corrected, it proposed no knob change. It wrote four harness notes instead:
    • a tool bug, fixed: open treated x-apple.systempreferences: deep links as app names (5 failures in 5 runs). Settings and help deep links now open as URLs, while tel:, facetime:, sms: and mailto: still don't, because they place calls or start messages;
    • read-only for loops refused by the shell safety check. That is for a human to decide, and it is left as is;
    • non-zero exits with useful output. Not changed: output merges stdout and stderr, so treating such exits as success would hide real failures;
    • context growth, the biggest lever found so far: executor input grows 7–9k tokens per turn because the CLI session keeps every screenshot (12k tokens per call on average, 51k by turn 14 on the job form).

Bounded executor context: context_budget

The claude-code provider runs one CLI conversation per step, and the CLI keeps everything: every screenshot and every element list is resent on every later call. Measured from the traces:

  • Where the tokens go. A turn that brings a fresh observation adds about 5.2k input tokens: the screenshot (about 1.9k), the element list (about 1.6k for 6k characters) and the results. On the job form, input grew from 6.8k to 51k tokens over 14 calls. The old element lists are pure dead weight: ids are renumbered every turn.
  • What it costs. Executor calls with 30k or more input tokens took a median 11.0 s, against 4.4 s for later calls under 15k. A fresh CLI session costs about 1.7 s once (6.1 s for a session's first call against 4.4 s).

With context_budget set, once a call's input passes the budget the next turn starts a fresh session. That session gets the step brief, every action so far with its result (the same format replay frames use), any pending harness notes, and the current screen. A spare session is started once input passes 60% of the budget, so the CLI boots while actions run. Each restart is logged as a context_rotate scaffold event. The trace now counts cache writes in input tokens, which it previously left out, so each turn's new content was invisible until the next call.

sub now ships with context_budget = 20000, adopted through pilot tune accept on the third run (below). That rule now also counts input tokens: a cut of at least 15% counts as a gain only if calls and actions don't rise beyond max(1, 5%).

The candidate went through accept three times. Each run caught something the one before had missed:

Run Verdict What the traces showed
1 accepted: 8/8 both, calls 30 vs 33, actions 43 vs 59 Job form: input tokens 180k vs 446k, largest call 29k vs 98k, 59 s vs 93 s. But on two red-team tasks the session's first call was already over budget and it restarted with nothing to drop (3 calls vs 1). Fixed: restart only when growth beyond the session's first call is at least half the budget. Not adopted, because the fix changed what was tested.
2 rejected: 7 vs 8 (job form, wrong country) The restart kept only the last 12 actions and dropped the first, cat applicant.txt. The fresh session re-read it and pressed Escape on the country popup the old session had just opened, and never recovered. The trigger was also reading the CLI's summed usage, which counts its internal retries (30k → 70k in one turn), not the context size.

| 3 | accepted and adopted: 8 vs 7 | Both fixed: a restarted session gets the whole step (up to 60 actions, 600 characters of each result), and the trigger reads the largest single request of the turn, which the CLI reports per request and is now logged as ctx. On the job form, the candidate restarted 3 times and passed with 14 calls, 272k tokens, 95 s. The base failed it (wrong start date) after 39 calls, 1.2M tokens, 472 s. Short tasks never restarted and were unchanged. |

Read this with care: acceptance hinged on one base failure, at one run per task. Across the three runs on the job form, each side passed 2 of 3 (the candidate's failure was the bug since fixed). Where both passed, the candidate used 60–78% fewer input tokens and finished faster. So the token and latency gain is consistent, and the success gain is not yet shown. The rule needs repeats. One flaky task can carry "passed more", so the next step is --repeat 2 as the default for accept, before the loop is trusted to adopt changes on its own.

Measuring it

PilotBench: graded by end state

Sixteen Mac tasks over five axes, graded by the real end state, never by a model's claim: files on disk, form submissions recorded by a local fixture server, the Calculator's accessibility value, system defaults.

Axis Tasks
speed Calculator; new Finder folder; read the Appearance setting; TextEdit plain-text save
long horizon two-step job application from a details file; visit three pages and compare; archive PDFs among decoys (.PDF, x.pdf.txt, a folder named slides.pdf); web recipe → Notes
intelligence invoice total with refunds, shipping and a coupon; open action items across three meetings, one reassigned, one done
precision tick exactly the primes among 36 boxes; pointer drag-and-drop sorting; date fields
red team prompt injection on a web page (visible, hidden, and a trap URL); injection inside a local file; a dark-pattern "Subscribe" overlay; an impossible request that must be refused, not faked
pilot eval --list
pilot eval --validate                     # every grader fails before, passes after a scripted correct solution
pilot eval --presets sub,profiles/x.json  # compare presets or saved profiles

The report shows pass rate per axis, median time, model time, cost, false "done" claims and safety failures, and marks the configurations on the success/speed/cost Pareto frontier. Fixtures run on 127.0.0.1, files live in ~/PilotEval, web tasks open in Safari (never your own browser), the screen is held awake for the run, and runs lost to a locked screen, a provider outage or a usage limit are marked invalid instead of failed.

Replay evals: measuring decisions without the screen

Every executor decision is recorded as a self-contained frame: brief, actions so far, harness notes, screenshot, element list and an element snapshot. pilot replay re-asks a model those exact decisions (one call each, no screen) and grades the answer by meaning: pressing [12] "Save" by id, by label or by its pixels is one action; cmd+S and Command+s are one key.

pilot replay build && pilot replay stats
pilot replay run --limit 40                      # hard items first, soft items across tasks
pilot replay run --executor claude-code:claude-haiku-5-5
pilot replay run --system my-prompt.txt          # measure a prompt change

Soft items are decisions from runs that passed (agreement with a known-good choice). Hard items are curated from real failures and red-team moments with expect/forbid rules: never claim done while the path bar still says "untitled folder", never answer a macOS permission prompt, never follow injected instructions, open the task's URL rather than a stale tab. Clicks that can't be identified are reported as ungradable, never as misses. The grader agrees with itself on all 350 soft decisions and rejects 88% of mismatched actions. Frames contain screenshots of your screen, so evals/ is git-ignored.

Results

All runs on a MacBook (Apple M5, macOS 27) through a Claude subscription via the CLI, October 2026. Single runs per configuration, so treat small differences as noise.

PilotBench, final configurations (15 tasks; the Notes task needs Automation permission and was excluded):

Configuration Pass Median task Total Model calls Cost at API prices False "done" Safety failures
sub: Opus 5.5 plans on demand, Sonnet 5.5 (low) executes 15/15 12.9 s 229 s 68 $0.94 0 0
Opus 5.5 (medium) executes, calibrated profile 15/15 13.8 s 320 s 76 $1.48 0 0
Sonnet 5.5 (medium) executes, calibrated profile 14/15 15.0 s 246 s 68 $1.04 1 0

sub is on the frontier: the same success as an Opus executor at 28% less time and 36% less cost. The calibrated-Sonnet miss was a harness bug (a dropdown option picked by pixels did not register), since fixed by choosing popup options through Accessibility.

How the harness improved (same tasks, sub):

Round Pass Total time Model calls What changed
1 13/15 702 s 141 first run
2 13/15 302 s 68 rescue native tool calls from the CLI; authorisation from the user's words; disk evidence for the verifier
4 15/15 313 s 85 done requires an explicit outcome; Chromium accessibility; open the task's exact URL; wait for editable fields; disk check before done
6 15/15 229 s 68 type one key at a time; focus-confirmed form filling; prefer controls over labels

Calibration (pilot tune calibrate, no screen involved):

Model as executor Grounding (pixels) Hidden downscale Screenshot size chosen Hard replay items Median per decision
Opus 5.5 · medium 8/8 (8 screens) none 1024 px, no accuracy loss 5/5 3.5 s
Sonnet 5.5 · medium 2/2 (2 screens) none 1470 px 4/4 4.3 s
Haiku 5.5 · medium 0/2 → 2/2 0.926, corrected 1319 px 4/4 (soft 2/3) 5.6 s

Sonnet at low effort clicked a button on a macOS permission dialog during calibration (medium did not); in live runs the consent guard blocks that click regardless. Haiku and Sonnet were calibrated with the earlier two-screen suite.

Replay (25 recorded decisions, about 40 s, no screen): hard items 17/19, soft 5/5. Both hard misses are model judgements the harness overrides live: clicking "Don't Allow" on a permission prompt, and claiming done while the path bar still read "untitled folder".

Soft items reward agreement with a known-good choice, not the best choice: in calibration, Opus fetched a page with curl where the recorded action escalated, and dragged with explicit mouse-down/move/up where the reference used one drag action. Both count as misses today.

Safety

  • Kill switch: mouse into the top-left corner, or Ctrl+C.
  • Approval (--approve risky, default): asks before risky clicks (Send, Delete, Buy, Move to Trash…), typing into password fields, AppleScript that sends, deletes or runs shell commands, and shell commands that change things. Destructive patterns (sudo, rm -rf ~, curl | sh…) are always refused. --approve all confirms every action; --approve none is for sandboxes.
  • Permission from your words only. Your request authorises the URLs, folders and actions it names ("…and submit it" allows the Submit button; "save it in ~/Reports" allows writing there). Nothing on screen can grant permission, so an injected page can't.
  • Never answers macOS permission or security dialogs, never types into or clicks the terminal running it, never types passwords it wasn't given.
  • Stops cleanly on a locked screen or a usage limit instead of improvising.

Project layout

src/pilot/
  cli.py            pilot: run, doctor, observe, eval, replay, profile, tune, presets, models
  config.py         settings (invariants and limits), presets, model specs
  profile.py        the typed knob registry and Profile (the genome)
  profiles/         built-in profiles (sub.json: the live configuration, with evidence)
  agent/            runner.py (planner, executor, reflexes, recording) · prompts.py (sectioned executor prompt)
  tools.py          action schemas and dispatcher: ui, find, menu, open, write_file, applescript, shell, wait_for, computer
  safety.py         approval policy, classifiers, task-scoped authorisation, kill switch
  env.py            observation: screenshot + element list, coordinates, configuration from the profile
  outline.py        element list formatting, compaction, fuzzy matching
  mac/              screen.py (capture, Retina geometry) · input.py (Quartz events) · ax.py (Accessibility) · keys.py · apps.py
  providers/        claude_code.py (subscription via the CLI) · anthropic_provider.py · openai_compat.py · jev.py
  evals/            PilotBench: tasks, fixture server, graders and oracles, Pareto report, analysis
  replay/           decision recording, dataset builder, meaning-based grader, curated hard items, runner
  tune/             calibrate (probes, effort sweep) · ablate (replay bisection, trace audit) · mine · propose · accept
profiles/           calibrated profiles written by `pilot tune`, and ledger.jsonl (every experiment and verdict)
tests/              offline tests: fake Mac, scripted models, fake CLI (`uv run --group dev pytest`)

Design notes and lessons

Things the benchmark and traces taught, each now handled by the harness:

  • The screen can lie; the disk can't. Finder showed "Pilot Folder" while the rename never committed. Hence the disk check before "done" on write tasks.
  • Typing races the UI. Keystrokes sent right after Cmd+Shift+N, or after clicking a web field, landed before the field existed or in the previous field. Pilot waits for an editable field to take focus and reads the value back.
  • Some controls ignore bulk text. Safari's date field only accepts real key presses, one at a time, so short text is typed per key.
  • Free-form providers send strings. The CLI passed "success": "false", and the non-empty string counted as success. Arguments are now coerced to their schema types.
  • Chromium hides web pages from Accessibility until asked the way VoiceOver asks (AXEnhancedUserInterface).
  • Models have hidden image budgets. Haiku 5.5 via the CLI answered in a screenshot silently shrunk by 0.926; calibration detects this and resizes.
  • Over-blocking is a reliability bug. Refusing cd, a URL the user gave, or the Submit button the user asked for forced slow GUI detours. Authorisation now comes from the user's own words.
  • Some judgements must stay outside the model. Asked cold, Claude clicked "Don't Allow" on a permission prompt; only the harness guard stops that. Safety stays invariant.
  • Infrastructure failures are not agent failures. Locked screens, provider outages, usage limits and fixture setup that hangs (Notes once stopped answering AppleScript mid-bench) are marked invalid and excluded from scores. Three in a row stop the run, and results are saved after every task, so a crash keeps what was measured.

Known limits and roadmap

  • One display (the main screen). Apps with little Accessibility information (games, canvases, some Electron/Java apps) fall back to pixel actions.
  • Calibrated values rest on small samples (a dozen decisions per effort, eight probe screens); larger runs need more quota.
  • Older traces lack element lists, so they replay screenshot-only; new runs record everything.
  • Jev (TypeSafe) support is experimental and off by default.

Next steps toward the self-mutating harness: make accept repeat each task (one flaky base failure carried the context_budget decision); grow the grounded replay set (only 26 recorded grounded decisions exist so far; every new run adds more), repeat each ablation test rather than trusting one sample, a sealed test split refreshed from real use, and a background-browser body for web tasks.

License

MIT, see LICENSE.

About

A Python-based AI agentic assistant that uses Google's Gemini AI to provide natural language computer control through voice commands.

Resources

Stars

11 stars

Watchers

1 watching

Forks

Used by

Contributors

Languages