<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Krenin</title>
    <description>The latest articles on DEV Community by Krenin (@krenin88).</description>
    <link>https://dev.to/krenin88</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4177303%2F5b948b9a-4031-4f9f-b3dd-981f68f9ee20.jpg</url>
      <title>DEV Community: Krenin</title>
      <link>https://dev.to/krenin88</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krenin88"/>
    <language>en</language>
    <item>
      <title>Test Automation with agent-device (Part 3)</title>
      <dc:creator>Krenin</dc:creator>
      <pubDate>Sun, 01 Nov 2026 13:08:42 +0000</pubDate>
      <link>https://dev.to/krenin88/test-automation-with-agent-device-part-3-195i</link>
      <guid>https://dev.to/krenin88/test-automation-with-agent-device-part-3-195i</guid>
      <description>&lt;p&gt;This post covers saving test flows as agent-device &lt;code&gt;.ad&lt;/code&gt; scripts and running them again, using a Flutter app. It goes through writing an &lt;code&gt;.ad&lt;/code&gt; file, getting the app into the right state before a replay, reading the report when a replay stops and fixing it, and the token cost of agent-driven QA. It is based on agent-device 0.21.31 (2026-10-25).&lt;/p&gt;

&lt;p&gt;Installation and the basic loop are in &lt;a href="https://dev.to/krenin88/test-automation-with-agent-device-part-1-7nj"&gt;Part 1&lt;/a&gt;, and real-device setup is in &lt;a href="https://dev.to/krenin88/test-automation-with-agent-device-part-2-1noa"&gt;Part 2&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an .ad file is
&lt;/h2&gt;

&lt;p&gt;When an agent checks an app, it reads the screen, picks the next thing to tap, and reads the result, over and over. Exploring the same flow this way every time keeps costing time and tokens. An &lt;code&gt;.ad&lt;/code&gt; file pins down the result of that exploration as a list of commands, and &lt;code&gt;replay&lt;/code&gt; runs the list from top to bottom.&lt;/p&gt;

&lt;p&gt;Each line has the form &lt;code&gt;&amp;lt;command&amp;gt; [positional...] [flag...]&lt;/code&gt;, the same as an agent-device command in the terminal. Quoting follows shell rules too, so a selector containing spaces is wrapped like &lt;code&gt;'label="..."'&lt;/code&gt;. Below is the start of a flow that opens the app fresh, taps a button on the first screen, types into an input, and checks the result screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# First-run flow — the app ID comes in via -e APP=
open ${APP} --relaunch
wait 'label="&amp;lt;first-screen button&amp;gt;"' 30000
press 'label="&amp;lt;first-screen button&amp;gt;"'
fill 'id="find-input"' "&amp;lt;text to type&amp;gt;"
press 'label="&amp;lt;search button&amp;gt;" role=button'
wait 'label="&amp;lt;result title&amp;gt;"' 60000
wait text "&amp;lt;text the result should show&amp;gt;" 10000
back
close
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;#&lt;/code&gt; comments work.&lt;/strong&gt; The docs do not say so, so the first line was a comment in a test replay, and it passed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not hard-code the app ID.&lt;/strong&gt; Leave it as &lt;code&gt;${APP}&lt;/code&gt; and pass &lt;code&gt;-e APP=&amp;lt;bundle id&amp;gt;&lt;/code&gt; at replay time, and the same file works for builds with different IDs, such as a dev build and a release build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The number after &lt;code&gt;wait&lt;/code&gt; is a timeout in milliseconds.&lt;/strong&gt; Give a long one to steps that wait on the network or on model inference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device-selection flags stay out of the file.&lt;/strong&gt; &lt;code&gt;--platform&lt;/code&gt;, &lt;code&gt;--serial&lt;/code&gt;, &lt;code&gt;--device&lt;/code&gt;, and &lt;code&gt;--settle&lt;/code&gt; are not part of a script step; they go on the replay command.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are two ways to make an &lt;code&gt;.ad&lt;/code&gt; file. You can start recording with &lt;code&gt;open &amp;lt;bundle id&amp;gt; --relaunch --save-script=&amp;lt;file&amp;gt;.ad&lt;/code&gt;, go through the flow, and save with &lt;code&gt;session save-script&lt;/code&gt;, or you can write it by hand. The 24-step flow in this post was written by hand, based on a flow the agent had already passed manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting to a "first launch" state before replaying
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;replay&lt;/code&gt; only runs the commands written down; it does not reset the app. If the flow starts by checking the first-launch consent dialog, an app that still remembers an earlier consent stops right at step 2. So a separate runner script deletes and reinstalls the app before each replay.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Android emulator&lt;/span&gt;
adb &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;serial&amp;gt; uninstall &amp;lt;bundle &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
adb &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;serial&amp;gt; &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &amp;lt;apk path&amp;gt;
adb &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;serial&amp;gt; shell cmd locale set-app-locales &amp;lt;bundle &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--locales&lt;/span&gt; en-US
agent-device replay &amp;lt;file&amp;gt;.ad &lt;span class="nt"&gt;--platform&lt;/span&gt; android &lt;span class="nt"&gt;--serial&lt;/span&gt; &amp;lt;serial&amp;gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;APP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;bundle &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--timeout&lt;/span&gt; 240000

