The capability matrix admitted `viewport` on every Apple simulator and
device while no Apple interactor implements `setViewport`, so the command
was routed to the device and only rejected inside dispatch with the
backend-shaped "viewport is not supported by this backend". Nothing can
serve it there: Apple screen geometry is fixed by the selected device type
and neither simctl nor XCTest exposes a resize primitive, so viewport is a
web-surface contract (its CLI summary and docs already say so).
Deny it in the descriptor's capability facet, matching the adjacent Android
denial, and give the Apple family an unsupported hint that names the surface
that does support it. Admission now fails before the request reaches the
device: "viewport is not supported on this device" with supportedOn: web.
The iOS simulator coverage manifest's known-gap row becomes a
capability-denial row owned by the static coverage test, which retires the
whole known-gap level and the `full:known-gaps` live scenario that existed
only to pin this failure.
Closes#1407
* test: characterize replay-test reporter contract and extend R10
P3 of #1478 moves the replay-test scheduler into `packages/replay-test` and
makes attempt identity scheduler-owned. Before production code moves, pin what
a shipped custom reporter actually observes today, and close the import
boundary the extraction has to end up satisfying.
Reporter values (all pinned as shipped, none proposed):
- the `RequestProgressEvent` -> reporter-value projection field by field,
including key presence for absent optionals and the dropped `command` events;
- `session` provenance across the seam: the start value is always `attempt-1`
(it is built before any attempt runs), step values track the running attempt
through the per-attempt AsyncLocalStorage context, and result values carry the
attempt that produced them, so a retried case reports three different
sessions to one reporter;
- the shard-scoped session prefix and device identity a sharded run reports;
- module export spelling precedence, the six optional hook names, the live-hook
vs final-hook error asymmetry, and exit-code recommendation semantics.
Late-timeout finalization/cleanup:
- finalization always runs before cleanup, and the timing trace records that
order (`finalize_start/stop` then `cleanup_start/stop`);
- a replay settling inside the 2s grace window cleans up once and is not marked
`timeout_cleanup_pending`, and the raced TIMEOUT response still wins;
- a replay that misses the window defers its second cleanup until the abandoned
replay settles, and that late cleanup's failure is swallowed.
R10 now rejects replay-test imports from `src/request/**` and engine internals
(`src/replay/`, `src/compat/`, `src/maestro/`, `src/ad-replay/`), with the four
imports that exist today recorded as shrink-only migration entries so the rule
enforces immediately and the extraction must delete them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
* test(daemon): assert reporter hint on the real onTestResult value
The failing-suite characterization supplied `hint` as input but never
asserted it on the hook value, so dropping `hint: error.hint` from the
scheduler path left all reporter tests green. Assert it on the real
reporter value so the ratchet catches a shipped field going missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(daemon): reject unsupported raw --save-script flags at request ingress
`flags.saveScript` arms session-script publication the moment a successful
handler records the request's action, so any recordable command reaching a
handler with the raw flag set could arm publication and write a `.ad`
artifact — `record stop` immediately through record-only cleanup, `trace` and
the interaction commands later on close. The CLI/Node/MCP surfaces only emit
the flag for its released owners, so that arming path was reachable only by a
hand-built wire request or a free-form `batch` step.
Close it at the daemon request seam (`createRequestHandler`), the single entry
point both the socket and HTTP transports funnel through, and ahead of
admission, session lookup, device resolution, and handler dispatch. Ownership
is declared on the command descriptor (`saveScriptFlagOwner`) rather than
re-stated as a string set, so `open`, `close`, and `replay` keep the flag and
everything else is rejected with an ADR 0010 shaped `INVALID_ARGS` carrying a
hint, diagnosticId, and logPath.
With that path pinned closed, a record-only session can no longer carry
`recordSession`, so `record stop`'s immediate `writeSessionLog` could only ever
be a no-op; delete it.
Refs #1478.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
* test(daemon): assert ADR 0010 diagnostics on save-script rejection
The HTTP test helper projected only code/message/hint, so `diagnosticId`
and `logPath` could regress while the transport tests stayed green.
Preserve both in the helper and assert on each transport that
`diagnosticId` is present, `logPath` is present and exists, and the log
records `save_script_flag_rejected`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 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>
* fix: publish replay refs after response projection
* fix: retain replay device lock through finalization
* fix: verify Apple text wait misses with snapshot
* fix: lock replay device before open effects
* fix: lock replay device before open preparation
W-pre of #1490: kernel's file set becomes its public API before it
moves to packages/. Three files drifted into the sink because R1
pushes shared code down, not because they are foundational:
- audio-probe-support -> src/contracts/ (capability policy consumed by
platform plugins and interactor registration)
- input-validation -> src/contracts/ (lowest-rank consumer is
contracts/gesture-input)
- observability-redaction -> src/commands/observability/runtime/
(colocated with its only consumer, diagnostics-format)
Pure moves; no behavior change. Kernel retains errors, device,
snapshot, contracts, collections, rect, redaction, daemon-error.
Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep
Co-authored-by: Claude <noreply@anthropic.com>
* test: add scroll-edge-state suite, close mutation gap (#1455)
scroll-edge-state.ts had no dedicated test file and scored 28.75%
mutation (92/320) — the worst of the five tracked decision kernels.
Adds src/utils/scroll-edge-state.test.ts covering every exported
function's decision branches (captureScrollEdgeState,
runScrollEdgePasses, formatScrollEdgeMessage) plus the private
container-selection/scope/signature logic they exercise. Raises the
scoped mutation score to 95.31% (305/320); the 15 remaining survivors
are named, justified-equivalent mutants (documented in the PR).
* refactor(test): split scroll-edge-state suite, drop signature test-gaming
Splits the 1,323-line scroll-edge-state.test.ts into three behavior-focused
sibling suites (container selection, scope naming, pass orchestration) plus a
shared fixtures module, per AGENTS.md's per-file LOC tripwire and sibling-test
convention.
Also removes the dead `signature`/buildScrollStateSignature/collectSubtreeNodes/
hasAncestor production code: it had zero consumers anywhere in the repo, and
the tests pinning its private string format were the mutation-score-gaming
flagged in review — with it gone, there's nothing left to game.
Addresses review feedback on PR #1474 (#1455).
* refactor(test): split scroll-edge-state suite further, tighten unreachable branch
Splits container-selection.test.ts (562 LOC, over the 500-LOC extract
tripwire) into four narrower suites by durable question: container
existence, hidden-edge decisions, target resolution, and broad selection.
Adds a scrollNode() fixture builder to cut inline node-literal repetition
across the suite.
Removes the "captureNodes resolves to undefined" test: the real contract
(both production call sites, and captureScrollEdgeState's own captureNodes
type) always resolves to a real array, so this exercised an unreachable
branch behind a type-unsound cast. Tightens analyzeScrollEdgeState's
parameter type to drop the now-provably-unreachable `| undefined`.
Simplifies buildScrollContainerScope to filter out non-string
identifier/label fields instead of mapping them to an empty-string
placeholder — behaviorally identical (the placeholder always failed
isUsefulScope's length check anyway) but removes a StringLiteral mutation
surface that a synthetic fallback could never legitimately kill without
coupling a test to Stryker's specific replacement text.
Addresses further review feedback on PR #1474 (#1455).
* 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>
* 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>
* feat(examples): add runnable Node.js SDK examples under examples/sdk/
examples/test-app is a fixture and the repo's only prior examples/
content; the real SDK usage patterns lived only in
website/docs/docs/client-api.md with no runnable script anywhere.
Adds four standalone, typechecked examples covering the minimum surface
from #1463: root client session (create -> open -> snapshot/tap ->
close with typed error handling), agent-device/metro
(normalizeBaseUrl/resolveRuntimeTransport), agent-device/contracts
(centerOfRect on a snapshot node), and agent-device/batch (runBatch for
a custom transport). Each imports the published `agent-device/...`
subpaths rather than relative src/ paths.
examples/sdk/tsconfig.json path-maps those subpaths to src/sdk/ so
`pnpm typecheck` (now also run against this tsconfig) checks the
examples in CI without a prior build, workspace link, or publish step.
Running an example for real still resolves `agent-device` as a
self-referencing package after `pnpm build`.
src/__tests__/client-api-examples-drift.test.ts guards the examples
against drifting from client-api.md's subpath API manifest in both
directions, picked up automatically by the existing unit-core vitest
project (no new script or workflow needed).
examples/README.md indexes the new examples and notes that test-app/
remains a fixture, not an example; it is not renamed or moved.
Refs #1463
* fix: address Fallow findings on the new SDK examples
Fallow flagged the four examples/sdk/*.ts files as unused files (not
reachable from any entry point) and three functions as high complexity.
- Register the examples as manual entry points in .fallowrc.json,
matching how other standalone scripts (scripts/patch-xcuitest-runner-icon.ts,
scripts/runner-request-count/run.ts) are already declared.
- Reduce complexity in client-session.ts and contracts-result.ts by
extracting device-resolution/error-reporting and rect-assertion
helpers out of main().
- Reduce complexity in the drift guard's parseSubpathManifest by
splitting bullet-matching and backtick-name extraction into their
own functions.
Verified: pnpm check:fallow --base <PR base sha> now reports no issues,
and pnpm check:tooling / pnpm test:unit stay green.
Refs #1463
* fix: compile client-api.md's actual code snippets, not just its symbol manifest
Addresses review feedback on #1463's drift guard: the existing guard only
parsed the doc's "Public subpath API" bullet manifest and compared imported
symbol names, so a fenced ```ts snippet could drift or stop compiling
without the guard noticing.
Added test/integration/client-api-doc-snippets.test.ts, which extracts every
fenced ```ts block from client-api.md and typechecks it against the real
agent-device/* sources (reusing examples/sdk/tsconfig.json's existing paths
mapping, read via `tsc --showConfig` so there's one source of truth). Free
identifiers that continue a `client`/`snapshot` from an earlier snippet are
stubbed — typed against the real SDK return type, not `any`, so continuation
snippets still get real checking. Lives in the Node integration lane
(test/integration/*.test.ts), not vitest's unit-core: it spawns a real tsc
Program, well past the unit suite's 2.5s budget.
Running this check against the existing doc surfaced real, pre-existing
snippet bugs (unrelated to the new examples), fixed here:
- "sessions.artifacts": `result.cloudArtifacts` accessed without narrowing
the `CloudArtifactsResult | DaemonArtifactsResult` union first.
- "Device cloud sessions": `platform`/`device` were passed into the client
constructor config, which doesn't accept them; moved to the `apps.open()`
call where those fields actually belong.
- "Android ADB providers": the inline `exec` handler had no parameter types,
so it failed under strict/noImplicitAny; annotated with the real
`AndroidAdbExecutorOptions` type.
Two further gaps the check surfaced are pre-existing product/API-surface
questions out of scope for this PR (not the new examples), so they're
allowlisted in KNOWN_DOC_GAPS with comments rather than silently patched:
- "Remote Metro helpers" documents prepareRemoteMetro/reloadRemoteMetro/
stopMetroTunnel/resolveRemoteConfigProfile as public, but none of them are
exported from agent-device/metro or agent-device/remote-config today.
- "Web sessions"/audio probe pass `platform` to `observability.network()`/
`.audio()`, but NetworkOptions/AudioOptions have no `platform` field even
though the CLI's network/audio commands accept `--platform`.
Refs #1463
* fix: close the doc-snippet compiler's stubbing hole and the two suppressed gaps
Addresses the second round of review feedback on #1463's drift guard:
1. stubFreeNamesAndRecompile auto-stubbed every "Cannot find name" as `any`,
so a typo like `cliet.apps.open()` would silently pass on the second
compile. It now only stubs identifiers in an explicit allowlist
(KNOWN_FREE_NAME_STUB_TYPES) — the real SDK-derived continuations
(`client`, `androidClient`, `snapshot`) plus the doc's own invented
host-glue names, each typed precisely rather than loosely. Anything else
is left as a real compile failure. Added a regression test that feeds a
`cliet` typo through the guard and asserts it fails.
2. KNOWN_DOC_GAPS filtered six real compiler errors out of the final
assertion while the test claimed every snippet compiles. Investigated
both and fixed the actual contracts instead of suppressing them:
- `prepareMetroRuntime`/`reloadMetro` (src/metro/client-metro.ts) and
`stopMetroTunnel` (src/metro/metro.ts) already existed and matched the
doc's described workflow almost exactly (same result shape) but were
never re-exported from `agent-device/metro`; same for
`resolveRemoteConfigProfile` and `agent-device/remote-config`. Added
the four exports and fixed the doc's stale function names
(`prepareRemoteMetro`/`reloadRemoteMetro`) and one stale field name
(`profileKey` -> `companionProfileKey` on the prepare call) to match.
- `NetworkOptions`/`AudioOptions` (src/contracts/client-observability.ts)
had no `platform` field even though the CLI's `network`/`audio`
commands accept `--platform` for the same use case, and the client
methods already forward the options object to the daemon generically
(`executeCommand('network'|'audio', options)`) — so this was a type
gap, not a runtime one. Switched both from AgentDeviceRequestOverrides
to DeviceCommandBaseOptions (matching PerfOptions' existing pattern),
closing the gap for real instead of stripping `platform` from the doc.
- The "Android installFromSource()" snippet was missing its
`createAgentDeviceClient` import outright; added it.
KNOWN_DOC_GAPS is gone — every fenced snippet now compiles for real, and
the test's assertion matches what it claims.
3. Switched the raw `execFileSync` calls to `runCmdSync` from
src/utils/exec.ts, per AGENTS.md's process-execution invariant (this is
a .ts integration test, not a packaging fixture that needs to stay
dependency-free).
Refs #1463
* docs: fix stale reloadRemoteMetro() prose reference to reloadMetro()
The prose right after the Remote Metro helpers snippet still named the old
function; the compile guard only checks the fenced snippet, not surrounding
prose, so it didn't catch this leftover from the prior rename.
Refs #1463
---------
Co-authored-by: Claude <noreply@anthropic.com>
* ci: fold single-grep jobs into steps, call named pnpm scripts
- Merge ios-runner-swift-compat and no-test-di-seams (each just
checkout + one rg assertion) into steps of a new static-checks job,
keeping each step's own failure message. Removes two job-scheduling/
checkout overheads and two PR status-check lines.
- Replace the layering-guard job's inlined copies of check:layering and
depgraph:test with the named pnpm scripts, removing the silent-drift
risk between the workflow and package.json.
- Fix the same drift in conformance-regenerate.yml, which inlined
maestro:conformance:regenerate byte-for-byte.
- Leave affected-selector's inline node invocation as-is: R8's zero-dep
closure check (scripts/layering/zero-dep-jobs.ts) finds a job's entry
scripts by matching literal paths in the run: block, so switching to
`pnpm check:affected:test` would zero out its entries and make R8
fail closed. Documented inline why this one stays inlined.
- Leave publish-mcp-registry.yml's sync-mcp-metadata --check alone: that
job never runs the setup-node-pnpm action, so pnpm isn't provisioned
there at all.
Refs #1462
* ci: teach R8 to resolve pnpm script names, drop affected-selector's inline copy
R8's zero-dep-job entry scan matched literal script paths in a run: block,
so a bare `pnpm <script>` invocation found zero entries and R8 failed
closed — the reason affected-selector kept an inline node command instead
of calling pnpm check:affected:test (#1462). zeroDepJobs now also resolves
a pnpm script name against package.json and scans the resolved command for
entry paths, so affected-selector can call the named script like every
other job.
Also replaced the other workflows' inlined copies of named package.json
scripts (test:replay:*, perf, perf:android, maestro:conformance:differential,
check:mcp-metadata, size) with their pnpm names, keeping each job's
CI-specific trailing flags — found via a repo-wide sweep for any run: block
whose text duplicates a scripts entry.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L67kDSTAJwaoLCwANEJFRM
* fix: revert publish-mcp-registry pnpm regression, recurse R8 alias resolution
publish-mcp-registry.yml's job only provisions Node via actions/setup-node,
never the repo's setup-node-pnpm action, so pnpm is never installed there —
the earlier sweep's `pnpm check:mcp-metadata` would have broken the release
path. Reverted to the direct node invocation with a comment explaining why,
matching the PR's own stated rationale for leaving it alone.
zeroDepJobs' pnpm-alias resolution only expanded one level: a resolved
script that itself invoked another named pnpm script had its entries
silently dropped from R8's closure. resolveRunEntries now recurses through
chained aliases with a per-chain visited set, so a nested alias's entries
are found and a cycle stops re-expanding a repeated name instead of
recursing forever. Added coverage for both the chained and cyclic cases.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L67kDSTAJwaoLCwANEJFRM
---------
Co-authored-by: Claude <noreply@anthropic.com>
* obs: add repo-health snapshot command aggregating existing analyzers into one JSON
Adds `pnpm repo-health [--json]`, an offline command that aggregates the
signals this repo already computes into one deterministic JSON snapshot by
reusing each analyzer (depgraph, layering ratchets, coverage json-summary,
size-report, fallow baselines, slow-test ratchet, bench/skillgym registries)
rather than reimplementing any metric.
Carries mandatory v1 provenance (schemaVersion, commit, per-analyzer content
hashes, input provenance) so #1424 can persist history. Component metrics
(instability/abstractness/main-sequence distance) are observatory-only. The
only gating behaviour is the depgraph-vs-layering R6 consistency assertion,
already wired into the Layering Guard job via scripts/depgraph/model.test.ts.
Closes#1423
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* repo-health: hash read artifacts and flag staleness in provenance
Coverage and size are artifacts repo-health reads but does not produce, so
they carry no producing-commit stamp. Previously provenance recorded only
their paths and the current HEAD, so a snapshot taken after a source edit
without rerunning those producers would pair prior metrics with the new SHA —
#1424 would persist a false commit-indexed history entry.
Now each read artifact is bound via artifactProvenance() to its content hash
(so history keys on the bytes the metrics came from, not a commit they may
predate) and an explicit `stale` flag (true when a production source file is
newer than the artifact). The coverage/size analyzer-config hashes are added
to provenance.tool, and the human summary marks stale artifacts. Adds a pure
stale-artifact regression test.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* repo-health: bind artifact freshness to the whole producer input set
The stale check derived freshness only from listSourceFiles() (src/*.ts) and
applied it to both coverage and size. Coverage also depends on tests and vitest
config; size-report.mjs reads package.json and runs npm pack — so a change to a
non-src producer input could leave an old artifact marked stale:false while
provenance recorded the current HEAD, a commit the metrics predate.
Each read artifact now declares its full producer input set (PRODUCER_INPUTS)
and is flagged stale when ANY tracked input in that set is newer than the
artifact (via git ls-files mtimes). Freshness that cannot be proven — an empty
observable input set — is reported stale, never falsely fresh. The input set is
recorded as provenance.inputs.*.producerInputs. Adds isArtifactStale() with a
regression covering a non-src input newer than the artifact and the unprovable
case.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* repo-health: prove artifact freshness from a stamped producer commit; route git through runCmdSync
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>
* test(kernel): cover all six errors.ts exports with table-driven cases (#1456)
Extends errors.test.ts beyond normalizeError/toAppErrorCode:
- AppError construction and NormalizedError/AppError instanceof contract
- asAppError identity/wrap/duck-typing behavior (cross-realm errors,
non-Error throws, fallback code precedence)
- isAgentDeviceError real-instance-only contract
- retriableForErrorCode and defaultHintForCode, table-driven over
KNOWN_APP_ERROR_CODES (the module's own source of truth) rather than a
hand-copied code subset
- normalizeAgentDeviceError delegation to normalizeError
- a cross-cutting composition test asserting the retriable/hint contract
request-router.ts's enrichDaemonError and the CLI's printHumanError
actually rely on
No production changes; mutation re-run and PR to follow.
* test(kernel): pin maybeEnrichCommandFailedMessage gating and regex boundaries (#1456)
Adds targeted cases for branches the table-driven/contract tests didn't
reach: the COMMAND_FAILED/processExitError/string-stderr gating in
maybeEnrichCommandFailedMessage, GENERIC_EXIT_MESSAGE's `-?\d+$` boundary
(negative code, trailing text, non-numeric code, whitespace in the tool
token), and STDERR_NOISE_PREFIX's zero-or-more-whitespace semantics for
both the adb/xcrun/simctl and "error:" groups.
* test(kernel): close remaining mutation gaps in firstStderrLine/detail helpers (#1456)
Each addition here was verified against a hand-applied mutation (matching
Stryker's actual survivor diffs) before landing, confirming it fails on
the mutant and passes on the original:
- firstStderrLine: line-trim-before-prefix-match (a real skip-pattern
line ahead of the leading-whitespace one is required, since
redactDiagnosticData already .trim()s the whole stderr string once
before firstStderrLine ever sees it), and the >200-char truncation
branch (both sides of the boundary: 200 exact vs 201+).
- stringDetail/booleanDetail: reject a wrong-typed diagnosticId/logPath/
hint/retriable instead of surfacing it.
- stripDiagnosticMeta: details drops to undefined once stripping the
known meta keys leaves nothing behind.
* style(kernel): apply oxfmt to errors.test.ts
Fixes the Lint & Format CI failure on PR #1456 — format:check flagged
this file, oxfmt --write resolves it with no behavior change.
* test(kernel): split errors.test.ts into focused sibling suites (#1456)
Per review feedback: the growing errors.test.ts hit 601 lines, past
AGENTS.md's extract-before-500 threshold (tests are not exempt). Splits
along the reviewer's requested seams, moving tests verbatim with no
assertion changes:
- errors-message.test.ts (234 lines) — stderr/message normalization:
maybeEnrichCommandFailedMessage, firstStderrLine, the
GENERIC_EXIT_MESSAGE/STDERR_NOISE_PREFIX regex boundaries.
- errors-metadata.test.ts (157 lines) — hint/diagnosticId/logPath/
retriable/supportedOn lifting and stripping, divergence passthrough,
the stringDetail/booleanDetail/stripDiagnosticMeta type-guard
contracts, and the normalizeError/retriableForErrorCode/
defaultHintForCode cross-cutting contract.
- errors-code-policy.test.ts (228 lines) — AppError construction,
asAppError/isAgentDeviceError duck-typing contract, toAppErrorCode,
the retriableForErrorCode/defaultHintForCode table-driven policy
tests, and normalizeAgentDeviceError.
Also repoints scripts/mutation/ownership.test.ts's two references to
the deleted errors.test.ts path at errors-code-policy.test.ts — the
mutation ownership deriver reads the file from disk to follow its
import graph, so a stale path silently resolves to "owns nothing"
instead of failing loudly.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* test(apple): split apple/core index.test.ts into per-module files (#1459)
apple/core/__tests__/index.test.ts had grown to 3,487 lines / 100 test()
call sites across nine unrelated production modules with zero describe()
blocks, mirroring the aggregation disease android already fixed with its
per-concern split. Cut along the existing import boundaries: apps.test.ts,
interactions.test.ts, simulator.test.ts, physical-device-screenshot.test.ts,
screenshot.test.ts, screenshot-status-bar.test.ts, devicectl.test.ts.
config.ts and runner/runner-client.ts have no dedicated file since no test's
primary subject is one of their own exports (their symbols are only
referenced as constants/mocked dependencies inside other modules' tests).
Move-only: every test body is byte-identical to the original apart from
relocated shared setup. Device fixtures and the xcrun-stub harness used by
2+ files moved to a new apple-core-stub-helpers.ts; each split file keeps
only the vi.mock() blocks its own tests actually exercise (narrowed from
the original's blanket five-module mock, verified safe since the dropped
mocks always defaulted to real-passthrough behavior anyway).
Verified 100 test() call sites total across the new files (matches
original), and vitest.config.ts's SUBPROCESS_STUB_TESTS glob updated to
the new file list. Per repo convention, these process-spawn suites are
timeout-flaky under CPU contention; a timeout on an unrelated file is not
a regression, only assertion failures are.
* style: run oxfmt on apps.test.ts
CI's Lint & Format check caught a multi-line import in the new
apps.test.ts header that oxfmt wants collapsed onto one line.
* 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>
R1-R3 were three hand-written predicate functions. Each was short, but each
buried its boundary in control flow: you had to read the early-returns to learn
that R3 tolerates dynamic imports, or that R1 opens exactly one door. They are
now data in scripts/layering/zone-policy.ts -- which zones a boundary governs,
which import kinds it tolerates, which path prefixes are its declared seam --
walked by one small evaluator. A fourth zone boundary becomes a table entry
rather than a fourth predicate to keep consistent with the other three.
R1 is deliberately two entries rather than one with a special case, because
"kernel may import contracts type-only" and "kernel may import nothing else at
all" are two statements, and writing them separately is what makes the single
open door visible.
The refactor exposed a real gap: checkLayeringRules had NO unit test. The only
thing exercising R1-R3 was the real tree, which is clean, so a rule that had
silently stopped matching would have looked exactly like a rule being obeyed.
zone-policy.test.ts now asserts each boundary fires and each documented
exemption holds, including that src/daemon/client/ is excluded from the daemon
seam. Verified end-to-end by injecting one violation per rule plus an exempt
file: the gate reports 4 zone-policy violations (R3 twice, catching the
daemon/client case), ignores the type-only and dynamic edges, and exits 1.
Also records in docs/dependency-graph-findings.md the result of spiking
eslint-plugin-boundaries under oxlint's jsPlugins, so nobody repeats it. It
does work -- jsPlugins loads npm ESLint plugins with no ESLint install, and
R1-R3 are all expressible once you know importKind: "value" and
settings["boundaries/dependency-nodes"]. Not adopted: no ratchet mechanism (the
thing that took R6 from 61 to 7 incrementally), it cannot express R4-R9 so the
architecture would be defined in two places, it misreads inline `{ type Foo }`
specifiers as value imports (one false positive on providers/limrun/android.ts),
message interpolation renders empty under the current selector syntax, and it
costs 230 transitive packages on an API documented as alpha.
No behaviour change: 932 source files, R6 = 7, R7 = 41 fields, R8 clean,
R9 = 102, all identical to before.
Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur
Co-authored-by: Claude <noreply@anthropic.com>
* build(android): unify snapshot/ime helper build+package scripts
Replace the four ~75%-duplicated shell scripts with one parameterized
build script and one parameterized package script, mirroring
scripts/build-xcuitest-apple.sh's env-var-driven pattern. The helper
is selected via AGENT_DEVICE_ANDROID_HELPER or a first positional arg;
per-helper differences (HELPER_DIR/PACKAGE_NAME, snapshot's
test-compile+run step, ime's aapt2 resource-compile step, and the
manifest JSON fields) live in small case blocks.
All package.json entry points keep their names and output paths.
Verified byte-level equivalence between main and this branch: identical
unzip -l listings, identical classes.dex SHA-256 for both helpers, and
identical manifest fields (only the per-signing-run sha256 differs).
Fixes#1461
* chore: drop stale android/multitouch-helper .gitignore entries
The multitouch helper was consolidated away in #1281; these two lines
were never cleaned up.
Replace the 18-branch if-chain in handleSessionCommands with a handler
table keyed by session-routed command name, satisfies-checked against
DescriptorSessionRouteCommandName (derived from command-descriptor
registry entries with route: 'session'). A missing handler entry is now
a compile error instead of a runtime routing gap caught only by
expectHandlerResponse. Command-kind groupings (inventory/state/
observability/replay) map several command names to the same shared
handler reference, preserving the former getSessionCommandKind
clustering without the runtime lookup.
Deletes the fallow-ignore-next-line complexity waiver the if-chain
required. getSessionCommandKind itself is no longer called from
production code (only exercised by its own unit test), so it's added
to the fallow production-exports allowlist alongside the existing
test-only-consumer entries.
Claude-Session: https://claude.ai/code/session_01U7JRr87E2QguHF36XrVbph
Co-authored-by: Claude <noreply@anthropic.com>
* test(replay): split session-replay-vars.test.ts by actual subject (#1460)
session-replay-vars.test.ts contained zero references to
buildReplayBuiltinVars (the module it was named after) — it actually held
tests of replay/vars.ts, replay/script.ts, and runReplayScriptFile. Redistribute
each test to where its subject actually lives:
- vars.ts unit tests move to new src/replay/__tests__/vars.test.ts
- script.ts env-directive/actionLines tests move into script.test.ts (none
were duplicates of existing coverage there)
- runReplayScriptFile tests merge into a new sibling,
session-replay-runtime-maestro.test.ts, since they depend on a
vi.mock('../../../core/dispatch.ts') configuration incompatible with the
one already in session-replay-runtime.test.ts
- one genuine duplicate (`--update` env-directive regression test) is
deleted in favor of the equivalent already in session-replay-runtime.test.ts
No production code changed.
* test(replay): reduce complexity of a moved runReplayScriptFile test
The Fallow Code Quality gate flagged the "dispatches resolved literals
with file env overridden by CLI" test in the new
session-replay-runtime-maestro.test.ts for exceeding the cyclomatic
complexity threshold, since fallow audits it as newly-added code in
this file. Extract the unresolved-interpolation check into a helper
and destructure the captured calls to cut branching, with identical
assertions.
---------
Co-authored-by: Claude <noreply@anthropic.com>
The installed-package test only exercised 3 of 11 export subpaths (io,
metro, and contracts as a bare side-effect import), so a broken subpath
export shipped silently despite external consumers importing all 11.
The consumer script now derives the subpath list from the installed
package.json exports map and runs a named-export smoke invocation per
subpath; a subpath added or removed without updating the smoke table
fails the deepEqual gate. remote-config is type-only at runtime, so
module resolution is its entire check.
* test: enumerate error-code recovery quizzes in a unit-lane gate
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test: mark recovery quizzes structurally and derive retriability from the enumeration
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>
* test(ci): gate PRs on changed-line coverage (#1418)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* refactor: split coverage-changed model into small helpers for fallow
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* refactor(ci): simplify coverage-changed reporting and CLI surface
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>
* chore(deps): add Renovate config and enforce packageManager pnpm version in CI
Refs #1422
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* chore: bump pnpm to 11.17.0 and format the whole repo with oxfmt
format/format:check drop their hand-maintained path list: oxfmt already skips
node_modules and honors .gitignore, so the only exclusion list is
.oxfmtrc.json ignorePatterns.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(mutation): accept either quote style in the affected-lane path filter
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* chore(deps): keep fixture-app runtime deps as individual Renovate PRs
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>
* test: gate public command surface against commands.md reference
Enumerate PUBLIC_COMMANDS against website/docs/docs/commands.md in both
directions with a waivered unit-lane gate, and document the drift found on
main (doctor, react-native).
Refs #1420
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test: recognize tilde-fenced code blocks in command-doc gate
Refs #1420
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>