Skip to content

refactor(settings): declare settings once and admit them per setting - #3662

Merged
thymikee merged 4 commits into
mainfrom
refactor/declarative-settings-admission
Oct 15, 2026
Merged

thymikee merged 4 commits into
mainfrom
refactor/declarative-settings-admission

Conversation

@thymikee

@thymikee thymikee commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Summary

settings now declares every setting once and lets admission check support per setting.

  • One table. contracts/src/settings-catalog.ts holds each setting's forms, app source, app scope and help. CLI/daemon parsing (one parser), usage, the invalid-args message, settingsAppScope, the client type, the MCP setting enum and settings --help derive from it.
  • Per-setting admission. Owners declare settingsRuntimeOperationFacts({ target, unsupported, write, read }); admission checks the requested setting before binding, so a refusal never expires the ref frame. Each owner's fact record is keyed by the setting list its dispatch switch is exhaustive over, so the two cannot drift. This removes the daemon's pre-admission refusals and the Apple leaf guards (ADR 0019 note added).
  • pollUntilSettled in host-kit/retry replaces the Android alert and fill-verification loops.

Public changes (CHANGELOG): an unserved setting answers UNSUPPORTED_OPERATION with the owner's reason, replacing macOS INVALID_ARGS and setting-unsupported-on-leaf; the parser refuses a permission action or mode its target does not take and stray extra arguments; Android alert wait 0 reports a visible alert.

agent-device settings wifi on --platform macos   # UNSUPPORTED_OPERATION, reason unsupported-platform-leaf

51 files, +1480/−1779: over the 2,500-line gross guide; one migration kept as one PR on request, net −299.

Validation

  • pnpm check:affected --run passed at 38bfc3a60.
  • Live run at 38bfc3a60 on an Android 15 emulator, an iPhone 17 Pro simulator (iOS 27.0) and the macOS host; transcript in the PR comments.

🤖 Generated with Claude Code

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 49 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread src/commands/capture/settings.test.ts
Comment thread packages/daemon/src/__tests__/support/snapshot-runtime-fixture.ts Outdated
Comment thread packages/host-kit/src/internal/retry.ts
Comment thread packages/platform-android/src/runtime.test.ts Outdated
Comment thread packages/contracts/src/settings.ts Outdated
Comment thread packages/host-kit/src/internal/retry.test.ts Outdated
Comment thread src/commands/capture/settings.ts
Comment thread packages/platform-apple/src/system/runtime.test.ts
Comment thread packages/platform-apple/src/system/runtime.test.ts
Comment thread packages/platform-android/src/__tests__/fill-verification.test.ts
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-15 18:49 UTC

@thymikee

Copy link
Copy Markdown
Member Author

The settings catalog and per-setting admission look sound in code at e0a7c21, but this PR changes device-facing routes and has no device run yet. CI is pending: all 20 non-passing checks are still queued, none have failed, and the diff overlaps command-docs-gate, Repo Guards/layering, Bundle Size and the Android/iOS smoke routes. I know of no conflicts. Before merge, please post a live run at head.

The Android alert wait/accept/dismiss and fill-verification loops now run on pollUntilSettled (alert.ts:154), and alert wait 0 changes behavior. Every settings request on every target now goes through the new strict daemon parser and per-subject admission. The Apple owner guards that refused text-size and biometrics on tvOS and visionOS are gone, so admission is now the only refusal. Admission is unit-tested only against a condensed copy of the owner facts (snapshot-runtime-fixture.ts:173), never against the shipped owner tables through admitRuntimeOperations. A mismatch between a real owner table and the daemon admission, or a timing change in the sampler, would reach users without a device run. Please post a run on an Android emulator showing: alert wait 0 --json with a dialog already visible returns the alert; alert accept dismisses it; fill @eN hello reports verified text; settings animations off succeeds; settings faceid match returns UNSUPPORTED_OPERATION with reason unsupported-platform-leaf. On an iOS simulator: settings appearance dark and settings text-size large, then bare settings text-size reads large; after snapshot, settings animations off returns UNSUPPORTED_OPERATION and click @eN on the same ref still works, which shows the frame did not expire. On macOS: settings wifi on --platform macos --json returns UNSUPPORTED_OPERATION with reason unsupported-platform-leaf. Moving the pollUntilSettled commit to its own PR and running only the settings part here would also clear this.

Not blocking, and fine to take or leave: commit f11c7a3 (pollUntilSettled, the Android alert and fill rewrites, and their tests) is unrelated to declaring settings, carries its own public change in alert wait 0, and drives much of the churn, so it could be its own PR with its own Android run; ANDROID_ADB_SETTINGS (settings-runtime.ts:145) and every other owner's served list restate that owner's dispatch switch with nothing tying them together, so a rule that each owner derives its served settings from the same handler table keyed by SettingName (with the Android list moving to platform-android) would remove the class; the daemon reads app === 'positional' as "this setting requires an app" (snapshot-settings.ts:276), which turns a wire-encoding detail into a requirement rule, so declaring appRequired in the catalog would be clearer; and parseSettingsMutation trims the state (settings.ts:267) so the daemon now accepts on while the comment at settings.ts:421 still says padding is rejected, so either stop trimming or drop that claim and add a CHANGELOG line.