&lt;span class="c"&gt;# iOS simulator&lt;/span&gt;
xcrun simctl uninstall &amp;lt;UDID&amp;gt; &amp;lt;bundle &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
xcrun simctl &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;UDID&amp;gt; &amp;lt;Runner.app path&amp;gt;
agent-device replay &amp;lt;file&amp;gt;.ad &lt;span class="nt"&gt;--platform&lt;/span&gt; ios &lt;span class="nt"&gt;--udid&lt;/span&gt; &amp;lt;UDID&amp;gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;APP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;bundle &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;--timeout&lt;/span&gt; 300000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Android, &lt;code&gt;set-app-locales&lt;/code&gt; opens just this app in English while leaving the device language alone. On the iOS simulator no per-app language command was used, so the app follows the simulator's language setting. &lt;code&gt;replay&lt;/code&gt; has a default work budget of 60 seconds, so raise it with &lt;code&gt;--timeout&lt;/code&gt; for longer flows.&lt;/p&gt;

&lt;p&gt;Deleting the app also deletes its data. So the runner script accepts only the designated test emulators (checked by name with &lt;code&gt;adb -s &amp;lt;serial&amp;gt; emu avd name&lt;/code&gt;) and simulator UDIDs, and stops with exit code 2 if it gets any other device or an app ID outside your own. An emulator serial such as &lt;code&gt;emulator-5554&lt;/code&gt; is assigned in boot order and does not tell you which AVD it is, which is why the check uses the name.&lt;/p&gt;

&lt;p&gt;With that in place, the 24-step flow finished with &lt;code&gt;Replayed 24 steps&lt;/code&gt; on both runs on each platform: 8.8 and 9.4 seconds on the Android emulator, 13.9 and 14.0 seconds on the iOS simulator.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a replay stops
&lt;/h2&gt;

&lt;p&gt;When a replay gets stuck partway, it fails with &lt;code&gt;REPLAY_DIVERGENCE&lt;/code&gt;. The report includes the step number and its line, the actionable elements on screen at that moment, and selector suggestions for that screen. So you do not have to start over after a fix, it also tells you how to resume with &lt;code&gt;replay --from &amp;lt;step&amp;gt; --plan-digest &amp;lt;value&amp;gt;&lt;/code&gt;. When no automatic repair is possible, it says &lt;code&gt;Repair hint: manual&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A first-launch system dialog.&lt;/strong&gt; The flow that passed on Android stopped at step 2 the first time it ran on the iOS simulator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error (REPLAY_DIVERGENCE): Replay failed at step 2 (wait "label=\"&amp;lt;first-screen button&amp;gt;\"" 30000): wait timed out for selector: ...
Current surface: Allow "&amp;lt;app name&amp;gt;" to track your activity across other companies' apps ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The report's &lt;code&gt;Current surface&lt;/code&gt; showed the App Tracking Transparency (ATT) dialog. It is a system dialog that appears on first launch only on iOS, so the flow written against Android had no step for it. After closing it with &lt;code&gt;agent-device alert dismiss&lt;/code&gt; and replaying again, the run got past this step. The runner script does not handle it, so on builds that show this dialog, close it before replaying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input role names that differ by platform.&lt;/strong&gt; Next, the replay stopped at the &lt;code&gt;fill&lt;/code&gt; in step 5. The first selector was &lt;code&gt;role=text-field&lt;/code&gt;, which matched on Android, but the iOS simulator exposed the same input as an &lt;code&gt;other&lt;/code&gt; element with a label. To find one selector that works on both platforms, these were tried in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;editable=true&lt;/code&gt; — passed on Android, &lt;code&gt;Selector did not match&lt;/code&gt; on iOS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;role=textfield || role=text-field&lt;/code&gt; — &lt;code&gt;||&lt;/code&gt; is the "or" syntax that appeared in the report's selector suggestions. It still did not match on iOS.&lt;/li&gt;
&lt;li&gt;Adding &lt;code&gt;Semantics(identifier:)&lt;/code&gt; to the Flutter widget — passed on both platforms&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last fix is one line of app code: &lt;code&gt;identifier: 'find-input'&lt;/code&gt; added to the existing &lt;code&gt;Semantics&lt;/code&gt; around the input, with no visible change on screen. The selector becomes &lt;code&gt;id="find-input"&lt;/code&gt;. As noted in Part 1, a Flutter &lt;code&gt;Key&lt;/code&gt; does not show up in the accessibility tree, so adding an &lt;code&gt;identifier&lt;/code&gt; is the reliable way to run the same script on both platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token cost and the rules for cutting it
&lt;/h2&gt;

