---
name: foldsmith-duo
description: Adapt a whole existing iOS app to iPhone Duo (Apple's foldable, iOS 27.1), hands-free, from one prompt to every screen verified in every pose. Classifies each screen against a measured decision table, applies a named layout pattern, proves every pose with screenshots, and keeps .duo/migration.json current, so the user can follow it (in Claude Code the Foldsmith plugin's pane draws it) and a later session resumes. Use when the user mentions iPhone Duo, foldable iPhone, the fold or crease or hinge, book or laptop pose, vertical toolbars or tab bars, ArrangementView, ReservedRegion, "squeezed when half open", or asks to make an app work on the Duo's inner and outer displays. Also audits an app without changing it, and exports the App Store's iPhone Duo screenshots from its captures.
license: MIT
---

# Foldsmith: layout patterns for iPhone Duo

iPhone and iPhone Duo are trademarks of Apple Inc. Foldsmith is independent and is not affiliated with, sponsored by, or
endorsed by Apple Inc.

This skill routes; the knowledge lives in `references/` (the pattern pages in `references/patterns/`). Do not improvise a layout rule that is not in the decision table.

Paths here (`references/`, `tools/`) are relative to this skill's folder, wherever it is installed (`~/.claude/skills/`, `.agents/skills/`, `.cursor/skills/`, a plugin); run the tools from the app's repository by that folder's path, with their interpreters (`bash …/tools/capture-app.sh`, `python3 …/tools/progress.py`), since an installer may not keep the executable bit. Nothing here needs Claude Code: the tools are shell, Python and Swift, and the progress file is plain JSON.

