agent-desktop
AGENTS.md4.8k starsChanged 2 days agoApache-2.0View on GitHub
Guidance for AI coding agents working in this repository. agent-desktop is a tool that AI agents invoke, not an agent. It is a Rust CLI and C library that reads native accessibility trees and returns structured JSON with ref-based element ids. The observe-act loop lives in the calling agent. It never embeds an LLM and has no GUI or interactive prompt. It does not automate web pages (use agent-browser). It cannot see UIs that expose no accessibility tree. Each platform…
We split this AGENTS.md into the rules it gives an agent
24 rules. About 0 are common, 7 are distinctive, and 17 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
17
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 59
When a plan contradicts the code, the code says what shipped.
- Distinctive: line 70
Write each deferral into the receiving phase of the local roadmap before the PR opens, with the evidence that forced it, and name it in the …
- Very specific: line 77
Never add Co-Authored-By lines, AI attribution or "Generated with" footers.
- Very specific: line 78
Use Conventional Commits: feat:, fix:, feat!: or a BREAKING CHANGE: footer, docs:, style:, refactor:, perf:, chore:, ci:, test:.
- Very specific: line 105
Only two crates wire a platform to core: the binary (src/) and the FFI crate.
- Very specific: line 121
Create crates/core/src/commands/<name>.rs with an execute() function, its tests in <name>_tests.rs, and register it in commands/mod.rs.
- Very specific: line 123
Add the CLI variant in src/cli/mod.rs and its arguments under src/cli_args/.
- Very specific: line 124
Add its arm to dispatch() and to policy_for() in src/command_policy/mod.rs.
- Very specific: line 125
Add its name to parse_command() in src/batch/mod.rs.
- Very specific: line 127
Update the command counts in README.md and in this file.
- Very specific: line 130
Update the matching skill in the same change: skills/agent-desktop/ for cross-platform behavior, the platform skill for what differs, …
- Distinctive: line 172
At most 7 fields per struct and 5 parameters per function: Use a config struct or a builder.
- Very specific: line 173
Explicit
pubboundaries: Only lib.rs re-exports. Internal modules are pub(crate). - Very specific: line 190
Every command prints one envelope.
- Very specific: line 206
Every snapshot carries data.complete.
- Very specific: line 213
Use recovery.strategy only when disposition.retry is safe.
- Very specific: line 227
All state lives under one root, ~/.agent-desktop by default.
- Very specific: line 240
Read attributes in batches with AXUIElementCopyMultipleAttributeValues, not one call per attribute.
- Distinctive: line 262
Performance: Check every substantive change for latency against the merge-base before merge.
- Distinctive: line 273
Dogfood: Every platform sub-phase drives its own surface against real software and keeps a judged report.
- Distinctive: line 274
Run it as a stranger. The operator gets the shipped skill and the built binary, and nothing else: no source, no plan.
- Distinctive: line 276
Run every example in the shipped docs verbatim in the platform's default shell.
- Very specific: line 280
Read every step's ok. Wait with the tool's own wait primitives.
- Distinctive: line 286
Every requirement in a plan maps to at least one test that fails when the requirement is violated.
≈ “Common” is an estimate: at least 1 in 5 of the 36,368 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AGENTS.md
Guidance for AI coding agents working in this repository.
agent-desktop is a tool that AI agents invoke, not an agent. It is a Rust CLI and C library that reads native accessibility trees and returns structured JSON with ref-based element ids. The observe-act loop lives in the calling agent. It never embeds an LLM and has no GUI or interactive prompt. It does not automate web pages (use agent-browser). It cannot see UIs that expose no accessibility tree.
Commands
Each platform adapter compiles only on its own OS and the workspace names all three, so an unscoped cargo build,
cargo test --workspace or cargo clippy --all-targets fails on every host. Scope cargo to the host's package set,
the same set .githooks/pre-commit uses:
HOST_PKGS=(-p agent-desktop-core -p agent-desktop-macos -p agent-desktop -p agent-desktop-ffi)
# Windows: replace agent-desktop-macos with agent-desktop-windows. Linux: with agent-desktop-linux.
# An array expands to separate arguments in both bash and zsh; a plain string does not in zsh.
cargo build "${HOST_PKGS[@]}" # debug build
cargo build --release -p agent-desktop # release CLI; CI fails it above 15 MB
cargo test "${HOST_PKGS[@]}" # every test target
cargo test "${HOST_PKGS[@]}" --lib # library tests only; skips the binary crate
cargo test -p agent-desktop # binary crate: CLI contract, dispatch, batch, policy
cargo test "${HOST_PKGS[@]}" <test_name> # one test by name
cargo clippy "${HOST_PKGS[@]}" --all-targets -- -D warnings # zero warnings
cargo fmt --all -- --check
cargo check -p agent-desktop-core --all-targets --target x86_64-pc-windows-msvc # core cross-compiles
cargo check -p agent-desktop-core --all-targets --target x86_64-unknown-linux-gnu
cargo tree -p agent-desktop-core # must name no platform crate
AGENT_DESKTOP_E2E_EXCLUSIVE=1 bash tests/e2e/run.sh # macOS E2E; builds its own artifacts
bash scripts/perf-baseline-compare.sh # macOS perf A/B against the merge-basescripts/cargo-test-isolated-home.sh test "${HOST_PKGS[@]}" --lib runs tests with a throwaway HOME, which keeps them out
of your real ~/.agent-desktop. The pre-commit hook uses it.
Enable the hook once per clone with git config core.hooksPath .githooks. For staged Rust or TOML changes it runs the
source-rule scripts (check-rust-file-size.sh, check-no-phase-references.sh, check-stale-ref-constructor-misuse.sh),
fmt, clippy and library tests over the host set. FFI changes add the C ABI passthrough test and the cbindgen header
check. Bypass it only in an emergency: git commit --no-verify or SKIP_PRECOMMIT=1 git commit ....
Where the truth lives
- Shipped behavior is the code and its tests. A doc that contradicts the code is a defect; fix it in the same change.
- Agent usage is the skills under
skills/. They compile into the binary:agent-desktop skills get desktopandagent-desktop skills get platform. - Vocabulary is
CONCEPTS.md. The JSON contract isdocs/json-output.md. Past lessons are indocs/solutions/, with YAML frontmatter (module,tags,problem_type); search it before you work in a documented area. - User docs live in
apps/docsand are served at agent-browser.dev/desktop. Use thetechnical-writerskill (.agents/skills/technical-writer/SKILL.md) when you write or edit prose: docs pages, READMEs, skills, changelog entries, commit messages, PR descriptions, and issues. The docs serve developers and the agents that call agent-desktop, so keep them complete, short, and checked against the code. - Planning documents (the phase roadmap
phases.md, sub-phase plans, brainstorms) live in.compound-engineering/at the repo root. That folder is untracked on purpose. Read it when you plan. Never link to it or cite it from a committed file, because it does not exist in any other checkout. - When a plan contradicts the code, the code says what shipped. Correct the plan in place, with no "previously said"
note, and cite what disproved it: a
probes/**/FINDINGS.mdrow id or the source line.
Build it; do not defer it
- The implementer makes the work land. "Harder than expected", "the PR is already large" and "a later phase touches this" are not reasons to defer.
- Defer only when the work is blocked: impossible on the platform, dependent on infrastructure that does not exist, or possible only by breaking a stated invariant or safety property. Name which one.
- Measure before you defer. Settle an unknown with a probe; a deferral that rests on an assumption is a guess.
- Reducing scope is the owner's call. If the work cannot land, say so and stop. Never narrow it quietly.
- Write each deferral into the receiving phase of the local roadmap before the PR opens, with the evidence that forced it, and name it in the PR description. If it changes what the current phase delivers, correct that phase too.
Branches and commits
maincarries macOS, Windows and Linux. Fixes and features branch offmainand merge back as squash merges;mainrequires linear history.- Never add
Co-Authored-Bylines, AI attribution or "Generated with" footers. - Use Conventional Commits:
feat:,fix:,feat!:or aBREAKING CHANGE:footer,docs:,style:,refactor:,perf:,chore:,ci:,test:. Writetype: imperative summaryin lowercase after the colon, and say why. - Versions follow semver from 1.0.0. A release is one PR that bumps the workspace version in
Cargo.toml,Cargo.lock,npm/package.jsonandCHANGELOG.md. Merging it starts.github/workflows/release.yml, which publishes npm and the GitHub release;docs/releasing.mdhas the steps.
Architecture
crates/core/ agent-desktop-core: PlatformAdapter trait in adapter/, shared types in domain folders,
one file per command in commands/
crates/macos/ agent-desktop-macos: AX API
crates/windows/ agent-desktop-windows: UI Automation
crates/linux/ agent-desktop-linux: AT-SPI
crates/ffi/ agent-desktop-ffi: cdylib and the committed C header include/agent_desktop.h
src/ agent-desktop binary: cli/, cli_args/, dispatch/, batch/, command_policy/, command_host/,
diagnostic/, visual_debug/
skills/ agent skills, compiled into the binary
tests/ golden fixtures, E2E harness per OS, FFI smoke tests
probes/ raw platform measurements; FINDINGS.md rows are citable evidence
apps/docs/ Geistdocs site served at agent-browser.dev/desktop as a Vercel Microfrontends child appDependency inversion (non-negotiable).
- Core defines
PlatformAdapterand every shared type. Platform crates implement the trait. - Core never imports a platform crate, and platform crates never import each other. CI fails if
cargo tree -p agent-desktop-corenames a platform crate. - Only two crates wire a platform to core: the binary (
src/) and the FFI crate. - The platform is chosen at compile time by
#[cfg(target_os)]inbuild_adapter()(src/main.rs,crates/ffi/src/adapter.rs) with target-gated dependencies insrc/Cargo.toml([target.'cfg(target_os = "macos")'.dependencies]), never unconditional dependencies behind sourcecfgs. PlatformAdapteris split acrosscrates/core/src/adapter/(mod.rsplus theactions,input,observationandsystemfragments). Every method defaults toAdapterError::not_supported, so an adapter implements only what it supports. Strict resolution (resolve_element_strict*) returnsSTALE_REFfor zero matches andAMBIGUOUS_TARGETfor two or more.- Core must run on every OS. A
#[cfg]branch in core that no CI lane executes is a hypothesis, not shipped code: add a lane or move it behindPlatformAdapter.
Dispatch. dispatch() in src/dispatch/mod.rs is a direct match that calls each command's execute() in
crates/core/src/commands/. There is no command trait or registry. Batch is not a second dispatcher: it deserializes
JSON entries into the same Commands enum, runs the same CommandPolicy preflight and calls the same dispatch().
Adding a command.
- Create
crates/core/src/commands/<name>.rswith anexecute()function, its tests in<name>_tests.rs, and register it incommands/mod.rs. - Add the CLI variant in
src/cli/mod.rsand its arguments undersrc/cli_args/. - Add its arm to
dispatch()and topolicy_for()insrc/command_policy/mod.rs. Both matches are exhaustive. - Add its name to
parse_command()insrc/batch/mod.rs;every_cli_subcommand_is_known_to_batch_parserfails otherwise. Add it to the surface lists insrc/cli/contract_command_surface_tests.rs. - Update the command counts in
README.mdand in this file;src/cli/doc_literal_drift_tests.rschecks them. - A new
Actionvariant goes incrates/core/src/actions/action.rs. A new adapter method goes in the matchingPlatformAdapterfragment with anot_supporteddefault. - Update the matching skill in the same change:
skills/agent-desktop/for cross-platform behavior, the platform skill for what differs,skills/agent-desktop-ffi/for the C ABI. The skills compile into the binary and are what agents read.
Crate layout. Core's root holds only lib.rs and domain folders named after CONCEPTS.md words: actionability,
actions, adapter, commands, context, cursor_overlay, input, locator, notifications, ref_action,
refs, response, session, snapshot, storage, system, trace, tree and wait.
- A platform crate's root holds
lib.rs,adapter.rs(the trait impl) and five layer folders, plusbin/where a helper binary exists.tree/reads, traverses and resolves the UI;actions/acts on elements;input/synthesizes raw keyboard, mouse and clipboard input;system/handles app lifecycle, windows, permissions and screenshots;notifications/handles the OS notification surface. - Inside a layer, a concept with four or more source files gets a subfolder named from the shared vocabulary
(
resolve,walk,properties,select,scroll,clipboard,keyboard,mouse,window,capture, ...). File names keep their prefix (system/window/window_ops.rs). A file named like its folder becomes itsmod.rs. - Tests sit beside their code as
foo_tests.rs, declared fromfoo.rswith#[cfg(test)] #[path = "foo_tests.rs"] mod tests;. - Capability impls live in
adapter.rsor in the matching layer'sadapter.rs.
Grounding rule. scripts/check-architecture.sh (rules in scripts/check_architecture.py) runs in the pre-commit
hook and in CI next to the source rules. Before it scans, it plants a violation of every rule, so a rule that stops
detecting fails the gate. It fails when core depends on a platform or OS crate, a platform crate reaches another,
core gains OS-conditional code outside its allow-list, a crate's top-level layout drifts, a capability is implemented
in the wrong layer, core domains reach commands/, or a test file is not declared beside its code. Its lists (core
dependencies, core domains, binary folders, OS allow-list) change only in review. The CI cargo tree isolation steps
remain the transitive check.
Coding standards
- 400 lines at most per hand-written
.rsfile (scripts/check-rust-file-size.sh). Split by responsibility. A generated.rsfile with an@generatedmarker in its first five lines is exempt, and the fix goes in the generator, not the output. Probe.ps1scripts are exempt. The C header is generated by cbindgen and checked by CI for drift: change the Rust ABI orcrates/ffi/cbindgen.tomland regenerate it withscripts/update-ffi-header.sh, never edit it by hand. - No inline comments. Names carry the meaning. Standalone
//lines, end-of-line//comments and non-doc/* */blocks are all rejected. The allowed forms are///on public items, only when the name is not enough, and//!module docs.scripts/check-rust-file-size.shenforces this together with the size cap, throughscripts/check_rust_comments.py. - No delivery-plan references in
crates/**orsrc/**: no phase, sub-phase, plan decision id (KTD<n>) or plan unit id (U<n>). Write what is true about the code and why. Probe ledger row ids such asA15-7are allowed and encouraged.scripts/check-no-phase-references.shenforces this. - One domain struct or enum per file (
node.rsholdsAccessibilityNode). One command per file, named after the command. - At most 7 fields per struct and 5 parameters per function. Use a config struct or a builder.
- Explicit
pubboundaries. Onlylib.rsre-exports. Internal modules arepub(crate). No wildcard re-exports. - Naming. Crates are
agent-desktop-<name>; module files are singularsnake_case; types arePascalCase; functions are verb-firstsnake_case; constants areSCREAMING_SNAKE_CASE; CLI flags are kebab-case.
Errors.
- No
unwrap(),expect()orpanic!outside tests; panics are test-only. Propagate with?or match. - Adapter functions return
Result<T, AdapterError>. Command handlers returnResult<serde_json::Value, AppError>.main()turns anAppErrorinto the JSON envelope and the exit code. - An error carries
code(ErrorCode),message, and optionalsuggestion,platform_detailanddetails, plus its delivery disposition. - Codes:
PERM_DENIED,ELEMENT_NOT_FOUND,APP_NOT_FOUND,ACTION_FAILED,ACTION_NOT_SUPPORTED,STALE_REF,AMBIGUOUS_TARGET,WINDOW_NOT_FOUND,PLATFORM_NOT_SUPPORTED,TIMEOUT,INVALID_ARGS,NOTIFICATION_NOT_FOUND,SNAPSHOT_NOT_FOUND,POLICY_DENIED,APP_UNRESPONSIVE,INTERNAL. - Exit codes:
0success,1structured error,2argument or parse error.
Output contract
Every command prints one envelope. docs/json-output.md holds the full contract.
{ "version": "2.5", "ok": true, "command": "snapshot",
"data": { "app": "Finder", "snapshot_id": "s8f3k2p9", "ref_count": 14, "complete": true, "tree": { } } }{ "version": "2.5", "ok": false, "command": "click",
"error": { "code": "STALE_REF", "message": "...", "suggestion": "...",
"recovery": { "strategy": "refresh_snapshot_then_retry_original", "retryable": true,
"requires_fresh_snapshot": true },
"disposition": { "delivery": "not_delivered", "retry": "safe" } } }- Omit
Nonefields and empty arrays. Compact mode omits bounds.ref_countandtreelive insidedata. - Every snapshot carries
data.complete. A snapshot that runs out of observation budget succeeds with"complete": false,"truncated": true,"nodes_observed"and the tree it saw; each cut node carries"subtree_truncated": true. A--rootdrill-down returnsTIMEOUTinstead of a partial tree. - Pointer, drag, clipboard, close-app and window commands return the adapter's
OperationReportasdata.delivered_verifiedneeds a postcondition read,delivered_unverifiedmeans a completed dispatch without one, andnot_deliveredis only for an operation already observed as satisfied. Core never adds an unconditional success flag such asclicked. - Use
recovery.strategyonly whendisposition.retryissafe.
Refs and state
CONCEPTS.md defines refs, snapshots, sessions, cursors and traces. These rules are the ones code most often breaks:
- Refs are snapshot-qualified (
@s8f3k2p9:e1) and allocated depth-first. Only the qualified form is accepted as input; a bare@e1fails withINVALID_ARGS.@eNis the internal RefMap key only. - An element gets a ref when its role is interactive (
INTERACTIVE_ROLESincrates/core/src/tree/roles.rs) or it advertises a primary action.SetFocus,RightClickandScrollToalone do not qualify, because web runtimes advertise them on nearly every node. - Actions re-identify the element strictly from
RefEntryevidence: pid, role, path and source surface, stable text identity and a bounds hash. Control values are volatile and never identity. A mismatch returnsSTALE_REF; several live candidates returnAMBIGUOUS_TARGET. - All state lives under one root,
~/.agent-desktopby default.AGENT_DESKTOP_HOMErelocates it (crates/core/src/storage/state_root.rs). Refmaps live atsnapshots/<snapshot_id>/refmap.json, capped at 1 MB each; the store keeps the newest 128 snapshots per namespace and prunes to 96. Command code goes throughRefStore, which only reads the legacylast_refmap.jsonto migrate it. - A session is selected by
--session, thenAGENT_DESKTOP_SESSION, then none.session startdoes not activate a session for later processes; pass its id. Snapshot lookup never searches another session's namespace.
macOS adapter gotchas
- macOS reuses
AXUIElementRefpointers across sibling branches. Guard traversal with an ancestor-path set, never a global visited set, or real subtrees disappear. AXElementkeeps its pointerpub(crate)to prevent a double free.ClonemustCFRetainandDropmustCFRelease.- Read attributes in batches with
AXUIElementCopyMultipleAttributeValues, not one call per attribute.
Testing and CI
- Unit tests use small purpose-built adapter stubs (see
crates/core/src/adapter/test_support.rs). Golden JSON intests/fixtures/guards serialization. - The E2E harnesses (
tests/e2e/,tests/e2e-windows/,tests/e2e-linux/) drive the built binary against real apps. They check every effect by independent observation, never by the command's ownok: true. The macOS harness covers every ref action in headless and--headedmode. - CI runs fmt, the MSRV build, the core cross-compile checks, clippy and tests per OS, the core isolation check, a 15 MB binary size check and the FFI header and panic guards.
Command surface
60 commands spanning App/Window, Observation, Interaction, Scroll, Keyboard, Mouse, Notifications, Clipboard, Wait,
System (including session, trace, skills) and Batch. 56 of the 60 command names are operational on macOS; the
four held-input names (key-down, key-up, mouse-down, mouse-up) fail closed. Windows and Linux expose the same
command names. Each platform's limits are in its skill (skills/agent-desktop-macos/, skills/agent-desktop-windows/,
skills/agent-desktop-linux/).
Definition of done
Performance. Check every substantive change for latency against the merge-base before merge. A latency change must be intended and explained, never found by users.
- macOS:
bash scripts/perf-baseline-compare.shwritesreport.html. Add--apps "Slack,Google Chrome"for dense Electron and Chromium coverage. - Windows: that script cannot run there. Use the probe cost method instead: min of seven runs with the warm-up
discarded, reported as the min with the median and max beside it (
probes/windows/FINDINGS.mdA15-13, A18-7). - Linux: there is no baseline comparison. Hold these budgets: release CLI at most 15 MiB; every command under a silent
display, a silent bus or a stopped app returns its documented code within its timeout plus 1 s; a snapshot of a
real app is
complete: trueor cleanly truncated; FFI threads, descriptors and memory stay flat across repeated calls; the glibc floor stays at 2.35 or lower (scripts/check-elf-glibc-floor.sh).
Dogfood. Every platform sub-phase drives its own surface against real software and keeps a judged report.
- Run it as a stranger. The operator gets the shipped skill and the built binary, and nothing else: no source, no plan. A subagent that does it is told so and denied the repo.
- Run every example in the shipped docs verbatim in the platform's default shell. An example that cannot run as written is a defect of the same weight as a broken command.
- Reaching for a flag, variable or ordering the skill does not document ends the run as a finding.
- A report with no findings is a failed dogfood, not a passed one.
- Read every step's
ok. Wait with the tool's own wait primitives; a fixed sleep and then a check is a race. Confirm a failure twice before you report it. Measure a behavioral claim in both directions before you write it down. - Give every finding exactly one disposition: fixed here, with a named test that you invert-verify (break the fix, watch the test fail, restore); owned elsewhere, written into the receiving phase of the local roadmap; or accepted, with a reason. "Recorded" is not a disposition.
- Reports and their evidence stay out of version control.
- Every requirement in a plan maps to at least one test that fails when the requirement is violated.
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 28,693 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository9 other files it gives its agents.
- apps/docs/AGENTS.mdAGENTS.md
- architecture-diagramSkill
- technical-writerSkill
- agent-desktop-ffiSkill
- agent-desktop-linuxSkill
- agent-desktop-macosSkill
- agent-desktopSkill
- agent-desktop-windowsSkill
- jev-desktopSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