&lt;p&gt;Token cost was the reason for using &lt;code&gt;.ad&lt;/code&gt; in the first place. Most of the cost of agent-driven QA came not from output but from cache reads. A cache read is the earlier conversation the agent re-reads on every response. The first real-device check of two apps took about 40 minutes and came to 121 responses, 77,985 output tokens, and 30,225,404 cache-read tokens, roughly 250,000 tokens re-read per response.&lt;/p&gt;

&lt;p&gt;The QA agent was already on Sonnet, so the model was not the cause. In the same stretch, 11 screenshots were read into the conversation, and as the context grew, every response re-read more. So five rules went into the QA agent's instructions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save a flow as &lt;code&gt;.ad&lt;/code&gt; the first time it passes, and use &lt;code&gt;replay&lt;/code&gt; from then on. When it gets stuck, fix only that step using the divergence report.&lt;/li&gt;
&lt;li&gt;Act with &lt;code&gt;--settle&lt;/code&gt; and look only at the returned diff. Do not read the full screen tree.&lt;/li&gt;
&lt;li&gt;Save screenshots to files only, and read at most one per item into the conversation.&lt;/li&gt;
&lt;li&gt;End the session per item: one app, one thing to check. Past 40 responses, it is time to stop.&lt;/li&gt;
&lt;li&gt;Read logs and build output with &lt;code&gt;grep&lt;/code&gt; or &lt;code&gt;tail&lt;/code&gt;, only the lines you need.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The batch after the rules (three emulator checks, an iPhone attempt, an Android real device, about 30 minutes) came to 105 responses, 44,113 output tokens, and 19,171,162 cache-read tokens, about 180,000 cache-read tokens per response. The items checked were different, so the two sets of numbers cannot be compared directly, and the 40-response limit was not met. Tokens after adopting &lt;code&gt;.ad&lt;/code&gt; have not been measured separately yet, and neither replay on a real device nor running it in CI has been tried.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;agent-device repository: &lt;a href="https://github.com/callstack/agent-device" rel="noopener noreferrer"&gt;https://github.com/callstack/agent-device&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;agent-device agent setup docs: &lt;a href="https://oss.callstack.com/agent-device/docs/agent-setup" rel="noopener noreferrer"&gt;https://oss.callstack.com/agent-device/docs/agent-setup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Flutter &lt;code&gt;SemanticsProperties.identifier&lt;/code&gt;: &lt;a href="https://api.flutter.dev/flutter/semantics/SemanticsProperties/identifier.html" rel="noopener noreferrer"&gt;https://api.flutter.dev/flutter/semantics/SemanticsProperties/identifier.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>flutter</category>
      <category>testing</category>
    </item>
    <item>
      <title>Test Automation with agent-device (Part 2)</title>
      <dc:creator>Krenin</dc:creator>
      <pubDate>Tue, 27 Oct 2026 08:26:42 +0000</pubDate>
      <link>https://dev.to/krenin88/test-automation-with-agent-device-part-2-1noa</link>
      <guid>https://dev.to/krenin88/test-automation-with-agent-device-part-2-1noa</guid>
      <description>&lt;p&gt;agent-device drives real devices with the same commands it uses for simulators and emulators. A real iPhone, though, needs a signed test app installed on the device to drive the screen, and a real Android device has to be selected explicitly depending on how it is connected. This post covers why real devices were needed, how to prepare an iPhone and an Android device, and what to watch out for when handling apps on them. It is based on agent-device 0.21.31 (2026-10-24).&lt;/p&gt;

&lt;p&gt;Installation and the basic loop are in &lt;a href="https://dev.to/krenin88/test-automation-with-agent-device-part-1-7nj"&gt;Part 1&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why real devices
&lt;/h2&gt;

