DuckTerm Field Notes

A 21h 54m goal-driven engineering story

From iOS-only to Android-ready

A nearly autonomous cross-platform port: from Ghostty black frames and silent Mosh input to Chrome permission detective work, Google Play delivery, RevenueCat catalog closure, and three devices cross-examining one another.

  • Goal-driven
  • iOS → Android
  • Ghostty + Mosh
  • Play + RevenueCat
  • Evidence first
Cross-bridge from iOS terminal to Android terminal The phone on the left and the tablet on the right are connected by a bridge of code, testing, and evidence. iOS / Ghostty $ ssh duck@host PARITY_READY scroll · ime · tui Android / Ghostty $ mosh duck@host SAME_SESSION_OK pixelcopy · pip · pad GOAL fail-closed first 403 ≠ Doomsday evidence or it didn’t happen
21h54mLogical Goal, the time taken is not single process uptime
4,441Tool calling pattern, not independent test number
3API35、Pixel API30、Xiaomi Pad
30/30The final connected suite snapshot is not added to the old snapshot
259Visual inspection calls within the Goal window

It started from a seemingly simple request: for all the good things that iOS already has, Android should also have them, right?If this sentence is handed over to traditional project management, it will grow into a row of Jira; handed over to this Goal, it will grow into Ghostty native terminal, Mosh real UDP, Pad large screen, PiP, Widget, notifications, Google Play and RevenueCat - plus a duck that gradually learns to look at screenshots, open a browser, check 403, and doubt its own evidence.

01 / THE CONTRACT

Android is not iOS wearing a green jacket

The most common way to make mistakes in the so-called "adaptation from iOS to Android" is to copy the screenshot of the page pixel by pixel.What really needs to be migrated is the product contract: the user can only enter a Chinese character once to the remote end; SSH cannot be disconnected if the renderer fails; the new frame must be seen when returning from the background; the current plan must be recognized for subscription recovery; the Pad is not a mobile phone that is enlarged horizontally.

So each capability is translated into three layers: shared semantics, platform implementation, and evidence boundary.iOS can use its own viewport primitive, and Android can use Ghostty ABI 3; but "finger down to browse the older history and click to return to live" must be consistent.

flowchart LR
  I["iOS already has the ability"] --> C{"shared product contract"}
  C --> A["Android native implementation"]
  A --> D["Platform differentiation strategy"]
  D --> V["Three devices and real service verification"]
  V --> R{"Evidence passed?"}
  R -- "no" --> F["Locate the root cause and narrow the claim"]
  F --> V
  R -- "yes" --> S["Mini-batch review and atomic submission"]
            
Figure 1: Instead of copying the implementation, migrate the contract.Failure will return to the verification loop instead of "looks about the same" release.
⌨️

Enter the contract

IME composition remains local and committed exactly once; physical keys, Ctrl, Alt, and sticky modifier cannot be serialized.

🖼️

rendering contract

Wide cell, selection, snapshot, scrollback and cursor must be consistent on the real frame.

🛟

failed contract

GL fails to cut xterm, but SSH transport, ring history and user input remain alive.

💳

business contract

Monthly payment, annual payment, and lifetime are mapped consistently in Play and RevenueCat. When configuration is lacking, it is better not to sell than to guess the SKU.

Cross-platform is not about "looking the same", but "keeping the same promise even when something goes wrong".

02 / THE NATIVE LINE

Make Ghostty fail before letting it succeed

The first rule of Android Ghostty is a bit counter-intuitive: never start out optimistic.Synchronous constants are always set to `ok=false` first. The asynchronous self-test must prove that native core, ABI, VT, EGL, shader, texture upload, draw and readback are all true before switching from xterm to Ghostty.

This avoids the classic mobile metaphysics of "dark it first and then talk about it" in the first frame.There is also a 2.5-second upper limit for health certification: if the GPU really wants to enter meditation, the user can watch the spinner for up to 2.5 seconds, and then get a working xterm instead of a Zen-like permanent blank.

stateDiagram-v2
  [*] --> Pending
  Pending --> Ghostty: health proof succeeds before timeout
  Pending --> Xterm: timeout or proof fails
  Ghostty --> Xterm: runtime GL failure
  Xterm --> StableSession: same transport and ring replay
  Ghostty --> StableSession: native renderer active
  StableSession --> [*]
  note right of Ghostty
    Native VT plus full GL path
  end note
            
Figure 2: Renderer is a replaceable surface, not a transport.Even if GL fails during operation, the session continues.
GHOSTTY_HEALTH
core.ok=true
gl.maxRgbDelta=2

$ printf '中文 😀 é'
中文 😀 é

same session
FORCED_GL_FAIL
renderer unhealthy
→ xterm fallback

$ echo STILL_ALIVE
STILL_ALIVE

connect count: 1
MOSH_ROAM
Wi-Fi off
DURING_OK
Wi-Fi on
AFTER_OK

same PID · same UDP

