Agentic development

How coding agents (Codex, Claude Code, VS Code agent mode, and other MCP clients) build, run, drive, and see Day apps. Every capability lives in the day CLI behind stable commands, so all agents and editors share one implementation; editor extensions only register it.

Fast functional walkthroughs

Use day launch -t <target> --script dayscript/walkthrough.yaml --fast to apply decorative motion at its destination state. This passes DAY_TEST_FAST=1 to the app, which forces Day’s reduced-motion gate on (docs/accessibility.md): fast mode and the user’s reduce-motion setting are one mechanism, so what a fast run shows is what a user who asked for less motion sees. It does not change the device’s global animation settings. The equivalent --env DAY_TEST_FAST=1 also works, as does --env DAY_REDUCE_MOTION=1, the spelling for a reduced-motion screenshot variant. The shared daybrite/actions workflow requests this by default with fast-scripts: true; set fast-scripts: false for animation coverage. An explicit DAY_TEST_FAST in launch-env overrides that input. Both app and CLI must be rebuilt for full acceleration. Local launches retain normal motion unless requested; omit --fast and any fast env override for animation tests.

Day’s explicit/implicit property animations and finite canvas tweens honor the policy across toolkits. GTK also disables its native animations for this process; Android disables navigation transitions, cover slides, list diff animations and smooth list scrolling. UIKit suppresses Day navigation and presentation animations, with existing system-modal dismissal workarounds retained. Harmony already suppresses scripted navigation transitions. Native system UI, third-party players and continuous frame subscriptions are not a virtual clock and may still animate.

Literal pause steps retain their duration. Mark a pause only for decorative motion as pause: { secs: 0.7, animation: true }; --fast replaces it with wait_idle, which flushes reactive work and checks the toolkit’s transition gate. The runner keeps these pauses until an ordinary engine reply confirms that the app enabled fast motion; older apps and a pause before the first engine reply retain their wait. Screenshot render checkpoints still run. For asynchronous application work, prefer an assert_text or wait_for with an appropriate timeout: these already retry until their condition holds. For idempotent fixture cleanup, tap: { id: remove_fixture, if_present: true } skips an absent element after UI transitions settle. Wait for the containing screen first: this does not wait for an asynchronously loaded element to appear. Existing elements must still be visible and enabled; ordinary taps continue to fail on missing IDs. select takes the same flag, for a control that only some layouts have: select: { id: pane_picker, index: 0, if_present: true } switches a phone’s one-pane editor back to its first pane and passes on a wide window, where both panes show and there is no switcher. Do not mark network loads, media playback, physics or crash-observation pauses as animation pauses. Fast mode is not evidence that an animation itself works correctly.

Dialogs during walkthroughs

Scripted launches default to DAY_TEST_DIALOGS=scripted, independently of fast motion. Day alerts, prompts and file pickers await assert_presented / respond without opening native UI. Use --dialogs native (or --env DAY_TEST_DIALOGS=native) for native integration coverage. Scripts can switch with dialog_mode: { mode: scripted } or { mode: native } after answering pending requests; assert_not_presented checks for unanswered Day requests. See dialog testing for examples, native dismissal limitations, and OS prompts outside Day’s control. With --keep-alive, the CLI restores native mode once the scripts finish; pass --dialogs scripted to keep answering prompts with day drive.

The session registry

Every day launch records its app’s dayscript-engine coordinates in build/day/sessions.json:

[{ "target": "macos-appkit", "appId": "dev.example.app", "profile": "debug",
   "enginePort": 34832, "engineToken": "…", "startedAt": 1783961597094 }]

The engine now rides every launch (loopback TCP, token-gated), not just --script runs, so an app the developer opened an hour ago is still drivable. One session per target; entries drop on day stop and are replaced by a new launch of the same target. A launch’s engine env (DAYSCRIPT_PORT/DAYSCRIPT_TOKEN) reaches the app the same way scripted runs always did (process env on desktop, intent extras on Android, --ps want-params on OpenHarmony).

day drive: dayscript against a running app

day drive -t macos-appkit --steps-json \
  '[{"navigate":{"route":"controls"}},{"wait_idle":null},
    {"tap":{"id":"increment-button","repeat":2}},
    {"assert_text":{"id":"counter-label","text":"2 clicks"}},
    {"screenshot":"after"}]'

Steps use the walkthrough vocabulary (single-key mapping form, or flattened {"op": …}); the step catalog lives in the dayscript reference (website; the shipped list is DESIGN.md Appendix C) rather than being copied here, where it has drifted before. The output is one JSON object ({target, steps: [{op, ok, error?, screenshot?}…], failed}) on stdout; screenshots land in build/day/screenshots/_drive/ and are inlined as base64 for callers that want the pixels. Device targets get their engine port forwarded automatically (adb / hdc), like scripted runs.

When the app dies mid-script

Startup transport failures, including a lost compiled element-registry handshake, use the same engine-loss diagnostics as a connection lost during a walkthrough. On Android the runner relaunches once if the failure occurs before any walkthrough action is sent and no crash artifact is found. A second startup failure remains a failure. Invalid registry responses and losses after an action has been sent are not retried this way; an action may already have changed app state.