&lt;p&gt;Simulators and emulators are enough to check app screens, but they cannot reproduce how a build shipped through the store behaves on a real user's device. Three things had to be checked on real devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Store-build updates and reinstalls.&lt;/strong&gt; Installing a TestFlight build over the existing App Store version (the update path), and deleting and reinstalling the app and then restoring a backup, can only be checked on a real device. Whether the backup file was overwritten was checked in the device's Files app under iCloud Drive, by comparing file size and modification time before and after the install.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System dialogs on first launch.&lt;/strong&gt; The order in which system dialogs such as App Tracking Transparency (ATT) appear relative to the app's own screens has to be seen on a real install. A reinstall also resets that consent history, so first work out whether the state you want to test can actually be produced on the device.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real ads.&lt;/strong&gt; The Google Mobile Ads SDK automatically treats an emulator as a test device. logcat prints &lt;code&gt;This request is sent from a test device.&lt;/code&gt;, and even a release build with production ad unit IDs shows only test ads. That means a no-fill (the ad server finds no ad to serve) cannot be reproduced on an emulator either. On a real device that is not registered as a test device, logcat prints a &lt;code&gt;setTestDeviceIds&lt;/code&gt; hint and real ads are requested. In that case, never tap the ad (invalid clicks), watch a production ad to the end at most once per session, and judge the rest from the ad-load results in logcat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing a real iPhone
&lt;/h2&gt;

&lt;p&gt;On a real iPhone, agent-device installs an XCTest runner on the device. The XCTest runner is a test app that reads the screen and taps on your behalf, and it has to be signed with a development certificate to install. The first run takes a while because the runner gets built, and the cable has to stay connected throughout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic signing.&lt;/strong&gt; According to the agent-device docs, if an Apple developer account is signed in to Xcode, the team ID is all you need. To change the runner's bundle ID, also set &lt;code&gt;AGENT_DEVICE_IOS_BUNDLE_ID&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_DEVICE_IOS_TEAM_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;team &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Manual signing.&lt;/strong&gt; If Xcode has no account, or the existing development profile is Xcode-managed, automatic signing will not work. In that case, create a development profile for the runner yourself with an App Store Connect API key. fastlane's &lt;code&gt;sigh&lt;/code&gt; creates a profile for a wildcard app ID (&lt;code&gt;*&lt;/code&gt;) containing the registered devices.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# key.json: key_id, issuer_id, key (contents of the .p8 file), in_house: false&lt;/span&gt;
fastlane sigh &lt;span class="nt"&gt;--development&lt;/span&gt; &lt;span class="nt"&gt;--app_identifier&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt; &lt;span class="nt"&gt;--team_id&lt;/span&gt; &amp;lt;team &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api_key_path&lt;/span&gt; key.json &lt;span class="nt"&gt;--provisioning_name&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;profile name&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output_path&lt;/span&gt; &amp;lt;folder&amp;gt; &lt;span class="nt"&gt;--skip_install&lt;/span&gt; &lt;span class="nt"&gt;--skip_certificate_verification&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Putting the .p8 file path in &lt;code&gt;key.json&lt;/code&gt; (&lt;code&gt;key_filepath&lt;/code&gt;) failed; putting the file contents in &lt;code&gt;key&lt;/code&gt; worked. Copy the generated profile to &lt;code&gt;~/Library/MobileDevice/Provisioning Profiles/&amp;lt;UUID&amp;gt;.mobileprovision&lt;/code&gt;, and delete the key file once you are done.&lt;/p&gt;

&lt;p&gt;Then pass the signing details as environment variables. The signing identity is the SHA-1 value shown by &lt;code&gt;security find-identity -v -p codesigning&lt;/code&gt;, and the profile was specified by the name given to &lt;code&gt;sigh&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_DEVICE_IOS_TEAM_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;team &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_DEVICE_IOS_SIGNING_IDENTITY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;certificate SHA-1&amp;gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_DEVICE_IOS_PROVISIONING_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;profile name&amp;gt;"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AGENT_DEVICE_STATE_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;separate state folder&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AGENT_DEVICE_STATE_DIR&lt;/code&gt; is the agent-device daemon's state folder. A separate one was set to keep this work apart from the default daemon used by other work. Bundling these variables and the working directory (&lt;code&gt;cd&lt;/code&gt;) into one wrapper script runs every command in the same environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mac and device settings.&lt;/strong&gt; Besides signing, three things have to be right for the runner to work.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Developer tools security on the Mac must be enabled. If it is off, the runner stops at &lt;code&gt;Developer mode is disabled for Apple development tools&lt;/code&gt;. This is a Mac-side setting, separate from Developer Mode on the device; run &lt;code&gt;sudo DevToolsSecurity -enable&lt;/code&gt; with admin rights.&lt;/li&gt;
&lt;li&gt;iPhone Settings → Developer → "Enable UI Automation" must be on. If it is off, the runner installs and the test starts, but nothing responds.&lt;/li&gt;
&lt;li&gt;Keep the screen on and unlocked. When the runner timed out at "Starting XCTest runner", the device was locked. A retry after unlocking never happened, so the cause is only circumstantial, but checking the lock state first with &lt;code&gt;xcrun devicectl device info lockState --device &amp;lt;UDID&amp;gt;&lt;/code&gt; is the safer habit.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Handling apps on an iPhone
&lt;/h2&gt;