The terminal film is a representation of the article restoration, not the original image of the operation background; markers, results and boundaries are from real equipment evidence.

Those hard bones that are "easily" put together

Input method composition, xterm key sequence, touch selection, ActionMode copy, native scrollback, background compositor, PixelCopy probe, CPU snapshot, Pad keyboard focus mode... each item looks like P3 individually, but when stacked together, it determines "is this a terminal or a flashing black rectangle".

03 / THE BROWSER DETECTIVE

The browser had access, but no ready-made key

The permissions of Play Console are in the logged-in Chrome profile; the built-in Playwright is a clean profile and nothing can be seen; CDP has not been opened yet.The easiest answer is "ask the user to do it manually".Goal chose another path: first find out which browser context has permissions, and then compress user participation into a necessary consent.

Browser permission detective diagram Agent identifies Chrome consent through screenshots. After user authorization, CDP takes over the page and then connects to gcloud, Play API and RevenueCat. chrome:// remote debugging Allow remote debugging? User allows CONTROL PLANE ✓ CDP snapshot + click ✓ gcloud API enablement ✓ Play publisher SA ✓ RevenueCat v2 secrets stay outside repo snapshot then click
Figure 3: CDP control page, cannot control its own authorization pop-up window.Screenshots and OS automation are responsible for reaching consent, and users are responsible for trust.

If there is no ready-made bridge, generate one in the temporary directory: MCP SDK + `chrome-devtools-mcp` + thin stdio client, and then use persistent PTY to continuously send `snapshot → click → snapshot`.It is not packaged as a "platform capability" and is cleaned up after completion; the truly reusable conclusion is a dedicated profile, origin allowlist, fixed version and default desensitization.

A practical joke: 403 doesn’t necessarily mean that Google doesn’t love you.It could be an OAuth scope, GCP IAM, Play App permission, Billing permission, or the permission is still on the way.Layer first and try again; don’t use the refresh button as a rosary.
sequenceDiagram
  actor U as user
  participant A as Goal Agent
  participant B as Chrome
  participant G as gcloud
  participant P as Play Publisher API
  participant R as RevenueCat API
  A->>B: Found that there is already a login state and target account context
  U->>B: Explicitly allow remote debugging
  B-->>A: CDP page control available
  A->>G: Enable the API and create two sets of least privileged identities
  A->>B: Bind app permissions in Play Console
  A->>P: Create catalog idempotently and upload internal AAB
  A->>R: Create Android app and product mapping
  P-->>A: track and artifact status
  R-->>A: entitlement and offering status
  A-->>U: Request only business facts and irreversible release decisions
            
Figure 4: Browser, Cloud IAM, Play permissions and RevenueCat are the four control planes.Mixing them into one "Login Successful" will only yield more 403s.
04 / THE EVIDENCE LAB

A black screenshot is not a GPU obituary

The most dangerous moment for Android phone verification is when the first screenshot looks very convincing.SurfaceView may be completely black in system screenshots; Xiaomi screencap may be 0 bytes; screenrecord may only record the world without hardware layer.Conversely, when the remote tmux sees the marker, it only proves that the transport is alive, but not that the local compositor drew it.

Three-Equipment Evidence Laboratory API35 emulator, Pixel 4 and Xiaomi Pad are respectively responsible for new system, old system real machine and OEM large screen verification. API35 emulator New API · contract Pixel 4 API30 physical IME · FCM · SQLite Xiaomi Pad API33 · MIUI master detail Pad · Surface · rotation
Figure 5: The three devices are not the same test multiplied by three, but three complementary assumptions: new API, old real machine, and OEM large screen.

Therefore, the evidence is designed as multiple oracles: unique marker, UI tree, PixelCopy, CPU raster, PID, dumpsys, logcat, and remote pane.They must falsify each other, not line up and applaud.

flowchart TB
  M["Inject unique marker"] --> U["UI tree and control status"]
  M --> X["Screenshot or PixelCopy"]
  M --> S["PID · dumpsys · logcat"]
  M --> T["Remote SSH/Mosh/tmux status"]
  U --> C{"Are multiple oracles consistent?"}
  X --> C
  S --> C
  T --> C
  C -- "no" --> A["Classifying test artifacts"]
  A --> N["Replace nonce and oracle and rerun"]
  N --> M
  C -- "yes" --> B["Write bounded conclusions"]
            
Figure 6: The more evidence, the better, but the more independent the source, the better.When there is a conflict, first doubt the collection chain, then doubt the product, and also doubt your first judgment.
phenomenon seenAlmost came to the wrong conclusionLast used orthogonal evidence
Pad screenshot all blackGhostty compositor hangs againPixelCopy + CPU bitmap + generation
marker appears twiceEntering exactly-once failsThe new nonce proves that the timeout batch overlaps with the rerun
Wi-Fi disconnected and restoredMosh completes cross-NAT roamOnly declare the same session to be restored; the real roam depends on the network identity
Metro cold start ANR2.5 seconds renderer defer stuckNo recurrence of embedded/cached bundle cold start
05 / THE LONG GOAL

