agent-scenario-loop 0.1.0

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/app/profile-session.ts +812 -0
  4. package/core/config-template.json +41 -0
  5. package/dist/core/agent-summary.d.ts +15 -0
  6. package/dist/core/agent-summary.js +177 -0
  7. package/dist/core/artifact-contract.d.ts +151 -0
  8. package/dist/core/artifact-contract.js +897 -0
  9. package/dist/core/artifact-layout.d.ts +56 -0
  10. package/dist/core/artifact-layout.js +61 -0
  11. package/dist/core/artifact-writer.d.ts +44 -0
  12. package/dist/core/artifact-writer.js +55 -0
  13. package/dist/core/comparison.d.ts +133 -0
  14. package/dist/core/comparison.js +294 -0
  15. package/dist/core/evidence-interpreter.d.ts +28 -0
  16. package/dist/core/evidence-interpreter.js +69 -0
  17. package/dist/core/execution-plan.d.ts +44 -0
  18. package/dist/core/execution-plan.js +95 -0
  19. package/dist/core/planner.d.ts +132 -0
  20. package/dist/core/planner.js +812 -0
  21. package/dist/core/ports.d.ts +198 -0
  22. package/dist/core/ports.js +146 -0
  23. package/dist/core/run-index.d.ts +62 -0
  24. package/dist/core/run-index.js +143 -0
  25. package/dist/core/schema-validator.d.ts +86 -0
  26. package/dist/core/schema-validator.js +407 -0
  27. package/dist/index.d.ts +11 -0
  28. package/dist/index.js +27 -0
  29. package/dist/runner/agent-device-driver.d.ts +126 -0
  30. package/dist/runner/agent-device-driver.js +168 -0
  31. package/dist/runner/agent-device.d.ts +295 -0
  32. package/dist/runner/agent-device.js +1271 -0
  33. package/dist/runner/android-adb-driver.d.ts +175 -0
  34. package/dist/runner/android-adb-driver.js +399 -0
  35. package/dist/runner/android-adb.d.ts +254 -0
  36. package/dist/runner/android-adb.js +1618 -0
  37. package/dist/runner/argent-driver.d.ts +183 -0
  38. package/dist/runner/argent-driver.js +297 -0
  39. package/dist/runner/argent.d.ts +349 -0
  40. package/dist/runner/argent.js +1211 -0
  41. package/dist/runner/check-plan.d.ts +45 -0
  42. package/dist/runner/check-plan.js +210 -0
  43. package/dist/runner/cli.d.ts +20 -0
  44. package/dist/runner/cli.js +23 -0
  45. package/dist/runner/compare-latest.d.ts +99 -0
  46. package/dist/runner/compare-latest.js +233 -0
  47. package/dist/runner/compare.d.ts +58 -0
  48. package/dist/runner/compare.js +157 -0
  49. package/dist/runner/demo-loop.d.ts +45 -0
  50. package/dist/runner/demo-loop.js +170 -0
  51. package/dist/runner/example-android-live.d.ts +137 -0
  52. package/dist/runner/example-android-live.js +454 -0
  53. package/dist/runner/example-ios-live.d.ts +137 -0
  54. package/dist/runner/example-ios-live.js +471 -0
  55. package/dist/runner/host-doctor.d.ts +131 -0
  56. package/dist/runner/host-doctor.js +628 -0
  57. package/dist/runner/init-project.d.ts +88 -0
  58. package/dist/runner/init-project.js +263 -0
  59. package/dist/runner/ios-simctl-driver.d.ts +69 -0
  60. package/dist/runner/ios-simctl-driver.js +97 -0
  61. package/dist/runner/ios-simctl.d.ts +254 -0
  62. package/dist/runner/ios-simctl.js +1415 -0
  63. package/dist/runner/live-android.d.ts +137 -0
  64. package/dist/runner/live-android.js +539 -0
  65. package/dist/runner/live-comparison.d.ts +67 -0
  66. package/dist/runner/live-comparison.js +147 -0
  67. package/dist/runner/live-ios.d.ts +137 -0
  68. package/dist/runner/live-ios.js +460 -0
  69. package/dist/runner/live-proof-summary.d.ts +263 -0
  70. package/dist/runner/live-proof-summary.js +465 -0
  71. package/dist/runner/live-proof.d.ts +467 -0
  72. package/dist/runner/live-proof.js +920 -0
  73. package/dist/runner/local-env.d.ts +64 -0
  74. package/dist/runner/local-env.js +155 -0
  75. package/dist/runner/profile-android.d.ts +82 -0
  76. package/dist/runner/profile-android.js +671 -0
  77. package/dist/runner/profile-ios.d.ts +108 -0
  78. package/dist/runner/profile-ios.js +532 -0
  79. package/dist/runner/profile-mobile.d.ts +254 -0
  80. package/dist/runner/profile-mobile.js +1307 -0
  81. package/dist/runner/validate-project.d.ts +273 -0
  82. package/dist/runner/validate-project.js +1501 -0
  83. package/docs/adapters.md +145 -0
  84. package/docs/api.md +94 -0
  85. package/docs/authoring.md +196 -0
  86. package/docs/concepts.md +136 -0
  87. package/docs/consumer-rehearsal.md +115 -0
  88. package/docs/contracts.md +267 -0
  89. package/docs/live-proofs.md +270 -0
  90. package/docs/principles.md +46 -0
  91. package/examples/event-logs/app-startup-baseline.log +4 -0
  92. package/examples/event-logs/app-startup-current.log +4 -0
  93. package/examples/minimal-app/README.md +70 -0
  94. package/examples/mobile-app/README.md +302 -0
  95. package/examples/mobile-app/app.json +22 -0
  96. package/examples/mobile-app/asl/package-scripts.json +32 -0
  97. package/examples/mobile-app/asl.config.json +37 -0
  98. package/examples/mobile-app/event-logs/android-app-startup.log +4 -0
  99. package/examples/mobile-app/event-logs/android-open-close-cycle.log +12 -0
  100. package/examples/mobile-app/event-logs/android-scroll-settle.log +12 -0
  101. package/examples/mobile-app/event-logs/app-startup.log +4 -0
  102. package/examples/mobile-app/event-logs/open-close-cycle.log +12 -0
  103. package/examples/mobile-app/event-logs/scroll-settle.log +12 -0
  104. package/examples/mobile-app/index.ts +20 -0
  105. package/examples/mobile-app/metro.config.js +20 -0
  106. package/examples/mobile-app/package.json +62 -0
  107. package/examples/mobile-app/patches/expo-modules-jsi@56.0.10.patch +19 -0
  108. package/examples/mobile-app/plugins/with-ios-build-compat.js +271 -0
  109. package/examples/mobile-app/pnpm-lock.yaml +4440 -0
  110. package/examples/mobile-app/runner-manifests/evidence-provider.json +79 -0
  111. package/examples/mobile-app/runner-manifests/primary-runner.json +19 -0
  112. package/examples/mobile-app/scenarios/android/app-startup-video.json +73 -0
  113. package/examples/mobile-app/scenarios/android/app-startup.json +44 -0
  114. package/examples/mobile-app/scenarios/android/open-close-cycle.json +54 -0
  115. package/examples/mobile-app/scenarios/android/scroll-settle.json +49 -0
  116. package/examples/mobile-app/scenarios/ios/app-startup.json +44 -0
  117. package/examples/mobile-app/scenarios/ios/open-close-cycle.json +54 -0
  118. package/examples/mobile-app/scenarios/ios/scroll-settle.json +49 -0
  119. package/examples/mobile-app/scenarios/mobile/app-startup.json +91 -0
  120. package/examples/mobile-app/scenarios/mobile/open-close-cycle.json +160 -0
  121. package/examples/mobile-app/scenarios/mobile/scroll-settle.json +148 -0
  122. package/examples/mobile-app/scripts/asl-capture-accessibility-provider.mjs +112 -0
  123. package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +127 -0
  124. package/examples/mobile-app/src/devtools/profile-session.ts +7 -0
  125. package/examples/mobile-app/src/example-screen.tsx +322 -0
  126. package/examples/mobile-app/tsconfig.json +16 -0
  127. package/examples/mobile-app/tsconfig.typecheck.json +13 -0
  128. package/examples/runners/README.md +44 -0
  129. package/examples/runners/adb-android.json +25 -0
  130. package/examples/runners/agent-device-android.json +27 -0
  131. package/examples/runners/agent-device-ios.json +27 -0
  132. package/examples/runners/argent-android.json +32 -0
  133. package/examples/runners/argent-ios.json +32 -0
  134. package/examples/runners/argent-react-profiler-provider.json +15 -0
  135. package/examples/runners/axe-accessibility-provider.json +24 -0
  136. package/examples/runners/manual-log-ingest.json +9 -0
  137. package/examples/runners/rozenite-profiler-provider.json +9 -0
  138. package/examples/runners/script-accessibility-provider.json +24 -0
  139. package/examples/runners/script-memory-provider.json +24 -0
  140. package/examples/runners/script-network-provider.json +24 -0
  141. package/examples/runners/script-profiler-provider.json +30 -0
  142. package/examples/runners/xcodebuildmcp-ios.json +29 -0
  143. package/examples/scenarios/ios/app-startup.json +28 -0
  144. package/examples/scenarios/ios/open-close-cycle.json +35 -0
  145. package/examples/scenarios/mobile/app-startup.json +72 -0
  146. package/examples/scenarios/mobile/media-open-close.json +141 -0
  147. package/examples/scenarios/mobile/open-close-cycle.json +135 -0
  148. package/examples/scenarios/mobile/scroll-settle.json +106 -0
  149. package/package.json +240 -0
  150. package/schemas/budget-verdict.schema.json +115 -0
  151. package/schemas/causal-run.schema.json +279 -0
  152. package/schemas/comparison.schema.json +196 -0
  153. package/schemas/health.schema.json +108 -0
  154. package/schemas/live-proof-set.schema.json +195 -0
  155. package/schemas/live-proof.schema.json +413 -0
  156. package/schemas/manifest.schema.json +204 -0
  157. package/schemas/metrics.schema.json +137 -0
  158. package/schemas/project-validation.schema.json +343 -0
  159. package/schemas/runner-capabilities.schema.json +217 -0
  160. package/schemas/scenario.schema.json +400 -0
  161. package/schemas/verdict.schema.json +88 -0
  162. package/templates/evidence-provider.json +83 -0
  163. package/templates/gitignore-snippet +9 -0
  164. package/templates/integration-readme.md +125 -0
  165. package/templates/mobile-scenario.json +133 -0
  166. package/templates/package-scripts.json +32 -0
  167. package/templates/primary-runner.json +19 -0
  168. package/templates/project.config.json +37 -0
  169. package/templates/scripts/asl-capture-accessibility-provider.mjs +112 -0
  170. package/templates/scripts/asl-capture-profiler-provider.mjs +127 -0
