Skip to content

fix(ios): confirm foldable rotation from the lit panel in either simulator set (#4343) - #4440

Merged
thymikee merged 16 commits into
callstack:mainfrom
sittingmongoose:fix/foldable-scoped-rotation
Nov 13, 2026
Merged

thymikee merged 16 commits into
callstack:mainfrom
sittingmongoose:fix/foldable-scoped-rotation

Conversation

@sittingmongoose

@sittingmongoose sittingmongoose commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #4343: rotate a multi-panel simulator through guest HID and confirm the requested orientation from a fresh established lit panel in either simulator set. Incomplete, unknown-power and multi-lit tables cannot dispatch or confirm. Picker offsets were measured on iPhone Duo; a new multi-panel model needs its own mapping.

Panel-count classification uses the set source: CoreDevice for default simulators, guest for scoped simulators. Pose pre-checks use that source too; separate lit-panel reads explicitly request the guest table. Single-panel default rotation prepares no helper. Transient unresolved guest reads retry inside the settle loop without dispatch. An unresolved final read reports orientation-panel-table-unavailable with a helper/display-table diagnostic hint.

18 files changed. Scope includes the controller, routing, docs and regression/provider fixtures. No model-name guard.

Validation

Head 83a4ebb49c043382bda6c927bc554ccb8edf8d64:

  • Focused foldable/display inventory unit tests: 187 passed, 2 skipped. Fold provider scenarios: 7 passed.
  • Typecheck, repository lint, full format and format check pass.
  • Command docs, ADR citations, documentation prose and guidance checks pass; guidance is 39,961 / 40,000 bytes.
  • Maintainer's guard-drop and retry suggestions implemented; both follow-up threads resolved. Self-review only; no device work this round.

No full gate rerun requested. The prior gate at 94cf7fd is base-blocked by the matching upstream smoke failure. Earlier live Duo evidence predates this head. Fork CI needs maintainer approval; fresh review remains pending.

@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 11 files

Reply to a comment to ask cubic a question or push back. It learns from your replies.

View guided diff | Re-trigger cubic

Comment thread website/docs/docs/commands.md Resolved
Comment thread .changeset/foldable-scoped-rotation.md Resolved
@thymikee

Copy link
Copy Markdown
Member

The review of 7caf447 found two issues that need attention before merge.

The shipped scoped rotation route and native helper have not been demonstrated together from this build (orientation.ts). The reported measurements used modified release bundles, so they do not establish that this head admits, binds, packages, and runs the helper. Please build this head and Fold.m, start a fresh daemon from that build, and run the ordinary CLI with --ios-simulator-device-set and the scoped Duo UDID. Record the SHA, build, commands, exit statuses, scoped helper arguments and outputs. Exercise portrait and both landscapes on both panels, inner landscape-left pud dispatch, a repeated already-showing target, and rotation immediately after folding. Show the sole lit panel's currentOrientation and screenshots; ambiguous reads must not confirm success, and an unachievable target must report orientation-unverified-scoped-set. Record the same scoped command on the base, where it refuses, and show that neither devicectl nor the default set runs.

The panel orientation map duplicates the existing display inventory parser (picker-delta.ts). The two owners can diverge when orientations change. Please extract one panel-content-orientation parser in display-inventory.ts, use it for appleDisplayRotation and the foldable controller, and remove the copied map and lookup while keeping picker compensation in the foldable module.

Not blocking: you can take or leave the suggestion to derive the offset from the last dispatched picker and a fresh same-panel observation, use the measured offset only before calibration, and omit persistent panelOffset while preserving unreadable-read waits, panel-change reset, and final confirmation.

Could the existing scoped address and panel inventory machinery use the extracted parser with one bounded rotation loop and one target-minus-offset calculation, deriving calibration from the last dispatch and a fresh same-panel observation? The scoped loop and pud support have measurement support; extracting the parser and removing the superseded offset branch would preserve ADR 0025's ownership without an ADR change.

Six lower-priority open threads still apply.

CI reports one passing check and no failing ones, probably because the fork run is waiting for maintainer approval; it does not establish native compilation, device lanes, or an exact-head check:affected receipt. No conflicts are known. Before merge, please remove the duplicate parser, provide the scoped Duo evidence above, and record the exact-head check:affected receipt.

sittingmongoose added a commit to sittingmongoose/agent-device that referenced this pull request Nov 11, 2026
…d foldable rotation

Review of callstack#4440:
- display-inventory owns the only currentOrientation parser
  (panelContentRotation); appleDisplayRotation and the scoped foldable
  rotation both use it, and picker-delta drops its copied map.
- The scoped loop has one target-minus-offset step. The offset comes from
  this command's last dispatch and a fresh reading of the same panel, and
  from the measured table only before that. The persistent panelOffset and
  the separate delta branch are gone.
- A read that names no single lit panel neither confirms nor dispatches;
  there is no blind calibration dispatch any more.
- The unverified error names only what the final read saw.
- Tests assert exact dispatch sequences; the measurement fixture keeps the
  one contradicting reading, marked as the exception.
- Docs, help text and changeset name the failure and qualify the model.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@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.

2 issues found across 10 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/platform-apple/src/foldable/orientation.ts">

<violation number="1" location="packages/platform-apple/src/foldable/orientation.ts:217">
P2: The offset learned from a same-panel reading assumes the last dispatch took effect. If the panel still shows the pre-dispatch orientation, the offset is wrong and the next dispatch can be `portrait-upside-down`, which is not a user target. Consider keeping the measured offset when the reading does not move from the value before the dispatch, or document the side effect.</violation>
</file>

<file name="packages/platform-apple/src/core/config.ts">

<violation number="1" location="packages/platform-apple/src/core/config.ts:71">
P3: This comment says the attempt budget and settle delay were "Verified on iPhone Duo", but the PR description says that run used a port of this branch's logic in released bundles, not a build of this branch. Qualify the claim so the doc does not read as verification of this head.</violation>
</file>

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

View guided diff | Re-trigger cubic

@sittingmongoose

Copy link
Copy Markdown
Contributor Author

Exact-head device evidence and review follow-up (head 09b2765b2)

Summary

  • Parser refactor: one parser and one offset step.
  • Six cubic threads: fixed, replied and resolved.
  • Device run: this head, built and packed, ran on an iPhone Duo in a scoped set through the ordinary CLI and a fresh daemon. Base refuses the same command.
  • check:affected: receipt below.

1. Review changes in 09b2765b2

  • One parser. display-inventory.ts exports panelContentRotation(currentOrientation).
    • appleDisplayRotation and the scoped foldable rotation both use it.
    • The copied map and panelRotationForOrientation are gone from picker-delta.ts.
    • Picker compensation stays in the foldable module.
  • One offset step (your optional suggestion, taken). The loop has one bounded calculation, pickerValueForTarget(target, offset) = target - offset.
    • After a dispatch to the same panel: the offset comes from the last dispatched picker value and a fresh reading of that panel.
    • Before any dispatch, or after a fold lit another panel: the measured offset applies (outer 0, inner +90).
    • The persistent panelOffset and the separate delta branch are removed.
  • No dispatch without a reading. A read that names no single lit panel neither confirms nor dispatches; the loop waits for the next read. The old blind calibration dispatch is gone. The panel-change reset and the final confirming read are kept.
  • Error from the final read only. The orientation-unverified-scoped-set error reports only what the final read saw. An undecided final read says so and omits observedOrientation and rawPanelOrientation.

2. Build

Head Base (merge-base)
Commit 09b2765b2830532d3008419ebaa7cfcf602023b6 a8774f3632eb432a1dadbde8627da30586f9ee39
Build pnpm install --frozen-lockfile --offline && pnpm build (tsdown, 670 files) in a clean worktree same
Package npm pack --ignore-scripts → agent-device-0.21.37-dev.tgz, sha256 b2006df9…7c2677 sha256 c87e0b4e…57d69d
apple/fold-helper/Fold.m in the tarball sha256 4f78df1f…62d194, identical to the head source stock
  • Packing. The tarball was packed with --ignore-scripts, so release:prepare and the XCUITest runner packaging did not run. None of the commands below need the runner.
  • Host. macOS 27.3 arm64, Xcode 27.3, iOS 27.3 runtime, Node 25.6.1.
  • Duo. An iPhone Duo simulator in a scoped device set, from a local simulator pool.
  • Daemon isolation. Each build ran from its own unpacked folder, with its own fresh daemon, AGENT_DEVICE_STATE_DIR and AGENT_DEVICE_HOME.
  • Call log. PATH was prefixed with a logging xcrun shim. It records every argv and captures the stdout and exit status of fold-helper calls; it is otherwise a pass-through.

Helper build by the head daemon:
xcrun --sdk iphonesimulator clang -mios-simulator-version-min=15.0 -fobjc-arc -Wall -Wextra -framework Foundation -framework IOKit <package>/apple/fold-helper/Fold.m -o ~/.agent-device/fold-helper/.950f94d1….tmp/fold-helper → exit 0.
Every head step below runs that helper, fold-helper/950f94d11269d473a277638fe11c5be7.

Command form, for every step:
node <package>/bin/agent-device.mjs <command> --platform ios --udid 68C534CE-922D-4F27-AEA0-1CA29EF33999 --ios-simulator-device-set <scoped set> --json
It runs from one working directory, after open com.apple.mobilesafari with the same flags.

3. Head: scoped Duo results

Lit panel: LCD = outer (primary: true), LCD-1 = inner.
The helper column lists the fold-helper argv after the binary, in order. Every call exited 0.

Step Command Exit Helper calls → lit panel currentOrientation Screenshot
H1 orientation landscape-left (closed) 0 panels (routing) → LCD rot0; panels → LCD rot0; rotate landscape-left; panels → LCD rot270 678x466
H2 orientation landscape-left (repeat) 0 panels → LCD rot270; no rotate 678x466
H3 orientation landscape-right 0 panels → LCD rot270; rotate landscape-right; panels → LCD rot90 678x466
H4 orientation portrait 0 panels → LCD rot90; rotate portrait; panels → LCD rot0 466x678
H5 fold open 0 panels; set open; hinge 0 → 180°, valid; panels → LCD and LCD-1 both activeOn –
H6 orientation portrait (inner) 0 panels → both lit: no confirm, no dispatch; panels → LCD-1 rot90 (LCD off); rotate landscape-left; panels → LCD-1 rot0 669x951
H7 orientation landscape-left (inner) 0 panels → LCD-1 rot0; rotate portrait-upside-down (sent as pud); panels → LCD-1 rot270 951x669
H8 orientation landscape-right (inner) 0 panels → LCD-1 rot270; rotate portrait; panels → LCD-1 rot90 951x669
H9 fold closed 0 panels; set closed; hinge 0 → 0°, valid; panels → both activeOn –
H10 orientation landscape-left, right after H9 0 panels → both lit; panels → both lit; panels → LCD rot0 (LCD-1 off); rotate landscape-left; panels → LCD rot270 678x466
H11 open com.apple.Preferences (portrait-only on the outer panel) 0 – –
H12 orientation landscape-right with Settings in front 1 panels → LCD rot0, then six rounds of rotate (landscape-right, portrait-upside-down, landscape-left, portrait, landscape-right, portrait-upside-down), each followed by panels → LCD rot0 466x678

H12 error:
COMMAND_FAILED, "did not confirm rotation to landscape-right: the lit panel shows portrait", details {"requestedOrientation":"landscape-right","observedOrientation":"portrait","rawPanelOrientation":"rot0","reason":"orientation-unverified-scoped-set"}.

Reads that named two lit panels never confirmed and never dispatched:

  • in H6, right after unfolding;
  • in H10, right after folding closed.

On this runtime the table names both panels lit for about 2–4 s after a pose change in either direction, not only after folding closed (the readLitPanel comment mentions only the closing case; the code makes no such distinction).

Raw helper output, first H10 read:

simctl --set <scoped set> spawn 68C534CE-922D-4F27-AEA0-1CA29EF33999 <helper> panels
{"version":1,"displays":[{"primary":true,"currentOrientation":"rot0","pointScale":3,"backlightState":"activeOn","nativeSize":[1398,2034],"type":{"integrated":{}},"name":"LCD","displayId":1},{"primary":false,"currentOrientation":"rot90","pointScale":3,"backlightState":"activeOn","nativeSize":[2007,2853],"type":{"integrated":{}},"name":"LCD-1","displayId":3}]}

Call counts:

  • Head run: 72 xcrun calls, all simctl --set <scoped set>; 0 devicectl; 0 default-set simctl; 40 panels; 13 rotate.
  • Base run (below): 9 calls; 0 devicectl; every simctl names --set.
H1 outer landscape-left H2 repeat (no dispatch) H3 outer landscape-right H4 outer portrait
H1 H2 H3 H4
H6 inner portrait H7 inner landscape-left (pud) H8 inner landscape-right
H6 H7 H8
H10 right after folding closed H12 unachievable target (Settings stays portrait)
H10 H12

GitHub has no API for comment attachments, so the screenshots are hosted on an images-only branch of the fork.

Full head helper log (sanitized; … elides home paths)

=== STEP H0-open-safari 19:50:00.000Z agent-device open com.apple.mobilesafari
  19:50:01.334Z simctl --set …/ios launch 68C534CE-922D-4F27-AEA0-1CA29EF33999 com.apple.mobilesafari
=== END H0-open-safari rc=0 19:50:02.000Z

=== STEP H1-outer-landscape-left 19:50:02.000Z agent-device orientation landscape-left
  19:50:02.573Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:50:02.993Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:50:03.270Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-left  -> exit 0
  19:50:04.182Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
=== END H1-outer-landscape-left rc=0 19:50:08.000Z

=== STEP H1-shot 19:50:08.000Z agent-device screenshot shots/H1-outer-landscape-left.png
  19:50:08.529Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
  19:50:08.815Z simctl --set …/ios io 68C534CE-922D-4F27-AEA0-1CA29EF33999 screenshot --display=LCD …/H1-outer-landscape-left.png
=== END H1-shot rc=0 19:50:09.000Z

=== STEP H2-outer-landscape-left-repeat 19:50:09.000Z agent-device orientation landscape-left
  19:50:09.774Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
=== END H2-outer-landscape-left-repeat rc=0 19:50:10.000Z

=== STEP H2-shot 19:50:10.000Z agent-device screenshot shots/H2-outer-landscape-left-repeat.png
  19:50:10.552Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
  19:50:10.952Z simctl --set …/ios io 68C534CE-922D-4F27-AEA0-1CA29EF33999 screenshot --display=LCD …/H2-outer-landscape-left-repeat.png
=== END H2-shot rc=0 19:50:11.000Z

=== STEP H3-outer-landscape-right 19:50:11.000Z agent-device orientation landscape-right
  19:50:12.070Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
  19:50:12.358Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-right  -> exit 0
  19:50:13.268Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot90) LCD-1(primary=False,off,rot90)
