Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c45f982
feat(flow): add an idle condition that waits for the screen to stop m…
hubgan Aug 4, 2026
1eb9be5
fix(flow): make the idle condition warn instead of failing the run
hubgan Aug 5, 2026
5a59d31
docs(skills): name the step warning field rather than its rendered glyph
hubgan Aug 5, 2026
c7a14d4
fix(flow): report the small, permanent motion a spinner makes
hubgan Aug 6, 2026
0d98a25
fix(flow): stop calling a wedged app an animating screen
hubgan Aug 6, 2026
7cb9afd
fix(flow): reject an idle wait that cannot contain its own settle
hubgan Aug 6, 2026
ce97918
fix(flow): let a flow past a screen whose tree reads back empty
hubgan Aug 6, 2026
2808f82
fix(flow): tell a `when: { idle: true }` guard what it should have been
hubgan Aug 6, 2026
2884d7c
fix(flow): name the spelling when `idle` is written as a step of its own
hubgan Aug 6, 2026
e93cc42
test(flow): pin that a fractional idle timeout gets a real answer
hubgan Aug 6, 2026
b9b5b58
fix(flow): stop one missed capture from blinding two intervals
hubgan Aug 6, 2026
f6601eb
perf(flow): take Chromium's settle capture at the scale it asks for
hubgan Aug 6, 2026
9c94ce1
test(flow): hold the idle check's constants and unexercised branches
hubgan Aug 6, 2026
7c03269
docs(flow): make every description of the idle step match what it does
hubgan Aug 6, 2026
1f98b23
docs(flow): cite the measured numbers behind the localized-motion floor
hubgan Aug 6, 2026
5c38fac
fix(flow): aim the Chromium settle capture at the window, not the pag…
hubgan Aug 6, 2026
87bde5e
fix(flow): keep the system status bar out of the settle's comparison
hubgan Aug 6, 2026
cd1f7b5
fix(flow): bound the tvOS settle capture so a wedged xcrun is killed
hubgan Aug 6, 2026
a12d918
fix(flow): print a passing step's warning in a directory run
hubgan Aug 6, 2026
f332de3
fix(flow): stop a blank read from counting as a look at the screen
hubgan Aug 6, 2026
b4a0068
fix(flow): end an assert+idle mix in one error instead of two
hubgan Aug 6, 2026
741f125
fix(flow): size the localized-motion floor in pixels, not in frame area
hubgan Aug 6, 2026
c93d515
docs(flow): make every account of the idle step add up
hubgan Aug 6, 2026
22f38d5
test(flow): hold the idle branches nothing was watching
hubgan Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions packages/argent-cli/src/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ export interface StepReport {
status: "pass" | "fail" | "skip" | "error";
reason?: string;
/**
* Legacy: older tool-servers passed a snapshot that adopted a missing
* baseline and annotated it with this caveat (a missing baseline now fails
* the step). Rendered for wire compat with a not-yet-updated server.
* A step that passed in a way that weakens it as proof — raised today by
* `await: { idle: true }`, which never fails a run and says here what its
* green actually bought (see StepReport.warning in the tool-server's
* flow-run). Also carries the caveat older tool-servers put on a snapshot
* that adopted a missing baseline, which now fails the step instead. Live
* either way: dropping the field would silently delete the only thing the
* readiness check reports.
*/
warning?: string;
tool?: string;
Expand Down Expand Up @@ -340,14 +344,19 @@ export function renderArtifactLines(report: FlowReport): string[] {
* Batch mode prints only what needs attention: each fail/error step with its
* under-lines, numbered by walking the full step list so the numbers match a
* single-mode rerun of the same flow.
*
* A PASSING step carrying a warning needs attention too. `await: { idle: true }`
* only ever warns on a step that passed, and renderSummary counts every warning
* whatever its status — so skipping those here printed "1 warning" with the
* text nowhere on screen, which is the whole of what the step reports.
*/
export function renderFailedSteps(report: FlowReport): string[] {
const lines: string[] = [];
let n = 0;
for (const s of report.steps) {
if (s.kind === "echo") continue;
n++;
if (s.status !== "fail" && s.status !== "error") continue;
if (s.status !== "fail" && s.status !== "error" && !s.warning) continue;
lines.push(renderStepLine(s, n, report.flow));
if (s.warning) lines.push(renderUnderStepLine(s, n, `⚠ ${s.warning}`));
if (s.artifacts && typeof s.artifacts === "object") {
Expand Down
15 changes: 15 additions & 0 deletions packages/argent-cli/test/flow-render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,21 @@ describe("flow report rendering", () => {
expect(renderFailedSteps(mkReport([{ index: 0, kind: "tap", status: "pass" }]))).toEqual([]);
});

it("renderFailedSteps prints a passing step's warning, which renderSummary counts", () => {
// `await: { idle: true }` only ever warns on a step that PASSED, and the
// summary counts warnings whatever the status — so a directory run used to
// report "1 warning" with the text nowhere on screen.
const report = mkReport([
{ index: 0, kind: "tap", status: "pass" },
{ index: 1, kind: "idle", status: "pass", warning: "the screen never held still" },
]);
expect(renderFailedSteps(report)).toEqual([
" ⚠ 2 idle",
" ⚠ the screen never held still",
]);
expect(renderSummary(report)).toContain("1 warning");
});

it("renderBatchSummary mirrors the step summary's verdict shape", () => {
expect(renderBatchSummary({ total: 3, passed: 2, failed: 1, skipped: 0 })).toBe(
"FAIL — 3 flows: 2 passed, 1 failed, 0 skipped"
Expand Down
10 changes: 7 additions & 3 deletions packages/argent-mcp/src/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,13 @@ export type FlowStepResult = {
status?: "pass" | "fail" | "skip" | "error";
reason?: string;
/**
* Legacy: older tool-servers passed a snapshot that adopted a missing
* baseline and annotated it with this caveat (a missing baseline now fails
* the step). Rendered for wire compat with a not-yet-updated server.
* A step that passed in a way that weakens it as proof — raised today by
* `await: { idle: true }`, which never fails a run and says here what its
* green actually bought (see StepReport.warning in the tool-server's
* flow-run). Also carries the caveat older tool-servers put on a snapshot
* that adopted a missing baseline, which now fails the step instead. Live
* either way: dropping the field would silently delete the only thing the
* readiness check reports.
*/
warning?: string;
tool?: string;
Expand Down
16 changes: 15 additions & 1 deletion packages/skills/skills/argent-create-flow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Beyond raw `tool:` steps and `echo:`, flows support declarative directives inter
| `scroll-to` | `- scroll-to: "Order #1234"` (scrolls down) or `- scroll-to: { target: …, direction: right, within: … }` | momentum-free scroll until the target is visible |
| `pinch` | `- pinch: { on: "Map", scale: 3 }` or `- pinch: { scale: 0.5 }` | two-finger zoom in (`scale` > 1) or out (`< 1`); big scales chain gestures; `on` optional — defaults to screen center; open-loop — assert the visible result |
| `rotate` | `- rotate: { on: "Map", by: 90 }` or `- rotate: { by: -45 }` | two-finger rotation by degrees (+ CW, − CCW, within ±3000°; options map only); `on` optional — screen center default; not `tool: rotate` (orientation) |
| `await` | `- await: { visible: Home }` | wait for a UI condition |
| `await` | `- await: { visible: Home }` or `- await: { idle: true }` | wait for a UI condition, or for the screen to stop moving |
| `wait` | `- wait: 500` | pause for a fixed number of milliseconds (last resort — prefer `await`) |
| `assert` | `- assert: { visible: Welcome }` | check a condition, hard-fail if it never holds |
| `snapshot` | `- snapshot: home` or `- snapshot: { name: home, maxMismatch: 0.5, cropOn: { id: order-summary } }` | diff a screenshot — or one element's region — against a stored baseline |
Expand Down Expand Up @@ -85,6 +85,20 @@ This condition-as-key form is the only spelling. `await` also accepts an optiona

For a custom poll interval or bundleId, drop to an explicit `- tool: await-ui-element` step — but the raw tool polls the trimmed `describe` tree, so a testID it reports as not found can still resolve fine as an `await:` directive (see Selectors). Prefer the directive.

**`await: { idle: true }` — the one condition with no selector.** It waits until the screen has content and stops moving in **both** the UI tree and the rendered pixels. Options: `minStableMs` (how long stillness must hold, default 250) and `timeout` (default 7500, and it has to leave room for the hold plus the 600ms a settle costs — three reads spanning two 200ms polls, plus the 200ms of budget the closing round has to have left to be allowed to start — or the parser rejects the step). Reach for it when a transition's motion is invisible to the tree — an iOS push or modal dismissal commits its hierarchy up front and then animates a layer for a few hundred milliseconds, and a cross-fade or scrim moves no node at all — which is exactly when an element `await:` returns while the screen is still sliding, and the next tap lands on a moving target.

It **never fails a run.** Readiness is not an acceptance criterion, so every outcome short of a clean settle passes carrying a `warning` on the step — read it rather than stepping over it:

- **the screen never held still** — it spent the timeout and went ahead. Plenty of healthy screens never stop (a video, a shimmer, a carousel, live-updating text); a screen that never finished loading looks the same from here.
- **a small part of it was still changing** — a spinner, a caret, a progress dot, moving during the stretch of stillness the step settled on. Too small to be the screen moving, so the settle completed anyway; if it is a loading spinner, the screen was still loading when this step returned.
- **the tree stayed empty** — the screen rendered no accessible content. Sometimes the app (a canvas, a video surface), sometimes a screen that never arrived.
- **settled on the UI tree alone** — the screen could not be screenshotted often enough to compare a pair, so presentation-layer motion (a push, a fade, a dismissing modal) was not waited out.
- **the screen came back with content on too few reads** — a settle takes three of them spanning two polls, and this step got fewer, so it ended without ever being able to tell whether the screen was moving. A slow tree source, or a window that was blank for most of the wait.

Only a tree source that cannot be read stops the run, as an `error` — one that fails outright, one that answers and then wedges, or one that never answers at all within the step (that last one may simply be slow: raise the step's `timeout` before suspecting the app). That is a broken window, not a verdict about the app: the run is not ok and every later step is skipped.

It is **not** a screen check either: a dropped tap leaves the source screen perfectly idle. Put it **after** the element `await:` that names the destination, never instead of one. There is no `assert` form (waiting is the whole point), no `when:` form, and the recorder cannot emit one — every `idle` step is hand-written. Do not sprinkle it after every step: each one costs a settle, and it cannot fail, so a flow full of them is slower without being stricter.

### `type` and `scroll-to`

`type` presses Enter after typing to commit the value and dismiss the keyboard, so it can't cover later targets. For a chained form whose fields feed one explicit submit — e.g. email then password then a `tap: "Log in"` — set `submit: false` on the intermediate fields so a premature Enter doesn't fire the form early: `type: { into: password, text: "hunter2", submit: false }`.
Expand Down
Loading