I ran no tests, and I did not regenerate settings --help or the MCP schema, so I have not compared the derived usage with the website docs. I could not confirm that the new frame-survives-refusal handler tests fail on main. I also did not measure bundle or eager-closure growth from the new contracts subpath.

The cubic-dev-ai threads that still apply are the Limrun owner-list restatement (#3662 (comment)) and the settings-catalog help dropping per-owner --app resolution (#3662 (comment)), plus 11 lower-priority ones. These threads do not apply and can be resolved:

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.24 MB 5.24 MB +5.9 kB
Package (unpacked) 5.24 MB 5.24 MB +5.9 kB
Package (download) 1.57 MB 1.58 MB +1.6 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 22.2 ms 21.9 ms -0.3 ms
CLI --help 60.0 ms 60.4 ms +0.4 ms

@thymikee
thymikee force-pushed the refactor/declarative-settings-admission branch from e0a7c21 to 38bfc3a Compare October 15, 2026 16:21
@thymikee

Copy link
Copy Markdown
Member Author

Rebased on main and pushed 38bfc3a60; pnpm check:affected --run passes there. Live run at that head (fresh pnpm build, build:android, clean:daemon; Android snapshots report android-helper 0.21.29-dev).

Android 15 emulator (ReactNative_API_35)

  • alert wait 0 --json with the system uninstall dialog already up: success, waitedMs: 283, alert Do you want to uninstall this app?. alert dismiss → Alert dismissed. Reopened, alert accept → handled: true, button: OK, and alert get then reports No alert visible.
  • fill @e8 hello → Filled 5 chars with no mismatch warning; the next snapshot shows the field holding hello.
  • settings animations off → success; window_animation_scale and animator_duration_scale read 0, then on restores 1. settings text-size → large; settings wifi TRUE → success.
  • settings faceid match and settings reset-keychain clear → UNSUPPORTED_OPERATION, unsupported-platform-leaf.
  • settings permission deny accessibility → INVALID_ARGS settings permission deny does not apply to accessibility. Use grant|reset.; settings clear-app-state <app> typo → INVALID_ARGS usage.

iPhone 17 Pro simulator, iOS 27.0

  • settings appearance dark, settings text-size extra-large, then bare settings text-size → extra-large; simctl ui agrees (dark extra-large). Restored to light/large, and the read returns large.
  • After snapshot -i: settings animations off and settings fingerprint match → UNSUPPORTED_OPERATION, unsupported-platform-leaf; click @e6 on the same ref → Tapped @e6, which opens General.

macOS host

  • settings wifi on, settings text-size and settings clear-app-state <app> → UNSUPPORTED_OPERATION, unsupported-platform-leaf, with the host hint.
  • settings permission grant accessibility full → INVALID_ARGS takes no mode; --app on it → setting_app_not_consumed.

Response to the non-blocking points:

  • Served lists. Each owner's dispatch now owns its list and switches over it exhaustively (satisfies never). The owner's fact record is typed Record<ThatList, RuntimeOperationFact> through a type-only import, so a missing or extra name on either side fails to compile. ANDROID_ADB_SETTINGS and servedSettings are gone from contracts; Android's list lives in platform-android/src/settings.ts, and Limrun's two legs key off the Android and Limrun iOS lists.
  • appRequired. Declared in the catalog for clear-app-state; the daemon reads it instead of app === 'positional'.
  • State padding. Kept the trim and made the on/off read trim too, so every vocabulary accepts the same padding. The stale comment is gone, and the CHANGELOG says so.
  • Split. Kept as one PR as requested; the Android run above covers the pollUntilSettled loops.
  • Admission coverage. Each owner's runtime test now reads its shipped facts through runtimeSubjectFact, the same lookup admission runs. That covers the Android per-setting facts, the Apple iPadOS row and the refusal reasons. The daemon fixture branches per platform.

@thymikee

Copy link
Copy Markdown
Member Author

The earlier findings at e0a7c21 are fixed. At 38bfc3a one problem remains: the app field text (below) states a rule the Android owner does not follow. All 20 non-passing checks are queued, not failed. They include command-docs-gate, Repo Guards, Bundle Size, Typecheck and the Android and iOS smoke lanes for settings, alert and fill. Before merge, those checks need to go green. The live device transcript is the author's; I did not reproduce it. I ran no tests, did not regenerate the settings --help or MCP output, and did not measure bundle growth from the new settings-catalog subpath. The daemon handler tests still use a hand-written copy of the owner facts, so no single test runs a shipped owner table through admitRuntimeOperations.

Please fix before merge: the app field text in capture/settings.ts dropped main's per-target rules and now says only that a name resolves as open resolves it, yet on Android an alias that open resolves to an intent is refused with setting_app_not_package. The "unchanged from main" reply on that thread does not hold (see line 46 of b6f0703). MCP and SDK callers that read only the input schema get the wrong rule. Restoring the per-target clause, or reusing the sentence the catalog permission help carries, would give the three surfaces one rule.

On the other threads, the app description thread still applies, and the other 21 are fixed or benign, so they can be resolved: r4283187835, r4283187848, r4283187857, r4283187866, r4283187875, r4283187886, r4283187891, r4283187927, r4283187935, r4283187943, r4283187951, r4283187966, r4283187986, r4283187995, r4283188008, r4283188023, r4283188029, r4283188039, r4283188051, r4283188057, r4283188074 and r4283188083, all under #3662 (comment)_ (each is fixed at this head or matches main's behavior).

thymikee and others added 4 commits October 15, 2026 19:47
Add pollUntilSettled to host-kit/retry: it samples a probe until a
settled check accepts or a deadline passes, always takes the first
sample, lands the last one at the deadline, and stops on an aborted
signal. The Android alert wait, alert-dismiss confirmation, and fill
verification use it instead of their own loops, and their tests drop
the cadence assertions the helper's tests now own.

An already-visible Android alert now answers alert wait 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One catalog row per setting (settings-catalog.ts) now carries its forms,
app source, app scope, and help. CLI and daemon parsing, usage, the
invalid-args message, app scope, the MCP setting enum, the public client
type, and settings --help are derived from it.

Owners declare settings support per setting through
settingsRuntimeOperationFacts({ target, unsupported, write, read }), and
admission checks the requested setting's fact before binding. That
replaces the daemon's pre-admission macOS and Apple text-size refusals
and the Apple owner leaf guards: an unserved setting answers
UNSUPPORTED_OPERATION "settings <setting> is not supported on this
device" with the owner's reason and hint, and the ref frame survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thymikee
thymikee force-pushed the refactor/declarative-settings-admission branch from 38bfc3a to 20ffe6f Compare October 15, 2026 18:00
@thymikee

Copy link
Copy Markdown
Member Author

Rebased on main (e8f24cd) and pushed 20ffe6f0b. That fixes the remaining finding: the app field text in src/commands/capture/settings.ts restores main's per-target resolution clause, including Android's setting_app_not_package refusal for an alias that resolves to an intent, plus the list of device-wide changes. pnpm check:affected --run, pnpm lint and pnpm format:check pass at this head. Not changed: the daemon handler tests still use the condensed fixture facts. Shipped owner tables are covered in each owner's runtime test through runtimeSubjectFact.

@thymikee

Copy link
Copy Markdown
Member Author

This PR is ready at 20ffe6f. The earlier findings from 38bfc3a are fixed. The app description in settings --help now carries the per-target rules and the setting_app_not_package refusal, and it matches the Android settings code (src/commands/capture/settings.ts#L39). I only read that help string. I ran no tests and did not regenerate the help or MCP schema output. I also did not reproduce your live device transcript.

Not blocking: the Limrun Android leg in packages/provider-limrun/src/interaction-operations.ts#L369 repeats the AndroidSetting write table and the text-size read table from the local Android runtime, and the daemon test fixture holds a third copy. You could export one write table and one read table from @agent-device/platform-android/mechanics and spread them in each owner, so a per-setting change lands once. You can take this or leave it.

On the open threads, the cubic thread on the app description text no longer applies at this commit, so you can resolve it: #3662 (comment)

No conflicts. No check has failed, but 11 checks are still queued or in progress after the rebase push, and I did not verify your local gate run. Before merge, Repo Guards, Lint & Format, Integration, Smoke and Coverage need to pass.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 15, 2026
@thymikee
thymikee merged commit e2aa9dc into main Oct 15, 2026
25 checks passed
@thymikee
thymikee deleted the refactor/declarative-settings-admission branch October 15, 2026 18:39
thymikee added a commit that referenced this pull request Oct 16, 2026
…ctions-1

* origin/main:
  feat(react-devtools): inspect React components and profile renders through the Metro inspector (#3613)
  refactor(daemon-contracts): drop the registration and claims fixture exports (#3689)
  refactor(test-kit): drop the daemon-contracts dependency (#3686)
  refactor(test-kit): drop command-registry by making the replay-script generator a root fixture (#3687)
  fix(ios): handle delayed launch confirmations without stopping XCTest (#3682)
  refactor(test-kit): move the dispatch-disclosure harness out of contracts (#3690)
  refactor(test-kit): host the target classification capture fixture (#3691)
  ci: fold six CI jobs into four with parallel and background steps; cancel superseded and closed-PR runs (#3658)
  build(typescript): move root project references into tsconfig.typecheck.json (#3688)
  chore(daemon): declare @agent-device/proxy as a devDependency, since only a test imports it (#3685)
  refactor(settings): declare settings once and admit them per setting (#3662)

# Conflicts:
#	packages/platform-android/src/alert.ts
#	website/docs/docs/limrun.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant