Conversation
Each platform coverage manifest already derives its classification summary from its own rows, and the smoke test that owns the manifest then compared that derived value to a hand-incremented literal. Adding one public command therefore cost five files whose only edit was `54` becoming `55`, while the invariant those files actually protect — every public command is classified, and the published rollup is the manifest's own — was asserted nowhere. `assertCoverageClassificationSummaryDerivedFromManifest` now recounts the manifest independently of the builder, ties `total` to the public catalog's size, and checks the buckets still sum. Parity was verified against the deleted literals before removal, as ADR 0008 requires: macOS 22/15/18/55, web 42/1/12/55, Linux 29/9/17/55, tvOS 17/38/0/55, Android 14/0/41/55, all equal to the derived rollup at this head. The web lane's `liveCommands.length === 12` goes the same way: the loop beneath it already proves each live claim is executed by the smoke scenario, so a count proved only that the list was not empty. A planted mis-bucketed summary and a planted denominator drift both fail, naming the platform.
…s it The test enumerated eleven commands to prove that the daemon's fallthrough route is reached on purpose. Ten of those commands already declare the decision as `daemon.route: 'generic'`, which is where `getDaemonCommandRoute` reads it from, so the list was a second source of truth that every new generic command had to be added to. The expected set is now that trait, projected. What remains hand-written is the case the list was actually guarding — a catalog command that declares no daemon facet at all and lands on the `?? 'generic'` fallback — and it is asserted alone, with a message that names the declaration to add instead. Removing `viewport`'s facet locally fails with `['viewport', 'install-from-source']` against that one expected member.
… helper `createUnavailableRuntimeFactsForTest` restated all twenty-seven fail-closed cells, plus the nine lifecycle cells, to say what `createFullyUnavailablePlatformRuntimeFacts` already builds: every cell unavailable for one reason. The two forms are equal because the input's `network` cell carries the same sentinel the omitted optional cells would have inherited. A new runtime cell therefore costs nothing here, which is the point of the fail-closed default: a test that overrides a cell states its own exception, and one that does not inherits the refusal without anyone remembering to write it down.
… as one button family `home`, `app-switcher` and `action-button` are the same shape end to end: one press, no arguments, nothing returned, and only the set of owners that carry the control differs. The action-button PR (#2702) paid for that shape as if it were new: a contracts group with no facts helper, a required `Interactor` member every owner had to stub, a third identical daemon module with its own dispatcher arm and conformance row, and a refusal cell restated in every owner and every fixture that enumerates `RuntimeFacts`. One `system-button-runtime` contracts module now owns the family. Its facts helper takes the keyboard family's shape (`unsupported` plus optional per-button cells), so an owner with no such control states one denial and a button added to `SYSTEM_BUTTONS` is refused there without an edit; `UnavailablePlatformRuntimeFacts` carries one `systemButton` family cell the same way it carries `keyboard`. `Interactor.actionButton` is optional like `keyboardDismiss`: facts admit the press only where the interactor implements it, and the binder fails closed on a missing member instead of resolving it as a successful no-op, so the seven throwing stubs are gone. On the daemon side one table row per command replaces the three byte-identical modules, the three dispatcher arms and the three conformance rows; the conformance table and its test derive the command list from the registry. The keyboard family keeps its private method guard: the eager-closure gate holds that entry at its merge-base closure, and importing the shared guard would grow it by one module. Refusal hints for the buttons an owner does not carry now name the family rather than the one button (`Android has no key event for this system button.`); the command name already leads the UNSUPPORTED_OPERATION message. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
… evidence A command whose only input is device selection (`appstate`, `home`, `app-switcher`, `action-button`) restated the same metadata, empty CLI schema, reader and daemon writer four times over. `defineParameterlessCommandFacet` derives them from the name, so such a facet is its name, description, text, `run` and formatter. The readers and writers it replaces were exported for the family test only; the test now reads them through the family. The coverage declarations cited three test names by literal in eighteen rows. They are named evidence in `evidence.ts` now, the same way the Linux and tvOS rows already cite theirs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
…on module `home-runtime` and `app-switcher-runtime` leave the pinned export set and `system-button-runtime` joins it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
The fail-closed test facts now derive from `createFullyUnavailablePlatformRuntimeFacts`, which absorbs the `systemButton` family cell this branch had named there by hand. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
…s facet The provider-coverage scanner maps `client.command.<method>(` calls in provider scenarios to a command name by reading the facet block that declares the method. It recognised `defineCommandFacet` blocks only, so the system commands that moved onto `defineParameterlessCommandFacet` lost their mapping and `app-switcher` read as uncovered. The scanner now reads both block forms; the new model test is red without it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
#2713 landed on main with two commits beyond the head this branch had merged; its own files take main's version. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
Size Report
Startup median (7 runs, lower is better):
|
|
The iOS The failure is I have re-run the failed job once. If it fails again I will treat it as real and dig into the scenario. Generated by Claude Code |
|
The re-run of iOS
My one re-run is spent. The other 17 checks are green. A maintainer re-run of the iOS job is the next step; I will keep the PR watched and treat a further failure with any new signature as this PR's. Generated by Claude Code |
|
This is a good refactor with two blocking issues, reviewed at e0dba23.
Is there a smaller design than this family shape? I did not find one: the PR is net -190 production lines, the required- The CI failure ( The author's report that unit, layering, fallow and provider-integration gates pass on e0dba23 is unverified by me. The Not blocking: the eight tracked replay artifacts under .agent-device/test-artifacts changed only in local tmpdir paths and timing jitter and can be checked out back from c442cf6, the provider-webdriver and Limrun Before merge, route the keyboard binder through the shared |
…ional-member guard `requireKeyboardMethod` was a verbatim copy of `requireInteractorMethod`. The keyboard binder now uses the shared guard, loaded on the call rather than at module evaluation: the `keyboard-runtime` facade's eager closure is held at its merge-base size by `eager-closure-budgets`, and a static edge to the binding module would grow it by one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
…son change The eight Maestro replay artifacts under `.agent-device/test-artifacts` had been swept into the first commit by a local test run; they return to main's bytes. The CHANGELOG entry now also names the Limrun and WebDriver inactive-session refusal that moved onto the family cell. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1
|
Addressed at Shared guard. Artifacts. The eight replay artifacts were swept into the first commit by a local test run; Live run. One correction to the premise: the CI iOS lane pins plus Not taken. Generated by Claude Code |
|
CI is green on What remains is on the maintainer side: the full-tier simulator run for Generated by Claude Code |
|
Reviewed at c020e10, as a follow-up to the review at e0dba23 (#2715 (comment)). The shared guard is fixed. The live run is still missing. This PR puts All 18 checks pass on c020e10. The earlier Smoke Tests failure on e0dba23 was in the long-press scroll search, which this delta does not touch. I did not run the eager-closure-budgets gate myself; I take the green CI as confirmation that keyboard-runtime.ts keeps its merge-base closure size. Not blocking: the 3-line comment at keyboard-runtime.ts#L113 explains the lazy import as a workaround for the closure budget. Would moving The remaining step before merge is that full-tier live run on c020e10. |
|
Thanks. On the live run: Dispatched. Android already has it on this head. The Android
Keyboard binder. Yes: moving Generated by Claude Code |
|
Replay Manual on
To reach the E2E step without touching this PR, I dispatched the workflow again from a scratch branch, Generated by Claude Code |
|
Full-tier results from the second dispatch, run 35554714567 on the scratch branch ( Android Full Emulator Suite: green again, the same steps as the first run. iOS: So every smoke scenario (including the The Housekeeping. The scratch branch Still yours: the single Generated by Claude Code |
Summary
Follow-up to #2702 and #2713. 65 files, +851/−983 over main.
home,app-switcherandaction-buttonare one shape end to end (one press, no arguments, nothing returned), and #2702 paid for that shape as if it were new: a refusal cell restated in every owner and fixture, a requiredInteractormember stubbed by every owner without the hardware, and a third byte-identical daemon module with its own dispatcher arm and conformance row.packages/contracts/src/system-button-runtime.tsowns the family and replaceshome-runtimeandapp-switcher-runtime. Its facts helper takes the keyboard family's shape (unsupportedplus optional per-button cells), so a button added toSYSTEM_BUTTONSis refused by every owner that has not named it, with no edit there.UnavailablePlatformRuntimeFactscarries onesystemButtoncell likekeyboard.Interactor.actionButtonis optional likekeyboardDismiss; the binder fails closed on a missing member throughrequireInteractorMethod, which the keyboard binder now shares. Seven throwing stubs are deleted.src/daemon/system-button-runtime.tsis one table row per command, replacing three modules, three switch arms and three conformance rows; tests derive the command list from the registry.defineParameterlessCommandFacetderives metadata, CLI schema, reader and writer for the four parameterless system commands; the coverage rows cite named evidence instead of repeated literals.No public API change. Refusal hints for a button an owner lacks now name the family (CHANGELOG). The next system button touches about 28 files instead of 72.
Gross churn is above the 1,000-line budget: three modules fold into one. The branch can split into contracts+owners, daemon, and CLI+coverage if preferred.
Validation
Tested at
c020e10.pnpm check:affected --runselected 59 checks. Every locally runnable gate passes: format, lint, typecheck, layering (246), di-seams, fallow, mcp-metadata, build, package, integration-node, macos-coverage, unit and smoke (70), provider-integration (66 files, 211 tests), integration-progress, replay-compat, daemon-wire-compat, eager-closure budgets, production-exports, bundle-owner-files, command-docs, agent-guidance, gate-manifest, tmpdir-leaks.app-log-session-resource,durable-capture/adoption,durable-capture-resourceeachchmod 0o500a directory and expect a write to fail; the container runs as root. Unrelated to the diff.defineCommandFacetblocks, soapp-switcherbriefly read as uncovered; thechore(gates)commit widens it, with a model test that was red before the fix.smoketier only, andhome/app-switcherare exercised byfull:lifecycle-system, so the changed device path needs a full-tier simulator run before merge (command in the review thread). The smoke lane'sautomation-longpressfailure one0dba23is the lane's known scroll-search instability and does not reach this diff.🤖 Generated with Claude Code
https://claude.ai/code/session_014x96iYiFTbAwD4YEKU3PQ1