| Files | Last commit | Last update |
|---|---|---|
feat: add trace viewer and replay artifacts Adds the trace read/replay layer on the session-first foundation: `trace show` merges per-process segments into one deterministic timeline (bounded JSON for agents), and `trace export` renders a single self-contained, XSS-safe HTML viewer for humans. Opt-in `session start --screenshots` captures pre/post-action screenshots and refmap copies; command.start/end boundary events and a versioned trace.meta header make a step-by-step replay reconstructable. Redaction is hardened so raw caller arguments never leak into trace-reachable error messages. Available across CLI, batch, and FFI. | 4 months ago | |
feat: integrate Linux support with privacy hardening (#311) * chore: remove three uncalled lab scripts No workflow, hook, script or test calls them: a symlink helper for local skills, a root-only uinput stub whose legs were never written, and an unrun self-test of the npm wrapper smoke, which CI runs directly. * docs: remove stale evaluation records and unreferenced diagrams The architecture diagrams and the Slack token illustration were not linked from anywhere and showed Linux as planned. The acceptance audit was a withdrawn status record. Two solution docs were folded into the docs that already carried their context, so each lesson has one home. Validated in the docs removal ledger (docs-3, 6, 8, 11, 12, 13). * docs: correct solution docs that contradict the code Many learnings named moved files, deleted tests, stale function signatures, or rules the code no longer follows (bare refs accepted, native apps never activated, one box lock instead of two slots). Each now cites the code that holds today, and none cites the planning docs or the untracked dogfood reports. * docs: correct CONCEPTS.md against the shipped adapters Several definitions described one platform as if it were every platform, missed the Linux adapter, cited probe rows that measure something else, used a screenshot flag that does not exist, and called dogfood reports committed. Session Manifest folds into Session and the must-catch fixture into Gate Self-Test, where each was already half defined. * docs: correct contributor docs, the FAQ, the JSON contract and runbooks CONTRIBUTING.md now points to CLAUDE.md for the engineering rules instead of keeping a second copy that had drifted (a MockAdapter that does not exist, an E2E command that exits without the exclusive-desktop variable). The JSON contract described an unknown disposition the code no longer emits, the FAQ repeated the README platform table, and the Windows runner runbook cited plan ids and a trigger branch that merged. * ci: assert the release-ffi unwind profile once per release ffi-release-panic-guard already runs the identical grep and publish-github needs it, so the copy in ffi-release-gates added nothing. * chore: let release-please bump the Linux skill version skills/agent-desktop-linux/SKILL.md carries the x-release-please-version marker and is published with the other skills, but was missing from extra-files, so its version would have stayed behind. * docs: make the test harness READMEs runnable as written Several harness commands failed as documented (the Linux consumer run without AGENT_DESKTOP_WM_ROOT, the discovery inversion without a header path, a headless probe binary that swiftc names differently) and the fixtures README listed files that do not exist. The Linux E2E guide drops plan history and keeps what an operator needs to run and read it. * Revert "chore: stop shipping the jev-desktop skill" This reverts commit 34826e104af928a345dbdfeaa83a9a9b72745fb0. * docs: fold the ref-root viewport lesson into the clipping doc The shared clipping doc already states the same incident and the repair that superseded it, so the lesson keeps one home. Validated in the docs removal ledger (docs-14). * docs: say jev-desktop needs a repository clone, and match it to its scripts The skill teaches node scripts/jev/*.mjs, which ship in no installed package, so it now says up front that it needs a clone of the repository and a TYPESAFE_API_KEY. Checked against the scripts: run.mjs retries 429/503/529 twice while act.mjs does not, --bin and --execute belong to act.mjs only, act.mjs sends longer values and the ancestor path, why is null on act, the stop list and exit codes were incomplete, and JEV_VERIFY_MS was missing. * docs: correct FFI handle, wait and last-error text against the header ad_free_handle works after the adapter is destroyed (native_handle.rs tests prove it), so the rule is to free handles before destroying the adapter only for use, not for release. The wait bullet now uses the header's AdWaitMode field names, AdActionStep lists mechanism and verified, the delivery-semantics accessor copies values instead of lending a pointer, and the JSON-string table gains ad_execute_by_ref_timeout, ad_trace_show and ad_trace_export. * ci: cut duplicate jobs and steps from the CI workflow Each removed piece repeated a check that another job already runs on the same OS, or depended on a file that is leaving the repository: - platform-check: three legs were subsets of clippy --all-targets on the same runner; the windows-11-arm leg is now a clippy step in test-windows-arm. - ffi-header-drift, ffi-python-smoke and ffi-passthrough now run as conditional steps on the ffi-panic-guard legs that used the same OS, so the header drift gate and the stub checks keep running on every PR. - check-release-consistency.sh and check-npm-package.js run in the supply-chain workflow on every PR, push and weekly, and need no OS. - the ubuntu bash3 compatibility run was weaker than the macOS Bash 3.2 run. - trash and trash-cli installs had no caller left. - check-phases-ledger-citations.ps1 read docs/phases.md, which leaves the repository, and would fail the Windows lane. The viewer_filter.js test moves here from the inspector workflow, so the shipped viewer code keeps a CI test. * chore: remove the unshipped inspector web UI tools/inspector is a local developer viewer that no shipped artifact, skill or release step uses, and its workflow only tested it. The viewer_filter.js test it also ran now lives in the CI workflow. * ci: give the release dry run a concurrency group and state why it stands alone The reason it is a separate read-only workflow lived only in docs/phases.md, which leaves the repository. A concurrency group stops a busy PR from queueing many full rehearsals, and trash-cli had no caller left. * ci: run the Windows capability probes only when probe files change The probes measure the hosted runner. The one contract they check against crate code, secure-value withholding, is also a plain unit test in the Windows CI lane, so code-only PRs no longer pay for a 30-minute probe run. * docs: document the jev scripts beside the code The jev-desktop skill left the shipped skill set, so its usage notes move next to the scripts they describe, checked against the current flags and environment variables. * chore: remove an unreferenced benchmark screenshot No file links to it. * docs: fix three FFI ownership and panic claims a review disproved A codex review proved three overstatements against source: ad_abi_version returns a constant with no panic trap, so only entrypoints that can panic catch_unwind; last-error strings are borrowed and never freed, so not every returned pointer has a free function; and ad_free_handle validates the adapter before it accepts a null handle. Remaining em-dashes in the FFI references became plain punctuation. * refactor(core): drop two helpers that nothing calls RefStore::for_tests and ErrorPayload::with_recovery had no caller in any crate or test. They sit in public modules, so rustc could not flag them. Recovery hints are still attached by recovery_for_code. Ledger rows A-01, A-02. * test(ffi): drop an unused re-export from the shared test module No ffi integration test names AdIdentifierKind through the common module; the file-level allow(unused_imports) hid it. Ledger row A-08. * ci: keep the 20-minute FFI timeout on legs that only run the panic probe Only the aarch64-apple-darwin leg gained the header drift and stub smoke steps, so only it needs the longer bound. * test(linux): drop live probes that only print timings semantic_click_latency_probe and the vocabulary report, timed pass and label timings asserted nothing and no gate reads their output. The vocabulary live test keeps every assertion path. Ledger rows A-12, A-13. * refactor(linux): drop stale dead_code allows on display errors DisplayCause, as_str and display_cause_error all have production callers, so the allows only hid any future dead code. Ledger row A-15. * refactor(windows): compile the canonical lock path only for tests Only the lease tests call canonical_lock_path, so a cfg(test) gate replaces the allow(dead_code) and the doc no longer promises a future caller. Ledger row A-16. * docs: cut the README to 130 lines and move install detail to docs/install.md The README keeps the pitch, install, the observe-act loop, the agent section, the command table and a plain platform table. Install-script allowlisting, attestation, the source build, per-OS permissions and platform footnotes move to docs/install.md unchanged. * docs: record the parameterized-replacement findings beside their probes The payload shape, the background no-op and the input-context route are what a maintainer needs when running replace_range_probe.swift, so they belong in the harness README rather than in a one-trial log. * docs: state what the jev scripts actually send and restore --no-values does not withhold text passed with --text, the clipboard is only put back when it held text, and a run reads the screen more than once per turn. The README said otherwise. * chore: stop naming a removed workflow in the redaction gate comments windows-e2e.yml no longer exists; the -Path override is described by what it does. * docs: note parameterized attributes in the real-app probe guide A setter that does nothing does not prove an editor has no write path; the probe lists parameterized operations such as AXReplaceRangeWithText, and the replacement range is in UTF-16 units. * docs: remove the Numbers replacement trial log now its findings live with the probe The real-app README carries the payload shape, the background no-op, the input-context route and the inventory lesson. Validated in the docs removal ledger (docs-15, codex and opencode both REMOVE). * chore: make infra comments match the repository The gitignore comment said no normalized twin is committed, the Windows e2e runner said no CI workflow touches it, one gate comment named a function that is being removed, and another described docs/phases.md as in the repository. * docs: make the PR template gate commands run as written The E2E line exited 2 without AGENT_DESKTOP_E2E_EXCLUSIVE=1, a string HOST_PKGS reaches cargo as one argument in zsh, and the skill checklist left out the Linux skill. * fix(npm): point the postinstall skill hint at commands that exist claude mcp add-skill is not a Claude Code subcommand. The hint now names the binary's own skills command and the skills installer the README documents. * docs: drop the jev README now that the jev-desktop skill documents the scripts Two descriptions of the same scripts would drift; the skill is the one that ships. * docs: shape the README for users, contributors and agents The fast path stays on top (install, a quick start, the agent section), followed by reference sections and a documentation map that points each reader to the right file. It restores facts the shorter version dropped: headless-by-default delivery, PowerShell ref quoting, the readable JSON sample, the accessibility-tree row and the per-OS package sets. * docs: name the CI job that now runs the FFI Python smoke The ffi-python-smoke job was folded into the macOS leg of the ffi-panic-guard job, so two docs pointed at a job that no longer runs. * refactor(core): share empty_trace_dir_error between trace show and export The two copies were byte-identical, so one pub(crate) helper in trace_read now serves both calls. Ledger row B-07. * ci: give the Linux live e2e job an explicit read-only token The job inherited an empty permission set, unlike its macOS twin; checkout needs contents: read and nothing more. * ci: pin the probe workflow's checkout to the same release as every other workflow It was the only workflow left on v7.0.0. * docs(core): make two rustdocs match the code they describe transform_tree prunes on is_ref_able, not on role alone, and the drill-down completeness note belongs on run_from_ref_with_context, not on count_nodes. * docs: state the jev read cost for run.mjs as well as act.mjs lead-infra's review found the skill gave the snapshot cost for act.mjs only. run.mjs reads the screen before and after each action (none after DRILL or WIDEN), and both scripts add a surface snapshot when an overlay is open and no region is pinned (desktop.mjs:62-76, act.mjs:236-237). The other five corrections already held in the merged text. * fix(cli): list linux among the skills get aliases skills get linux already resolves, but the help text left it out. * test(visual-debug): stop naming the removed inspector in a test env var The capture child marker now says what it marks, since tools/inspector is gone. * refactor: replace per-crate ensure_budget copies with Deadline::ensure_remaining Twelve private macOS helpers, two private Linux helpers, and the shared Linux bus and Windows permissions helpers all had the same body. One Deadline method now carries it, with unit tests. The DeliveryTracker variants, the mouse and chain variants, and Linux input/deliver.rs keep their own checks because they add delivery details. Ledger row B-01. * docs: restore rules and definitions an earlier cut dropped CLAUDE.md again states the @generated exemption, that every non-doc comment form is rejected while //! module docs are allowed, and that expect and panic are test-only like unwrap. CONCEPTS.md again says a ref-addressed drag picks up at the bounds center (and the slider workaround) and names the must-catch and must-pass fixture halves. * docs: restore two oracle contracts to the Linux E2E guide The rewrite dropped what the pointer delivery legs read back and what the dead-owner recovery leg requires, both of which an operator needs to judge a failing leg. * chore: keep the gitignore note on which capture twins are tracked Per-area probe directories commit their normalized twins; only the twins under probes/<platform>/captures/ are regenerated and untracked. * docs: say in Coordinate Fallback that raw --xy input focuses nothing A raw coordinate resolves with no ref and no window identity, so the caller owns where it lands; the fallback entry now states it directly. * refactor(windows): drop file-level dead_code allows in system modules Windows clippy with -D warnings passes with all twelve removed, so every item in those files is used and the allows only hid future dead code. The menu source agreement test no longer cites the allow. Ledger row A-22. * refactor(windows): parse w-<n> window ids in one place observe and surfaces carried byte-identical parsers with the same InvalidArgs message; window_ops now owns parse_window_id next to window_id. Ledger row B-08. * refactor(linux): share the already-satisfied pointer select result The combo and row select paths built the same skipped XTEST.click result with their own copy of the route constant; select_pointer now holds one. Ledger row B-11c. * refactor(linux): share clipboard delivery helpers between X11 and Wayland set_clear and wayland carried the same verify grace, fresh deadline, and not-delivered and unverified error builders; delivery.rs now holds one copy and messages stay with the callers. Ledger row B-11d. * refactor: build stale-evidence errors with AdapterError::stale_ref_because Three adapter copies of stale_evidence_error repeated the body of the existing core constructor, so the resolvers now call it directly. A core unit test pins its code, suggestion and disposition. Ledger row B-02. * refactor: move the live-evidence completeness helpers into core incomplete_live_evidence, essential_live_evidence_complete and known_actions were byte-identical across the adapters and work only on core locator types; live_element now owns them, with unit tests. Ledger row B-05. * refactor: move notification identity verification into core The macOS and Windows checks were byte-identical over core notification types, so core now exports verify_notification_identity with unit tests. Linux keeps its own check because its suggestion and disposition differ. Ledger row B-06. * chore: point the stale-ref gate at the constructor callers now use stale_evidence_error is gone; evidence-refused refs now build their error with AdapterError::stale_ref_because, so the comment and the failure hint name that. * test(e2e-linux): drop plan requirement ids from leg descriptions R9, R19, R22, R24, R32, R33 and R35 named requirements in a plan that has left the repository, so the prefixes meant nothing to a reader. Nothing parses the requirement text; leg ids are unchanged. * chore: stop the privacy scan comment from claiming the repo ships planning docs * test(e2e-linux): drop duplicate legs and dead harness helpers Six legs repeated the observation of a stronger surviving leg on the same tiers, two unit tests restated another test's assertions, and several helpers had no caller. Each row was validated by two independent models (ledger TR-145..175). * test(e2e-windows): drop a racing zero-timeout leg and dead fixture code The delayed-enable zero-timeout leg raced the fixture timer while the permanently disabled leg proves the same no-wait refusal; the click row's mechanism key is now asserted instead of ignored, and an unused fixture overload and an always-true self-test line are gone (ledger TR-149, TR-151, TR-152, TR-186). * test(e2e): fold the absolute-timeout exit code into the deadline test The removed test repeated the descendant-deadline test and added only the 124 exit code, which the survivor now asserts (ledger TR-185). * ci: run the four hermetic scripts self-tests in the fmt job perf_ab_probe_test.py, test-linux-live-gate.py, test-session-env.sh and test-remote-linux.sh guard the perf report math, live-gate shard and receipt rules, desktop environment selection, and the remote kill guard, yet no job ran them; each is stub-driven, needs no display or network, and takes about a second. The two that start private sessions stay manual (one already runs in linux-e2e). * test: make every self-rerun test prove its child ran the target test A test that re-runs its own binary with a libtest filter passed with zero tests run when the filter was wrong, which already hid the Linux cursor overlay check (its filter missed the overlay module). Each self-rerun site now asserts the child printed "1 passed; 0 failed"; the overlay filter is corrected, and the interactive clipboard test compiles again under its feature. * test(e2e-linux): drop duplicate contract self-tests and dead test helpers Registry and gate checks were asserted twice; the surviving registry test now also refuses an empty registry so it cannot pass vacuously (ledger TR-177..183). * test(core): drop tests that restate a stronger test or only exercise a mock Each removed test was matched by a surviving test at the same or a stronger boundary, or asserted only a mock's or a constant's own value; two merges keep the unique assertions (role round trip, toggleable negatives) in the survivors (ledger TR-001..019). * test(macos): drop duplicate and tautological tests, fold unique assertions into survivors Survivors now carry the sibling-radio suggestion, the AXOpen stale classification and the native plus DOM identifier pair (ledger TR-058..074). * test: drop CLI and conformance tests that only exercise their own mock The conformance window-identity tests asserted the mock adapter's behavior, and the version and wait-metadata assertions repeated stronger tests (ledger TR-142, TR-143, TR-161..163). * test(ffi): drop duplicate C ABI tests and keep their unique checks in survivors Typed discriminant round trips, out-of-range values, a second trapped panic and the last-error pointer across successful calls now live in the surviving tests (ledger TR-035..044). * test(windows): drop duplicate dispatch, notification and state tests The adapter's inline tests were byte-identical to adapter_tests.rs; the R2 advertisement tests now live as rung call counts and isolated advertisement cases in the action and tree tests (ledger TR-073..091). * refactor(linux): import test-only step types in the select tests, not production select_row.rs and select_pointer_combo.rs carried cfg(test) imports only for their tests' use super::*; the tests now import ActionStep and StepMechanism. * test(e2e): add error-envelope and batch failure legs to the macOS harness Agents branch on exit code and error.code for a bare @e1, an unknown app, a missing surface and a parse error, and on batch's continue versus stop-on-error results; no live leg proved them. Self-tests check every expected code exists in the core enum and that each leg asserts code and exit together. * test(e2e-linux): add error-envelope and batch failure legs Two registered legs drive the real binary on both X11 tiers: recoverable error envelopes (INVALID_ARGS for a bare @e1, APP_NOT_FOUND with exit 1) and batch per-entry failure with and without --stop-on-error, observed on the app itself. Pure verdict tests with lying fakes keep them checkable without a desktop. * test(e2e-windows): add error-envelope and batch failure legs Eight Reliability legs cover INVALID_ARGS, SNAPSHOT_NOT_FOUND, STALE_REF for an absent element, APP_NOT_FOUND and ELEMENT_NOT_FOUND with exit codes, and batch halt versus continue with per-entry codes. Not yet run: they need a Windows host (contract -SelfTest, the U7 self-tests, then the live suite). * refactor(core): route deadline clock reads and poll sleeps through one test-freezable seam Several core tests flaked on loaded CI runners because their deadlines ran on the real clock. Deadline now reads time and sleeps through crate-private now() and sleep(), which outside cfg(test) are the same std calls; tests can freeze and advance a thread-local virtual clock. Public API and production behavior are unchanged (codex review: no behavior change). * test(macos): replace the inventory retry wall-clock assertion with counted attempts The test asserted more than one attempt inside a real budget, failed on 6 branches when a slow runner fit only one, and 45b28ce9 only raised the budget to 500 ms. The retry and the partial-inventory refusal are now proven by attempt counts on a fake capture that never consumes its deadline. * fix(windows): confirm a target's exit on its process handle before reporting a menu-wait read error A window that stops pumping during teardown reads as APP_UNRESPONSIVE before its process object is signaled, so the single liveness re-check lost the race and the wait reported the transient code instead of STALE_REF (failed on 7 branches). The wait now blocks on the process handle for at most the remaining deadline, capped at one second, before it keeps the read error. First run on Windows CI. * test(linux): drop duplicate input, close, tray and tie-break tests Each removed test repeated a survivor at the same boundary; the unique recovery text, baseline counts and tie cases moved into the survivors (ledger TR-046..057). * test(e2e): add stale-ref and key-combo legs to the macOS harness The removed-element leg accepted four error codes; it now requires STALE_REF with a safe retry. A RefChurn leg proves STALE_REF and AMBIGUOUS_TARGET after an in-place UI swap, and a press leg proves combos by reading the field itself. * test(e2e-linux): add stale-ref-after-UI-change and key-combo legs The stale leg refreshes nothing between a real menu change and the click and requires STALE_REF with its recovery; the press leg checks ctrl+a, backspace and return by AT-SPI text, not by the command's own ok. * test(e2e-windows): add stale-ref contract and key-combo legs The removable row now reports clicks, so a stale ref proven by refusal also proves nothing was delivered; press legs cover ctrl+a, shift+tab, return and the blocked alt+f4 refusal. Not yet run: needs a Windows host. * docs: point the live-gate runbook at the focus leg that stays The lift leg duplicated lifecycle.focus_window_active_reads and never read the restoration it named, so the tests cleanup removes it; the retained live owner is the active-window read. * test(core): run the settle-loop and artifact-lock budget tests on the virtual clock These tests failed on loaded Windows and macOS runners because a real 50 ms lock wait or settle window expired before the code under test ran; a 16 x 20 ms retry wrapper hid it. They now freeze the virtual clock, so lock polls and settle sleeps advance time deterministically, and the retry wrapper is gone. * fix(windows): treat E_UNEXPECTED on a UIA read as a transient failure A provider tearing down a window or shell host answers one read with E_UNEXPECTED; it was classified terminal and narrowed to a non-retryable APP_UNRESPONSIVE, so one read ended a whole wait (failed on 4 branches). It is now a retryable unresponsive read inside the caller's existing deadline; E_FAIL stays terminal. First run on Windows CI. * test(linux): make vacuous assertions able to fail Repaired tests named a behavior their assertions could not detect (a negative check that any value passed, a banned-name list with dead entries, an unverified result asserted only as not verified); each now checks the stated contract. * test(macos): make vacuous assertions able to fail, each proven by a production mutation Process-group kill, semantic-only disclosure chains, single capture, strategy fall-through, scan tolerance, endpoint naming, overlay acknowledgement, the select depth cap and the name-plan slots now fail when the behavior breaks. * test(ffi): make C ABI tests assert what their names promise Modifier cap, JSON error forwarding, invalid discriminants, envelope fields, dirty out-params on a worker thread, wait error envelopes and the session adapter's fail-closed path each had an assertion that could not fail; every repair was proven by breaking the production path once. * test(e2e): make harness self-tests fail for the regressions they name * test(e2e-linux): make harness self-tests and a menu leg assert structured results The menu timeout leg now checks the structured wait_timeout details, and the oracle, runner, session-sweep, workflow and verdict self-tests assert the specific failure each one names. * test(e2e-windows): replace diagnostic scaffolding and soft guards with real assertions The headed double-click leg used debug output, a fixed sleep and one read; it now uses Assert-Effect like its siblings. The handle-set and overlay controls no longer skip silently, and the Chromium rate leg fails when every sample is stale. Not yet run: needs a Windows host. * test(core): drop find, get, wait, session and trace tests that repeat stronger tests The removed find tests ran a cfg(test)-only snapshot collector production never uses; that collector's count helper went with them. Merges keep the observed-role negative, the null-bounds case, the conflicting wait flags as a table and the identifier preference in the survivors (ledger TR-020..034). * test(windows): drop input fake-sink, policy, clipboard and surface tests that repeat survivors The keyboard and mouse send tests only checked that the test fake stored what it was given; the clipboard, chain and surface merges keep their unique cases as table rows or assertions in the survivors (ledger TR-092..106). * test(windows): drop tree property, hit-test and resolve tests that repeat survivors Each removed test had an identical or stronger twin in the same module family (ledger TR-124..140). * test(linux): make clipboard, keyboard-frame, remap, capture and close tests assert their contracts The remap test now proves the deadline cancelled after the remap and that the keymap was restored, the off-screen capture test places its window instead of skipping, and the close gate is checked for both graceful and forced closes. * test: make CLI dispatch, overlay and real-app tests assert what they claim Real-app tests now fail when their precondition is missing instead of passing silently, the overlay show test pins POLICY_DENIED, and the conformance test checks each capability's outcome, not only that the call returned. * test(real-apps): assert the Numbers failure stop and drop a dead plan citation * test(macos): make capture-scope, screenshot and child-label tests assert their contracts The capture-scope tests now run the production scope filter instead of a test copy of it (the single-pid case is kept beside the pid-set case), the screenshot timeout checks the child is gone, and the child-label rule is checked for every role it names. * test(core): order concurrent snapshot writers instead of retrying lock timeouts, and pin tray ids Eight writers polled one lock with no fairness and a 30 s retry hid starvation that failed CI on four branches; the writers now take turns, and a single-thread test on the frozen clock proves a held store lock still blocks a writer. A new test pins that allocation keeps a RuntimeId identifier and its kind, which no test covered after the tray identity change. * test(windows): drop lifecycle, launch, HRESULT and signal tests that repeat survivors Merges move the exit-code serialization cases into core's process-state tests and the HRESULT detail strings and generation checks into the surviving Windows tests (ledger TR-108..121). * test(windows): drop two duplicate tree tests and make hit-test and resolve tests assert their contracts hit_test now judges a probe through judge_probe, so a probe miss is tested on the production path (same behavior: a miss is Unknown, a hit is judged); the fixture teardown, minimized-window, anchor and timeout tests assert what they name (ledger TR-122, TR-123, TF-144..157). * test(linux): make capture-cap, tie, key-owner, menu and tray tests assert their contracts * test(core): run live-locator and ref-action wait tests on the virtual clock These tests had their budgets widened and an exact attempt count loosened to 'at least one' to survive slow runners; on the frozen clock the retry loop's own sleeps advance time, so the original exact counts and small budgets hold. * test(windows): drop the setup-time stopwatch from the headless notification wait refusal The elapsed < 2 s assertion measured COM and adapter setup and failed twice on loaded runners; POLICY_DENIED is not retryable, so a burned deadline would already surface as TIMEOUT and fail the code assertion. * test(windows): decide modern-capture frame-wait expiry inside the wait, not by wall time The fallback test asserted TIMEOUT against an 80 ms budget that real capture setup could consume first (failing as INTERNAL); the cfg(test) frame hold now starts its budget inside the frame wait, and the pattern test waits for a frame showing the fixture instead of sleeping 500 ms. Production frame wait unchanged. * test(windows): signal the menu fixture's open state from real menu mode The fixture announced the popup before TrackPopupMenu entered menu mode and ignored its result, so the chromium composition test compared two live reads that could straddle the flip; readiness now comes from menu mode itself and the comparison waits until both sources agree. * test(linux): make tray, signal, settle, walker and shell-owner tests assert their contracts The error-code boundary test now enumerates codes exhaustively at compile time instead of a hand-typed list of 16, and the remaining repairs check the behavior their names state. * test(macos): replace real-clock budgets and stopwatches with counted attempts and events Stabilize-loop tests could expire before their first attempt on a slow runner; they now use budgets the fakes never consume and assert exact attempt counts, and the overlay acknowledgement tests block on a release channel instead of a 700 ms sleep. * test: isolate command-host sockets per test and assert batch deadlines by value Fixed socket and symlink names keyed by pid were never removed, so pid reuse could fail a later run; each test now owns a removed-on-drop directory. Batch deadline tests assert the budget the adapter received instead of wall time. * test(ffi): isolate the round-trip test's state root and require the snapshot log event The round-trip test set HOME under its own lock but left AGENT_DESKTOP_HOME alone, so a developer's state root leaked in; it now uses the shared isolated home helper. Log-callback tests no longer fall back to the first delivery. * test(core): make actionability, trace, find and close tests assert their contracts Each repaired test was proven by breaking its production path once. The benchmark example's rect test moved into the library's rect tests so it runs in CI, and the find roles hint is now tested through execute instead of a cfg(test)-only copy, which is deleted. * test(windows): make dispatch, envelope, scroll and fixture tests assert their contracts Permissive Err arms, restated literals and a self-comparing fixture expectation are replaced by assertions on the delivered outcome. * test(linux): make tree walker, projection and placement tests assert their contracts * test(core): make session, overlay-routing, hover and pointer tests assert their contracts Session precedence tests pass the env value directly instead of mutating the process environment, gc is actually exercised on an unreadable manifest, the skeleton golden compares the whole tree, and hover counts receiver checks instead of timing them. * test(windows): make drag, capture, HRESULT, signal and wait tests assert their contracts Assertions true by construction (an intersection member found in its own set, an assert_ne after an exact assert_eq, a uniqueness loop over a test-local list) are replaced or removed, and the remaining checks name the behavior they guard. * test(linux): make resolve tests assert the unread-region refusal they name * test(core): drop the null ref_id from the skeleton golden fixture The skeleton golden test now compares the whole serialized tree, and the serializer omits a missing ref_id, so the fixture must not carry one. * test(core): make ref, refs, trace-read and policy tests assert their contracts The XSS gate now asserts the exact escaped payload, the oversize-refmap guard is exercised through the production writer, poll and wait tests count resolves instead of timing them, and a new test pins identifier kinds through RefEntry identity. Each repair was proven by breaking its production path once. * test(windows): make lifecycle, overlay spawn and shell-surface tests assert their contracts A tautological ambiguous-target shape check now serializes the production value, a self-comparing error-code helper is gone, and the taskbar test no longer returns early on an empty tree. * test(npm): drop duplicate platform and wrapper tests and check the real staging directory The staging-cleanup tests read the destination, which installArchive never stages in, so a cleanup regression still passed; they now compare the package bin directory before and after. Duplicate platform-table and wrapper tests are gone, with their unique helper-name and Trash-warning checks kept in the survivors (ledger TR-189, TR-191..193, TF-187, TF-188). * ci: run the Numbers sheet-script self-test in the fmt job numbers_sheets_test.py drives numbers-sheets.sh against a fake binary in a temporary home and proves its failure stop; it is hermetic and nothing ran it. * test: nest extra test modules so moved anchors keep room under the line cap ref_action.rs (400 lines) and the Windows tree/resolve.rs (398 lines) become their folders' mod.rs in the restructure and must then declare their new child modules. Their extra test modules are declared from the files' tests modules instead, so both anchors stay under 400 lines. The nested test files import their parent by absolute path, keeping what `super::` meant before. The Linux action scan walks src/actions recursively, so the banned-call scan keeps reading the action files that move into select/, scroll/ and write/, and it asserts files from each of those groups are still scanned. * refactor(cli): give the command host and diagnostics their own folders Nine command_host*.rs files and three diagnostic*.rs files sat loose in src/, so the binary's root read as a flat list. Each family now lives in a folder whose mod.rs is the former entry file; the helpers were already #[path] children, so no module path changes. * refactor(core): group core's leaf types into response, input, notifications, storage and wait Sixty-seven of core's 270 root files are the envelope and error model, the input and notification parameter types, the on-disk state root, deadlines and signals, plus adapter and session helpers. They now sit in folders named after the concepts in CONCEPTS.md, so a reader finds a type by its domain instead of scanning one flat list. Module paths gain one segment; root re-exports in lib.rs keep every public name unchanged, and adapter becomes a public module because it now holds the public hit_test, live_element and native_handle modules. * refactor(core): group core's tree, action, system and snapshot types by domain The accessibility-tree model, the Action model and its execution types, app and window and process types, snapshot building and the context module were 115 loose root files. They now live in tree/, actions/, system/, snapshot/ and context/, named like the platform crates' layers so a type a platform crate uses from its input/ or system/ lives under the same name in core. The stray actionability tests move into actionability/. Two test files import one module once so the longer paths keep them under 400 lines. * refactor(core): group refs, ref actions, locators and traces into their folders The ref map, ref store, allocation and identity files, the headless ref action flow, the locator with its live evaluator, and the trace writer with its reader were the last loose families at core's root. Each family now lives under the module that already anchored it (refs.rs, ref_action.rs, locator.rs and trace.rs become their folders' mod.rs), so those module paths do not change. trace becomes public because it now holds the public trace_read and trace_sanitize modules. lib.rs is the only file left at the root. commands/ stays flat: its contract tests require commands/<name>.rs. * refactor(macos): group the macOS layers into concept folders Each layer held one flat list: 62 files in tree/, 68 in system/, 45 in actions/. Concepts with four or more sources now have a folder named with the vocabulary all three platform crates share: tree/resolve/ and tree/properties/, actions/chain/ and actions/select/, input/clipboard/, keyboard/ and mouse/, system/app/, process/, window/ and permissions/. cf_type.rs leaves the crate root. The cursor-overlay and permission-guide Objective-C bridges move next to the Rust files that declare them, with build.rs and the native test imports following. File names do not change. * refactor(windows): group the Windows tree layer into concept folders tree/ held 117 files in one folder. Ref re-identification, the Win32 test fixtures, the UIA walkers, the property reads and the hit test now each have a folder (resolve/, fixture/, walk/, properties/, hit_test/), using the same names as the other platform crates. The fixtures' self-rerun filters and the hit-test scan labels follow the moves, and the public tree paths used by the uia_tree_dump example and the resolution probe are updated with them. * refactor(windows): group the Windows system and input layers into concept folders system/ held about 70 loose sources and input/ about 26. Capture, windows, shell surfaces, wait signals, the COM runtime glue, menus and the shared test seams now each have a folder under system/, and clipboard, keyboard and mouse each have one under input/, with the names the other platform crates use. Public names in lib.rs do not change. Four clipboard helpers now sit under the clipboard module's existing cfg(target_os = "windows"); the crate only builds for Windows, so the set of compiled and run code is the same. * refactor(linux): group the Linux tree layer into concept folders tree/ held 209 files in one folder, the most crowded in the repository. It now has thirteen concept folders (walk, resolve, hit_test, menu, surfaces, placement, live_read, observe, properties, element, vocabulary, capabilities, live) and keeps two loose sources. All live rigs and live suites sit in tree/live/, so the live gate selects them by one prefix; its module strings, its self-test and the runbook follow the moves, and the PARALLEL_READS entry for a module that never existed is dropped. * refactor(linux): group the Linux action, input and X11 layers into concept folders actions/ held 95 files and input/ about 57 loose files, and system/x11/ (87 files) and system/window/ (57 files) had grown past one screen. Selection, scrolling, writes and live rigs now have folders under actions/; keyboard, mouse, physical targeting and the clipboard owner have folders under input/; the X11 client splits into capture, control, monitor, stacking and window, and the window operation pipeline gets window/window_op/. system/ keeps the shape it already had. The live gate's module strings follow the moves. * ci: fail the build when platform code leaks or a crate's layout drifts Platform isolation was checked only through cargo tree on CI and one PowerShell scan, so a source-level leak or a file dropped in the wrong layer reached review unnoticed. scripts/check-architecture.sh checks manifests through cargo metadata and sources through a small lexer: core depends only on its listed crates, only the binary and FFI wire platform crates, no platform crate reaches another, core keeps OS-conditional code in an exact allow-list, every crate keeps its top-level layout, capabilities are implemented in their layer, core domains do not reach commands, and tests are declared beside their code. It plants a violation of every rule before it scans, so a rule that stops detecting fails the gate. It runs in the pre-commit hook and next to the source rules in CI; the cargo tree steps stay as the transitive check. * docs: describe the crate layout and the guard that keeps it CLAUDE.md described the platform layers but not where core's files or a layer's sub-concepts go, so the next contributor or agent would guess. It now states core's domain folders, the shared platform layout and subfolder vocabulary, the file-naming and test-placement rules, and the grounding rule that enforces them. * docs: follow the source restructure in path citations The restructure moved test and module files into feature folders, so three code-span paths and a handful of folder-relative citations in the solution docs, CONCEPTS.md and two test READMEs named files that no longer exist. * test: point the e2e error-code contract at the moved error_code.rs error_code.rs moved to crates/core/src/response/ in the core restructure. The e2e harness contract test builds the path from parts, so the literal path rewrite did not see it and two contract tests failed to open the file. * docs: re-point short-form file citations at their moved files Seven citations named a moved file by its path relative to its crate (for example system/shell_surface_raise.rs), a form the full-path rewrite did not match. Each now names the file's new folder. Short citations of files that did not move, such as the Windows actions/ labels, are unchanged. * test(e2e): read written fields directly and run the host ref-churn leg fully hosted The first live run failed five legs on the harness, not the product: batch and ref-churn checks read SwiftUI echo labels that set-value does not promise to update, and the host-client variant enabled the host only for one command and used a non-private temporary directory, so the host refused it with INVALID_ARGS. The legs now read the field's own value, and the whole host run uses the host and the private temp dir. The stale-ref and close-app assertions are unchanged: they expose pre-existing product defects. * test(e2e): skip three known product defects by exact signature and end the churn fixture by pid No failing leg ships, and no real defect is hidden. The scroll, default-CLI stale-ref and close-app cases skip only when their known defect signature appears, naming the defect, file and origin commit; any other result still asserts, so each case re-arms itself once the product is fixed. When close-app leaves the RefChurn fixture running, the leg ends its own process and asserts it is gone. * fix(macos): let close-app end a process that has no LaunchServices launch date Termination refused whenever NSRunningApplication reported no launch date, so close-app could not end an app launched outside LaunchServices, contrary to the rule that absent evidence is not conflicting evidence. The bridge now passes whether a date is present, and only a present date more than five seconds from the verified instance refuses; pid, start-time token and exit checks are unchanged (codex review: only the intended change). * fix(macos): reveal a clipped scroll area through its enclosing scroll view Scroll-into-view looked for the clipping viewport starting at the target, so a target that is itself a scroll area became its own viewport, produced no direction, and stayed clipped (ACTION_FAILED, live offscreen). The original walk still runs unchanged; only when it returns the target itself does the search repeat from the target's parent (codex review: only the intended change). * test(e2e): assert scroll and close-app again now that both defects are fixed The two signature skips would hide a regression of the fixed defects, so the scroll case asserts unconditionally again and a surviving churn fixture after close-app fails the leg while the leg still ends its own process. Only the default-CLI stale-ref skip remains, for the documented aliasing gap. * test(e2e): gate two more known product defects by signature and stop the command host Live, an unknown-app lookup returned TIMEOUT because NSWorkspace listed one pid twice and the app inventory refuses a list with a skipped record; that case and the batch entry using it now accept only that exact signature, naming the defect. The default-CLI sibling check shares the documented aliasing defect. The hosted ref-churn run now stops the command host it started, which otherwise kept the desktop lease for its 15-minute idle life. * fix(e2e): parse only stdout from captured command output The binary prints its JSON envelope on stdout and tracing warnings on stderr. Every capture the suite parses as JSON merged stderr, so one warning line made json.loads fail. A WebKit WebContent process that NSWorkspace lists twice now makes every --app lookup warn about a duplicate pid; AE4 read an empty window list and aborted the suite. All captures now discard stderr, matching find_target, and a harness self-test refuses a capture that merges it again. * chore: move runbooks and residual review notes out of git into the local planning folder They are operator and review working notes, not product docs: no code, test, workflow or doc reads them. They now live with the other planning material in the globally ignored .compound-engineering/ folder. * test: restore two test assertions the repair pass over-tightened and stop echoing a session id CI showed both repairs were wrong, not the product. The Windows dispatch test now accepts any deliberate refusal again (headless double-click is refused with POLICY_DENIED by design), as it did before. The macOS helper timeout test asserts it returns within a second again, which is what its name promises; the replacement assertion held on one machine and tested nothing about blocking. The overlay test message now prints only the field it checks, so it no longer writes a session id into a failure message (CodeQL rust/cleartext-logging). * docs: tighten test-quality learnings and note the dropped accname agreement test * docs: point two measurement learnings at the code and scripts that hold them * docs: correct the Windows hardening claim in the CI-lane learning * docs: point the snapshot window citation at current code and tighten policy learnings * docs: drop a cited regression test that does not exist * docs: tighten accessibility runtime learnings without changing their facts * docs: tighten Numbers and Xcode learning wording * docs: use the measured popover timings in the wait-for-closed learning * docs: tighten Xcode and native learnings; keep the empty-search learning until merged * docs: split one dense sentence in the agent contract * docs: tighten prose in the contributor and output docs * docs: tighten the find-name learning wording * docs: tighten the concepts glossary wording without changing its terms * docs: tighten test README prose and keep every command and link * docs: cut the learnings store to the lessons that are still true and live nowhere else Two reviewers checked every learning (codex Sol proposed, codex Luna validated with the burden of proof on deletion); a learning stayed whenever they disagreed. Removed dated evaluation logs, trial records and lessons already carried by code, tests, CLAUDE.md, CONCEPTS.md or the skills; moved the one unique lesson of five learnings into a surviving one; trimmed the rest to rule, fix and one evidence pointer. The two remote dev-box learnings are gone: they described one maintainer's machines, not the product. 52 files and about 46,700 words become 15 files and about 11,100 words. Citations in the e2e harness and test READMEs now point at the surviving learnings. * docs(windows): stop citing two learnings that were removed The rule each comment cited is stated in the comment itself, so the pointer is dropped rather than re-aimed. * chore: remove tooling and paths tied to one maintainer's dev machines agent-desktop is a product for any team, so the repository no longer carries a remote-box workflow: remote-linux.sh (worktree sync to an SSH alias), box-slot.sh (a lock on that box), session-env.sh (importing that box's desktop session) and their two self-tests are removed, with their fmt-job lines. Private-session sessions now default to ${XDG_RUNTIME_DIR:-/tmp}/agent-desktop-sessions, overridable with AGENT_DESKTOP_SESSION_BASE, at every site that creates, discovers or cleans them (shell wrappers, live gate, e2e runner, Rust live tests); the window manager defaults to the system install. Docs and the Linux probe ledger no longer cite personal paths. * test(e2e-linux): take the stale-ref process baseline after the app is running The leg read the process list before Calculator started, so a correct refusal failed its own 'must not end the app' check. The unit test now models the launched process and fails on the old ordering. * fix(linux-tests): keep each OS user's private sessions under their own base Without XDG_RUNTIME_DIR every user shared /tmp/agent-desktop-sessions, so the second user could not create a session. The fallback is now /tmp/agent-desktop-sessions-<uid>, created 0700, at every site that computes it. * test(macos): restore the screenshot timeout test the repair pass over-tightened The repair also required the killed child to be reaped inside the product's cleanup deadline; on a loaded CI runner the product truthfully reports that it was not, so the assertion tested the runner, not the timeout. * fix(linux-tests): let the live gate merge receipts without creating a session base --merge starts no sessions, so it no longer needs a writable session base; the self-test proves it with a base that cannot be created. * test(linux): make the proc-scan test fail with a diagnosis instead of a bare unwrap It failed once on CI with no evidence. The helper no longer proceeds silently when the child never becomes ready, and a missed scan reports what /proc shows for the child, so the next occurrence names its cause. * fix: harden desktop boundaries and bound native operations * fix: install inspector signal handlers before readiness * test(linux): stop running renamed copies of sleep, which Ubuntu 26.04 no longer supports GitHub's ubuntu-latest now includes Ubuntu 26.04 runners, where coreutils is a transitional package over the uutils multi-call binary. That binary picks its tool from argv[0], so a copy of sleep renamed for a test exits at once and the scan finds a zombie. proc_scan failed on every 26.04 run, on branches without this cleanup too. One shared helper now copies the standalone system shell and blocks it on a held stdin pipe, which keeps the test's name on any Linux; the five tests that renamed sleep use it. * test(linux): keep blocking fixtures alive in the background without stdin A background command in a non-interactive shell gets /dev/null as stdin before any redirection, so the orphan launcher's blocking copy read EOF and exited. The copied shell now loops on 'sleep 1' instead of reading stdin; that keeps it alive under its own name in the foreground and the background alike, and lets both launchers return to their original form. * test: make native fixtures portable across CI hosts * test: use an owned process fixture for Linux discovery * fix: preserve trace readability and clear stale overlays * perf: avoid repeated trace ledger replacements * fix: reject shared helper writes and isolate Linux test fixtures * fix(linux): list every top-level window the registry exposes, whatever its role The window list admitted a root child only for roles 23, 16 and 69, a modal panel or a showing filler. Toolkits give a top-level window the role of its widget, so the GTK3 Save As file chooser (19), a Qt message box (2) and a Qt push button shown as a window (43) were never listed, while the modal gate, which reads every root child, refused focus because of the same dialog (L41-1). A root child that implements Component is now a top-level, and one predicate decides it for listing, frame identity, menu inventory and the shell content window. Dialog-ness is one predicate too: the AT-SPI dialog roles read from libatspi (alert, color chooser, dialog, file chooser, font chooser, L41-4), used by the window list, focus, sheets and nested dialogs. A blank, showing top-level that holds only menus is a popup menu surface (the GTK3 context menu and Chromium menus, L41-2, L18-7): menu observation keeps it and the window list drops it. The size identity for unnamed windows applies to every toolkit without global positions instead of GTK4 role-39 panels only, and the duplicate reports_global_position copy is gone. * fix(linux): attribute a launch handed to a running instance by its new window Launching thunar with the session's daemon running failed with "exited with status 0 without a verified application owner" although a new Thunar window opened (L41-6): the launcher forwards to `Thunar --daemon` and exits, and the attach rule also compares the argument list. After a clean launcher exit with no strict owner, the one running process of the same canonical executable that shows a top-level window absent from its pre-launch baseline now owns the launch, and with --activate that window is reported. Several such processes stay ambiguous, none keeps the failure. Strict argument identity for attaching before a spawn is unchanged. * fix(linux): publish hidden and offscreen in snapshots from the action gate's rule A snapshot gave Mousepad's hidden toolbar button a ref with no state, and the headless click then refused it as offscreen (L41-5): the snapshot projection never emitted hidden or offscreen, while the live projection the gate reads derived both. The snapshot now applies the same visibility functions to the facts its walk already holds (states, extents, frame, scroll container, minimized frame) with no extra reads; an input it lacks stays unknown and adds no token. The sentinel and placed-rectangle tests, copied in four places, now live in one module, readiness recognises both web document roles (L3-13), and the GTK4 popup origin is chosen by one rule for snapshot and live reads. * fix(linux): rebuild the desktop alias cache after an edit inside one timestamp tick The in-place desktop edit test failed 2 of 5 runs on unchanged code: the cache compared inode, length and ctime, and an edit of the same length inside one coarse kernel tick keeps the same ctime, so a stale alias index was served. Like git's racily clean rule, a cached index is now trusted only when every stamp is older than its build time minus a tick margin; otherwise it is rebuilt on the next call. * test(linux): add widget-zoo fixtures and the ref-contract e2e legs The bar for a snapshot is that every ref it gives out lands when its action is performed headless, verified by an independent read, or carries hidden, offscreen or disabled and is refused with no effect. Three fixture windows (GTK3, GTK4, Qt) expose the standard widgets, each logging its own effect, and the ref_contract legs drive every ref of a fresh headless snapshot against the log and raw AT-SPI. The window legs open the fixture's dialog, file chooser, message box and top-level button and require each to be listed, reachable as a sheet when modal, and operable to completion. The harness's own window oracle counts Component root children, the same rule as the product (L41-1). * docs(probes): record top-level, popup, visibility and launch measurements Area L41 records the toolkit census that disproves the role-based window rule of L8-2, the popup shapes that are and are not root children, the closed-dialog husks, the libatspi dialog role numbers, the snapshot and gate disagreement on Mousepad's hidden toolbar, and the Thunar daemon hand-off. The environment table names the XFCE container the new rows were measured in. * refactor(linux): state the unplaced-node test without a double negation * fix(linux): recheck racy desktop files by content instead of rebuilding the alias index Rebuilding whenever a desktop file was newer than the build tick made every lookup right after an edit rebuild, so the cache was never reused (the in-place edit test failed 5 of 5). As in git's racily clean rule, only the files that changed within the tick keep a content hash; equal stamps reuse the index unless one of those files now reads different. The test passes 10 of 10. * fix: avoid unsafe at-spi property and interface reads * test: verify linux refs against addressed raw controls * fix: include display bounds in snapshot visibility * docs: explain thunar file views and literal newlines * docs: keep file-view guidance application independent * fix: observe and resolve showing alert surfaces * fix: restore checks after the Linux cleanup merge * fix: serialize trace emission and repair review gates * refactor: align native security types with module boundaries * fix: select option rows through their advertised semantic verb * fix: avoid sibling scrollbar mutations during reveal * fix: retain unanswered scoped surface reads * fix: activate exported menus outside the accessibility dispatcher * test: select the active chooser filename field * docs: record semantic reliability evidence and headless policy * test: repair independent Linux workflow oracles * fix: preserve menu fallback and stop expired launches * fix: await queued windows and reclaim stale clipboard records * fix: preserve clipboard scan timeout codes * fix: verify combo commits and bound optional snapshot reads * fix: complete linux ref delivery and safe refusal contracts (#307) * fix: complete linux ref delivery and safe refusal contracts * fix: complete Wayland menu and chooser ref contracts * test: report unsupported action setup as unexercised * test: separate concurrent overlay fixture sockets * test: enforce snapshot promises in linux ref contracts (#308) * test: align harness checks with retained native refs * fix: prevent private desktop data from leaking through diagnostics * fix(ci): reflect successful writes in the JEV fixture | 15 days ago | |
feat: progressive skeleton traversal with ref-rooted drill-down (#20) * feat: add data model foundation for progressive skeleton traversal Add children_count, root_ref, skeleton fields to core types. Add get_subtree() to PlatformAdapter trait. Add remove_by_root_ref() and write-side size check to RefMap. No behavioral changes yet. * feat: implement progressive skeleton traversal with ref-rooted drill-down Add --skeleton and --root flags to snapshot command for token-efficient accessibility tree exploration. Skeleton mode clamps depth to 3 levels and annotates truncated containers with children_count, allowing AI agents to discover regions before drilling into them. Named containers at skeleton boundaries (via name or description) receive refs as drill-down targets. The --root flag starts traversal from a previous ref with scoped invalidation — only refs from that drill-down are replaced on re-drill. Key changes: - New ref_alloc.rs: shared ref helpers (INTERACTIVE_ROLES, actions_for_role, ref_entry_from_node, is_collapsible) extracted from snapshot.rs - New snapshot_ref.rs: drill-down logic with DrillDownConfig, scoped invalidation via root_ref tagging on RefEntry - macOS count_children() uses raw CFArrayGetCount without materializing AXElement wrappers for performance at skeleton boundaries - RefMap write-side size check prevents >1MB files - Skeleton anchors consider both name and description for Electron compat * fix: mention --skeleton in STALE_REF error suggestion * docs: document --skeleton and --root flags in skill reference * docs: update phases.md and CLAUDE.md for progressive skeleton traversal Add skeleton traversal as Phase 1 objective P1-O10. Document --skeleton and --root flags, get_subtree() trait method, new core modules, and platform-agnostic notes for Phase 2/3. Update risk mitigations and performance optimizations table. * docs: make progressive skeleton traversal the default agent workflow Update SKILL.md observe-act loop to skeleton-first approach. Add progressive skeleton traversal as the primary workflow pattern. Update anti-patterns, key principles, and command quick reference to lead with --skeleton + --root. Full snapshot remains documented as fallback for simple apps. * fix: preserve skeleton drill-down anchors * fix: preserve drill-down refs across skeleton re-snapshots Skeleton snapshots now load the existing refmap and remove only skeleton-level refs (root_ref: None), preserving drill-down refs accumulated via --root. Previously, build() always created a fresh RefMap, breaking the skeleton → drill → act → skeleton(verify) workflow by wiping all drill-down refs on the verify step. * fix: preserve drill-down depth for root snapshots * fix: verify AX action effect on Electron elements before trusting success Chromium's AX implementation returns kAXErrorSuccess for AXPress, AXConfirm, etc. but only toggles ARIA state without firing DOM event handlers. This caused the click chain to short-circuit on false positives, preventing CGClick from ever being reached. The fix detects web elements via AXWebArea ancestor walk, then verifies each AX action actually had a DOM effect by comparing focused element pointers before and after. When all AX methods produce no real effect, CGClick fires as the genuine last resort. Native elements are unaffected — the original verified_press behavior is preserved in a separate code path. * chore: ignore .context/ for local tooling artifacts * style: collapse web_action_had_effect signature to single line * test: cover RefMap save oversize rejection Extract the serialize+size-check step into a private helper so the 1MB write rejection path can be unit-tested without filesystem I/O. Also moves the size check above the directory creation in save() so an oversized refmap no longer creates ~/.agent-desktop on rejection. * test: assert stale-ref suggestion mentions --skeleton Locks in the Phase 4 polish requirement that STALE_REF errors guide agents back to a skeleton refresh, not just a plain snapshot. * test: cover skeleton-to-drill-down counter continuity Asserts that skeleton refs (@e1..@e10) survive a scoped invalidation of @e3, that drill-down refs allocated after the skeleton continue from @e11 instead of resetting, and that remove_by_root_ref drops only the drill-down children. * test: cover --root + --surface rejection at execute() boundary Adds a NoopAdapter that uses every PlatformAdapter trait default to exercise execute()'s validation guards without standing up a real adapter. Verifies that combining --root with a non-Window surface returns INVALID_ARGS, and that the Window surface does not trigger the guard. * test: add filesystem-redirected integration tests for run_from_ref Adds a thread-local HOME override + RAII HomeGuard so RefMap::save and RefMap::load can be exercised against an isolated temp directory without env-var racing across parallel tests. Also adds a StubAdapter that implements PlatformAdapter with a canned subtree response so the full run_from_ref drill-down flow can be unit-tested without standing up macOS AX state. Closes seven Phase 3 plan acceptance items in one pass: - save+load roundtrip with HOME override - oversize save rejection preserves previous file on disk - run_from_ref returns subtree and persists drill refs - stale root ref → STALE_REF with skeleton suggestion - re-drill replaces drill refs only, counter continues - multiple drill-downs from @e1 and @e2 coexist - empty subtree drill-down produces no new refs * test: add golden fixtures for skeleton output and drill-down refmap Adds two committed JSON fixtures under tests/fixtures/ plus matching unit tests that build the same input trees, run them through allocate_refs / run_from_ref, and assert the produced ref ids, parent-child layout, and root_ref tagging match the golden expectations. Locks in the JSON shape so future serialization changes have to be deliberate. * fix: bound build_subtree recursion on Electron wrapper chains Anonymous AXGroup/AXGenericElement wrappers do not advance the semantic depth counter (web-wrapper depth-skip), so a long chain of nested wrappers could recurse arbitrarily deep without ever hitting either max_depth or ABSOLUTE_MAX_DEPTH. The previous code checked `depth >= ABSOLUTE_MAX_DEPTH` against the semantic depth, which stayed at 0 throughout a wrapper chain. On pathological Electron trees this risked stack exhaustion before any cap engaged. Add a separate `raw_depth` parameter that always increments and use it for the absolute cap. Semantic `depth` still drives `max_depth` and the skeleton boundary so the wrapper-flattening behavior is preserved on normal trees. Also drops the long-unused `_include_bounds` parameter; bounds filtering happens in `allocate_refs` in core, not here. * chore: add pre-commit hook running fmt + clippy + tests Mirrors the CI quality gates (cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test --lib --workspace) so a failing commit never reaches origin. Skips automatically when no Rust/TOML files are staged. Bypass with --no-verify or SKIP_PRECOMMIT=1 when a non-Rust hotfix is genuinely needed. Hook lives under .githooks/ and is opt-in per clone: git config core.hooksPath .githooks Setup instructions added to CLAUDE.md. * fix: prevent orphaned drill-down refs leaking across skeleton refresh remove_skeleton_refs() previously dropped every entry whose root_ref was None and kept every entry whose root_ref was Some. After a series of skeleton refreshes that pattern leaked drill-down refs whose root anchor had already been removed: they could never be cleaned up by a future remove_by_root_ref(target) because target referred to a non-existent anchor, and they accumulated until the 1MB write guard fired. Restructure the function to keep only: - skeleton anchors that have at least one drill-down pointing at them - drill-down refs whose root anchor is among the kept anchors Unreferenced anchors are still dropped (the original intent), and orphaned drill-downs are dropped at the same time so the refmap can no longer accumulate dead entries. Updated existing test to assert the new keep-when-referenced semantics and added a regression test for the orphan-drilldown leak path. * fix: align resolve traversal with snapshot child-attribute set find_element_recursive previously walked AXChildren first then fell back to AXContents only, and resolve_element_name only derived its fallback label from AXChildren. Snapshot building, however, uses the full child_attributes() list (AXChildren, AXContents, AXChildrenInNavigationOrder) via copy_children. That asymmetry meant refs minted for containers exposed only through AXChildrenInNavigationOrder appeared in the snapshot output but produced false STALE_REF errors on drill-down or action commands because the resolver could not find them again. Route both call sites through child_attributes() so resolution sees the same children as snapshotting. * fix: bypass AXConfirm on web elements and add skeleton anchors to drill-down * fix: compare AX elements via CFEqual in web_action_had_effect AXUIElementCopyAttributeValue follows the CoreFoundation Create rule and returns a freshly-allocated CF object on every call, so raw pointer equality (`before.0 != after.0`) between two separate copies of AXFocusedUIElement was ALWAYS true even when the focused element was unchanged. That made Step 1 (AXPress) of activate_web_element appear successful every time and left Steps 2-4 of the escalation chain (AXConfirm bypass, child actions, CGClick fallback) dead code on web elements. Replace the pointer comparison with CFEqual, which compares accessibility element identity rather than heap addresses. * refactor: unify allocate_refs across snapshot and drill-down paths allocate_refs in snapshot.rs and allocate_refs_with_root in snapshot_ref.rs were near-exact copies that differed only in whether each allocated ref carried a root_ref tag. The duplication meant any bug fix or behavior change in one path risked silently drifting from the other (bot flagged this after the first round of fixes added even more duplicated skeleton-anchor logic to allocate_refs_with_root). Move the shared logic into ref_alloc.rs behind a new RefAllocConfig struct with Option<&str> for the root_ref_id, and delete the snapshot_ref.rs copy. Both snapshot::build and snapshot_ref::run_from_ref now route through the same function with their respective configs. append_surface_refs and the existing unit tests in both modules are updated to use the shared function + config. * chore: drop with_root from drill test names after allocator unification The snapshot_ref tests kept their original test_allocate_refs_with_root_* names after the allocator dedupe even though allocate_refs_with_root no longer exists. Rename them to test_drill_alloc_* so grep for allocate_refs_with_root returns zero matches and nobody assumes a second allocator path exists. * docs: compound DRY ref-allocator dedupe into knowledge base Adds docs/solutions/best-practices/deduplicate-ref-allocator-via-config-struct-2026-04-14.md documenting the root cause, guidance, consequences, trigger conditions, and before/after of the allocate_refs / allocate_refs_with_root duplication that landed on PR #20 and was unified in d06a6c2. Also: - gitignore allows docs/solutions/** (was caught by docs/* rule) - CLAUDE.md workspace tree surfaces docs/solutions/ so fresh agents discover the knowledge store - docs/phases.md drops two stale DrillDownConfig references (lines 62 and 226) — the symbol no longer exists in the codebase * refactor: drop unused root_ref field from TreeOptions TreeOptions.root_ref was populated from SnapshotArgs.root_ref by tree_options() and then never read anywhere. The actual routing lives in execute(): `if let Some(ref root) = args.root_ref` branches to snapshot_ref::run_from_ref(adapter, &opts, root), passing the root id as an explicit argument — opts.root_ref was never consulted by any caller. Delete the field from TreeOptions and its Default impl and stop populating it in tree_options(). SnapshotArgs.root_ref remains as the single source of truth for the drill-down target. * fix: suppress skeleton flag when --root is set tree_options() clamped depth based on skeleton && root_ref.is_none() but still forwarded skeleton: args.skeleton unconditionally. When a caller passed --skeleton --root @e3, opts.skeleton stayed true, so adapter.get_subtree on macOS built a truncated skeleton tree instead of the full drill-down subtree, AND ref_alloc::allocate_refs tagged skeleton-anchor refs with the drill-down root_ref — both unintended. Tie the skeleton flag to the same root_ref.is_none() guard the depth clamp already uses. A drill-down is always a full subtree now, never a skeleton view. Adds test_tree_options_suppresses_skeleton_for_drill_down to lock the behavior in and extends the existing clamp test to assert the flag still propagates on non-drill paths. * fix: detect web action effect via value + selected + focus change The previous focus-change-only heuristic in web_action_had_effect was wrong for non-Chromium AXWebArea elements (Safari WKWebView, Mail, Notes): AXPress on a checkbox toggles AXValue correctly but does NOT shift the focused UI element. web_action_had_effect returned false, the chain escalated to CGClick, and CGClick toggled the checkbox a second time — net zero visible change. The pointer-equality bug that ae78cbc replaced was masking this by always returning true; CFEqual exposed the real signal gap. Capture element state into a PreActionState struct (focused, value, selected) before any AX action runs, and consider the action to have had an effect if ANY of those three fields differs afterward. Element value covers WKWebView checkboxes, text fields, sliders. Selected covers tabs, radio buttons, and list items. Focused still covers Chromium's DOM-driven focus shifts. Triggers on whichever signal is most relevant to the element type without needing role-specific code. Also re-exports copy_bool_attr from crates/macos/src/tree for use by the chain-steps module. * fix: emit skeleton boundary when drill path is about to hit raw cap In skeleton mode, a chain of anonymous web wrappers (AXGroup / AXGenericElement with empty title and value) never advances the semantic `depth` counter, so `child_depth > max_depth` never fires no matter how deep the chain runs. The recursion then hits the `raw_depth >= ABSOLUTE_MAX_DEPTH` top-of-function guard and silently returns None, dropping the subtree without any children_count marker. Agents see truncated output with no indication that anything was dropped. Extend the at_skeleton_boundary condition to also fire when child_raw_depth is about to hit ABSOLUTE_MAX_DEPTH, so skeleton mode always emits a visible truncation node for deep wrapper chains instead of disappearing them. The old `raw_depth < ABSOLUTE_MAX_DEPTH` half of the guard was redundant — the top-of-function check already ensures the current frame has raw budget; the meaningful question is whether the CHILD frames will. * perf: compute is_in_webarea once per verified-press call try_focus_then_verified_confirm_or_press called is_in_webarea(el) to gate AXConfirm, then fell through to do_verified_press which called is_in_webarea(el) again on its first line. Each call walks up to 20 AX parents via per-element IPC (AXUIElementCopyAttributeValue on AXParent), so every web-area click through this path paid the cost twice. Extract a private dispatch_verified_press(el, caps, in_web) that takes the web-area flag as an input. do_verified_press keeps its existing (el, caps) signature for chain_defs compatibility and computes is_in_webarea once, then delegates. The focus-then-confirm path also computes it once and passes it through. The result is identical on both paths with one AX traversal instead of two. * fix: skeleton anchors in drill-downs must not inherit root_ref; restore AXBrowser AXContents fallback Skeleton anchors discovered while processing a drill-down subtree were being tagged with the drill root_ref, making them indistinguishable from regular drill entries. re-drills would delete those anchors via remove_by_root_ref, breaking sub-drilling. They now always carry root_ref=None so they survive re-drills as stable targets. AXBrowser child resolution dropped the AXContents fallback when child_attributes() was unified — the resolver now uses ["AXColumns","AXContents"] matching the original traversal order. * fix: suppress skeleton anchor creation in drill-down mode to prevent orphaned ref accumulation * fix: bounds-based resolver pruning and CGClick fallback on chain timeout Resolver was doing exhaustive DFS through entire AX tree (depth 50) to find a single element. For large documents like a dense spreadsheet this meant visiting tens of thousands of cells via AX IPC, causing multi-minute hangs before the click chain even started. Two changes: - find_element_recursive now prunes subtrees whose spatial bounds do not contain the target element's centre point, and aborts the whole search after five seconds. Numbers table -> button not inside -> entire table skipped. Resolution went from >1m49s to <50ms. - execute_chain now sets a 1s app-level AX messaging timeout so calls to children/parents fail fast instead of blocking for the system default 6s, and when the 10s chain deadline fires it attempts the CGClick step before returning failure so the coordinate fallback is always reached. * refactor: resolve 9 code-review todos for progressive skeleton traversal - skeleton refresh now starts from RefMap::new() (removes stale ref accumulation across refreshes) - drill-down snapshot resolves real WindowInfo via PID lookup instead of synthetic empty id - --root flag validates ref format before RefMap lookup (returns INVALID_ARGS not STALE_REF) - ABSOLUTE_MAX_DEPTH truncation emits boundary node with children_count instead of silently dropping - fix ref vs ref_id field name in commands-observation.md JSON examples - fix phases.md TreeOptions listing root_ref (it lives in SnapshotArgs, not TreeOptions) - add batch snapshot skeleton/root examples to commands-system.md - split snapshot.rs and snapshot_ref.rs test modules into separate _tests.rs files (143/69 LOC) - add integration tests for skeleton + drill-down workflow including invalid --root validation * docs: compound progressive snapshot review hardening * docs: tighten progressive snapshot compound write-up * docs: update README with progressive skeleton traversal and fix command count to 50 * docs: correct command count to 53, add Notifications section and platform notes --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> | 6 months ago | |
feat: integrate Linux support with privacy hardening (#311) * chore: remove three uncalled lab scripts No workflow, hook, script or test calls them: a symlink helper for local skills, a root-only uinput stub whose legs were never written, and an unrun self-test of the npm wrapper smoke, which CI runs directly. * docs: remove stale evaluation records and unreferenced diagrams The architecture diagrams and the Slack token illustration were not linked from anywhere and showed Linux as planned. The acceptance audit was a withdrawn status record. Two solution docs were folded into the docs that already carried their context, so each lesson has one home. Validated in the docs removal ledger (docs-3, 6, 8, 11, 12, 13). * docs: correct solution docs that contradict the code Many learnings named moved files, deleted tests, stale function signatures, or rules the code no longer follows (bare refs accepted, native apps never activated, one box lock instead of two slots). Each now cites the code that holds today, and none cites the planning docs or the untracked dogfood reports. * docs: correct CONCEPTS.md against the shipped adapters Several definitions described one platform as if it were every platform, missed the Linux adapter, cited probe rows that measure something else, used a screenshot flag that does not exist, and called dogfood reports committed. Session Manifest folds into Session and the must-catch fixture into Gate Self-Test, where each was already half defined. * docs: correct contributor docs, the FAQ, the JSON contract and runbooks CONTRIBUTING.md now points to CLAUDE.md for the engineering rules instead of keeping a second copy that had drifted (a MockAdapter that does not exist, an E2E command that exits without the exclusive-desktop variable). The JSON contract described an unknown disposition the code no longer emits, the FAQ repeated the README platform table, and the Windows runner runbook cited plan ids and a trigger branch that merged. * ci: assert the release-ffi unwind profile once per release ffi-release-panic-guard already runs the identical grep and publish-github needs it, so the copy in ffi-release-gates added nothing. * chore: let release-please bump the Linux skill version skills/agent-desktop-linux/SKILL.md carries the x-release-please-version marker and is published with the other skills, but was missing from extra-files, so its version would have stayed behind. * docs: make the test harness READMEs runnable as written Several harness commands failed as documented (the Linux consumer run without AGENT_DESKTOP_WM_ROOT, the discovery inversion without a header path, a headless probe binary that swiftc names differently) and the fixtures README listed files that do not exist. The Linux E2E guide drops plan history and keeps what an operator needs to run and read it. * Revert "chore: stop shipping the jev-desktop skill" This reverts commit 34826e104af928a345dbdfeaa83a9a9b72745fb0. * docs: fold the ref-root viewport lesson into the clipping doc The shared clipping doc already states the same incident and the repair that superseded it, so the lesson keeps one home. Validated in the docs removal ledger (docs-14). * docs: say jev-desktop needs a repository clone, and match it to its scripts The skill teaches node scripts/jev/*.mjs, which ship in no installed package, so it now says up front that it needs a clone of the repository and a TYPESAFE_API_KEY. Checked against the scripts: run.mjs retries 429/503/529 twice while act.mjs does not, --bin and --execute belong to act.mjs only, act.mjs sends longer values and the ancestor path, why is null on act, the stop list and exit codes were incomplete, and JEV_VERIFY_MS was missing. * docs: correct FFI handle, wait and last-error text against the header ad_free_handle works after the adapter is destroyed (native_handle.rs tests prove it), so the rule is to free handles before destroying the adapter only for use, not for release. The wait bullet now uses the header's AdWaitMode field names, AdActionStep lists mechanism and verified, the delivery-semantics accessor copies values instead of lending a pointer, and the JSON-string table gains ad_execute_by_ref_timeout, ad_trace_show and ad_trace_export. * ci: cut duplicate jobs and steps from the CI workflow Each removed piece repeated a check that another job already runs on the same OS, or depended on a file that is leaving the repository: - platform-check: three legs were subsets of clippy --all-targets on the same runner; the windows-11-arm leg is now a clippy step in test-windows-arm. - ffi-header-drift, ffi-python-smoke and ffi-passthrough now run as conditional steps on the ffi-panic-guard legs that used the same OS, so the header drift gate and the stub checks keep running on every PR. - check-release-consistency.sh and check-npm-package.js run in the supply-chain workflow on every PR, push and weekly, and need no OS. - the ubuntu bash3 compatibility run was weaker than the macOS Bash 3.2 run. - trash and trash-cli installs had no caller left. - check-phases-ledger-citations.ps1 read docs/phases.md, which leaves the repository, and would fail the Windows lane. The viewer_filter.js test moves here from the inspector workflow, so the shipped viewer code keeps a CI test. * chore: remove the unshipped inspector web UI tools/inspector is a local developer viewer that no shipped artifact, skill or release step uses, and its workflow only tested it. The viewer_filter.js test it also ran now lives in the CI workflow. * ci: give the release dry run a concurrency group and state why it stands alone The reason it is a separate read-only workflow lived only in docs/phases.md, which leaves the repository. A concurrency group stops a busy PR from queueing many full rehearsals, and trash-cli had no caller left. * ci: run the Windows capability probes only when probe files change The probes measure the hosted runner. The one contract they check against crate code, secure-value withholding, is also a plain unit test in the Windows CI lane, so code-only PRs no longer pay for a 30-minute probe run. * docs: document the jev scripts beside the code The jev-desktop skill left the shipped skill set, so its usage notes move next to the scripts they describe, checked against the current flags and environment variables. * chore: remove an unreferenced benchmark screenshot No file links to it. * docs: fix three FFI ownership and panic claims a review disproved A codex review proved three overstatements against source: ad_abi_version returns a constant with no panic trap, so only entrypoints that can panic catch_unwind; last-error strings are borrowed and never freed, so not every returned pointer has a free function; and ad_free_handle validates the adapter before it accepts a null handle. Remaining em-dashes in the FFI references became plain punctuation. * refactor(core): drop two helpers that nothing calls RefStore::for_tests and ErrorPayload::with_recovery had no caller in any crate or test. They sit in public modules, so rustc could not flag them. Recovery hints are still attached by recovery_for_code. Ledger rows A-01, A-02. * test(ffi): drop an unused re-export from the shared test module No ffi integration test names AdIdentifierKind through the common module; the file-level allow(unused_imports) hid it. Ledger row A-08. * ci: keep the 20-minute FFI timeout on legs that only run the panic probe Only the aarch64-apple-darwin leg gained the header drift and stub smoke steps, so only it needs the longer bound. * test(linux): drop live probes that only print timings semantic_click_latency_probe and the vocabulary report, timed pass and label timings asserted nothing and no gate reads their output. The vocabulary live test keeps every assertion path. Ledger rows A-12, A-13. * refactor(linux): drop stale dead_code allows on display errors DisplayCause, as_str and display_cause_error all have production callers, so the allows only hid any future dead code. Ledger row A-15. * refactor(windows): compile the canonical lock path only for tests Only the lease tests call canonical_lock_path, so a cfg(test) gate replaces the allow(dead_code) and the doc no longer promises a future caller. Ledger row A-16. * docs: cut the README to 130 lines and move install detail to docs/install.md The README keeps the pitch, install, the observe-act loop, the agent section, the command table and a plain platform table. Install-script allowlisting, attestation, the source build, per-OS permissions and platform footnotes move to docs/install.md unchanged. * docs: record the parameterized-replacement findings beside their probes The payload shape, the background no-op and the input-context route are what a maintainer needs when running replace_range_probe.swift, so they belong in the harness README rather than in a one-trial log. * docs: state what the jev scripts actually send and restore --no-values does not withhold text passed with --text, the clipboard is only put back when it held text, and a run reads the screen more than once per turn. The README said otherwise. * chore: stop naming a removed workflow in the redaction gate comments windows-e2e.yml no longer exists; the -Path override is described by what it does. * docs: note parameterized attributes in the real-app probe guide A setter that does nothing does not prove an editor has no write path; the probe lists parameterized operations such as AXReplaceRangeWithText, and the replacement range is in UTF-16 units. * docs: remove the Numbers replacement trial log now its findings live with the probe The real-app README carries the payload shape, the background no-op, the input-context route and the inventory lesson. Validated in the docs removal ledger (docs-15, codex and opencode both REMOVE). * chore: make infra comments match the repository The gitignore comment said no normalized twin is committed, the Windows e2e runner said no CI workflow touches it, one gate comment named a function that is being removed, and another described docs/phases.md as in the repository. * docs: make the PR template gate commands run as written The E2E line exited 2 without AGENT_DESKTOP_E2E_EXCLUSIVE=1, a string HOST_PKGS reaches cargo as one argument in zsh, and the skill checklist left out the Linux skill. * fix(npm): point the postinstall skill hint at commands that exist claude mcp add-skill is not a Claude Code subcommand. The hint now names the binary's own skills command and the skills installer the README documents. * docs: drop the jev README now that the jev-desktop skill documents the scripts Two descriptions of the same scripts would drift; the skill is the one that ships. * docs: shape the README for users, contributors and agents The fast path stays on top (install, a quick start, the agent section), followed by reference sections and a documentation map that points each reader to the right file. It restores facts the shorter version dropped: headless-by-default delivery, PowerShell ref quoting, the readable JSON sample, the accessibility-tree row and the per-OS package sets. * docs: name the CI job that now runs the FFI Python smoke The ffi-python-smoke job was folded into the macOS leg of the ffi-panic-guard job, so two docs pointed at a job that no longer runs. * refactor(core): share empty_trace_dir_error between trace show and export The two copies were byte-identical, so one pub(crate) helper in trace_read now serves both calls. Ledger row B-07. * ci: give the Linux live e2e job an explicit read-only token The job inherited an empty permission set, unlike its macOS twin; checkout needs contents: read and nothing more. * ci: pin the probe workflow's checkout to the same release as every other workflow It was the only workflow left on v7.0.0. * docs(core): make two rustdocs match the code they describe transform_tree prunes on is_ref_able, not on role alone, and the drill-down completeness note belongs on run_from_ref_with_context, not on count_nodes. * docs: state the jev read cost for run.mjs as well as act.mjs lead-infra's review found the skill gave the snapshot cost for act.mjs only. run.mjs reads the screen before and after each action (none after DRILL or WIDEN), and both scripts add a surface snapshot when an overlay is open and no region is pinned (desktop.mjs:62-76, act.mjs:236-237). The other five corrections already held in the merged text. * fix(cli): list linux among the skills get aliases skills get linux already resolves, but the help text left it out. * test(visual-debug): stop naming the removed inspector in a test env var The capture child marker now says what it marks, since tools/inspector is gone. * refactor: replace per-crate ensure_budget copies with Deadline::ensure_remaining Twelve private macOS helpers, two private Linux helpers, and the shared Linux bus and Windows permissions helpers all had the same body. One Deadline method now carries it, with unit tests. The DeliveryTracker variants, the mouse and chain variants, and Linux input/deliver.rs keep their own checks because they add delivery details. Ledger row B-01. * docs: restore rules and definitions an earlier cut dropped CLAUDE.md again states the @generated exemption, that every non-doc comment form is rejected while //! module docs are allowed, and that expect and panic are test-only like unwrap. CONCEPTS.md again says a ref-addressed drag picks up at the bounds center (and the slider workaround) and names the must-catch and must-pass fixture halves. * docs: restore two oracle contracts to the Linux E2E guide The rewrite dropped what the pointer delivery legs read back and what the dead-owner recovery leg requires, both of which an operator needs to judge a failing leg. * chore: keep the gitignore note on which capture twins are tracked Per-area probe directories commit their normalized twins; only the twins under probes/<platform>/captures/ are regenerated and untracked. * docs: say in Coordinate Fallback that raw --xy input focuses nothing A raw coordinate resolves with no ref and no window identity, so the caller owns where it lands; the fallback entry now states it directly. * refactor(windows): drop file-level dead_code allows in system modules Windows clippy with -D warnings passes with all twelve removed, so every item in those files is used and the allows only hid future dead code. The menu source agreement test no longer cites the allow. Ledger row A-22. * refactor(windows): parse w-<n> window ids in one place observe and surfaces carried byte-identical parsers with the same InvalidArgs message; window_ops now owns parse_window_id next to window_id. Ledger row B-08. * refactor(linux): share the already-satisfied pointer select result The combo and row select paths built the same skipped XTEST.click result with their own copy of the route constant; select_pointer now holds one. Ledger row B-11c. * refactor(linux): share clipboard delivery helpers between X11 and Wayland set_clear and wayland carried the same verify grace, fresh deadline, and not-delivered and unverified error builders; delivery.rs now holds one copy and messages stay with the callers. Ledger row B-11d. * refactor: build stale-evidence errors with AdapterError::stale_ref_because Three adapter copies of stale_evidence_error repeated the body of the existing core constructor, so the resolvers now call it directly. A core unit test pins its code, suggestion and disposition. Ledger row B-02. * refactor: move the live-evidence completeness helpers into core incomplete_live_evidence, essential_live_evidence_complete and known_actions were byte-identical across the adapters and work only on core locator types; live_element now owns them, with unit tests. Ledger row B-05. * refactor: move notification identity verification into core The macOS and Windows checks were byte-identical over core notification types, so core now exports verify_notification_identity with unit tests. Linux keeps its own check because its suggestion and disposition differ. Ledger row B-06. * chore: point the stale-ref gate at the constructor callers now use stale_evidence_error is gone; evidence-refused refs now build their error with AdapterError::stale_ref_because, so the comment and the failure hint name that. * test(e2e-linux): drop plan requirement ids from leg descriptions R9, R19, R22, R24, R32, R33 and R35 named requirements in a plan that has left the repository, so the prefixes meant nothing to a reader. Nothing parses the requirement text; leg ids are unchanged. * chore: stop the privacy scan comment from claiming the repo ships planning docs * test(e2e-linux): drop duplicate legs and dead harness helpers Six legs repeated the observation of a stronger surviving leg on the same tiers, two unit tests restated another test's assertions, and several helpers had no caller. Each row was validated by two independent models (ledger TR-145..175). * test(e2e-windows): drop a racing zero-timeout leg and dead fixture code The delayed-enable zero-timeout leg raced the fixture timer while the permanently disabled leg proves the same no-wait refusal; the click row's mechanism key is now asserted instead of ignored, and an unused fixture overload and an always-true self-test line are gone (ledger TR-149, TR-151, TR-152, TR-186). * test(e2e): fold the absolute-timeout exit code into the deadline test The removed test repeated the descendant-deadline test and added only the 124 exit code, which the survivor now asserts (ledger TR-185). * ci: run the four hermetic scripts self-tests in the fmt job perf_ab_probe_test.py, test-linux-live-gate.py, test-session-env.sh and test-remote-linux.sh guard the perf report math, live-gate shard and receipt rules, desktop environment selection, and the remote kill guard, yet no job ran them; each is stub-driven, needs no display or network, and takes about a second. The two that start private sessions stay manual (one already runs in linux-e2e). * test: make every self-rerun test prove its child ran the target test A test that re-runs its own binary with a libtest filter passed with zero tests run when the filter was wrong, which already hid the Linux cursor overlay check (its filter missed the overlay module). Each self-rerun site now asserts the child printed "1 passed; 0 failed"; the overlay filter is corrected, and the interactive clipboard test compiles again under its feature. * test(e2e-linux): drop duplicate contract self-tests and dead test helpers Registry and gate checks were asserted twice; the surviving registry test now also refuses an empty registry so it cannot pass vacuously (ledger TR-177..183). * test(core): drop tests that restate a stronger test or only exercise a mock Each removed test was matched by a surviving test at the same or a stronger boundary, or asserted only a mock's or a constant's own value; two merges keep the unique assertions (role round trip, toggleable negatives) in the survivors (ledger TR-001..019). * test(macos): drop duplicate and tautological tests, fold unique assertions into survivors Survivors now carry the sibling-radio suggestion, the AXOpen stale classification and the native plus DOM identifier pair (ledger TR-058..074). * test: drop CLI and conformance tests that only exercise their own mock The conformance window-identity tests asserted the mock adapter's behavior, and the version and wait-metadata assertions repeated stronger tests (ledger TR-142, TR-143, TR-161..163). * test(ffi): drop duplicate C ABI tests and keep their unique checks in survivors Typed discriminant round trips, out-of-range values, a second trapped panic and the last-error pointer across successful calls now live in the surviving tests (ledger TR-035..044). * test(windows): drop duplicate dispatch, notification and state tests The adapter's inline tests were byte-identical to adapter_tests.rs; the R2 advertisement tests now live as rung call counts and isolated advertisement cases in the action and tree tests (ledger TR-073..091). * refactor(linux): import test-only step types in the select tests, not production select_row.rs and select_pointer_combo.rs carried cfg(test) imports only for their tests' use super::*; the tests now import ActionStep and StepMechanism. * test(e2e): add error-envelope and batch failure legs to the macOS harness Agents branch on exit code and error.code for a bare @e1, an unknown app, a missing surface and a parse error, and on batch's continue versus stop-on-error results; no live leg proved them. Self-tests check every expected code exists in the core enum and that each leg asserts code and exit together. * test(e2e-linux): add error-envelope and batch failure legs Two registered legs drive the real binary on both X11 tiers: recoverable error envelopes (INVALID_ARGS for a bare @e1, APP_NOT_FOUND with exit 1) and batch per-entry failure with and without --stop-on-error, observed on the app itself. Pure verdict tests with lying fakes keep them checkable without a desktop. * test(e2e-windows): add error-envelope and batch failure legs Eight Reliability legs cover INVALID_ARGS, SNAPSHOT_NOT_FOUND, STALE_REF for an absent element, APP_NOT_FOUND and ELEMENT_NOT_FOUND with exit codes, and batch halt versus continue with per-entry codes. Not yet run: they need a Windows host (contract -SelfTest, the U7 self-tests, then the live suite). * refactor(core): route deadline clock reads and poll sleeps through one test-freezable seam Several core tests flaked on loaded CI runners because their deadlines ran on the real clock. Deadline now reads time and sleeps through crate-private now() and sleep(), which outside cfg(test) are the same std calls; tests can freeze and advance a thread-local virtual clock. Public API and production behavior are unchanged (codex review: no behavior change). * test(macos): replace the inventory retry wall-clock assertion with counted attempts The test asserted more than one attempt inside a real budget, failed on 6 branches when a slow runner fit only one, and 45b28ce9 only raised the budget to 500 ms. The retry and the partial-inventory refusal are now proven by attempt counts on a fake capture that never consumes its deadline. * fix(windows): confirm a target's exit on its process handle before reporting a menu-wait read error A window that stops pumping during teardown reads as APP_UNRESPONSIVE before its process object is signaled, so the single liveness re-check lost the race and the wait reported the transient code instead of STALE_REF (failed on 7 branches). The wait now blocks on the process handle for at most the remaining deadline, capped at one second, before it keeps the read error. First run on Windows CI. * test(linux): drop duplicate input, close, tray and tie-break tests Each removed test repeated a survivor at the same boundary; the unique recovery text, baseline counts and tie cases moved into the survivors (ledger TR-046..057). * test(e2e): add stale-ref and key-combo legs to the macOS harness The removed-element leg accepted four error codes; it now requires STALE_REF with a safe retry. A RefChurn leg proves STALE_REF and AMBIGUOUS_TARGET after an in-place UI swap, and a press leg proves combos by reading the field itself. * test(e2e-linux): add stale-ref-after-UI-change and key-combo legs The stale leg refreshes nothing between a real menu change and the click and requires STALE_REF with its recovery; the press leg checks ctrl+a, backspace and return by AT-SPI text, not by the command's own ok. * test(e2e-windows): add stale-ref contract and key-combo legs The removable row now reports clicks, so a stale ref proven by refusal also proves nothing was delivered; press legs cover ctrl+a, shift+tab, return and the blocked alt+f4 refusal. Not yet run: needs a Windows host. * docs: point the live-gate runbook at the focus leg that stays The lift leg duplicated lifecycle.focus_window_active_reads and never read the restoration it named, so the tests cleanup removes it; the retained live owner is the active-window read. * test(core): run the settle-loop and artifact-lock budget tests on the virtual clock These tests failed on loaded Windows and macOS runners because a real 50 ms lock wait or settle window expired before the code under test ran; a 16 x 20 ms retry wrapper hid it. They now freeze the virtual clock, so lock polls and settle sleeps advance time deterministically, and the retry wrapper is gone. * fix(windows): treat E_UNEXPECTED on a UIA read as a transient failure A provider tearing down a window or shell host answers one read with E_UNEXPECTED; it was classified terminal and narrowed to a non-retryable APP_UNRESPONSIVE, so one read ended a whole wait (failed on 4 branches). It is now a retryable unresponsive read inside the caller's existing deadline; E_FAIL stays terminal. First run on Windows CI. * test(linux): make vacuous assertions able to fail Repaired tests named a behavior their assertions could not detect (a negative check that any value passed, a banned-name list with dead entries, an unverified result asserted only as not verified); each now checks the stated contract. * test(macos): make vacuous assertions able to fail, each proven by a production mutation Process-group kill, semantic-only disclosure chains, single capture, strategy fall-through, scan tolerance, endpoint naming, overlay acknowledgement, the select depth cap and the name-plan slots now fail when the behavior breaks. * test(ffi): make C ABI tests assert what their names promise Modifier cap, JSON error forwarding, invalid discriminants, envelope fields, dirty out-params on a worker thread, wait error envelopes and the session adapter's fail-closed path each had an assertion that could not fail; every repair was proven by breaking the production path once. * test(e2e): make harness self-tests fail for the regressions they name * test(e2e-linux): make harness self-tests and a menu leg assert structured results The menu timeout leg now checks the structured wait_timeout details, and the oracle, runner, session-sweep, workflow and verdict self-tests assert the specific failure each one names. * test(e2e-windows): replace diagnostic scaffolding and soft guards with real assertions The headed double-click leg used debug output, a fixed sleep and one read; it now uses Assert-Effect like its siblings. The handle-set and overlay controls no longer skip silently, and the Chromium rate leg fails when every sample is stale. Not yet run: needs a Windows host. * test(core): drop find, get, wait, session and trace tests that repeat stronger tests The removed find tests ran a cfg(test)-only snapshot collector production never uses; that collector's count helper went with them. Merges keep the observed-role negative, the null-bounds case, the conflicting wait flags as a table and the identifier preference in the survivors (ledger TR-020..034). * test(windows): drop input fake-sink, policy, clipboard and surface tests that repeat survivors The keyboard and mouse send tests only checked that the test fake stored what it was given; the clipboard, chain and surface merges keep their unique cases as table rows or assertions in the survivors (ledger TR-092..106). * test(windows): drop tree property, hit-test and resolve tests that repeat survivors Each removed test had an identical or stronger twin in the same module family (ledger TR-124..140). * test(linux): make clipboard, keyboard-frame, remap, capture and close tests assert their contracts The remap test now proves the deadline cancelled after the remap and that the keymap was restored, the off-screen capture test places its window instead of skipping, and the close gate is checked for both graceful and forced closes. * test: make CLI dispatch, overlay and real-app tests assert what they claim Real-app tests now fail when their precondition is missing instead of passing silently, the overlay show test pins POLICY_DENIED, and the conformance test checks each capability's outcome, not only that the call returned. * test(real-apps): assert the Numbers failure stop and drop a dead plan citation * test(macos): make capture-scope, screenshot and child-label tests assert their contracts The capture-scope tests now run the production scope filter instead of a test copy of it (the single-pid case is kept beside the pid-set case), the screenshot timeout checks the child is gone, and the child-label rule is checked for every role it names. * test(core): order concurrent snapshot writers instead of retrying lock timeouts, and pin tray ids Eight writers polled one lock with no fairness and a 30 s retry hid starvation that failed CI on four branches; the writers now take turns, and a single-thread test on the frozen clock proves a held store lock still blocks a writer. A new test pins that allocation keeps a RuntimeId identifier and its kind, which no test covered after the tray identity change. * test(windows): drop lifecycle, launch, HRESULT and signal tests that repeat survivors Merges move the exit-code serialization cases into core's process-state tests and the HRESULT detail strings and generation checks into the surviving Windows tests (ledger TR-108..121). * test(windows): drop two duplicate tree tests and make hit-test and resolve tests assert their contracts hit_test now judges a probe through judge_probe, so a probe miss is tested on the production path (same behavior: a miss is Unknown, a hit is judged); the fixture teardown, minimized-window, anchor and timeout tests assert what they name (ledger TR-122, TR-123, TF-144..157). * test(linux): make capture-cap, tie, key-owner, menu and tray tests assert their contracts * test(core): run live-locator and ref-action wait tests on the virtual clock These tests had their budgets widened and an exact attempt count loosened to 'at least one' to survive slow runners; on the frozen clock the retry loop's own sleeps advance time, so the original exact counts and small budgets hold. * test(windows): drop the setup-time stopwatch from the headless notification wait refusal The elapsed < 2 s assertion measured COM and adapter setup and failed twice on loaded runners; POLICY_DENIED is not retryable, so a burned deadline would already surface as TIMEOUT and fail the code assertion. * test(windows): decide modern-capture frame-wait expiry inside the wait, not by wall time The fallback test asserted TIMEOUT against an 80 ms budget that real capture setup could consume first (failing as INTERNAL); the cfg(test) frame hold now starts its budget inside the frame wait, and the pattern test waits for a frame showing the fixture instead of sleeping 500 ms. Production frame wait unchanged. * test(windows): signal the menu fixture's open state from real menu mode The fixture announced the popup before TrackPopupMenu entered menu mode and ignored its result, so the chromium composition test compared two live reads that could straddle the flip; readiness now comes from menu mode itself and the comparison waits until both sources agree. * test(linux): make tray, signal, settle, walker and shell-owner tests assert their contracts The error-code boundary test now enumerates codes exhaustively at compile time instead of a hand-typed list of 16, and the remaining repairs check the behavior their names state. * test(macos): replace real-clock budgets and stopwatches with counted attempts and events Stabilize-loop tests could expire before their first attempt on a slow runner; they now use budgets the fakes never consume and assert exact attempt counts, and the overlay acknowledgement tests block on a release channel instead of a 700 ms sleep. * test: isolate command-host sockets per test and assert batch deadlines by value Fixed socket and symlink names keyed by pid were never removed, so pid reuse could fail a later run; each test now owns a removed-on-drop directory. Batch deadline tests assert the budget the adapter received instead of wall time. * test(ffi): isolate the round-trip test's state root and require the snapshot log event The round-trip test set HOME under its own lock but left AGENT_DESKTOP_HOME alone, so a developer's state root leaked in; it now uses the shared isolated home helper. Log-callback tests no longer fall back to the first delivery. * test(core): make actionability, trace, find and close tests assert their contracts Each repaired test was proven by breaking its production path once. The benchmark example's rect test moved into the library's rect tests so it runs in CI, and the find roles hint is now tested through execute instead of a cfg(test)-only copy, which is deleted. * test(windows): make dispatch, envelope, scroll and fixture tests assert their contracts Permissive Err arms, restated literals and a self-comparing fixture expectation are replaced by assertions on the delivered outcome. * test(linux): make tree walker, projection and placement tests assert their contracts * test(core): make session, overlay-routing, hover and pointer tests assert their contracts Session precedence tests pass the env value directly instead of mutating the process environment, gc is actually exercised on an unreadable manifest, the skeleton golden compares the whole tree, and hover counts receiver checks instead of timing them. * test(windows): make drag, capture, HRESULT, signal and wait tests assert their contracts Assertions true by construction (an intersection member found in its own set, an assert_ne after an exact assert_eq, a uniqueness loop over a test-local list) are replaced or removed, and the remaining checks name the behavior they guard. * test(linux): make resolve tests assert the unread-region refusal they name * test(core): drop the null ref_id from the skeleton golden fixture The skeleton golden test now compares the whole serialized tree, and the serializer omits a missing ref_id, so the fixture must not carry one. * test(core): make ref, refs, trace-read and policy tests assert their contracts The XSS gate now asserts the exact escaped payload, the oversize-refmap guard is exercised through the production writer, poll and wait tests count resolves instead of timing them, and a new test pins identifier kinds through RefEntry identity. Each repair was proven by breaking its production path once. * test(windows): make lifecycle, overlay spawn and shell-surface tests assert their contracts A tautological ambiguous-target shape check now serializes the production value, a self-comparing error-code helper is gone, and the taskbar test no longer returns early on an empty tree. * test(npm): drop duplicate platform and wrapper tests and check the real staging directory The staging-cleanup tests read the destination, which installArchive never stages in, so a cleanup regression still passed; they now compare the package bin directory before and after. Duplicate platform-table and wrapper tests are gone, with their unique helper-name and Trash-warning checks kept in the survivors (ledger TR-189, TR-191..193, TF-187, TF-188). * ci: run the Numbers sheet-script self-test in the fmt job numbers_sheets_test.py drives numbers-sheets.sh against a fake binary in a temporary home and proves its failure stop; it is hermetic and nothing ran it. * test: nest extra test modules so moved anchors keep room under the line cap ref_action.rs (400 lines) and the Windows tree/resolve.rs (398 lines) become their folders' mod.rs in the restructure and must then declare their new child modules. Their extra test modules are declared from the files' tests modules instead, so both anchors stay under 400 lines. The nested test files import their parent by absolute path, keeping what `super::` meant before. The Linux action scan walks src/actions recursively, so the banned-call scan keeps reading the action files that move into select/, scroll/ and write/, and it asserts files from each of those groups are still scanned. * refactor(cli): give the command host and diagnostics their own folders Nine command_host*.rs files and three diagnostic*.rs files sat loose in src/, so the binary's root read as a flat list. Each family now lives in a folder whose mod.rs is the former entry file; the helpers were already #[path] children, so no module path changes. * refactor(core): group core's leaf types into response, input, notifications, storage and wait Sixty-seven of core's 270 root files are the envelope and error model, the input and notification parameter types, the on-disk state root, deadlines and signals, plus adapter and session helpers. They now sit in folders named after the concepts in CONCEPTS.md, so a reader finds a type by its domain instead of scanning one flat list. Module paths gain one segment; root re-exports in lib.rs keep every public name unchanged, and adapter becomes a public module because it now holds the public hit_test, live_element and native_handle modules. * refactor(core): group core's tree, action, system and snapshot types by domain The accessibility-tree model, the Action model and its execution types, app and window and process types, snapshot building and the context module were 115 loose root files. They now live in tree/, actions/, system/, snapshot/ and context/, named like the platform crates' layers so a type a platform crate uses from its input/ or system/ lives under the same name in core. The stray actionability tests move into actionability/. Two test files import one module once so the longer paths keep them under 400 lines. * refactor(core): group refs, ref actions, locators and traces into their folders The ref map, ref store, allocation and identity files, the headless ref action flow, the locator with its live evaluator, and the trace writer with its reader were the last loose families at core's root. Each family now lives under the module that already anchored it (refs.rs, ref_action.rs, locator.rs and trace.rs become their folders' mod.rs), so those module paths do not change. trace becomes public because it now holds the public trace_read and trace_sanitize modules. lib.rs is the only file left at the root. commands/ stays flat: its contract tests require commands/<name>.rs. * refactor(macos): group the macOS layers into concept folders Each layer held one flat list: 62 files in tree/, 68 in system/, 45 in actions/. Concepts with four or more sources now have a folder named with the vocabulary all three platform crates share: tree/resolve/ and tree/properties/, actions/chain/ and actions/select/, input/clipboard/, keyboard/ and mouse/, system/app/, process/, window/ and permissions/. cf_type.rs leaves the crate root. The cursor-overlay and permission-guide Objective-C bridges move next to the Rust files that declare them, with build.rs and the native test imports following. File names do not change. * refactor(windows): group the Windows tree layer into concept folders tree/ held 117 files in one folder. Ref re-identification, the Win32 test fixtures, the UIA walkers, the property reads and the hit test now each have a folder (resolve/, fixture/, walk/, properties/, hit_test/), using the same names as the other platform crates. The fixtures' self-rerun filters and the hit-test scan labels follow the moves, and the public tree paths used by the uia_tree_dump example and the resolution probe are updated with them. * refactor(windows): group the Windows system and input layers into concept folders system/ held about 70 loose sources and input/ about 26. Capture, windows, shell surfaces, wait signals, the COM runtime glue, menus and the shared test seams now each have a folder under system/, and clipboard, keyboard and mouse each have one under input/, with the names the other platform crates use. Public names in lib.rs do not change. Four clipboard helpers now sit under the clipboard module's existing cfg(target_os = "windows"); the crate only builds for Windows, so the set of compiled and run code is the same. * refactor(linux): group the Linux tree layer into concept folders tree/ held 209 files in one folder, the most crowded in the repository. It now has thirteen concept folders (walk, resolve, hit_test, menu, surfaces, placement, live_read, observe, properties, element, vocabulary, capabilities, live) and keeps two loose sources. All live rigs and live suites sit in tree/live/, so the live gate selects them by one prefix; its module strings, its self-test and the runbook follow the moves, and the PARALLEL_READS entry for a module that never existed is dropped. * refactor(linux): group the Linux action, input and X11 layers into concept folders actions/ held 95 files and input/ about 57 loose files, and system/x11/ (87 files) and system/window/ (57 files) had grown past one screen. Selection, scrolling, writes and live rigs now have folders under actions/; keyboard, mouse, physical targeting and the clipboard owner have folders under input/; the X11 client splits into capture, control, monitor, stacking and window, and the window operation pipeline gets window/window_op/. system/ keeps the shape it already had. The live gate's module strings follow the moves. * ci: fail the build when platform code leaks or a crate's layout drifts Platform isolation was checked only through cargo tree on CI and one PowerShell scan, so a source-level leak or a file dropped in the wrong layer reached review unnoticed. scripts/check-architecture.sh checks manifests through cargo metadata and sources through a small lexer: core depends only on its listed crates, only the binary and FFI wire platform crates, no platform crate reaches another, core keeps OS-conditional code in an exact allow-list, every crate keeps its top-level layout, capabilities are implemented in their layer, core domains do not reach commands, and tests are declared beside their code. It plants a violation of every rule before it scans, so a rule that stops detecting fails the gate. It runs in the pre-commit hook and next to the source rules in CI; the cargo tree steps stay as the transitive check. * docs: describe the crate layout and the guard that keeps it CLAUDE.md described the platform layers but not where core's files or a layer's sub-concepts go, so the next contributor or agent would guess. It now states core's domain folders, the shared platform layout and subfolder vocabulary, the file-naming and test-placement rules, and the grounding rule that enforces them. * docs: follow the source restructure in path citations The restructure moved test and module files into feature folders, so three code-span paths and a handful of folder-relative citations in the solution docs, CONCEPTS.md and two test READMEs named files that no longer exist. * test: point the e2e error-code contract at the moved error_code.rs error_code.rs moved to crates/core/src/response/ in the core restructure. The e2e harness contract test builds the path from parts, so the literal path rewrite did not see it and two contract tests failed to open the file. * docs: re-point short-form file citations at their moved files Seven citations named a moved file by its path relative to its crate (for example system/shell_surface_raise.rs), a form the full-path rewrite did not match. Each now names the file's new folder. Short citations of files that did not move, such as the Windows actions/ labels, are unchanged. * test(e2e): read written fields directly and run the host ref-churn leg fully hosted The first live run failed five legs on the harness, not the product: batch and ref-churn checks read SwiftUI echo labels that set-value does not promise to update, and the host-client variant enabled the host only for one command and used a non-private temporary directory, so the host refused it with INVALID_ARGS. The legs now read the field's own value, and the whole host run uses the host and the private temp dir. The stale-ref and close-app assertions are unchanged: they expose pre-existing product defects. * test(e2e): skip three known product defects by exact signature and end the churn fixture by pid No failing leg ships, and no real defect is hidden. The scroll, default-CLI stale-ref and close-app cases skip only when their known defect signature appears, naming the defect, file and origin commit; any other result still asserts, so each case re-arms itself once the product is fixed. When close-app leaves the RefChurn fixture running, the leg ends its own process and asserts it is gone. * fix(macos): let close-app end a process that has no LaunchServices launch date Termination refused whenever NSRunningApplication reported no launch date, so close-app could not end an app launched outside LaunchServices, contrary to the rule that absent evidence is not conflicting evidence. The bridge now passes whether a date is present, and only a present date more than five seconds from the verified instance refuses; pid, start-time token and exit checks are unchanged (codex review: only the intended change). * fix(macos): reveal a clipped scroll area through its enclosing scroll view Scroll-into-view looked for the clipping viewport starting at the target, so a target that is itself a scroll area became its own viewport, produced no direction, and stayed clipped (ACTION_FAILED, live offscreen). The original walk still runs unchanged; only when it returns the target itself does the search repeat from the target's parent (codex review: only the intended change). * test(e2e): assert scroll and close-app again now that both defects are fixed The two signature skips would hide a regression of the fixed defects, so the scroll case asserts unconditionally again and a surviving churn fixture after close-app fails the leg while the leg still ends its own process. Only the default-CLI stale-ref skip remains, for the documented aliasing gap. * test(e2e): gate two more known product defects by signature and stop the command host Live, an unknown-app lookup returned TIMEOUT because NSWorkspace listed one pid twice and the app inventory refuses a list with a skipped record; that case and the batch entry using it now accept only that exact signature, naming the defect. The default-CLI sibling check shares the documented aliasing defect. The hosted ref-churn run now stops the command host it started, which otherwise kept the desktop lease for its 15-minute idle life. * fix(e2e): parse only stdout from captured command output The binary prints its JSON envelope on stdout and tracing warnings on stderr. Every capture the suite parses as JSON merged stderr, so one warning line made json.loads fail. A WebKit WebContent process that NSWorkspace lists twice now makes every --app lookup warn about a duplicate pid; AE4 read an empty window list and aborted the suite. All captures now discard stderr, matching find_target, and a harness self-test refuses a capture that merges it again. * chore: move runbooks and residual review notes out of git into the local planning folder They are operator and review working notes, not product docs: no code, test, workflow or doc reads them. They now live with the other planning material in the globally ignored .compound-engineering/ folder. * test: restore two test assertions the repair pass over-tightened and stop echoing a session id CI showed both repairs were wrong, not the product. The Windows dispatch test now accepts any deliberate refusal again (headless double-click is refused with POLICY_DENIED by design), as it did before. The macOS helper timeout test asserts it returns within a second again, which is what its name promises; the replacement assertion held on one machine and tested nothing about blocking. The overlay test message now prints only the field it checks, so it no longer writes a session id into a failure message (CodeQL rust/cleartext-logging). * docs: tighten test-quality learnings and note the dropped accname agreement test * docs: point two measurement learnings at the code and scripts that hold them * docs: correct the Windows hardening claim in the CI-lane learning * docs: point the snapshot window citation at current code and tighten policy learnings * docs: drop a cited regression test that does not exist * docs: tighten accessibility runtime learnings without changing their facts * docs: tighten Numbers and Xcode learning wording * docs: use the measured popover timings in the wait-for-closed learning * docs: tighten Xcode and native learnings; keep the empty-search learning until merged * docs: split one dense sentence in the agent contract * docs: tighten prose in the contributor and output docs * docs: tighten the find-name learning wording * docs: tighten the concepts glossary wording without changing its terms * docs: tighten test README prose and keep every command and link * docs: cut the learnings store to the lessons that are still true and live nowhere else Two reviewers checked every learning (codex Sol proposed, codex Luna validated with the burden of proof on deletion); a learning stayed whenever they disagreed. Removed dated evaluation logs, trial records and lessons already carried by code, tests, CLAUDE.md, CONCEPTS.md or the skills; moved the one unique lesson of five learnings into a surviving one; trimmed the rest to rule, fix and one evidence pointer. The two remote dev-box learnings are gone: they described one maintainer's machines, not the product. 52 files and about 46,700 words become 15 files and about 11,100 words. Citations in the e2e harness and test READMEs now point at the surviving learnings. * docs(windows): stop citing two learnings that were removed The rule each comment cited is stated in the comment itself, so the pointer is dropped rather than re-aimed. * chore: remove tooling and paths tied to one maintainer's dev machines agent-desktop is a product for any team, so the repository no longer carries a remote-box workflow: remote-linux.sh (worktree sync to an SSH alias), box-slot.sh (a lock on that box), session-env.sh (importing that box's desktop session) and their two self-tests are removed, with their fmt-job lines. Private-session sessions now default to ${XDG_RUNTIME_DIR:-/tmp}/agent-desktop-sessions, overridable with AGENT_DESKTOP_SESSION_BASE, at every site that creates, discovers or cleans them (shell wrappers, live gate, e2e runner, Rust live tests); the window manager defaults to the system install. Docs and the Linux probe ledger no longer cite personal paths. * test(e2e-linux): take the stale-ref process baseline after the app is running The leg read the process list before Calculator started, so a correct refusal failed its own 'must not end the app' check. The unit test now models the launched process and fails on the old ordering. * fix(linux-tests): keep each OS user's private sessions under their own base Without XDG_RUNTIME_DIR every user shared /tmp/agent-desktop-sessions, so the second user could not create a session. The fallback is now /tmp/agent-desktop-sessions-<uid>, created 0700, at every site that computes it. * test(macos): restore the screenshot timeout test the repair pass over-tightened The repair also required the killed child to be reaped inside the product's cleanup deadline; on a loaded CI runner the product truthfully reports that it was not, so the assertion tested the runner, not the timeout. * fix(linux-tests): let the live gate merge receipts without creating a session base --merge starts no sessions, so it no longer needs a writable session base; the self-test proves it with a base that cannot be created. * test(linux): make the proc-scan test fail with a diagnosis instead of a bare unwrap It failed once on CI with no evidence. The helper no longer proceeds silently when the child never becomes ready, and a missed scan reports what /proc shows for the child, so the next occurrence names its cause. * fix: harden desktop boundaries and bound native operations * fix: install inspector signal handlers before readiness * test(linux): stop running renamed copies of sleep, which Ubuntu 26.04 no longer supports GitHub's ubuntu-latest now includes Ubuntu 26.04 runners, where coreutils is a transitional package over the uutils multi-call binary. That binary picks its tool from argv[0], so a copy of sleep renamed for a test exits at once and the scan finds a zombie. proc_scan failed on every 26.04 run, on branches without this cleanup too. One shared helper now copies the standalone system shell and blocks it on a held stdin pipe, which keeps the test's name on any Linux; the five tests that renamed sleep use it. * test(linux): keep blocking fixtures alive in the background without stdin A background command in a non-interactive shell gets /dev/null as stdin before any redirection, so the orphan launcher's blocking copy read EOF and exited. The copied shell now loops on 'sleep 1' instead of reading stdin; that keeps it alive under its own name in the foreground and the background alike, and lets both launchers return to their original form. * test: make native fixtures portable across CI hosts * test: use an owned process fixture for Linux discovery * fix: preserve trace readability and clear stale overlays * perf: avoid repeated trace ledger replacements * fix: reject shared helper writes and isolate Linux test fixtures * fix(linux): list every top-level window the registry exposes, whatever its role The window list admitted a root child only for roles 23, 16 and 69, a modal panel or a showing filler. Toolkits give a top-level window the role of its widget, so the GTK3 Save As file chooser (19), a Qt message box (2) and a Qt push button shown as a window (43) were never listed, while the modal gate, which reads every root child, refused focus because of the same dialog (L41-1). A root child that implements Component is now a top-level, and one predicate decides it for listing, frame identity, menu inventory and the shell content window. Dialog-ness is one predicate too: the AT-SPI dialog roles read from libatspi (alert, color chooser, dialog, file chooser, font chooser, L41-4), used by the window list, focus, sheets and nested dialogs. A blank, showing top-level that holds only menus is a popup menu surface (the GTK3 context menu and Chromium menus, L41-2, L18-7): menu observation keeps it and the window list drops it. The size identity for unnamed windows applies to every toolkit without global positions instead of GTK4 role-39 panels only, and the duplicate reports_global_position copy is gone. * fix(linux): attribute a launch handed to a running instance by its new window Launching thunar with the session's daemon running failed with "exited with status 0 without a verified application owner" although a new Thunar window opened (L41-6): the launcher forwards to `Thunar --daemon` and exits, and the attach rule also compares the argument list. After a clean launcher exit with no strict owner, the one running process of the same canonical executable that shows a top-level window absent from its pre-launch baseline now owns the launch, and with --activate that window is reported. Several such processes stay ambiguous, none keeps the failure. Strict argument identity for attaching before a spawn is unchanged. * fix(linux): publish hidden and offscreen in snapshots from the action gate's rule A snapshot gave Mousepad's hidden toolbar button a ref with no state, and the headless click then refused it as offscreen (L41-5): the snapshot projection never emitted hidden or offscreen, while the live projection the gate reads derived both. The snapshot now applies the same visibility functions to the facts its walk already holds (states, extents, frame, scroll container, minimized frame) with no extra reads; an input it lacks stays unknown and adds no token. The sentinel and placed-rectangle tests, copied in four places, now live in one module, readiness recognises both web document roles (L3-13), and the GTK4 popup origin is chosen by one rule for snapshot and live reads. * fix(linux): rebuild the desktop alias cache after an edit inside one timestamp tick The in-place desktop edit test failed 2 of 5 runs on unchanged code: the cache compared inode, length and ctime, and an edit of the same length inside one coarse kernel tick keeps the same ctime, so a stale alias index was served. Like git's racily clean rule, a cached index is now trusted only when every stamp is older than its build time minus a tick margin; otherwise it is rebuilt on the next call. * test(linux): add widget-zoo fixtures and the ref-contract e2e legs The bar for a snapshot is that every ref it gives out lands when its action is performed headless, verified by an independent read, or carries hidden, offscreen or disabled and is refused with no effect. Three fixture windows (GTK3, GTK4, Qt) expose the standard widgets, each logging its own effect, and the ref_contract legs drive every ref of a fresh headless snapshot against the log and raw AT-SPI. The window legs open the fixture's dialog, file chooser, message box and top-level button and require each to be listed, reachable as a sheet when modal, and operable to completion. The harness's own window oracle counts Component root children, the same rule as the product (L41-1). * docs(probes): record top-level, popup, visibility and launch measurements Area L41 records the toolkit census that disproves the role-based window rule of L8-2, the popup shapes that are and are not root children, the closed-dialog husks, the libatspi dialog role numbers, the snapshot and gate disagreement on Mousepad's hidden toolbar, and the Thunar daemon hand-off. The environment table names the XFCE container the new rows were measured in. * refactor(linux): state the unplaced-node test without a double negation * fix(linux): recheck racy desktop files by content instead of rebuilding the alias index Rebuilding whenever a desktop file was newer than the build tick made every lookup right after an edit rebuild, so the cache was never reused (the in-place edit test failed 5 of 5). As in git's racily clean rule, only the files that changed within the tick keep a content hash; equal stamps reuse the index unless one of those files now reads different. The test passes 10 of 10. * fix: avoid unsafe at-spi property and interface reads * test: verify linux refs against addressed raw controls * fix: include display bounds in snapshot visibility * docs: explain thunar file views and literal newlines * docs: keep file-view guidance application independent * fix: observe and resolve showing alert surfaces * fix: restore checks after the Linux cleanup merge * fix: serialize trace emission and repair review gates * refactor: align native security types with module boundaries * fix: select option rows through their advertised semantic verb * fix: avoid sibling scrollbar mutations during reveal * fix: retain unanswered scoped surface reads * fix: activate exported menus outside the accessibility dispatcher * test: select the active chooser filename field * docs: record semantic reliability evidence and headless policy * test: repair independent Linux workflow oracles * fix: preserve menu fallback and stop expired launches * fix: await queued windows and reclaim stale clipboard records * fix: preserve clipboard scan timeout codes * fix: verify combo commits and bound optional snapshot reads * fix: complete linux ref delivery and safe refusal contracts (#307) * fix: complete linux ref delivery and safe refusal contracts * fix: complete Wayland menu and chooser ref contracts * test: report unsupported action setup as unexercised * test: separate concurrent overlay fixture sockets * test: enforce snapshot promises in linux ref contracts (#308) * test: align harness checks with retained native refs * fix: prevent private desktop data from leaking through diagnostics * fix(ci): reflect successful writes in the JEV fixture | 15 days ago |
Test Fixtures
Hand-written golden files that tests compare serialized output against. None is captured from a real app.
| Fixture | Asserted by |
|---|---|
skeleton-tree.json |
test_skeleton_fixture_matches_golden in crates/core/src/snapshot/snapshot_tests.rs |
drilldown-refmap.json |
test_drilldown_refmap_matches_golden_fixture in crates/core/src/snapshot/snapshot_ref_merge_tests.rs |
trace_show/trace/*.jsonl |
show_merges_fixture_segments_with_expected_shape in crates/core/src/commands/trace_tests.rs |
The JSON fixtures are loaded at compile time, so a missing or renamed file breaks the build:
let golden = include_str!("../../../../tests/fixtures/skeleton-tree.json");
Any change to the shape a fixture pins must update the fixture in the same commit.