zeno-mobile-runner 0.2.1 → 0.2.2

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/FEATURES.md +1 -1
  3. package/README.md +1 -1
  4. package/build.zig.zon +2 -2
  5. package/clients/kotlin/README.md +1 -1
  6. package/clients/kotlin/build.gradle.kts +1 -1
  7. package/clients/python/pyproject.toml +1 -1
  8. package/clients/rust/Cargo.lock +1 -1
  9. package/clients/rust/Cargo.toml +1 -1
  10. package/clients/typescript/package.json +1 -1
  11. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  12. package/docs/protocol.md +10 -10
  13. package/package.json +3 -1
  14. package/prebuilds/darwin-arm64/zmr +0 -0
  15. package/prebuilds/darwin-x64/zmr +0 -0
  16. package/prebuilds/linux-arm64/zmr +0 -0
  17. package/prebuilds/linux-x64/zmr +0 -0
  18. package/scripts/create-react-native-expo-demo-app.sh +11 -13
  19. package/shims/ios/ZMRShim.swift +40 -12
  20. package/shims/ios/ZMRShimUITestCase.swift +135 -15
  21. package/src/android.zig +10 -9
  22. package/src/android_emulator.zig +22 -11
  23. package/src/android_screen_recording.zig +11 -7
  24. package/src/bundle.zig +10 -9
  25. package/src/bundle_redaction.zig +29 -28
  26. package/src/bundle_tar.zig +15 -12
  27. package/src/cli_devices.zig +7 -3
  28. package/src/cli_discover.zig +7 -3
  29. package/src/cli_doctor.zig +7 -3
  30. package/src/cli_draft.zig +51 -47
  31. package/src/cli_explore.zig +7 -3
  32. package/src/cli_import.zig +8 -4
  33. package/src/cli_info.zig +13 -6
  34. package/src/cli_init.zig +9 -5
  35. package/src/cli_inspect.zig +8 -4
  36. package/src/cli_run.zig +22 -16
  37. package/src/cli_serve.zig +3 -3
  38. package/src/cli_trace.zig +25 -12
  39. package/src/cli_validate.zig +8 -4
  40. package/src/command.zig +81 -99
  41. package/src/config.zig +2 -1
  42. package/src/config_diagnostics.zig +2 -1
  43. package/src/config_paths.zig +2 -1
  44. package/src/doctor.zig +8 -7
  45. package/src/doctor_hints.zig +1 -1
  46. package/src/errors.zig +5 -5
  47. package/src/importer.zig +8 -7
  48. package/src/ios.zig +19 -18
  49. package/src/ios_devices.zig +6 -5
  50. package/src/ios_lifecycle.zig +4 -4
  51. package/src/json_rpc.zig +39 -40
  52. package/src/json_rpc_methods.zig +8 -8
  53. package/src/json_rpc_observation.zig +9 -8
  54. package/src/json_rpc_params.zig +1 -1
  55. package/src/json_rpc_trace.zig +22 -21
  56. package/src/main.zig +19 -10
  57. package/src/mcp.zig +28 -19
  58. package/src/mcp_trace.zig +30 -29
  59. package/src/report.zig +39 -36
  60. package/src/report_html.zig +5 -4
  61. package/src/runner.zig +2 -1
  62. package/src/runner_actions.zig +20 -17
  63. package/src/runner_diagnostics.zig +4 -4
  64. package/src/runner_events.zig +55 -51
  65. package/src/runner_native.zig +21 -19
  66. package/src/runner_waits.zig +46 -41
  67. package/src/scaffold.zig +25 -24
  68. package/src/scenario.zig +4 -3
  69. package/src/stdio.zig +129 -0
  70. package/src/trace.zig +34 -26
  71. package/src/trace_summary.zig +3 -2
  72. package/src/trace_summary_diagnostic.zig +15 -13
  73. package/src/validation.zig +5 -4
  74. package/src/version.zig +1 -1
