Commit Graph

50 Commits

Author SHA1 Message Date
Michał Pierzchała e832325e87 refactor(substrate): split host mechanics into @agent-device/host-kit capability ports (#2088)
* refactor: split generic host mechanics into @agent-device/host-kit (#2082 W1)

The shared src/utils closure that blocked the platform-family moves lands
on declared owners: generic host mechanics form a new private
@agent-device/host-kit package between kernel and capture-kit, and
capture-kit keeps capture, snapshot, and recording behavior, depending on
host-kit for the mechanics it needs. tar-stream and yauzl move with the
archive code.

Every seam's exported subpaths are pinned in package-boundaries.test.ts,
the layering model ranks the new zone, R13's allow-list names it, and each
seam carries an exact eager-closure row. ADR-0019's substrate amendment
describes the layout.

Tests that mocked two of the moved modules separately became duplicate
same-seam vi.mock factories, where the second silently replaced the first;
those are merged, and the mocks that production code reaches past are
pinned at their injection points instead.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH

* refactor(host-kit): one narrow capability port per export

The four technical barrels (exec/fs/values/request) grouped by category
rather than by capability, so a consumer needing one mechanic evaluated
unrelated ones. Each export is now a single capability over the host
machine: command, process, diagnostics, retry, archive, file, request,
version. A port re-exports only what a consumer of that capability uses,
and every port carries its own eager-closure row.

Most of the old values barrel was never host mechanics. Pure record
readers, config-source values, result text, memoization, async scoping,
coordinate validation, and device-scope parsing touch no process, file, or
environment, so they join kernel's other primitives instead.

Closures fall accordingly: capture-kit's png-worker-client from 20 to 10,
png-resize from 28 to 18, session-teardown from 79 to 68, and the CLI from
386 to 380.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH

* chore: drop the migration inventories and trim the touched comments

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH

* docs: trim the touched host-kit and mutation-lane comments

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH

* docs: keep tool directives only in the touched files

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH

* docs: keep tool directives only across the touched tree

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH

* fix: point the Swift parity comment at the real TS twin and test

The W1 move rewrote this citation to packages/contracts/src/mobile-snapshot-semantics.ts,
which does not exist: the module went to capture-kit while isTapPointInsideViewport itself
went to packages/contracts/src/snapshot-visibility.ts. The TS test line was left pointing at
the pre-move path. Both now resolve.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH

* fix: repoint comment citations at the homes this refactor moved them to

The W1 move left ~20 comment citations pointing at src/utils/*.ts and
src/request/*.ts paths that no longer exist. Each now names the capability
port that owns the symbol, which survives further file moves:

  exec -> host-kit/command          host-process, owner-identity -> host-kit/process
  diagnostics -> host-kit/diagnostics   atomic-file, process-lock -> host-kit/file
  retry -> host-kit/retry           request progress/cancel -> host-kit/request
  version -> host-kit/version       ttl-memo, source-value, parsing, device-isolation,
                                    keyed-lock, success-text -> kernel subpaths

Comment-only; no closure, budget, or behavior change. ADR citations are left
as written, being dated records of the decision rather than live references.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-28 07:46:48 +02:00
Michał Pierzchała a904ef0d5d fix(fuzz): run parser cases in a worker process, not the runner's thread (#2053) (#2055)
The unit-lane corpus replay executed adversarial parser cases on worker
threads of the Vitest worker running the test file. A fault in a worker
thread ends its whole process, so a case that faulted killed the test
runner: `[vitest-pool]: Worker forks emitted error / Worker exited
unexpectedly`, with no test, file, or case named. Six of six Coverage
deaths before #1994's split were this one file out of ~1100, and the
uninstrumented second leg it created then lost the same file six more
times in three days.

Cases now run in a worker *process*. The two faults a case cannot report
about itself are both classified from outside it: a case that never
returns is a `hang` (unchanged), and one that ends the process it runs in
is a new `crash` failure carrying the exit code or signal and the tail of
the worker's stderr — the death certificate the lane used to lose. A
sixth self-check target seeds that kind, so a regression in reporting it
fails the harness self-check like every other kind.
2026-08-26 20:40:57 +02:00
Michał Pierzchała 80a3fdc79c perf: raise local vitest worker cap to four (#2049) 2026-08-26 13:25:32 +02:00
Michał Pierzchała dbc4f2f955 chore(test): start the subprocess-stub kill-criterion experiment (#1823) (#2007)
Deletes the serialized `subprocess-stub` Vitest project and drops
SUBPROCESS_STUB_TESTS from unit-core's exclude, so its two real
spawners (client-metro.test.ts, harness.test.ts — corpus-replay.test.ts
already left for fuzz-worker in #1994) run un-serialized in the default
forks pool per #1823's own kill criterion. Revert if a timeout-shaped
failure shows up before 20 consecutive CI runs pass clean.

The files stay excluded from the mutation lane (SERIALIZED_TESTS):
that exclusion is about mutant-rerun cost, independent of Vitest
project structure. Updated the comments/docs/scripts that described
the old project by name so none of them assert a project that no
longer exists.


Claude-Session: https://claude.ai/code/session_015YPgKE1xmjdqh7T1q987DA

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-24 20:07:47 +02:00
Michał Pierzchała d713988c5a docs(agents): simplify testing and pull-request guidance wording (#1997)
* refactor(lint): replace the facade import scan with a lint rule

The surviving half of `contracts-entry-closure.test.ts` walked ~490 candidate
files and parsed each one to assert that nothing value-imports the two wide
contracts facades. `eslint/no-restricted-imports` already states exactly that,
and `allowTypeImports` already draws the one distinction that made the walker
seem necessary: `import type` is erased, so it stays legal.

Verified rather than assumed, because the override semantics are not additive:
a same-rule override REPLACES the parent, so a top-level rule would have been
silently dropped for `src/**`, and the existing `"off"` entry for `exec.ts` and
the test tree would have exempted the files that carried most of the cost
#1959 removed. So the paths are added per zone, and the blanket `"off"` becomes
a facade-only config that keeps the `node:child_process` exemption it existed
for.

Planted red in all three zones — `src/core/capabilities.ts`, a `src/__tests__`
file, and `packages/capture-kit/src` — each flagged, while a type-only import in
the same probe file was not. A first probe read as a pass because the sed that
built it produced a type-only import; the zone was re-probed with a real value
import rather than trusting the green.

Misconfiguration fails loudly, which is why this is safe to rely on: a typo'd
rule name makes oxlint exit 1 with "Rule not found in plugin", not pass silently
(the failure mode #1976 records for the `rg` assertions).

What a linter cannot replace, and stays: the eager-closure budgets. Those are a
transitive-weight property — a module already imported grows an import, and the
cost arrives without any single file's import list changing. Per-file rules
cannot see that, and `no-restricted-imports` can only ban specifiers named in
advance, which is precisely what #1950/#1956/#1959 could not have named.

* docs(agents): simplify testing and pull-request guidance wording

testing.md sat 15 bytes under the 10k per-doc check:agent-guidance cap.
Rewrite both docs in shorter, plainer sentences without dropping any
fact, threshold, or identifier (backtick-identifier sets verified
unchanged against the previous revision). Also fix testing.md's gate
catalog sentence being separated from its code block and the missing
blank line before pull-requests.md's Reviewing section.
2026-08-24 16:40:53 +02:00
Michał Pierzchała c4b1a6131e dx(test): opt-in worker-count override for solo local vitest runs (#1964)
* dx(test): opt-in worker-count override for solo local vitest runs

resolveVitestMaxWorkers() caps local runs at 2 workers so parallel
worktrees and spawn-heavy tests keep headroom, but a solo run that owns
the machine pays 6x on a 12-core host for no benefit.

Add AGENT_DEVICE_VITEST_MAX_WORKERS to opt in to a higher cap. It is
clamped to os.cpus().length so a runaway value can't oversubscribe the
host, and it is a no-op in CI (CI already derives its own worker count).
A missing, blank, non-numeric, non-integer, or non-positive value falls
through to the existing default cap rather than throwing. Default
(unset) behavior is unchanged.

Closes #1962

* docs: tighten the worker-override note to fit the agent-guidance budget

docs/agents/testing.md sits at a 10,000-byte per-file ceiling enforced by
check:agent-guidance, and the first phrasing pushed it to 10,065. Restate
the override in one tighter bullet that leads with the "solo run only"
caveat, which is the constraint a reader most needs.

* fix(test): clamp the worker override with os.availableParallelism()

Node documents cpus().length as unfit for sizing application parallelism:
it ignores CPU affinity and cgroup limits, so it can report a pool wider
than the process may actually use. Clamping against it would inflate the
very ceiling this override's safety clamp exists to enforce.

availableParallelism() honors those constraints, so the clamp now means
what it claims on constrained hosts. Test updated to match.

* test: keep the resolver cases in the already-included setup test

Review feedback: a new test file beside the resolver, plus its entry in
vitest.config.ts's unit-core include list, is a change to test discovery
that the mutation lane's `vitest related` graph reads. Fold the override
cases into src/__tests__/hermetic-env-setup.test.ts, which is already in
the unit suite and already imports the resolver, and drop the config edit
entirely so this PR no longer touches test discovery at all.

Same six assertions, no coverage lost.
2026-08-24 10:26:25 +02:00
Michał Pierzchała cb65d6ca1f refactor(tests): replace the test-utils barrel with direct module imports (#1956)
* refactor(tests): replace the test-utils barrel with direct module imports

The barrel re-exported 13 modules, so every importer evaluated all of them
(store-factory alone drags 16 daemon session-store files; property-arbitraries
drags fast-check). Importing the backing modules directly cuts the unit
suite's aggregate eager module evaluations from 153,401 to 144,344 (-5.9%),
measured with the eager-import-closure walker. Deleting the barrel makes the
tax unrepresentable instead of pinning it with a guard test.

* docs(testing): point fixture guidance at the test-utils modules, not the deleted barrel

* test: extract replay session fixture
2026-08-22 12:21:35 +02:00
Michał Pierzchała aed00aef42 docs: capture gesture verification lessons (#1913) 2026-08-20 19:49:55 +02:00
Michał Pierzchała 40e4b0dd3e docs(agents): restore and enforce progressive disclosure (#1888)
* docs(agents): restore and enforce progressive disclosure

* test(maestro): pin typed selector fallback signal

* docs(agents): address progressive disclosure review

* docs(agents): restore orphaned traps and close guidance-gate bypasses

- AGENTS.md: skills carry a minimal start/routing card; command semantics
  stay in versioned CLI help (the skills contract enumerates two skills by
  hand, so prose retains ownership for the rest)
- testing.md: restore the two local-only XCTest snags CI never hits
  (unsigned-bundle policy refusal signature + first-run automation permission)
- scripts/gate/routing.ts: record GitHub's 300-changed-file path-filter limit
  at the paths-ignore assertion it bounds
- agent-guidance-contract.test.ts: recurse docs/agents so nested guidance
  cannot evade the byte budgets while the gate stays green
2026-08-20 16:58:26 +02:00
Michał Pierzchała dd2a18ed4d perf(check-affected): stop running coverage locally, CI stays authoritative (#1908)
* perf(check-affected): stop running coverage locally, CI stays authoritative

The `coverage` gate re-ran the affected Vitest suite under instrumentation
on every `check:affected --run`, adding real overhead for signal the
dedicated `Coverage` CI job already enforces on every PR. Mark it
GitHub-authoritative and let `vitest-related`/`unit`/`provider-integration`
run locally on their own instead of being folded into a coverage pass.

Also removes the now-dead dedupe machinery in run.ts that existed only to
support the local coverage-instrumented run.

* fix: keep affected tests fast and bounded
2026-08-20 16:44:48 +02:00
Michał Pierzchała 80b4769230 test(fuzz): structured CLI/Maestro generators that reach command validation and assert error codes (#1781 B2) (#1866)
* test(fuzz): structured CLI/Maestro generators that reach command validation and assert error codes (#1781 B2)

* test(fuzz): pin the rediscovered #1433 excess-positional case and keep numeric flag samples inside their range

* style: apply oxfmt to the new fuzz modules

* perf(fuzz): derive the CLI validation surface lazily so unrelated harness paths keep their startup

* test(fuzz): resolve validation generators in the run path so corpus replay keeps its small module graph

* test(fuzz): weight the CLI budget toward command validation, pin the finite classes as seeds, guard lazy surface derivation

* docs(testing): describe the validation lane's layer split, seed-pinned classes, and PR-time gates

* refactor(fuzz): split the validation generator into CLI and Maestro modules, mirrored in tests

* refactor(fuzz): collapse the flag-shaped mutation classes and seed literals, derive class coverage from declarations

* fix(fuzz): hash every case-generation module in configHash, guarded by an import-closure test

* test(fuzz): assert CLI command and flag-key coverage against the registry, and close the six gaps it found
2026-08-20 08:00:08 +02:00
Michał Pierzchała 393eb30a28 ci: give check:affected real Apple ownership rules and route ios.yml on them (#1781 A9-2) (#1857)
* ci: give check:affected real Apple ownership rules and route ios.yml on them (#1781 A9-2)

Device-lane ownership by platform family in the affected selector
(scripts/check-affected/device-lanes.ts): a TypeScript-only Apple change now
carries replay-ios/replay-ios-device/replay-macos in a narrow plan, other
families own only their own lanes, shared runtime surface owns every lane,
unit tests own none. Golden tables (contracts/fixtures) own the parity unit
test and both runner builds instead of failing open.

ios.yml pull_request paths-ignore is routed on that ownership; the gate
manifest asserts the list against the selector over every tracked path both
ways (scripts/gate/routing.ts, ROUTED_LANES). push to main is unfiltered.
Path coverage exempts declared manual-only checks the way owned does.

* ci: tighten routing assertion shape (fallow: unused exports, complexity)

* ci: name parked checks in check:affected --run skips

* ci: bound the routed-lane exemption to sibling workflows (review of #1857)

The exact-name .github exemption was unbounded: naming the lane's own
setup-apple-runner-build or boot-ios-test-simulator action skipped the lane
that runs them and the manifest stayed green. Lane now carries the transitive
composite-action closure plus its own workflow file (Lane.uses, same walk
declaredGates does), and the exemption refuses anything in it.

Also: an unowned path under an ignored root (a non-TS fixture under a family
root) asked for the ignore entry to be removed, which would un-route every
sibling in that tree; it now asks for a selector owner. Both cases pinned,
both proven red against the pre-fix code. Documents GitHub's 300-changed-file
path-filter limit in docs/agents/testing.md.

* ci: close the routed-lane exemption over composite-action support files

Lane.uses recorded only each composite action's action.yml, so a support file
the descriptor executes was exemptible as if it were an unrelated sibling
workflow: ios.yml uses setup-fixture-app, whose action.yml runs
"$GITHUB_ACTION_PATH/fetch-artifact.sh", and that script runs its siblings
resolve-artifact-name.sh and trusted-artifact.mjs — references that exist only
inside shell, one level past anything YAML parsing sees.

The closure unit is the action's directory now. It needs no shell model and
cannot miss a file however deep the reference chain runs; the coarseness is
harmless because a file in an action's own directory belongs to that action.
All three files pinned, red against the descriptor-only closure.
2026-08-19 17:35:23 +02:00
Michał Pierzchała ef2094c9d7 docs: keep size review in CI and local feedback fast (#1842)
* feat(size): measure a base ref in one command (pnpm size --base <ref>)

The Size workflow already compares base and PR builds; locally that needed a
manual checkout, install, build, --json, and --compare dance, so budgets were
negotiated late. --base <ref> does the workflow's recipe in a detached worktree
under .tmp/size-base/<sha> (kept for reuse, other bases pruned) and compares
against it: first run ~1-2 min, later runs against the same base ~3s.

Documents the local caveat: npm tarball/unpacked rows compare a fresh base
against a working tree that may carry locally built helper artifacts.

* feat(tooling): pnpm pr:evidence — one paste-ready, SHA-stamped evidence block for PR bodies

Composes what the repo already measures instead of hand-transcribing it after
every rebase: exact merge-base and head, changed-file areas, the affected
selector's plan (local vs GitHub-authoritative, fail-open summarized), the
layering guard verdict, depgraph counts with a real delta against the base (a
throwaway git worktree, no install — the script analyzes its cwd while its
imports resolve from this checkout), and, behind flags, the changed-line
coverage table and pnpm size --base. It claims nothing about CI: the last line
links the head's checks. ~20s default tier.

The pure model (grouping, report parsing, rendering) has node:test coverage
registered as the pr-evidence-model gate, run in the Affected-check Selector
job next to the selector it reads.

* fix(tooling): pr:evidence measures pristine head/base worktrees from an os.tmpdir scratch; size --base gets a per-SHA lock, completion stamp, and non-destructive eviction

Review (three P1s):
- pr:evidence created its scratch under an untracked .tmp/ that a fresh
  checkout lacks (ENOENT). Scratch now lives under os.tmpdir(), which exists
  by construction; a real entrypoint regression runs the whole pipeline with
  --base HEAD (no origin/main needed) and asserts JSON shape plus cleanup of
  both worktrees and the scratch.
- Untracked or uncommitted production files could move the layering/depgraph
  numbers the block labels as HEAD's. Head is now measured from a pristine
  worktree of the head commit exactly like base, and the affected plan takes
  the head SHA (the literal HEAD folds the working tree in). The dirty flag
  now counts untracked files and says they are not in the block.
- size --base force-pruned other cached bases without locking and trusted a
  dist/src that could be half-built. Per-SHA .lock (pid, O_EXCL) held from
  before the worktree exists until the base report is read; a live lock on
  the same base fails fast, a stale one is replaced; eviction skips worktrees
  whose lock owner is alive; dist/.size-base-complete marks a finished build.
  Orchestration tests run the real script against a throwaway git repo with
  pnpm/npm shimmed on PATH (build once, reuse, live lock, stale lock,
  interrupted build, guarded vs idle eviction). Also fixes the /tmp →
  /private/tmp realpath mismatch those tests surfaced (git lists worktrees by
  real path, so the registration check removed a live worktree).

* fix(tooling): symlink-identity locks with compare-then-unlink; evict under the victim's lock; pr:evidence registers worktrees on add and cleans up exhaustively

Review (three P1s):
- Lock creation/takeover races: the lock is now a symlink whose target is
  the owner identity (pid:nonce), created with its identity in one syscall
  (no empty-file window), taken over only by compare-then-unlink on the exact
  identity judged stale, and verified after creation; release unlinks only a
  link that still names this run. Real overlapping-process tests: two runs on
  one base (exactly one builds, the other fails fast), and a takeover race
  against a simulated other taker across delays straddling the acquire window
  (a live lock is never unlinked, both never proceed).
- Cross-base eviction: a victim is removed only while holding its own lock,
  acquired through the same path, so a run wanting it after the check finds
  it locked rather than half-removed; a live-locked victim is skipped.
- pr:evidence worktrees: withWorktrees registers each worktree the moment its
  add succeeds and sweeps every resource on the way out, collecting failures
  instead of stopping at the first; planted reds for both (second add fails →
  first removed; removal of the middle one throws → the others still go).

* test(size): serialize the size --base orchestration file with the other real spawners

Caught running the full unit suite on the rebased branch: the file passed in
isolation but intermittently failed under broad file parallelism, where it took
14s versus ~5.5s alone. It spawns node scripts/size-report.mjs per case, which
spawns git and the shimmed package managers under it — the SUBPROCESS_STUB_TESTS
class exactly (starved spawns surface as a vitest test timeout instead of the
orchestration assertion the case is about), so it joins that serialized project
with its spawn named at the entry, per docs/agents/testing.md. No rerun layer is
involved: the flake is removed, not retried. Two full-suite runs green after.

* refactor(size): extract the base-cache claim protocol and make stale takeover atomic

Review (P1 + architecture):

Stale-claim removal was compare-then-unlink (readlink then unlink; lstat then
rm for a stray file), so another taker could replace the observed entry with
its live claim between the two syscalls and this run would delete the
replacement. Removal now happens only while holding the entry's takeover mutex
— an atomically created directory — and re-verifies the claim inside it. A
replacement can appear only by creating one on a free path (the abandoned
claim occupies it until the unlink) or by another takeover (needs the mutex),
so removal cannot delete a replacement. A mutex leaked by a process killed
inside its sub-millisecond critical section is reclaimed by age, and even a
wrong reclamation is contained: both takers re-verify inside, and the winner
is still decided by the atomic symlink() that follows.

The protocol moves out of size-report.mjs into scripts/size-base-cache.mjs
(AGENTS.md: extract past 500 LOC) — 719 → 536, with the entry lifecycle
(claim → evict others → ensure worktree → build if unstamped → measure →
release) owned by the module behind withPreparedBaseWorktree. Mirrored tests
in scripts/__tests__/size-base-cache.test.ts plant every dangerous
interleaving directly on the filesystem: replacement-after-observation, a
takeover held by another run, age reclamation, release-after-retarget, and a
stray non-symlink. They need no subprocess and run in 9ms, so the raced
single-process case was dropped from the orchestration file, which keeps only
what real processes can show. Planted red: removing the mutex makes the
contended case delete the claim it must not touch.

* ci(size): preserve the reporter's whole module graph, and gate that it stays whole

The Size workflow measures the base commit with the PR's reporter, so it
copies the reporter out of the tree before checking the base out. Extracting
size-base-cache.mjs made the reporter a two-file graph while the step still
copied one file, and the base measurement died with ERR_MODULE_NOT_FOUND —
after every deterministic gate had passed, because nothing local reproduces
that copy.

The step now copies the scripts directory, so a further split cannot leave an
import behind, and size-report-preserved-closure.test.ts holds it to the
reporter's real relative-import closure and to running the preserved copy
rather than the checked-out tree. Planted red: restoring the single-file copy
fails both cases, naming scripts/size-base-cache.mjs. Verified by running the
reporter from a copied directory exactly as the workflow does.

* fix(size): the takeover mutex has one holder for life; split report publishing out of the reporter

Review (P1 + architecture):

Age-based reclamation of the takeover mutex reintroduced the split ownership
the mutex exists to prevent: a holder that is merely slow — paused or
SIGSTOPed past any threshold — could have its mutex force-removed and
replaced, putting two takers inside the supposedly exclusive section, where
either could unlink the claim the other had just created; the unconditional
pathname-based release could also delete the replacement mutex. The mutex is
now a symlink naming its holder, created in one syscall, never reclaimed at
any age, and released only by the run that owns it. A mutex leaked by a
process killed inside a three-syscall critical section wedges one cache entry
with the path to clear in the message, rather than silently deleting another
run's live claim. Planted red: restoring age reclamation displaces a
day-old delayed holder, which the new case pins.

Publishing the report to a PR is a separate question from measuring and
formatting it, so it moves to scripts/size-report-comment.mjs with the marker
and retry policy it owns; its existing regression drives it through the real
script unchanged. scripts/size-report.mjs is 386 LOC — under the 500 tripwire
and below the 512 it had on base.

* test: prove delayed size cache holder is preserved

* docs: keep size review in CI
2026-08-19 14:22:52 +02:00
Michał Pierzchała d07b837621 test: classify the runner XCTests — pure decisions to a macOS host lane, simulator semantics gated os(iOS) (#1781 A7) (#1861)
Every declared AgentDeviceRunnerUITests method now belongs to a lane, and
the #if guard is the classification: AGENT_DEVICE_RUNNER_UNIT_TESTS alone
means a pure runner decision (runs on the macOS host on every PR — ci.yml's
existing compile job now executes the bundle it builds), '&& os(iOS)' means
runner/XCTest semantics (simulator lanes only). check:xctest-selection
evaluates the guards per platform, derives each lane's reach, and fails on
a flagged identifier that is undeclared or uncompiled on that lane, on a
declared test no lane reaches (found the two tvOS-only tests, dark since
birth — widened to os(tvOS) || os(macOS)), and on testCommand reaching any
lane. The host and nightly lanes assert executed == derived reach, so a
missing -D flag or a guard that compiles a file out reads red, not as a
smaller green. One duplicate test deleted (sparse-verdict assertions folded
into its twin).
2026-08-19 13:59:45 +02:00
Michał Pierzchała e4c3b420a4 test: refuse foreign-pid signals from unit-test workers (Coverage fork death, #1824) (#1854)
* ci: w3-1824 experiment — trace fork signals and plant pid sentinels in the Coverage job

Temporary instrumentation for #1824. Every vitest fork logs each real
process.kill it sends to a foreign pid (and every kill/pkill it spawns);
the Coverage job parks sentinel processes on the pids the Apple runner
tests fabricate (4141/4242/4343/4444) and reports which of them survive
the run. Reverted before this PR leaves draft.

* test: refuse foreign-pid signals from unit-test workers

A vitest worker may signal only itself and the processes it spawned.
src/__tests__/hermetic-signal-setup.ts records any other process.kill,
answers it with ESRCH (so best-effort kill paths proceed as if the pid
were dead), and fails the sending test by name in afterEach.

The senders this catches today are the Apple runner tests, which
fabricate runner child pids (4242, 4141, 4343, 4444) and mocked the
liveness reads in host-process.ts but not the signal writes:
killRunnerProcessTree delivered real SIGINT/SIGTERM/SIGKILL to those
pids and their process groups — 146 signals per run of
runner-session.test.ts. On the CI runner the sibling vitest forks live
in that pid band, so the Coverage job periodically lost one fork
mid-file with no test attributed (issue #1824, 6 of the last 40 red CI
runs).

The group-signal write moves behind signalProcessGroupBestEffort in
host-process.ts, next to signalPidsBestEffort, so the runner tests mock
the signal seam in the same place they already mock the liveness reads.

Refs #1824

* Revert "ci: w3-1824 experiment — trace fork signals and plant pid sentinels in the Coverage job"

This reverts commit a8a020099c.

* test: refuse spawned kill/pkill writes too, and share the guard with the mutation lane

Review of #1854 found three gaps in the first pass:

- The guard intercepted process.kill only, so the spawned half of the same
  function family was unguarded: runner-disposal spawns `pkill -P <pid>` and
  `pkill -f 'xcodebuild.*AgentDeviceRunner.env.session-...'`, and
  request-router-open.test.ts fired that pattern kill twice per suite run. On a
  developer machine with a live Apple runner, `pnpm test` could reach it. The
  setup file now refuses kill/pkill/killall spawns with ENOENT — which the
  best-effort callers already tolerate — and records them the same way; that
  test stubs the Apple tool seam.
- vitest.mutation.config.ts hard-coded its own setupFiles list, so the Stryker
  lane ran without the guard. SETUP_FILES is now exported from vitest.config.ts
  and imported there, next to the SUBPROCESS_STUB_TESTS import that already
  crossed the same boundary.
- 'processes it spawned' meant direct children only; a grandchild started
  through a shell wrapper was refused with advice that did not fit. The docs and
  the failure message now say direct children and name the remedy.

Synchronous spawns are no longer remembered as own pids: spawnSync and
execFileSync have already exited when they return, so keeping their pids would
license a signal to whatever inherits them next.

Refs #1824

* test: end signal authority at child exit, and guard the promisified execFile path

Re-review of #1854 found two holes in the guard itself, both the class it
exists to close:

- An async child's pid stayed authorized for the worker's lifetime after it
  was reaped. A pid is a claim on a process-table slot, and the kernel reissues
  that slot once it is free, so 'I spawned this pid once' licensed a signal to
  whatever holds it now. Authority now ends on exit/close. Cleanup that signals
  a child must gate on isProcessAlive, which is what the existing cleanup paths
  already do.
- wrapSpawner copied execFile's original util.promisify.custom onto the
  wrapper, and promisify() resolves through that symbol instead of calling the
  function — so every promisified caller got an unguarded execFile, bypassing
  both the kill-binary refusal and child tracking. That path is now wrapped too.

hermetic-signal-setup.test.ts covers both, plus the allowed cases they could
regress into: a reaped child's pid is refused, a promisified execFile cannot
smuggle a pkill, a promisified child is still tracked, a live foreign pid is
refused, and signal 0 stays free. Reverting either fix reds three of them.

Refs #1824
2026-08-19 13:55:16 +02:00
Michał Pierzchała b12a3e3cb3 test: pin test files over 1,000 lines at their exact length so they can only shrink (#1843)
* test: pin test files over 1,000 lines at their exact length so they can only shrink

AGENTS.md has said for a while that past 1,000 lines is architecture debt and
tests are not exempt; nothing enforced it, and the second-largest test file
gained 55 lines in the PR before this one. This is the slow-test ratchet's
shape for a reader's context instead of wall clock: the 26 test files over the
tripwire are pinned at their exact length (R9-style equality pin, #1781 A6);
growth fails, shrink fails until the pin is lowered in the same PR, a file
that drops under the line leaves the list, and a new file may not cross it.
One directory walk per unit run, ~250ms; the pin list emptying deletes it.

* test(ratchet): hold giant test files to their merge-base length so pin edits cannot admit growth

Review (P1): the equality pin compared measured lengths only against the pin
map in the same checkout, so growing a file and raising its pin, or adding a
new >1,000-line file with a pin, stayed green. The gate is now history-backed:
every test file over the tripwire may be no longer than at the merge-base with
origin/main (renames followed; new files may not cross the line), and no pin
may exceed its file's base length — one git cat-file --batch spawn, parsed by
bytes because the sizes are bytes. Both bypasses planted red against real git
on a pinned file and on a fresh 1,001-line file with a pin added.

* test(ratchet): a pin on a file at or under the tripwire is itself a finding

Review: a new pin for an unchanged sub-tripwire file (900 pinned at 900)
passed equality and history and grew the map. Pins now exist only for files
over the tripwire — any other pin is red with 'remove it' — which also
subsumes the old shrink-under-the-line message. Planted red in-file and
against real git (a 186-line test pinned at 186). The android snapshot test
pin bootstraps 1636→1660: main grew that file in #1846 before this gate
exists, and history agrees (1660 at the merge-base).
2026-08-18 19:40:34 +02:00
Michał Pierzchała 72d421fe36 docs(agents): ADR 0019 unit checklist, owning-seam mock rule, worktree and rebase guidance (#1836)
* docs(agents): ADR 0019 unit checklist, owning-seam mock rule, worktree and rebase guidance

Retro follow-up (item 2). Adds docs/agents/adr-0019-unit.md — the order of
operations for one command unit with the declaration site for each step, the
evidence a unit review must carry, and what 'done' is not — so the pattern
rediscovered during the snapshot unit (#1779) is written down once.

testing.md: mock the seam the code under test consumes (fake inspectFacts /
bindDevice), not the generic dispatchCommand mock; a migrating command moves
its tests off the dispatch mock in the same PR.

AGENTS.md: fresh-worktree preflight (pnpm install + build in the worktree;
layering scan reads tracked files only) and concurrent-agent hygiene (one full
gate per host, verify subagent edits with git -C, one PR per worktree).

pull-requests.md: two readiness claims (published-and-reported vs merge-ready)
and the rebase rule — main has no up-to-date protection; rebase on conflict or
when `check:affected --base <merge-base> --head origin/main` names your surface.

* docs(agents): name the admitted-plan token in the ADR 0019 unit checklist (#1841)

* docs(agents): merge-ready owes live evidence only for changed device-facing paths

* docs(agents): the unit checklist documents the admission API on main; #1841 updates the row when it lands
2026-08-18 18:43:27 +02:00
Michał Pierzchała 6a8beb653e feat(mcp): compact server instructions in both eras + MCP-only help tool (#1839)
* feat(mcp): compact server instructions in both eras + MCP-only help tool (#1833)

MCP-only clients got no workflow guidance: server/discover carried two
sentences, legacy initialize carried nothing, and the CLI guides
(agent-device --help, help <topic>) were unreachable over MCP.

- MCP_SERVER_INSTRUCTIONS: one MCP-phrased workflow card (<2 KB, the
  Claude Code truncation limit) returned by server/discover and legacy
  initialize alike.
- help tool, router-owned (not a command descriptor): no topic -> the CLI
  decision card; topic -> agent-device help <topic|command> text, prefixed
  with the one-line CLI->tool-property mapping; unknown topic -> isError
  listing the topics. listCommandTools() stays descriptor-only for the AI
  SDK; the router composes descriptors + help.
- Move src/cli/parser/cli-help{,-overview}.ts to src/cli-schema/ so
  src/mcp (rank 3) can import the renderers without a layering back-edge
  into src/cli (rank 6).

* fix(mcp): name terminal-only commands in help guides; colocate cli-help tests with their sources

- The MCP guide preamble claimed every `agent-device <command>` line is a
  tool of that name; `help web` tells the reader to run `web setup` /
  `web doctor` and no `web` tool exists. The preamble now lists the exact
  CLI-only set (listCliCommandNames minus listMcpExposedCommandNames) —
  derived, not scanned out of prose where `device`/`web` are ordinary
  words. Regression: help web names `web` as terminal-only, and the listed
  set equals the registry difference.
- cli-help-*.test.ts move from src/cli/parser/__tests__ to src/cli-schema/
  to mirror the moved sources.

* perf(mcp): tighten the guide card, tool description, and preamble

Instructions card 1572 -> 1378 bytes (paid every session), tool
description and preamble trimmed, HELP_TOOL built once as a const.
Bundle delta vs main 3189 -> 2715 bytes; the remainder is the guide text
itself, which the bundle carried in no MCP-phrased form before.
2026-08-18 17:48:36 +02:00
Michał Pierzchała 0fb38f1da2 test: prune abandoned test-run tmp directories at run setup (#1834)
* test: prune abandoned test-run tmp directories at run setup

A run killed before its teardown (tool-timeout SIGKILL, OOM, cancelled job)
left /tmp/agent-device-test-run-<pid>-* behind, and check:tmpdir-leaks — which
runs after test:unit in check:unit — flagged every dead-pid directory it
found. It could not tell this run's leak from a historical one, so one killed
run made every later, otherwise-green gate on the host fail.

Both TMPDIR redirection entry points (the Vitest global setup and the
node --test wrapper) now prune dead-pid run directories before creating their
own, printing one [tmpdir] line when they did; the post-run check keeps its
semantics and can now only ever name the run that just finished. Live owners
(a concurrent run in another worktree) are never touched.

The root/prefix constants move into check-tmpdir-leaks-model.ts, next to the
liveness classification, so the setup can import the prune without a cycle.

* test(tmpdir): a run directory is live while any process still holds it as TMPDIR, not only while its owner runs

Review (P1): owner-pid liveness alone would prune a directory out from under
the orphaned children of a SIGKILLed run — the node --test chain, Vitest forks,
or a daemon a test spawned all keep running with that TMPDIR. The liveness
model now reads every process's TMPDIR (ps -E on macOS, /proc/<pid>/environ
on Linux) and treats a run directory as live while its owner pid is alive OR
any process's TMPDIR points into it; both the prune and the post-run leak
check use it. Regression: a wrapped probe spawns a detached long-lived child,
only the wrapper is SIGKILLed, the next prune preserves the directory; after
every consumer exits, the next prune removes it. Planted red with owner-only
liveness: the orphaned directory is pruned.
2026-08-18 17:48:12 +02:00
Michał Pierzchała 423927fdd8 chore(mutation): shrink to report-only — drop the ratchet, baseline and graduation (#1457, #1781) (#1828)
* chore(mutation): shrink the lane to report-only (#1457, #1781 wave 2)

The mutation harness's two real catches (#1474, #1475) both came from humans
reading the weekly score report. The ratchet half never operated: the baseline
was committed exactly twice (8cce0ef6b, 60400d04b), both times with
`stableRuns: 0, gating: false`, and was never updated after the very fixes it
triggered — the weekly job computed a new baseline and then `git checkout --`d
it, uploading a proposal nobody applied in 3+ weeks. A gate nobody arms is
harness weight; the report is the part that paid.

Deletes ratchet.ts + ratchet.test.ts, mutation-baselines/, and every
baseline/graduation/gating path in run.ts (`--update`, `mutation:baseline`).
run.ts now exits non-zero only on a harness failure, never on a score. The
report renders the per-kernel table (kernel, score, killed, survived, total,
timeouts) plus the surviving mutants a strengthening PR works from.

Kernel scoping stays: stryker.config.json and KERNEL_MODULES are untouched.

* fix(mutation): restore denominator coverage and publish the table before judging the shard set

Review of #1828:
- `report.test.ts` re-asserts that Ignored/CompileError/RuntimeError leave the
  denominator — the one behaviour `ratchet.test.ts` covered and nothing replaced.
  A `tally()` edit that counted tool noise would have deflated every published
  score with a green `mutation:test`.
- `assertShardsCoverModules` now runs after `emit()`, so an incomplete shard set
  still publishes the kernels that completed instead of only an error string.
  This makes the workflow comments' claim about the job summary true rather than
  re-wording them down.

* chore(mutation): trigger the affected lane on exactly the paths that can select mutants

The PR lane returns an empty matrix unless the diff touches the harness, so the
kernel-source and `**/*.test.ts` triggers only bought a 1-4 min no-op job on
~96% of PRs. `on.pull_request.paths` is now exactly `LANE_TOOLING` plus the
workflow file, asserted in both directions by workflow.test.ts against the
exported constant — a missing path would let a harness change merge unproven,
an extra one starts a job that can only answer `[]`.

Also drops the workflow header's contradictory scope paragraph: it claimed the
lane selects on kernel sources and any test reaching one, which has not been
true since the ratchet went.

* fix(mutation): score and publish a short shard set before failing on the count

The expected-count check ran inside readShardedReports, before anything was
summarized, so on the weekly's real `--expect-shards 10` one dead shard threw
away the nine that had reported — the earlier reorder only moved the
zero-mutants check. The merge now returns the shard count, and both verdicts
run after emit() with the same exit code and `score` stage.

Regression uses the weekly argument shape (`--expect-shards 10`, one shard
present) and asserts the reporting kernel's row reaches stdout while the run
still fails.
2026-08-18 17:47:29 +02:00
Michał Pierzchała 4b44c1c53a chore(test): remove the contention retry and shrink the subprocess-stub project (#1781 A4) (#1827)
The enumerated single-retry policy (#1419) has fired zero times since it
landed on 2026-07-29: 0 of 234 sampled Coverage-job lane envelopes
(2026-08-11 to 2026-08-18) have retryCount > 0, and none of 17 recent
failed runs was retried (5 refused "outside the enumerated retry list",
4 refused "unhandled error"). All three trackers its entries pointed at
(#1098, #1414, #1419) are closed. It cost ~1,454 LOC, a per-run secret
marker threaded through a setup file on every Vitest project, and a
standing obligation for every future gate reporter to call the blocker
bus.

Delete the scripts, tests and fixtures, the check:contention-retry
script and gate, the envelope artifact upload, and the runner-timeout
setup file; test:coverage:ci is a plain `vitest run --coverage` again.
lane-envelope.ts stays: the mutation, fuzz and concurrency-torture lanes
build their envelopes from it. run-blocker-bus.ts goes: its only
consumer was the retry's failure sink, and its only publisher already
fails the run by setting process.exitCode.

Keep the subprocess-stub project for the three files that really spawn
(client-metro, fuzz harness, fuzz corpus-replay) and drop the three that
run in 31/212/277ms in CI, which cannot contend for anything. The list
is now a plain array in vitest.config.ts with the reason at each entry.
Membership and the project's kill criterion live in #1823.

Because test:coverage:ci is a bare vitest run, the gate manifest reads
its projects directly, so OPAQUE_RUNNERS no longer needs it and an
unrun Vitest project becomes unrepresentable rather than detected; the
audit test now constructs that state by project-scoping the script.
2026-08-18 15:35:25 +02:00
Michał Pierzchała 142d156338 ci(ios): run the full XCTest suite nightly and check the PR test list (#1781 A7) (#1789)
* ci(ios): run the full XCTest suite nightly and check the PR test list (#1781 A7)

* fix(ci): skip the runner server entry point in the nightly and validate both test flags

* docs(ci): restate the nightly lane cost and timeout honestly

* docs(ci): stop quoting XCTest counts that drift between commits

* ci(ios): tighten the nightly timeout to the measured suite duration
2026-08-18 12:00:43 +02:00
Michał Pierzchała ccf64f6797 ci: move parked device replay suites to a dispatch-only workflow (#1781 A1) (#1794)
* ci: move parked device replay suites to a dispatch-only workflow (#1781 A1)

Both full-tier device jobs have failed every scheduled run since 2026-07-24: the
Android suite inside full-tier scenarios that had never executed end to end, the
iOS suite on varying steps. They move to .github/workflows/replays-manual.yml,
which has no `schedule:`, so the schedule stops emitting a guaranteed failure while
the suites stay runnable on demand.

A job-level `if: github.event_name == 'workflow_dispatch'` would have looked the
same and lied: `workflowLanes()` decides `qualifying` per workflow FILE and never
reads job-level `if:`, so the manifest kept reporting replay-android, replay-ios,
and replay-ios-device as scheduled-lane owners — the silent-owner-loss failure the
manifest exists to catch. A separate file is what the file-level model already
reads correctly.

Those three checks now have no pull_request/schedule owner, so they are declared as
MANUAL_ONLY_OWNERS rather than folded into UNPROVABLE_OWNERS, whose claim ("it runs,
this loader cannot see it") is no longer true for replay-android. check:gate-manifest
drops from 48 to 46 wired checks and names the three on every run. Two tests pin it:
a dispatch-only lane is non-qualifying however many gates it declares, and every
manual-only declaration must name a registered check that no qualifying lane owns, so
a re-scheduled lane cannot keep a stale exemption.

* ci: attest manual-only checks against their dispatch lane (#1781 A1)

Review P1: MANUAL_ONLY_OWNERS was a negative allowlist — it proved each entry named a
registered check no qualifying lane owned, but nothing tied the entry to a lane that can
still run it. Deleting a parked job, or its run-gate step, would have left the manifest
green and still printing the check as manual-only: parked coverage silently becoming
deleted coverage.

Each entry now names its dispatch lane, and a new 'manual-only' audit assertion resolves
that name against the derived model: the lane must exist, must still be dispatch-only, and
must still declare the gate. replay-android carries an explicit `opaque` flag because its
gate sits inside the third-party emulator action's `script:` (#1429), so the job's
existence is the whole attestation the model can make — and the flag says so rather than
letting an unreadable lane look like a declaring one.

Four regressions pin both directions: deleting a declaration reports the check as unowned;
deleting the parked job fails with 'no workflow defines'; re-scheduling the lane fails
until the entry is dropped; and a parked lane that loses its run-gate step fails unless the
entry is opaque.

* ci: make manual-only mean dispatch-only, not merely non-qualifying (#1781 A1)

Review follow-up: the attestation checked `qualifying === false`, which is true of any lane
that is not pull_request/schedule. Swapping `workflow_dispatch` for `push` in
replays-manual.yml would have kept the audit green and the checks printed as manual-only,
while the runs nobody starts by hand quietly started themselves on every push.

The lane model now keeps the trigger names instead of collapsing them into that one bit, and
the manual-only assertion requires `workflow_dispatch` and nothing else. Three planted
regressions cover the gap the review named: a parked lane re-triggered by `push` fails, a
parked lane with no trigger at all fails, and the loader test pins that trigger kinds survive
into the model (a push lane reads `[push]`, the nightly reads `[schedule, workflow_dispatch]`).
2026-08-18 09:59:34 +02:00
Michał Pierzchała 9c22467832 refactor(ci): make gate ownership structural (#1429) (#1753)
* test(ci): prove every registered gate is owned and reachable (#1429)

A check that silently stops running looks exactly like a green build. Two
suites had already stopped: `check:tmpdir-leaks` (with its model tests) and
`test:fixture-cache` are real package scripts that no workflow ran, reachable
only through the `check:unit` aggregate CI never invokes.

`CHECK_CATALOG` becomes the registry of every check and `pnpm gate <id>` the
only way CI runs one, so finding what a lane runs is a scan for `pnpm gate`
rather than an attempt to interpret shell. `pnpm check:gate-manifest` then
asserts against the real workflows that every registered check is run by some
qualifying lane (per unit, not per script name), that every check the real
selector activates for a path is run by a lane that path would start (#1420's
class), and that every Vitest project and suite script belongs to a check.

The wiring that keeps those honest is asserted too: a gate id must name a
registered check, an `if:` must be ruled on in GATE_CONDITIONS so `if: false`
unowns what it guards, an action declared to run a gate is proven to, and a
job whose steps the loader cannot open fails closed.

It deliberately does not try to prove CI runs project code only through
`pnpm gate`. Whether a shell block executes project code is not decidable from
its text, so shell this model does not recognise earns no ownership credit —
the failure direction is a check reported unowned, never one waved through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ

* test(ci): update the two suites that assert on rewired workflow text

`scripts/mutation/workflow.test.ts` and `test/ci/trusted-fixture-artifact.test.mjs`
read the workflow and action files and assert on their command text, so routing
those steps through `pnpm gate <id>` moved what they were matching.

They are the two suites the manifest cannot help with: it proves a gate is still
run, not that a test asserting on how CI spells a command was updated with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ

* fix(ci): credit gates by execution shape, and keep every guard

Three ways the manifest could report a gate as owned when it does not run.

1. Crediting was a substring scan over `run:`, which #1429 explicitly rules
   out — "do not infer reachability from a command name merely appearing in
   workflow text". `false && pnpm gate x`, a gate inside `if false; then … fi`,
   one named in a heredoc, and `echo pnpm gate x` all credited it. There is a
   live instance: conformance-regenerate.yml's "Fail if regeneration changed
   anything" step names `pnpm gate maestro-regenerate` inside an error message
   telling a human to run it, and that credited the gate.

   A gate now counts only as the first command segment of a line, and a body
   carrying shell structure earns nothing. Reachability inside a script is not
   decidable, so this does not try: unrecognised shape means no credit and the
   check reports unowned. `VAR=$(pnpm gate x …)` is read, since the assignment
   form is unambiguous and the gate runs.

2. Job-level `if:` was not modelled at all, though six live jobs carry one, so
   a job that cannot run still credited every gate inside it. Two conditions on
   the mutation lanes are now declared.

3. A caller's `if:` REPLACED the guard on a nested composite-action step
   (`guard[0] ?? step.condition`), so an outer `always()` erased an inner
   `if: false`. Steps carry every guard between the lane and the step.

Also corrects two source comments that still claimed project code run outside
the runner fails the manifest. It does not: such a step earns no credit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ

* ci: add the run-gate action that names a gate structurally

The seam the ownership proof will read instead of shell. A lane says which
gate it runs in `with.gate`, a typed input the manifest reads straight out of
the YAML and validates against CHECK_CATALOG.

Nothing here is wired yet — the ~60 call sites and the model change follow.
Added first so the target of that conversion is reviewable on its own.

`args` cannot select which gate runs; it is appended after the id, so the
worst a wrong value does is fail the gate it already named. There is no
`|| true` and no output capture: the gate's exit code is the step's exit code,
so a gate cannot run without being able to fail its lane.

Part of #1429.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ

* merge: main (#1770) and route its three new steps through the runner

#1770 landed the orphan-check fix on main, wiring `check:tmpdir-leaks`,
`check:tmpdir-leaks:test` and `test:fixture-cache` into Coverage, Layering
Guard and Integration Tests. This branch had wired the same three through
`pnpm gate`, so the merge produced two steps per check rather than a conflict
— each check ran twice.

Kept main's steps, with the placement and reasoning reviewed on #1770, and
changed only their `run:` line to the canonical runner. Dropped this branch's
duplicates. Net effect on CI is unchanged: the same three checks, in the same
three lanes, once each.

Gate manifest green after the merge: 47 checks wired across 33 lanes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ

* fix(ci): address review — suite detection, freerange, glob, vacuous skip-list

Six review findings plus the mutation blocker.

[bug] `registered` was shape-only, so a `test:*` script running
`node src/bin.ts test <dir>` resolved to a `script:` leaf and was invisible.
Four `test:replay:*` scripts were owned only because someone hand-registered
them; `test:replay:android` was neither registered nor reported while the
nightly ran the same six .ad files by inlining them. A `test:*` script is now
a suite by name. `replay-android` is registered, and the nightly runs the
script instead of re-listing its files so the two cannot drift.

  The nightly invokes it inside `reactivecircus/android-emulator-runner`'s
  `script:` input — shell handed to a third-party action this loader does not
  read — so the suite executes but cannot be credited. Recorded in
  UNPROVABLE_OWNERS with that exact reason rather than assumed.

  The fixed detector also found a second orphan the review did not name:
  `test:integration:progress`. That one is a reporter whose `--check` sibling
  is the registered gate, so it is declared in REPORTING_SCRIPTS — a
  declaration that itself fails when inert.

[bug] `freerange` defaulted to localRunnable, so fail-open ran `fr` (a Bun
binary) on the pre-push path. Now false.

[suggestion] The `--run` skip-list asserted `build:android-snapshot-helper`,
a name `android-helpers` no longer uses, so it could not fail. Derived from
the catalog instead.

[suggestion] `matchesGlob` joined `**` splits with `.*`, making the adjacent
slash mandatory — GitHub's `**` matches zero directories, so
`src/**/*.test.ts` did not match `src/a.test.ts`. Pinned against
`packages/*/src/**/*.test.ts`.

[suggestion] Deleted the unwired `run-gate` action. It had no callers, was
absent from GATE_ACTIONS, and its comment described a system that had not
shipped. It returns with the rewiring, not before.

[suggestion] Collapsed the module headers that narrated discarded designs.

Mutation: `daemon entrypoint publishes HTTP metadata and cleans up on
shutdown` is the only test here that spawns a real daemon process. It takes
~1.1s alone but exceeds Vitest's 5s default inside Stryker's dry run, which
aborts the sweep before a single mutant runs. Given 30s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ

* fix(mutation): order sandbox aliases longest-first so subpaths resolve

Every shard of the mutation sweep aborted in Stryker's dry run with:

  Cannot find package '@agent-device/selectors/engine' imported from
    .tmp/stryker/sandbox-*/src/core/selector-pipeline.ts

The alias was generated correctly; it just never won. Vite matches a STRING
alias by prefix and takes the first hit, and `workspaceSpecifierTargets`
emitted the bare `@agent-device/selectors` ahead of the subpath entries. The
bare entry therefore captured `@agent-device/selectors/engine` and rewrote it
to `…/src/index.ts/engine`, which does not exist; Node fell back to real
package resolution, could not find the subpath inside the sandbox, and the dry
run failed before a single mutant ran — so the shard uploaded an empty
envelope instead of a report and the ratchet failed for want of one.

Sorting longest specifier first makes the most specific alias win:

  @agent-device/selectors/engine -> packages/selectors/src/engine.ts
  @agent-device/selectors/ast    -> packages/selectors/src/ast.ts
  @agent-device/selectors        -> packages/selectors/src/index.ts

`/ast` never tripped this because nothing in a related test set imported it;
`selector-pipeline.ts` introduced the first subpath import that mattered
(#1744), so the mutation lane has been unable to run since that landed. Any
PR touching `scripts/mutation/**` — which fails open into the full sweep —
would have hit it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ

* refactor: derive gate ownership from workflow structure

* fix: run gates without optional arguments

* fix: resolve mutation workspace subpaths exactly

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-12 16:02:18 +02:00
Michał Pierzchała f5d9789764 feat: enforce local device claims and reconcile stale owners (#1735)
* feat: enforce local device claims

* fix: address device claim review feedback

* fix: persist canonical daemon claim state directory
2026-08-11 16:18:45 +02:00
Michał Pierzchała 05a1d76f2e test: add daemon RPC wire-surface compatibility gate (#1717)
* test: gate daemon RPC wire compatibility against the last released tag (#1432)

ADR 0006 fixes exactly when DAEMON_RPC_PROTOCOL_VERSION must be bumped, and
nothing checked that it was. The runtime guard (readRemoteDaemonHealth) refuses
a mismatched peer, but only fires when someone remembered the bump — a wire
change that skipped it left both sides advertising protocol 2 while parsing
different payloads, which is the failure ADR 0006 exists to prevent.

Local daemons cannot skew (isReusableDaemonInfo takes over on any package
version mismatch). Cross-machine is skewed by design — proxy, cloud/limrun, a
remote macOS host — and ADR 0006 explicitly rules package version out as the
compatibility gate there, so the one boundary where skew is intended was the
one boundary with no gate.

test/wire-compat/surface.ts declares the wire surface grouped by the ADR bullet
each group serves, quoting it, with an `uncovered` note where a bullet is only
partly digestible (the /health and /rpc literals inside http-server.ts stay
reviewer-owned: a moved route 404s at connect time rather than misparsing).
ledger.json records what each declaration hashes to, at which protocol version.

Two gates, split for the same reason the replay-compat corpus splits:
- unit-core holds the ledger to its source and prints the digest to paste;
- Released-Surface Compatibility reads the ledger at the last RELEASED tag and
  requires the drift since then to carry a bump or a compatibleChanges ack.

From one commit a bumped ledger and an unbumped one are both just an edited
file, so only a released baseline can tell them apart. Acks are keyed by the
digest they cover, so one "added an optional field" cannot launder later
changes. Digests ignore comments and formatting; the manifest's closure is
derived from the AST, so a field typed by an unlisted sibling fails rather than
sitting outside the gate.

CI cost: one added job (checkout + toolchain + two node scripts, ~1 min),
mirroring the existing full-history replay-compat job.

* test: close wire-surface overclaim and make the closure fail closed (#1432)

Addresses both review P1s on #1717.

P1 — the manifest materially overclaimed ADR 0006 coverage. It quoted all four
bullets while digesting only the payload TYPES, so the producer and consumer
seams could break a skewed peer without moving a listed digest. Now listed on
both sides of every boundary: JSON-RPC method sets and the projections that
turn each method's params into a DaemonRequest, createRpcError/sendJson/
writeRpcResponseEnvelope, resolveToken and the auth-hook types, upload
preflight/finalize/308 handlers and the resumable ticket shape, artifact route
and download/inventory framing, REST error mapping, and the client's own
payload builder, lease-method mapping, response parser and error projection.
57 -> 117 declarations.

What stays out is now named rather than implied: createDaemonHttpServer's
dispatch wiring and the /health and /rpc literals inside it. Everything it
dispatches WITH is digested individually, and a moved route 404s at connect
time rather than misparsing — the loud failure, not the silent one.

P1 — imported and re-exported payload shapes escaped the closure.
declarationHomes() scanned only the manifest's own files and the walk
continued silently when a name could not be placed, so a listed type could
gain foo?: ImportedShape from a new module and stay green. Resolution is now
explicit and fails closed: relative imports, workspace specifiers (through the
owning package's own exports map, so a re-pointed export cannot drop a type),
and facade re-export chains. Every referenced name must land on a listed
declaration, a waiver with a written reason, a declared external module, or the
TS/Node global set. Fixed two extractor blind spots the walk exposed: a
declaration's own generic parameters and `as const` were being reported as
references.

Planted-red proofs (wire-mutations.test.ts): 13 cases independently mutate
method naming, response serialization, response parsing, auth projection,
upload ticket shape, 308 framing, artifact framing, REST error mapping, and
progress framing, each asserting the digest moves; 3 probes prove the closure
really reaches across a package boundary, a facade re-export, and a plain
relative import. Mutations apply inside the declaration's own span — a
whole-file replace silently hit a sibling sharing the substring, which is how
the first draft of one case passed vacuously.

The largest waiver pair (InternalRequestOptions, CommandFlags) rests on ADR
0006's own additive rule: they reach the peer inside DaemonRequest's untyped
flags/input bags, and the decision says a new flag needs no bump. Digesting
them would fire the gate on every new CLI flag and train reviewers to
rubber-stamp acks.

* test: list the consumer half of the auxiliary HTTP boundaries (#1432)

Addresses the remaining review P1 on #1717. The manifest claimed both sides of
response/upload/artifact framing while listing nothing from upload-client.ts,
daemon-artifacts.ts, or the health consumer in daemon-client-transport.ts, so
those parsers could narrow without moving a listed digest or protocol 2.

Now listed (117 -> 141 declarations):

- /health consumer: RemoteDaemonHealth, readHealthPayload, readDaemonHttpHealth,
  readRemoteDaemonHealth. This is the sharpest of the three — narrowing the
  reader or the comparison disables the very refusal ADR 0006 exists to
  guarantee, and nothing else in the repo would notice.
- /upload consumer: UploadResponse, UploadPreflightResponse, UploadPreflightResult,
  parseUploadPreflightResult, requestUploadPreflight, uploadDirectArtifact,
  tryDirectUploadWithResume, shouldRetryDirectUpload, finalizeDirectUpload,
  uploadLegacyArtifact, ARTIFACT_HASH_ALGORITHM, isStringRecord, and
  PreparedUploadArtifact — whose sha256/sizeBytes/fileName/artifactType/
  contentType fields ARE the preflight body the daemon parses.
- /artifacts/* consumer: DaemonArtifactEndpoint, buildDaemonArtifactUrl,
  isRemoteDaemon, DownloadRemoteArtifactParams, downloadRemoteArtifact,
  materializeRemoteArtifacts, resolveMaterializedArtifactPath.

Running the closure fail-closed over the new files surfaced three more stops,
each decided rather than skipped: PreparedUploadArtifact listed (it is payload),
UploadProgressSink waived (client-local rendering, never leaves the process),
and src/daemon/types.ts#DaemonArtifact waived as a re-export alias of the listed
kernel type, matching its DaemonRequest/DaemonResponse siblings.

10 more planted-red mutations cover the new seams: health version-read and
mismatch-refusal defeated, RemoteDaemonHealth field dropped, preflight parser
narrowed, preflight/legacy response shapes narrowed, finalize body key renamed,
ticket field renamed, artifact tenant header dropped, artifact URL moved. A
fourth closure probe proves the upload-consumer files are genuinely reached by
the walk rather than merely listed. 22 -> 33 tests.

The README now states the coverage as a producer/consumer table per boundary,
so the claim is checkable at a glance instead of asserted in prose.

* test: list the client half of the resumable 308 contract (#1432)

Addresses the third review P1 on #1717. Listing the daemon's
handleResumableUpload proved it still PRODUCES 308; nothing proved the client
still CONSUMES the released one. src/remote/upload-stream.ts owns that half and
was entirely outside the manifest, so a newer client could stop accepting
`upload-offset`, change how it reads `Range: bytes=0-N`, or emit a different
resumed `Content-Range` without moving one of the 141 listed digests.

Now listed (141 -> 151): UploadStreamResponse, streamFileToHttpRequest,
streamFileToHttpRequestAttempt, buildUploadRequestHeaders, isUploadResumeStatus,
isUploadRedirectStatus, parseUploadResumeOffset, parseNonNegativeIntegerHeader,
firstHeaderValue, MAX_UPLOAD_REDIRECTS.

streamFileToHttpRequestAttempt is listed despite its size, unlike
createDaemonHttpServer which stays in `uncovered`. The distinction is stated at
the declaration: the HTTP server only dispatches to handlers that are each
digested, while the attempt loop IS the resume state machine — it decides
whether a 308 continues the upload and what the next request carries, so its
sequencing alone can break a released daemon while every helper keeps its digest.

6 new planted-red mutations prove the client half moves the ledger: a dropped
`upload-offset` fallback, narrowed Range parsing, a changed resumed
Content-Range, 308 no longer treated as continue, a narrowed UploadStreamResponse,
and dropped header-value coercion. 33 -> 39 tests.

Closure fail-closed surfaced two more stops: UploadStreamProgressOptions waived
(local byte-progress rendering) and URL/URLSearchParams added to the global set.

README now carries a `/upload` resume row in the producer/consumer table, and
names the pattern behind three rounds of review: the coverage sentence kept
getting written ahead of the coverage, so the table and the `uncovered` notes
are the claims to trust — they are checkable against surface.ts, prose is not.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-10 20:52:29 +02:00
Michał Pierzchała c06bed9f77 refactor: extract platform device inventory runtime (#1699)
* refactor: extract platform inventory runtime

* fix: preserve scoped Apple inventory tooling

* fix: preserve Apple tool cancellation

* refactor: tighten platform inventory boundaries
2026-08-10 12:51:59 +02:00
vw2x 4b432fb59b feat: add HarmonyOS support (#1683)
* feat: add HarmonyOS device automation foundation

Add HDC-backed discovery, snapshots, application lifecycle, and core mobile interactions.

Route HarmonyOS through the platform registry and client contracts.

Cover parsing and capability parity with focused tests.

* feat: support HarmonyOS HAP deployment

Install and reinstall signed HAP archives through HDC.

Resolve bundle identities from module metadata and relaunch after package replacement.

Extend deploy routing and capability coverage for HarmonyOS.

* feat: add HarmonyOS single-pointer gestures

Execute pan, fling, and swipe plans through HDC uiInput primitives.

Derive scroll coordinates from the live ArkUI viewport.

Keep unsupported multi-touch gestures explicitly rejected.

* refactor: split session inventory command handling

Separate session, device, capability, and app inventory response paths.

Preserve the public inventory response contract while reducing handler complexity.

* feat: support HarmonyOS keyboard actions

Route HarmonyOS enter, return, and dismiss through HDC key events.

Expose supported keyboard actions through the system command metadata.

Keep keyboard visibility inspection explicitly unsupported.

* fix: reject unsupported HarmonyOS drag gestures

Keep drag unavailable until HDC can preserve source and destination hold semantics.

* feat: add HarmonyOS app log streaming

1. Stream HarmonyOS app logs through PID-scoped hilog sessions.\n2. Record HarmonyOS app identity during bundle-id opens for app-scoped commands.\n3. Cover backend routing and bundle identity resolution.

* feat: report HarmonyOS foreground app state

1. Read the foreground HarmonyOS mission through aa dump.\n2. Expose HarmonyOS appstate with package and ability metadata.\n3. Add parser coverage for foreground and missing-state cases.

* fix: advertise appstate through capabilities

1. Classify appstate in the command descriptor capability matrix.\n2. Surface supported appstate commands in capability inventory.\n3. Cover the advertised Android capability contract.

* feat: sample HarmonyOS process performance

1. Sample HarmonyOS process CPU and resident memory through HDC.\n2. Expose the verified metrics through the shared perf command.\n3. Keep frame and memory snapshot collection explicitly unavailable.

* feat: clear HarmonyOS app state

1. Add HarmonyOS settings clear-app-state through bundle cleanup.\n2. Force stop the app before clearing data and cache.\n3. Reject all unverified HarmonyOS settings explicitly.

* docs: document HarmonyOS support

1. Describe HarmonyOS HDC prerequisites and HAP installation.\n2. Add HarmonyOS to platform discovery and product documentation.\n3. Document verified performance limits for the public HDC surface.

* fix: preserve HarmonyOS deploy session identity

1. Bind a resolved HarmonyOS bundle after install or reinstall.\n2. Keep app-scoped logs and observability available after deployment.\n3. Cover session identity preservation for HarmonyOS reinstall.

* test: lock HarmonyOS capability boundary

1. Add an independent HarmonyOS capability-matrix oracle and exact advertised-command regression test.
2. Document current HDC-backed support and evidence-based unsupported command boundaries.

* refactor: simplify HarmonyOS shared platform boundaries

1. Split device selection and settings dispatch into focused helpers without changing behavior.
2. Keep HarmonyOS serial selection and lock-policy classification covered by regression tests.
3. Remove Fallow complexity findings from the HarmonyOS diff against upstream main.

* fix: bound default HarmonyOS HDC commands

1. Apply a 15 second timeout to ordinary HDC operations.
2. Preserve operation-specific timeout budgets for installation and capture paths.
3. Add regression coverage for default and overridden HDC timeouts.

* feat: add HarmonyOS screen recording

Implement physical-device whole-screen recording through the system recorder and HDC media transfer.

Reject unsupported HarmonyOS recording scopes and export flags.

Cover capability routing, media retrieval, cleanup, and simulator rejection.

* feat: report HarmonyOS HDC readiness

Add an HDC version check to the HarmonyOS doctor flow.

Document HarmonyOS as a supported doctor platform and cover the result.

* refactor: simplify HarmonyOS recording checks

Reduce recording validation and test complexity without changing behavior.

* test: cover HarmonyOS platform contracts

Synchronize public platform expectations across CLI, MCP, replay, and inventory tests.

Mock HarmonyOS inventory probes to preserve concurrent test behavior.

* test: model HarmonyOS recording capability

Require a physical HarmonyOS device in the independent capability parity oracle.

* test: cover HarmonyOS input and lifecycle paths

Exercise HDC input, lifecycle, installation, and relaunch command sequences.

* test: cover HarmonyOS device observability paths

Exercise discovery, screenshot validation, and process performance sampling.

* docs: define HarmonyOS CI hardware policy

Keep HDC hardware validation local and require mocked CI contract tests.

* fix: honor HarmonyOS app inventory filters

* fix: bound HarmonyOS app inventory classification

1. 限制应用元数据分类并发并为默认清单设置整体时限.
2. 将请求取消信号传递给 HarmonyOS 应用清单读取.
3. 补充失败时中止在飞读取且不继续排队的回归测试.

* fix: preserve HarmonyOS inventory failure causes

1. 保留触发应用元数据分类失败的原始错误, 避免被取消同级任务覆盖.
2. 补充总时限中止在飞读取且不启动排队任务的回归测试.
3. 验证后序任务失败时保留默认筛选的恢复提示.
2026-08-09 10:29:20 +02:00
Michał Pierzchała ac52281448 fix(test): deterministic temp-dir cleanup across node --test lanes (#1661)
* fix(test): deterministic temp-dir cleanup across node --test lanes

node --test has no global setup/teardown hook, so unlike Vitest (#1593) every
node --test package.json script (maestro:conformance, mutation:test,
check:affected:test, check:coverage-changed:test, check:layering,
depgraph:test, check:tmpdir-leaks:test, check:contention-retry,
test:fixture-cache, test:smoke(:web), test:integration:node,
test:concurrency-torture) still created scratch directories against the
real, unredirected os.tmpdir(), with cleanup only as reliable as each call
site's own try/finally — which a crash, OOM, or timeout kill bypasses
entirely.

Add scripts/node-test-tmpdir.ts: it wraps the whole `node --test`
invocation as a child process, redirecting TMPDIR to one disposable,
pid-tagged directory (shared root/prefix with the Vitest lane) and removing
it from the process 'exit' event, which fires on normal completion, a
thrown error, or a forwarded SIGINT/SIGTERM alike. Every node --test script
now runs through it. check-tmpdir-leaks.ts already scans by root/prefix, so
it covers both mechanisms with no changes to its detection logic.

Verified: a node --test process that mkdtemp's then gets SIGKILL'd leaves a
directory behind unwrapped; wrapped and SIGTERM'd, TMPDIR is redirected and
the directory is gone with no orphaned processes. All 13 wrapped lanes and
the full Vitest suite (5,591 tests) pass with zero residual
agent-device-test-run-* directories after the run.

Fixes #1595

* test(tmpdir): ratchet every node --test script through the wrapper

The 13 lanes wrapped in package.json were a one-time hand sweep with
nothing enforcing the pattern going forward — a 14th node --test script
added later without scripts/node-test-tmpdir.ts would silently reopen
#1595 for that one lane.

Add a structural check to scripts/node-test-tmpdir.test.ts (now part of
check:tmpdir-leaks:test) that reads package.json and fails if any script
invokes `node ... --test` without routing through the wrapper. Dumb
string matching over the scripts map, no shell parsing, with an explicit
(currently empty) NODE_TEST_WRAPPER_BYPASS_ALLOWLIST for any lane that
must legitimately bypass it. Verified it both passes on the current
package.json and fails when a synthetic unwrapped `node --test` script is
added.

* fix(test): preserve the Swift cache and close the raw node --test bypasses

Review on #1661 found two gaps:

1. The wrapper only overrode TMPDIR, so it discarded and forced a
   recompile of the durable Swift compiler cache every run instead of
   mirroring vitest-tmpdir-global-setup.ts's carve-out for it. Read
   os.tmpdir() before the child's TMPDIR redirect takes effect and set
   AGENT_DEVICE_SWIFT_CACHE_DIR from that (only when unset), same as the
   Vitest lane — the two now share one durable cache instead of each
   discarding and recompiling their own. Added a probe assertion
   (scripts/node-test-tmpdir.test.ts) that fails without the fix and
   passes with it (verified both ways).

2. docs/agents/testing.md documented raw `node --test` commands for the
   iOS smoke files, and the android/ios/conformance-regenerate/nightly
   workflows invoked `node --test` directly outside package.json. Routed
   all of them through scripts/node-test-tmpdir.ts so the documented
   local commands and CI lanes get the same crash/timeout-safe cleanup
   the package.json scripts already have.
2026-08-07 13:26:20 +02:00
Michał Pierzchała 23a3016e9b fix: stop the unit suite from leaking temp directories (#1593)
* fix: stop the unit suite from leaking temp directories

~650 test call sites across the unit suite create scratch directories via
fs.mkdtemp(path.join(os.tmpdir(), ...)) or shared factories (makeSessionStore)
with no cleanup, ever. Over time this accumulated 1.16M+ orphaned directories
in the real system tmpdir, slow enough to make tools that enumerate $TMPDIR at
startup (e.g. opencode) take 1-2 minutes to launch.

Rather than migrate every call site, redirect os.tmpdir() itself for the
lifetime of the whole `vitest run` invocation: scripts/vitest-tmpdir-global-setup.ts
wires in as vitest's globalSetup/globalTeardown, points TMPDIR at one
/tmp-rooted directory (verified: env mutations here propagate to every forked
worker, confirmed empirically), and removes it in one recursive rm after every
worker across every project finishes. Since os.tmpdir() reads TMPDIR on every
call, this covers all ~650 call sites without touching any of them.

Rooted at /tmp rather than nested inside the current (already deep, on macOS)
os.tmpdir(): that broke real AF_UNIX socket tests (runner-usbmux.test.ts) by
pushing socket paths past the 104-byte sun_path limit. A per-file afterAll
hook was tried first but proved unreliable — 5 of 7 workers in one run never
ran it before their process was torn down; the global setup/teardown pair
(one process, confirmed single execution) is the mechanism that's actually
guaranteed to run once.

Also adds:
- scripts/check-tmpdir-leaks.ts: CI/local guard asserting no
  agent-device-test-run-* directory survives a run (a leftover one means a
  worker was killed before cleanup could run).
- src/__tests__/test-utils/tmp-dir.ts: documented mkdtempForTest /
  mkdtempForTestSync helpers, the discoverable way to get a scratch dir going
  forward (mirrors the src/utils/exec.ts pattern for node:child_process).
- scripts/check-test-tmpdir-helper.ts: ratchet guard capping raw
  fs.mkdtemp/mkdtempSync call sites in test files at today's count (632); it
  can only shrink as call sites migrate to the helper.

* fix: make check-tmpdir-leaks scan the same root the fix actually uses

check-tmpdir-leaks.ts was scanning os.tmpdir() for leftover run
directories, but vitest-tmpdir-global-setup.ts creates them under a
hard-coded /tmp. On macOS those are different paths (TMPDIR is a deep
per-user /var/folders/.../T/ directory) — the guard could never find a
leak on the exact platform the original leak happened on, only on
Linux CI where os.tmpdir() already is /tmp.

Export TEST_RUN_TMP_ROOT and TEST_RUN_TMP_PREFIX from the global-setup
module and import them in the leak check instead of recomputing a path
that can drift. Switched from a fixed pid-based directory name to
fs.mkdtempSync so a same-named leftover from a prior killed run (or,
on a shared machine, another user) can't collide with a live run.

Also fixes two stale comments (in this file and ci.yml) that still
described the per-file afterAll hook design that was abandoned in
favor of the global setup/teardown pair, and notes the check only
covers vitest runs, not the node --test lanes (test:smoke,
test:integration:node).

Verified live: with the old code the guard reported no leaks even with
a real orphaned /tmp/agent-device-test-run-* directory present (left
by a command that got killed mid-run); with this fix it correctly
found and reported it.

* simplify: drop the tmpdir ratchet guard, keep the leak check local-only

Two guards were more than this needed:

- check:test-tmpdir-helper (ratchet on raw fs.mkdtemp call counts)
  protects nothing a bug could actually trigger — the leak is already
  fixed architecturally regardless of call-site count, so this was
  pure style/discoverability nudging. Dropped the script and its
  check:tooling/CI wiring; kept mkdtempForTest/mkdtempForTestSync in
  tmp-dir.ts as the documented option without enforcing it.

- check:tmpdir-leaks in CI added little: GitHub-hosted runners are
  destroyed after each job, so a leftover directory there is harmless
  by construction, and a worker getting killed mid-run would already
  surface as a job failure some other way. Its real value is local,
  on the long-lived dev machines where the original leak actually
  accumulated — kept it wired into check:unit, dropped the CI step.

* fix: don't flag a concurrent vitest run's tmpdir as a leak

check-tmpdir-leaks.ts reported every agent-device-test-run-* directory
as a leak, but a concurrent vitest run in another worktree legitimately
keeps its own directory present until its own teardown finishes. On a
machine that regularly runs several worktrees at once, that made
check:unit fail on unrelated in-progress work.

Embed the owning process's pid in the directory name (still random-
suffixed via mkdtempSync, so same-pid reuse across separate runs can't
collide) and have the leak check skip any directory whose pid is still
alive (process.kill(pid, 0)) — only directories whose owning process
already exited without running its globalTeardown are real leaks.

Split the pure logic into check-tmpdir-leaks-model.ts (findLeakedRunDirectories,
with an injectable liveness check for testing) so it has a real
regression suite, including the concurrent-run case, instead of only
being exercised by hand.

* refactor: migrate raw fs.mkdtemp call sites to mkdtempForTest(Sync)

Migrates 629 raw fs.mkdtemp(Sync)(path.join(os.tmpdir(), PREFIX)) call
sites across 168 test files to the mkdtempForTest / mkdtempForTestSync
helpers (src/__tests__/test-utils/tmp-dir.ts), so there's one
documented, discoverable way to get a scratch dir in a test — cleanup
already didn't depend on the call-site shape (the global TMPDIR
redirect covers any of them), this is purely for consistency and
discoverability, same reasoning as src/utils/exec.ts for
node:child_process. Existing manual per-test cleanup (fs.rm in
finally/afterEach/onTestFinished blocks) is untouched — the global
teardown is a fallback for killed workers, not a replacement for tests
cleaning up after themselves.

Migrated with a one-off AST-based codemod (oxc-parser, since regex
mismatched multi-line calls and complex prefix expressions like
`options?.tempPrefix ?? 'default-'`) rather than by hand across 168
files. The codemod isn't included — it doesn't need to survive this
commit. Caught and fixed one real bug in it during review: a small
number of files declare a second import statement later in the file,
after some of the matched call sites, which broke a naive "insert
after the textually-last ImportDeclaration" placement; fixed to insert
after the top contiguous import block instead, plus a self-check that
re-parses every generated file before writing it.

Also fixes 5 fallow dead-code findings the branch introduced: the
vitest globalSetup functions (setup/teardown) are only referenced by
the config-string path vitest.config.ts hands to globalSetup, invisible
to static analysis — suppressed with the documented convention.
isProcessAlive didn't need to be exported (nothing outside the module
uses it). And dropped a barrel re-export of the new helpers from
test-utils/index.ts: nothing actually imports through the barrel
(matching the existing makeSessionStore convention, which is imported
directly from store-factory.ts everywhere despite also being
barrel-exported), so the re-export was genuinely dead.

Documents the convention in docs/agents/testing.md.

Verified: full unit suite (5308 tests) passes except the one
pre-existing, unrelated package-exports.test.ts failure; typecheck,
lint, and format all clean; fallow audit clean against the PR base.

* fix: correct fallow suppression token and drop unused barrel re-export

These were meant to be part of 397cdd6d7 (verified locally before that
commit) but didn't actually get staged — caught by CI's Fallow Code
Quality check re-running against the pushed commit, which still had
the plural 'unused-exports' token (fallow expects singular
'unused-export') and the dead barrel re-export.

* fix: migrate the two mkdtemp call sites the rebase silently reintroduced

Rebasing onto main pulled in #1594's two new test cases in this file,
added independently of this branch's migration, still using raw
fs.mkdtempSync(path.join(os.tmpdir(), ...)). Git's line-based merge
found no textual conflict with this branch's removal of the os import
(the changes touch non-overlapping regions), so it silently produced
a file that doesn't typecheck. Migrated both to mkdtempForTestSync for
consistency with the rest of the file, caught by CI's Typecheck,
Fallow Code Quality, and FreeRange checks re-running against the
pushed commit.

* test: pin Vitest tmpdir lifecycle

* fix: preserve the Swift cache across test runs
2026-08-05 08:56:39 +02:00
Michał Pierzchała 4f8dc3f31e refactor: move selector engine into workspace package (#1589)
* refactor: move selector engine into workspace package

* refactor(selectors): trim the package façade to its real consumers

Follow-up to the selector-package cutover, from a structural review of it.

- Drop 15 façade symbols with no consumer anywhere in the repo:
  selectorUsesKey (added by the cutover, never called), isNodeVisible /
  isNodeEditable (the real helpers are contracts/snapshot's), normalizeText,
  splitIsSelectorArgs, IS_PREDICATE_REQUIRED_MESSAGE, four nested Replay
  types, SelectorDisambiguationDisclosure, and the four kernel type
  re-exports every consumer already imports from kernel directly.
- Delete SelectorCapturePolicyInput.selectorExpression, which
  deriveSelectorCapturePolicy never read; the policy varies only by
  predicate, so it takes one now. Two of the four tests asserted that the
  unread parameter had no effect and could not fail; they go with it.
- Return the Maestro export vocabulary to the maestro package. The cutover
  inlined MAESTRO_TEXT/STATE_SELECTOR_KEYS' values into the CLI call site,
  leaving both constants dead in the package that owns the concept and no
  gate over the two copies. MAESTRO_SELECTOR_PROJECTION is now the one
  statement of it.
- Dedupe SelectorDiagnostics and SelectorDisambiguationDisclosure, declared
  character-for-character twice across the AST/string seam, and name the two
  shared option shapes once instead of five inline copies. The parser-side
  resolution types take an Ast prefix so the twins read as twins.
- Delete three identity wrappers: parsePrivateSelector,
  selectorExpressionToMaestro, and the formatSelectorFailure forwarder —
  nothing passes it a chain any more, so the SelectorChain | string union
  and its branch go too.
- Delete internal/index.ts, an AST barrel whose only consumer was one test
  in the same directory (renamed to engine.test.ts), and the match.ts
  pass-through that existed to feed it.
- ReplaySelectorGrammar had three variants for two behaviors; 'wait' and
  'ordinary' were the same path. It is 'is' | 'positional' now.
- Drop the deleted src/sdk/selectors.ts from .fallowrc.json's entry list.

Behavior unchanged. pnpm check green: 598 unit files / 5278 tests, smoke
35 passed / 3 live skipped, layering 71/71, depgraph 22/22, mutation config
45/45, fallow clean, package smoke sound. Counterfactual: pointing
MAESTRO_SELECTOR_PROJECTION.textKeys at the state keys turns three
replay-maestro-export cells red; restored before commit.

* test(selectors): split the engine aggregation test by source concept

`internal/index.test.ts` (renamed `engine.test.ts` when its barrel went away)
was a 708-line aggregation over the whole engine — past the 500-line tripwire
and mirroring no source module, so it also ran as one serial unit.

It becomes five files that each mirror what they test, plus the parser cells
folded into the existing parse test:

  resolve.test.ts                 alternative fallback, strict uniqueness,
                                  first-match existence
  resolve-disambiguation.test.ts  ADR 0012 ranking: deepest, smallest-area,
                                  winner-vs-challenger disclosure, tie fallback
  resolve-viewport.test.ts        the visibility half: on-screen beats
                                  off-screen, including inside an off-screen
                                  scroll container
  match.test.ts                   per-key matching semantics (text, role,
                                  focused, appname/windowtitle, decoded
                                  newline labels)
  arguments.test.ts               where the selector ends and the command's
                                  positionals begin, both grammars
  parse.test.ts                   +6 grammar/escape cells beside the existing
                                  property tests

The login-form tree shared by resolve.test.ts and match.test.ts moves to
`__tests__/login-form-nodes.ts` rather than being copied into both.

All 27 cells are carried over unchanged and still pass; no file now exceeds
224 lines. pnpm check green: 602 unit files / 5278 tests, layering 71/71,
depgraph 22/22, mutation config 45/45, fallow clean over 127 changed files.

* revert(selectors): keep agent-device/selectors public, behind one AST subpath

The cutover removed the `agent-device/selectors` public subpath as part of
tightening the API. It is in use, so the removal is reverted: the subpath ships
the same ten symbols v0.20.5 shipped, with the same signatures.

That has to coexist with the reason the package façade is string-only, so the
AST leaves through one named door instead of the main one:

  @agent-device/selectors        string-in/string-out; every in-repo consumer
  @agent-device/selectors/ast    the published parser surface; one consumer,
                                 src/sdk/selectors.ts

`packages/selectors/src/ast.ts` re-exports parseSelectorChain,
tryParseSelectorChain, isSelectorToken, the AST-taking findSelectorChainMatch
and resolveSelectorChain, isNodeVisible, isNodeEditable, and types
SelectorChain / SelectorDiagnostics. `formatSelectorFailure` keeps its
published `SelectorChain | string` first parameter as a shim here rather than
widening internal/resolve.ts back to a union — the compatibility obligation
sits at the boundary that owes it.

This is strictly narrower than main, where the AST was reachable from anywhere
in src/ via src/selectors/*. Two gates hold it there: facade-symbols.ts pins
./ast to exactly the v0.20.5 list, and package-boundaries.test.ts asserts
src/sdk/selectors.ts is the only file outside the package that imports it.

Restored alongside: the ./selectors export and tsdown entry/chunk group, the
.fallowrc.json entry, the package-exports supported-subpath list, and both
client-api.md sections. No CHANGELOG entry — nothing is removed any more.

pnpm check green: 602 unit files / 5278 tests, smoke 35 passed / 3 live
skipped, layering 71/71 (10 packages, 32 subpaths), depgraph 22/22, mutation
config 45/45, fallow clean over 129 changed files, package smoke imported all
12 published entry points with publint and attw passing. Verified functionally
against the built dist: the doc's parse -> findSelectorChainMatch example
returns the same shapes as before, resolveSelectorChain still returns an AST
`selector`, and formatSelectorFailure still accepts a chain.

* fix(selectors): correct the two expectations that still assume the removal

Review P1s on a792415a: restoring the public subpath left two gates asserting
it was gone.

- installed-package-metro.test.ts moved `agent-device/selectors` into the
  blocked-specifier list. It goes back to the subpath smoke set, running the
  same `isSelectorToken('||')` + `parseSelectorChain` check it ran before the
  removal, so the file's only remaining delta from main is a formatter reflow.
- owner-files-no-leak.test.ts asserted `dist/src/sdk-selectors.js` was absent.
  It requires the stable named chunk again, and still rejects an auto-numbered
  `selectors2.js` fallback — the pair is what proves the restored tsdown chunk
  group is doing its job, verified against a clean build.

PR body corrected: the removal is no longer described as intentional API
tightening.

* refactor(selectors): satisfy the widened fallow scope after rebase

main's #1591 (the follow-up filed from this review) removed `packages/**` from
.fallowrc.json's ignorePatterns, so the new package is audited for the first
time. Everything below is a finding fallow could not previously see.

Dead surface, all confirmed consumer-free:

- 12 type re-exports from the `.` façade whose shapes consumers only ever
  reach structurally.
- MAESTRO_TEXT_SELECTOR_KEYS / MAESTRO_STATE_SELECTOR_KEYS, orphaned by this
  branch's own MAESTRO_SELECTOR_PROJECTION change, and the test-util
  SELECTOR_VALUE_HAZARDS. All three are module-local now.
- IS_PREDICATE_USAGE_HINT fails --production because its only consumer is the
  is-argument-surface parity test. It gets a commented `ignoreExports` entry
  rather than deletion: the constant is what makes the daemon and CLI raise
  ONE hint instead of two copied strings (ADR 0010), so the test asserting
  that is the point, not an accident.

`fast-check` is now declared by the package that imports it.

Duplication, split by what could be proven:

- `isUsefulVisibilityAnchor` existed character-for-character in both
  packages/selectors and packages/maestro. Moved to
  @agent-device/contracts/snapshot, which both already depend on and which
  already owns this vocabulary. Safe because the `normalizeType` each copy
  called is itself character-identical to the contracts one — checked before
  moving, since a different normalizer would have silently changed which
  nodes anchor.
- maestro additionally reimplemented `normalizeType`, `buildSnapshotNodeMap`
  (as `buildSnapshotNodeByIndex`) and `findSnapshotAncestor`, all
  character-identical to contracts'. Deleted in favour of the shared ones.
- The three scroll-ancestor walks are NOT deduped. They are structurally the
  same walk but each uses a different scrollable predicate, and I have no
  evidence the three agree; collapsing them would be a Maestro-conformance
  change, not a cleanup. Both maestro sites now say so, and the work is filed
  separately.

`projectSelectorExpression` (15 cyclomatic / 22 cognitive, written by the
cutover) splits into a dispatcher plus `readAgreedTextValue` and
`projectSelectorTerms`; all three are under threshold.

Rebase note: the one conflict, in package-boundaries.test.ts, resolved to
NEITHER side — #1591 had already deleted `AdReplayVerifiedTargetGuard` as an
unused export, and this branch deletes the seven ReplaySelectorPort names, so
the conflicting block is empty.

* build: record fast-check for packages/selectors in the lockfile

Declaring the dependency in packages/selectors/package.json without
regenerating pnpm-lock.yaml made every CI job fail in its install step with
ERR_PNPM_OUTDATED_LOCKFILE. My local `pnpm install --frozen-lockfile` printed
"+ 1 dependencies were added: fast-check@^4.9.0" and exited 0, which read as
success but was the same mismatch CI refuses.

Regenerated with the pinned pnpm 11.17.0, not the 11.5.3 on this machine:
11.5.3 rewrites peer-dependency resolution keys repo-wide (dropping
`(supports-color@7.2.0)` suffixes) and produced a 222-line diff. With the
pinned version the diff is the 4 lines this change actually needs, plus
pnpm's alphabetical re-sort of the root selectors entry.
2026-08-04 19:06:39 +02:00
Michał Pierzchała 2e74b789fd feat: verify device cloud connections (#1564)
* feat: verify device cloud connections

* refactor: unify connect provider adapters

* refactor: separate connect verification facts

* fix: tighten connect provider verification

* fix: use neutral cloud connection wording

* perf: deduplicate local affected checks

* refactor: simplify affected check runner

* refactor: derive connect workflow from verification
2026-08-03 16:47:57 +02:00
Michał Pierzchała 4c2a30cc8f docs(agents): a green check is evidence only once you have seen it red (#1547)
* docs(agents): a green check is evidence only once you have seen it red

Three vacuous regression tests shipped in one day (an edge-run input the
retired regex handled in one pass, invariants the old implementation
already satisfied, an entry point whose trimming defused the flagged
pattern); review's counterfactual checks caught all three. The same proof
discipline already existed piecemeal for moved tests and structural gates —
name it once and point to the mechanical proof shapes.

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(agents): trim the obvious, keep the earned

Dropped three bullets: open-before-close (CLI help and
device-verification.md own it), don't-remove-without-migration (subsumed by
the stronger no-fallback scope rule), and generic Node built-ins advice
(engines owns the version). Strengthened the oxfmt rule with the confirmed
mechanism: a path argument bypasses ignorePatterns, not just hides drift —
one path-scoped run re-quoted 44 excluded conformance corpus files.

Co-Authored-By: Claude <noreply@anthropic.com>

* Revert "docs(agents): trim the obvious, keep the earned"

This reverts commit 0229cbadba.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-01 15:11:26 +02:00
Michał Pierzchała 0ee2a86129 refactor: extract contracts workspace package (#1499)
* refactor: extract contracts workspace package

* fix: preserve screenshot diff result contract

* test: stabilize Android keyboard smoke
2026-07-30 17:07:46 +02:00
Michał Pierzchała 76453add71 refactor: pnpm workspace + @agent-device/kernel pilot (#1490 W0) (#1494)
* refactor: pnpm workspace + @agent-device/kernel pilot (#1490 W0)

Extend the workspace with packages/* and move the kernel behind an
enforced public API: packages/kernel with nine consumer-earned subpath
exports (errors, device, snapshot, contracts, collections, rect,
redaction, daemon-error, bounds — the last absorbed from utils as Rect
vocabulary). Every kernel import repo-wide becomes the
@agent-device/kernel/<sub> specifier; kernel tests move to
src/__tests__/kernel/ and exercise the package surface. The root
declares the package in devDependencies (workspace:*), tsdown bundles
it (noExternal) so the published artifact and its runtime dependency
manifest are unchanged.

Gate rewiring in the same change, per the W0 brief:
- R1 kernel-sink retires (physically subsumed); new R11
  package-boundaries guards no-root-back-imports, relative tunnelling
  past exports maps, undeclared workspace deps, and non-exported
  subpaths, with runtime resolution pins via import.meta.resolve.
- resolveImportEdges and mutation ownership follow workspace
  specifiers through exports maps, keeping R4 cycle checks, depgraph,
  and derived test ownership connected across the seam (kernel-errors
  still owns 495 tests). listSourceFiles includes packages/*/src.
- kernel becomes an unranked zone; mutation registry, stryker mutate
  globs, and the mutation-affected workflow path filter move to
  packages/kernel/src/errors.ts.
- check:affected gains packages/ ownership (manifests fail open);
  vitest and coverage include packages/*/src; fallow ignores
  packages/** (its resolver cannot follow workspace specifiers).
- The affected-selector CI job installs dependencies: its closure now
  crosses workspace specifiers, and the R8 relative exception is
  unsafe for production src files (Node ESM does not realpath, so dual
  specifier/relative loads would instantiate modules twice). The R8
  zero-dep set is pinned empty with that rationale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* fix: address W0 review — mutation sandbox, exports-map resolution, tsc -b

Review findings on #1494, all five:

1. contracts-schema-public.test.ts reads the kernel source at its
   packages/ path (fs access invisible to the codemod and typecheck).
2. Mutation lane: Stryker sandboxes the tree but pnpm's node_modules
   symlink resolves @agent-device/* back to the real repo, so mutants
   in the sandbox never load and vitest.related finds no tests.
   vitest.mutation.config.ts now aliases each EXPORTED specifier to
   its source (derived from exports maps, never a wildcard), keeping
   resolution inside the mutated tree. Validated: kernel-errors module
   runs end to end (dry run 3,984 tests, mutants killed, exit 0).
3. Layering/depgraph resolve workspace specifiers through the
   exports-derived map (workspaceSpecifierTargets) instead of
   reconstructing paths, so '.'-facade packages resolve; the
   positional fallback remains only for map-less fixtures (P0 pin).
4. Per-package project references implemented: packages/kernel is
   composite (emitDeclarationOnly -> dist-types, gitignored), the root
   references it, and typecheck becomes tsc -b — probed to catch type
   errors on both sides under TypeScript 7 native.
5. R11's relative-route exception now requires membership in an actual
   R8 zero-dep job closure (zeroDepClosureFiles walks entries), not
   mere scripts/ placement — closing the dual-instantiation bypass.

Also from review discussion: daemon-error moves out of the kernel
package to src/client/ — its consumers (cli, client facade) rehydrate
wire DaemonErrors client-side; the daemon only produces them. Kernel
drops to 8 exported subpaths before any of them ship.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* refactor: one exports-map reader for mutation alias and ownership

Fallow flagged workspaceExportAliases (cognitive 15, CRAP 90). The
manifest-reading logic already exists as workspaceSpecifierTargets in
scripts/layering/package-boundaries.ts, so both the Stryker sandbox
alias table and the mutation ownership walker now consume it instead
of carrying near-clones. Behavior unchanged; mutation suite 45/45 and
changed-code fallow green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* fix: composite kernel without a root references edge

FreeRange runs plain `tsc -p tsconfig.json`, and a root `references`
entry makes non-build-mode TypeScript demand the referenced project's
built declarations (TS6305) — a standing "build first" tax on every
plain -p consumer (fr, editors). Keep the per-package composite
project and build it in typecheck (`tsc -b packages/kernel` before the
root and examples/sdk passes), but drop the root references edge: root
consumption resolves through exports to source, identical to runtime
and to the bundler. Probed: plain -p green with no prebuilt output;
kernel-side type errors still caught by its own build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* fix: R11 uses the layering parser; mutation config is a fallow entry

Review blockers on #1494:

- R11's private single-quote regex could miss a double-quoted or
  re-export route into packages/*/src. specifierSites now delegates to
  the layering model's parseImports (both quote styles, side-effect
  imports, re-exports, dynamic imports), with direct regressions for
  each formerly-invisible form.
- vitest.mutation.config.ts becomes a declared fallow entry instead of
  a tolerated unused-file finding: the full-repo audit now reports it
  reachable (unused files 2 -> 1; the remainder predates this PR).

FreeRange clean-checkout evidence: with packages/kernel/dist-types and
every *.tsbuildinfo deleted, `pnpm check:freerange` reports 0 findings
on this head — the TS6305 topology died with the root references edge
in the previous commit; check:freerange has no build precondition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-30 12:12:46 +02:00
Michał Pierzchała adcbdda8f0 perf: speed up unit tests and streamline checks (#1488)
* perf: speed up unit tests and streamline checks

* fix: validate canonical packaging workflows
2026-07-29 18:53:10 +02:00
devin-ai-integration[bot] 885c1486bb test(ci): single-retry policy for enumerated contention-flaky files (timeouts only) (#1448)
* test(ci): single-retry policy for enumerated contention-flaky files

* fix: satisfy fallow

* test(ci): read failures through a lane reporter so timeouts stay distinguishable

* test(ci): cover the lane reporter and drop its duplicated boilerplate

* chore(fallow): own the retry lane's tool-loaded export seams

* test(ci): block retries on non-test failures and classify timeouts structurally

* test(ci): decide retry eligibility from runner metadata and route gate verdicts through blockers

* refactor(ci): name the retry policy's rules in code instead of comments

* test(ci): mark runner-aborted timeouts inside the runner instead of inferring them

* test(ci): make timeout provenance a per-run secret, not a writable flag

Cover direct task.meta mutation in the real child-Vitest fixture gate.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(ci): retry the failed files in the first run's project and coverage modes

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: drop deleted repo-health file from the retry list after #1480

Rebase onto main post-#1480: the SkillGym/repo-health descope deleted
scripts/repo-health/run.test.ts, whose CONTENTION_RETRY_FILES entry
would now fail this PR's own missing-file check, and inlined the
slow-test budgets into the reporter, resolving the budgets-module
import. Envelope comments now point at scripts/lib/lane-envelope.ts
instead of the closed #1430.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-29 12:53:33 +02:00
Michał Pierzchała 53e4be5f86 Remove SkillGym suite and repo-health snapshot infrastructure (#1480)
* chore: drop SkillGym and the repo-health aggregator (#1412 descope)

Remove the SkillGym harness (test/skillgym/), its check-affected lane,
package scripts, and devDependency — the help-conformance bench is now
the single non-gating small-model oracle. skills/ markdown classifies
as docs in the affected-check selector instead of failing open.

Remove scripts/repo-health: its only gating assertion duplicated the
Layering Guard job, its case-count metric imported the deleted SkillGym
suite, and its sole planned consumer (#1424 / PR #1477) was closed with
the Track C descope on #1412.

Verified: check-affected node --test suites, oxfmt, oxlint, tsc,
check:layering, fallow audit vs origin/main, and the full unit suite
(unit-core + subprocess-stub) all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* fix(scripts): fold slow-test budgets into the reporter for production-exports

The Fallow production-exports gate flagged all three budget exports:
their in-file consumer (SLOW_TEST_RATCHET) and the repo-health entry
point that kept the module reachable were both removed in the descope,
leaving the config-loaded reporter as the only consumer — invisible to
--production analysis. The data-only module's second consumer is gone,
so per the boundaries-are-earned norm the constants move into the
reporter instead of gaining a suppression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

* docs: align skills/ format policy and purge last SkillGym mention

Address both P2 review findings on #1480: the testing-matrix row and
the selector's formatGate both still claimed oxfmt covers skills/,
while selectChecks classifies skills/*.md docs-only (oxfmt ignores
**/*.md, so the claim was a no-op even before). The matrix now states
the docs-only policy and formatGate drops the dead underSkills fact.
The merged examples/README.md index (from #1469) loses its skillgym
mention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-29 11:45:22 +02:00
Michał Pierzchała 4e4ecdea0d test(ios): expand simulator e2e coverage (#1408)
* test(ios): expand simulator e2e coverage

* test(ios): make coverage checks host portable

* test(ios): handle deep link confirmation

* test(ios): fix deep link prompt selector

* ci: stabilize full simulator nightly

* test(ios): stabilize permission prompt lifecycle

* test(e2e): wait for route-specific landmarks

* test(e2e): reset permissions from inactive app

* test(e2e): redeliver trusted cold deep links

* test(ios): verify orientation native readback

* test(ios): stabilize simulator permission coverage

* test(ios): wait for tab target after deep link

* test(ios): paginate full event timeline

* test(ios): simplify event pagination coverage

* test(ios): stabilize simulator e2e coverage

* test(ios): model simulator recorder lifetime

* ci(test-app): cache fixture dependencies

* fix(ci): isolate test app cache by node

* fix(ios): settle fixture route navigation

* test(ci): waive unbenchmarked ios system UI help

* fix(ios): tolerate delayed simulator scale lookup

* 0.20.1

* test(ci): remove superseded system UI waiver

* fix(ios): harden simulator e2e reliability

* chore: clarify Apple runner CI steps

* fix(ios): wait before fixture home snapshot

* fix(ios): require exact catalog navigation

* chore(ci): format rebased workflows

* fix(ios): retry unobserved fixture navigation

* refactor(test): remove iOS e2e workarounds

* fix(ci): verify fixture artifact provenance

* fix(ci): align fixture artifact fingerprints

* fix(ci): use unified Android helper packager

* perf(ci): scope fixture build concurrency

* chore: format fixture artifact tests

* test(ios): update split Apple coverage owner

* fix(ios): accept deep-link confirmation alerts
2026-07-28 20:08:45 +02:00
devin-ai-integration[bot] e0b8463ef8 feat(scripts): blast-radius query — dependents, owning gates, live-coverage owners (#1425) (#1470)
* feat(scripts): blast-radius query over the depgraph model (#1425)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(scripts): keep --limit bound to its value in depgraph affected

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-28 18:20:39 +02:00
devin-ai-integration[bot] 006c4cadc9 test: nightly parser fuzz lane — parser input fails as typed AppErrors, never hangs (#1414) (#1438)
* test: nightly parser fuzz lane with typed-AppError invariant (#1414)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(fuzz): run envelope, artifact promotion, and harness self-check tests (#1414)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(fuzz): shared scheduled-lane envelope on every terminal path, watchdog after ready (#1414)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(fuzz): envelope for malformed options; add scheduled-lane health consumer (#1414, #1430)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(lanes): actions:read scope, terminal error envelope, first-due grace (#1414, #1430)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(lanes): anchor first-run grace to schedule registration, use exec helper in tests (#1414, #1430)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(lanes): portable POSIX pickaxe pattern for schedule registration (#1414, #1430)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(fuzz): fast-check generators over the shared hazard list, drop the bundled lane-health work (#1414)

- Strip scripts/scheduled-lane/* and scheduled-lane-health.yml: that watcher is #1430's own
  deliverable and collides with PR #1439's implementation of the same lane. What this lane owes
  (a per-run envelope) moves into scripts/fuzz/envelope.ts.
- Rebase onto #1437 and rebuild the generator layer on fast-check: cases come from arbitraries
  sharing SELECTOR_VALUE_HAZARDS with the property suite, and counterexamples are shrunk, so a
  failure names a minimal input plus fast-check's seed/path instead of a 20k-char random string.
- Route harness.test.ts into the serialized subprocess-stub project.
- Drop the AGENT_DEVICE_FUZZ_STARTUP_DELAY_MS test seam: the ready handshake is now proven by a
  case budget far below real worker startup.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(fuzz): replay the regression corpus through the worker watchdog (#1414)

A promoted hang case used to wedge the unit job until the CI timeout, because corpus replay called
checkCase in-process. It now goes through the same worker-backed watchdog the nightly lane uses, so
such a case fails against a 5s per-case budget; the file moves to the serialized subprocess-stub
project with the rest of the worker-driven fuzz tests.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(fuzz): let the watchdog outlive vitest's default case timeout (#1414)

A wedged parser was surfacing as a bare 'Test timed out in 5000ms' instead of the named hang:
failure that says which input wedged, because the file's vitest timeout was shorter than the
watchdog budget times the number of replayed cases.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(fuzz): complete drift provenance in the lane envelope (#1414)

configHash now covers every input that decides what a seed generates (generate.ts and the shared property arbitraries, not just the arbitraries/targets/invariant), and tool records fast-check's installed version. A generation-loop edit or a fast-check upgrade previously changed the case set while the envelope looked unchanged. A test recomputes the hash with each input omitted so a future omission fails.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-28 11:29:25 +02:00
devin-ai-integration[bot] e545544dfa test(daemon): seeded concurrency torture lane for session/lease/lock invariants (#1439)
* test(daemon): seeded concurrency torture lane for session/lease/lock invariants

Refs #1416

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(daemon): drive torture lane through real lock plan + review fixes

- derive each op's lock plan from production resolveRequestExecutionLockKeys
  via a fake device-inventory provider, so reverting the router's same-device
  serialization trips the overlap invariant (verified)
- assert exact replay: full scheduler trace, terminal outcome, contention
- assert real same-device lock contention in the sweep + a forced 2-client case
- split harness into bindings/invariants/envelope modules (all <500 LOC)
- emit #1430 scheduled-lane envelope (schema/SHA/hash/seed range/duration/result)
  and upload it from the nightly workflow

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(daemon): pass claim data as plain view accessors (fallow)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(daemon): gate lock plan on shouldLockSessionExecution; sweep replay + forced-device contention; whole-lane envelope

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(daemon): add real-scope runLocked serialization guard; whole-lane envelope duration

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(daemon): allow seed 0 replay; add seed-0 regression (TORTURE_SEED must accept 0)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* obs(#1430): add scheduled-lane freshness/cadence health watcher

Discovers schedule: workflows from .github/workflows/, reads recent
scheduled runs via the GitHub API, and opens/pings a tracking issue when
a lane misses or fails two consecutive cadences. Pure model unit-tested
and gated on PRs; API I/O + issue open/ping run nightly.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(daemon): give scheduled-lane watcher a two-cadence newborn grace

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(daemon): anchor lane grace on schedule-introduction, not workflow age

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(daemon): derive schedule-activation semantically via git, through runCmdSync

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(daemon): add merge-commit regression pinning first-parent + committer time

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(daemon): pin scheduled-lane-health issue-write route via stubbed fetch

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(daemon): unbundle #1430 watcher; make torture lane nightly-only

Strip the scheduled-lane-health watcher (scripts + workflow + PR gate) —
it is #1430's deliverable and collides with PR #1438's workflow of the
same filename; keep only this lane's #1430 envelope writer.

Move the torture lane under test/integration/nightly/ so it is out of the
test:integration:node glob, and run it via an explicit, disclosed PR step
plus the nightly sweep.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs: fix stale torture-lane paths after nightly/ move

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(daemon): adopt shared lane-envelope for torture lane

Rebase onto main (post-#1441) and replace the lane-local LaneEnvelope
dialect with the shared scripts/lib/lane-envelope.ts builder, so the
#1430 health watcher parses one schema: commitSha->commit, sourceHash->
configHash, seedRange/runs moved into the typed data payload, and the
sweep encoded as seed "<start>-<end>".

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-28 10:29:41 +02:00
devin-ai-integration[bot] 8cce0ef6b8 test: ratchet mutation score over enumerated decision kernels (#1441)
* test: ratchet mutation score over enumerated decision kernels

Adds a Stryker (vitest runner) mutation lane scoped to the decision kernels,
a per-module baseline with tool/config provenance, and a ratchet that only
lets scores rise. Non-gating until two consecutive stable weekly sweeps.

Refs #1415

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore: declare the mutation test-scope seam for production-export analysis

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: own kernel tests in the mutation registry and ship the #1430 lane envelope

- restore bench:help-conformance, broken by a formatting-path edit
- kernel test files select their module on PRs (registry `tests` + workflow paths),
  asserted to reach the kernel through the import graph
- every mutation run writes the standard scheduled-lane artifact envelope
- move src/utils/__tests__/errors.test.ts beside its source per the mirror rule

Refs #1415, #1430

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: derive kernel test ownership and land the scheduled-lane health monitor

Ownership of a kernel's tests is now computed from the static import graph
(scripts/mutation/ownership.ts) instead of a hand-listed set, so a test that
reaches a kernel indirectly -- src/__tests__/daemon-error.test.ts through
src/daemon.ts -- selects that kernel on a PR. The PR lane triggers on every src
test and shards the derived modules, keeping wall clock at one module.

The lane envelope (#1430) is now written on every exit path with the stage it
reached, so a crash before any mutant runs is distinguishable from a lane that
never ran. Adds the derived cadence monitor (scripts/lane-health, daily
workflow): scheduled lanes are enumerated from .github/workflows/ and reported
dark, failing, or never-run against their own cron cadence.

* fix: merge only Stryker reports from a shard directory

The shard artifacts now carry the lane envelope beside mutation.json, and the
merge globbed every .json under the download path, so the ratchet job fed the
envelope to the report parser and died after the mutants had already run.

* fix(mutation): fail on incomplete shard sets and envelope pre-run failures

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(mutation): downgrade a passing envelope when a later lane step fails

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(mutation): shard by registry, defer the PR lane, drop the bundled watcher

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs: describe registry sharding and the deferred PR mutation lane

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(mutation): make the pre-graduation tooling exception select real mutants

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(mutation): give the worktree fixture commits their own identity

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-28 10:07:14 +02:00
devin-ai-integration[bot] d747ef6230 test: frozen replay-compat corpus with expected verdicts (#1417) (#1436)
* test: frozen replay-compat corpus with expected verdicts (#1417)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: pin replay-compat corpus bytes to released blobs and assert via parseReplayInput

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: lock replay-compat provenance kind by corpus area and verify it in CI

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs: describe corpus provenance-kind lock and CI job

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: prune replay-compat corpus to minimal witnesses per shipped form

Reviewer feedback on #1436: the mechanism earns its place, the dataset did not.
Drop the 30 corpus entries whose bytes repeat a syntactic form or a migration
refusal another entry already witnesses (platform twins and adjacent-release
re-recordings), leaving 22 deliberate entries; make note required and state per
entry which form or refusal it is the sole witness of.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: address corpus review nits (typed coverage list, cap rationale, derived-citation note)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs: split corpus rule — form from the release, verdict from today's parser

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs: format corpus README emphasis markers

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-28 10:06:00 +02:00
devin-ai-integration[bot] 8670a88afa test: property-based testing foundation (fast-check) over parse/print and geometry kernels (#1437)
* test: property-based testing foundation over parse/print and geometry kernels (#1413)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test: derive selector and replay command domains from their owning registries

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-27 17:15:39 +02:00
Michał Pierzchała 2d1d70613f feat(bench): renderer-pinned samples, topic-coverage gate, error-recovery quizzes; trim skillgym to agentic checks (#1411)
* feat(bench): renderer-pinned samples, topic-coverage gate, error quizzes; trim skillgym to agentic checks

The help conformance bench's quoted CLI output is now sourced from
scripts/help-conformance-sample-outputs.mjs, and every sample is rebuilt
through the real production renderers (settle output formatters,
printHumanError, formatSnapshotText, refMutationAdmissionResponse) by
scripts/__tests__/help-conformance-sample-outputs.test.ts — a rendering or
message change fails deterministically instead of leaving the bench grading
against output the CLI no longer prints. This retires the fabricated
recoverable-failure envelope (production never throws a textual settle
timeout; that case is replaced by a real DEVICE_IN_USE recovery quiz).

Bench cases move to scripts/help-conformance-cases.mjs and are enumerated
against the help-topic registry: helpTopicIds() is exported from cli-help,
and scripts/__tests__/help-conformance-topic-coverage.test.ts fails when a
help topic has neither a bench case nor an explicit waiver. New case
families: error-envelope recovery quizzes (device-in-use, stale pinned ref,
ambiguous find match, app-not-installed) pinned to real error text, topic
coverage for tv/web/react-native/debugging/workflow, and a metamorphic twin
of the settled-diff quiz.

The skillgym smoke suite shrinks from 119 cases to the 5 that measure what
only an agentic runner can show: skill routing plus output interpretation
with a proven local CLI help probe (local-cli-help-policy). Its embedded
samples now import the same pinned constants, replacing hand-transcribed
output that had already drifted from the renderer. Knowledge checks belong
to the bench; live fixture behavior belongs to the iOS simulator e2e suite.

* review: drive error samples through the real producers; enforce local-help on the routing smoke

The DEVICE_IN_USE, AMBIGUOUS_MATCH, and APP_NOT_INSTALLED parity tests no
longer hand-author the producer message before rendering: each drives the
actual producer — buildDeviceInUseBySessionError (extracted in
session-open.ts and called by the handler), buildAmbiguousMatchError (now
exported from find.ts), and buildAppNotInstalledError (extracted in
app-resolution.ts and thrown by the resolver). Because each factory is
exported from its producer file and called by the production path, dropping
the production call would make it test-only and fail
check:production-exports — the wiring is gate-enforced, not conventional.

open-and-snapshot now sets requireLocalCliHelp and
allowOnlyLocalCliHelpCommands, so the 'skill plus local help' claim is
observed rather than assumed; without them the case can pass on model prior
alone.
2026-07-27 14:24:36 +02:00
Michał Pierzchała 1a76344685 docs: restructure AGENTS.md and CONTEXT.md for progressive disclosure (#1402)
* docs: restructure AGENTS.md and CONTEXT.md for progressive disclosure

Apply the Claude 5 context-engineering guidance to the repo's agent docs:
keep the always-loaded file to gotchas and invariants, and move situational
guidance one hop away behind a routing table.

AGENTS.md 315 -> 229 lines. Cut generic agent-behavior boilerplate, three-way
duplication (Common Mistakes restated Hard Rules; Finding Source Owners
restated the registry section), and facts visible from the repo itself.
Kept verbatim: the expensive-lessons principles, enforcement gates, Hard
Rules, and environment traps.

Split out docs/agents/{cli-flags,pull-requests,device-verification}.md and
folded the Testing Matrix into docs/agents/testing.md, reframed around
pnpm check:affected so the prose stops duplicating the selector.

CONTEXT.md keeps all 50 terms, now grouped under a section index so a task
loads one section instead of the whole glossary.

* fix(check-affected): move the selector-owning sentinel to the Testing Matrix

The Testing Matrix moved from AGENTS.md to docs/agents/testing.md, but the
affected-check selector still treated only AGENTS.md as selector-owning. A
later matrix edit would have been classified as inert docs and skipped the
fail-open, so the selector could keep deriving gates from a spec that had
changed underneath it.

Move the sentinel with the prose, as a named SELECTOR_OWNING_DOCS set so the
next move is one line, and fix the two in-code comments plus the testing.md
paragraph that still pointed at the AGENTS.md matrix.

* docs: restore two rules dropped by the AGENTS.md split

Review caught two repo-specific rules that did not survive the move. Both are
prose without any backticked identifier, so the identifier-diff used to verify
the split could not see them.

- "Test through public interfaces; do not add unrelated production exports
  solely to enable tests" returns next to the behavioral-tests rule in
  docs/agents/testing.md, with the reason it exists.
- The guidance-ownership rule (decide whether new guidance/schema/metadata
  belongs to the command surface, CLI grammar, CLI help, MCP projection, or
  daemon runtime) returns to the always-loaded Docs & skills section, since it
  governs all command-surface work and not just the flag case.

Also point the ADR routing row at docs/adr/README.md, which is already the
"read when you touch…" index, rather than at the bare directory.
2026-07-25 12:13:09 +02:00
devin-ai-integration[bot] 47134bf764 feat: add derived fail-open check:affected selector (#1195)
* feat: add derived fail-open check:affected selector

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor: simplify selector for complexity gate; add docs

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: fail open on ambiguous non-source fixtures; guard catalog against real package.json/vitest.config

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor: use src/utils/exec.ts process helpers in check:affected runner

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(check:affected): SkillGym ownership, honest catalog, working-tree discovery

- Add SkillGym ownership for skills/ and test/skillgym/; stop short-circuiting
  their Markdown as docs-only (findings 2 & 4).
- Drop the fabricated GitHub 'SkillGym' job: it is a local-only gate, now
  localRunnable with no CI job, guarded by a workflow-existence self-test (3).
- Fold working-tree (staged/unstaged/untracked) state into local discovery and
  disable rename detection so both rename paths classify (1).
- Add run.test.ts entrypoint regressions (real diff/status/rename discovery,
  --run order/skip/stop-on-failure).

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(check:affected): union staged + unstaged diffs so they cannot cancel

A single `git diff HEAD` nets index against working tree, so a staged add
and an unstaged delete of the same file cancel and hide it. Collect
`--cached` (staged) and unstaged diffs separately and union them; add a
cancellation regression test.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(check:affected): cover required suite gates

* refactor(check:affected): delegate tests to vitest

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-10 17:53:52 +02:00
Michał Pierzchała 2557670193 test: slow-test ratchet and speed rules from measured experiments (#1099)
* test: slow-test ratchet, budget-derived emulator poll, speed guidance from experiments

Measured (2026-07-04, full unit suite: 340 files / 3,210 tests / 48s wall):
wall clock was bounded by the slowest FILE (44.6s android monolith at ~7x
file-level parallelism), and the slowest tests were sleeping through real
production budgets (10.8s proving 'times out' by waiting the constant out,
8s emulator polls at 1Hz, real retry backoff). Two config experiments
rejected with data: --no-isolate exploded the suite to 205s (module state
thrashes across files sharing workers) and --pool=threads changed nothing.

- scripts/vitest-slow-test-reporter.ts: the slow-test ratchet. Unit budget
  2.5s / integration 15s; failure at 2x budget (the band between reports
  without failing so host-load variance cannot make the gate cry wolf);
  36 pinned offenders, exact keys, ratchet-only pin (tracking #1098).
- waitForAndroidEmulatorByAvdName: poll cadence derives from the caller's
  budget (min 1s, floor 50ms, ~timeout/20) — devices.test.ts 25.6s -> 2.8s
  (9x) in isolation, and short-budget production calls stop sampling at
  1Hz against small budgets.
- vitest.config: slowTestThreshold 500 for local visibility; reporter
  wired; isolation/pool decisions documented with the measurements.
- docs/agents/testing.md 'Speed rules' + AGENTS.md testing bullet: the
  three conversion patterns in preference order (budget-derived cadence,
  budget-wiring assertion, fake clocks), the no-seam constraint, and the
  file-granularity Amdahl argument that makes the monolith test split a
  wall-clock fix, not just navigation.

* fix: fallow findings on the slow-test gate — import edge, factory reporter, unit tests

The string-path reporter wiring read as a dead file (fallow cannot see
vitest's reporter loading); the config now imports the factory, making
the edge real and type-checked. The class shape tripped the
unused-class-members rule (framework callbacks are invisible to
reference analysis) — converted to a factory returning the Reporter
object, with the classification and rendering logic extracted as pure
exported functions. Those functions now carry their own unit tests
(budget bands, integration budgets, pin matching, warn-vs-fail
rendering), which also grounds the CRAP estimate in real references.
Canary re-verified: unpinned 5.2s sleeper fails the run with exit 1;
clean runs exit 0.
2026-07-04 19:13:06 +02:00
Michał Pierzchała c6fd3dc972 test: add live web platform smoke (#832)
* test: add live web platform smoke

* test: harden web smoke cleanup
2026-06-21 13:05:51 +02:00