&lt;p&gt;Listing and deleting installed apps on a real device is done with Xcode's &lt;code&gt;devicectl&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcrun devicectl list devices
xcrun devicectl device info apps &lt;span class="nt"&gt;--device&lt;/span&gt; &amp;lt;UDID&amp;gt;
xcrun devicectl device uninstall app &lt;span class="nt"&gt;--device&lt;/span&gt; &amp;lt;UDID&amp;gt; &amp;lt;bundle-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner is removed the same way. Its bundle ID is &lt;code&gt;com.callstack.agentdevice.runner.uitests.xctrunner&lt;/code&gt;, and running &lt;code&gt;open&lt;/code&gt; after deleting it builds and installs the runner again.&lt;/p&gt;

&lt;p&gt;When installing a test build from TestFlight while the App Store version of the same app is already installed, a warning says "You may lose the app's data." The Install button in that warning overwrites the app. It is safer to have the agent stop at this warning and ask a human.&lt;/p&gt;

&lt;p&gt;Deleting an app wipes its on-device data. If the app has a backup, take a screenshot showing that backup is on in the app's settings and that the last backup time is just now before deleting; otherwise, stop instead of deleting.&lt;/p&gt;

&lt;p&gt;One more screen-handling note: tapping a toggle in a settings screen by ref sometimes hits the middle of the row and does not flip it. In that case, use the element's rect from &lt;code&gt;snapshot --json&lt;/code&gt; and tap the right edge of the toggle by coordinates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting a real Android device
&lt;/h2&gt;

&lt;p&gt;A real Android device connected over USB debugging behaves exactly like an emulator. With wireless debugging, the device may not show up in &lt;code&gt;adb devices&lt;/code&gt; right after connecting. It appears only after the &lt;code&gt;_adb-tls-connect&lt;/code&gt; service shows up in &lt;code&gt;adb mdns services&lt;/code&gt;, so do not conclude it is missing; check again a few seconds later.&lt;/p&gt;

&lt;p&gt;A wireless serial is a long mDNS name and easy to mix up with an emulator. Add &lt;code&gt;--serial&lt;/code&gt; to every &lt;code&gt;adb&lt;/code&gt; and &lt;code&gt;agent-device&lt;/code&gt; command. agent-device sessions are created per working directory, so if an emulator was already in use from the same directory, you get an "already bound to android device" error. In that case, open a separate session with &lt;code&gt;--session &amp;lt;name&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb mdns services
agent-device open &amp;lt;bundle-id&amp;gt; &lt;span class="nt"&gt;--platform&lt;/span&gt; android &lt;span class="nt"&gt;--serial&lt;/span&gt; &amp;lt;serial&amp;gt; &lt;span class="nt"&gt;--session&lt;/span&gt; &amp;lt;name&amp;gt; &lt;span class="nt"&gt;--foreground&lt;/span&gt;
agent-device close &lt;span class="nt"&gt;--session&lt;/span&gt; &amp;lt;name&amp;gt;
adb disconnect &amp;lt;serial&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things commonly go wrong on Android:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quote adb shell commands as one string.&lt;/strong&gt; Unless the whole thing is wrapped, as in &lt;code&gt;adb shell "cmd notification post -t 'title' &amp;lt;tag&amp;gt; 'body'"&lt;/code&gt;, adb splits the arguments on spaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buttons hidden by the keyboard are not found.&lt;/strong&gt; Close the keyboard with &lt;code&gt;adb shell input keyevent KEYCODE_BACK&lt;/code&gt; and snapshot again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quote selectors that contain spaces.&lt;/strong&gt; Use the &lt;code&gt;label="..."&lt;/code&gt; form; passing flags as one variable blob gives "Unknown flag".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When something other than the app, such as an interstitial ad, is on top, a snapshot alone makes it hard to tell what state the app is in. Check the system state with adb as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;serial&amp;gt; shell dumpsys activity activities | &lt;span class="nb"&gt;grep &lt;/span&gt;topResumedActivity   &lt;span class="c"&gt;# what is in front right now&lt;/span&gt;
adb &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;serial&amp;gt; shell pidof &amp;lt;package&amp;gt;                                          &lt;span class="c"&gt;# is the app process alive&lt;/span&gt;
adb &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;serial&amp;gt; logcat &lt;span class="nt"&gt;-d&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"FATAL|AndroidRuntime"&lt;/span&gt;                     &lt;span class="c"&gt;# traces of a crash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Match the snapshot against whether the front screen is the ad or the app, whether the app is still alive behind it, and whether a crash left a record. Ad screens put many &lt;code&gt;webview&lt;/code&gt; and &lt;code&gt;link&lt;/code&gt; nodes into the accessibility tree, so filter those out when reading a snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;agent-device repository: &lt;a href="https://github.com/callstack/agent-device" rel="noopener noreferrer"&gt;https://github.com/callstack/agent-device&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;agent-device agent setup docs: &lt;a href="https://oss.callstack.com/agent-device/docs/agent-setup" rel="noopener noreferrer"&gt;https://oss.callstack.com/agent-device/docs/agent-setup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;fastlane sigh docs: &lt;a href="https://docs.fastlane.tools/actions/sigh/" rel="noopener noreferrer"&gt;https://docs.fastlane.tools/actions/sigh/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Android wireless debugging: &lt;a href="https://developer.android.com/tools/adb#wireless" rel="noopener noreferrer"&gt;https://developer.android.com/tools/adb#wireless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>flutter</category>
      <category>testing</category>
    </item>
    <item>
      <title>Test Automation with agent-device (Part 1)</title>
      <dc:creator>Krenin</dc:creator>
      <pubDate>Sat, 24 Oct 2026 18:08:13 +0000</pubDate>
      <link>https://dev.to/krenin88/test-automation-with-agent-device-part-1-7nj</link>
      <guid>https://dev.to/krenin88/test-automation-with-agent-device-part-1-7nj</guid>
      <description>&lt;p&gt;agent-device is an open-source tool from Callstack that lets an AI agent drive iOS and Android apps and check what is on screen. This post covers why it was picked for testing a Flutter app, how to install it for Claude Code, the basic command loop, and what to watch out for with Flutter. It is based on version 0.21.31 (2026-10-24).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agent-device
