@swmansion/argent 0.20.1-next.7 → 0.20.1-next.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16776,10 +16776,10 @@ var require_browser = __commonJS({
16776
16776
  exports2.useColors = useColors;
16777
16777
  exports2.storage = localstorage();
16778
16778
  exports2.destroy = /* @__PURE__ */ (() => {
16779
- let warned = false;
16779
+ let warned2 = false;
16780
16780
  return () => {
16781
- if (!warned) {
16782
- warned = true;
16781
+ if (!warned2) {
16782
+ warned2 = true;
16783
16783
  console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.");
16784
16784
  }
16785
16785
  };
@@ -109158,12 +109158,16 @@ function settleWithin(p, ms, signal) {
109158
109158
  };
109159
109159
  p.then(
109160
109160
  (value) => finish({ type: "value", value }),
109161
- (err) => finish({ type: "error", error: err instanceof Error ? err.message : String(err) })
109161
+ (err) => {
109162
+ const cause = err instanceof Error ? err : new Error(String(err));
109163
+ finish({ type: "error", error: cause.message, cause });
109164
+ }
109162
109165
  );
109163
109166
  if (signal?.aborted) return finish({ type: "aborted" });
109164
109167
  const onAbort = () => finish({ type: "aborted" });
109165
109168
  signal?.addEventListener("abort", onAbort, { once: true });
109166
109169
  teardown.push(() => signal?.removeEventListener("abort", onAbort));
109170
+ if (ms === void 0) return;
109167
109171
  const timer = setTimeout(() => finish({ type: "timeout" }), Math.max(0, ms));
109168
109172
  teardown.push(() => clearTimeout(timer));
109169
109173
  });
@@ -133794,89 +133798,130 @@ var ARGENT_PROFILER_PREFIX = "__argent_";
133794
133798
  function isArgentProfilerFunction(name) {
133795
133799
  return name.startsWith(ARGENT_PROFILER_PREFIX);
133796
133800
  }
133797
- function buildCpuSampleIndex(cpuProfile, firstCommitTimestampMs) {
133801
+ function buildCpuSampleIndex(cpuProfile) {
133798
133802
  const { nodes, samples, timeDeltas, startTime, endTime } = cpuProfile;
133799
133803
  const nodeMap = /* @__PURE__ */ new Map();
133800
133804
  for (const node of nodes) {
133801
133805
  nodeMap.set(node.id, node);
133802
133806
  }
133803
- const cpuStartMs = startTime / 1e3;
133804
- let clockOffsetMs = 0;
133805
- if (firstCommitTimestampMs !== null && firstCommitTimestampMs > 0) {
133806
- const diff = firstCommitTimestampMs - cpuStartMs;
133807
- if (Math.abs(diff) > 1e3) {
133808
- clockOffsetMs = diff;
133809
- }
133810
- }
133811
133807
  const timestampsMs = new Float64Array(samples.length);
133812
- let accumulatedUs = startTime;
133808
+ const intervalStartsMs = new Float64Array(samples.length);
133809
+ let accumulatedUs = 0;
133813
133810
  for (let i = 0; i < samples.length; i++) {
133814
- accumulatedUs += timeDeltas[i] ?? 0;
133815
- timestampsMs[i] = accumulatedUs / 1e3 + clockOffsetMs;
133811
+ const raw = timeDeltas[i];
133812
+ const delta = Number.isFinite(raw) && (raw ?? 0) >= 0 ? raw : 0;
133813
+ intervalStartsMs[i] = accumulatedUs / 1e3;
133814
+ accumulatedUs += delta;
133815
+ timestampsMs[i] = accumulatedUs / 1e3;
133816
133816
  }
133817
133817
  return {
133818
+ childToParent: buildChildToParent(nodeMap),
133818
133819
  timestampsMs,
133820
+ intervalStartsMs,
133819
133821
  sampleNodeIds: samples,
133820
133822
  nodeMap,
133821
133823
  durationMs: (endTime - startTime) / 1e3
133822
133824
  };
133823
133825
  }
133826
+ var IDLE_FRAME_NAMES = /* @__PURE__ */ new Set(["(idle)", "(program)", "(root)", "[idle]", "[root]"]);
133827
+ function isIdleFrame(name) {
133828
+ return !name || IDLE_FRAME_NAMES.has(name);
133829
+ }
133824
133830
  function queryCpuWindow(index, startMs, endMs, topN2 = 5) {
133825
- const { timestampsMs, sampleNodeIds, nodeMap } = index;
133831
+ const { timestampsMs, intervalStartsMs, sampleNodeIds, nodeMap } = index;
133832
+ const n = timestampsMs.length;
133833
+ const sampleRangeMs = {
133834
+ start: n > 0 ? intervalStartsMs[0] : 0,
133835
+ end: n > 0 ? timestampsMs[n - 1] : 0
133836
+ };
133837
+ const empty2 = {
133838
+ hotspots: [],
133839
+ samplesInWindow: 0,
133840
+ coveredMs: 0,
133841
+ idleMs: 0,
133842
+ sampleRangeMs,
133843
+ medianIntervalMs: 0,
133844
+ maxIntervalMs: 0
133845
+ };
133846
+ if (n === 0) return empty2;
133826
133847
  let lo = 0;
133827
- let hi = timestampsMs.length;
133848
+ let hi = n;
133828
133849
  while (lo < hi) {
133829
133850
  const mid = lo + hi >>> 1;
133830
133851
  if (timestampsMs[mid] < startMs) lo = mid + 1;
133831
133852
  else hi = mid;
133832
133853
  }
133833
- const selfHits = /* @__PURE__ */ new Map();
133834
- let totalSamples = 0;
133835
- for (let i = lo; i < timestampsMs.length; i++) {
133836
- if (timestampsMs[i] > endMs) break;
133854
+ const selfMsByNode = /* @__PURE__ */ new Map();
133855
+ const intervals = [];
133856
+ let samplesInWindow = 0;
133857
+ let coveredMs = 0;
133858
+ let idleMs = 0;
133859
+ let maxIntervalMs = 0;
133860
+ for (let i = lo; i < n; i++) {
133861
+ const from2 = intervalStartsMs[i];
133862
+ if (from2 > endMs) break;
133863
+ const overlap = Math.min(endMs, timestampsMs[i]) - Math.max(startMs, from2);
133864
+ if (overlap <= 0) continue;
133865
+ samplesInWindow++;
133866
+ coveredMs += overlap;
133867
+ intervals.push(timestampsMs[i] - from2);
133868
+ if (timestampsMs[i] - from2 > maxIntervalMs) maxIntervalMs = timestampsMs[i] - from2;
133837
133869
  const nodeId = sampleNodeIds[i];
133838
- selfHits.set(nodeId, (selfHits.get(nodeId) ?? 0) + 1);
133839
- totalSamples++;
133840
- }
133841
- if (totalSamples === 0) return [];
133842
- const windowDurationMs = endMs - startMs;
133843
- const avgIntervalMs = totalSamples > 1 ? windowDurationMs / totalSamples : 1;
133844
- const childToParent = /* @__PURE__ */ new Map();
133845
- for (const node of nodeMap.values()) {
133846
- for (const childId of node.children ?? []) {
133847
- childToParent.set(childId, node.id);
133870
+ const node = nodeMap.get(nodeId);
133871
+ if (isIdleFrame(node?.callFrame.functionName)) {
133872
+ idleMs += overlap;
133873
+ continue;
133848
133874
  }
133875
+ selfMsByNode.set(nodeId, (selfMsByNode.get(nodeId) ?? 0) + overlap);
133849
133876
  }
133850
- const totalHits = /* @__PURE__ */ new Map();
133851
- for (const [nodeId, hits] of selfHits) {
133852
- totalHits.set(nodeId, (totalHits.get(nodeId) ?? 0) + hits);
133877
+ if (samplesInWindow === 0) return { ...empty2, sampleRangeMs };
133878
+ intervals.sort((a, b) => a - b);
133879
+ const medianIntervalMs = intervals[Math.floor(intervals.length / 2)] ?? 0;
133880
+ const childToParent = index.childToParent ?? buildChildToParent(nodeMap);
133881
+ const totalMsByNode = /* @__PURE__ */ new Map();
133882
+ for (const [nodeId, ms] of selfMsByNode) {
133883
+ totalMsByNode.set(nodeId, (totalMsByNode.get(nodeId) ?? 0) + ms);
133853
133884
  let current = nodeId;
133885
+ const seen = /* @__PURE__ */ new Set([current]);
133854
133886
  while (childToParent.has(current)) {
133855
133887
  const parent = childToParent.get(current);
133856
- totalHits.set(parent, (totalHits.get(parent) ?? 0) + hits);
133888
+ if (seen.has(parent)) break;
133889
+ seen.add(parent);
133890
+ totalMsByNode.set(parent, (totalMsByNode.get(parent) ?? 0) + ms);
133857
133891
  current = parent;
133858
133892
  }
133859
133893
  }
133860
133894
  const entries = [];
133861
- for (const [nodeId, hits] of selfHits) {
133895
+ for (const [nodeId, ms] of selfMsByNode) {
133862
133896
  const node = nodeMap.get(nodeId);
133863
133897
  if (!node) continue;
133864
133898
  const name = node.callFrame.functionName;
133865
- if (!name || name === "(idle)" || name === "(program)" || name === "(root)" || name === "[idle]" || name === "[root]")
133866
- continue;
133867
133899
  if (isArgentProfilerFunction(name)) continue;
133868
- const selfMs = Math.round(hits * avgIntervalMs * 100) / 100;
133869
- const totalMs = Math.round((totalHits.get(nodeId) ?? hits) * avgIntervalMs * 100) / 100;
133870
133900
  entries.push({
133871
133901
  name,
133872
- selfMs,
133873
- totalMs,
133902
+ selfMs: Math.round(ms * 100) / 100,
133903
+ totalMs: Math.round((totalMsByNode.get(nodeId) ?? ms) * 100) / 100,
133874
133904
  url: node.callFrame.url || void 0,
133875
133905
  lineNumber: node.callFrame.lineNumber >= 0 ? node.callFrame.lineNumber : void 0
133876
133906
  });
133877
133907
  }
133878
133908
  entries.sort((a, b) => b.selfMs - a.selfMs);
133879
- return entries.slice(0, topN2);
133909
+ return {
133910
+ hotspots: entries.slice(0, topN2),
133911
+ samplesInWindow,
133912
+ coveredMs,
133913
+ idleMs,
133914
+ sampleRangeMs,
133915
+ medianIntervalMs,
133916
+ maxIntervalMs
133917
+ };
133918
+ }
133919
+ function buildChildToParent(nodeMap) {
133920
+ const childToParent = /* @__PURE__ */ new Map();
133921
+ for (const node of nodeMap.values()) {
133922
+ for (const childId of node.children ?? []) childToParent.set(childId, node.id);
133923
+ }
133924
+ return childToParent;
133880
133925
  }
133881
133926
  function correlateCpuWithCommits(summaries, index, topNPerCommit = 5) {
133882
133927
  if (!index) return summaries;
@@ -133884,28 +133929,35 @@ function correlateCpuWithCommits(summaries, index, topNPerCommit = 5) {
133884
133929
  if (summary.isMargin) return summary;
133885
133930
  const startMs = summary.timestampMs;
133886
133931
  const endMs = summary.timestampMs + summary.totalRenderMs;
133887
- const hotspots = queryCpuWindow(index, startMs, endMs, topNPerCommit);
133932
+ const { hotspots } = queryCpuWindow(index, startMs, endMs, topNPerCommit);
133888
133933
  if (hotspots.length === 0) return summary;
133889
133934
  return { ...summary, cpuHotspots: hotspots };
133890
133935
  });
133891
133936
  }
133892
133937
  function serializeCpuSampleIndex(index) {
133893
133938
  return {
133939
+ version: 2,
133894
133940
  timestampsMs: Array.from(index.timestampsMs),
133941
+ intervalStartsMs: Array.from(index.intervalStartsMs),
133895
133942
  sampleNodeIds: index.sampleNodeIds,
133896
133943
  nodes: [...index.nodeMap.values()],
133897
133944
  durationMs: index.durationMs
133898
133945
  };
133899
133946
  }
133900
133947
  function deserializeCpuSampleIndex(raw) {
133948
+ if (raw?.version !== 2 || !Array.isArray(raw.timestampsMs) || !Array.isArray(raw.nodes)) {
133949
+ throw new Error("unsupported CPU sample index format");
133950
+ }
133901
133951
  const nodeMap = /* @__PURE__ */ new Map();
133902
133952
  for (const node of raw.nodes) {
133903
133953
  nodeMap.set(node.id, node);
133904
133954
  }
133905
133955
  return {
133906
133956
  timestampsMs: new Float64Array(raw.timestampsMs),
133957
+ intervalStartsMs: new Float64Array(raw.intervalStartsMs ?? []),
133907
133958
  sampleNodeIds: raw.sampleNodeIds,
133908
133959
  nodeMap,
133960
+ childToParent: buildChildToParent(nodeMap),
133909
133961
  durationMs: raw.durationMs
133910
133962
  };
133911
133963
  }
@@ -134208,8 +134260,7 @@ async function runPipeline(input, options) {
134208
134260
  hotCommitIndices,
134209
134261
  input.sessionMeta.unattributedByCommit
134210
134262
  );
134211
- const firstCommitTs = preprocessed.length > 0 ? preprocessed[0].timestamp : null;
134212
- const cpuSampleIndex = input.flamegraph ? buildCpuSampleIndex(input.flamegraph, firstCommitTs) : null;
134263
+ const cpuSampleIndex = input.flamegraph ? buildCpuSampleIndex(input.flamegraph) : null;
134213
134264
  const hotCommitSummaries = correlateCpuWithCommits(rawHotCommitSummaries, cpuSampleIndex);
134214
134265
  const preprocessedCommitTree = { ...input.commitTree, commits: preprocessed };
134215
134266
  const reduceOutput = reduce(
@@ -140615,8 +140666,12 @@ init_zod();
140615
140666
  init_src();
140616
140667
  var import_fs18 = require("fs");
140617
140668
  var timeWindowSchema = external_exports.object({
140618
- start: external_exports.coerce.number().describe("Start of window in ms (performance.now clock)"),
140619
- end: external_exports.coerce.number().describe("End of window in ms (performance.now clock)")
140669
+ start: external_exports.coerce.number().describe(
140670
+ "Start of window in ms since profiling started \u2014 the same clock profiler-commit-query prints"
140671
+ ),
140672
+ end: external_exports.coerce.number().describe(
140673
+ "End of window in ms since profiling started \u2014 the same clock profiler-commit-query prints"
140674
+ )
140620
140675
  });
140621
140676
  var zodSchema53 = external_exports.object({
140622
140677
  port: external_exports.coerce.number().default(8081).describe("Metro server port"),
@@ -140626,7 +140681,9 @@ var zodSchema53 = external_exports.object({
140626
140681
  mode: external_exports.enum(["top_functions", "time_window", "call_tree", "component_cpu"]).describe(
140627
140682
  "Query mode: top_functions (global hotspots), time_window (CPU in a time range), call_tree (callers/callees of a function), component_cpu (CPU during a component's commits)"
140628
140683
  ),
140629
- time_window_ms: timeWindowSchema.optional().describe("Time window filter for time_window mode (ms, performance.now clock)"),
140684
+ time_window_ms: timeWindowSchema.optional().describe(
140685
+ "Time window filter for time_window mode (ms since profiling started \u2014 the same clock profiler-commit-query prints)"
140686
+ ),
140630
140687
  component_name: external_exports.string().optional().describe("Component name for component_cpu mode"),
140631
140688
  function_name: external_exports.string().optional().describe("Function name for call_tree mode"),
140632
140689
  top_n: external_exports.coerce.number().int().positive().default(15).describe("Number of results to return (default 15)"),
@@ -140659,31 +140716,60 @@ async function getIndex(sessionPaths) {
140659
140716
  }
140660
140717
  const cpuProfile = await readCpuProfile(sessionPaths.cpuProfilePath);
140661
140718
  let commitTree = null;
140662
- let firstCommitTs = null;
140663
140719
  if (sessionPaths.commitsPath) {
140664
140720
  const onDisk = await readCommitTree(sessionPaths.commitsPath);
140665
140721
  commitTree = { commits: onDisk.commits };
140666
- firstCommitTs = onDisk.commits[0]?.timestamp ?? null;
140667
140722
  }
140668
- return { index: buildCpuSampleIndex(cpuProfile, firstCommitTs), commitTree };
140723
+ return { index: buildCpuSampleIndex(cpuProfile), commitTree };
140724
+ }
140725
+ function explainEmptyWindow(res, startMs, endMs) {
140726
+ const range = `${res.sampleRangeMs.start.toFixed(1)}\u2013${res.sampleRangeMs.end.toFixed(1)}ms`;
140727
+ const window2 = `${startMs.toFixed(1)}\u2013${endMs.toFixed(1)}ms`;
140728
+ if (res.sampleRangeMs.end === 0 && res.samplesInWindow === 0) {
140729
+ return "_The CPU profile contains no samples. Sampling produced no data for this session \u2014 that is a capture failure, not a measurement of idleness._";
140730
+ }
140731
+ if (endMs < res.sampleRangeMs.start || startMs > res.sampleRangeMs.end) {
140732
+ return `_No CPU samples exist in ${window2} \u2014 that is outside the recorded sample range (${range}). This is a coverage gap, not a measurement: nothing can be concluded about CPU cost here. Sample times are ms since profiling started, the same clock \`profiler-commit-query\` prints._`;
140733
+ }
140734
+ if (res.samplesInWindow > 0) {
140735
+ return `_${res.samplesInWindow} sample(s) covering ${res.coveredMs.toFixed(1)}ms fell inside ${window2}, and all of them were idle \u2014 the JS thread was not executing during this window. Native or UI-thread work would not appear here; use \`native-profiler-start\` for that._`;
140736
+ }
140737
+ return `_No CPU samples fell inside ${window2} (${(endMs - startMs).toFixed(1)}ms wide), although it lies within the recorded range (${range}). The sampler runs roughly every ${res.medianIntervalMs > 0 ? res.medianIntervalMs.toFixed(1) : "13"}ms, so a window this narrow can contain none at all. **Absence of samples is not evidence that this commit was cheap.** Widen the window, or use \`mode=component_cpu\`._`;
140738
+ }
140739
+ function coverageNote(res, startMs, endMs) {
140740
+ const widthMs = endMs - startMs;
140741
+ const lines = [
140742
+ `**Window:** ${startMs.toFixed(1)}ms \u2192 ${endMs.toFixed(1)}ms (${widthMs.toFixed(1)}ms)`,
140743
+ `**Samples:** ${res.samplesInWindow} covering ${res.coveredMs.toFixed(1)}ms` + (res.idleMs > 0 ? `, of which ${res.idleMs.toFixed(1)}ms idle` : "") + ` \u2014 sampling interval ~${res.medianIntervalMs.toFixed(1)}ms. Self-times sum to sampled coverage, not to the window width.`
140744
+ ];
140745
+ if (widthMs > 0 && widthMs < 3 * res.medianIntervalMs) {
140746
+ lines.push(
140747
+ `> This window is narrower than ~3 sampling intervals, so every figure carries \xB11 sample (\u2248${res.medianIntervalMs.toFixed(1)}ms).`
140748
+ );
140749
+ }
140750
+ if (res.maxIntervalMs > 50 && res.maxIntervalMs > 5 * res.medianIntervalMs) {
140751
+ lines.push(
140752
+ `> The sampler stalled for ${res.maxIntervalMs.toFixed(1)}ms inside this window; that whole gap is attributed to whichever function was caught by the sample that ended it.`
140753
+ );
140754
+ }
140755
+ return lines.join("\n\n");
140669
140756
  }
140670
140757
  function renderTopFunctions(index, topN2, startMs, endMs) {
140671
- const windowStart = startMs ?? index.timestampsMs[0];
140672
- const windowEnd = endMs ?? index.timestampsMs[index.timestampsMs.length - 1];
140673
- const hotspots = queryCpuWindow(index, windowStart, windowEnd, topN2);
140674
- if (hotspots.length === 0) return "_No CPU hotspots found in the specified range._";
140758
+ const windowStart = startMs ?? index.intervalStartsMs[0] ?? 0;
140759
+ const windowEnd = endMs ?? index.timestampsMs[index.timestampsMs.length - 1] ?? 0;
140760
+ const res = queryCpuWindow(index, windowStart, windowEnd, topN2);
140761
+ if (res.hotspots.length === 0) return explainEmptyWindow(res, windowStart, windowEnd);
140675
140762
  const header = "| Function | Self (ms) | Total (ms) | Location |";
140676
140763
  const sep7 = "|---|---|---|---|";
140677
- const rows = hotspots.map((hs) => {
140764
+ const rows = res.hotspots.map((hs) => {
140678
140765
  const loc = hs.url ? `${shortenUrl(hs.url)}${hs.lineNumber != null ? `:${hs.lineNumber}` : ""}` : "\u2014";
140679
140766
  return `| \`${hs.name}\` | ${hs.selfMs} | ${hs.totalMs} | ${loc} |`;
140680
140767
  });
140681
- const rangeNote = startMs != null ? `**Window:** ${startMs.toFixed(1)}ms \u2192 ${endMs.toFixed(1)}ms
140682
-
140683
- ` : "";
140684
140768
  return `## CPU Hotspots
140685
140769
 
140686
- ${rangeNote}${header}
140770
+ ${coverageNote(res, windowStart, windowEnd)}
140771
+
140772
+ ${header}
140687
140773
  ${sep7}
140688
140774
  ${rows.join("\n")}`;
140689
140775
  }
@@ -140813,7 +140899,7 @@ function renderComponentCpu(index, commitTree, componentName, topN2) {
140813
140899
  }
140814
140900
  const aggregated = /* @__PURE__ */ new Map();
140815
140901
  for (const window2 of commitWindows.values()) {
140816
- const hotspots = queryCpuWindow(index, window2.start, window2.end, 50);
140902
+ const { hotspots } = queryCpuWindow(index, window2.start, window2.end, 50);
140817
140903
  for (const hs of hotspots) {
140818
140904
  const existing = aggregated.get(hs.name);
140819
140905
  if (existing) {
@@ -140867,6 +140953,10 @@ Requires react-profiler-stop (and ideally react-profiler-analyze) to have been c
140867
140953
  Modes:
140868
140954
  - top_functions: Global CPU hotspots ranked by self-time. Optional time_window_ms to filter.
140869
140955
  - time_window: CPU breakdown for a specific time range (e.g. during a slow commit or hang).
140956
+
140957
+ Self-times are the summed sampling intervals of the samples that landed in the window, so they
140958
+ measure sampled coverage rather than the window's width and do not change if you widen the query.
140959
+ Every table states how many samples it covers and how much of that was idle.
140870
140960
  - call_tree: For a given function_name, show its callees and optionally callers.
140871
140961
  - component_cpu: For a given component_name, aggregate CPU activity across all its commits.
140872
140962
  Use when investigating JS CPU hotspots or correlating CPU cost with specific components.
@@ -140952,8 +141042,12 @@ Fails if no CPU profile is stored \u2014 run react-profiler-stop first.`,
140952
141042
  init_zod();
140953
141043
  init_src();
140954
141044
  var timeRangeSchema = external_exports.object({
140955
- start: external_exports.coerce.number().describe("Start of range in ms (performance.now clock)"),
140956
- end: external_exports.coerce.number().describe("End of range in ms (performance.now clock)")
141045
+ start: external_exports.coerce.number().describe(
141046
+ "Start of range in ms since profiling started \u2014 the same clock profiler-commit-query prints"
141047
+ ),
141048
+ end: external_exports.coerce.number().describe(
141049
+ "End of range in ms since profiling started \u2014 the same clock profiler-commit-query prints"
141050
+ )
140957
141051
  });
140958
141052
  var zodSchema54 = external_exports.object({
140959
141053
  port: external_exports.coerce.number().default(8081).describe("Metro server port"),
@@ -145165,19 +145259,18 @@ async function queryVegaTree(device) {
145165
145259
 
145166
145260
  // ../tool-server/src/tools/flows/flow-tree.ts
145167
145261
  async function fetchFlowTree(registry2, device, launchedNativeApp) {
145168
- if (device.platform === "ios") {
145169
- return queryFullHierarchyTree(registry2, device, launchedNativeApp);
145170
- }
145171
- if (device.platform === "android") {
145172
- return queryAndroidFullHierarchy(registry2, device);
145173
- }
145174
- if (device.platform === "chromium") {
145175
- return queryChromiumTree(registry2, device);
145176
- }
145177
- if (device.platform === "vega") {
145178
- return queryVegaTree(device);
145179
- }
145180
- return fetchTree(registry2, device);
145262
+ const source = FLOW_TREE_SOURCES[device.platform];
145263
+ if (!source) return fetchTree(registry2, device);
145264
+ return source(registry2, device, launchedNativeApp);
145265
+ }
145266
+ var FLOW_TREE_SOURCES = {
145267
+ ios: (registry2, device, launchedNativeApp) => queryFullHierarchyTree(registry2, device, launchedNativeApp),
145268
+ android: (registry2, device) => queryAndroidFullHierarchy(registry2, device),
145269
+ chromium: (registry2, device) => queryChromiumTree(registry2, device),
145270
+ vega: (_registry, device) => queryVegaTree(device)
145271
+ };
145272
+ function supportsFlowTree(platform) {
145273
+ return FLOW_TREE_SOURCES[platform] !== void 0;
145181
145274
  }
145182
145275
 
145183
145276
  // ../tool-server/src/tools/flows/flow-add-step.ts
@@ -145511,11 +145604,16 @@ function runSequenceOutcome(result) {
145511
145604
  }
145512
145605
  return void 0;
145513
145606
  }
145607
+ var NESTED_ORCHESTRATORS = /* @__PURE__ */ new Map([
145608
+ [FLOW_EXECUTE_TOOL_ID, flowExecuteOutcome],
145609
+ [RUN_SEQUENCE_TOOL_ID, runSequenceOutcome]
145610
+ ]);
145611
+ function isNestedOrchestratorTool(tool) {
145612
+ return NESTED_ORCHESTRATORS.has(tool);
145613
+ }
145514
145614
  function nestedOrchestratorOutcome(tool, result) {
145515
145615
  if (!isRecord(result)) return void 0;
145516
- if (tool === FLOW_EXECUTE_TOOL_ID) return flowExecuteOutcome(result);
145517
- if (tool === RUN_SEQUENCE_TOOL_ID) return runSequenceOutcome(result);
145518
- return void 0;
145616
+ return NESTED_ORCHESTRATORS.get(tool)?.(result);
145519
145617
  }
145520
145618
 
145521
145619
  // ../tool-server/src/tools/flows/flow-pixels.ts
@@ -145659,6 +145757,7 @@ var FOCUS_REPORTING_SOURCES = /* @__PURE__ */ new Set([
145659
145757
  ]);
145660
145758
  var SETTLE_POLL_MS = 250;
145661
145759
  var SETTLE_TIMEOUT_MS = 3e3;
145760
+ var SETTLE_MIN_READS = 2;
145662
145761
  var MAX_SCROLL_ITERATIONS = 25;
145663
145762
  var SCROLL_INCREMENT = 0.5;
145664
145763
  var MIN_SCROLL_INCREMENT = 0.05;
@@ -145709,19 +145808,34 @@ function flowSelectorToFrame(tree, sel) {
145709
145808
  }
145710
145809
  return void 0;
145711
145810
  }
145712
- async function settleTree(env) {
145811
+ function provenTreeOutage(env) {
145812
+ const proven = env.treeOutage?.proven;
145813
+ return proven && proven.deviceId === env.device.id ? proven.error : void 0;
145814
+ }
145815
+ function readFlowTree(env) {
145816
+ return fetchFlowTree(env.registry, env.device, env.launchedNativeApp).then((data) => {
145817
+ if (env.treeOutage) env.treeOutage.proven = void 0;
145818
+ return data;
145819
+ });
145820
+ }
145821
+ async function settleTree(env, opts = {}) {
145713
145822
  const deadline = Date.now() + SETTLE_TIMEOUT_MS;
145714
145823
  let prevFp;
145715
145824
  let prevTree;
145716
145825
  let lastError;
145826
+ let reads = 0;
145717
145827
  for (; ; ) {
145718
145828
  if (env.signal?.aborted) return void 0;
145829
+ const proven = provenTreeOutage(env);
145830
+ if (opts.skipProvenOutage && proven) throw proven;
145831
+ const read = await settleWithin(readFlowTree(env), void 0, env.signal);
145719
145832
  let tree;
145720
- try {
145721
- ({ tree } = await fetchFlowTree(env.registry, env.device, env.launchedNativeApp));
145722
- } catch (err) {
145723
- lastError = err instanceof Error ? err : new Error(String(err));
145833
+ if (read.type === "value") {
145834
+ tree = read.value.tree;
145835
+ } else if (read.type === "error") {
145836
+ lastError = read.cause;
145724
145837
  }
145838
+ reads += 1;
145725
145839
  if (env.signal?.aborted) return void 0;
145726
145840
  if (tree !== void 0) {
145727
145841
  const fp = treeFingerprint(tree);
@@ -145730,7 +145844,11 @@ async function settleTree(env) {
145730
145844
  prevTree = tree;
145731
145845
  }
145732
145846
  if (Date.now() >= deadline) {
145733
- if (prevTree === void 0 && lastError !== void 0) throw lastError;
145847
+ if (reads < SETTLE_MIN_READS) continue;
145848
+ if (prevTree === void 0 && lastError !== void 0) {
145849
+ if (env.treeOutage) env.treeOutage.proven = { deviceId: env.device.id, error: lastError };
145850
+ throw lastError;
145851
+ }
145734
145852
  return prevTree;
145735
145853
  }
145736
145854
  if (!await sleepOrAbort(SETTLE_POLL_MS, env.signal)) return void 0;
@@ -145779,7 +145897,7 @@ async function waitForFocus(env, into, tappedFrame) {
145779
145897
  for (; ; ) {
145780
145898
  if (env.signal?.aborted) return;
145781
145899
  try {
145782
- const { tree, source } = await fetchFlowTree(env.registry, env.device, env.launchedNativeApp);
145900
+ const { tree, source } = await readFlowTree(env);
145783
145901
  if (!FOCUS_REPORTING_SOURCES.has(source)) return;
145784
145902
  const target = flowSelectorToFrame(tree, into) ?? tappedFrame;
145785
145903
  if (collectFocused(tree, []).some((n) => framesOverlap(n.frame, target))) return;
@@ -145892,6 +146010,25 @@ async function runDirective(env, step) {
145892
146010
  return runRotate(env, step);
145893
146011
  }
145894
146012
  }
146013
+ async function settleForGesture(env) {
146014
+ let warning;
146015
+ if (supportsFlowTree(env.device.platform)) {
146016
+ try {
146017
+ await settleTree(env, { skipProvenOutage: true });
146018
+ } catch (err) {
146019
+ warning = unsettledGestureWarning(err);
146020
+ }
146021
+ }
146022
+ if (env.signal?.aborted) return { aborted: true };
146023
+ return warning !== void 0 ? { warning } : {};
146024
+ }
146025
+ function warned(settle) {
146026
+ return settle.warning !== void 0 ? { warning: settle.warning } : {};
146027
+ }
146028
+ function unsettledGestureWarning(err) {
146029
+ const reason = err instanceof Error ? err.message : String(err);
146030
+ return `dispatched without settling the screen first: the UI tree could not be read (${reason}), so there was no way to tell whether anything was moving. The gesture went out against whatever was in flight, and one aimed at a moving element can miss it entirely - this step passing says it was sent, not that it landed. Restore the tree source, or put an explicit \`wait:\` in front of gestures that follow a transition.`;
146031
+ }
145895
146032
  async function resolveTargetPoint(env, target) {
145896
146033
  if (target.selector) {
145897
146034
  const frame = await waitForFrame(env, target.selector);
@@ -145899,26 +146036,29 @@ async function resolveTargetPoint(env, target) {
145899
146036
  if (!frame) {
145900
146037
  return { fail: { ok: false, reason: offscreenHint(target.selector) } };
145901
146038
  }
145902
- return getDescribeTapPoint(frame);
146039
+ return { point: getDescribeTapPoint(frame) };
145903
146040
  }
145904
146041
  if (typeof target.x === "number" && typeof target.y === "number") {
145905
- return { x: target.x, y: target.y };
146042
+ const settle = await settleForGesture(env);
146043
+ if (settle.aborted) return { fail: ABORTED_OUTCOME };
146044
+ return { point: { x: target.x, y: target.y }, ...warned(settle) };
145906
146045
  }
145907
146046
  return { fail: { ok: false, reason: "gesture needs a selector or x/y coordinates" } };
145908
146047
  }
145909
146048
  async function runTap(env, target) {
145910
- const point = await resolveTargetPoint(env, target);
145911
- if ("fail" in point) return point.fail;
146049
+ const resolved = await resolveTargetPoint(env, target);
146050
+ if ("fail" in resolved) return resolved.fail;
145912
146051
  await invokeOnDevice(env, "gesture-tap", {
145913
- ...point,
146052
+ ...resolved.point,
145914
146053
  ...target.times !== void 0 ? { clickCount: target.times } : {}
145915
146054
  });
145916
- return { ok: true };
146055
+ return { ok: true, ...warned(resolved) };
145917
146056
  }
145918
146057
  var DEFAULT_LONG_PRESS_MS = 800;
145919
146058
  async function runLongPress(env, step) {
145920
- const point = await resolveTargetPoint(env, step);
145921
- if ("fail" in point) return point.fail;
146059
+ const resolved = await resolveTargetPoint(env, step);
146060
+ if ("fail" in resolved) return resolved.fail;
146061
+ const point = resolved.point;
145922
146062
  const duration3 = step.duration ?? DEFAULT_LONG_PRESS_MS;
145923
146063
  if (env.device.platform === "chromium") {
145924
146064
  await invokeOnDevice(env, "gesture-drag", {
@@ -145936,17 +146076,21 @@ async function runLongPress(env, step) {
145936
146076
  ]
145937
146077
  });
145938
146078
  }
145939
- return { ok: true };
146079
+ return { ok: true, ...warned(resolved) };
145940
146080
  }
145941
146081
  async function runPinch(env, step) {
145942
146082
  let center = { x: 0.5, y: 0.5 };
145943
146083
  let frame;
146084
+ let settle = {};
145944
146085
  if (step.selector) {
145945
146086
  const resolved = await waitForFrame(env, step.selector);
145946
146087
  if (resolved === "aborted") return ABORTED_OUTCOME;
145947
146088
  if (!resolved) return { ok: false, reason: offscreenHint(step.selector) };
145948
146089
  frame = resolved;
145949
146090
  center = getDescribeTapPoint(resolved);
146091
+ } else {
146092
+ settle = await settleForGesture(env);
146093
+ if (settle.aborted) return ABORTED_OUTCOME;
145950
146094
  }
145951
146095
  const { n, per } = decomposePinch(step.scale);
145952
146096
  const guards = systemEdgeGuards(env.device);
@@ -145977,11 +146121,11 @@ async function runPinch(env, step) {
145977
146121
  await invokeOnDevice(env, "gesture-pinch", args);
145978
146122
  if (i < n - 1 && !await sleepOrAbort(PINCH_SETTLE_MS, env.signal)) return ABORTED_OUTCOME;
145979
146123
  }
145980
- return { ok: true };
146124
+ return { ok: true, ...warned(settle) };
145981
146125
  }
145982
146126
  async function fetchScreenAspect(env) {
145983
146127
  try {
145984
- const { screen } = await fetchFlowTree(env.registry, env.device, env.launchedNativeApp);
146128
+ const { screen } = await readFlowTree(env);
145985
146129
  return screen && screen.width > 0 && screen.height > 0 ? screen.width / screen.height : void 0;
145986
146130
  } catch {
145987
146131
  return void 0;
@@ -145990,12 +146134,16 @@ async function fetchScreenAspect(env) {
145990
146134
  async function runRotate(env, step) {
145991
146135
  let center = { x: 0.5, y: 0.5 };
145992
146136
  let frame;
146137
+ let settle = {};
145993
146138
  if (step.selector) {
145994
146139
  const resolved = await waitForFrame(env, step.selector);
145995
146140
  if (resolved === "aborted") return ABORTED_OUTCOME;
145996
146141
  if (!resolved) return { ok: false, reason: offscreenHint(step.selector) };
145997
146142
  frame = resolved;
145998
146143
  center = getDescribeTapPoint(resolved);
146144
+ } else {
146145
+ settle = await settleForGesture(env);
146146
+ if (settle.aborted) return ABORTED_OUTCOME;
145999
146147
  }
146000
146148
  const aspect = await fetchScreenAspect(env);
146001
146149
  const guards = systemEdgeGuards(env.device);
@@ -146037,7 +146185,7 @@ async function runRotate(env, step) {
146037
146185
  if (env.signal?.aborted) return ABORTED_OUTCOME;
146038
146186
  throw err;
146039
146187
  }
146040
- return { ok: true };
146188
+ return { ok: true, ...warned(settle) };
146041
146189
  }
146042
146190
  async function runType(env, step) {
146043
146191
  const frame = await waitForFrame(env, step.into);
@@ -146069,7 +146217,7 @@ async function waitForCondition(env, step, timeoutMs) {
146069
146217
  for (; ; ) {
146070
146218
  if (env.signal?.aborted) return ABORTED_OUTCOME;
146071
146219
  try {
146072
- const data = await fetchFlowTree(env.registry, env.device, env.launchedNativeApp);
146220
+ const data = await readFlowTree(env);
146073
146221
  lastMatches = flowFindAll(data.tree, step.selector);
146074
146222
  fetchError = void 0;
146075
146223
  everMatched ||= lastMatches.length > 0;
@@ -146164,11 +146312,7 @@ async function waitForIdle(env, step) {
146164
146312
  const roundStartedAt = Date.now();
146165
146313
  let answeredReadMs;
146166
146314
  const [read, frame] = await Promise.all([
146167
- settleWithin(
146168
- fetchFlowTree(env.registry, env.device, env.launchedNativeApp),
146169
- roundBudget,
146170
- env.signal
146171
- ).then((r) => {
146315
+ settleWithin(readFlowTree(env), roundBudget, env.signal).then((r) => {
146172
146316
  answeredReadMs = Date.now() - roundStartedAt;
146173
146317
  return r;
146174
146318
  }),
@@ -148807,6 +148951,7 @@ async function treeSourceGate(registry2, device, bundleId, signal) {
148807
148951
  async function runLaunch(state3, app) {
148808
148952
  const env = deviceEnv(state3);
148809
148953
  const { registry: registry2, device, signal } = env;
148954
+ if (state3.treeOutage) state3.treeOutage.proven = void 0;
148810
148955
  if (device.platform === "chromium") return runChromiumLaunch(state3, app);
148811
148956
  const bundleId = appIdForPlatform(app, device.platform);
148812
148957
  if (!bundleId) {
@@ -149028,8 +149173,8 @@ when \`on\` is omitted; distinct from the \`rotate\` tool, which changes device
149028
149173
  for a UI condition, and additionally takes the one condition that has no selector: \`idle: true\` waits
149029
149174
  until the screen has content and stops moving in BOTH the UI tree and the rendered pixels (it never
149030
149175
  fails a run \u2014 a screen that never settles passes carrying a \`warning\`, which is what makes it safe to
149031
- persist; the one outcome that does stop the run is an \`error\` for a tree source that could not be read
149032
- at all \u2014 a broken window rather than a verdict about the app, which leaves the run not-ok and skips
149176
+ persist; the one idle outcome that does stop the run is an \`error\` for a tree source THIS step could not
149177
+ read at all \u2014 a broken window rather than a verdict about the app, which leaves the run not-ok and skips
149033
149178
  every later step; it says nothing about WHICH screen settled \u2014 a dropped tap leaves the source screen
149034
149179
  perfectly idle \u2014 so pair it with the element check that names the destination); \`wait\` pauses for a fixed number of milliseconds; \`assert\` checks one now; \`snapshot\`
149035
149180
  diffs a screenshot \u2014 or, with \`cropOn: <selector>\`, one element's cropped region \u2014 against a stored
@@ -149037,6 +149182,14 @@ baseline (a missing baseline fails the step \u2014 set updateBaselines to adopt
149037
149182
  cropped element whose size drifted fails on dimensions); \`echo\` annotates; \`run\` executes another flow
149038
149183
  inline \u2014 a YAML path resolved against the directory of the flow file that references it (co-located
149039
149184
  runs only).
149185
+ A selector-less gesture \u2014 a coordinate \`tap\`/\`long-press\`, or a \`pinch\`/\`rotate\` with no \`on\` \u2014 resolves
149186
+ no frame out of the tree, so an unreadable tree source does NOT stop it the way it stops \`idle\`: it
149187
+ settles best-effort, dispatches anyway, and the step PASSES carrying a \`warning\` that quotes the source's
149188
+ own error. That green says the gesture was SENT, not that it landed. Restore the tree source (usually
149189
+ relaunch the app so the instrumentation loads), or accept the warning where the app can serve no tree;
149190
+ the first such gesture proves the outage and later ones spend that verdict without paying the settle
149191
+ window again. A tree read that comes back, or a relaunch, retires that verdict \u2014 which only makes the
149192
+ next gesture pay a fresh window, and it warns again if the source is still down.
149040
149193
  A \`when:\` block (condition + \`steps:\`, no else) runs its steps only if the condition holds \u2014
149041
149194
  checked once with the short assert grace \u2014 for one-sided divergences like interstitials and coach
149042
149195
  marks; a skipped block reports distinctly and failures inside an entered block are real failures.
@@ -149118,6 +149271,12 @@ Pass exactly one flow source: name for a saved flow under project_root, or flow_
149118
149271
  device,
149119
149272
  deviceIsExplicit: Boolean(params.device),
149120
149273
  signal,
149274
+ // One holder per ExecState, shared by nested `run:` flows: `deviceEnv`
149275
+ // spreads the reference, so what one step's settle learns about the
149276
+ // tree source the next one already has. A `tool: flow-execute` builds
149277
+ // its own instead, which is why that step spends this verdict rather
149278
+ // than inheriting whatever the sub-run proved.
149279
+ treeOutage: {},
149121
149280
  flowsDir,
149122
149281
  viaUpload,
149123
149282
  baselineKey: baselineKeyFor(canonicalPath, flowName),
@@ -149711,6 +149870,10 @@ async function execLeafStep(state3, step, index, scope) {
149711
149870
  try {
149712
149871
  if (FOREGROUND_CHANGING_TOOLS.has(step.name)) {
149713
149872
  state3.launchedNativeApp = void 0;
149873
+ if (state3.treeOutage) state3.treeOutage.proven = void 0;
149874
+ }
149875
+ if (isNestedOrchestratorTool(step.name) && state3.treeOutage) {
149876
+ state3.treeOutage.proven = void 0;
149714
149877
  }
149715
149878
  const result = await invokeSubTool(registry2, ctx, step.name, args);
149716
149879
  if (isUnmetUiWaitResult(step.name, result)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/argent",
3
- "version": "0.20.1-next.7",
3
+ "version": "0.20.1-next.9",
4
4
  "mcpName": "io.github.software-mansion/argent",
5
5
  "description": "MCP server for iOS Simulator and Android Emulator control",
6
6
  "license": "Apache-2.0",
@@ -112,6 +112,8 @@ In a `scroll-to` map, put the selector under `target:`. The map supports `up`, `
112
112
 
113
113
  `type` presses Enter unless `submit: false`. A polished focus tap plus keyboard call usually needs `submit: false`. Store external values as `{{secret:NAME}}`. The runner uses the first source that defines the name: environment `ARGENT_SECRET_NAME`; project `.argent/secrets.env`; project `.env.local`, then `.env`; then `~/.argent/secrets.env`. The two `secrets.env` files accept the bare `NAME`, but the shared dotenv files expose only `ARGENT_SECRET_`-prefixed keys, so a bare `NAME=…` in `.env` or `.env.local` stays unresolved. The runner redacts every resolved value, so do not use a placeholder for content a report must show.
114
114
 
115
+ A **selector-less gesture** — a coordinate `tap`/`long-press`, or a `pinch`/`rotate` with no `on:` — resolves no frame, so a tree source it cannot read does not fail it. It settles best effort, dispatches anyway, and the step **passes carrying a warning** that quotes the source's own error. That green says the gesture was sent, not that it landed: one aimed at a moving element can miss it entirely. Restore the tree source, usually by relaunching the app so the instrumentation loads. Accept the warning only where the app serves no tree at all, and put an explicit `wait:` before a gesture that follows a transition. The first such gesture proves the outage and later ones spend that verdict without paying the settle window again. A tree read that comes back, or a relaunch, retires that verdict — which only makes the next gesture pay a fresh window, and it warns again if the source is still down.
116
+
115
117
  ## Verification conditions
116
118
 
117
119
  ```yaml
@@ -164,7 +166,7 @@ It **never fails a run.** Every outcome short of a clean settle passes with a wa
164
166
  - **settled on the UI tree alone** — no screenshot pair could be read, so presentation-layer animation was never waited out.
165
167
  - **too few reads** — a settle needs three reads across two intervals and this step got fewer, so it ended with no evidence either way.
166
168
 
167
- Only a tree source that cannot be read stops the run, as an errored step — one still failing when the wait ends, one that wedges after answering, one that answers with an empty tree it flags as degraded (an unattached Vega toolkit, an AX service asking to be relaunched), or one that never answers (raise `timeout` before suspecting the app). The run is then not ok and every later step is skipped. A single failed read is not that: the hold restarts from the next good read.
169
+ Only a tree source this step could not read stops the run, as an errored step — one still failing when the wait ends, one that wedges after answering, one that answers with an empty tree it flags as degraded (an unattached Vega toolkit, an AX service asking to be relaunched), or one that never answers (raise `timeout` before suspecting the app). The run is then not ok and every later step is skipped. A single failed read is not that: the hold restarts from the next good read. The same outage stops no [selector-less gesture](#directives), which needs no frame and passes with its own warning instead.
168
170
 
169
171
  `idle` proves readiness only and never identifies the screen, so it cannot serve as acceptance evidence or replace the identity gate. Gate the next action on a stable element. Add `idle` during polish after each screen change, not after every step.
170
172
 
@@ -239,4 +239,6 @@ Manual rescue invalidates the pass. An `errored` step was never evaluated: an `i
239
239
 
240
240
  **A passing step that carries a `warning` is a finding, not noise.** `await: { idle: true }` raises [six different warnings](flow-yaml.md#idle-readiness) and they do not share one meaning. Two say the screen was moving; one says the wait ran out mid-hold and is repaired by raising the step's `timeout:`; one says the tree stayed empty; one says the tree did hold still and only the screenshot pairs were missing, so the capture path is what to check; one says the step ended with no evidence either way. No report separates intended motion from a load that never finished. Read which one it is, look at that screen, disclose what you found, and confirm the following step targets a stable element rather than stillness.
241
241
 
242
+ A [selector-less gesture](flow-yaml.md#directives) raises a warning of a different shape, not one of those six: a tree-source outage left it unsettled, so it dispatched blind and the green says only that the gesture was sent. Restore the source, usually by relaunching the app so the instrumentation loads. Accept it only where the app serves no tree at all, such as the [injection-free iOS form](reliability-and-recovery.md#terminally-non-injectable-ios-apps).
243
+
242
244
  One uninterrupted full pass completes a normal flow. `argent-qa-flows` requires two consecutive passes of unchanged YAML. For CI, use `argent flow run <name> [--platform ...]`; it exits non-zero on failure.
@@ -62,6 +62,8 @@ Apple system apps cannot load the instrumentation, and nothing in the launch pat
62
62
  - A point focus tap plus raw keyboard with `delayMs: 500`.
63
63
  - Raw swipes with `settle: true` because `scroll-to` needs the missing flow tree. Momentum-free scrolling keeps later coordinate taps valid.
64
64
 
65
+ Every point tap or long-press in such a flow passes **carrying a warning**. The app loads no instrumentation, so every tree read fails and each [selector-less gesture](flow-yaml.md#directives) dispatches unsettled. Nothing here repairs it. Accept the warnings, read each green as "the gesture was sent, not that it landed", and put an explicit `wait:` or a raw `tool: await-ui-element` before a gesture that follows a transition. Raw `tool:` steps never take that settle, so they never warn.
66
+
65
67
  Report that the flow is injection-free and its coordinates are not portable. It cannot satisfy the QA contract. Report the artifact and platform blocker instead.
66
68
 
67
69
  The same fragment fallback covers a normally injectable app that is broken in the environment: raw `restart-app` in place of `launch:` still makes a self-resetting flow. Either way it is not e2e and cannot complete `argent-qa-flows`, which requires a leading `launch:`. Report the blocker rather than labeling that fallback a completed QA test.
@@ -111,6 +113,7 @@ Classify before editing:
111
113
  | Partial divergence | An intermediate result disagrees with its echo | Find the first divergent transition |
112
114
  | Acceptance failure | Actions pass but a requested check fails | Preserve the check and investigate behavior |
113
115
  | Idle warning | A readiness step passes without settling | Read [which of the six warnings](flow-yaml.md#idle-readiness) it is, then gate the next action on a stable element |
116
+ | Unsettled gesture | A selector-less gesture passes unsettled | Restore the tree source, usually by relaunching the app; the green says [only that the gesture was sent](flow-yaml.md#directives) |
114
117
 
115
118
  Then:
116
119
 
@@ -101,7 +101,7 @@ After the last edit and audit, set the streak to zero:
101
101
  2. Seed, review, and freeze snapshot baselines. Baseline updates do not count as passes.
102
102
  3. Before mobile pass 1, recycle Argent services for this flow's device: two warm passes are correlated evidence, because a fixed timing margin can pass twice simply because environment speed did not change. Scope `stop-all-simulator-servers` to `devices: [<device>]`. Never omit the scope — a bare call is the machine-wide sweep, and step 7 restarts this proof often enough to reap every other agent's devices repeatedly. Use the MCP call for `flow-execute`, or `argent run stop-all-simulator-servers --devices <device>` from the standalone runner's install. The reset must not change app or account data. For Chromium, let the runner boot the declared app and omit `device`. Vega owns no recyclable Argent services, so the teardown is a no-op there and both passes are warm.
103
103
  4. Run from the flow's launch and setup without baseline-update mode. Count a pass only when `ok: true` and every acceptance check executed. A false `when:` can skip optional setup only. An errored step does not advance the streak, and the count mixes two kinds — read each reason. One that could not run (an unreadable tree under `idle`, an unresolvable `run:` target) is environment: fix it and rerun. **A failed `launch:` also scores `errored`, and it is a verdict about the app** — an app that no longer installs or starts is the regression this test exists to catch, so report it instead of rerunning.
104
- 5. Resolve every passing-step warning before completion. `await: { idle: true }` raises [six different warnings](../argent-create-flow/references/flow-yaml.md#idle-readiness), so read which one it is first. Two say the screen was moving. One says the wait ran out mid-hold and needs a larger `timeout:`. One says the tree stayed empty. One — **settled on the UI tree alone** — says the hierarchy did hold still and only the screenshot pairs were missing, so inspect the capture path rather than the app's rendering. One says the step ended with no evidence either way. Inspect the screen, disclose the cause, and verify that surrounding acceptance checks use stable elements rather than stillness.
104
+ 5. Resolve every passing-step warning before completion. `await: { idle: true }` raises [six different warnings](../argent-create-flow/references/flow-yaml.md#idle-readiness), so read which one it is first. Two say the screen was moving. One says the wait ran out mid-hold and needs a larger `timeout:`. One says the tree stayed empty. One — **settled on the UI tree alone** — says the hierarchy did hold still and only the screenshot pairs were missing, so inspect the capture path rather than the app's rendering. One says the step ended with no evidence either way. Inspect the screen, disclose the cause, and verify that surrounding acceptance checks use stable elements rather than stillness. A [selector-less gesture](../argent-create-flow/references/flow-yaml.md#directives) — a coordinate `tap`/`long-press`, or a `pinch`/`rotate` with no `on:` — warns in a different shape: a tree-source outage left it unsettled, so it dispatched blind and the green says only that the gesture was sent. Restore the tree source, usually by relaunching the app so the instrumentation loads, and rerun. Accepting that warning needs an app that serves no tree, which cannot satisfy this contract anyway.
105
105
  6. Run the same YAML again immediately with the same runner. Do not manually reset app or account data.
106
106
  7. Reset the streak after any failure, edit, re-recording, baseline update, or state-changing manual recovery. Repair through `argent-create-flow`, audit again, and restart with fresh services.
107
107