=== END H3-outer-landscape-right rc=0 19:50:13.000Z

=== STEP H3-shot 19:50:13.000Z agent-device screenshot shots/H3-outer-landscape-right.png
  19:50:14.011Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot90) LCD-1(primary=False,off,rot90)
  19:50:14.469Z simctl --set …/ios io 68C534CE-922D-4F27-AEA0-1CA29EF33999 screenshot --display=LCD …/H3-outer-landscape-right.png
=== END H3-shot rc=0 19:50:15.000Z

=== STEP H4-outer-portrait 19:50:15.000Z agent-device orientation portrait
  19:50:15.814Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot90) LCD-1(primary=False,off,rot90)
  19:50:16.167Z simctl --set devices/ios spawn <udid> fold-helper rotate portrait  -> exit 0
  19:50:17.133Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
=== END H4-outer-portrait rc=0 19:50:17.000Z

=== STEP H4-shot 19:50:17.000Z agent-device screenshot shots/H4-outer-portrait.png
  19:50:17.836Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:50:18.162Z simctl --set …/ios io 68C534CE-922D-4F27-AEA0-1CA29EF33999 screenshot --display=LCD …/H4-outer-portrait.png
=== END H4-shot rc=0 19:50:18.000Z

=== STEP H5-fold-open 19:50:32.000Z agent-device fold open
  19:50:33.156Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:50:33.465Z simctl --set devices/ios spawn <udid> fold-helper set open  -> exit 0
  19:50:33.746Z simctl --set devices/ios spawn <udid> fold-helper hinge 0  -> exit 0  {"version":1,"angleDegrees":180,"angleValid":true}
  19:50:34.206Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,activeOn,rot90)
=== END H5-fold-open rc=0 19:50:34.000Z

=== STEP H6-inner-portrait 19:50:34.000Z agent-device orientation portrait
  19:50:34.980Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,activeOn,rot90)
  19:50:36.097Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot90)
  19:50:38.583Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-left  -> exit 0
  19:50:42.563Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot0)
=== END H6-inner-portrait rc=0 19:50:42.000Z

=== STEP H6-shot 19:50:43.000Z agent-device screenshot shots/H6-inner-portrait.png
  19:50:43.307Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot0)
  19:50:43.595Z simctl --set …/ios io 68C534CE-922D-4F27-AEA0-1CA29EF33999 screenshot --display=LCD-1 …/H6-inner-portrait.png
=== END H6-shot rc=0 19:50:44.000Z

=== STEP H7-inner-landscape-left 19:50:44.000Z agent-device orientation landscape-left
  19:50:44.659Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot0)
  19:50:44.931Z simctl --set devices/ios spawn <udid> fold-helper rotate portrait-upside-down  -> exit 0
  19:50:45.835Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot270)
=== END H7-inner-landscape-left rc=0 19:50:46.000Z

=== STEP H7-shot 19:50:46.000Z agent-device screenshot shots/H7-inner-landscape-left.png
  19:50:46.425Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot270)
  19:50:46.736Z simctl --set …/ios io 68C534CE-922D-4F27-AEA0-1CA29EF33999 screenshot --display=LCD-1 …/H7-inner-landscape-left.png
=== END H7-shot rc=0 19:50:47.000Z

=== STEP H8-inner-landscape-right 19:50:47.000Z agent-device orientation landscape-right
  19:50:47.681Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot270)
  19:50:47.941Z simctl --set devices/ios spawn <udid> fold-helper rotate portrait  -> exit 0
  19:50:48.820Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot90)
=== END H8-inner-landscape-right rc=0 19:50:49.000Z

=== STEP H8-shot 19:50:49.000Z agent-device screenshot shots/H8-inner-landscape-right.png
  19:50:49.402Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot90)
  19:50:49.685Z simctl --set …/ios io 68C534CE-922D-4F27-AEA0-1CA29EF33999 screenshot --display=LCD-1 …/H8-inner-landscape-right.png
=== END H8-shot rc=0 19:50:50.000Z

=== STEP H9-fold-closed 19:50:50.000Z agent-device fold closed
  19:50:50.610Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot90)
  19:50:50.927Z simctl --set devices/ios spawn <udid> fold-helper set closed  -> exit 0
  19:50:51.189Z simctl --set devices/ios spawn <udid> fold-helper hinge 0  -> exit 0  {"version":1,"angleDegrees":0,"angleValid":true}
  19:50:51.679Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,activeOn,rot90)
=== END H9-fold-closed rc=0 19:50:52.000Z

=== STEP H10-outer-landscape-left-after-fold 19:50:52.000Z agent-device orientation landscape-left
  19:50:52.609Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,activeOn,rot90)
  19:50:53.698Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,activeOn,rot90)
  19:50:54.829Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:50:56.098Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-left  -> exit 0
  19:50:57.857Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
=== END H10-outer-landscape-left-after-fold rc=0 19:50:58.000Z

=== STEP H10-shot 19:50:58.000Z agent-device screenshot shots/H10-outer-landscape-left-after-fold.png
  19:50:58.905Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
  19:50:59.574Z simctl --set …/ios io 68C534CE-922D-4F27-AEA0-1CA29EF33999 screenshot --display=LCD …/H10-outer-landscape-left-after-fold.png
=== END H10-shot rc=0 19:51:00.000Z

=== STEP H11-open-settings 19:51:15.000Z agent-device open com.apple.Preferences
  19:51:15.749Z simctl --set …/ios launch 68C534CE-922D-4F27-AEA0-1CA29EF33999 com.apple.Preferences
=== END H11-open-settings rc=0 19:51:19.000Z

=== STEP H11-shot 19:51:19.000Z agent-device screenshot shots/H11-outer-settings.png
  19:51:20.291Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:51:20.816Z simctl --set …/ios io 68C534CE-922D-4F27-AEA0-1CA29EF33999 screenshot --display=LCD …/H11-outer-settings.png
=== END H11-shot rc=0 19:51:21.000Z

=== STEP H12-outer-settings-landscape-right 19:51:21.000Z agent-device orientation landscape-right
  19:51:22.243Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:51:22.931Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-right  -> exit 0
  19:51:24.163Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:51:24.520Z simctl --set devices/ios spawn <udid> fold-helper rotate portrait-upside-down  -> exit 0
  19:51:25.493Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:51:25.777Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-left  -> exit 0
  19:51:26.677Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:51:26.953Z simctl --set devices/ios spawn <udid> fold-helper rotate portrait  -> exit 0
  19:51:27.847Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:51:28.093Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-right  -> exit 0
  19:51:29.046Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:51:29.358Z simctl --set devices/ios spawn <udid> fold-helper rotate portrait-upside-down  -> exit 0
  19:51:30.720Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
=== END H12-outer-settings-landscape-right rc=1 19:51:31.000Z

=== STEP H12-shot 19:51:31.000Z agent-device screenshot shots/H12-outer-settings-landscape-right-unverified.png
  19:51:31.371Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  19:51:31.725Z simctl --set …/ios io 68C534CE-922D-4F27-AEA0-1CA29EF33999 screenshot --display=LCD …/H12-outer-settings-landscape-right-unverified.png
=== END H12-shot rc=0 19:51:32.000Z

=== STEP H13-close 19:52:14.000Z agent-device close
=== END H13-close rc=0 19:52:15.000Z

ISSUES: none (no devicectl call; every simctl call names --set)

4. Base a8774f3: the same scoped command refuses

Step Command Exit Helper calls
B0 open com.apple.mobilesafari 0 none (simctl --set … launch)
B1 orientation landscape-left 1 panels (routing) → LCD rot0; no rotate

B1 error:
UNSUPPORTED_OPERATION, "cannot rotate in a scoped simulator set", details {"reason":"unsupported-device-scope"}.
No devicectl call and no default-set simctl call ran.