## What was asked
- A question about the Duo (a pose, an API, a pattern, a defect class): answer it from `references/` and run nothing.
- "Audit" or "check" (or `/foldsmith-audit`): the audit only. Start below, then steps 1-4, with the fix each screen would get in `.duo/PLAN.md` (in its Local or its Structural section, as step 4 classes it; "Progress file" gives the shape), then the App Store set from the cells already fine (`python3 tools/appstore.py --kind before`). No code change, no branch, no commit; the progress file stays at phase `classify`, where `/foldsmith-start` takes it up later. An app with no scene lifecycle (T13): say so first in both files and stop. A progress file already past `classify` (a migration's: `migrate`, `verify` or `done`): capture nothing into `before`, change no phase, and report from the file what is left (`python3 tools/progress.py check` and its `todo`).
- Only a request to adapt or migrate the app (or `/foldsmith-start`) runs the migration. The hands-free rules below are the migration's; an audit asks nothing either.

## Hands-free
One prompt takes the whole app from audit to verified. The user follows `.duo/migration.json` (in Claude Code, the Foldsmith plugin's pane draws it) and can interrupt at any time; you do not wait for them.
- Never ask a question or stop for approval. Where a choice is needed, take the one the decision table and the pattern page give, and write why in the log.
- Never stop at a problem. Record it in `todo` with the reason, and go on to the next screen. The run ends only when every screen is verified or listed in `todo`.
- Make every local change; a structural one only when the person asked for it (`/foldsmith-start --structural`, or in words). Which is which: `references/decision-table.md`, "Local and structural changes". A structural change not made gets a proposal in `.duo/PLAN.md`'s Structural section (what, why, the cells it fixes, the routes `bash tools/routes.sh <app folder> <types it touches>` lists, its commits) and a line in `todo`; the cells only it would fix stay `finding`, labelled `T14 · structural: .duo/PLAN.md`.
- `.duo/PLAN.md` has two sections. `## Local`: one item per screen, the pattern and the fix the migration makes; as each is committed it is marked made (`made: <commit>`), or moved to `todo` with why. `## Structural (proposed)`: the proposals above, which only `--structural` makes, and only those. An audit writes both; a migration that takes up an audit keeps them apart, so a later `--structural` run and the report never read a local fix as a proposal.
- Order: every local change on every screen first, then steps 6 and 7, each committed, so `duo-migration` can be merged alone. Structural work, when asked for, comes after, one screen group at a time, each revertible alone: `phase migrate`, the change and every route `tools/routes.sh` lists in one commit, then steps 6 and 7 again for its screens.
- Commit in the app's repository after each change, on a branch `duo-migration`, so each can be reviewed or reverted on its own: `Duo: <screen> → <pattern>`, naming every screen the change fixes when it fixes several (`Duo: timeline, article → Fold-aligned split`: one split view serves both). Commit `.duo/migration.json`, `.duo/flows/`, `.duo/PLAN.md` and the reports with it; keep `.duo/captures/`, `.duo/appstore/`, `.duo/env` and the `.duo/.*.lock` files out of git (`.git/info/exclude`). Never push.
- Never create, clone or erase simulators.

## Start or resume
- First `bash tools/preflight.sh <app folder>`, at every start and every resume, in a shell that has not sourced `.duo/env` (its paths point into this skill's folder, which moves when the skill is updated, hence every resume; a value inherited from `.duo/env` all the same is derived again, not kept): read-only checks of this Mac and the app's repository (`references/simulator.md`, "Before the first capture"). If it blocks (exit 1), end the run with its fix list: each `missing` line carries its fix, and the fixes are the person's, with one exception: on `duo-migration`, uncommitted changes outside `.duo/` are the migration's own (a change an earlier session left unfinished, or a build's side effect such as a rewritten `Package.resolved`). Finish and commit that change, or put it aside with the stash line the preflight prints (it keeps `.duo/`), log which, and run the preflight again. Otherwise it writes `.duo/env` (`DUO_UDID`, `DUO_PY` = the bundled driver `tools/driver/duo.py`, `DUO_TAP_XCTESTRUN` once the tap runner is built): start every other tool call with `source .duo/env &&`. A `note` that the tap runner is not built (or is older than its source) prints the `xcodebuild` line that builds it: run it, then the preflight again.
- If `.duo/migration.json` exists, resume from it: skip every cell already `verified` or `unchanged`, keep `done`, take `todo` from the top, and clear what `progress.py check` lists (an open issue too). Never redo finished work. A `needs sign-in` screen in `todo`: check it again; reached, carry on hands-free without reinstalling, and run no flow with `APP_PATH` (it uninstalls the app, signing the person out) until those screens are captured into `after` (`references/simulator.md`, "Screens behind a login").
- Otherwise start at step 1: `python3 tools/progress.py init <app> --simulator "$DUO_UDID"`.
- Full captures run in the background; read the exit code: 0 ok, 2 a wrong call (`.duo/env` not sourced, an `ONLY` or `SCREEN` that names nothing; nothing was touched), 3 cells not captured (a skip, a failed shot or snapshot), 4 disk (put it first in `todo` and end the run; delete nothing), 5 locked (another capture is running: wait), 128 + n stopped by a signal. Never start a build or a second capture beside one. After any stop (a signal, a shell's time limit, exit 4 once there is space), run the same command with `RESUME=1`: it keeps the cells already captured and takes the rest (`references/simulator.md`, "Long captures").

## Workflow
1. **Find the screens.** Run the static scans (`references/detectors-static.md`) first. An app with no scene lifecycle (T13) cannot launch on the 27.1 SDK: adopt it with Apple's `app-resizability` skill if it is installed and commit that (`Duo: scene lifecycle`); otherwise put T13 in `todo` and end the run, since nothing can be captured. Read the app's navigation (root view, tabs, navigation destinations, sheets, settings) and list the screens a user spends time in: each top-level destination, each list and its detail, and the sheets a user opens often. Write `.duo/flows/<app>.flow`: `BUNDLE=<bundle id>`, optional `LAUNCH_ARGS=…`, optional `APP_PATH=<the built .app>` (only for an app that must start fresh at every screen, one that remembers its onboarding: the capture uninstalls the app and installs it afresh before each screen, clearing its data), and one `"<screen>=<tap>;<tap>"` line per screen in `SCREENS=( … )` (tap syntax: the header of `tools/capture-corpus-flow.sh`). Build the app unmodified for the simulator against the iOS 27.1 SDK, into a folder outside its repository so the build leaves nothing for the preflight to find (`source .duo/env && xcodebuild -scheme <scheme> -destination "platform=iOS Simulator,id=$DUO_UDID" -derivedDataPath ~/Library/Caches/duo-build/<app> build`, with `-project` or `-workspace` as the app has), and install it (`xcrun simctl install "$DUO_UDID" ~/Library/Caches/duo-build/<app>/Build/Products/Debug-iphonesimulator/<App>.app`; the simulator must be booted: `python3 "$DUO_PY" panels` boots it when it is not). Check each line reaches its screen with one `closed` capture into a scratch folder (`source .duo/env && ONLY=closed bash tools/capture-app.sh .duo/flows/<app>.flow .duo/captures/<app>/reach`); it exits 3 and names each screen it could not reach. Rerun one that was skipped once (`SCREEN=<screen>`); still skipped, it goes to `todo` with the reason and its line moves out of the main flow, so the captures that follow are not stopped by it; do not guess at it. One that needs an account goes as `needs sign-in: <screen>`, its line into `.duo/flows/<app>-login.flow` for the second pass, once the app's UI-test targets offer no way in: look there for launch arguments and mock or demo modes (`rg launchArguments`, `-UITest`, `mock`) and use them in `LAUNCH_ARGS`. Never type credentials. A screen that shows its layout only with content (saved items, open tabs, a list the person fills) gets some before the audit, made through the app itself with the tap runner (save, open, add; never by writing its files, never by signing in), and logged. The progress file uses the flow's screen names: the capture records its shots under them.
2. **Audit before touching code.** With the unmodified build installed (step 1), capture every screen in every pose: `source .duo/env && bash tools/capture-app.sh .duo/flows/<app>.flow .duo/captures/<app>/before` (`references/simulator.md`). It runs the detectors (`tools/detect`) and writes each shot's path to its cell as `before`. Each cell gets `finding` (label = the taxonomy ids, all of them: `T14 · T7`) or `unchanged`. Write `.duo/audit.md`: one row per screen × pose with the taxonomy class ids found (`references/taxonomy.md`). Look at every shot: a defect the detectors miss is still a `finding`, and a detector line the shot does not bear out is not one (log why): a map's pins and place names pan, so T1 on them is not a finding; T7 where the same screen is there in another presentation (an overlay with Close on the cover, inline on the inner display) is not one either. A capture line saying the shut phone lost the screen after a pose is evidence for T7 (state lost on the fold) in that pose; an app that crashed on the way to a pose is T7 there too (the detectors report it; find the crash report). If the capture exits 3, some cells were not captured (skipped, or no shot or snapshot): capture them again once (`SCREEN=<screen> ONLY=<poses>`, same folder) before you write the audit, never count them clean; a cell still missing goes to `todo` with the reason, as in step 1.
3. **Classify each screen.** Decide content relationship (hierarchical / peer / supporting / continuous / object) and interaction mode (viewing / editing / controlling) from the model and the screen's purpose, not from its current layout. A feed is classed by what its items are (the decision table's I1): uniform rows that each open a page are hierarchical, cards complete where they stand are peer. A map is continuous and no pattern reflows it: it stays whole, and only what sits on it (a search field, a card) moves off the fold, by the decision table's map rule; that is a fix to make (cells `proposed`, label "map rule"), not "no pattern". Record the reasoning in `.duo/audit.md`.
4. **Look up the pattern.** `references/decision-table.md` maps (relationship, mode, pose) → pattern. A cell marked `UNJUSTIFIED` counts as no cell; a pose with no finding there stays `unchanged`. Pages exist for **Fold-aligned split**, **Even grid**, **Reflowing columns** and **Arrangement** (`references/patterns/`), each with its reference implementation beside it (`FoldAlignedSplit.swift`, `EvenGrid.swift`, `ReflowingColumns.swift`, `Arrangement.swift`). Mark each cell a change you will make fixes `proposed` (label = pattern). A screen with no cell, or one whose pattern has no page yet (Whole object), goes to `todo` as "no pattern yet" with its findings, and is left as it is. Classify each planned change by the pattern page's **Change** line: a structural one not asked for goes to `.duo/PLAN.md`, its cells left `finding`, not `proposed` (Hands-free).
5. **Migrate, one screen at a time.** Make the changes step 4 planned (a structural one only when asked for). Apply the reference implementation beside the pattern page, gated as its **Change** line says. For P1 in SwiftUI use `FoldAlignedTiledSplit` (with `FoldAlignedSplit.swift`) in place of the app's `NavigationSplitView`, which keeps both panes upright; keep the selection and any sidebar path in the caller. An app whose split is a `TabView` sidebar keeps it (the page's Solution): add `readsBookFold()` on the window's root and `foldAwareSheetPlacement()` on each sheet, and fold with every tab selected. A UIKit app follows the page's UIKit section where it has one (without a split view controller, that section's container is structural); otherwise translate the SwiftUI reference and log how. Bar items that must go vertical are symbols with a title, or custom views set to `axisBehavior = .verticalPreferred`; where the vertical bar is short, choose what stays with `visibilityPriority` and put the app's own menu items in the bar's overflow (`additionalOverflowItems`) rather than nesting a More in it. A custom view turned into a titled item reaches every device, so it is structural unless gated where the window has a vertical bar: keep the custom view and set `.verticalPreferred` (local). Where it must become a titled item, keep its accessibility label, the state it drew (Wikipedia's open-contents button lost its fill everywhere) and its enabled state in the overflow. An article in a web view follows Reflowing columns' "When the text is a web page" (structural). For P4 use `Arrangement` (`.split` for two views neither may cover, `.overlay` for controls over scrolling content) in place of the app's `HStack`, `VStack` or `ZStack` of the two views, inside the screen's navigation container, only where the window has a vertical bar or a reserved region, the app's own stack elsewhere (`ArrangementView` lays the views out by the window's shape on every iOS 27.1 device; the system's layout everywhere is structural: when asked for, capture the screen on an iOS 27.1 simulator without a fold too); a UIKit app uses `UIArrangementViewController` (the page's Reference implementation). A hierarchy with three levels (sidebar, list, detail) gets P1 with two panes, the list pane and the detail: unfolded from the sidebar, the sidebar sits beside the detail, and a picked item pushes the list onto the list pane (the page's "Three levels", structural even in the app's own split; a UIKit triple-column split follows its UIKit section). Probe the poses a change touches while you work (`references/simulator.md`: about 25 s a shot, or `SCREEN=<screen> ONLY=<poses>` for a screen behind taps); the full capture is for step 6. Where a container is called for, prefer the system's (`NavigationSplitView`, `TabView`, `ArrangementView`) over custom geometry. Keep navigation containers outside `ArrangementView`. Never put `ArrangementView` inside `List`/`ScrollView`. Gate iOS 27.1 APIs behind `#available` with the pre-Duo layout as fallback, and gate what you change for the Duo the same way even where its API is older (`visibilityPriority` is iOS 27.0, `additionalOverflowItems` 16.0), on the condition that calls for it (a vertical bar, a reserved region), and behind `#if os(iOS) && !targetEnvironment(macCatalyst)` in code that a Mac Catalyst, visionOS or macOS target also compiles (their 27.1-era SDKs have no reserved regions; the reference files do this already); build those targets too. A new container (structural) wraps the screen on every iOS 27.1 device, so without a reserved region the app must still behave as before. Never branch on device identity, idiom or orientation. Build after each change; cells become `migrated`; commit.
6. **Prove it.** Rebuild, install the build over the old one (`xcrun simctl install`, as in step 1), recapture every pose with the same flows into `.duo/captures/<app>/after/` (`capture-app.sh` runs the detectors over it too, into `after/detectors.md`), and look at each shot. `capture-app.sh` writes each shot's path to its cell as `after`. A cell with no finding left becomes `verified`. A finding that remains gets one more fix; if it is still there, the cell stays `finding` and the screen goes to `todo` with what is wrong. A screen must never end worse than it started: a finding in a pose that was clean before, or a shot that reads worse (text cut off at a pane's edge or broken mid-word, a control gone, a pane's content stopping short of its edge), is a regression. Fix it, or revert that change for the screen, before anything goes to `todo`; `todo` holds what is left, never what the run broke. `done <screen>: …` and `phase done` refuse while `progress.py check` fails for it: a pose worse by the detectors (a class new to the pose too), a cell left `unchanged` whose snapshots differ, an open regression issue (`references/simulator.md` says how each is cleared; a detector line the shot does not bear out is waived, and the report says why). Record every finding no detector table shows (review, look, test) when you find it, with `progress.py issue add` (`--regression` for anything worse than before: fixed or its change reverted, never left). Most of what Wikipedia's reviews found, no detector reads, so look for it on purpose: in every changed pose measure each pane's controls and rows against the pane's edges in the snapshot (a contact sheet hides 84 pt), check that nothing is clipped, and check that every control the change moved (into an overflow, out of the vertical bar, into another pane) still works and is still reachable, opening the overflow to see it; say where each one went, and count each action that now takes more taps as a cost in the report. Then read your diff for what no shot shows (`bash tools/routes.sh <app folder> <types you changed>` lists routes and walks): every walk of the hierarchy you changed (`as? UINavigationController`, `selectedViewController`, `presentedViewController`, `viewControllers`: state restoration, theming, routing, coordinators handed a navigation controller), every other route into a changed screen (user activities, deep links, notifications, not only the flow's), what a view you added covers (hide it from accessibility), and every state that follows the screen it belongs to (a pane showing one article's contents after another opens). If you can start a subagent, give a fresh one the diff and the before and after captures, not your conclusions, ask it for regressions, record each one it confirms (`issue add --regression`), and fix it: on Wikipedia each such review found regressions the run had passed, among them state restoration and theming broken on every iOS 27.1 device. A screen whose cells are all `verified` or `unchanged` goes to `done`.
7. **Report.** Set `phase` to `done` and write `.duo/REPORT.md`: per screen the pattern, the before/after shots per pose, what is left and why (every open issue among it), and which screens wait on a structural change in `.duo/PLAN.md`. If a corpus rule or an API name in `references/` does not match the SDK you compiled against, say so and prefer the compiler. Export the App Store set: `python3 tools/appstore.py`. With a `needs sign-in` screen left, the report and your closing message say: sign in on the Duo simulator yourself, naming it as the progress file does (`iPhone Duo (<its UDID>)`; the next preflight takes that one), then run `/foldsmith-start` again.

## Poses to capture, always, in this order
closed · closed-sideways · book · laptop · tent · flat-sideways · flat-upright. These are Apple's six HIG poses (closed, book, laptop, tent — standing on its edges —, flat upright, flat sideways), plus the outer display sideways. A screen is done only when every one is captured and verified. Pose control: the `duo.py` driver (see `references/simulator.md`); tent is laptop's fold turned hinge-up (`duo.py fold laptop`, `orient landscape-right`). Split View's half window cannot be driven headless: lay out from size classes and it follows.

## Progress file
After every step, and after each screen within a step, update `.duo/migration.json` in the app's repo, so the user (or the Foldsmith pane, in Claude Code) and the next session see where things stand. Change it only through `tools/progress.py` (`phase`, `screen`, `cell`, `todo`, `done`, `waive`, `issue` (`add`, `fix`, `wontfix`), `log`, and `check` to read what keeps a screen from done; `todo --remove` and `done --remove` take a wrong line back; `references/simulator.md`), which locks it and writes it whole, so a capture running beside you loses nothing and the pane never reads half a file.
```json
{
  "app": "Mail",
  "phase": "screens | audit | classify | migrate | verify | done",
  "simulator": "<Duo simulator UDID>",
  "poses": ["closed", "closed-sideways", "book", "laptop", "tent", "flat-sideways", "flat-upright"],
  "screens": [
    { "name": "Inbox", "pattern": "Fold-aligned split",
      "cells": { "book": { "state": "verified", "label": "split",
                           "before": ".duo/captures/Mail/before/inbox-book-inner.png",
                           "after": ".duo/captures/Mail/after/inbox-book-inner.png" } } }
  ],
  "done": ["Inbox: Fold-aligned split, verified in every pose"],
  "todo": ["Viewer: no pattern yet (Whole object); T1 in laptop", "Message: verify tent"],
  "issues": [{ "id": "I1", "screen": "Message", "poses": ["book"], "severity": "medium", "regression": true,
               "found_by": "review", "what": "Reply opens in the list pane", "status": "open" }],
  "log": ["newest last, one line per action"]
}
```
Cell `state`: `pending` (not captured), `unchanged` (fine as is), `finding` (label = the taxonomy ids), `proposed` (label = pattern), `migrated`, `verified`. A pose with no cell counts as `pending`. `before` and `after` are paths relative to the app's repo; leave them out until the shot exists. `todo` is what is left, in the order you will do it; `done` is what is finished; `issues` are what no detector table shows, `open` until `issue fix` or `issue wontfix`.

## Hard rules (from Apple, cited in references/apple.md)
- Layout from size classes and reserved regions; hinge angle only for effects.
- Continuously scrolling content does not displace across the fold.
- Bars: use navigation-container toolbars; every item needs title + symbol; don't override default placement; overflow priorities on frequent and status-bearing items.
- Laptop pose: viewed content on the upper half, controls on the lower half. Book pose: alerts move to the trailing half, and sheets slide off the fold (the patterns put them trailing; T15).
- Request inactive division regions when a column count or split depends on the fold.