package/src/stdio.zig ADDED
@@ -0,0 +1,129 @@
1
+ const std = @import("std");
2
+
3
+ const default_buffer_size = 8192;
4
+ var process_environ: ?std.process.Environ = null;
5
+ var process_threaded: ?std.Io.Threaded = null;
6
+
7
+ fn processIo() std.Io {
8
+ if (process_threaded) |*threaded| return threaded.io();
9
+ return std.Io.Threaded.global_single_threaded.io();
10
+ }
11
+
12
+ pub fn initProcess(init: std.process.Init.Minimal, allocator: std.mem.Allocator) void {
13
+ process_environ = init.environ;
14
+ process_threaded = .init(allocator, .{
15
+ .argv0 = .init(init.args),
16
+ .environ = init.environ,
17
+ });
18
+ }
19
+
20
+ pub fn deinitProcess() void {
21
+ if (process_threaded) |*threaded| {
22
+ threaded.deinit();
23
+ process_threaded = null;
24
+ }
25
+ process_environ = null;
26
+ }
27
+
28
+ pub fn io() std.Io {
29
+ return processIo();
30
+ }
31
+
32
+ pub fn sleepNs(nanoseconds: u64) void {
33
+ std.Io.sleep(
34
+ processIo(),
35
+ std.Io.Duration.fromNanoseconds(@intCast(nanoseconds)),
36
+ .awake,
37
+ ) catch {};
38
+ }
39
+
40
+ pub fn nowNs() i96 {
41
+ return std.Io.Clock.real.now(processIo()).nanoseconds;
42
+ }
43
+
44
+ pub fn nowMs() i64 {
45
+ return @intCast(@divTrunc(nowNs(), std.time.ns_per_ms));
46
+ }
47
+
48
+ pub fn getenv(name: []const u8) ?[]const u8 {
49
+ const environ = process_environ orelse return null;
50
+ const block = environ.block;
51
+ const Block = @TypeOf(block);
52
+ if (Block != std.process.Environ.PosixBlock) return null;
53
+
54
+ for (block.view().slice) |entry_ptr| {
55
+ const entry = std.mem.span(entry_ptr);
56
+ if (entry.len <= name.len or entry[name.len] != '=') continue;
57
+ if (std.mem.eql(u8, entry[0..name.len], name)) return entry[name.len + 1 ..];
58
+ }
59
+ return null;
60
+ }
61
+
62
+ pub fn access(path: []const u8) !void {
63
+ return accessWithOptions(path, .{});
64
+ }
65
+
66
+ pub fn accessWithOptions(path: []const u8, options: std.Io.Dir.AccessOptions) !void {
67
+ return std.Io.Dir.cwd().access(processIo(), path, options);
68
+ }
69
+
70
+ pub fn readFileAlloc(allocator: std.mem.Allocator, path: []const u8, limit: usize) ![]u8 {
71
+ return std.Io.Dir.cwd().readFileAlloc(processIo(), path, allocator, .limited(limit));
72
+ }
73
+
74
+ pub const Output = struct {
75
+ buffer: [default_buffer_size]u8 = undefined,
76
+ file_writer: std.Io.File.Writer = undefined,
77
+ initialized: bool = false,
78
+
79
+ pub fn init(self: *Output, file: std.Io.File) void {
80
+ self.file_writer = file.writerStreaming(processIo(), &self.buffer);
81
+ self.initialized = true;
82
+ }
83
+
84
+ pub fn writer(self: *Output) *std.Io.Writer {
85
+ return &self.file_writer.interface;
86
+ }
87
+
88
+ pub fn flush(self: *Output) !void {
89
+ if (self.initialized) try self.file_writer.interface.flush();
90
+ }
91
+
92
+ pub fn deinit(self: *Output) void {
93
+ self.flush() catch {};
94
+ self.initialized = false;
95
+ }
96
+ };
97
+
98
+ pub const Input = struct {
99
+ buffer: [default_buffer_size]u8 = undefined,
100
+ file_reader: std.Io.File.Reader = undefined,
101
+
102
+ pub fn init(self: *Input, file: std.Io.File) void {
103
+ self.file_reader = file.readerStreaming(processIo(), &self.buffer);
104
+ }
105
+
106
+ pub fn reader(self: *Input) *std.Io.Reader {
107
+ return &self.file_reader.interface;
108
+ }
109
+ };
110
+
111
+ pub fn readLineAlloc(reader: *std.Io.Reader, allocator: std.mem.Allocator, max_bytes: usize) !?[]u8 {
112
+ var out: std.Io.Writer.Allocating = .init(allocator);
113
+ errdefer out.deinit();
114
+
115
+ _ = try reader.streamDelimiterLimit(&out.writer, '\n', .limited(max_bytes));
116
+
117
+ const next = reader.peek(1) catch |err| switch (err) {
118
+ error.EndOfStream => {
119
+ if (out.writer.end == 0) {
120
+ out.deinit();
121
+ return null;
122
+ }
123
+ return try out.toOwnedSlice();
124
+ },
125
+ else => |actual| return actual,
126
+ };
127
+ if (next.len > 0 and next[0] == '\n') reader.toss(1);
128
+ return try out.toOwnedSlice();
129
+ }
package/src/trace.zig CHANGED
@@ -1,4 +1,5 @@
1
1
  const std = @import("std");