Base helper log
=== STEP B0-open-safari 19:48:03.000Z agent-device open com.apple.mobilesafari
  19:48:03.595Z simctl --set …/ios list -j
  19:48:04.437Z simctl --set …/ios launch 68C534CE-922D-4F27-AEA0-1CA29EF33999 com.apple.mobilesafari
  19:48:04.840Z simctl --set …/ios list devices -j
  19:48:06.223Z xcrun --sdk iphonesimulator clang … apple/snapshot-bridge/*.m (snapshot bridge build)
=== END B0-open-safari rc=0 19:48:08.000Z

=== STEP B1-orientation-landscape-left 19:48:13.000Z agent-device orientation landscape-left
  19:48:13.842Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
=== END B1-orientation-landscape-left rc=1 19:48:15.000Z

ISSUES: none (no devicectl call; every simctl call names --set)

5. check:affected receipt (exact head)

node --experimental-strip-types scripts/check-affected/run.ts --base upstream/main --run

  • Receipt: exactCommit: true, resolvedHead 09b2765b2830532d3008419ebaa7cfcf602023b6, merge-base a8774f3632eb, checkout unchanged.
  • 21 checks selected; 8 skipped as GitHub-authoritative or parked.

The receipt itself ends failed at unit, so the checks after it were run separately on the same head:

Check Result
format, lint, typecheck, layering, fallow, native-dupes, build passed
vitest-related passed (needs zip on PATH, which the Linux host lacked on the first try)
unit (check:unit) failed on this Linux host, same as base.
• Receipt run: 20 of 19,067 failed; it overlapped with my parallel worktree builds.
• Quiet rerun: 4 of 19,067 failed.
• Isolation: 3 of those 4 (snapshot-producer-emitters ×2 at 5 s timeouts, downloadable-artifact-http) pass when run alone at this head.
• Remaining failure: macos-helper-package-contents times out at 5 s here and fails the same way on base a8774f3 (test:unit on base: 1 failed, that test).
provider-integration (test:integration:provider) passed, 257/257 (run separately: the receipt stops at its first failure)
command-docs, docs-prose passed (run separately)
docs-links passed on Node 26.13.0; the host's Node 22.22 is below Astro's >=22.23.4 (run separately)
coverage, web-smoke, replay-ios, replay-ios-device, replay-macos, replay-linux, linux-command-evidence, replay-android skipped by check:affected (GitHub-authoritative or parked)

Limits

  • One device type (iPhone Duo) and one runtime (iOS 27.3 / Xcode 27.3).
  • The package was not built with release:prepare (no XCUITest runner packaging).
  • The unachievable case uses six bounded rounds (about 8 s) and leaves the picker at the last dispatched value.
  • The host was shared and under load during the run.

sittingmongoose added a commit to sittingmongoose/agent-device that referenced this pull request Nov 11, 2026
…move

Review of callstack#4440 (cubic): the offset re-derived from the last dispatch and
a same-panel reading assumed that dispatch had landed. A reading taken
before it landed, or a panel whose app cannot show the target, taught a
wrong offset; on the device a portrait-only app made the loop cycle
through six picker values, upside-down included. A reading that still
shows what the panel showed before the last dispatch now teaches nothing
and gets no further dispatch: the loop re-reads, and a panel that never
follows fails unverified after one dispatch.

Also qualify the attempt-budget comment and note that both panels report
lit for a few seconds after an unfold as well as a fold.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@sittingmongoose

sittingmongoose commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Head is now c75affacf: fix for the two new cubic threads

Update: the exact-head device run for c75affacf is posted in the next comment.

cubic's re-review of 09b2765b2 found a real flaw. The loop derived an offset from a same-panel reading even when that panel had not moved since the last dispatch: either the dispatch had not landed yet, or the app in front cannot show the orientation. Step H12 of the device run above shows it: with portrait-only Settings in front, the loop cycled through six picker values.

c75affacf (on top of 09b2765b2):

  • No dispatch on an unmoved reading. The loop records what the lit panel showed at each dispatch. A same-panel reading that still shows it teaches no offset and gets no further dispatch; the loop re-reads. A panel that never follows now fails orientation-unverified-scoped-set after one dispatch.
  • Tests. A new test covers a dispatch that lands late (stale reads in between: one exact dispatch, no re-learning). The never-follows and undecided-final-read tests now expect a single dispatch.
  • Comments and changeset. The attempt-budget comment is qualified. The both-lit comment now covers unfolding as well as folding. The changeset mentions the new rule.
  • Diff. 4 files: orientation.ts, orientation.test.ts, config.ts (comment) and the changeset.

What the 09b2765b2 device run still shows about this head. The new rule only acts when a reading after a dispatch equals the reading before it.

  • H1–H10: every dispatch moved the lit panel before the next read (rot0→rot270, rot270→rot90, rot90→rot0, rot90→rot0, rot0→rot270, rot270→rot90, rot0→rot270). The rule never fires there, so c75affacf takes the same decisions on the same readings.
  • H12: this is the only step whose behaviour changes, from six dispatches to one.

I'll still post an exact-head run of c75affacf, built and packed the same way. Its tarball is ready (sha256 1495c66c…40b283).

Why that run isn't here yet. The simulator host's pool can't boot a fresh iPhone Duo at the moment. Four leases in a row (20:15–20:46 UTC) were declined with boot-timeout: each new device's first boot ran past the pool's boot deadline, and the device was then deleted.

check:affected at c75affacf (--base upstream/main --run)

Both receipts: exactCommit: true, resolvedHead c75affacf5cc…, merge-base a8774f3632eb, checkout unchanged.

Receipt Passed Stopped at
run 1 format, lint, typecheck, layering, fallow, native-dupes, build, vitest-related unit: 1 of 19,068 failed. snapshot-source/preparation.test.ts uses 300 ms deadlines. Run alone, alternating head and base, it failed 1 of 5 at this head and 1 of 5 at base a8774f3. The PR does not touch snapshot-source.
run 2 format, lint, typecheck, layering, fallow, native-dupes, build vitest-related: 1 of 352 failed, a 5 s timeout in maestro-direct-selector.contract. It passed in run 1, and a separate vitest related rerun at this head passed 352/352.
Check, run separately at c75affacf Result
provider-integration (test:integration:provider) passed, 257/257
command-docs, docs-prose passed
docs-links passed on Node 26.13.0 (the Astro build needs Node >=22.23.4)

The host is shared, and its load average was about 30 during these runs.

sittingmongoose added a commit to sittingmongoose/agent-device that referenced this pull request Nov 11, 2026
@sittingmongoose

Copy link
Copy Markdown
Contributor Author

Exact-head device evidence for c75affacf (current head)

The pool booted a Duo after all, so here is the same battery on the current head. It replaces the "pending" note above.

Setup

  • Build: the same procedure as for 09b2765b2: pnpm build, then npm pack --ignore-scripts (tarball sha256 1495c66c…40b283).
  • Daemon: a fresh one from the unpacked package, with its own state and home directories.
  • Command form: the ordinary CLI, --platform ios --udid D822B3D1-A31F-4E73-B3D6-B5C849C8382B --ios-simulator-device-set <scoped set> --json.
  • Device: iOS 27.3 iPhone Duo, Safari in front unless noted.

Helper build. Fold.m is byte-identical to 09b2765b2 (sha256 4f78df1f…), so the helper cache key matched. I moved that cache entry aside before H15, and this head's daemon rebuilt it:
xcrun --sdk iphonesimulator clang -mios-simulator-version-min=15.0 -fobjc-arc -Wall -Wextra -framework Foundation -framework IOKit <package>/apple/fold-helper/Fold.m -o ~/.agent-device/fold-helper/.950f94d1….tmp/fold-helper → exit 0.

Step Command Exit Helper calls → lit panel currentOrientation Screenshot
H1 orientation landscape-left (closed) 0 panels (routing) → LCD rot0; panels → LCD rot0; rotate landscape-left; panels → LCD rot270 678x466
H2 orientation landscape-left (repeat) 0 panels → LCD rot270; no rotate 678x466
H3 orientation landscape-right 0 panels → LCD rot270; rotate landscape-right; panels → LCD rot90 678x466
H4 orientation portrait 0 panels → LCD rot90; rotate portrait; panels → LCD rot0 466x678
H5 fold open 1 panels; set open → 0; hinge 0 hit its 4 s timeout → fold-pose-dispatched-unverified; panels → LCD-1 lit rot90, so the panel did unfold. This is the first fold after a fresh boot on a loaded host (seen before; fold is unchanged in this PR). –
H6 orientation portrait (inner) 0 panels → LCD-1 rot90; rotate landscape-left; panels → LCD-1 rot0 669x951
H7 orientation landscape-left (inner) 0 panels → LCD-1 rot0; rotate portrait-upside-down (sent as pud); panels → LCD-1 rot270 951x669
H8 orientation landscape-right (inner) 0 panels → LCD-1 rot270; rotate portrait; panels → LCD-1 rot90 951x669
H9 fold closed 0 set closed; hinge 0 → 0°, valid; panels → both activeOn –
H10 orientation landscape-left, right after H9 0 panels → both lit; panels → both lit (neither confirmed nor dispatched); panels → LCD rot0; rotate landscape-left; panels → LCD rot270 678x466
H11 open com.apple.Preferences (portrait-only on the outer panel) 0 – –
H12 orientation landscape-right with Settings in front 1 panels → LCD rot0; one rotate landscape-right; then six panels → LCD rot0, with no further dispatch 466x678
H15 orientation landscape-left, with the freshly built helper 0 panels → LCD rot90; rotate landscape-left; panels → LCD rot270 678x466

H12 error:
COMMAND_FAILED, "did not confirm rotation to landscape-right: the lit panel shows portrait", details {"observedOrientation":"portrait","rawPanelOrientation":"rot0","reason":"orientation-unverified-scoped-set"}.
On 09b2765b2 this step cycled through six picker values; now it dispatches once.

Call counts (H0–H13): 67 xcrun calls, all simctl --set <scoped set>; 0 devicectl; 0 default-set simctl; 38 panels; 8 rotate.

H1 outer landscape-left H2 repeat (no dispatch) H3 outer landscape-right H4 outer portrait
H1 H2 H3 H4
H6 inner portrait H7 inner landscape-left (pud) H8 inner landscape-right
H6 H7 H8
H10 right after folding closed H12 unachievable target, one dispatch H15 freshly built helper
H10 H12 H15

Base a8774f3 refusal: unchanged, see above (UNSUPPORTED_OPERATION, unsupported-device-scope, one panels read, no rotate, no devicectl).
check:affected receipts for this head: in the previous comment.

Full helper log for this head (sanitized)

=== STEP H0-open-safari 20:50:13.000Z agent-device open com.apple.mobilesafari
  20:50:14.965Z simctl --set …/ios launch D822B3D1-A31F-4E73-B3D6-B5C849C8382B com.apple.mobilesafari
  20:50:16.900Z simctl --set …/ios spawn D822B3D1-A31F-4E73-B3D6-B5C849C8382B …/snapshot-bridge serve …/snapshot.sock --idle-timeout 60 --exit-on-disconnect false
=== END H0-open-safari rc=0 20:50:18.000Z

=== STEP H1-outer-landscape-left 20:50:18.000Z agent-device orientation landscape-left
  20:50:18.898Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  20:50:19.255Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  20:50:19.522Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-left  -> exit 0
  20:50:20.504Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
=== END H1-outer-landscape-left rc=0 20:50:20.000Z

=== STEP H1-shot 20:50:20.000Z agent-device screenshot shots/H1-outer-landscape-left.png
  20:50:21.062Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
  20:50:21.340Z simctl --set …/ios io D822B3D1-A31F-4E73-B3D6-B5C849C8382B screenshot --display=LCD …/H1-outer-landscape-left.png
=== END H1-shot rc=0 20:50:22.000Z

=== STEP H2-outer-landscape-left-repeat 20:50:22.000Z agent-device orientation landscape-left
  20:50:22.538Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
=== END H2-outer-landscape-left-repeat rc=0 20:50:23.000Z

=== STEP H2-shot 20:50:23.000Z agent-device screenshot shots/H2-outer-landscape-left-repeat.png
  20:50:23.712Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
  20:50:24.263Z simctl --set …/ios io D822B3D1-A31F-4E73-B3D6-B5C849C8382B screenshot --display=LCD …/H2-outer-landscape-left-repeat.png
=== END H2-shot rc=0 20:50:25.000Z

=== STEP H3-outer-landscape-right 20:50:25.000Z agent-device orientation landscape-right
  20:50:25.438Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
  20:50:25.894Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-right  -> exit 0
  20:50:26.918Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot90) LCD-1(primary=False,off,rot90)
=== END H3-outer-landscape-right rc=0 20:50:27.000Z

=== STEP H3-shot 20:50:27.000Z agent-device screenshot shots/H3-outer-landscape-right.png
  20:50:27.832Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot90) LCD-1(primary=False,off,rot90)
  20:50:28.830Z simctl --set …/ios io D822B3D1-A31F-4E73-B3D6-B5C849C8382B screenshot --display=LCD …/H3-outer-landscape-right.png
=== END H3-shot rc=0 20:50:29.000Z

=== STEP H4-outer-portrait 20:50:29.000Z agent-device orientation portrait
  20:50:30.257Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot90) LCD-1(primary=False,off,rot90)
  20:50:30.817Z simctl --set devices/ios spawn <udid> fold-helper rotate portrait  -> exit 0
  20:50:31.952Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
=== END H4-outer-portrait rc=0 20:50:32.000Z

=== STEP H4-shot 20:50:32.000Z agent-device screenshot shots/H4-outer-portrait.png
  20:50:33.379Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  20:50:33.985Z simctl --set …/ios io D822B3D1-A31F-4E73-B3D6-B5C849C8382B screenshot --display=LCD …/H4-outer-portrait.png
=== END H4-shot rc=0 20:50:34.000Z

=== STEP H5-fold-open 20:50:41.000Z agent-device fold open
  20:50:42.280Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  20:50:45.471Z simctl --set devices/ios spawn <udid> fold-helper set open  -> exit 0
  20:50:47.182Z simctl --set devices/ios spawn <udid> fold-helper hinge 0
  20:50:51.318Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot90)
=== END H5-fold-open rc=1 20:50:53.000Z

=== STEP H6-inner-portrait 20:50:53.000Z agent-device orientation portrait
  20:50:54.784Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot90)
  20:50:56.391Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-left  -> exit 0
  20:50:58.619Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot0)
=== END H6-inner-portrait rc=0 20:50:59.000Z

=== STEP H6-shot 20:50:59.000Z agent-device screenshot shots/H6-inner-portrait.png
  20:51:00.417Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot0)
  20:51:01.435Z simctl --set …/ios io D822B3D1-A31F-4E73-B3D6-B5C849C8382B screenshot --display=LCD-1 …/H6-inner-portrait.png
=== END H6-shot rc=0 20:51:04.000Z

=== STEP H7-inner-landscape-left 20:51:04.000Z agent-device orientation landscape-left
  20:51:04.771Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot0)
  20:51:05.248Z simctl --set devices/ios spawn <udid> fold-helper rotate portrait-upside-down  -> exit 0
  20:51:06.634Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot270)
=== END H7-inner-landscape-left rc=0 20:51:07.000Z

=== STEP H7-shot 20:51:07.000Z agent-device screenshot shots/H7-inner-landscape-left.png
  20:51:07.815Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot270)
  20:51:08.325Z simctl --set …/ios io D822B3D1-A31F-4E73-B3D6-B5C849C8382B screenshot --display=LCD-1 …/H7-inner-landscape-left.png
=== END H7-shot rc=0 20:51:09.000Z

=== STEP H8-inner-landscape-right 20:51:09.000Z agent-device orientation landscape-right
  20:51:09.855Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot270)
  20:51:10.326Z simctl --set devices/ios spawn <udid> fold-helper rotate portrait  -> exit 0
  20:51:11.527Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot90)
=== END H8-inner-landscape-right rc=0 20:51:12.000Z

=== STEP H8-shot 20:51:12.000Z agent-device screenshot shots/H8-inner-landscape-right.png
  20:51:12.605Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot90)
  20:51:13.071Z simctl --set …/ios io D822B3D1-A31F-4E73-B3D6-B5C849C8382B screenshot --display=LCD-1 …/H8-inner-landscape-right.png
=== END H8-shot rc=0 20:51:14.000Z

=== STEP H9-fold-closed 20:51:14.000Z agent-device fold closed
  20:51:14.525Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,off,rot0) LCD-1(primary=False,activeOn,rot90)
  20:51:15.038Z simctl --set devices/ios spawn <udid> fold-helper set closed  -> exit 0
  20:51:15.747Z simctl --set devices/ios spawn <udid> fold-helper hinge 0  -> exit 0  {"version":1,"angleDegrees":0,"angleValid":true}
  20:51:16.745Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,activeOn,rot90)
=== END H9-fold-closed rc=0 20:51:17.000Z

=== STEP H10-outer-landscape-left-after-fold 20:51:17.000Z agent-device orientation landscape-left
  20:51:17.977Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,activeOn,rot90)
  20:51:19.124Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,activeOn,rot90)
  20:51:20.389Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  20:51:20.889Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-left  -> exit 0
  20:51:21.923Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
=== END H10-outer-landscape-left-after-fold rc=0 20:51:22.000Z

=== STEP H10-shot 20:51:22.000Z agent-device screenshot shots/H10-outer-landscape-left-after-fold.png
  20:51:22.654Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
  20:51:22.980Z simctl --set …/ios io D822B3D1-A31F-4E73-B3D6-B5C849C8382B screenshot --display=LCD …/H10-outer-landscape-left-after-fold.png
=== END H10-shot rc=0 20:51:23.000Z

=== STEP H11-open-settings 20:51:23.000Z agent-device open com.apple.Preferences
  20:51:23.980Z simctl --set …/ios launch D822B3D1-A31F-4E73-B3D6-B5C849C8382B com.apple.Preferences
=== END H11-open-settings rc=0 20:51:26.000Z

=== STEP H12-outer-settings-landscape-right 20:51:26.000Z agent-device orientation landscape-right
  20:51:26.558Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  20:51:26.971Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-right  -> exit 0
  20:51:27.980Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  20:51:30.259Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  20:51:31.207Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  20:51:32.111Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  20:51:32.988Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  20:51:33.861Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
=== END H12-outer-settings-landscape-right rc=1 20:51:34.000Z

=== STEP H12-shot 20:51:34.000Z agent-device screenshot shots/H12-outer-settings-landscape-right-unverified.png
  20:51:34.402Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot0) LCD-1(primary=False,off,rot90)
  20:51:34.666Z simctl --set …/ios io D822B3D1-A31F-4E73-B3D6-B5C849C8382B screenshot --display=LCD …/H12-outer-settings-landscape-right-unverified.png
=== END H12-shot rc=0 20:51:35.000Z

=== STEP H13-close 20:51:57.000Z agent-device close
=== END H13-close rc=0 20:51:57.000Z

=== STEP H14-open-safari 20:52:11.000Z agent-device open com.apple.mobilesafari
  20:52:12.104Z simctl --set …/ios launch D822B3D1-A31F-4E73-B3D6-B5C849C8382B com.apple.mobilesafari
  20:52:14.496Z simctl --set …/ios spawn D822B3D1-A31F-4E73-B3D6-B5C849C8382B …/snapshot-bridge serve …/snapshot.sock --idle-timeout 60 --exit-on-disconnect false
=== END H14-open-safari rc=0 20:52:15.000Z

=== STEP H15-outer-landscape-left-fresh-helper 20:52:15.000Z agent-device orientation landscape-left
  20:52:16.827Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot90) LCD-1(primary=False,off,rot90)
  20:52:17.200Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot90) LCD-1(primary=False,off,rot90)
  20:52:17.475Z simctl --set devices/ios spawn <udid> fold-helper rotate landscape-left  -> exit 0
  20:52:18.437Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
=== END H15-outer-landscape-left-fresh-helper rc=0 20:52:18.000Z

=== STEP H15-shot 20:52:18.000Z agent-device screenshot shots/H15-outer-landscape-left-fresh-helper.png
  20:52:19.107Z simctl --set devices/ios spawn <udid> fold-helper panels  -> exit 0  LCD(primary=True,activeOn,rot270) LCD-1(primary=False,off,rot90)
  20:52:19.543Z simctl --set …/ios io D822B3D1-A31F-4E73-B3D6-B5C849C8382B screenshot --display=LCD …/H15-outer-landscape-left-fresh-helper.png
=== END H15-shot rc=0 20:52:20.000Z

=== STEP H16-close 20:52:27.000Z agent-device close
=== END H16-close rc=0 20:52:28.000Z

ISSUES: none (no devicectl call; every simctl call names --set)

@thymikee

Copy link
Copy Markdown
Member

At c75affa, the earlier review's delayed-read issue is fixed, but scoped orientation can still accept an incomplete panel inventory.

readLitPanel can accept the remaining lit panel when a second lit integrated panel lacks geometry or identity. This can report success or dispatch compensation without establishing a single lit panel. Require activeDisplay before accepting a lit reading, and add an interactor regression with two activeOn integrated entries, one missing geometry; it must not dispatch or confirm and must end with orientation-unverified-scoped-set.

Could display-inventory.ts own strict lit-panel selection for both appleLitPanel and readLitPanel, preserving the incomplete-table refusal, while picker calibration stays in foldable/orientation.ts?

No lower-priority open threads still apply. The threads on lit-panel wording, picker coverage, orientation sequence, dispatch count, final read, changeset wording, delayed effects, and budget evidence are addressed at this commit and can be resolved.

CI reports one passing check and no failures; two local receipts stopped on timeouts that appear unrelated, supported by the reported base reproduction and related-test pass. No conflicts are known. Before merge, fix the incomplete-inventory case and obtain a passing exact-head validation or CI receipt; the reported check does not establish a full passing gate.

sittingmongoose added a commit to sittingmongoose/agent-device that referenced this pull request Nov 11, 2026
Review of callstack#4440: readLitPanel filtered the parsed panels for a single lit
one, so a table with a second lit integrated entry that lacked identity
or geometry (which the parser drops) still produced a reading. The
rotation then confirmed, or dispatched compensation, without one
established lit panel.

display-inventory now owns the strict selection (establishedLitPanel): a
resolved multi-panel table that parsed every integrated entry, and so
named an active display, with exactly one lit panel, which is that
display. appleLitPanel and the scoped rotation both select through it;
the incomplete-table capture refusal is unchanged and picker calibration
stays in the foldable module.

Regression: an interactor test with two activeOn integrated entries, one
without geometry, for a target the parsed panel already shows and for
another target: no dispatch, no confirmation, orientation-unverified-
scoped-set. On c75affa it returned success in both cases.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@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.

1 issue found across 4 files (changes from recent commits).

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

View guided diff | Re-trigger cubic

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/platform-apple/src/core/display-inventory.ts">

<violation number="1" location="packages/platform-apple/src/core/display-inventory.ts:426">
P2: `establishedLitPanel` only counts `power === 'lit'`, so a panel whose power is `unknown` is silently treated as dark. The new doc says an unparsed entry 'may be lit too', and `readPanelPower` says an unknown panel may be showing content, so one lit panel plus one unknown sibling should establish nothing. Treat any panel with `power === 'unknown'` as blocking establishment, and add a test for that mix.</violation>
</file>

sittingmongoose added a commit to sittingmongoose/agent-device that referenced this pull request Nov 11, 2026
Review of callstack#4440 (cubic): establishedLitPanel counted only power 'lit', so a
lit panel beside a sibling of unknown power (an unrecognized backlight
state, which readPanelPower deliberately does not call dark) was still
established, and the scoped rotation could confirm or dispatch from it.
Any panel of unknown power now establishes none, for runner routing and
the scoped rotation alike; the capture fallback is unchanged.

Fail-first: on bfdbd9b the new selector test and both interactor cases
(target already showing, another target) failed - the rotation returned
success in both.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
sittingmongoose added a commit to sittingmongoose/agent-device that referenced this pull request Nov 11, 2026
@thymikee

Copy link
Copy Markdown
Member

The shared selection rule at 4a2d30b consolidates the earlier ownership concern, and the new refusal strengthens the centralized invariant. I found no additional code defect in the static delta review.

Could you build and package 4a2d30b, then use a fresh daemon and ordinary CLI on a scoped Duo to validate the shared selection rule for rotation and runner panel routing? The latest supplied device run is from c75affa, so it does not establish current behavior. Record the SHA, commands, exits, helper arguments, sole-lit currentOrientation, and screenshots for portrait and both landscapes on both panels, inner landscape-left pud, a repeated target, immediate post-fold ambiguous reads, and an unachievable target ending in orientation-unverified-scoped-set. Show zero dispatch or confirmation during ambiguous reads, no devicectl or default-set calls, and a fresh observable runner interaction on each panel. Keep the existing base refusal evidence and automated incomplete and unknown-table cases. Selection rule

The unknown-power sibling thread no longer applies: the current code rejects unknown-power siblings, with selector and interactor regression coverage. No lower-priority threads remain applicable.

CI reports one passing check and no failures. The earlier c75affa local timeouts were in snapshot-source and Maestro paths outside this delta, with reported base reproduction or a separate pass; they do not establish a complete passing current-head gate. There are no known conflicts. Before merge, please provide the current-head live evidence above and a passing exact-head check:affected or equivalent CI receipt.

@sittingmongoose

Copy link
Copy Markdown
Contributor Author

Current-head evidence for 4a2d30b2b: live scoped Duo run and check:affected receipt

The unknown-power thread is resolved, and all 9 review threads are resolved.

1. Automated cases at 4a2d30b2b (fail-first)

Interactor regressions (foldable/orientation.test.ts). Each runs with a target the parsed panel already shows and with another target; all must end orientation-unverified-scoped-set with no rotate and no observedOrientation:

  • Incomplete table: two activeOn integrated entries, the inner one without geometry.
  • Unknown power: a lit outer panel, plus an inner panel whose backlight state is unrecognized.

Selector tests (core/__tests__/display-inventory.test.ts). establishedLitPanel and appleLitPanel both return nothing for:

  • an incomplete table;
  • two lit panels;
  • a lit panel beside an unknown-power sibling;
  • single-panel and unresolved tables.

They both return the panel for a complete table with one lit panel. runner-panel.test.ts keeps its existing incomplete-table and power-undecided runner-routing cases.

Fail-first results:

Tests Against c75affacf source Against bfdbd9bf6 At 4a2d30b2b
incomplete-table interactor cases 2 failed (returned {orientation: 'landscape-left'} and {orientation: 'landscape-right'}) pass pass
unknown-power selector + interactor cases – 3 failed (both rotations returned success) pass
orientation.test.ts + display-inventory.test.ts – 3 failed / 80 passed all pass; platform-apple unit suite 1548 passed

2. Live scoped Duo run at 4a2d30b2b

Setup

  • Build: pnpm build in a clean worktree at 4a2d30b2b7a0876b93b3d520ed1e97cb65e6d10e, then npm pack --ignore-scripts (sha256 7f8b5753…862b0e). Fold.m sha256 is 4f78df1f….
  • Daemon: unpacked into its own folder, with its own AGENT_DEVICE_STATE_DIR and AGENT_DEVICE_HOME, and a logging xcrun shim on PATH.
  • Command form: the ordinary CLI, node <package>/bin/agent-device.mjs <command> --platform ios --udid BFC8BE59-8DF6-4787-B622-5CF960505F4F --ios-simulator-device-set <scoped set> --json.
  • Host: macOS 27.3, Xcode 27.3, iOS 27.3; device pool simlock 0.7.0; Node 26.13.0 on the Mac. Safari in front unless noted.

Helper build: xcrun --sdk iphonesimulator clang -mios-simulator-version-min=15.0 -fobjc-arc -Wall -Wextra -framework Foundation -framework IOKit <package>/apple/fold-helper/Fold.m -o ~/.agent-device/fold-helper/.950f94d1….tmp/fold-helper → exit 0, at the first command (22:24:40Z). The cache entry had been removed beforehand.

LCD = outer panel (primary: true), LCD-1 = inner. Helper argv is shown after the binary.

Step Command Exit Helper calls → sole lit panel currentOrientation Screenshot
H1 orientation landscape-left (closed) 0 panels (routing) → LCD rot0; panels → LCD rot0; rotate landscape-left; panels → LCD rot270 678x466
H2 orientation landscape-left (repeat) 0 panels → LCD rot270; no rotate –
H3 orientation landscape-right 0 panels → LCD rot270; rotate landscape-right; panels → LCD rot90 678x466
H4 orientation portrait 0 panels → LCD rot90; rotate portrait; panels → LCD rot0 466x678
H5 fold open 0 panels; set open; hinge 0 → valid; panels → LCD off, LCD-1 activeOn rot90 –
H6 orientation portrait (inner) 0 panels → LCD-1 rot90; rotate landscape-left; panels → LCD-1 rot0 669x951
H7 orientation landscape-left (inner) 0 panels → LCD-1 rot0; rotate portrait-upside-down (sent as pud); panels → LCD-1 rot270 951x669
H8 orientation landscape-right (inner) 0 panels → LCD-1 rot270; rotate portrait; panels → LCD-1 rot90 951x669
H9 fold closed 1 set closed → 0; hinge 0 hit its 4 s timeout → fold-pose-dispatched-unverified; panels → LCD and LCD-1 both activeOn. The device did close: next step. –
H10 orientation landscape-left, right after H9 0 panels → both lit: no confirm, no dispatch; panels → LCD rot0 (LCD-1 off); rotate landscape-left; panels → LCD rot270 678x466
H11 open com.apple.Preferences (portrait-only on the outer panel) 0 – –
H12 orientation landscape-right with Settings in front 1 panels → LCD rot0; one rotate landscape-right; six panels → LCD rot0, with no further dispatch 466x678

H12 error:
COMMAND_FAILED, "did not confirm rotation to landscape-right: the lit panel shows portrait", details {"observedOrientation":"portrait","rawPanelOrientation":"rot0","reason":"orientation-unverified-scoped-set"}.

Call counts: 65 xcrun calls (64 simctl, all --set <scoped set>, plus the clang build); 0 devicectl; 0 default-set simctl; 36 panels; 8 rotate.

H1 outer landscape-left H3 outer landscape-right H4 outer portrait
H1 H3 H4
H6 inner portrait H7 inner landscape-left (pud) H8 inner landscape-right
H6 H7 H8
H10 right after folding closed H12 unachievable target
H10 H12

Base refusal: unchanged. See the a8774f3 run earlier in this PR (UNSUPPORTED_OPERATION, unsupported-device-scope, one panels read, no rotate, no devicectl).

Full helper log of this run (sanitized)
=== STEP H0-open-safari 22:24:33.000Z agent-device open com.apple.mobilesafari
=== END H0-open-safari rc=0 22:24:39.000Z
=== STEP H1-outer-landscape-left 22:24:39.000Z agent-device orientation landscape-left
  22:24:40.367Z clang … Fold.m build
  22:24:41.883Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
  22:24:42.822Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
  22:24:43.939Z fold-helper rotate landscape-left -> exit 0
  22:24:45.578Z fold-helper panels -> exit 0  LCD(P,activeOn,rot270) LCD-1(-,off,rot90)
=== END H1-outer-landscape-left rc=0 22:24:46.000Z
  22:24:46.991Z fold-helper panels -> exit 0  LCD(P,activeOn,rot270) LCD-1(-,off,rot90)
=== STEP H2-outer-landscape-left-repeat 22:24:49.000Z agent-device orientation landscape-left
  22:24:49.540Z fold-helper panels -> exit 0  LCD(P,activeOn,rot270) LCD-1(-,off,rot90)
=== END H2-outer-landscape-left-repeat rc=0 22:24:50.000Z
=== STEP H3-outer-landscape-right 22:24:50.000Z agent-device orientation landscape-right
  22:24:50.663Z fold-helper panels -> exit 0  LCD(P,activeOn,rot270) LCD-1(-,off,rot90)
  22:24:51.448Z fold-helper rotate landscape-right -> exit 0
  22:24:52.825Z fold-helper panels -> exit 0  LCD(P,activeOn,rot90) LCD-1(-,off,rot90)
=== END H3-outer-landscape-right rc=0 22:24:53.000Z
  22:24:54.139Z fold-helper panels -> exit 0  LCD(P,activeOn,rot90) LCD-1(-,off,rot90)
=== STEP H4-outer-portrait 22:24:55.000Z agent-device orientation portrait
  22:24:56.185Z fold-helper panels -> exit 0  LCD(P,activeOn,rot90) LCD-1(-,off,rot90)
  22:24:56.787Z fold-helper rotate portrait -> exit 0
  22:24:58.029Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
=== END H4-outer-portrait rc=0 22:24:58.000Z
  22:24:59.131Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
=== STEP H5-fold-open 22:25:00.000Z agent-device fold open
  22:25:01.360Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
  22:25:02.291Z fold-helper set open -> exit 0
  22:25:03.807Z fold-helper hinge 0 -> exit 0
  22:25:06.718Z fold-helper panels -> exit 0  LCD(P,off,rot0) LCD-1(-,activeOn,rot90)
=== END H5-fold-open rc=0 22:25:07.000Z
=== STEP H6-inner-portrait 22:25:07.000Z agent-device orientation portrait
  22:25:08.185Z fold-helper panels -> exit 0  LCD(P,off,rot0) LCD-1(-,activeOn,rot90)
  22:25:08.883Z fold-helper rotate landscape-left -> exit 0
  22:25:10.267Z fold-helper panels -> exit 0  LCD(P,off,rot0) LCD-1(-,activeOn,rot0)
=== END H6-inner-portrait rc=0 22:25:11.000Z
  22:25:11.771Z fold-helper panels -> exit 0  LCD(P,off,rot0) LCD-1(-,activeOn,rot0)
=== STEP H7-inner-landscape-left 22:25:13.000Z agent-device orientation landscape-left
  22:25:14.646Z fold-helper panels -> exit 0  LCD(P,off,rot0) LCD-1(-,activeOn,rot0)
  22:25:18.263Z fold-helper rotate portrait-upside-down -> exit 0
  22:25:21.950Z fold-helper panels -> exit 0  LCD(P,off,rot0) LCD-1(-,activeOn,rot270)
=== END H7-inner-landscape-left rc=0 22:25:24.000Z
  22:25:25.660Z fold-helper panels -> exit 0  LCD(P,off,rot0) LCD-1(-,activeOn,rot270)
=== STEP H8-inner-landscape-right 22:25:29.000Z agent-device orientation landscape-right
  22:25:30.149Z fold-helper panels -> exit 0  LCD(P,off,rot0) LCD-1(-,activeOn,rot270)
  22:25:31.415Z fold-helper rotate portrait -> exit 0
  22:25:33.451Z fold-helper panels -> exit 0  LCD(P,off,rot0) LCD-1(-,activeOn,rot90)
=== END H8-inner-landscape-right rc=0 22:25:34.000Z
  22:25:35.771Z fold-helper panels -> exit 0  LCD(P,off,rot0) LCD-1(-,activeOn,rot90)
=== STEP H9-fold-closed 22:25:37.000Z agent-device fold closed
  22:25:38.037Z fold-helper panels -> exit 0  LCD(P,off,rot0) LCD-1(-,activeOn,rot90)
  22:25:38.844Z fold-helper set closed -> exit 0
  22:25:39.819Z fold-helper hinge 0
  22:25:43.811Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,activeOn,rot90)
=== END H9-fold-closed rc=1 22:25:44.000Z
=== STEP H10-outer-landscape-left-after-fold 22:25:45.000Z agent-device orientation landscape-left
  22:25:45.747Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,activeOn,rot90)
  22:25:47.373Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
  22:25:48.400Z fold-helper rotate landscape-left -> exit 0
  22:25:49.989Z fold-helper panels -> exit 0  LCD(P,activeOn,rot270) LCD-1(-,off,rot90)
=== END H10-outer-landscape-left-after-fold rc=0 22:25:50.000Z
  22:25:51.716Z fold-helper panels -> exit 0  LCD(P,activeOn,rot270) LCD-1(-,off,rot90)
=== STEP H11-open-settings 22:25:54.000Z agent-device open com.apple.Preferences
=== END H11-open-settings rc=0 22:25:59.000Z
=== STEP H12-outer-settings-landscape-right 22:25:59.000Z agent-device orientation landscape-right
  22:26:00.587Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
  22:26:02.120Z fold-helper rotate landscape-right -> exit 0
  22:26:03.979Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
  22:26:05.942Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
  22:26:07.935Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
  22:26:09.220Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
  22:26:10.577Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
  22:26:11.903Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
=== END H12-outer-settings-landscape-right rc=1 22:26:12.000Z
  22:26:13.115Z fold-helper panels -> exit 0  LCD(P,activeOn,rot0) LCD-1(-,off,rot90)
=== STEP H13-close 22:26:16.000Z agent-device close
=== END H13-close rc=1 22:26:55.000Z
COUNTS {'calls': 65, 'simctl': 64, 'withset': 64, 'devicectl': 0, 'rotate': 8, 'panels': 36, 'clang': 1}
ISSUES none

3. Runner interaction on each panel: not shown

Why. On this host, a Duo lease comes from a shared simulator pool through T3 Code's device tools.

  • The lock. Opening a lease also opens a session in the pool's own agent-device daemon (the installed release). That daemon pre-builds the iOS runner and takes the device's host-wide runner lease (~/.agent-device/apple-runner/leases/<udid>.json.lock, owner = that daemon) for as long as the lease lives.
  • The effect. This build's first runner request (snapshot -i) waited and failed with process_lock_timeout (dispatched: "no"). Closing the pool daemon's session releases the runner but also ends the lease.
  • The retry. A second lease attempt was declined: the pool's one Duo was in use by another session. I stopped there under my two-attempt limit.

What covers runner routing instead. Runner panel routing goes through the same selector as the rotation (appleLitPanel → establishedLitPanel). It is covered by the selector tests above and by runner-panel.test.ts. The package used for the runner attempt also carried the runner sources (dist/apple/runner, via scripts/package-apple-runner-source.mjs), so the remaining gap is device access, not packaging.

4. check:affected at 4a2d30b2b

Command: node --experimental-strip-types scripts/check-affected/run.ts --base upstream/main --run, on Node 24.22.1 (needed for the Astro docs build) and pnpm 11.17.0. Run 4 also set AGENT_DEVICE_VITEST_MAX_WORKERS=2.

All four receipts: exactCommit: true, resolvedHead 4a2d30b2b7a0…, merge-base a8774f3632eb, checkout unchanged. 21 checks selected; 8 skipped as GitHub-authoritative or parked.

Receipt format, lint, typecheck, layering, fallow, native-dupes, build, vitest-related unit (19,077 tests)
run 1 all passed 1 failed: snapshot-source/preparation.test.ts
run 2 all passed 1 failed: macos-helper-package-contents.test.ts
run 3 all passed 1 failed: macos-helper-package-contents.test.ts
run 4 all passed 2 failed: both of the above

Neither test is in this delta, and both fail on base a8774f3 on this host.

  • snapshot-source/preparation.test.ts uses 300 ms deadlines, and its build counter reads 0 instead of 1 under load. In alternating isolated runs it failed 3 of 10 at this head and 2 of 10 at base (earlier: 1 of 5 each).
  • macos-helper-package-contents.test.ts times out at 5 s around an npm pack dry run. Base's own test:unit run on this host failed with exactly this test (1 of 19,047).
  • Rerun alone at this head: each passed 3 of 3.

The checks the receipt never reached, run separately at this head:

Check Result
provider-integration (test:integration:provider) passed, 257/257
command-docs, docs-prose passed
docs-links passed

So I don't have a single all-green receipt. The host is a shared machine with a load average of 25–30 from other work, and four runs each failed only on these two timing tests. Every other selected check passed at this exact head. If you prefer, approving the fork's CI run would give an independent receipt.

@thymikee

Copy link
Copy Markdown
Member

Thanks, the live evidence at 4a2d30b covers what the review asked for. The scoped run from a packed build shows both panels in portrait and both landscapes, the inner landscape-left pud dispatch, a repeated target with no dispatch, no confirmation or dispatch while both panels read lit right after folding, and an unachievable target ending in orientation-unverified-scoped-set. All 64 simctl calls used the scoped set, with no devicectl and no default-set calls.

The runner interaction on each panel is still not shown, because the shared pool held the runner lock. I accept that gap here: the only runner-routing change since c75affa is a new refusal for unknown-power siblings, the live tables never reported unknown power, and the selector and runner-panel tests cover that refusal.

The local check:affected runs at this head passed every selected check except two timing tests outside this delta, which also fail on the base on that host and pass when rerun alone. The fork's CI run still needs maintainer approval for an independent receipt. No code findings remain, so this is ready for maintainer review.

@thymikee thymikee added ready-for-human Valid work that needs human implementation, judgment, or maintainer merge and removed ready-for-human Valid work that needs human implementation, judgment, or maintainer merge labels Nov 11, 2026
@thymikee

Copy link
Copy Markdown
Member

Thanks for the thorough live validation. Rotating a leased foldable in a scoped set is a real gap, and the command surface stays the same, which is good. I removed ready-for-human for now because I think the change can be smaller and behave the same in both simulator sets.

  1. One foldable route instead of two. In the default set a foldable still rotates and confirms by polling devicectl, while the scoped set gets a second route: dispatch through the fold helper, confirm from the lit panel, with its own settle wait and its own orientation-unverified-scoped-set reason. Your live run shows the panel table confirms rotation reliably. Could every foldable use that route, in both sets, and drop the devicectl polling and its separate error for foldables? That removes a path instead of adding one, and an agent sees the same behavior wherever the device lives.

  2. A fixed panel offset instead of a learned one. The scoped route works out the panel offset at run time from the last dispatch and a fresh reading, with a measured fallback (outer panel 0, inner panel +90), which is why portrait on the inner panel sends landscape-left and landscape-left sends pud. Is that offset a fixed property of this simulator, or have you seen it vary? If it is fixed, a constant per-panel table could replace scopedPanelOffset, awaitingDispatchEffect and most of picker-delta.ts.

  3. The one-command-late effect from orientation: a foldable in a scoped simulator set is refused, and the fold helper's rotate lands one command late #4343. With a single route, the settle and re-read handling would apply to the default set too. Does the default set show the same late effect, or does devicectl hide it?

If one of these does not hold (for example the offset really varies, or the panel table is not available in the default set), please say why, and the current shape can stay for that part. The live evidence format you used would cover the merged route as well; a run in the default set plus the scoped run you already have would be enough.

@sittingmongoose sittingmongoose changed the title fix(ios): rotate a foldable iPhone simulator in a scoped simulator set (#4343) fix(ios): confirm foldable rotation from the lit panel in either simulator set (#4343) Nov 12, 2026
@sittingmongoose

sittingmongoose commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Implemented the requested smaller design at 20841c06340e7b1483b0286dd3a7a45ed9053e77, after measuring both sets before changing production code.

  1. One route works in both sets. The default-set guest panels table is available and has the same two integrated panels, primary, backlight state and currentOrientation as the scoped set. All foldable rotation now dispatches through the fold helper and confirms the established lit panel. CoreDevice orientation polling, its helper and the separate scoped error are removed. Both sets use orientation-unverified when the target cannot be confirmed. Normal capture keeps ADR 0025's existing source policy.
  2. The offsets stayed fixed. The previous 18 inner/+90 and 9 outer/0 readings agree with the new measurements. Default raw dispatches covered all four inner picker values and all three supported outer targets; fresh scoped commands agree. A constant table replaces learned state and all picker-delta code. Safari refuses upside-down on the outer panel, retaining the previous frame; this was not an offset change.
  3. Default readback hides the visible offset and has a stale sample. After raw inner portrait, two guest reads already show rot90, then CoreDevice reports landscapeLeft -> portrait without another dispatch. After raw inner landscape-right, the guest already shows rot180, while CoreDevice reports portrait -> landscapeRight. The old default CLI returns success for inner portrait/left/right with visible rotations rot90/rot0/rot180. The measurements show the fixed panel offset plus stale CoreDevice readback; they do not show HID waiting for the next command.

Full report, raw evidence and reproduction drivers.

Fresh packed-daemon ordinary-CLI results in both DEFAULT and SCOPED sets:

Panel Requested Helper picker Sole lit panel after PNG size
outer portrait portrait LCD rot0 466x678
outer landscape-left landscape-left LCD rot270 678x466
outer landscape-right landscape-right LCD rot90 678x466
inner portrait landscape-left LCD-1 rot0 669x951
inner landscape-left portrait-upside-down (wire pud) LCD-1 rot270 951x669
inner landscape-right portrait LCD-1 rot90 951x669
  • Repeat: each set reads once and dispatches zero times.
  • Post-fold ambiguity: default/scoped make four/three multi-lit reads and dispatch nothing until only LCD is lit, then confirm rot270.
  • Unachievable target: Settings stays rot0 after one landscape-right dispatch; both commands exit 1 with orientation-unverified.
  • No CoreDevice rotation calls: zero during all 18 orientation commands, including admission and confirmation. Default screenshot/discovery operations remain separate.
  • Safety rules retained: incomplete, unreadable, unknown-power and multi-lit tables cannot establish a panel. Unexpected readings cannot recalibrate the fixed table.

The live archive was built at b5116d2b9; the final head differs from the live build only in tests and documentation. Freshly repacking the unchanged validated production outputs at 20841c063 produced a byte-identical archive, SHA-256 54d32f270c3919002c02f1d07e2dad2aee77c9ca3a72c1001de7453a68b607b9. Each run used a fresh owned daemon; installed bundles and simlock patches were untouched. The default Duo was deleted and scoped leases shut down.

Validation: fail-first shared matrix at 4a2d30b 22 failed; revised focused suite 178 passed, 2 skipped; provider fold integration 7 passed. Final exact-head check:affected --run: failed at the unchanged smoke-web-platform.test.ts:103 shutdown IPC assertion, after 19,025 Vitest tests passed; exactCommit: true; explicit skips and unrun stages retained (receipt). The first gate passed all 19,025 Vitest tests but failed one unchanged web-shutdown IPC acknowledgement; unchanged base and head each passed its smoke command afterward. That intermittent test issue remains recorded for the tool batch; the full recheck result above is separate from those focused rechecks. GitHub retains authority for skipped native/device lanes, integration and coverage.

One existing limit is explicitly recorded: initial unreadable hardware classification retains its runner fallback. Known multi-panel devices keep the guarded foldable path. This PR does not claim to fix that separate initial-classification case. Cold self-review only; no independent review of this revision.

Review follow-up at 20841c063: exact default/scoped commands, the current ADR rotation contract and renamed budget reference are fixed. The redundant CoreDevice fixture branch is removed; all 53 orientation tests pass. The 39,960-byte guidance passes its five checks. The measured-in-both-sets claim is retained with the linked raw live measurements. All fourteen current review threads are resolved; the new re-review remains pending.

All five documentation catalog gates pass separately at this head (log); this does not turn the failed full receipt into a pass.

The cause remains unproven: this is not labelled Base-blocked. The earlier 61c5a43fe full green receipt is retained at its own SHA. The final PR remains a draft pending the shutdown-check blocker and independent CI; fork workflows require maintainer approval.

@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.

4 issues found across 18 files (changes from recent commits).

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

View guided diff | Re-trigger cubic

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/platform-apple/src/core/config.ts">

<violation number="1" location="packages/platform-apple/src/core/config.ts:37">
P3: This rename leaves docs/adr/0025-foldable-apple-panels.md:109 pointing at `IOS_SCOPED_DISPLAY_PROBE_BUDGET_MS`, which no longer exists. Update that ADR reference to `IOS_GUEST_DISPLAY_PROBE_BUDGET_MS`.</violation>
</file>

<file name="docs/agents/device-foldable.md">

<violation number="1" location="docs/agents/device-foldable.md:5">
P2: This rewrite drops the scoped-set guidance for listing panels. The unscoped `devicectl` line above still applies to scoped sets, where CoreDevice does not verify. Name both cases explicitly and use an exact command rather than bracketed optional syntax.</violation>
</file>

<file name="docs/adr/0025-foldable-apple-panels.md">

<violation number="1" location="docs/adr/0025-foldable-apple-panels.md:61">
P2: This contract now says scoped-set and default-set foldable rotation both read the lit panel through the guest table. The 2026-10-26 amendment below still says the verdict comes from `devicectl device orientation get` and that scoped sets are refused with `unsupported-device-scope`. Update or supersede that amendment so the ADR describes one rotation contract.</violation>
</file>

<file name="packages/platform-apple/src/foldable/orientation.test.ts">

<violation number="1" location="packages/platform-apple/src/foldable/orientation.test.ts:184">
P3: The `devicectl` branch added to `fakeDuo` is unreachable: rotations go through the fold helper for both sets, and any devicectl call would already fail the `devicectl` toEqual([]) assertions. Remove the branch and its `deviceOrientation` conversion, and reword the throw message so it does not say 'in a scoped set'.</violation>
</file>

Comment thread docs/agents/device-foldable.md Resolved
Comment thread docs/adr/0025-foldable-apple-panels.md Resolved
Comment thread .changeset/foldable-scoped-rotation.md Resolved
@thymikee

Copy link
Copy Markdown
Member

Thanks for the update. At 61c5a43 the change is not ready to merge yet: one routing problem remains, and the checks show one passing check and no failures. No conflicts are known.

The delta passes foldHelper to the routing probe at orientation.ts:65, and its presence alone selects the guest table at display-inventory.ts:117. appleFoldableFacts admits every iOS and iPadOS simulator, and interactor.ts:172 runs this probe before every simulator orientation. So the first orientation per daemon on any default-set iPhone or iPad, single-panel included, now builds the fold helper (clang, up to 30s, plus the cold toolchain probe) and spawns it. Before, it made one short devicectl read. The test at orientation.test.ts:76 now expects a single-panel simulator to spawn ['panels']. The guest read is capped at 15s (IOS_GUEST_DISPLAY_PROBE_BUDGET_MS), which is shorter than the build budget. On a cold host a default-set Duo can resolve as unresolved, the probe returns false at :67, and the command takes the runner rotation. Per the docblock at :51-53, that rotation has no effect on a foldable and echoes the requested value, so it reports success when nothing rotated. Base could not reach this state. The rule should be: panel-count classification uses the set's existing source, and only the reads that need the lit panel use the guest table. The smallest fix is to drop the foldHelper argument at :65. If the author keeps guest classification, please show a single-panel default-set iPhone (and an iPad) with no built fold helper yet: one orientation landscape-left with exit code, elapsed time, the fold-helper build and panels spawn in the diagnostics, and the runner rotation. Please also show a default-set Duo first rotation after deleting ~/.agent-device/fold-helper that still classifies as multi-panel within the budget. None of that is covered by the Duo-only evidence, which is author-reported and which I did not inspect.

Would it be simpler to make the source explicit instead of implying it from the helper, for example queryAppleDisplayInventory(device, { source: 'guest', foldHelper }) from readLitPanel and setAppleFoldPose only? isAppleFoldableSimulator and readFoldablePanelTable would keep the set-based source, foldHelper would go back to being a reuse handle, and single-panel simulators would keep their base route with no new evidence needed. The rest of the simplification looks good.

Not blocking, and fine to take or leave: PANEL_PICKERS.*.offset is read only by the diagnostic at orientation.ts:166, while the values maps already encode the offset, so the two can drift apart. Drop offset or derive values from it.

The earlier review (4a2d30b) is addressed in the rest of the delta. Three earlier doc threads still apply: device-foldable.md:4-5 panel-listing guidance, ADR 0025 stale contract at :378-381, and ADR 0025 removed budget name at :109. One lower-priority thread on test coverage still applies. The changeset thread does not apply, because that text refers to the live measurements in your report. Please resolve it.

This review was static only: no build, test or device run, and I did not read the external evidence or the check:affected receipt. Before merge, fix the routing probe (or add the cold-start evidence above) and update the three doc threads.

@sittingmongoose
sittingmongoose marked this pull request as draft November 12, 2026 16:06
@sittingmongoose

Copy link
Copy Markdown
Contributor Author

Fixed the routing blocker in 94cf7fd723bb454ccdbca87d2a04c1b3b4523eed using the explicit-source option:

  • isAppleFoldableSimulator and readFoldablePanelTable keep the set-based source. Default single-panel classification builds/spawns no helper.
  • Only rotation lit-panel reads and fold-pose reads request source: 'guest'. A supplied helper is again only a reuse handle.
  • A cold default Duo classifies from CoreDevice even when helper preparation would fail. Subsequent guest failure stays on the foldable route and returns orientation-unverified; it cannot report runner success.
  • Dropped the duplicate offset properties and diagnostic field, leaving the picker values as the sole mapping owner.

Fail-first on 20841c063: 2 failed (single-panel helper work and cold Duo misclassification). Fixed suite: 181 passed, 2 skipped, including the cold-helper/no-runner-success regression and helper-handle source test. Typecheck, format and focused lint pass. Logs and patch.

The four docs/test comments are fixed and all 14 threads are resolved. The measured-in-both-sets statement is retained with the linked live evidence, as your review confirms.

The full exact-head gate is running in durable job pr4440-r2-routing-final. It is not reported as passed. The earlier Duo runs are evidence for the unchanged guest dispatch/read path, with their original build SHA recorded; this new routing head has not been claimed live-tested or byte-identical. The offered base-source classification option was chosen, so no new single-panel live evidence or Mac run was added. The PR remains draft pending the final gate and independent CI approval.

@thymikee

Copy link
Copy Markdown
Member

The fixes from the earlier review (#4440 (comment)) are in place at 94cf7fd, and I found no new problems in the changes since 61c5a43. Default-set classification is back on the base devicectl source, and the lit-panel confirmation now covers both simulator sets.

The four earlier review threads are fixed at this head, so the author can resolve them: the foldable docs command routing (#4440 (comment)), the ADR 0025 classification text (#4440 (comment)), the probe budget name at ADR line 110 (#4440 (comment)), and the stale devicectl branch in fakeDuo (#4440 (comment)). The changeset thread does not apply, because that text refers to the author's live Duo measurements and not to the test fixtures (#4440 (comment)).

This was a read-only review of the diff. I did not build, run tests or use a device. The live Duo proofs for the guest controller are author-reported and predate this head, but the controller is unchanged, so they need no rerun. The cold helper failure claim is verified for the default set only: a scoped set still classifies from the guest table under the 15s probe budget, as it did on base.

The checks report no runs yet, because this is a fork PR and CI waits for maintainer approval. Before merge, a maintainer needs to approve the fork CI and see it pass, the author needs to share the exact-head check:affected result, and the PR needs to leave draft.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 12, 2026
@sittingmongoose

Copy link
Copy Markdown
Contributor Author

Thanks for the re-review. All review threads are resolved at 94cf7fd.

Exact-head check:affected at 94cf7fd (pnpm check:affected --base upstream/main --head 94cf7fd --run):

  • Static, build and related steps pass.
  • Unit: all 19,028 Vitest tests pass (70 skipped). The node smoke suite then fails one test: test/integration/smoke-web-platform.test.ts:103, "web shutdown cleanup reaps the exact daemon that survived graceful shutdown" (expected cleanup to escalate after the child ignored SIGTERM).

Same failure on upstream main, same host. To check whether this PR causes it, I ran pnpm build && pnpm run check:unit back to back on upstream main 9cbbae2 and on this head, with the same environment and worker count:

Tree Vitest Smoke suite
upstream main 9cbbae2 19,271 pass / 70 skipped fails the same smoke-web-platform.test.ts:103 assertion
this PR, 94cf7fd 19,028 pass / 70 skipped fails the same smoke-web-platform.test.ts:103 assertion

Run on its own, the test passes on both trees. So it fails on this host only inside the full run, and it does so on main too. This PR does not touch web or process code. I'll leave that one to your CI runners.

Marking the PR ready for review. Fork CI still needs maintainer approval.

@sittingmongoose
sittingmongoose marked this pull request as ready for review November 12, 2026 17:57
@thymikee

Copy link
Copy Markdown
Member

The earlier review at 94cf7fd was clean. At e79ae67 the fixes for the earlier inline threads have landed, but the new model guard needs live proof before merge.

orientation.ts:128 now refuses every multi-panel rotation with unsupported-foldable-model unless DeviceInfo.model equals the literal 'iPhone Duo'. That value comes only from the simctl devicetype name (simulator-inventory.ts:59-70). If the real name differs in either simulator set, or devicetypes are missing from the scoped-set listing, every Duo rotation that 94cf7fd confirmed is refused before dispatch. The unit tests set model: 'iPhone Duo' in the shared simulator() factory, so they cannot catch this. Please run on a real iPhone Duo simulator at this head, in the default set and in a scoped set. agent-device devices --json must show model "iPhone Duo" for the Duo UDID. Then agent-device orientation landscape-left on it must return orientation landscape-left, not unsupported-foldable-model. Please paste both outputs.

Could we drop the model guard instead? Multi-panel classification is already the admission, and the iPhone Duo is the only multi-panel simulator. ADR 0025 could say the picker offsets are measured on iPhone Duo and that a new multi-panel model needs its own mapping. If a gate is still wanted, would it fit better on the stable deviceTypeIdentifier in the fold fact owner (foldable/runtime.ts) than on the presentation name inside the controller? A model-based gate needs the DeviceInfo.model contract comment changed first, or a typed device-type field to carry the identifier. Dropping the guard also removes the need for the live proof above.

Not blocking, and you can take or leave these: readLitPanel now throws on any unresolved guest read, so one transient failed read mid-loop aborts a rotation the next read might confirm (it could retry inside the loop and throw orientation-panel-table-unavailable only when the final read is unresolved, or the ADR could say one failed read ends the rotation); and if the guard stays, the DeviceInfo.model comment in device.ts:59 and the "no model" comments in foldable/runtime.ts:23-26 and navigation/runtime.ts:153 should be updated, with both reasons added to the changeset.

For the open review threads: the guard thread at orientation.ts:128 still applies until the proof or the drop above, and the retry-loop thread still applies for the note above. These are fixed at head and can be resolved: orientation.test.ts:147 asserts empty classification reads, commands.md:128 scopes the helper route, pose.ts:50-53 matches base and is pinned by tests, device-foldable.md:3 reads as asked, ADR 0025:381 lost the dated line, and system/index.ts:293 names the new reason.

Only one external review check has reported, and it passed. Fork CI has not run because it needs maintainer approval, so no unit or provider-scenario job validates this head, and the unit and provider-scenario passes reported at 8db5091 are unconfirmed by CI. The reported smoke failure in smoke-web-platform.test.ts:103 is in web shutdown code this diff does not touch, and it reproduces on main. There are no conflicts. This was a read-only diff review: I did not build, run tests or use a device, and I could not verify the real simctl devicetype name of the iPhone Duo because no fixture records it. I could not tell whether transient guest reads happen during a live rotation. Before merge, we need the live Duo proof at e79ae67 in both sets, or the guard dropped.

@thymikee thymikee removed the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 12, 2026
@sittingmongoose

Copy link
Copy Markdown
Contributor Author

At 83a4ebb, dropped the model-name guard per your suggestion; multi-panel classification remains the admission. Transient unresolved guest reads retry inside the settle loop without dispatch or confirmation, and only an unresolved final read reports orientation-panel-table-unavailable. Docs and changeset are aligned.

Local checks: focused unit 187 passed / 2 skipped; fold provider scenarios 7 passed; typecheck, full format/check, lint, command docs, ADR citations, prose and guidance checks pass (39,961 / 40,000 bytes). No full gate rerun or device work. Both follow-up threads are resolved. Fork CI still needs maintainer approval.

@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 6 files (changes from recent commits).

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

View guided diff | Re-trigger cubic

@thymikee

Copy link
Copy Markdown
Member

I reviewed e2e124e against the earlier findings (#4440 (comment)). All of them are fixed, and I found no new problems in the code. The model guard is gone, so a multi-panel classification is the only way into the fold-helper route. Unresolved reads inside the loop now retry, and only the final read fails with orientation-panel-table-unavailable.

Three inline threads from the other review are fixed at this head and can be resolved: the fold-helper route scoping #4440 (comment), the model guard #4440 (comment), and the unresolved-read retry #4440 (comment).

This was a read-only diff review. I did not build, run unit or provider-scenario tests, or use a device, so the 187-passed local result is unconfirmed. I did not do a new live run at this commit. The diff from 94cf7fd to e2e124e keeps the dispatch and in-loop confirm route identical to the live-verified commit, and only the final error reason differs. Unit tests cover that error path. I could not tell whether transient guest reads happen during a live Duo rotation.

The only reported check, an external review check, passed. CI for the fork has not run, so no unit or provider-scenario job covers this head yet. No conflicts are known. A maintainer needs to approve the fork CI run on e2e124e, and those jobs must pass before merge.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 12, 2026
@thymikee

Copy link
Copy Markdown
Member

This branch now conflicts with main after today's merges. The review at e2e124e was clean (#4440 (comment)). Could you rebase onto main? I will re-check the rebased head.

@thymikee thymikee removed the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 12, 2026
sittingmongoose and others added 16 commits November 12, 2026 20:45
Dispatch through the fold helper and confirm from the lit panel's currentOrientation, choosing each picker value for the turn still needed because the guest applies picker events relatively. Map portrait-upside-down to the picker's pud wire spelling in the fold helper.
The outer panel reads the orientation-picker value itself (offset 0) and
the inner panel reads it plus 90 degrees. Measured on iPhone Duo (iOS 27.3)
against the panel table over 27 dispatches and five pose changes in both
directions: the picker value survives a fold, and only the lit panel's
offset changes. After a fold that left the picker at the upside-down
value, which the outer panel's UI could not show, one dispatch of the
target with the measured offset still landed it.

The guest model in the tests now follows that measurement: one picker
value, a per-panel offset, folds that keep the picker, and the few
seconds after a fold to closed when the table names both panels lit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…d foldable rotation

Review of callstack#4440:
- display-inventory owns the only currentOrientation parser
  (panelContentRotation); appleDisplayRotation and the scoped foldable
  rotation both use it, and picker-delta drops its copied map.
- The scoped loop has one target-minus-offset step. The offset comes from
  this command's last dispatch and a fresh reading of the same panel, and
  from the measured table only before that. The persistent panelOffset and
  the separate delta branch are gone.
- A read that names no single lit panel neither confirms nor dispatches;
  there is no blind calibration dispatch any more.
- The unverified error names only what the final read saw.
- Tests assert exact dispatch sequences; the measurement fixture keeps the
  one contradicting reading, marked as the exception.
- Docs, help text and changeset name the failure and qualify the model.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…move

Review of callstack#4440 (cubic): the offset re-derived from the last dispatch and
a same-panel reading assumed that dispatch had landed. A reading taken
before it landed, or a panel whose app cannot show the target, taught a
wrong offset; on the device a portrait-only app made the loop cycle
through six picker values, upside-down included. A reading that still
shows what the panel showed before the last dispatch now teaches nothing
and gets no further dispatch: the loop re-reads, and a panel that never
follows fails unverified after one dispatch.

Also qualify the attempt-budget comment and note that both panels report
lit for a few seconds after an unfold as well as a fold.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review of callstack#4440: readLitPanel filtered the parsed panels for a single lit
one, so a table with a second lit integrated entry that lacked identity
or geometry (which the parser drops) still produced a reading. The
rotation then confirmed, or dispatched compensation, without one
established lit panel.

display-inventory now owns the strict selection (establishedLitPanel): a
resolved multi-panel table that parsed every integrated entry, and so
named an active display, with exactly one lit panel, which is that
display. appleLitPanel and the scoped rotation both select through it;
the incomplete-table capture refusal is unchanged and picker calibration
stays in the foldable module.

Regression: an interactor test with two activeOn integrated entries, one
without geometry, for a target the parsed panel already shows and for
another target: no dispatch, no confirmation, orientation-unverified-
scoped-set. On c75affa it returned success in both cases.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review of callstack#4440 (cubic): establishedLitPanel counted only power 'lit', so a
lit panel beside a sibling of unknown power (an unrecognized backlight
state, which readPanelPower deliberately does not call dark) was still
established, and the scoped rotation could confirm or dispatch from it.
Any panel of unknown power now establishes none, for runner routing and
the scoped rotation alike; the capture fallback is unchanged.

Fail-first: on bfdbd9b the new selector test and both interactor cases
(target already showing, another target) failed - the rotation returned
success in both.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@sittingmongoose
sittingmongoose force-pushed the fix/foldable-scoped-rotation branch from a22cb8c to 92e5558 Compare November 12, 2026 20:45
@sittingmongoose

Copy link
Copy Markdown
Contributor Author

Rebased onto main d2426ff: new head 92e5558. (A merge commit went up for a few minutes before I saw your note; it has been replaced.)

  • The only conflict was website/docs/docs/commands.md, where main rewrote the back lines next to this PR's orientation line. I kept main's back text unchanged and kept each commit's own orientation line. Main had not changed that line. packages/command-surface/src/system/index.ts merged cleanly.
  • git range-diff against e2e124e: 11 of 16 commits are identical. The other 5 differ only in that docs context.
  • The rebased tree is byte-identical to a tested merge of the same two sides. That merge passed pnpm typecheck, pnpm lint, pnpm format:check, check:command-docs, check:mcp-metadata, check:agent-guidance, and 461 focused unit tests (platform-apple foldable, command-surface, CLI help). I did not run a full gate or a device run on this head.

@thymikee

Copy link
Copy Markdown
Member

The rebase onto main at 92e5558 resolves the conflict from the earlier review, and I found no new problems in the change. The diff stat matches the previous head, and the range-diff shows the same changes carried over. I also read the two files next to the old conflict and they merged cleanly. The one CI check is green and there are no conflicts. I did not re-run the tests. Fork CI still needs maintainer approval to run the unit and provider-scenario jobs on this head before merge.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 12, 2026
@thymikee
thymikee merged commit 15e298f into callstack:main Nov 13, 2026
17 checks passed
thymikee added a commit that referenced this pull request Nov 13, 2026
…baseline

* origin/main:
  ci: use smaller runners for control and publishing jobs (#4492)
  docs(help): point verification at read-only commands, not bare find (#4497)
  fix(android): let open --relaunch accept a package Android restarts at once (#4498)
  fix(snapshot): name the captured Android app in the snapshot header (#4499)
  feat(proxy): configure trusted public upload origins (#4501)
  fix(android): restore screenshot demo permission state (#4477)
  feat(harmonyos): support app uninstall beside deploy (#4292)
  fix(ios): confirm foldable rotation from the lit panel in either simulator set (#4343) (#4440)
  chore: mark 0.21.37 as released
  fix(ios): make back the verified left-edge swipe (#4460)
  fix(ios): mark zero-area nodes unmeasured and point their press refusal at scroll --until (#4495)
  fix(ios): never repeat a synthesized gesture that may have run (#4474)
  fix(capture): publish cropped screenshots atomically after scaling (#4478)
  ci(android): reuse cached system images across emulator workflows (#4490)
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.

orientation: a foldable in a scoped simulator set is refused, and the fold helper's rotate lands one command late

2 participants