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,471 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.assertAggregatePassed = assertAggregatePassed;
5
+ exports.buildLiveRunId = buildLiveRunId;
6
+ exports.formatResult = formatResult;
7
+ exports.assertNoRegressedComparisons = assertNoRegressedComparisons;
8
+ exports.buildSkippedInteractionProofs = buildSkippedInteractionProofs;
9
+ exports.buildInteractionComparisonLane = buildInteractionComparisonLane;
10
+ exports.isTrustedProfileRun = isTrustedProfileRun;
11
+ exports.main = main;
12
+ exports.normalizeRunSuffix = normalizeRunSuffix;
13
+ exports.resolveIosDeviceId = resolveIosDeviceId;
14
+ exports.runExampleIosLiveProof = runExampleIosLiveProof;
15
+ exports.usage = usage;
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+ const { hasHelpFlag, writeUsage } = require('./cli');
19
+ const { parsePositiveInteger, parseArgs, runIosSimctlCapture, } = require('./ios-simctl');
20
+ const { compareLiveProfilesToLatest, isEnabledFlag, } = require('./live-comparison');
21
+ const { writeLiveProofSummary } = require('./live-proof-summary');
22
+ const { runAgentDeviceCapture } = require('./agent-device');
23
+ const { parseBaseArgs: parseArgentBaseArgs, runArgentCapture } = require('./argent');
24
+ const { loadAslLocalEnv, readStringArgOrEnv } = require('./local-env');
25
+ const { runProfileIos } = require('./profile-ios');
26
+ const EXAMPLE_PROFILES = [
27
+ {
28
+ label: 'startup',
29
+ runId: 'ios-live-startup',
30
+ scenario: 'app-startup.json',
31
+ scenarioId: 'app-startup',
32
+ },
33
+ {
34
+ label: 'open-close',
35
+ runId: 'ios-live-open-close',
36
+ scenario: 'open-close-cycle.json',
37
+ scenarioId: 'open-close-cycle',
38
+ },
39
+ {
40
+ label: 'scroll',
41
+ runId: 'ios-live-scroll',
42
+ scenario: 'scroll-settle.json',
43
+ scenarioId: 'scroll-settle',
44
+ },
45
+ ];
46
+ /**
47
+ * Prints CLI usage.
48
+ *
49
+ * @param {{write: (message: string) => unknown}} [output]
50
+ * @returns {void}
51
+ */
52
+ function usage(output = process.stderr) {
53
+ writeUsage([
54
+ 'Usage: asl-example-ios-live [--config <path>] [--out <dir>] [--bundle <id>] [--device <udid|booted>] [--xcrun <path>] [--ios-dev-client-url <url>] [--run-suffix <label>] [--compare-latest] [--fail-on-regression] [--agent-device-proof] [--argent-proof]',
55
+ '',
56
+ 'Runs the packaged example iOS live proof: simctl preflight, startup, open-close, and scroll-settle.',
57
+ 'The example app must already be installed on a booted iOS simulator and connected to Metro. Set ASL_EXAMPLE_IOS_DEV_CLIENT_URL for Expo dev-client builds that need an explicit Metro URL.',
58
+ 'Use --run-suffix to preserve multiple live proof artifact sets without changing deterministic default run ids.',
59
+ 'Use --compare-latest to compare each passed scenario against the latest trusted prior run under the artifact root.',
60
+ 'Use --fail-on-regression with --compare-latest to exit nonzero after writing evidence when any comparison regressed.',
61
+ 'Use --agent-device-proof to attach the shared startup UI assertion through agent-device; pass --agent-device-session-mode bind when a named session should still receive the configured UDID.',
62
+ 'Use --argent-proof to attach the shared startup UI assertion through Argent; set ASL_ARGENT_BIN and ASL_ARGENT_BASE_ARGS for non-global installs. iOS Argent screenshots fall back to simctl when Argent screenshot is unavailable.',
63
+ ], output);
64
+ }
65
+ /**
66
+ * Resolves the package root for source and built CLI execution.
67
+ *
68
+ * @returns {string}
69
+ */
70
+ function defaultPackageRoot() {
71
+ return path.resolve(__dirname, '..', '..');
72
+ }
73
+ /**
74
+ * Reads a JSON object from disk.
75
+ *
76
+ * @param {string} filePath
77
+ * @returns {Record<string, unknown>}
78
+ */
79
+ function readJson(filePath) {
80
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
81
+ }
82
+ /**
83
+ * Reads a nested object property from unknown JSON.
84
+ *
85
+ * @param {Record<string, unknown>} value
86
+ * @param {string} key
87
+ * @returns {Record<string, unknown> | null}
88
+ */
89
+ function readObjectProperty(value, key) {
90
+ const property = value[key];
91
+ return property && typeof property === 'object' && !Array.isArray(property)
92
+ ? property
93
+ : null;
94
+ }
95
+ /**
96
+ * Resolves the iOS bundle id from explicit input or ASL config.
97
+ *
98
+ * @param {{args: CliArgs, config: Record<string, unknown>}} options
99
+ * @returns {string | null}
100
+ */
101
+ function resolveIosBundleId({ args, config, }) {
102
+ if (typeof args.bundle === 'string') {
103
+ return args.bundle;
104
+ }
105
+ const app = readObjectProperty(config, 'app');
106
+ return typeof app?.iosBundleId === 'string' ? app.iosBundleId : null;
107
+ }
108
+ /**
109
+ * Resolves optional sibling iOS bundle ids that make simulator targeting ambiguous.
110
+ *
111
+ * @param {Record<string, unknown>} config
112
+ * @returns {string[]}
113
+ */
114
+ function resolveIosConflictingBundleIds(config) {
115
+ const app = readObjectProperty(config, 'app');
116
+ const configured = app?.iosConflictingBundleIds;
117
+ return Array.isArray(configured)
118
+ ? configured.filter((value) => typeof value === 'string' && value.trim().length > 0)
119
+ : [];
120
+ }
121
+ /**
122
+ * Resolves the simulator target used by every iOS proof lane.
123
+ *
124
+ * @param {CliArgs} args
125
+ * @returns {string}
126
+ */
127
+ function resolveIosDeviceId(args) {
128
+ return readStringArgOrEnv(args.device, ['ASL_IOS_UDID', 'ASL_EXAMPLE_IOS_UDID']) ?? 'booted';
129
+ }
130
+ /**
131
+ * Converts a caller-provided run suffix into a path-safe run-id segment.
132
+ *
133
+ * @param {unknown} value
134
+ * @returns {string | null}
135
+ */
136
+ function normalizeRunSuffix(value) {
137
+ if (typeof value !== 'string') {
138
+ return null;
139
+ }
140
+ const normalized = value
141
+ .trim()
142
+ .toLowerCase()
143
+ .replace(/[^a-z0-9._-]+/gu, '-')
144
+ .replace(/^-+|-+$/gu, '')
145
+ .slice(0, 64);
146
+ return normalized.length > 0 ? normalized : null;
147
+ }
148
+ /**
149
+ * Applies the optional run suffix while preserving deterministic defaults.
150
+ *
151
+ * @param {string} baseRunId
152
+ * @param {string | null} suffix
153
+ * @returns {string}
154
+ */
155
+ function buildLiveRunId(baseRunId, suffix) {
156
+ return suffix ? `${baseRunId}-${suffix}` : baseRunId;
157
+ }
158
+ /**
159
+ * Builds the comparison lane suffix for enabled interaction proofs.
160
+ *
161
+ * @param {string[]} runnerIds
162
+ * @returns {string}
163
+ */
164
+ function buildInteractionComparisonLane(runnerIds) {
165
+ return runnerIds.length > 0
166
+ ? `example-ios-live+${runnerIds.join('+')}`
167
+ : 'example-ios-live';
168
+ }
169
+ /**
170
+ * Reports whether profile evidence is healthy enough to trust sidecar proofs and comparisons.
171
+ *
172
+ * @param {{health: Record<string, unknown>, verdict: Record<string, unknown>}} run
173
+ * @returns {boolean}
174
+ */
175
+ function isTrustedProfileRun({ health, verdict, }) {
176
+ return health.healthStatus === 'passed' && verdict.verdictStatus === 'passed';
177
+ }
178
+ /**
179
+ * Builds skipped sidecar pointers for requested example runners when the profile batch failed.
180
+ *
181
+ * @param {{failedProfiles: IosLiveProfile[], requestedRunners: string[], runIdsByRunner: Record<string, string>}} options
182
+ * @returns {IosSkippedInteractionProof[]}
183
+ */
184
+ function buildSkippedInteractionProofs({ failedProfiles, requestedRunners, runIdsByRunner, }) {
185
+ if (failedProfiles.length === 0) {
186
+ return [];
187
+ }
188
+ const failedSummary = failedProfiles
189
+ .map((profile) => `${profile.label}=health:${profile.healthStatus ?? 'unknown'}/verdict:${profile.verdictStatus ?? 'unknown'}`)
190
+ .join(', ');
191
+ const reason = `Profile batch failed (${failedSummary}); sidecar interaction proof was skipped because aggregate runner evidence would not be trustworthy.`;
192
+ const labelsByRunner = {
193
+ 'agent-device': 'startup-ui',
194
+ argent: 'startup-ui-argent',
195
+ };
196
+ return requestedRunners.map((runnerId) => ({
197
+ label: labelsByRunner[runnerId] ?? `startup-ui-${runnerId}`,
198
+ nextAction: {
199
+ code: 'fix_profile_gate',
200
+ summary: 'Inspect the failed profile health and verdict before rerunning sidecar interaction proofs.',
201
+ },
202
+ reason,
203
+ runId: runIdsByRunner[runnerId] ?? `ios-${runnerId}-startup`,
204
+ runnerId,
205
+ scenarioId: 'app-startup',
206
+ }));
207
+ }
208
+ /**
209
+ * Throws after aggregate proof writing when the live proof itself failed.
210
+ *
211
+ * @param {IosLiveProofResult} result
212
+ * @returns {void}
213
+ */
214
+ function assertAggregatePassed(result) {
215
+ const proof = readJson(result.aggregateSummary.liveProofPath);
216
+ if (proof.status === 'passed') {
217
+ return;
218
+ }
219
+ throw new Error(`iOS example live proof failed. Inspect ${result.aggregateSummary.summaryPath}.`);
220
+ }
221
+ /**
222
+ * Throws after aggregate proof writing when fail-on-regression should gate the run.
223
+ *
224
+ * @param {RegressionGateOptions} options
225
+ * @returns {void}
226
+ */
227
+ function assertNoRegressedComparisons({ platformLabel, result, }) {
228
+ const regressed = result.comparisons.filter((comparison) => comparison.status === 'worse');
229
+ if (regressed.length === 0) {
230
+ return;
231
+ }
232
+ const labels = regressed.map((comparison) => comparison.label).join(', ');
233
+ throw new Error(`${platformLabel} example live proof found regressed comparison(s): ${labels}. Inspect ${result.aggregateSummary.summaryPath}.`);
234
+ }
235
+ /**
236
+ * Runs simctl preflight plus all canonical iOS example live profiles.
237
+ *
238
+ * @param {CliArgs} args
239
+ * @param {IosLiveProofOptions} [options]
240
+ * @returns {Promise<IosLiveProofResult>}
241
+ */
242
+ async function runExampleIosLiveProof(args, options = {}) {
243
+ const packageRoot = options.packageRoot ?? defaultPackageRoot();
244
+ const exampleRoot = path.join(packageRoot, 'examples', 'mobile-app');
245
+ const configPath = typeof args.config === 'string'
246
+ ? path.resolve(args.config)
247
+ : path.join(exampleRoot, 'asl.config.json');
248
+ const outputDir = typeof args.out === 'string'
249
+ ? path.resolve(args.out)
250
+ : path.resolve('artifacts/example-mobile-app/ios');
251
+ const config = readJson(configPath);
252
+ const bundleId = resolveIosBundleId({ args, config });
253
+ const deviceId = resolveIosDeviceId(args);
254
+ const agentDeviceSession = readStringArgOrEnv(args['agent-device-session'], [
255
+ 'ASL_IOS_AGENT_DEVICE_SESSION',
256
+ 'ASL_EXAMPLE_IOS_AGENT_DEVICE_SESSION',
257
+ ]);
258
+ const agentDeviceSessionMode = readStringArgOrEnv(args['agent-device-session-mode'], [
259
+ 'ASL_IOS_AGENT_DEVICE_SESSION_MODE',
260
+ 'ASL_EXAMPLE_IOS_AGENT_DEVICE_SESSION_MODE',
261
+ ]);
262
+ const runSuffix = normalizeRunSuffix(args['run-suffix']);
263
+ const iosDevClientUrl = readStringArgOrEnv(args['ios-dev-client-url'], [
264
+ 'ASL_IOS_DEV_CLIENT_URL',
265
+ 'ASL_EXAMPLE_IOS_DEV_CLIENT_URL',
266
+ ]);
267
+ const iosDevClientWaitMs = readStringArgOrEnv(args['ios-dev-client-wait-ms'], [
268
+ 'ASL_IOS_DEV_CLIENT_WAIT_MS',
269
+ 'ASL_EXAMPLE_IOS_DEV_CLIENT_WAIT_MS',
270
+ ]);
271
+ const aggregateRunId = buildLiveRunId('ios-live-proof', runSuffix);
272
+ const preflightRunId = buildLiveRunId('ios-live-preflight', runSuffix);
273
+ const agentDeviceRunId = buildLiveRunId('ios-agent-device-startup', runSuffix);
274
+ const argentRunId = buildLiveRunId('ios-argent-startup', runSuffix);
275
+ const enabledInteractionRunners = [
276
+ ...(isEnabledFlag(args['agent-device-proof']) ? ['agent-device'] : []),
277
+ ...(isEnabledFlag(args['argent-proof']) ? ['argent'] : []),
278
+ ];
279
+ const comparisonLane = buildInteractionComparisonLane(enabledInteractionRunners);
280
+ const runIdsByRunner = {
281
+ 'agent-device': agentDeviceRunId,
282
+ argent: argentRunId,
283
+ };
284
+ const preflightDir = path.join(outputDir, '_preflight', preflightRunId);
285
+ const preflight = await runIosSimctlCapture({
286
+ bundleId,
287
+ conflictingBundleIds: resolveIosConflictingBundleIds(config),
288
+ device: deviceId,
289
+ ...(options.executor ? { executor: options.executor } : {}),
290
+ outputDir: preflightDir,
291
+ runId: preflightRunId,
292
+ ...(typeof args.xcrun === 'string' ? { xcrunPath: args.xcrun } : {}),
293
+ });
294
+ if (preflight.health.healthStatus !== 'passed') {
295
+ throw new Error(`iOS live proof preflight failed; inspect ${preflight.runDir}/agent-summary.md.`);
296
+ }
297
+ const interactionProofs = [];
298
+ const profiles = [];
299
+ const failedProfiles = [];
300
+ for (const profile of EXAMPLE_PROFILES) {
301
+ const profileRunId = buildLiveRunId(profile.runId, runSuffix);
302
+ const result = await runProfileIos({
303
+ config: configPath,
304
+ device: deviceId,
305
+ ...(typeof args['log-last'] === 'string' ? { 'log-last': args['log-last'] } : {}),
306
+ launch: true,
307
+ out: outputDir,
308
+ ...(iosDevClientUrl ? { 'ios-dev-client-url': iosDevClientUrl } : {}),
309
+ ...(iosDevClientWaitMs ? { 'ios-dev-client-wait-ms': iosDevClientWaitMs } : {}),
310
+ 'profile-session': true,
311
+ 'profile-session-storage': true,
312
+ 'run-id': profileRunId,
313
+ scenario: path.join(exampleRoot, 'scenarios', 'mobile', profile.scenario),
314
+ 'simctl-capture': true,
315
+ 'simctl-out': path.join(outputDir, '_ios-simctl-captures', profileRunId),
316
+ ...(typeof args['wait-ms'] === 'string' ? { 'wait-ms': args['wait-ms'] } : {}),
317
+ ...(bundleId ? { bundle: bundleId } : {}),
318
+ ...(typeof args.xcrun === 'string' ? { xcrun: args.xcrun } : {}),
319
+ }, {
320
+ comparisonLane,
321
+ ...(options.delay ? { delay: options.delay } : {}),
322
+ ...(options.executor ? { executor: options.executor } : {}),
323
+ });
324
+ const profilePointer = {
325
+ healthStatus: typeof result.health.healthStatus === 'string' ? result.health.healthStatus : 'unknown',
326
+ label: profile.label,
327
+ runDir: result.runDir,
328
+ runId: profileRunId,
329
+ scenario: profile.scenario,
330
+ scenarioId: profile.scenarioId,
331
+ verdictStatus: typeof result.verdict.verdictStatus === 'string' ? result.verdict.verdictStatus : 'unknown',
332
+ };
333
+ profiles.push(profilePointer);
334
+ if (!isTrustedProfileRun({ health: result.health, verdict: result.verdict })) {
335
+ failedProfiles.push(profilePointer);
336
+ }
337
+ }
338
+ const skippedInteractionProofs = buildSkippedInteractionProofs({
339
+ failedProfiles,
340
+ requestedRunners: enabledInteractionRunners,
341
+ runIdsByRunner,
342
+ });
343
+ const profileBatchTrusted = failedProfiles.length === 0;
344
+ if (profileBatchTrusted && isEnabledFlag(args['agent-device-proof'])) {
345
+ const agentDeviceCapture = await runAgentDeviceCapture({
346
+ ...(typeof args['agent-device'] === 'string' ? { agentDevicePath: args['agent-device'] } : {}),
347
+ app: bundleId,
348
+ ...(options.agentDeviceExecutor ? { executor: options.agentDeviceExecutor } : {}),
349
+ open: true,
350
+ outputDir: path.join(outputDir, '_agent-device-captures', agentDeviceRunId),
351
+ platform: 'ios',
352
+ runId: agentDeviceRunId,
353
+ scenario: readJson(path.join(exampleRoot, 'scenarios', 'mobile', 'app-startup.json')),
354
+ ...(agentDeviceSession ? { session: agentDeviceSession } : {}),
355
+ ...(agentDeviceSessionMode
356
+ ? { sessionMode: agentDeviceSessionMode }
357
+ : {}),
358
+ udid: deviceId,
359
+ waitMs: parsePositiveInteger(args['agent-device-wait-ms'], 1000),
360
+ });
361
+ interactionProofs.push({
362
+ label: 'startup-ui',
363
+ runDir: agentDeviceCapture.runDir,
364
+ runId: agentDeviceRunId,
365
+ runnerId: 'agent-device',
366
+ scenarioId: 'app-startup',
367
+ });
368
+ }
369
+ if (profileBatchTrusted && isEnabledFlag(args['argent-proof'])) {
370
+ const argentBaseArgs = parseArgentBaseArgs(process.env.ASL_ARGENT_BASE_ARGS);
371
+ const argentCapture = await runArgentCapture({
372
+ app: bundleId,
373
+ argentCommand: process.env.ASL_ARGENT_BIN || 'argent',
374
+ ...(argentBaseArgs ? { baseArgs: argentBaseArgs } : {}),
375
+ commandTimeoutMs: parsePositiveInteger(process.env.ASL_ARGENT_COMMAND_TIMEOUT_MS, 60_000),
376
+ deviceId,
377
+ ...(options.delay ? { delay: options.delay } : {}),
378
+ ...(options.argentExecutor ? { executor: options.argentExecutor } : {}),
379
+ iosSimctlScreenshotFallback: true,
380
+ ...(options.executor ? { iosSimctlExecutor: options.executor } : {}),
381
+ outputDir: path.join(outputDir, '_argent-captures', argentRunId),
382
+ platform: 'ios',
383
+ runId: argentRunId,
384
+ scenario: readJson(path.join(exampleRoot, 'scenarios', 'mobile', 'app-startup.json')),
385
+ });
386
+ interactionProofs.push({
387
+ label: 'startup-ui-argent',
388
+ runDir: argentCapture.runDir,
389
+ runId: argentRunId,
390
+ runnerId: 'argent',
391
+ scenarioId: 'app-startup',
392
+ });
393
+ }
394
+ const comparisons = profileBatchTrusted && isEnabledFlag(args['compare-latest'])
395
+ ? await compareLiveProfilesToLatest({ outputDir, profiles })
396
+ : [];
397
+ const aggregateSummary = await writeLiveProofSummary({
398
+ comparisons,
399
+ interactionProofs,
400
+ outputDir,
401
+ platform: 'ios',
402
+ preflightDir: preflight.runDir,
403
+ preflightRunId,
404
+ profiles,
405
+ runId: aggregateRunId,
406
+ skippedInteractionProofs,
407
+ });
408
+ const result = {
409
+ aggregateSummary,
410
+ comparisons,
411
+ interactionProofs,
412
+ outputDir,
413
+ preflightDir: preflight.runDir,
414
+ profiles,
415
+ skippedInteractionProofs,
416
+ };
417
+ if (isEnabledFlag(args['fail-on-regression'])) {
418
+ assertNoRegressedComparisons({
419
+ platformLabel: 'iOS',
420
+ result,
421
+ });
422
+ }
423
+ assertAggregatePassed(result);
424
+ return result;
425
+ }
426
+ /**
427
+ * Formats the live proof result for humans and agents.
428
+ *
429
+ * @param {IosLiveProofResult} result
430
+ * @returns {string}
431
+ */
432
+ function formatResult(result) {
433
+ return [
434
+ 'iOS example live proof passed.',
435
+ `Live proof: ${result.aggregateSummary.summaryPath}`,
436
+ `Preflight: ${result.preflightDir}/agent-summary.md`,
437
+ ...result.profiles.map((profile) => (`${profile.label}: ${profile.runDir}/agent-summary.md`)),
438
+ ...result.interactionProofs.map((proof) => (`${proof.label}: ${proof.runDir}/agent-summary.md`)),
439
+ ...(result.comparisons.length > 0
440
+ ? [
441
+ 'Comparisons:',
442
+ ...result.comparisons.map((comparison) => (comparison.status === 'skipped'
443
+ ? `${comparison.label}: skipped - ${comparison.reason}`
444
+ : `${comparison.label}: ${comparison.status} ${comparison.summaryPath}`)),
445
+ ]
446
+ : []),
447
+ `Artifact root: ${result.outputDir}`,
448
+ ].join('\n');
449
+ }
450
+ /**
451
+ * Runs the example iOS live proof CLI.
452
+ *
453
+ * @returns {Promise<void>}
454
+ */
455
+ async function main() {
456
+ const argv = process.argv.slice(2);
457
+ if (hasHelpFlag(argv)) {
458
+ usage(process.stdout);
459
+ return;
460
+ }
461
+ loadAslLocalEnv();
462
+ const args = parseArgs(argv);
463
+ const result = await runExampleIosLiveProof(args);
464
+ process.stdout.write(`${formatResult(result)}\n`);
465
+ }
466
+ if (require.main === module) {
467
+ main().catch((error) => {
468
+ console.error(error instanceof Error ? error.message : String(error));
469
+ process.exitCode = 1;
470
+ });
471
+ }
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ type HostDoctorRequirement = 'agent-device' | 'android' | 'argent' | 'ios';
3
+ type CliArgs = {
4
+ adb?: string | boolean;
5
+ 'agent-device'?: string | boolean;
6
+ 'agent-device-require-platforms'?: string | boolean;
7
+ 'android-package'?: string | boolean;
8
+ 'android-serial'?: string | boolean;
9
+ argent?: string | boolean;
10
+ 'base-args'?: string | boolean;
11
+ 'command-timeout-ms'?: string | boolean;
12
+ 'ios-bundle'?: string | boolean;
13
+ 'ios-device'?: string | boolean;
14
+ out?: string | boolean;
15
+ require?: string | boolean;
16
+ 'run-id'?: string | boolean;
17
+ xcrun?: string | boolean;
18
+ [key: string]: string | boolean | undefined;
19
+ };
20
+ type HostDoctorChildResult = {
21
+ health?: Record<string, unknown>;
22
+ runDir: string;
23
+ verdict?: Record<string, unknown>;
24
+ };
25
+ type HostDoctorOptions = {
26
+ adbPath?: string;
27
+ agentDevicePath?: string;
28
+ agentDeviceRequiredPlatforms?: import('./agent-device-driver').AgentDevicePlatform[];
29
+ androidPackageName?: string | null;
30
+ androidPreflight?: (options: Record<string, unknown>) => Promise<HostDoctorChildResult>;
31
+ androidSerial?: string | null;
32
+ argentAvailability?: (options: Record<string, unknown>) => Promise<Record<string, unknown>>;
33
+ argentCommand?: string;
34
+ argentBaseArgs?: string[];
35
+ commandTimeoutMs?: number;
36
+ iosBundleId?: string | null;
37
+ iosDevice?: string | null;
38
+ iosPreflight?: (options: Record<string, unknown>) => Promise<HostDoctorChildResult>;
39
+ outputDir?: string;
40
+ requirements?: HostDoctorRequirement[];
41
+ runId?: string;
42
+ agentDeviceAvailability?: (options: Record<string, unknown>) => Promise<Record<string, unknown>>;
43
+ xcrunPath?: string;
44
+ };
45
+ type HostDoctorCheck = {
46
+ code: string;
47
+ message: string;
48
+ metadata?: Record<string, string | number | boolean | null>;
49
+ name: string;
50
+ source: 'runner';
51
+ status: 'failed' | 'passed';
52
+ };
53
+ type HostDoctorResult = {
54
+ agentSummary: string;
55
+ health: Record<string, unknown>;
56
+ raw: Record<string, unknown>;
57
+ runDir: string;
58
+ verdict: Record<string, unknown>;
59
+ };
60
+ /**
61
+ * Prints CLI usage.
62
+ *
63
+ * @param {{write: (message: string) => unknown}} output
64
+ * @returns {void}
65
+ */
66
+ declare function usage(output?: {
67
+ write: (message: string) => unknown;
68
+ }): void;
69
+ /**
70
+ * Parses `--key value` CLI arguments.
71
+ *
72
+ * @param {string[]} argv
73
+ * @returns {CliArgs}
74
+ */
75
+ declare function parseArgs(argv: string[]): CliArgs;
76
+ /**
77
+ * Parses the comma-separated host doctor requirements.
78
+ *
79
+ * @param {unknown} value
80
+ * @returns {HostDoctorRequirement[]}
81
+ */
82
+ declare function parseRequirements(value: unknown): HostDoctorRequirement[];
83
+ /**
84
+ * Builds a scalar ASL health check from a child preflight run.
85
+ *
86
+ * @param {{health: Record<string, unknown>, label: string, name: string, runDir: string}} options
87
+ * @returns {HostDoctorCheck}
88
+ */
89
+ declare function buildChildRunCheck({ health, label, name, runDir, }: {
90
+ health: Record<string, unknown>;
91
+ label: string;
92
+ name: string;
93
+ runDir: string;
94
+ }): HostDoctorCheck;
95
+ /**
96
+ * Builds a scalar ASL health check from a command-surface availability result.
97
+ *
98
+ * @param {{label: string, name: string, rawPath: string, result: Record<string, unknown>}} options
99
+ * @returns {HostDoctorCheck}
100
+ */
101
+ declare function buildAvailabilityCheck({ label, name, rawPath, result, }: {
102
+ label: string;
103
+ name: string;
104
+ rawPath: string;
105
+ result: Record<string, unknown>;
106
+ }): HostDoctorCheck;
107
+ /**
108
+ * Builds a host-specific agent summary for live-proof readiness.
109
+ *
110
+ * @param {{health: Record<string, unknown>, verdict: Record<string, unknown>}} options
111
+ * @returns {string}
112
+ */
113
+ declare function buildHostDoctorSummary({ health, verdict, }: {
114
+ health: Record<string, unknown>;
115
+ verdict: Record<string, unknown>;
116
+ }): string;
117
+ /**
118
+ * Runs host/device preflight checks and writes aggregate ASL artifacts.
119
+ *
120
+ * @param {HostDoctorOptions} options
121
+ * @returns {Promise<HostDoctorResult>}
122
+ */
123
+ declare function runHostDoctor({ adbPath, agentDeviceAvailability, agentDevicePath, agentDeviceRequiredPlatforms, androidPackageName, androidPreflight, androidSerial, argentAvailability, argentBaseArgs, argentCommand, commandTimeoutMs, iosBundleId, iosDevice, iosPreflight, outputDir, requirements, runId, xcrunPath, }?: HostDoctorOptions): Promise<HostDoctorResult>;
124
+ /**
125
+ * Runs the host doctor CLI.
126
+ *
127
+ * @returns {Promise<void>}
128
+ */
129
+ declare function main(): Promise<void>;
130
+ export { buildAvailabilityCheck, buildChildRunCheck, buildHostDoctorSummary, main, parseArgs, parseRequirements, runHostDoctor, usage, };
131
+ export type { CliArgs, HostDoctorCheck, HostDoctorOptions, HostDoctorRequirement, HostDoctorResult, };