A scripted run whose app crashes ends in engine connection lost, which says only that the app is gone. The runner then prints a post-mortem (crates/day-cli/src/diagnose.rs) from whatever this host can produce:

  • Harmony faultlogger reports: the current app’s fresh native or ArkTS crash report from /data/log/faultlog/faultlogger on the selected device, including the reason and faulting thread. Report age is measured using the guest clock to tolerate clock skew.
  • day-piece-break’s own artifacts (docs/break.md), when the app arms it: the kind of death, the panic message and location, the signal, how long the app lived, and the backtrace it captured. Reports are finalized on the app’s next launch, so a fresh crash shows its raw session artifacts instead; either way, only the ones whose backend and session start match the run that just failed, because the store is keyed by app id and every target shares it.
  • The OS crash report: on macOS and the iOS simulator, the .ips from ~/Library/Logs/DiagnosticReports, rendered as the exception, the termination reason, and the faulting thread’s frames rather than its several hundred lines of loaded-image addresses. It is matched by pid on a desktop launch, and the runner waits up to 30 s for it, because ReportCrash writes it well after the process dies, so looking once finds the previous run’s or nothing.
  • Windows: recent Application event-log records and local dumps in %LOCALAPPDATA%\CrashDumps. Enable dumps before reproducing the crash, from an administrator shell: reg add "HKLM\Software\Microsoft\Windows\Windows Error Reporting\LocalDumps" /v DumpType /t REG_DWORD /d 2 /f. Windows does not support LocalDumps under HKCU; see Microsoft’s WER settings.
  • Android: the emulator’s crash buffer (adb logcat -b crash), preceded by the emulator’s own log when day devices boot started it — how it booted (the RAM it settled on, cores, GPU mode, feature overrides), then its tail — read from the host, so it is there even when the guest has stopped answering and every adb call returns nothing. A guest that still answers also gives up the tail of its kernel log (adb shell dmesg), where a lockup or the OOM killer would have written.

Scripted launches also default RUST_BACKTRACE=1, so a panic’s stack is in the streamed log the first time; nobody is watching an unattended run to re-run it with the variable set. An explicit --env RUST_BACKTRACE=… wins.

The same post-mortem runs on a plain day launch (no script): an attached launch whose app dies on a fatal signal prints it before returning, and the command’s own exit code carries the signal (128 + signo; SIGABRT is 134), where it used to report 0 and look like a clean quit.

Under GitHub Actions the headline also becomes an ::error:: annotation, so the job page names the crash without anyone opening the log.

day stop / day relaunch

  • day stop -t <target>… | --all — terminate launches (per-platform: pkill / taskkill / simctl terminate / am force-stop / aa force-stop) and drop their sessions.
  • day relaunch -t <target>… | --all-running — stop + rebuild + launch, recording fresh sessions. This is the agent’s “apply my code changes” verb.

day mcp-server

A Model Context Protocol server on stdio (newline-delimited JSON-RPC). Each tool call shells back into the day CLI, so the server only forwards calls. Tools:

toolwraps
day_metadatametadata --json
day_doctordoctor [--toolkit …]
day_buildbuild -t … --profile …
day_launchlaunch -t … --detach [--locale …] [--env K=V]
day_relaunchrelaunch (no targets ⇒ all running sessions)
day_stopstop (no targets ⇒ --all)
day_runningthe session registry + reachability probe
day_drivedrive -t … --steps-json …; screenshots become MCP image content
day_screenshotdrive with wait_idle + screenshot
day_lintlint

A server serves exactly one project (the --project it was spawned with), and no tool takes a project argument, so every result opens with a line naming that project. An agent working in a window of several apps reads it and knows which server it is holding.

A tool call shells back into the server’s own executable, or into whatever DAY_SELF_COMMAND names (docs/environment.md). That matters when day itself is under development: with day.cliSource set, the editor runs the CLI as cargo run against the open checkout, and the extension passes the same invocation here, so a day-cli edit is compiled into the next tool call instead of the agent running the binary that was on disk when the server started. Changes to the server’s own dispatch (this file’s mcp.rs) still need MCP: Restart Server, which recompiles it on the way back up.

VS Code: the Day extension registers one server per Day project in the window, labeled Day: <app title> (day.mcp.enabled, default on). Agent mode then has all ten tools for each. Other MCP clients point at day --project <root> mcp-server. The AI-assisted development guide covers Codex project configuration, Claude Code registration, and shared project instructions.

The loop agents should follow

  1. day_metadata → what targets/locales exist. Read the project’s AGENTS.md (scaffolded by day new) for the page/localization/id conventions.
  2. Edit code with normal file tools.
  3. day_relaunch → compile errors come back in the result; fix; repeat.
  4. day_drive → navigate to the changed screen, assert ids/text, screenshot, and look at it. On every affected target.
  5. day launch -t <target> --script dayscript/walkthrough.yaml when the change touches walkthrough-covered flows.

Security posture

The engine binds loopback only and requires the per-launch token; sessions.json holds that token, scoped to the project’s own build directory. MCP clients surface tool calls for user confirmation per their own policy (VS Code agent mode does). The VS Code extension declares untrustedWorkspaces: false; none of this runs in Restricted Mode.