@@ -0,0 +1,1211 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.argentDriverActionCode = argentDriverActionCode;
5
+ exports.buildArgentHealth = buildArgentHealth;
6
+ exports.buildArgentAvailabilityCheck = buildArgentAvailabilityCheck;
7
+ exports.buildArgentSelectorHealthMetadata = buildArgentSelectorHealthMetadata;
8
+ exports.buildArgentVerdict = buildArgentVerdict;
9
+ exports.checkArgentAvailability = checkArgentAvailability;
10
+ exports.copyArgentCapture = copyArgentCapture;
11
+ exports.defaultArgentRawFileName = defaultArgentRawFileName;
12
+ exports.defaultIosSimctlFallbackScreenshotFileName = defaultIosSimctlFallbackScreenshotFileName;
13
+ exports.deriveArgentRootArgs = deriveArgentRootArgs;
14
+ exports.execFileCommand = execFileCommand;
15
+ exports.execFileCommandWithTimeout = execFileCommandWithTimeout;
16
+ exports.isArgentSelector = isArgentSelector;
17
+ exports.main = main;
18
+ exports.parseArgs = parseArgs;
19
+ exports.parseBaseArgs = parseBaseArgs;
20
+ exports.readArgentStepOptions = readArgentStepOptions;
21
+ exports.readScreenSize = readScreenSize;
22
+ exports.resolveArgentDriverSteps = resolveArgentDriverSteps;
23
+ exports.runArgentCapture = runArgentCapture;
24
+ exports.runArgentDriverStep = runArgentDriverStep;
25
+ exports.runIosSimctlScreenshotFallback = runIosSimctlScreenshotFallback;
26
+ exports.sanitizeArtifactFileSegment = sanitizeArtifactFileSegment;
27
+ exports.usage = usage;
28
+ exports.validateArgentDriverSteps = validateArgentDriverSteps;
29
+ exports.writeArgentAvailabilityArtifacts = writeArgentAvailabilityArtifacts;
30
+ const { spawn } = require('node:child_process');
31
+ const crypto = require('node:crypto');
32
+ const fs = require('node:fs');
33
+ const fsp = require('node:fs/promises');
34
+ const path = require('node:path');
35
+ const { buildAgentSummaryMarkdown } = require('../core/agent-summary');
36
+ const { createArtifactLayout } = require('../core/artifact-layout');
37
+ const { writeJsonArtifact, writeTextArtifact } = require('../core/artifact-writer');
38
+ const { buildScenarioExecutionPlan } = require('../core/execution-plan');
39
+ const { SCHEMAS, assertValidJson } = require('../core/schema-validator');
40
+ const { hasHelpFlag, writeUsage } = require('./cli');
41
+ const { createArgentDriver, formatArgentRawOutput, isArgentRootOnlyDescription, } = require('./argent-driver');
42
+ const { createIosSimctlDriver, formatIosSimctlRawOutput, } = require('./ios-simctl-driver');
43
+ const { loadAslLocalEnv, readBooleanArgOrEnv, readStringArgOrEnv, } = require('./local-env');
44
+ const DASH_VALUE_KEYS = new Set(['app-flag', 'base-args', 'device-flag']);
45
+ const DEFAULT_ARGENT_BASE_ARGS = ['run'];
46
+ const DEFAULT_ARGENT_REQUIRED_TOOLS = [
47
+ 'launch-app',
48
+ 'open-url',
49
+ 'describe',
50
+ 'screenshot',
51
+ 'gesture-tap',
52
+ 'gesture-swipe',
53
+ ];
54
+ /**
55
+ * Prints CLI usage.
56
+ *
57
+ * @param {{write: (message: string) => unknown}} output
58
+ * @returns {void}
59
+ */
60
+ function usage(output = process.stderr) {
61
+ writeUsage([
62
+ 'Usage: asl-argent --platform <ios|android> --scenario <path> --device <id> [--app <bundle-or-package>] [--out <dir>] [--run-id <id>]',
63
+ '',
64
+ 'Executes scenario-declared launch and portable driver actions through the external Argent CLI.',
65
+ 'Writes health.json, verdict.json, agent-summary.md, raw command transcripts, and screenshot captures.',
66
+ 'Use --check --out <dir> to verify the configured Argent command and required tool surface and preserve availability artifacts.',
67
+ 'Use --argent <binary> and --base-args "<args>" to adapt local Argent installs without bundling Argent.',
68
+ 'Use --device-flag and --app-flag when your Argent command expects platform-specific flag names.',
69
+ 'Use --command-timeout-ms <ms> to bound each external Argent invocation.',
70
+ 'Use --ios-simctl-screenshot-fallback on iOS when simctl should provide screenshot evidence if Argent screenshot is unavailable.',
71
+ 'Use --xcrun <path> to route the iOS simctl screenshot fallback through a specific xcrun binary.',
72
+ ], output);
73
+ }
74
+ /**
75
+ * Parses `--key value` CLI arguments.
76
+ *
77
+ * @param {string[]} argv
78
+ * @returns {CliArgs}
79
+ */
80
+ function parseArgs(argv) {
81
+ const args = {};
82
+ for (let index = 0; index < argv.length; index += 1) {
83
+ const token = argv[index];
84
+ if (token === '--') {
85
+ continue;
86
+ }
87
+ if (!token?.startsWith('--')) {
88
+ continue;
89
+ }
90
+ const equalsIndex = token.indexOf('=');
91
+ if (equalsIndex > 2) {
92
+ args[token.slice(2, equalsIndex)] = token.slice(equalsIndex + 1);
93
+ continue;
94
+ }
95
+ const key = token.slice(2);
96
+ const value = argv[index + 1];
97
+ if (value && (!value.startsWith('--') || DASH_VALUE_KEYS.has(key))) {
98
+ args[key] = value;
99
+ index += 1;
100
+ }
101
+ else {
102
+ args[key] = true;
103
+ }
104
+ }
105
+ return args;
106
+ }
107
+ /**
108
+ * Reads and parses a JSON object from disk.
109
+ *
110
+ * @param {string} filePath
111
+ * @returns {Record<string, unknown>}
112
+ */
113
+ function readJson(filePath) {
114
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
115
+ }
116
+ /**
117
+ * Reads a finite number from adapter metadata.
118
+ *
119
+ * @param {unknown} value
120
+ * @returns {number | undefined}
121
+ */
122
+ function readFiniteNumber(value) {
123
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
124
+ }
125
+ /**
126
+ * Reads a positive integer from CLI or scenario metadata.
127
+ *
128
+ * @param {unknown} value
129
+ * @param {number} fallback
130
+ * @returns {number}
131
+ */
132
+ function readPositiveInteger(value, fallback) {
133
+ const parsed = typeof value === 'string' ? Number(value) : value;
134
+ return typeof parsed === 'number' && Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
135
+ }
136
+ /**
137
+ * Creates a short random run id.
138
+ *
139
+ * @returns {string}
140
+ */
141
+ function createRunId() {
142
+ return crypto.randomBytes(6).toString('hex');
143
+ }
144
+ /**
145
+ * Runs a command and captures stdout, stderr, and exit code without throwing.
146
+ *
147
+ * @param {string} command
148
+ * @param {string[]} args
149
+ * @returns {Promise<CommandResult>}
150
+ */
151
+ function execFileCommand(command, args) {
152
+ return execFileCommandWithTimeout(command, args);
153
+ }
154
+ /**
155
+ * Runs a command with a bounded timeout and captures stdout, stderr, and exit code without throwing.
156
+ *
157
+ * @param {string} command
158
+ * @param {string[]} args
159
+ * @param {number} [timeoutMs]
160
+ * @returns {Promise<CommandResult>}
161
+ */
162
+ function execFileCommandWithTimeout(command, args, timeoutMs = 60_000) {
163
+ return new Promise((resolve) => {
164
+ const child = spawn(command, args, {
165
+ detached: process.platform !== 'win32',
166
+ stdio: ['ignore', 'pipe', 'pipe'],
167
+ });
168
+ let stdout = '';
169
+ let stderr = '';
170
+ let settled = false;
171
+ let timedOut = false;
172
+ let forceKillTimer = null;
173
+ /**
174
+ * Signals the spawned command and its process group when the platform supports it.
175
+ *
176
+ * @param {NodeJS.Signals} signal
177
+ * @returns {void}
178
+ */
179
+ const signalChildTree = (signal) => {
180
+ if (typeof child.pid !== 'number') {
181
+ return;
182
+ }
183
+ try {
184
+ if (process.platform === 'win32') {
185
+ child.kill(signal);
186
+ }
187
+ else {
188
+ process.kill(-child.pid, signal);
189
+ }
190
+ }
191
+ catch {
192
+ child.kill(signal);
193
+ }
194
+ };
195
+ const buildResult = (code, signal) => ({
196
+ command,
197
+ args,
198
+ exitCode: typeof code === 'number' ? code : signal ? 1 : 0,
199
+ stderr: [stderr, timedOut ? `Argent command timed out after ${timeoutMs}ms.` : ''].filter(Boolean).join('\n'),
200
+ stdout,
201
+ });
202
+ const timeout = setTimeout(() => {
203
+ timedOut = true;
204
+ signalChildTree('SIGTERM');
205
+ forceKillTimer = setTimeout(() => {
206
+ signalChildTree('SIGKILL');
207
+ finish(buildResult(null, 'SIGKILL'));
208
+ }, 1500);
209
+ }, timeoutMs);
210
+ const finish = (result) => {
211
+ if (settled) {
212
+ return;
213
+ }
214
+ settled = true;
215
+ clearTimeout(timeout);
216
+ if (forceKillTimer) {
217
+ clearTimeout(forceKillTimer);
218
+ }
219
+ child.unref();
220
+ child.stdout?.unref();
221
+ child.stderr?.unref();
222
+ child.stdout?.destroy();
223
+ child.stderr?.destroy();
224
+ resolve(result);
225
+ };
226
+ child.stdout?.setEncoding('utf8');
227
+ child.stderr?.setEncoding('utf8');
228
+ child.stdout?.on('data', (chunk) => {
229
+ stdout += chunk;
230
+ });
231
+ child.stderr?.on('data', (chunk) => {
232
+ stderr += chunk;
233
+ });
234
+ child.on('error', (error) => {
235
+ finish({
236
+ command,
237
+ args,
238
+ exitCode: 1,
239
+ stderr: stderr || error.message,
240
+ stdout,
241
+ });
242
+ });
243
+ child.on('exit', (code, signal) => {
244
+ // `exit` can arrive before pipe data events drain, while `close` can wait on
245
+ // wrapper-spawned helpers that inherited stdio. Defer one tick to capture
246
+ // buffered output without reintroducing inherited-pipe hangs.
247
+ setImmediate(() => finish(buildResult(code, signal)));
248
+ });
249
+ child.on('close', (code, signal) => {
250
+ finish(buildResult(code, signal));
251
+ });
252
+ });
253
+ }
254
+ /**
255
+ * Waits for the requested capture window.
256
+ *
257
+ * @param {number} ms
258
+ * @returns {Promise<void>}
259
+ */
260
+ function delay(ms) {
261
+ return new Promise((resolve) => {
262
+ setTimeout(resolve, ms);
263
+ });
264
+ }
265
+ /**
266
+ * Reads the first booted iOS simulator UDID from `simctl` JSON.
267
+ *
268
+ * @param {string} stdout
269
+ * @returns {string | null}
270
+ */
271
+ function parseBootedIosSimulatorUdid(stdout) {
272
+ try {
273
+ const parsed = JSON.parse(stdout);
274
+ for (const devices of Object.values(parsed.devices ?? {})) {
275
+ const booted = devices.find((device) => device.state === 'Booted' && typeof device.udid === 'string');
276
+ if (booted?.udid) {
277
+ return booted.udid;
278
+ }
279
+ }
280
+ }
281
+ catch {
282
+ return null;
283
+ }
284
+ return null;
285
+ }
286
+ /**
287
+ * Resolves Argent's iOS device id because Argent does not understand simctl's `booted` shorthand.
288
+ *
289
+ * @param {number} commandTimeoutMs
290
+ * @returns {Promise<string | null>}
291
+ */
292
+ async function resolveBootedIosSimulatorUdid(commandTimeoutMs) {
293
+ const result = await execFileCommandWithTimeout('xcrun', ['simctl', 'list', 'devices', 'booted', '-j'], Math.min(commandTimeoutMs, 10_000));
294
+ if (result.exitCode !== 0) {
295
+ return null;
296
+ }
297
+ return parseBootedIosSimulatorUdid(result.stdout);
298
+ }
299
+ /**
300
+ * Resolves the device id that should be passed to Argent.
301
+ *
302
+ * @param {{commandTimeoutMs: number, deviceId: string, platform: 'android' | 'ios', resolveBootedIosSimulatorUdid?: () => Promise<string | null>}} options
303
+ * @returns {Promise<{deviceId: string, requestedDeviceId?: string}>}
304
+ */
305
+ async function resolveArgentDeviceId({ commandTimeoutMs, deviceId, platform, resolveBootedIosSimulatorUdid: resolveBooted = () => resolveBootedIosSimulatorUdid(commandTimeoutMs), }) {
306
+ if (platform !== 'ios' || deviceId !== 'booted') {
307
+ return { deviceId };
308
+ }
309
+ const resolvedDeviceId = await resolveBooted();
310
+ return resolvedDeviceId ? { deviceId: resolvedDeviceId, requestedDeviceId: deviceId } : { deviceId };
311
+ }
312
+ /**
313
+ * Returns adapter options for an Argent-backed step.
314
+ *
315
+ * @param {ScenarioExecutionStep} step
316
+ * @returns {Record<string, unknown>}
317
+ */
318
+ function readArgentStepOptions(step) {
319
+ const argentOptions = step.adapterOptions?.argent;
320
+ return argentOptions && typeof argentOptions === 'object' && !Array.isArray(argentOptions)
321
+ ? argentOptions
322
+ : {};
323
+ }
324
+ /**
325
+ * Returns true when a normalized step has a portable selector.
326
+ *
327
+ * @param {unknown} value
328
+ * @returns {value is import('./argent-driver').ArgentSelector}
329
+ */
330
+ function isArgentSelector(value) {
331
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
332
+ return false;
333
+ }
334
+ const selector = value;
335
+ return typeof selector.kind === 'string' && typeof selector.value === 'string' && selector.value.length > 0;
336
+ }
337
+ /**
338
+ * Returns the default raw file name for one Argent action.
339
+ *
340
+ * @param {{driverAction: ArgentDriverStep['driverAction'], index: number}} options
341
+ * @returns {string}
342
+ */
343
+ function defaultArgentRawFileName({ driverAction, index, }) {
344
+ return `argent-${driverAction}-${index}.txt`;
345
+ }
346
+ /**
347
+ * Converts a scenario step id into a safe artifact filename segment.
348
+ *
349
+ * @param {string} value
350
+ * @returns {string}
351
+ */
352
+ function sanitizeArtifactFileSegment(value) {
353
+ const sanitized = value.replace(/[^a-z0-9._-]+/giu, '-').replace(/^-+|-+$/gu, '');
354
+ return sanitized || 'step';
355
+ }
356
+ /**
357
+ * Returns the simctl fallback screenshot filename for an Argent screenshot step.
358
+ *
359
+ * @param {ArgentDriverStep} driverStep
360
+ * @returns {string}
361
+ */
362
+ function defaultIosSimctlFallbackScreenshotFileName(driverStep) {
363
+ return driverStep.captureFileName && driverStep.captureFileName.length > 0
364
+ ? driverStep.captureFileName
365
+ : `ios-simctl-${sanitizeArtifactFileSegment(driverStep.stepId)}.png`;
366
+ }
367
+ /**
368
+ * Expands normalized scenario steps into Argent driver actions.
369
+ *
370
+ * @param {Record<string, unknown>} scenario
371
+ * @param {import('./argent-driver').ArgentScreenSize | undefined} screenSize
372
+ * @returns {ArgentDriverStep[]}
373
+ */
374
+ function resolveArgentDriverSteps(scenario, screenSize) {
375
+ const executionPlan = buildScenarioExecutionPlan(scenario);
376
+ return executionPlan.steps
377
+ .filter((step) => step.kind === 'launch' ||
378
+ ['assertVisible', 'inspectTree', 'screenshot', 'scroll', 'tap'].includes(String(step.driverAction)))
379
+ .map((step, index) => {
380
+ const argentOptions = readArgentStepOptions(step);
381
+ const action = step.kind === 'launch' ? 'launch' : step.driverAction;
382
+ const actionIndex = index + 1;
383
+ return {
384
+ driverAction: action,
385
+ ...(typeof argentOptions.appId === 'string' ? { appId: argentOptions.appId } : {}),
386
+ ...(typeof argentOptions.captureFileName === 'string' ? { captureFileName: argentOptions.captureFileName } : {}),
387
+ ...(typeof readFiniteNumber(argentOptions.durationMs) === 'number'
388
+ ? { durationMs: readFiniteNumber(argentOptions.durationMs) }
389
+ : {}),
390
+ ...(typeof readFiniteNumber(argentOptions.endX) === 'number' ? { endX: readFiniteNumber(argentOptions.endX) } : {}),
391
+ ...(typeof readFiniteNumber(argentOptions.endY) === 'number' ? { endY: readFiniteNumber(argentOptions.endY) } : {}),
392
+ rawFileName: typeof argentOptions.rawFileName === 'string' && argentOptions.rawFileName.length > 0
393
+ ? argentOptions.rawFileName
394
+ : defaultArgentRawFileName({ driverAction: action, index: actionIndex }),
395
+ required: step.required !== false,
396
+ ...(screenSize ? { screenSize } : {}),
397
+ ...(isArgentSelector(argentOptions.selector)
398
+ ? { selector: argentOptions.selector }
399
+ : isArgentSelector(step.selector)
400
+ ? { selector: step.selector }
401
+ : {}),
402
+ ...(typeof readFiniteNumber(argentOptions.startX) === 'number' ? { startX: readFiniteNumber(argentOptions.startX) } : {}),
403
+ ...(typeof readFiniteNumber(argentOptions.startY) === 'number' ? { startY: readFiniteNumber(argentOptions.startY) } : {}),
404
+ stepId: step.id,
405
+ waitMs: readPositiveInteger(argentOptions.waitMs ?? step.timeoutMs, 0),
406
+ ...(typeof readFiniteNumber(argentOptions.x) === 'number' ? { x: readFiniteNumber(argentOptions.x) } : {}),
407
+ ...(typeof readFiniteNumber(argentOptions.y) === 'number' ? { y: readFiniteNumber(argentOptions.y) } : {}),
408
+ };
409
+ });
410
+ }
411
+ /**
412
+ * Returns profile-time validation errors for Argent driver steps.
413
+ *
414
+ * @param {ArgentDriverStep[]} driverSteps
415
+ * @param {{app?: string | null}} options
416
+ * @returns {string[]}
417
+ */
418
+ function validateArgentDriverSteps(driverSteps, options = {}) {
419
+ const errors = [];
420
+ for (const step of driverSteps) {
421
+ const stepLabel = step.stepId ? `step \`${step.stepId}\`` : 'unnamed step';
422
+ if (step.driverAction === 'launch' && !step.appId && !options.app) {
423
+ errors.push(`${stepLabel} is a launch step but no app id was provided through --app or adapterOptions.argent.appId.`);
424
+ }
425
+ if (step.driverAction === 'tap' && (typeof step.x !== 'number' || typeof step.y !== 'number')) {
426
+ errors.push(`${stepLabel} uses driverAction \`tap\` but is missing adapterOptions.argent.x/y.`);
427
+ }
428
+ if (step.driverAction === 'scroll' && (typeof step.startX !== 'number' ||
429
+ typeof step.startY !== 'number' ||
430
+ typeof step.endX !== 'number' ||
431
+ typeof step.endY !== 'number')) {
432
+ errors.push(`${stepLabel} uses driverAction \`scroll\` but is missing adapterOptions.argent.startX/startY/endX/endY.`);
433
+ }
434
+ if (step.driverAction === 'assertVisible' && !step.selector) {
435
+ errors.push(`${stepLabel} uses driverAction \`assertVisible\` but is missing a portable selector.`);
436
+ }
437
+ }
438
+ return errors;
439
+ }
440
+ /**
441
+ * Builds scalar health metadata for one portable selector.
442
+ *
443
+ * @param {import('./argent-driver').ArgentSelector | undefined} selector
444
+ * @returns {Record<string, string>}
445
+ */
446
+ function buildArgentSelectorHealthMetadata(selector) {
447
+ if (!selector) {
448
+ return {};
449
+ }
450
+ return {
451
+ selectorKind: selector.kind,
452
+ ...(selector.match ? { selectorMatch: selector.match } : {}),
453
+ selectorValue: selector.value,
454
+ };
455
+ }
456
+ /**
457
+ * Builds the most specific next-action hint available from an Argent failure.
458
+ *
459
+ * @param {ArgentFailureHintOptions} options
460
+ * @returns {{nextAction: string, nextActionCode: string, argentDiagnostic?: string}}
461
+ */
462
+ function buildArgentFailureMetadata({ driverAction, fallbackCapturePath, missingRequiredScreenshot, rawFileName, result, rootOnlyDescription, }) {
463
+ const output = `${result.stdout}\n${result.stderr}`;
464
+ if (/ENOENT|command not found|not found/iu.test(output)) {
465
+ return {
466
+ argentDiagnostic: 'argent_command_unavailable',
467
+ nextAction: 'Install Argent, pass --argent with the local Argent command, or set --base-args/--device-flag/--app-flag to match the installed command before rerunning.',
468
+ nextActionCode: 'configure_argent_command',
469
+ };
470
+ }
471
+ if (/timed out after \d+ms/iu.test(output)) {
472
+ return {
473
+ argentDiagnostic: 'argent_command_timeout',
474
+ nextAction: 'Confirm the Argent command can run without package-manager or device-control prompts, increase --command-timeout-ms if the command is legitimately slow, then rerun.',
475
+ nextActionCode: 'fix_argent_command_timeout',
476
+ };
477
+ }
478
+ if (/SimulatorServer|simulator-server/iu.test(output)) {
479
+ return {
480
+ argentDiagnostic: 'argent_simulator_server_unavailable',
481
+ ...(fallbackCapturePath ? { fallbackCapturePath, fallbackProvider: 'ios-simctl' } : {}),
482
+ nextAction: fallbackCapturePath
483
+ ? `Argent could not start its simulator-server dependency for ${driverAction}, but iOS simctl fallback captured ${fallbackCapturePath}. Inspect raw/${rawFileName} before relying on Argent screenshot evidence.`
484
+ : `Argent could not start its simulator-server dependency for ${driverAction}. Inspect raw/${rawFileName}, verify the selected simulator is accessible to Argent, and use simctl or another screenshot provider when screenshot evidence is required.`,
485
+ nextActionCode: 'fix_argent_simulator_server',
486
+ };
487
+ }
488
+ if (rootOnlyDescription) {
489
+ return {
490
+ argentDiagnostic: 'root_only_description',
491
+ nextAction: `Argent returned only the root UI description for ${driverAction}. Inspect raw/${rawFileName}, confirm the app is foregrounded and visible to Argent, then rerun.`,
492
+ nextActionCode: 'fix_argent_visibility_target',
493
+ };
494
+ }
495
+ if (missingRequiredScreenshot) {
496
+ return {
497
+ argentDiagnostic: 'missing_screenshot_path',
498
+ nextAction: `Argent completed screenshot without reporting a saved file. Inspect raw/${rawFileName}, adjust the Argent command shape, or make the screenshot step optional before rerunning.`,
499
+ nextActionCode: 'fix_argent_screenshot_output',
500
+ };
501
+ }
502
+ return {
503
+ nextAction: `Inspect raw/${rawFileName}, confirm Argent can see the selected app/device, and rerun the capture.`,
504
+ nextActionCode: 'inspect_argent_driver_action',
505
+ };
506
+ }
507
+ /**
508
+ * Runs one Argent driver action.
509
+ *
510
+ * @param {{driver: import('./argent-driver').ArgentDriver, driverStep: ArgentDriverStep}} options
511
+ * @returns {Promise<import('./argent-driver').ArgentCommandResult>}
512
+ */
513
+ async function runArgentDriverStep({ driver, driverStep, }) {
514
+ if (driverStep.driverAction === 'launch') {
515
+ return driver.launchApp({
516
+ ...(driverStep.appId ? { appId: driverStep.appId } : {}),
517
+ rawFileName: driverStep.rawFileName,
518
+ });
519
+ }
520
+ if (driverStep.driverAction === 'assertVisible' && driverStep.selector) {
521
+ return driver.assertVisible({
522
+ ...(driverStep.appId ? { appId: driverStep.appId } : {}),
523
+ rawFileName: driverStep.rawFileName,
524
+ selector: driverStep.selector,
525
+ });
526
+ }
527
+ if (driverStep.driverAction === 'inspectTree') {
528
+ return driver.inspectTree({
529
+ ...(driverStep.appId ? { appId: driverStep.appId } : {}),
530
+ rawFileName: driverStep.rawFileName,
531
+ });
532
+ }
533
+ if (driverStep.driverAction === 'screenshot') {
534
+ return driver.screenshot({
535
+ rawFileName: driverStep.rawFileName,
536
+ });
537
+ }
538
+ if (driverStep.driverAction === 'scroll') {
539
+ return driver.scroll({
540
+ ...(typeof driverStep.durationMs === 'number' ? { durationMs: driverStep.durationMs } : {}),
541
+ endX: driverStep.endX,
542
+ endY: driverStep.endY,
543
+ rawFileName: driverStep.rawFileName,
544
+ ...(driverStep.screenSize ? { screenSize: driverStep.screenSize } : {}),
545
+ startX: driverStep.startX,
546
+ startY: driverStep.startY,
547
+ });
548
+ }
549
+ if (driverStep.driverAction === 'tap') {
550
+ return driver.tap({
551
+ rawFileName: driverStep.rawFileName,
552
+ ...(driverStep.screenSize ? { screenSize: driverStep.screenSize } : {}),
553
+ x: driverStep.x,
554
+ y: driverStep.y,
555
+ });
556
+ }
557
+ throw new Error(`Unsupported Argent driver action: ${driverStep.driverAction}`);
558
+ }
559
+ /**
560
+ * Captures an iOS screenshot through simctl when Argent's iOS screenshot backend is unavailable.
561
+ *
562
+ * @param {{capturesDir: string, deviceId: string, driverStep: ArgentDriverStep, executor?: CommandExecutor, xcrunPath: string}} options
563
+ * @returns {Promise<IosSimctlScreenshotFallbackResult>}
564
+ */
565
+ async function runIosSimctlScreenshotFallback({ capturesDir, deviceId, driverStep, executor = execFileCommandWithTimeout, xcrunPath, }) {
566
+ const fileName = defaultIosSimctlFallbackScreenshotFileName(driverStep);
567
+ const outputPath = path.join(capturesDir, fileName);
568
+ const rawFileName = `ios-simctl-${sanitizeArtifactFileSegment(driverStep.stepId)}-screenshot.txt`;
569
+ const driver = createIosSimctlDriver({
570
+ deviceUdid: deviceId,
571
+ executor,
572
+ xcrunPath,
573
+ });
574
+ const result = await driver.screenshot({
575
+ outputPath,
576
+ rawFileName,
577
+ });
578
+ try {
579
+ await fsp.access(outputPath);
580
+ }
581
+ catch {
582
+ return {
583
+ rawFileName,
584
+ result,
585
+ };
586
+ }
587
+ return {
588
+ capturePath: `captures/${fileName}`,
589
+ rawFileName,
590
+ result,
591
+ };
592
+ }
593
+ /**
594
+ * Builds a stable health code suffix for one Argent driver action.
595
+ *
596
+ * @param {ArgentDriverStep['driverAction']} driverAction
597
+ * @returns {string}
598
+ */
599
+ function argentDriverActionCode(driverAction) {
600
+ return driverAction.replace(/[A-Z]/gu, (match) => `_${match.toLowerCase()}`);
601
+ }
602
+ /**
603
+ * Builds a health artifact from Argent capture checks.
604
+ *
605
+ * @param {{flowId?: string, runId: string, scenarioId: string, checks: Record<string, unknown>[]}} options
606
+ * @returns {Record<string, unknown>}
607
+ */
608
+ function buildArgentHealth({ checks, flowId, runId, scenarioId, }) {
609
+ const failed = checks.some((check) => check.status === 'failed');
610
+ return assertValidJson({
611
+ schemaVersion: '1.0.0',
612
+ scenarioId,
613
+ ...(flowId ? { flowId } : {}),
614
+ runId,
615
+ healthStatus: failed ? 'failed' : 'passed',
616
+ checks,
617
+ }, SCHEMAS.health, 'Health artifact');
618
+ }
619
+ /**
620
+ * Builds a verdict artifact for Argent capture readiness.
621
+ *
622
+ * @param {{health: Record<string, unknown>, runId: string, scenarioId: string, flowId?: string}} options
623
+ * @returns {Record<string, unknown>}
624
+ */
625
+ function buildArgentVerdict({ flowId, health, runId, scenarioId, }) {
626
+ const passed = health.healthStatus === 'passed';
627
+ return assertValidJson({
628
+ schemaVersion: '1.0.0',
629
+ scenarioId,
630
+ ...(flowId ? { flowId } : {}),
631
+ runId,
632
+ healthStatus: health.healthStatus,
633
+ verdictStatus: passed ? 'not_evaluated' : 'inconclusive',
634
+ budgetChecks: [],
635
+ summary: passed
636
+ ? 'Argent capture passed; no product budget has been evaluated.'
637
+ : 'Argent capture failed; runtime scenario execution is not ready.',
638
+ }, SCHEMAS.verdict, 'Verdict artifact');
639
+ }
640
+ /**
641
+ * Splits CLI base args without invoking a shell.
642
+ *
643
+ * @param {unknown} value
644
+ * @returns {string[] | undefined}
645
+ */
646
+ function parseBaseArgs(value) {
647
+ if (typeof value !== 'string' || value.trim().length === 0) {
648
+ return undefined;
649
+ }
650
+ return value.trim().split(/\s+/u);
651
+ }
652
+ /**
653
+ * Returns root-level Argent args from an `argent run` command shape.
654
+ *
655
+ * @param {string[]} baseArgs
656
+ * @returns {string[]}
657
+ */
658
+ function deriveArgentRootArgs(baseArgs) {
659
+ return baseArgs.at(-1) === 'run' ? baseArgs.slice(0, -1) : baseArgs;
660
+ }
661
+ /**
662
+ * Converts one Argent availability check into a schema-safe health check.
663
+ *
664
+ * @param {ArgentAvailabilityCheck} check
665
+ * @returns {Record<string, unknown>}
666
+ */
667
+ function argentAvailabilityHealthCheck(check) {
668
+ return {
669
+ name: check.name,
670
+ status: check.status,
671
+ source: 'runner',
672
+ code: check.code,
673
+ message: check.message,
674
+ metadata: {
675
+ command: check.command,
676
+ args: check.args.join(' '),
677
+ exitCode: check.exitCode,
678
+ ...(check.stderrPreview ? { stderrPreview: check.stderrPreview } : {}),
679
+ ...(check.stdoutPreview ? { stdoutPreview: check.stdoutPreview } : {}),
680
+ ...(check.metadata ?? {}),
681
+ },
682
+ };
683
+ }
684
+ /**
685
+ * Writes ASL artifacts for an Argent command-surface availability check.
686
+ *
687
+ * @param {ArgentAvailabilityArtifactOptions} options
688
+ * @returns {Promise<{agentSummary: string, health: Record<string, unknown>, runDir: string, verdict: Record<string, unknown>}>}
689
+ */
690
+ async function writeArgentAvailabilityArtifacts({ outputDir, result, runId = createRunId(), }) {
691
+ const runDir = path.resolve(outputDir);
692
+ const layout = createArtifactLayout({ outputDir: runDir });
693
+ const checks = result.checks.map(argentAvailabilityHealthCheck);
694
+ const health = buildArgentHealth({
695
+ checks,
696
+ flowId: 'argent-availability',
697
+ runId,
698
+ scenarioId: 'argent-availability',
699
+ });
700
+ const verdict = assertValidJson({
701
+ schemaVersion: '1.0.0',
702
+ scenarioId: 'argent-availability',
703
+ flowId: 'argent-availability',
704
+ runId,
705
+ healthStatus: health.healthStatus,
706
+ verdictStatus: result.status === 'passed' ? 'not_evaluated' : 'inconclusive',
707
+ budgetChecks: [],
708
+ summary: result.status === 'passed'
709
+ ? 'Argent command surface is available; no product budget has been evaluated.'
710
+ : 'Argent command surface is unavailable; fix runner environment health before live proof.',
711
+ }, SCHEMAS.verdict, 'Verdict artifact');
712
+ const agentSummary = buildAgentSummaryMarkdown({ health, verdict });
713
+ await fsp.mkdir(layout.raw, { recursive: true });
714
+ await writeJsonArtifact({
715
+ filePath: layout.health,
716
+ value: health,
717
+ schema: SCHEMAS.health,
718
+ label: 'Health artifact',
719
+ });
720
+ await writeJsonArtifact({
721
+ filePath: layout.verdict,
722
+ value: verdict,
723
+ schema: SCHEMAS.verdict,
724
+ label: 'Verdict artifact',
725
+ });
726
+ await writeTextArtifact({
727
+ filePath: layout.agentSummary,
728
+ content: agentSummary,
729
+ });
730
+ await fsp.writeFile(path.join(layout.raw, 'argent-availability.json'), `${JSON.stringify(result, null, 2)}\n`, 'utf8');
731
+ return {
732
+ agentSummary,
733
+ health,
734
+ runDir,
735
+ verdict,
736
+ };
737
+ }
738
+ /**
739
+ * Returns a compact single-line preview for command diagnostics.
740
+ *
741
+ * @param {string} value
742
+ * @returns {string | undefined}
743
+ */
744
+ function previewCommandOutput(value) {
745
+ const preview = value.replace(/\s+/gu, ' ').trim();
746
+ return preview.length > 240 ? `${preview.slice(0, 237)}...` : preview || undefined;
747
+ }
748
+ /**
749
+ * Classifies an Argent availability failure into the next operational step.
750
+ *
751
+ * @param {CommandResult} result
752
+ * @returns {Record<string, string>}
753
+ */
754
+ function classifyArgentAvailabilityFailure(result) {
755
+ const diagnostic = `${result.stderr}\n${result.stdout}`;
756
+ if (/operation not permitted|permission denied|sandbox|eacces|eperm|cannot bind|smartsocket/iu.test(diagnostic)) {
757
+ return {
758
+ failureClass: 'host_access',
759
+ nextAction: 'Rerun Argent availability with host/device access before treating this as an app, scenario, or runner regression.',
760
+ nextActionCode: 'rerun_with_host_access',
761
+ };
762
+ }
763
+ if (/timed out|timeout/iu.test(diagnostic)) {
764
+ return {
765
+ failureClass: 'timeout',
766
+ nextAction: 'Confirm Argent can run without prompts, use a direct Argent binary when available, or increase --command-timeout-ms before rerunning the availability check.',
767
+ nextActionCode: 'increase_argent_timeout',
768
+ };
769
+ }
770
+ if (/enoent|not found|command not found|no such file or directory|could not determine executable/iu.test(diagnostic)) {
771
+ return {
772
+ failureClass: 'missing_binary',
773
+ nextAction: 'Install Argent, pass the correct binary with --argent, or provide the wrapper shape with --base-args before starting live proof.',
774
+ nextActionCode: 'configure_argent_binary',
775
+ };
776
+ }
777
+ return {
778
+ failureClass: 'command_surface',
779
+ nextAction: 'Inspect the failed Argent command output, fix the command surface, then rerun the availability check before starting live proof.',
780
+ nextActionCode: 'inspect_argent_availability',
781
+ };
782
+ }
783
+ /**
784
+ * Builds one availability check result from an Argent command execution.
785
+ *
786
+ * @param {{code: string, expectedPattern: RegExp, name: string, result: CommandResult}} options
787
+ * @returns {ArgentAvailabilityCheck}
788
+ */
789
+ function buildArgentAvailabilityCheck({ code, expectedPattern, name, result, }) {
790
+ const output = `${result.stdout}\n${result.stderr}`;
791
+ const expectedOutputFound = expectedPattern.test(output);
792
+ const completedBeforeWrapperTimeout = expectedOutputFound && /timed out after \d+ms/iu.test(result.stderr);
793
+ const passed = expectedOutputFound && (result.exitCode === 0 || completedBeforeWrapperTimeout);
794
+ const check = {
795
+ args: result.args,
796
+ code,
797
+ command: result.command,
798
+ exitCode: result.exitCode,
799
+ message: passed
800
+ ? completedBeforeWrapperTimeout
801
+ ? `${name} returned the expected Argent output before a wrapper timeout.`
802
+ : `${name} is available.`
803
+ : `${name} did not return the expected Argent output.`,
804
+ name,
805
+ status: passed ? 'passed' : 'failed',
806
+ };
807
+ if (!passed) {
808
+ const stderrPreview = previewCommandOutput(result.stderr);
809
+ const stdoutPreview = previewCommandOutput(result.stdout);
810
+ check.metadata = classifyArgentAvailabilityFailure(result);
811
+ if (stderrPreview) {
812
+ check.stderrPreview = stderrPreview;
813
+ }
814
+ if (stdoutPreview) {
815
+ check.stdoutPreview = stdoutPreview;
816
+ }
817
+ }
818
+ return check;
819
+ }
820
+ /**
821
+ * Verifies that the configured Argent command can invoke the ASL-required tool surface.
822
+ *
823
+ * @param {ArgentAvailabilityOptions} options
824
+ * @returns {Promise<ArgentAvailabilityResult>}
825
+ */
826
+ async function checkArgentAvailability({ argentCommand = 'argent', baseArgs = DEFAULT_ARGENT_BASE_ARGS, commandTimeoutMs = 30_000, executor, requiredTools = DEFAULT_ARGENT_REQUIRED_TOOLS, } = {}) {
827
+ const run = executor ?? ((command, args) => execFileCommandWithTimeout(command, args, commandTimeoutMs));
828
+ const checks = [];
829
+ const runHelp = await run(argentCommand, [...baseArgs, '--help']);
830
+ checks.push(buildArgentAvailabilityCheck({
831
+ code: 'argent_run_help_available',
832
+ expectedPattern: /Usage:\s+argent\s+run\s+<tool>/iu,
833
+ name: 'argent_run_help',
834
+ result: runHelp,
835
+ }));
836
+ const rootArgs = deriveArgentRootArgs(baseArgs);
837
+ for (const tool of requiredTools) {
838
+ const result = await run(argentCommand, [...rootArgs, 'tools', 'describe', tool]);
839
+ checks.push(buildArgentAvailabilityCheck({
840
+ code: `argent_tool_${tool.replace(/-/gu, '_')}_available`,
841
+ expectedPattern: new RegExp(`Tool:\\s+${tool.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}`, 'iu'),
842
+ name: `argent_tool_${tool}`,
843
+ result,
844
+ }));
845
+ }
846
+ const status = checks.every((check) => check.status === 'passed') ? 'passed' : 'failed';
847
+ return {
848
+ argentCommand,
849
+ baseArgs,
850
+ checks,
851
+ requiredTools,
852
+ status,
853
+ };
854
+ }
855
+ /**
856
+ * Returns a screen size from CLI values when both dimensions are present.
857
+ *
858
+ * @param {{width?: unknown, height?: unknown}} options
859
+ * @returns {import('./argent-driver').ArgentScreenSize | undefined}
860
+ */
861
+ function readScreenSize({ height, width, }) {
862
+ const parsedWidth = typeof width === 'string' ? Number(width) : width;
863
+ const parsedHeight = typeof height === 'string' ? Number(height) : height;
864
+ if (typeof parsedWidth !== 'number' || typeof parsedHeight !== 'number') {
865
+ return undefined;
866
+ }
867
+ if (!Number.isFinite(parsedWidth) || !Number.isFinite(parsedHeight) || parsedWidth <= 0 || parsedHeight <= 0) {
868
+ throw new Error('--screen-width and --screen-height must be positive numbers.');
869
+ }
870
+ return {
871
+ height: parsedHeight,
872
+ width: parsedWidth,
873
+ };
874
+ }
875
+ /**
876
+ * Copies a screenshot produced by Argent into the stable ASL captures folder.
877
+ *
878
+ * @param {{capturesDir: string, capturePath: string, preferredFileName?: string}} options
879
+ * @returns {Promise<string | null>}
880
+ */
881
+ async function copyArgentCapture({ capturePath, capturesDir, preferredFileName, }) {
882
+ try {
883
+ await fsp.access(capturePath);
884
+ }
885
+ catch {
886
+ return null;
887
+ }
888
+ const fileName = preferredFileName && preferredFileName.length > 0
889
+ ? preferredFileName
890
+ : path.basename(capturePath);
891
+ const destination = path.join(capturesDir, fileName);
892
+ if (path.resolve(capturePath) !== path.resolve(destination)) {
893
+ await fsp.copyFile(capturePath, destination);
894
+ }
895
+ return `captures/${fileName}`;
896
+ }
897
+ /**
898
+ * Runs scenario-declared portable actions through Argent and writes ASL artifacts.
899
+ *
900
+ * @param {ArgentCaptureOptions} options
901
+ * @returns {Promise<ArgentCaptureResult>}
902
+ */
903
+ async function runArgentCapture({ app = null, appFlag, argentCommand = 'argent', baseArgs, commandTimeoutMs = 60_000, delay: wait = delay, deviceFlag, deviceId, executor, iosSimctlExecutor, iosSimctlScreenshotFallback = false, outputDir = path.resolve('artifacts/argent-capture'), platform, resolveBootedIosSimulatorUdid: resolveBooted, runId = createRunId(), scenario, screenSize, waitMs = 0, xcrunPath = 'xcrun', }) {
904
+ const runDir = path.resolve(outputDir);
905
+ const layout = createArtifactLayout({ outputDir: runDir });
906
+ const rawDir = layout.raw;
907
+ await fsp.mkdir(rawDir, { recursive: true });
908
+ await fsp.mkdir(layout.captures, { recursive: true });
909
+ const executionPlan = buildScenarioExecutionPlan(scenario);
910
+ const raw = {};
911
+ const captures = {
912
+ screenshots: [],
913
+ };
914
+ const checks = [];
915
+ const driverActionMetadata = [];
916
+ const resolvedDriverSteps = resolveArgentDriverSteps(scenario, screenSize);
917
+ const driverStepErrors = validateArgentDriverSteps(resolvedDriverSteps, { app });
918
+ if (driverStepErrors.length > 0) {
919
+ throw new Error(`Invalid Argent driver step metadata: ${driverStepErrors.join(' ')}`);
920
+ }
921
+ const resolvedDevice = await resolveArgentDeviceId({
922
+ commandTimeoutMs,
923
+ deviceId,
924
+ platform,
925
+ ...(resolveBooted ? { resolveBootedIosSimulatorUdid: resolveBooted } : {}),
926
+ });
927
+ const driver = createArgentDriver({
928
+ ...(app ? { appId: app } : {}),
929
+ ...(appFlag ? { appFlag } : {}),
930
+ argentCommand,
931
+ ...(baseArgs ? { baseArgs } : {}),
932
+ ...(deviceFlag ? { deviceFlag } : {}),
933
+ deviceId: resolvedDevice.deviceId,
934
+ executor: executor ?? ((command, args) => execFileCommandWithTimeout(command, args, commandTimeoutMs)),
935
+ ...(screenSize ? { screenSize } : {}),
936
+ });
937
+ const metadata = {
938
+ app,
939
+ argentCommand,
940
+ baseArgs: baseArgs ?? ['run'],
941
+ captures,
942
+ commandTimeoutMs,
943
+ deviceId: resolvedDevice.deviceId,
944
+ driverActions: [],
945
+ platform,
946
+ ...(resolvedDevice.requestedDeviceId ? { requestedDeviceId: resolvedDevice.requestedDeviceId } : {}),
947
+ ...(screenSize ? { screenSize } : {}),
948
+ };
949
+ if (waitMs > 0) {
950
+ await wait(waitMs);
951
+ checks.push({
952
+ name: 'argent_capture_window_waited',
953
+ status: 'passed',
954
+ source: 'runner',
955
+ code: 'argent_capture_window_waited',
956
+ message: `Waited ${waitMs}ms before running Argent driver actions.`,
957
+ });
958
+ }
959
+ for (const driverStep of resolvedDriverSteps) {
960
+ if (driverStep.waitMs > 0) {
961
+ await wait(driverStep.waitMs);
962
+ checks.push({
963
+ name: 'argent_driver_action_waited',
964
+ status: 'passed',
965
+ source: 'runner',
966
+ code: 'argent_driver_action_waited',
967
+ message: `Waited ${driverStep.waitMs}ms before running Argent driver action ${driverStep.driverAction}.`,
968
+ metadata: {
969
+ driverAction: driverStep.driverAction,
970
+ stepId: driverStep.stepId,
971
+ },
972
+ });
973
+ }
974
+ const driverResult = await runArgentDriverStep({ driver, driverStep });
975
+ raw[driverResult.rawFileName] = formatArgentRawOutput(driverResult);
976
+ const rootOnlyDescription = ['assertVisible', 'inspectTree'].includes(driverStep.driverAction) &&
977
+ isArgentRootOnlyDescription(driverResult.stdout);
978
+ const missingRequiredScreenshot = driverStep.driverAction === 'screenshot' &&
979
+ driverResult.exitCode === 0 &&
980
+ !driverResult.capturePath &&
981
+ driverStep.required;
982
+ const failed = driverResult.exitCode !== 0 || rootOnlyDescription || missingRequiredScreenshot;
983
+ const codeSuffix = argentDriverActionCode(driverStep.driverAction);
984
+ let stableCapturePath = null;
985
+ let fallbackCapture = null;
986
+ if (driverStep.driverAction === 'screenshot' && driverResult.exitCode === 0 && driverResult.capturePath) {
987
+ stableCapturePath = await copyArgentCapture({
988
+ capturePath: driverResult.capturePath,
989
+ capturesDir: layout.captures,
990
+ ...(driverStep.captureFileName ? { preferredFileName: driverStep.captureFileName } : {}),
991
+ });
992
+ if (stableCapturePath) {
993
+ captures.screenshots.push(stableCapturePath);
994
+ }
995
+ }
996
+ if (driverStep.driverAction === 'screenshot' &&
997
+ failed &&
998
+ platform === 'ios' &&
999
+ iosSimctlScreenshotFallback) {
1000
+ fallbackCapture = await runIosSimctlScreenshotFallback({
1001
+ capturesDir: layout.captures,
1002
+ deviceId: resolvedDevice.deviceId,
1003
+ driverStep,
1004
+ ...(iosSimctlExecutor ? { executor: iosSimctlExecutor } : {}),
1005
+ xcrunPath,
1006
+ });
1007
+ raw[fallbackCapture.rawFileName] = formatIosSimctlRawOutput(fallbackCapture.result);
1008
+ if (fallbackCapture.capturePath) {
1009
+ stableCapturePath = fallbackCapture.capturePath;
1010
+ captures.screenshots.push(fallbackCapture.capturePath);
1011
+ }
1012
+ }
1013
+ const recoveredByFallback = Boolean(fallbackCapture?.capturePath);
1014
+ const status = failed && (driverStep.required === false || recoveredByFallback)
1015
+ ? 'warning'
1016
+ : failed
1017
+ ? 'failed'
1018
+ : 'passed';
1019
+ checks.push({
1020
+ name: `argent_${codeSuffix}`,
1021
+ status,
1022
+ source: 'runner',
1023
+ code: status === 'passed' ? `argent_${codeSuffix}_completed` : `argent_${codeSuffix}_failed`,
1024
+ message: status === 'passed'
1025
+ ? `Completed Argent driver action ${driverStep.driverAction}.`
1026
+ : `Argent driver action ${driverStep.driverAction} failed.`,
1027
+ metadata: {
1028
+ driverAction: driverStep.driverAction,
1029
+ ...(failed
1030
+ ? buildArgentFailureMetadata({
1031
+ driverAction: driverStep.driverAction,
1032
+ ...(fallbackCapture?.capturePath ? { fallbackCapturePath: fallbackCapture.capturePath } : {}),
1033
+ missingRequiredScreenshot,
1034
+ rawFileName: driverResult.rawFileName,
1035
+ result: driverResult,
1036
+ rootOnlyDescription,
1037
+ })
1038
+ : {}),
1039
+ ...buildArgentSelectorHealthMetadata(driverStep.selector),
1040
+ stepId: driverStep.stepId,
1041
+ },
1042
+ });
1043
+ if (fallbackCapture) {
1044
+ checks.push({
1045
+ name: 'ios_simctl_screenshot_fallback',
1046
+ status: fallbackCapture.capturePath ? 'passed' : driverStep.required ? 'failed' : 'warning',
1047
+ source: 'runner',
1048
+ code: fallbackCapture.capturePath
1049
+ ? 'ios_simctl_screenshot_fallback_completed'
1050
+ : 'ios_simctl_screenshot_fallback_failed',
1051
+ message: fallbackCapture.capturePath
1052
+ ? 'Captured iOS screenshot through simctl after Argent screenshot was unavailable.'
1053
+ : 'iOS simctl screenshot fallback failed after Argent screenshot was unavailable.',
1054
+ metadata: {
1055
+ driverAction: driverStep.driverAction,
1056
+ provider: 'ios-simctl',
1057
+ rawPath: `raw/${fallbackCapture.rawFileName}`,
1058
+ ...(fallbackCapture.capturePath ? { capturePath: fallbackCapture.capturePath } : {}),
1059
+ stepId: driverStep.stepId,
1060
+ },
1061
+ });
1062
+ }
1063
+ driverActionMetadata.push({
1064
+ args: driverResult.args,
1065
+ driverAction: driverStep.driverAction,
1066
+ exitCode: driverResult.exitCode,
1067
+ ...(stableCapturePath ? { capturePath: stableCapturePath } : {}),
1068
+ ...(fallbackCapture?.capturePath ? { captureProvider: 'ios-simctl' } : {}),
1069
+ rawPath: `raw/${driverResult.rawFileName}`,
1070
+ ...(driverStep.selector ? { selector: driverStep.selector } : {}),
1071
+ stepId: driverStep.stepId,
1072
+ });
1073
+ }
1074
+ metadata.driverActions = driverActionMetadata;
1075
+ metadata.captures = captures;
1076
+ const health = buildArgentHealth({
1077
+ checks,
1078
+ ...(executionPlan.flowId ? { flowId: executionPlan.flowId } : {}),
1079
+ runId,
1080
+ scenarioId: executionPlan.scenarioId,
1081
+ });
1082
+ const verdict = buildArgentVerdict({
1083
+ ...(executionPlan.flowId ? { flowId: executionPlan.flowId } : {}),
1084
+ health,
1085
+ runId,
1086
+ scenarioId: executionPlan.scenarioId,
1087
+ });
1088
+ const agentSummary = buildAgentSummaryMarkdown({ health, verdict });
1089
+ await Promise.all(Object.entries(raw).map(([fileName, content]) => fsp.writeFile(path.join(rawDir, fileName), `${content.trimEnd()}\n`, 'utf8')));
1090
+ await fsp.writeFile(path.join(rawDir, 'argent-metadata.json'), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
1091
+ await writeJsonArtifact({
1092
+ filePath: layout.health,
1093
+ value: health,
1094
+ schema: SCHEMAS.health,
1095
+ label: 'Health artifact',
1096
+ });
1097
+ await writeJsonArtifact({
1098
+ filePath: layout.verdict,
1099
+ value: verdict,
1100
+ schema: SCHEMAS.verdict,
1101
+ label: 'Verdict artifact',
1102
+ });
1103
+ await writeTextArtifact({
1104
+ filePath: layout.agentSummary,
1105
+ content: agentSummary,
1106
+ });
1107
+ return {
1108
+ agentSummary,
1109
+ captures,
1110
+ health,
1111
+ metadata,
1112
+ raw,
1113
+ runDir,
1114
+ verdict,
1115
+ };
1116
+ }
1117
+ /**
1118
+ * Runs the Argent capture CLI.
1119
+ *
1120
+ * @returns {Promise<void>}
1121
+ */
1122
+ async function main() {
1123
+ const argv = process.argv.slice(2);
1124
+ if (hasHelpFlag(argv)) {
1125
+ usage(process.stdout);
1126
+ return;
1127
+ }
1128
+ loadAslLocalEnv();
1129
+ const args = parseArgs(argv);
1130
+ const argentCommand = readStringArgOrEnv(args.argent, ['ASL_ARGENT_BIN']);
1131
+ const baseArgs = parseBaseArgs(readStringArgOrEnv(args['base-args'], ['ASL_ARGENT_BASE_ARGS']));
1132
+ const commandTimeoutMs = readPositiveInteger(readStringArgOrEnv(args['command-timeout-ms'], ['ASL_ARGENT_COMMAND_TIMEOUT_MS']), 60_000);
1133
+ if (args.check === true || args.check === 'true') {
1134
+ const result = await checkArgentAvailability({
1135
+ ...(argentCommand ? { argentCommand } : {}),
1136
+ ...(baseArgs ? { baseArgs } : {}),
1137
+ commandTimeoutMs,
1138
+ });
1139
+ if (typeof args.out === 'string') {
1140
+ await writeArgentAvailabilityArtifacts({
1141
+ outputDir: args.out,
1142
+ result,
1143
+ ...(typeof args['run-id'] === 'string' ? { runId: args['run-id'] } : {}),
1144
+ });
1145
+ }
1146
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1147
+ if (result.status !== 'passed') {
1148
+ process.exitCode = 1;
1149
+ }
1150
+ return;
1151
+ }
1152
+ if (typeof args.platform !== 'string' || typeof args.scenario !== 'string') {
1153
+ usage();
1154
+ process.exitCode = 1;
1155
+ return;
1156
+ }
1157
+ if (!['android', 'ios'].includes(args.platform)) {
1158
+ throw new Error('--platform must be one of android or ios.');
1159
+ }
1160
+ const platform = args.platform;
1161
+ const app = readStringArgOrEnv(args.app, platform === 'ios'
1162
+ ? ['ASL_IOS_APP_ID', 'ASL_EXAMPLE_IOS_APP_ID']
1163
+ : ['ASL_ANDROID_APP_ID', 'ASL_EXAMPLE_ANDROID_APP_ID']);
1164
+ const envDevice = readStringArgOrEnv(undefined, platform === 'ios'
1165
+ ? ['ASL_IOS_UDID', 'ASL_EXAMPLE_IOS_UDID']
1166
+ : ['ASL_ANDROID_SERIAL', 'ASL_EXAMPLE_ANDROID_SERIAL']);
1167
+ const deviceFlag = readStringArgOrEnv(args['device-flag'], ['ASL_ARGENT_DEVICE_FLAG']);
1168
+ const appFlag = readStringArgOrEnv(args['app-flag'], ['ASL_ARGENT_APP_FLAG']);
1169
+ const xcrunPath = readStringArgOrEnv(args.xcrun, ['ASL_XCRUN_PATH', 'ASL_IOS_XCRUN_BIN']);
1170
+ const deviceId = typeof args.device === 'string'
1171
+ ? args.device
1172
+ : platform === 'ios' && typeof args.udid === 'string'
1173
+ ? args.udid
1174
+ : platform === 'android' && typeof args.serial === 'string'
1175
+ ? args.serial
1176
+ : envDevice ?? null;
1177
+ if (!deviceId) {
1178
+ usage();
1179
+ process.exitCode = 1;
1180
+ return;
1181
+ }
1182
+ const screenSize = readScreenSize({ height: args['screen-height'], width: args['screen-width'] });
1183
+ const result = await runArgentCapture({
1184
+ ...(app ? { app } : {}),
1185
+ ...(appFlag ? { appFlag } : {}),
1186
+ ...(argentCommand ? { argentCommand } : {}),
1187
+ ...(baseArgs ? { baseArgs } : {}),
1188
+ commandTimeoutMs,
1189
+ ...(deviceFlag ? { deviceFlag } : {}),
1190
+ deviceId,
1191
+ iosSimctlScreenshotFallback: args['ios-simctl-screenshot-fallback'] === true ||
1192
+ readBooleanArgOrEnv(args['ios-simctl-screenshot-fallback'], ['ASL_ARGENT_IOS_SIMCTL_SCREENSHOT_FALLBACK']),
1193
+ ...(typeof args.out === 'string' ? { outputDir: args.out } : {}),
1194
+ platform,
1195
+ ...(typeof args['run-id'] === 'string' ? { runId: args['run-id'] } : {}),
1196
+ scenario: readJson(path.resolve(args.scenario)),
1197
+ ...(screenSize ? { screenSize } : {}),
1198
+ waitMs: readPositiveInteger(args['wait-ms'], 0),
1199
+ ...(xcrunPath ? { xcrunPath } : {}),
1200
+ });
1201
+ process.stdout.write(`${result.runDir}\n`);
1202
+ if (result.health.healthStatus !== 'passed') {
1203
+ process.exitCode = 1;
1204
+ }
1205
+ }
1206
+ if (require.main === module) {
1207
+ main().catch((error) => {
1208
+ console.error(error instanceof Error ? error.message : String(error));
1209
+ process.exitCode = 1;
1210
+ });
1211
+ }