&lt;/h2&gt;

&lt;p&gt;Flutter ships Android and iOS apps from one codebase, so the device-testing tool has to cover iOS too. Google Artemis, the first candidate, is a natural-language testing agent, but it supports Android only; iOS is still just a roadmap item.&lt;/p&gt;

&lt;p&gt;The other candidates looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;mobile-mcp&lt;/strong&gt; exposes taps, screenshots, and the accessibility tree on iOS and Android over MCP (the protocol an AI uses to call external tools). It has no sessions, replay, or evidence storage, so a verified flow is hard to hand off to CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maestro&lt;/strong&gt; is an E2E framework that pins flows in YAML, and it has an MCP server. It suits regression runs of fixed flows better than an agent exploring the screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arbigent&lt;/strong&gt; is an AI agent built on top of Maestro. Its author rates it 1/5 for speed and 3/5 for reliability, and its YAML format was still changing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commercial services&lt;/strong&gt; (Drizz, Minitap, and others) bundle a real-device farm with a natural-language agent, but the only public material was vendor material, so cost and quality could not be checked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;agent-device handles iOS and Android simulators, emulators, and real devices with one tool, and its docs state Flutter support explicitly. It ships both a CLI and an MCP server, can save a verified flow as an &lt;code&gt;.ad&lt;/code&gt; script to replay later, and can export to Maestro YAML. It is MIT-licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Install the CLI globally and check the environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; agent-device@latest
agent-device doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;doctor&lt;/code&gt; checks connected devices, toolchains, and dev-server status. In this setup it passed with only warnings for unused platforms (HarmonyOS, Vega).&lt;/p&gt;

&lt;p&gt;Register it with Claude Code as an MCP server. With &lt;code&gt;-s user&lt;/code&gt;, the same tool is available in every project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;-s&lt;/span&gt; user agent-device &lt;span class="nt"&gt;--&lt;/span&gt; agent-device mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent-device repository includes three agent skills: &lt;code&gt;agent-device&lt;/code&gt;, &lt;code&gt;ios-simulator&lt;/code&gt;, and &lt;code&gt;android-emulator&lt;/code&gt;. The official instruction is &lt;code&gt;npx skills add callstack/agent-device&lt;/code&gt;, but Claude Code's auto mode safety check blocked it. In that case, clone the repository and copy the three folders under &lt;code&gt;skills/&lt;/code&gt; into &lt;code&gt;~/.claude/skills/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To undo everything, run &lt;code&gt;claude mcp remove agent-device -s user&lt;/code&gt; and &lt;code&gt;npm uninstall -g agent-device&lt;/code&gt;, then delete the three skill folders.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic loop
&lt;/h2&gt;

