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,137 @@
1
+ #!/usr/bin/env node
2
+ type CliArgs = import('./android-adb').CliArgs;
3
+ type AndroidGenericLiveOptions = {
4
+ agentDeviceExecutor?: import('./agent-device').CommandExecutor;
5
+ argentExecutor?: import('./argent').CommandExecutor;
6
+ delay?: (ms: number) => Promise<void>;
7
+ executor?: import('./android-adb').CommandExecutor;
8
+ };
9
+ type AndroidGenericLiveResult = {
10
+ aggregateSummary: import('./live-proof-summary').LiveProofSummaryResult;
11
+ outputDir: string;
12
+ preflightDir: string;
13
+ profileDir: string;
14
+ };
15
+ type SkippedInteractionProof = import('./live-proof-summary').LiveProofSkippedInteractionProofPointer;
16
+ /**
17
+ * Prints CLI usage.
18
+ *
19
+ * @param {{write: (message: string) => unknown}} [output]
20
+ * @returns {void}
21
+ */
22
+ declare function usage(output?: {
23
+ write: (message: string) => unknown;
24
+ }): void;
25
+ /**
26
+ * Reads a JSON object from disk.
27
+ *
28
+ * @param {string} filePath
29
+ * @returns {Record<string, unknown>}
30
+ */
31
+ declare function readJson(filePath: string): Record<string, unknown>;
32
+ /**
33
+ * Resolves a stable scenario id from a scenario file.
34
+ *
35
+ * @param {{scenario: Record<string, unknown>, scenarioPath: string}} options
36
+ * @returns {string}
37
+ */
38
+ declare function resolveScenarioId({ scenario, scenarioPath, }: {
39
+ scenario: Record<string, unknown>;
40
+ scenarioPath: string;
41
+ }): string;
42
+ /**
43
+ * Resolves the Android package name from explicit input or ASL config.
44
+ *
45
+ * @param {{args: CliArgs, config: Record<string, unknown>}} options
46
+ * @returns {string | null}
47
+ */
48
+ declare function resolveAndroidPackageName({ args, config, }: {
49
+ args: CliArgs;
50
+ config: Record<string, unknown>;
51
+ }): string | null;
52
+ /**
53
+ * Converts an optional run suffix into a path-safe segment.
54
+ *
55
+ * @param {unknown} value
56
+ * @returns {string | null}
57
+ */
58
+ declare function normalizeRunSuffix(value: unknown): string | null;
59
+ /**
60
+ * Applies an optional suffix to deterministic run ids.
61
+ *
62
+ * @param {string} baseRunId
63
+ * @param {string | null} suffix
64
+ * @returns {string}
65
+ */
66
+ declare function buildRunId(baseRunId: string, suffix: string | null): string;
67
+ /**
68
+ * Throws if a profile or interaction proof failed before aggregate writing.
69
+ *
70
+ * @param {{kind: string, runDir: string, health: Record<string, unknown>, verdict?: Record<string, unknown>}} options
71
+ * @returns {void}
72
+ */
73
+ declare function assertPassedRun({ health, kind, runDir, verdict, }: {
74
+ health: Record<string, unknown>;
75
+ kind: string;
76
+ runDir: string;
77
+ verdict?: Record<string, unknown>;
78
+ }): void;
79
+ /**
80
+ * Reports whether profile evidence is trusted enough to run sidecar interaction proofs.
81
+ *
82
+ * @param {{health: Record<string, unknown>, verdict: Record<string, unknown>}} run
83
+ * @returns {boolean}
84
+ */
85
+ declare function isTrustedProfileRun({ health, verdict, }: {
86
+ health: Record<string, unknown>;
87
+ verdict: Record<string, unknown>;
88
+ }): boolean;
89
+ /**
90
+ * Builds skipped sidecar pointers for requested runners when the profile gate failed.
91
+ *
92
+ * @param {{requestedRunners: string[], runIdsByRunner: Record<string, string>, scenarioId: string, profileHealthStatus: unknown, profileVerdictStatus: unknown}} options
93
+ * @returns {SkippedInteractionProof[]}
94
+ */
95
+ declare function buildSkippedInteractionProofs({ profileHealthStatus, profileVerdictStatus, requestedRunners, runIdsByRunner, scenarioId, }: {
96
+ profileHealthStatus: unknown;
97
+ profileVerdictStatus: unknown;
98
+ requestedRunners: string[];
99
+ runIdsByRunner: Record<string, string>;
100
+ scenarioId: string;
101
+ }): SkippedInteractionProof[];
102
+ /**
103
+ * Throws after aggregate writing when the live proof itself failed.
104
+ *
105
+ * @param {AndroidGenericLiveResult} result
106
+ * @returns {void}
107
+ */
108
+ declare function assertAggregatePassed(result: AndroidGenericLiveResult): void;
109
+ /**
110
+ * Throws after aggregate proof writing when fail-on-regression should gate the run.
111
+ *
112
+ * @param {{result: AndroidGenericLiveResult, comparisons: Array<{label: string, status: string}>}} options
113
+ * @returns {void}
114
+ */
115
+ declare function assertNoRegressedComparisons({ comparisons, result, }: {
116
+ comparisons: Array<{
117
+ label: string;
118
+ status: string;
119
+ }>;
120
+ result: AndroidGenericLiveResult;
121
+ }): void;
122
+ /**
123
+ * Runs a generic one-scenario Android live proof.
124
+ *
125
+ * @param {CliArgs} args
126
+ * @param {AndroidGenericLiveOptions} [options]
127
+ * @returns {Promise<AndroidGenericLiveResult>}
128
+ */
129
+ declare function runAndroidLiveProof(args: CliArgs, options?: AndroidGenericLiveOptions): Promise<AndroidGenericLiveResult>;
130
+ /**
131
+ * Runs the generic Android live CLI.
132
+ *
133
+ * @returns {Promise<void>}
134
+ */
135
+ declare function main(): Promise<void>;
136
+ export { assertNoRegressedComparisons, assertAggregatePassed, assertPassedRun, buildRunId, buildSkippedInteractionProofs, isTrustedProfileRun, main, normalizeRunSuffix, readJson, resolveAndroidPackageName, resolveScenarioId, runAndroidLiveProof, usage, };
137
+ export type { AndroidGenericLiveOptions, AndroidGenericLiveResult, };
@@ -0,0 +1,539 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.assertNoRegressedComparisons = assertNoRegressedComparisons;
5
+ exports.assertAggregatePassed = assertAggregatePassed;
6
+ exports.assertPassedRun = assertPassedRun;
7
+ exports.buildRunId = buildRunId;
8
+ exports.buildSkippedInteractionProofs = buildSkippedInteractionProofs;
9
+ exports.isTrustedProfileRun = isTrustedProfileRun;
10
+ exports.main = main;
11
+ exports.normalizeRunSuffix = normalizeRunSuffix;
12
+ exports.readJson = readJson;
13
+ exports.resolveAndroidPackageName = resolveAndroidPackageName;
14
+ exports.resolveScenarioId = resolveScenarioId;
15
+ exports.runAndroidLiveProof = runAndroidLiveProof;
16
+ exports.usage = usage;
17
+ const fs = require('node:fs');
18
+ const path = require('node:path');
19
+ const { writeJsonArtifact } = require('../core/artifact-writer');
20
+ const { SCHEMAS } = require('../core/schema-validator');
21
+ const { hasHelpFlag, writeUsage } = require('./cli');
22
+ const { execFileCommand, parseArgs, parsePositiveInteger, runAndroidAdbPreflight } = require('./android-adb');
23
+ const { compareLiveProfilesToLatest, isEnabledFlag } = require('./live-comparison');
24
+ const { writeLiveProofSummary } = require('./live-proof-summary');
25
+ const { runAgentDeviceCapture } = require('./agent-device');
26
+ const { parseBaseArgs: parseArgentBaseArgs, runArgentCapture } = require('./argent');
27
+ const { loadAslLocalEnv, readStringArgOrEnv } = require('./local-env');
28
+ const { runProfileAndroid } = require('./profile-android');
29
+ /**
30
+ * Prints CLI usage.
31
+ *
32
+ * @param {{write: (message: string) => unknown}} [output]
33
+ * @returns {void}
34
+ */
35
+ function usage(output = process.stderr) {
36
+ writeUsage([
37
+ 'Usage: asl-live-android --config <path> --scenario <path> [--out <dir>] [--package <name>] [--serial <device>] [--run-id <id>] [--run-suffix <label>] [--compare-latest] [--fail-on-regression] [--agent-device-proof] [--argent-proof]',
38
+ '',
39
+ 'Runs one generic Android live proof: adb preflight, profile-session adb capture, optional sidecars, optional latest-trusted comparison, and aggregate live-proof artifacts.',
40
+ 'Use --android-dev-client-url <url> [--android-dev-client-wait-ms <ms>] [--android-dev-client-ready-pattern <pattern>] for Expo dev-client shells that must open a Metro session before profile-session deep links.',
41
+ 'Use --android-profile-session-storage [--android-profile-session-storage-key <key>] [--android-profile-command-storage-key <key>] when app startup control must be delivered through Android AsyncStorage.',
42
+ 'Use --agent-device-proof to attach scenario-declared portable driver actions through agent-device; pass --agent-device-session-mode bind when a named session should still receive the configured serial.',
43
+ 'Use --argent-proof to attach scenario-declared Argent-compatible driver actions; set ASL_ARGENT_BIN and ASL_ARGENT_BASE_ARGS for non-global installs.',
44
+ ], output);
45
+ }
46
+ /**
47
+ * Reads a JSON object from disk.
48
+ *
49
+ * @param {string} filePath
50
+ * @returns {Record<string, unknown>}
51
+ */
52
+ function readJson(filePath) {
53
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
54
+ }
55
+ /**
56
+ * Reads a nested object property from unknown JSON.
57
+ *
58
+ * @param {Record<string, unknown>} value
59
+ * @param {string} key
60
+ * @returns {Record<string, unknown> | null}
61
+ */
62
+ function readObjectProperty(value, key) {
63
+ const property = value[key];
64
+ return property && typeof property === 'object' && !Array.isArray(property)
65
+ ? property
66
+ : null;
67
+ }
68
+ /**
69
+ * Resolves a stable scenario id from a scenario file.
70
+ *
71
+ * @param {{scenario: Record<string, unknown>, scenarioPath: string}} options
72
+ * @returns {string}
73
+ */
74
+ function resolveScenarioId({ scenario, scenarioPath, }) {
75
+ return typeof scenario.id === 'string' && scenario.id.length > 0
76
+ ? scenario.id
77
+ : path.basename(scenarioPath, '.json');
78
+ }
79
+ /**
80
+ * Resolves the Android package name from explicit input or ASL config.
81
+ *
82
+ * @param {{args: CliArgs, config: Record<string, unknown>}} options
83
+ * @returns {string | null}
84
+ */
85
+ function resolveAndroidPackageName({ args, config, }) {
86
+ if (typeof args.package === 'string') {
87
+ return args.package;
88
+ }
89
+ const envPackage = readStringArgOrEnv(undefined, ['ASL_ANDROID_APP_ID', 'ASL_EXAMPLE_ANDROID_APP_ID']);
90
+ if (envPackage) {
91
+ return envPackage;
92
+ }
93
+ const app = readObjectProperty(config, 'app');
94
+ return typeof app?.androidPackage === 'string' ? app.androidPackage : null;
95
+ }
96
+ /**
97
+ * Converts an optional run suffix into a path-safe segment.
98
+ *
99
+ * @param {unknown} value
100
+ * @returns {string | null}
101
+ */
102
+ function normalizeRunSuffix(value) {
103
+ if (typeof value !== 'string') {
104
+ return null;
105
+ }
106
+ const normalized = value
107
+ .trim()
108
+ .toLowerCase()
109
+ .replace(/[^a-z0-9._-]+/gu, '-')
110
+ .replace(/^-+|-+$/gu, '')
111
+ .slice(0, 64);
112
+ return normalized.length > 0 ? normalized : null;
113
+ }
114
+ /**
115
+ * Applies an optional suffix to deterministic run ids.
116
+ *
117
+ * @param {string} baseRunId
118
+ * @param {string | null} suffix
119
+ * @returns {string}
120
+ */
121
+ function buildRunId(baseRunId, suffix) {
122
+ return suffix ? `${baseRunId}-${suffix}` : baseRunId;
123
+ }
124
+ /**
125
+ * Throws if a profile or interaction proof failed before aggregate writing.
126
+ *
127
+ * @param {{kind: string, runDir: string, health: Record<string, unknown>, verdict?: Record<string, unknown>}} options
128
+ * @returns {void}
129
+ */
130
+ function assertPassedRun({ health, kind, runDir, verdict, }) {
131
+ if (health.healthStatus === 'passed' && (!verdict || verdict.verdictStatus === 'passed')) {
132
+ return;
133
+ }
134
+ throw new Error([
135
+ `Android live proof failed for ${kind}.`,
136
+ `Health: ${health.healthStatus ?? 'unknown'}.`,
137
+ ...(verdict ? [`Verdict: ${verdict.verdictStatus ?? 'unknown'}.`] : []),
138
+ `Inspect ${runDir}/agent-summary.md.`,
139
+ ].join(' '));
140
+ }
141
+ /**
142
+ * Reports whether profile evidence is trusted enough to run sidecar interaction proofs.
143
+ *
144
+ * @param {{health: Record<string, unknown>, verdict: Record<string, unknown>}} run
145
+ * @returns {boolean}
146
+ */
147
+ function isTrustedProfileRun({ health, verdict, }) {
148
+ return health.healthStatus === 'passed' && verdict.verdictStatus === 'passed';
149
+ }
150
+ /**
151
+ * Builds skipped sidecar pointers for requested runners when the profile gate failed.
152
+ *
153
+ * @param {{requestedRunners: string[], runIdsByRunner: Record<string, string>, scenarioId: string, profileHealthStatus: unknown, profileVerdictStatus: unknown}} options
154
+ * @returns {SkippedInteractionProof[]}
155
+ */
156
+ function buildSkippedInteractionProofs({ profileHealthStatus, profileVerdictStatus, requestedRunners, runIdsByRunner, scenarioId, }) {
157
+ const reason = `Profile gate failed with health=${String(profileHealthStatus ?? 'unknown')} verdict=${String(profileVerdictStatus ?? 'unknown')}; sidecar interaction proof was skipped because timing and runner evidence would not be trustworthy.`;
158
+ return requestedRunners.map((runnerId) => ({
159
+ label: `interaction-${runnerId}`,
160
+ nextAction: {
161
+ code: 'fix_profile_gate',
162
+ summary: 'Inspect the profile health and verdict before rerunning sidecar interaction proofs.',
163
+ },
164
+ reason,
165
+ runId: runIdsByRunner[runnerId] ?? `${scenarioId}-android-${runnerId}`,
166
+ runnerId,
167
+ scenarioId,
168
+ }));
169
+ }
170
+ /**
171
+ * Builds adb arguments with an optional device serial prefix.
172
+ *
173
+ * @param {string | null} serial
174
+ * @param {string[]} args
175
+ * @returns {string[]}
176
+ */
177
+ function buildAndroidRunnerAdbArgs(serial, args) {
178
+ return serial ? ['-s', serial, ...args] : args;
179
+ }
180
+ /**
181
+ * Formats an adb command result as raw diagnostic evidence.
182
+ *
183
+ * @param {import('./android-adb').CommandResult} result
184
+ * @returns {string}
185
+ */
186
+ function formatAndroidRunnerCommandResult(result) {
187
+ return [
188
+ `$ ${result.command} ${result.args.join(' ')}`,
189
+ `exitCode=${result.exitCode}`,
190
+ result.stdout.trimEnd(),
191
+ result.stderr.trimEnd(),
192
+ ].filter(Boolean).join('\n');
193
+ }
194
+ /**
195
+ * Detects the known Argent Android instrumentation-helper crash signature.
196
+ *
197
+ * @param {string} text
198
+ * @returns {boolean}
199
+ */
200
+ function hasArgentAndroidDevtoolsCrash(text) {
201
+ return /(?:Process:\s*com\.argent\.androiddevtools|Crash of app com\.argent\.androiddevtools|FATAL EXCEPTION:\s*argent-androiddevtools)/u.test(text);
202
+ }
203
+ /**
204
+ * Runs one adb diagnostic command through the live proof executor seam.
205
+ *
206
+ * @param {{adbPath: string, args: string[], executor?: import('./android-adb').CommandExecutor}} options
207
+ * @returns {Promise<import('./android-adb').CommandResult>}
208
+ */
209
+ async function runAndroidRunnerAdbCommand({ adbPath, args, executor, }) {
210
+ return executor ? executor(adbPath, args) : execFileCommand(adbPath, args);
211
+ }
212
+ /**
213
+ * Attaches runner-side logcat evidence to an Argent Android sidecar proof.
214
+ *
215
+ * The Android live proof already owns adb, so this preserves Argent helper
216
+ * instability without making the standalone Argent runner depend on adb.
217
+ *
218
+ * @param {{adbPath: string, capture: import('./argent').ArgentCaptureResult, executor?: import('./android-adb').CommandExecutor, serial: string | null}} options
219
+ * @returns {Promise<void>}
220
+ */
221
+ async function attachArgentAndroidRunnerDiagnostics({ adbPath, capture, executor, serial, }) {
222
+ const logcatResult = await runAndroidRunnerAdbCommand({
223
+ adbPath,
224
+ args: buildAndroidRunnerAdbArgs(serial, ['logcat', '-d', '-v', 'time', '-t', '400']),
225
+ ...(executor ? { executor } : {}),
226
+ });
227
+ const rawDir = path.join(capture.runDir, 'raw');
228
+ await fs.promises.mkdir(rawDir, { recursive: true });
229
+ await fs.promises.writeFile(path.join(rawDir, 'adb-runner-logcat-after-argent.txt'), `${formatAndroidRunnerCommandResult(logcatResult)}\n`, 'utf8');
230
+ const rawOutput = `${logcatResult.stdout}\n${logcatResult.stderr}`;
231
+ if (!hasArgentAndroidDevtoolsCrash(rawOutput)) {
232
+ return;
233
+ }
234
+ const healthPath = path.join(capture.runDir, 'health.json');
235
+ const health = JSON.parse(fs.readFileSync(healthPath, 'utf8'));
236
+ health.checks = Array.isArray(health.checks) ? health.checks : [];
237
+ health.checks.push({
238
+ name: 'argent_android_helper_crash',
239
+ status: 'warning',
240
+ source: 'runner',
241
+ code: 'argent_android_helper_crash',
242
+ message: 'Argent Android helper crashed after producing sidecar evidence.',
243
+ metadata: {
244
+ nextAction: 'Inspect raw/adb-runner-logcat-after-argent.txt and rerun Argent if sidecar evidence becomes flaky.',
245
+ nextActionCode: 'inspect_argent_android_helper_crash',
246
+ rawPath: 'raw/adb-runner-logcat-after-argent.txt',
247
+ runnerProcess: 'com.argent.androiddevtools',
248
+ },
249
+ });
250
+ await writeJsonArtifact({
251
+ filePath: healthPath,
252
+ value: health,
253
+ schema: SCHEMAS.health,
254
+ label: 'Argent health artifact',
255
+ });
256
+ }
257
+ /**
258
+ * Throws after aggregate writing when the live proof itself failed.
259
+ *
260
+ * @param {AndroidGenericLiveResult} result
261
+ * @returns {void}
262
+ */
263
+ function assertAggregatePassed(result) {
264
+ const proof = readJson(result.aggregateSummary.liveProofPath);
265
+ if (proof.status === 'passed') {
266
+ return;
267
+ }
268
+ throw new Error(`Android live proof failed. Inspect ${result.aggregateSummary.summaryPath}.`);
269
+ }
270
+ /**
271
+ * Throws after aggregate proof writing when fail-on-regression should gate the run.
272
+ *
273
+ * @param {{result: AndroidGenericLiveResult, comparisons: Array<{label: string, status: string}>}} options
274
+ * @returns {void}
275
+ */
276
+ function assertNoRegressedComparisons({ comparisons, result, }) {
277
+ const regressed = comparisons.filter((comparison) => comparison.status === 'worse');
278
+ if (regressed.length === 0) {
279
+ return;
280
+ }
281
+ throw new Error(`Android live proof found regressed comparison(s): ${regressed.map((comparison) => comparison.label).join(', ')}. Inspect ${result.aggregateSummary.summaryPath}.`);
282
+ }
283
+ /**
284
+ * Runs a generic one-scenario Android live proof.
285
+ *
286
+ * @param {CliArgs} args
287
+ * @param {AndroidGenericLiveOptions} [options]
288
+ * @returns {Promise<AndroidGenericLiveResult>}
289
+ */
290
+ async function runAndroidLiveProof(args, options = {}) {
291
+ if (typeof args.config !== 'string' || typeof args.scenario !== 'string') {
292
+ throw new Error('Both --config and --scenario are required.');
293
+ }
294
+ const configPath = path.resolve(args.config);
295
+ const scenarioPath = path.resolve(args.scenario);
296
+ const config = readJson(configPath);
297
+ const scenario = readJson(scenarioPath);
298
+ const scenarioId = resolveScenarioId({ scenario, scenarioPath });
299
+ const packageName = resolveAndroidPackageName({ args, config });
300
+ const serial = readStringArgOrEnv(args.serial, ['ASL_ANDROID_SERIAL', 'ASL_EXAMPLE_ANDROID_SERIAL']);
301
+ const reactNativeDebugHost = readStringArgOrEnv(args['react-native-debug-host'], [
302
+ 'ASL_ANDROID_REACT_NATIVE_DEBUG_HOST',
303
+ 'ASL_EXAMPLE_ANDROID_DEBUG_HOST',
304
+ ]);
305
+ const androidDevClientUrl = readStringArgOrEnv(args['android-dev-client-url'], [
306
+ 'ASL_ANDROID_DEV_CLIENT_URL',
307
+ 'ASL_EXAMPLE_ANDROID_DEV_CLIENT_URL',
308
+ ]);
309
+ const androidDevClientWaitMs = readStringArgOrEnv(args['android-dev-client-wait-ms'], [
310
+ 'ASL_ANDROID_DEV_CLIENT_WAIT_MS',
311
+ 'ASL_EXAMPLE_ANDROID_DEV_CLIENT_WAIT_MS',
312
+ ]);
313
+ const androidDevClientReadyPattern = readStringArgOrEnv(args['android-dev-client-ready-pattern'], [
314
+ 'ASL_ANDROID_DEV_CLIENT_READY_PATTERN',
315
+ 'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_PATTERN',
316
+ ]);
317
+ const androidDevClientReadyQuietMs = readStringArgOrEnv(args['android-dev-client-ready-quiet-ms'], [
318
+ 'ASL_ANDROID_DEV_CLIENT_READY_QUIET_MS',
319
+ 'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_QUIET_MS',
320
+ ]);
321
+ const androidDevClientReadyTimeoutMs = readStringArgOrEnv(args['android-dev-client-ready-timeout-ms'], [
322
+ 'ASL_ANDROID_DEV_CLIENT_READY_TIMEOUT_MS',
323
+ 'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_TIMEOUT_MS',
324
+ ]);
325
+ const androidProfileSessionStorage = readStringArgOrEnv(args['android-profile-session-storage'], [
326
+ 'ASL_ANDROID_PROFILE_SESSION_STORAGE',
327
+ 'ASL_EXAMPLE_ANDROID_PROFILE_SESSION_STORAGE',
328
+ ]);
329
+ const androidProfileSessionStorageKey = readStringArgOrEnv(args['android-profile-session-storage-key'], [
330
+ 'ASL_ANDROID_PROFILE_SESSION_STORAGE_KEY',
331
+ 'ASL_EXAMPLE_ANDROID_PROFILE_SESSION_STORAGE_KEY',
332
+ ]);
333
+ const androidProfileCommandStorageKey = readStringArgOrEnv(args['android-profile-command-storage-key'], [
334
+ 'ASL_ANDROID_PROFILE_COMMAND_STORAGE_KEY',
335
+ 'ASL_EXAMPLE_ANDROID_PROFILE_COMMAND_STORAGE_KEY',
336
+ ]);
337
+ const agentDeviceSession = readStringArgOrEnv(args['agent-device-session'], [
338
+ 'ASL_ANDROID_AGENT_DEVICE_SESSION',
339
+ 'ASL_EXAMPLE_ANDROID_AGENT_DEVICE_SESSION',
340
+ ]);
341
+ const agentDeviceSessionMode = readStringArgOrEnv(args['agent-device-session-mode'], [
342
+ 'ASL_ANDROID_AGENT_DEVICE_SESSION_MODE',
343
+ 'ASL_EXAMPLE_ANDROID_AGENT_DEVICE_SESSION_MODE',
344
+ ]);
345
+ const outputDir = typeof args.out === 'string' ? path.resolve(args.out) : path.resolve('artifacts/asl/android-live');
346
+ const runSuffix = normalizeRunSuffix(args['run-suffix']);
347
+ const aggregateRunId = buildRunId(typeof args['run-id'] === 'string' ? args['run-id'] : 'android-live-proof', runSuffix);
348
+ const preflightRunId = buildRunId(`${scenarioId}-android-preflight`, runSuffix);
349
+ const profileRunId = buildRunId(`${scenarioId}-android-live`, runSuffix);
350
+ const agentDeviceRunId = buildRunId(`${scenarioId}-android-agent-device`, runSuffix);
351
+ const argentRunId = buildRunId(`${scenarioId}-android-argent`, runSuffix);
352
+ const enabledInteractionRunners = [
353
+ ...(isEnabledFlag(args['agent-device-proof']) ? ['agent-device'] : []),
354
+ ...(isEnabledFlag(args['argent-proof']) ? ['argent'] : []),
355
+ ];
356
+ const runIdsByRunner = {
357
+ 'agent-device': agentDeviceRunId,
358
+ argent: argentRunId,
359
+ };
360
+ const comparisonLane = enabledInteractionRunners.length > 0
361
+ ? `${scenarioId}-android-live+${enabledInteractionRunners.join('+')}`
362
+ : `${scenarioId}-android-live`;
363
+ const preflightDir = path.join(outputDir, '_preflight', preflightRunId);
364
+ const preflight = await runAndroidAdbPreflight({
365
+ ...(typeof args.adb === 'string' ? { adbPath: args.adb } : {}),
366
+ ...(options.delay ? { delay: options.delay } : {}),
367
+ ...(options.executor ? { executor: options.executor } : {}),
368
+ outputDir: preflightDir,
369
+ packageName,
370
+ ...(reactNativeDebugHost ? { reactNativeDebugHost } : {}),
371
+ runId: preflightRunId,
372
+ ...(serial ? { serial } : {}),
373
+ });
374
+ if (preflight.health.healthStatus !== 'passed') {
375
+ throw new Error(`Android live proof preflight failed; inspect ${preflight.runDir}/agent-summary.md.`);
376
+ }
377
+ const interactionProofs = [];
378
+ const profile = await runProfileAndroid({
379
+ ...(typeof args.adb === 'string' ? { adb: args.adb } : {}),
380
+ 'adb-capture': true,
381
+ 'clear-logcat': true,
382
+ config: configPath,
383
+ 'command-wait-ms': typeof args['command-wait-ms'] === 'string' ? args['command-wait-ms'] : '250',
384
+ launch: true,
385
+ 'launch-wait-ms': typeof args['launch-wait-ms'] === 'string' ? args['launch-wait-ms'] : '1500',
386
+ 'logcat-lines': typeof args['logcat-lines'] === 'string' ? args['logcat-lines'] : '1000',
387
+ out: outputDir,
388
+ ...(packageName ? { package: packageName } : {}),
389
+ 'profile-session': true,
390
+ ...(reactNativeDebugHost ? { 'react-native-debug-host': reactNativeDebugHost } : {}),
391
+ ...(androidDevClientUrl ? { 'android-dev-client-url': androidDevClientUrl } : {}),
392
+ ...(androidDevClientWaitMs ? { 'android-dev-client-wait-ms': androidDevClientWaitMs } : {}),
393
+ ...(androidDevClientReadyPattern ? { 'android-dev-client-ready-pattern': androidDevClientReadyPattern } : {}),
394
+ ...(androidDevClientReadyQuietMs ? { 'android-dev-client-ready-quiet-ms': androidDevClientReadyQuietMs } : {}),
395
+ ...(androidDevClientReadyTimeoutMs ? { 'android-dev-client-ready-timeout-ms': androidDevClientReadyTimeoutMs } : {}),
396
+ ...(isEnabledFlag(args['android-profile-session-storage']) || androidProfileSessionStorage === 'true'
397
+ ? { 'android-profile-session-storage': true }
398
+ : {}),
399
+ ...(androidProfileSessionStorageKey ? { 'android-profile-session-storage-key': androidProfileSessionStorageKey } : {}),
400
+ ...(androidProfileCommandStorageKey ? { 'android-profile-command-storage-key': androidProfileCommandStorageKey } : {}),
401
+ 'run-id': profileRunId,
402
+ scenario: scenarioPath,
403
+ ...(serial ? { serial } : {}),
404
+ ...(typeof args['wait-ms'] === 'string' ? { 'wait-ms': args['wait-ms'] } : {}),
405
+ }, {
406
+ comparisonLane,
407
+ ...(options.delay ? { delay: options.delay } : {}),
408
+ ...(options.executor ? { executor: options.executor } : {}),
409
+ });
410
+ const profileTrusted = isTrustedProfileRun({ health: profile.health, verdict: profile.verdict });
411
+ let skippedInteractionProofs = [];
412
+ if (!profileTrusted) {
413
+ skippedInteractionProofs = buildSkippedInteractionProofs({
414
+ profileHealthStatus: profile.health.healthStatus,
415
+ profileVerdictStatus: profile.verdict.verdictStatus,
416
+ requestedRunners: enabledInteractionRunners,
417
+ runIdsByRunner,
418
+ scenarioId,
419
+ });
420
+ }
421
+ if (profileTrusted && isEnabledFlag(args['agent-device-proof'])) {
422
+ const capture = await runAgentDeviceCapture({
423
+ ...(typeof args['agent-device'] === 'string' ? { agentDevicePath: args['agent-device'] } : {}),
424
+ app: packageName,
425
+ ...(options.agentDeviceExecutor ? { executor: options.agentDeviceExecutor } : {}),
426
+ open: true,
427
+ outputDir: path.join(outputDir, '_agent-device-captures', agentDeviceRunId),
428
+ platform: 'android',
429
+ runId: agentDeviceRunId,
430
+ scenario,
431
+ ...(serial ? { serial } : {}),
432
+ ...(agentDeviceSession ? { session: agentDeviceSession } : {}),
433
+ ...(agentDeviceSessionMode
434
+ ? { sessionMode: agentDeviceSessionMode }
435
+ : {}),
436
+ waitMs: parsePositiveInteger(args['agent-device-wait-ms'], 1000),
437
+ });
438
+ interactionProofs.push({
439
+ label: 'interaction-agent-device',
440
+ runDir: capture.runDir,
441
+ runId: agentDeviceRunId,
442
+ runnerId: 'agent-device',
443
+ scenarioId,
444
+ });
445
+ }
446
+ if (profileTrusted && isEnabledFlag(args['argent-proof'])) {
447
+ const argentBaseArgs = parseArgentBaseArgs(process.env.ASL_ARGENT_BASE_ARGS);
448
+ const adbPath = typeof args.adb === 'string' ? args.adb : 'adb';
449
+ await runAndroidRunnerAdbCommand({
450
+ adbPath,
451
+ args: buildAndroidRunnerAdbArgs(serial ?? null, ['logcat', '-c']),
452
+ ...(options.executor ? { executor: options.executor } : {}),
453
+ });
454
+ const capture = await runArgentCapture({
455
+ app: packageName,
456
+ argentCommand: process.env.ASL_ARGENT_BIN || 'argent',
457
+ ...(argentBaseArgs ? { baseArgs: argentBaseArgs } : {}),
458
+ commandTimeoutMs: parsePositiveInteger(process.env.ASL_ARGENT_COMMAND_TIMEOUT_MS, 60_000),
459
+ deviceId: serial ?? 'emulator-5554',
460
+ ...(options.delay ? { delay: options.delay } : {}),
461
+ ...(options.argentExecutor ? { executor: options.argentExecutor } : {}),
462
+ outputDir: path.join(outputDir, '_argent-captures', argentRunId),
463
+ platform: 'android',
464
+ runId: argentRunId,
465
+ scenario,
466
+ });
467
+ await attachArgentAndroidRunnerDiagnostics({
468
+ adbPath,
469
+ capture,
470
+ ...(options.executor ? { executor: options.executor } : {}),
471
+ serial: serial ?? null,
472
+ });
473
+ interactionProofs.push({
474
+ label: 'interaction-argent',
475
+ runDir: capture.runDir,
476
+ runId: argentRunId,
477
+ runnerId: 'argent',
478
+ scenarioId,
479
+ });
480
+ }
481
+ const profiles = [{
482
+ label: scenarioId,
483
+ runDir: profile.runDir,
484
+ runId: profileRunId,
485
+ scenarioId,
486
+ }];
487
+ const comparisons = profileTrusted && isEnabledFlag(args['compare-latest'])
488
+ ? await compareLiveProfilesToLatest({ outputDir, profiles })
489
+ : [];
490
+ const aggregateSummary = await writeLiveProofSummary({
491
+ comparisons,
492
+ interactionProofs,
493
+ outputDir,
494
+ platform: 'android',
495
+ preflightDir: preflight.runDir,
496
+ preflightRunId,
497
+ profiles,
498
+ runId: aggregateRunId,
499
+ skippedInteractionProofs,
500
+ });
501
+ const result = {
502
+ aggregateSummary,
503
+ outputDir,
504
+ preflightDir: preflight.runDir,
505
+ profileDir: profile.runDir,
506
+ };
507
+ if (isEnabledFlag(args['fail-on-regression'])) {
508
+ assertNoRegressedComparisons({ comparisons, result });
509
+ }
510
+ assertAggregatePassed(result);
511
+ return result;
512
+ }
513
+ /**
514
+ * Runs the generic Android live CLI.
515
+ *
516
+ * @returns {Promise<void>}
517
+ */
518
+ async function main() {
519
+ const argv = process.argv.slice(2);
520
+ if (hasHelpFlag(argv)) {
521
+ usage(process.stdout);
522
+ return;
523
+ }
524
+ loadAslLocalEnv();
525
+ const args = parseArgs(argv);
526
+ if (typeof args.config !== 'string' || typeof args.scenario !== 'string') {
527
+ usage();
528
+ process.exitCode = 1;
529
+ return;
530
+ }
531
+ const result = await runAndroidLiveProof(args);
532
+ process.stdout.write(`${result.aggregateSummary.summaryPath}\n`);
533
+ }
534
+ if (require.main === module) {
535
+ main().catch((error) => {
536
+ console.error(error instanceof Error ? error.message : String(error));
537
+ process.exitCode = 1;
538
+ });
539
+ }