2
+ const stdio = @import("stdio.zig");
2
3
  const selector = @import("selector.zig");
3
4
  const trace_json = @import("trace_json.zig");
4
5
  const types = @import("types.zig");
@@ -28,11 +29,11 @@ pub const TraceWriter = struct {
28
29
  }
29
30
 
30
31
  pub fn initWithOptions(allocator: std.mem.Allocator, root_dir: []const u8, capture: CaptureOptions) !TraceWriter {
31
- try std.fs.cwd().makePath(root_dir);
32
+ try std.Io.Dir.cwd().createDirPath(stdio.io(), root_dir);
32
33
  try resetTraceDirectory(allocator, root_dir);
33
34
  const artifacts_path = try std.fs.path.join(allocator, &.{ root_dir, "artifacts" });
34
35
  defer allocator.free(artifacts_path);
35
- try std.fs.cwd().makePath(artifacts_path);
36
+ try std.Io.Dir.cwd().createDirPath(stdio.io(), artifacts_path);
36
37
  return .{
37
38
  .allocator = allocator,
38
39
  .root_dir = try allocator.dupe(u8, root_dir),
@@ -51,7 +52,7 @@ pub const TraceWriter = struct {
51
52
  .scenario_name = try self.allocator.dupe(u8, scenario_name),
52
53
  .app_id = try dupeOptional(self.allocator, app_id),
53
54
  .status = try self.allocator.dupe(u8, "running"),
54
- .started_at_ms = std.time.milliTimestamp(),
55
+ .started_at_ms = stdio.nowMs(),
55
56
  };
56
57
  try self.writeManifest();
57
58
  }
@@ -68,7 +69,7 @@ pub const TraceWriter = struct {
68
69
  var manifest = &self.manifest.?;
69
70
  self.allocator.free(manifest.status);
70
71
  manifest.status = try self.allocator.dupe(u8, options.status);
71
- manifest.ended_at_ms = std.time.milliTimestamp();
72
+ manifest.ended_at_ms = stdio.nowMs();
72
73
  manifest.failed_step_index = options.failed_step_index;
73
74
  if (manifest.error_name) |value| self.allocator.free(value);
74
75
  manifest.error_name = try dupeOptional(self.allocator, options.error_name);
@@ -99,9 +100,9 @@ pub const TraceWriter = struct {
99
100
  pub fn writeArtifact(self: *TraceWriter, name: []const u8, bytes: []const u8) ![]const u8 {
100
101
  const path = try self.artifactPath(name);
101
102
  errdefer self.allocator.free(path);
102
- var file = try std.fs.cwd().createFile(path, .{ .truncate = true });
103
- defer file.close();
104
- try file.writeAll(bytes);
103
+ var file = try std.Io.Dir.cwd().createFile(stdio.io(), path, .{ .truncate = true });
104
+ defer file.close(stdio.io());
105
+ try std.Io.File.writeStreamingAll(file, stdio.io(), bytes);
105
106
  return path;
106
107
  }
107
108
 
@@ -110,15 +111,22 @@ pub const TraceWriter = struct {
110
111
  if (isPartialFailureEvent(kind, payload)) self.partial_failure_count += 1;
111
112
  const path = try std.fs.path.join(self.allocator, &.{ self.root_dir, "events.jsonl" });
112
113
  defer self.allocator.free(path);
113
- var file = try std.fs.cwd().createFile(path, .{ .truncate = false });
114
- defer file.close();
115
- try file.seekFromEnd(0);
114
+ const existing = stdio.readFileAlloc(self.allocator, path, 64 * 1024 * 1024) catch |err| switch (err) {
115
+ error.FileNotFound => "",
116
+ else => return err,
117
+ };
118
+ const had_existing_file = existing.ptr != "".ptr;
119
+ defer if (had_existing_file) self.allocator.free(existing);
120
+
121
+ var file = try std.Io.Dir.cwd().createFile(stdio.io(), path, .{ .truncate = true });
122
+ defer file.close(stdio.io());
116
123
  var write_buffer: [4096]u8 = undefined;
117
- var file_writer = file.writerStreaming(&write_buffer);
124
+ var file_writer = file.writerStreaming(stdio.io(), &write_buffer);
118
125
  const writer = &file_writer.interface;
126
+ if (existing.len > 0) try writer.writeAll(existing);
119
127
  try writer.print(
120
128
  "{{\"seq\":{d},\"timestampMs\":{d},\"kind\":\"{s}\",\"payload\":",
121
- .{ self.event_count, std.time.milliTimestamp(), kind },
129
+ .{ self.event_count, stdio.nowMs(), kind },
122
130
  );
123
131
  try trace_json.writeRedactedJsonPayload(self.allocator, writer, payload, self.capture.redaction);
124
132
  try writer.writeAll("}\n");
@@ -131,10 +139,10 @@ pub const TraceWriter = struct {
131
139
  defer self.allocator.free(file_name);
132
140
  const path = try self.artifactPath(file_name);
133
141
  errdefer self.allocator.free(path);
134
- var file = try std.fs.cwd().createFile(path, .{ .truncate = true });
135
- defer file.close();
142
+ var file = try std.Io.Dir.cwd().createFile(stdio.io(), path, .{ .truncate = true });
143
+ defer file.close(stdio.io());
136
144
  var write_buffer: [8192]u8 = undefined;
137
- var file_writer = file.writer(&write_buffer);
145
+ var file_writer = file.writerStreaming(stdio.io(), &write_buffer);
138
146
  try trace_json.writeSnapshotJsonRedacted(&file_writer.interface, snapshot, self.capture.redaction);
139
147
  try file_writer.interface.flush();
140
148
  return path;
@@ -149,10 +157,10 @@ pub const TraceWriter = struct {
149
157
  const manifest = self.manifest.?;
150
158
  const path = try std.fs.path.join(self.allocator, &.{ self.root_dir, "trace.json" });
151
159
  defer self.allocator.free(path);
152
- var file = try std.fs.cwd().createFile(path, .{ .truncate = true });
153
- defer file.close();
160
+ var file = try std.Io.Dir.cwd().createFile(stdio.io(), path, .{ .truncate = true });
161
+ defer file.close(stdio.io());
154
162
  var write_buffer: [4096]u8 = undefined;
155
- var file_writer = file.writer(&write_buffer);
163
+ var file_writer = file.writerStreaming(stdio.io(), &write_buffer);
156
164
  const writer = &file_writer.interface;
157
165
 
158
166
  try writer.writeAll("{");
@@ -223,7 +231,7 @@ fn resetTraceDirectory(allocator: std.mem.Allocator, root_dir: []const u8) !void
223
231
  for (stale_files) |name| {
224
232
  const path = try std.fs.path.join(allocator, &.{ root_dir, name });
225
233
  defer allocator.free(path);
226
- std.fs.cwd().deleteFile(path) catch |err| switch (err) {
234
+ std.Io.Dir.cwd().deleteFile(stdio.io(), path) catch |err| switch (err) {
227
235
  error.FileNotFound => {},
228
236
  else => return err,
229
237
  };
@@ -232,11 +240,11 @@ fn resetTraceDirectory(allocator: std.mem.Allocator, root_dir: []const u8) !void
232
240
  const artifacts_path = try std.fs.path.join(allocator, &.{ root_dir, "artifacts" });
233
241
  defer allocator.free(artifacts_path);
234
242
  var artifacts_exists = true;
235
- std.fs.cwd().access(artifacts_path, .{}) catch |err| switch (err) {
243
+ stdio.access(artifacts_path) catch |err| switch (err) {
236
244
  error.FileNotFound => artifacts_exists = false,
237
245
  else => return err,
238
246
  };
239
- if (artifacts_exists) try std.fs.cwd().deleteTree(artifacts_path);
247
+ if (artifacts_exists) try std.Io.Dir.cwd().deleteTree(stdio.io(), artifacts_path);
240
248
  }
241
249
 
242
250
  const Manifest = struct {
@@ -271,7 +279,7 @@ pub fn attachReportPath(allocator: std.mem.Allocator, root_dir: []const u8, repo
271
279
  const manifest_path = try std.fs.path.join(allocator, &.{ root_dir, "trace.json" });
272
280
  defer allocator.free(manifest_path);
273
281
 
274
- const content = std.fs.cwd().readFileAlloc(allocator, manifest_path, 1024 * 1024) catch |err| switch (err) {
282
+ const content = stdio.readFileAlloc(allocator, manifest_path, 1024 * 1024) catch |err| switch (err) {
275
283
  error.FileNotFound => return,
276
284
  else => return err,
277
285
  };
@@ -283,12 +291,12 @@ pub fn attachReportPath(allocator: std.mem.Allocator, root_dir: []const u8, repo
283
291
 
284
292
  const arena_allocator = parsed.arena.allocator();
285
293
  const owned_report_path = try arena_allocator.dupe(u8, report_path);
286
- try parsed.value.object.put("reportPath", .{ .string = owned_report_path });
294
+ try parsed.value.object.put(arena_allocator, "reportPath", .{ .string = owned_report_path });
287
295
 
288
- var file = try std.fs.cwd().createFile(manifest_path, .{ .truncate = true });
289
- defer file.close();
296
+ var file = try std.Io.Dir.cwd().createFile(stdio.io(), manifest_path, .{ .truncate = true });
297
+ defer file.close(stdio.io());
290
298
  var write_buffer: [4096]u8 = undefined;
291
- var file_writer = file.writer(&write_buffer);
299
+ var file_writer = file.writerStreaming(stdio.io(), &write_buffer);
292
300
  try std.json.Stringify.value(parsed.value, .{}, &file_writer.interface);
293
301
  try file_writer.interface.writeByte('\n');
294
302
  try file_writer.interface.flush();
@@ -1,4 +1,5 @@
1
1
  const std = @import("std");
2
+ const stdio = @import("stdio.zig");
2
3
  const trace = @import("trace.zig");
3
4
  const trace_summary_diagnostic = @import("trace_summary_diagnostic.zig");
4
5
 
@@ -59,7 +60,7 @@ const TerminalEvent = struct {
59
60
  pub fn read(allocator: std.mem.Allocator, trace_dir: []const u8) !Summary {
60
61
  const manifest_path = try std.fs.path.join(allocator, &.{ trace_dir, "trace.json" });
61
62
  defer allocator.free(manifest_path);
62
- const manifest_content = try std.fs.cwd().readFileAlloc(allocator, manifest_path, 1024 * 1024);
63
+ const manifest_content = try stdio.readFileAlloc(allocator, manifest_path, 1024 * 1024);
63
64
  defer allocator.free(manifest_content);
64
65
 
65
66
  const manifest = try std.json.parseFromSlice(std.json.Value, allocator, manifest_content, .{});
@@ -91,7 +92,7 @@ pub fn read(allocator: std.mem.Allocator, trace_dir: []const u8) !Summary {
91
92
 
92
93
  const events_path = try std.fs.path.join(allocator, &.{ trace_dir, events_path_value });
93
94
  defer allocator.free(events_path);
94
- if (std.fs.cwd().readFileAlloc(allocator, events_path, 64 * 1024 * 1024)) |events_content| {
95
+ if (stdio.readFileAlloc(allocator, events_path, 64 * 1024 * 1024)) |events_content| {
95
96
  defer allocator.free(events_content);
96
97
  try scanEvents(allocator, events_content, &terminal, &diagnostic, &partial_failure, &last_kind);
97
98
  } else |err| switch (err) {
@@ -142,44 +142,46 @@ fn writeJoinedStringArrayJson(writer: anytype, value: []const u8) !void {
142
142
 
143
143
  fn joinStringArray(allocator: std.mem.Allocator, value: std.json.Value, limit: usize) !?[]u8 {
144
144
  if (value != .array or value.array.items.len == 0) return null;
145
- var out = std.ArrayList(u8).empty;
146
- errdefer out.deinit(allocator);
145
+ var out: std.Io.Writer.Allocating = .init(allocator);
146
+ errdefer out.deinit();
147
+ const writer = &out.writer;
147
148
  var written: usize = 0;
148
149
  for (value.array.items) |item| {
149
150
  if (item != .string) continue;
150
- if (written > 0) try out.writer(allocator).writeAll(" | ");
151
- try out.writer(allocator).writeAll(item.string);
151
+ if (written > 0) try writer.writeAll(" | ");
152
+ try writer.writeAll(item.string);
152
153
  written += 1;
153
154
  if (written >= limit) break;
154
155
  }
155
156
  if (written == 0) {
156
- out.deinit(allocator);
157
+ out.deinit();
157
158
  return null;
158
159
  }
159
- return try out.toOwnedSlice(allocator);
160
+ return try out.toOwnedSlice();
160
161
  }
161
162
 
162
163
  fn joinNearestMatches(allocator: std.mem.Allocator, value: std.json.Value, limit: usize) !?[]u8 {
163
164
  if (value != .array or value.array.items.len == 0) return null;
164
- var out = std.ArrayList(u8).empty;
165
- errdefer out.deinit(allocator);
165
+ var out: std.Io.Writer.Allocating = .init(allocator);
166
+ errdefer out.deinit();
167
+ const writer = &out.writer;
166
168
  var written: usize = 0;
167
169
  for (value.array.items) |item| {
168
170
  if (item != .object) continue;
169
171
  const text = stringField(item.object, "text") orelse continue;
170
- if (written > 0) try out.writer(allocator).writeAll(" | ");
171
- try out.writer(allocator).writeAll(text);
172
+ if (written > 0) try writer.writeAll(" | ");
173
+ try writer.writeAll(text);
172
174
  if (intField(item.object, "score")) |score| {
173
- try out.writer(allocator).print(" (score {d})", .{score});
175
+ try writer.print(" (score {d})", .{score});
174
176
  }
175
177
  written += 1;
176
178
  if (written >= limit) break;
177
179
  }
178
180
  if (written == 0) {
179
- out.deinit(allocator);
181
+ out.deinit();
180
182
  return null;
181
183
  }
182
- return try out.toOwnedSlice(allocator);
184
+ return try out.toOwnedSlice();
183
185
  }
184
186
 
185
187
  fn dupeOptionalString(allocator: std.mem.Allocator, value: ?[]const u8) !?[]u8 {
@@ -1,4 +1,5 @@
1
1
  const std = @import("std");
2
+ const stdio = @import("stdio.zig");
2
3
  const errors = @import("errors.zig");
3
4
  const scenario = @import("scenario.zig");
4
5
 
@@ -23,7 +24,7 @@ pub const Result = struct {
23
24
  };
24
25
 
25
26
  pub fn validateFile(allocator: std.mem.Allocator, path: []const u8) !Result {
26
- const content = std.fs.cwd().readFileAlloc(allocator, path, 16 * 1024 * 1024) catch |err| return failure(allocator, null, err);
27
+ const content = stdio.readFileAlloc(allocator, path, 16 * 1024 * 1024) catch |err| return failure(allocator, null, err);
27
28
  defer allocator.free(content);
28
29
  const script = scenario.parseSlice(allocator, content) catch |err| return failure(allocator, content, err);
29
30
  defer script.deinit(allocator);
@@ -91,10 +92,10 @@ fn diagnoseFailure(allocator: std.mem.Allocator, content: []const u8, err: anyer
91
92
  => try pathDiagnostic(allocator, content, "$.steps", "steps"),
92
93
  error.StepMissingAction,
93
94
  error.StepActionMustBeString,
94
- error.UnknownAction,
95
- error.UnknownScenarioAction,
95
+ error.unknownAction,
96
+ error.unknownScenarioAction,
96
97
  => try pathDiagnostic(allocator, content, "$.steps[].action", "action"),
97
- error.UnknownScrollDirection,
98
+ error.unknownScrollDirection,
98
99
  => try pathDiagnostic(allocator, content, "$.steps[].direction", "direction"),
99
100
  error.StepMissingUrl,
100
101
  => try pathDiagnostic(allocator, content, "$.steps[].url", "url"),
package/src/version.zig CHANGED
@@ -1,4 +1,4 @@
1
- pub const runner_version = "0.2.1";
1
+ pub const runner_version = "0.2.2";
2
2
  pub const protocol_version = "2026-04-28";
3
3
  pub const protocol_min_compatible_version = "2026-04-28";
4
4
  pub const protocol_stability = "dev-preview";