The truth about ‘fully automatic’: people appear only where they add value

The logical duration of the Goal this time is 21 hours, 54 minutes and 42 seconds, but the machine uptime is shorter.It is not a mysterious process that insists on not sleeping, but relies on Git, tmux, pollable tools, plan status, review handoff, and external system status to continuously restore context.

Users in this round have very little actual participation: log in to RevenueCat, complete a small number of clicks in the Chrome/Google Developer Console that must be confirmed by the account holder, and give explicit authorization for irreversible actions such as commit/push.Login status discovery, most Play form filling and fact checking, gcloud/permission opening, API generation, device verification, purchase/restoration testing, screenshot recording, error recovery and sorting are all closed loop by Goal itself.

flowchart TB
  O["Stable Goal goal"] --> P["Stage plan and evidence threshold"]
  P --> G["Git atomic commit"]
  P --> T["tmux and pollable tools"]
  P --> E["Device and external service status"]
  P --> R["independent reviewer verdict"]
  G --> C["Continue after process or boot change"]
  T --> C
  E --> C
  R --> C
  C --> P
            
Figure 7: The persistence of a long goal comes from a recoverable state, not from a process that never exits.

Read the machine first, then write the code

The browser, CLI, device, login state, existing iOS contract and warehouse dirty state are all identified first.

Guarantee a way out first

Ghostty, Mosh, Billing, and OTA/native skew all define the direction of failure before enabling new capabilities.

Let real devices and services do the talking

Connected tests are followed by real SSH, UDP, FCM, Play, RevenueCat and Pixel layers.

Each high-risk line passes through the sentinel individually

P1/P2 are not backlogged to the end; the reviewer accepts the code and then the device evidence.

Accurate whitelist submission

The parallel work tree is not entrained, and temporary profiles, media, fixtures and secrets are cleaned up in time.

06 / THE SURPRISE LEDGER

Ten things the final diff does not tell you

🕵️

Find the permission entrance yourself

Determine which account can operate from the existing Chrome context instead of requiring the user to re-account the environment.

📸

If you don’t have CDP, take a screenshot first.

When the OS layer reaches consent, the user only performs the necessary authorization once, and then switches back to DOM automation.

🧰

If you lack tools, make the smallest tools

On-site generation of MCP, Play catalog, AAB upload and RevenueCat reconcile clients.

🔐

Two SAs, no master key

Billing and publisher are separated, and GCP IAM and Play App permissions are hierarchical.

🧯

Forced failure will not lose the session

The renderer crashes and the surface is changed, while the transport and history continue to work.

🎞️

If the video is hacked, the evidence will be replaced

Don’t use a broken collection chain to draw conclusions about a product.

🧪

DEV seam succeeds in not counterfeiting products

Debug injection still goes through the product manager/native gate, and DEV Pro is improperly purchased as evidence.

🧭

Actively narrow the statement

Wi-Fi recovery is recovery, not impersonation of a different NAT roam.

🧹

Cleaning is also delivering

Screenshots, videos, profiles, fixtures, DB, rotation and input method states are restored.

🪶

Make complexity reusable

JSONL forensics, CDP, device evidence, Play/RC bootstrap are all precipitated into manuals.

07 / THE REUSABLE PLAYBOOK

What can the next app copy?

Goal-driven Porting, v1

  1. When writing a contract, don’t write “Do as iOS does.”Clarify success, failure, fallback, and user-visible semantics.
  2. Find the authority first.Who has the final say on browser login status, Cloud IAM, store permissions, devices and services.
  3. First fail-closed.New native, billing, and OTA skew must have old paths that can work.
  4. If you lack tools, make thin bridges.Only the last mile is solved, and the default is temporary, observable, and destructible.
  5. Test the equipment by hypothesis.Emulators, old real machines, and OEM large screens all have their own tasks, not quantity games.
  6. Unique marker + multiple oracles.Pixel, structure, system, transport and remote end must have at least two mutual authentications.
  7. Think of 4xx as a map.Scope, IAM, App permission, Billing permission hierarchical positioning.
  8. Each danger boundary is reviewed individually.Close P1/P2 in small batches and run the real service again.
  9. Show up when you need a real person.This round actually only has login, a few consent/console clicks, and git side-effect authorization; if other projects involve MFA, legal facts, or production, these irreplaceable boundaries will be left to others.
  10. Clean up and write down the boundaries.Leave no secrets, fixtures, exaggerated “passes” and unrepeatable heroic stories.

The last counter-intuitive conclusion

The truly communicable AI engineering story is not "it worked for 22 hours straight", but "it knew when to do it on its own, when to look for evidence, when to admit that screenshots are not trustworthy, and when to have people click on it."The upper limit of automation is not determined by the number of clicks, but by the sense of boundaries.

The best Agent does not make every decision for you; it compresses your involvement to the few moments that truly require a call.