The step printed report.md and the guard log before the token search, so a leaked token would have reached the log first (GitHub masks the exact value there, but nothing from the session should be printed before the check). A dry run with a stub claude that writes the token into its report now prints nothing from it and deletes the files.
Written with simctl spawn defaults write, with and without a simulator restart, they changed nothing in runs 38729704027 and 38734049940: typing still capitalized, autocorrected ("Citester" became the profile name "Forester") and predicted. Removing them saves the restart; the Claude prompt says to check typed text in the tree.
Claude Code runs read-only commands (echo, cat, env) without asking in every permission mode, and Bash inherits CLAUDE_CODE_OAUTH_TOKEN, so allow rules alone could not stop a session from printing it (found on the desktop lane). claude-guard.py, a PreToolUse hook passed with --settings, lets through only one ui command with plain arguments and logs every decision to <layout>-guard.log, outside the session's directory. After each session, the session's files and the guard log are searched for the token (given to grep through a pipe); if it is there they are deleted before the upload and the step fails. Run 38729704027's session ran only ui commands and its files contain no token.
One Haiku 5.5 session per layout, one-hand UI first, each bounded by 700 turns, $10 and an auto-compact window of 200k tokens, so a long session does not re-read an ever larger context every turn. The validation session (run 38729704027) drove C2, D2 and D7 correctly in 55 turns for $0.03.
From the first session (run 38729704027): clear the search field with Cancel and check it before typing; the keyboard may still change words; a channel row shows the published subscriber count, not the listings' member records; with the one-hand UI later pages stack above earlier ones; disabled elements are marked; prompt.md is there to re-read after a compaction.
Run 38729704027 still capitalized and autocorrected typed text ("citester" became "Citester", "Neci" became "Neck"): the keyboard daemon reads these settings when it starts, so the simulator is restarted once after writing them.
After the smoke test, claude-test.sh runs one Claude Code session (pinned 2.1.300) per layout named in claude-scope, with the plan's cases, model, turns and budget from there. A session gets only Bash for the ui helper and Read/Write/Edit in its own directory (--restricted, dontAsk, no MCP), drives the app through ui, and writes report.md with screenshots; its transcript is stream-json. The app job declares the app-sim-claude environment; only the Claude step gets CLAUDE_CODE_OAUTH_TOKEN, and it runs only on workflow_dispatch or a head commit with [claude-test]. claude-usage.sh writes each session's turns, tokens and cost (estimated at API prices) to the job summary and claude-usage.json, even when the session fails. The plan is copied from the iOS directory search run. This run: C2, D2, D7 in the one-hand UI with Haiku 5.5, to validate the harness.
Set in com.apple.Preferences right after boot, before the app first shows the keyboard, so typed links (C4) and nonsense words (D10) reach the app as typed. The smoke test still compares typed text ignoring case.
After the M3 search in the one-hand UI, the header's y must be smaller than both results'. The search is then repeated with the top bar (ui layout top) and checked the same way, and the app is left in the one-hand UI for later steps. A layout the helper cannot set is reported as a warning, not a failure: that is the harness, not the app.
Adds tap-xy, swipe, clipboard, key, find, layout (sets the one-hand UI preference in the app group's defaults and relaunches), relaunch, text-size, directory stop/start, listings and requests (directory searches the app sent, from its log). Elements below the screen, such as keyboard keys, are counted instead of listed; shot also saves a smaller JPEG to look at and only takes plain names. The helper unsets CLAUDE_CODE_OAUTH_TOKEN, so nothing it runs inherits the token.
avatar.swift makes avatars the way the app makes profile images (JPEG 0.85 at scale 1, shrunk to at most 12500 characters). Three groups and three channels with such avatars match "ci avatar"; a CLI peer follows the search cursor to the end and records each page's entries, JSON and zstd sizes against the 10737-byte limit in seed.md. Also seeded: a long-named group and one with markdown in its description. Every entry is created with the profile JSON the app sends, and each approval is matched by the entry's name. backend.sh can also stop and start the directory and print its listings.
The admin was contact 3 of the directory, not 2 (run 38713931270), and a super user is matched by contact ID and name. The service now starts with a placeholder super user, the admin connects, and the service restarts with the ID read from its database. This run tests the group and channel registration, the CLI search, the app build and the smoke-test search.
Every run waited for macOS runners three times (core, backend, app), even
when core and backend only restored a cache, and those caches are evicted
within hours here. A plan job on ubuntu names both artifacts after the hash
of their sources and finds them in earlier runs of this branch in this
repository (actions: read); core and backend build only when none exists, and
the app job downloads from the run that built them. Artifacts are kept 30
days; the zip caches are dropped.
This run tests: the plan job, building and uploading both artifacts under
their new names (none exist yet), and the app job downloading them; then the
directory start with -y, the seeding, and the search in the app.
setup-haskell reports ~/.cabal/store, but without ~/.cabal cabal 3.10 keeps its store in XDG directories, so the saved cache held only dist-newstyle (run 38563035327: 401 MB). CABAL_DIR is set before Setup Haskell, so its cabal update, the build and the cache path agree. build-backend.sh prints the store size before building, as build-core.sh does.
The database is created by the CLI, so the service's own migration (20250924_directory_schema) asked for confirmation on stdin and the service exited. This run tests that the directory starts and the group and channel get listed.
The app is built with DIRECTORY_SERVICE_LINK set to the test directory's
short address (sed in the workflow). The smoke test types "ci seed", taps
"Search in Directory", accepts the first-search consent alert with "Search",
and expects the Directory results with #ci_group (Join group) and
#ci_channel (Join channel), retrying once if the search fails.
The steps now use scripts/ci/ios-sim/ui, which wraps AXe (tap by label or
value, type, wait, screenshot, compact accessibility tree) for scripts and
for an agent on the runner. Later steps get SIM_UDID and UI_OUT.
This run tests: the onboarding and typing steps on the new helper, the
consent alert, and the directory results in the app.
A new backend job builds simplex-chat and simplex-directory-service with the
server-capable configuration. The app job starts a chat relay and a directory
on the preset servers, registers and approves a group and a channel from a CLI
peer, and checks that a service request for "ci seed" finds both, before the
app is built. The app still uses the production directory link; the smoke
test is unchanged.
The cabal store cache now uses setup-haskell's store path, with a key prefix
per job, so rebuilds after Haskell changes reuse built dependencies.
This run tests: backend build, relay and directory start, registration and
approval over the preset servers, the service request, and that M2 still
passes.
Run 38448094338 reached the chat list and typed into the search field, which showed "Ci smoke" and the Search in Directory row, but the check looked for "ci smoke": the keyboard capitalizes the first letter. SwiftUI text fields expose no accessibility label, only the placeholder as the value of an empty field, so the fields are now tapped by value.
This run tests the smoke test end to end, with the core library restored from cache.
Adds the ios-sim workflow, triggered only by pushes to f/ci-ios-sim and by workflow_dispatch.
The core job builds lib:simplex-chat with cabal using the flake's iOS settings (swift JSON, client library, static roll-up archive, threaded RTS, CommonCrypto), packages it as the Hydra zip, and patches it for the simulator with scripts/ios/prepare.sh and mac2ios. The app job builds "SimpleX (iOS)" for the arm64 simulator against it and drives onboarding, the chat list and the search field with AXe, saving screenshots and accessibility trees.
This first run tests the whole pipeline, mainly whether the core library builds and links.