&lt;p&gt;The basic loop is: prepare a device, open the app, act, verify, close. Pick the device with &lt;code&gt;--platform&lt;/code&gt; and &lt;code&gt;--device&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-device boot &lt;span class="nt"&gt;--platform&lt;/span&gt; android &lt;span class="nt"&gt;--device&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;emulator name&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;--headless&lt;/span&gt;
agent-device &lt;span class="nb"&gt;install&lt;/span&gt; /absolute/path/app-debug.apk &lt;span class="nt"&gt;--platform&lt;/span&gt; android &lt;span class="nt"&gt;--device&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;emulator name&amp;gt;"&lt;/span&gt;
agent-device open &amp;lt;bundle-id&amp;gt; &lt;span class="nt"&gt;--platform&lt;/span&gt; android &lt;span class="nt"&gt;--device&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;emulator name&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;--foreground&lt;/span&gt;
agent-device press @e5 &lt;span class="nt"&gt;--settle&lt;/span&gt;
agent-device fill @e11 &lt;span class="s2"&gt;"text"&lt;/span&gt; &lt;span class="nt"&gt;--settle&lt;/span&gt;
agent-device &lt;span class="nb"&gt;wait &lt;/span&gt;text &lt;span class="s2"&gt;"&amp;lt;expected text&amp;gt;"&lt;/span&gt;
agent-device screenshot /absolute/path/home.png
agent-device close
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;open&lt;/code&gt; reads the screen and returns a ref such as &lt;code&gt;@e5&lt;/code&gt; for each element, like buttons and text fields. With &lt;code&gt;--settle&lt;/code&gt;, &lt;code&gt;press&lt;/code&gt; and &lt;code&gt;fill&lt;/code&gt; wait until the screen goes quiet and return only what changed, so the agent can pick its next target without rereading the whole screen.&lt;/p&gt;

&lt;p&gt;Three things commonly go wrong here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Refs go stale when the screen changes.&lt;/strong&gt; Pick a new ref from the diff returned by each action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A screenshot is not verification.&lt;/strong&gt; Verify with &lt;code&gt;wait text&lt;/code&gt; or &lt;code&gt;wait &amp;lt;selector&amp;gt;&lt;/code&gt;, and keep screenshots as evidence only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give &lt;code&gt;install&lt;/code&gt; an absolute path.&lt;/strong&gt; A relative path resolves against the agent-device daemon's working directory and fails with "App binary not found".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a Flutter app, build the iOS simulator app with &lt;code&gt;flutter build ios --simulator --debug&lt;/code&gt; and the Android app with &lt;code&gt;flutter build apk --debug&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flutter gotchas
&lt;/h2&gt;

&lt;p&gt;agent-device reads the screen through the accessibility tree. In Flutter, that means only widgets with text show up by default; icon buttons and containers do not. Add &lt;code&gt;Semantics(identifier:)&lt;/code&gt; or &lt;code&gt;semanticLabel&lt;/code&gt; to those widgets so a selector can find them. Flutter's &lt;code&gt;Key&lt;/code&gt; does not appear in the accessibility tree.&lt;/p&gt;

&lt;p&gt;Labels in the accessibility tree follow the language the device is set to, so the same app reads with different label text depending on that setting. A text-based selector changes with the device language, which makes &lt;code&gt;identifier&lt;/code&gt; the more stable choice.&lt;/p&gt;

&lt;p&gt;On iOS, AdMob test banners show up as &lt;code&gt;platform_view&lt;/code&gt;, &lt;code&gt;webview&lt;/code&gt;, and &lt;code&gt;link&lt;/code&gt; nodes. Selectors in replay scripts should avoid those nodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;agent-device repository: &lt;a href="https://github.com/callstack/agent-device" rel="noopener noreferrer"&gt;https://github.com/callstack/agent-device&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;agent-device agent setup docs: &lt;a href="https://oss.callstack.com/agent-device/docs/agent-setup" rel="noopener noreferrer"&gt;https://oss.callstack.com/agent-device/docs/agent-setup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Artemis: &lt;a href="https://github.com/google/artemis" rel="noopener noreferrer"&gt;https://github.com/google/artemis&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Maestro Flutter support docs: &lt;a href="https://docs.maestro.dev/platform-support/flutter" rel="noopener noreferrer"&gt;https://docs.maestro.dev/platform-support/flutter&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>flutter</category>
      <category>testing</category>
    </item>
    <item>
      <title>Building a game demo with Claude Code in one day</title>
      <dc:creator>Krenin</dc:creator>
      <pubDate>Sun, 11 Oct 2026 16:57:06 +0000</pubDate>
      <link>https://dev.to/krenin88/building-a-game-demo-with-claude-code-in-one-day-215g</link>
      <guid>https://dev.to/krenin88/building-a-game-demo-with-claude-code-in-one-day-215g</guid>
      <description>&lt;p&gt;On a Sunday afternoon I pulled in a game template. That was 1:46 PM. The next time I looked up it was 3 AM, and the last commit message read "trial (demo)". There were 86 commits in between.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;Reptile Catcher&lt;/strong&gt;. You go out to catch lizards by stunning them, sell them, and breed them for prettier patterns. It runs on Godot, and Claude Code wrote the code. My part was setting the direction, playtesting, and picking the assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  A working loop in five minutes
&lt;/h2&gt;

&lt;p&gt;Five minutes after the template went in, the catch-sell-breed loop was running. Honestly, I expected that much. AI can do that these days.&lt;/p&gt;

&lt;p&gt;What mattered was the next minute. The very next commit was a &lt;strong&gt;balance simulator where a bot plays the game for you&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Incremental games are a numbers game. Change one upgrade price and a 30-minute stretch can turn into three hours. I can't replay the whole thing from the start every time. So every time Claude changed a number, it ran the bot, and I made calls based on numbers like "4h 19m to the ending (bot)". Later we split the bot into five personalities: one that buys attack first, one that buys the cheapest thing, one that only breeds, and so on.&lt;/p&gt;

&lt;p&gt;The bot also caught a bug. While measuring the demo length, one bot never finished. It turned out it was stuck trying to buy an upgrade that was locked in the demo. The bug was on the bot side, but it showed me the lock rules were scattered across the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changing direction at 2:20 PM
&lt;/h2&gt;

&lt;p&gt;The first 30 minutes produced a screen of tabs and buttons. At 2:20 PM I changed direction completely: pixel art, catching as a "run" where you move a character yourself, and upgrades as a tree that branches out like a mind map.&lt;/p&gt;

&lt;p&gt;Normally, throwing things out at that point would scare me. But rebuilding was so cheap that I barely hesitated. That's the biggest difference I've felt working with AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI generates the sprites, I pick
&lt;/h2&gt;

&lt;p&gt;The pixel sprites came from PixelLab, connected to Claude through MCP. Claude generated a few candidates, and I picked one with notes like "number 3, but make the neck shorter." Claude recorded the seed of each picked sprite in a doc.&lt;/p&gt;

&lt;p&gt;The funny part is why some species got replaced.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The house gecko looked like every other species once it was shrunk to a 32-pixel side view, so I changed it to a skink.&lt;/li&gt;
&lt;li&gt;The flying lizard's wing membrane didn't show up at 32 pixels, so I changed it to a frilled lizard.&lt;/li&gt;
&lt;li&gt;The bearded dragon's walk cycle looked like a puppy no matter what, so I replaced it with a horned lizard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of regenerating until I got the sprite I wanted, I changed the game to fit what the tool draws well. For a small game, that was much faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  My job was removing what the AI built
&lt;/h2&gt;

&lt;p&gt;Around 7 PM I removed a whole set of features Claude had built that afternoon: auto-catching, auto-selling, and offline progress. Ten minutes later, prestige and the fame tree went too.&lt;/p&gt;

&lt;p&gt;I had decided this game would be "no failing, no prestige, one straight run that ends in 4 to 6 hours." Idle features and prestige had no place in that. With the AI building this fast, features pile up quickly, but deciding what to cut was still up to me. Looking back, that was most of my work on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still, a week to ship
&lt;/h2&gt;

&lt;p&gt;By 3 AM the demo scope was cut, but the actual release was October 11. In between, Claude added four bosses, drew the area backgrounds, fixed the demo lock bugs, and prepared the screenshots, cover, and web build for the itch.io page.&lt;/p&gt;

&lt;p&gt;So "made in a day" is only true up to the demo. Building took a day, but polishing and shipping took a week. I suspect the next game will go the same way.&lt;/p&gt;

&lt;p&gt;If you're building a numbers-heavy game with AI and don't have a simulator yet, I'd suggest having the AI build a bot right after the core loop. For me, that was the best decision of the whole project.&lt;/p&gt;

&lt;p&gt;The demo is on itch.io. Come catch some lizards 🦎&lt;br&gt;
👉 &lt;a href="https://krenin.itch.io/reptile-catcher" rel="noopener noreferrer"&gt;https://krenin.itch.io/reptile-catcher&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>gamedev</category>
      <category>godot</category>
    </item>
  </channel>
</rss>
