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,628 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.buildAvailabilityCheck = buildAvailabilityCheck;
5
+ exports.buildChildRunCheck = buildChildRunCheck;
6
+ exports.buildHostDoctorSummary = buildHostDoctorSummary;
7
+ exports.main = main;
8
+ exports.parseArgs = parseArgs;
9
+ exports.parseRequirements = parseRequirements;
10
+ exports.runHostDoctor = runHostDoctor;
11
+ exports.usage = usage;
12
+ const crypto = require('node:crypto');
13
+ const fs = require('node:fs');
14
+ const fsp = require('node:fs/promises');
15
+ const path = require('node:path');
16
+ const { createArtifactLayout } = require('../core/artifact-layout');
17
+ const { writeJsonArtifact, writeTextArtifact } = require('../core/artifact-writer');
18
+ const { SCHEMAS } = require('../core/schema-validator');
19
+ const { checkAgentDeviceAvailability, parseRequiredPlatforms } = require('./agent-device');
20
+ const { runAndroidAdbPreflight } = require('./android-adb');
21
+ const { checkArgentAvailability, parseBaseArgs } = require('./argent');
22
+ const { hasHelpFlag, writeUsage } = require('./cli');
23
+ const { runIosSimctlCapture } = require('./ios-simctl');
24
+ const { loadAslLocalEnv, readStringArgOrEnv } = require('./local-env');
25
+ const DEFAULT_REQUIREMENTS = ['android', 'ios'];
26
+ const REQUIREMENT_SET = new Set(['agent-device', 'android', 'argent', 'ios']);
27
+ /**
28
+ * Prints CLI usage.
29
+ *
30
+ * @param {{write: (message: string) => unknown}} output
31
+ * @returns {void}
32
+ */
33
+ function usage(output = process.stderr) {
34
+ writeUsage([
35
+ 'Usage: asl-host-doctor [--require android,ios[,agent-device,argent]] [--out <dir>]',
36
+ '',
37
+ 'Runs host/device preflight checks before mobile live proof commands.',
38
+ 'Writes health.json, verdict.json, agent-summary.md, and raw child preflight artifacts.',
39
+ 'Default requirements are android and ios; add agent-device or Argent when sidecar proofs must be available.',
40
+ 'Use --android-package, --android-serial, --ios-bundle, and --ios-device to target an installed app or specific target.',
41
+ 'Use --agent-device-require-platforms ios,android when agent-device discovery must prove booted OS targets.',
42
+ 'Use --argent <binary> and --base-args "<args>" to verify a non-global Argent command shape.',
43
+ 'Use --command-timeout-ms <ms> to bound agent-device and Argent availability checks.',
44
+ ], output);
45
+ }
46
+ /**
47
+ * Parses `--key value` CLI arguments.
48
+ *
49
+ * @param {string[]} argv
50
+ * @returns {CliArgs}
51
+ */
52
+ function parseArgs(argv) {
53
+ const args = {};
54
+ for (let index = 0; index < argv.length; index += 1) {
55
+ const token = argv[index];
56
+ if (token === '--') {
57
+ continue;
58
+ }
59
+ if (!token?.startsWith('--')) {
60
+ continue;
61
+ }
62
+ const key = token.slice(2);
63
+ const value = argv[index + 1];
64
+ if (value && !value.startsWith('--')) {
65
+ args[key] = value;
66
+ index += 1;
67
+ }
68
+ else {
69
+ args[key] = true;
70
+ }
71
+ }
72
+ return args;
73
+ }
74
+ /**
75
+ * Creates a short random run id.
76
+ *
77
+ * @returns {string}
78
+ */
79
+ function createRunId() {
80
+ return crypto.randomBytes(6).toString('hex');
81
+ }
82
+ /**
83
+ * Parses the comma-separated host doctor requirements.
84
+ *
85
+ * @param {unknown} value
86
+ * @returns {HostDoctorRequirement[]}
87
+ */
88
+ function parseRequirements(value) {
89
+ if (value === undefined || value === false) {
90
+ return [...DEFAULT_REQUIREMENTS];
91
+ }
92
+ if (value === true || typeof value !== 'string') {
93
+ throw new Error('--require must be a comma-separated list of android, ios, agent-device, and argent.');
94
+ }
95
+ const requirements = value
96
+ .split(',')
97
+ .map((item) => item.trim())
98
+ .filter(Boolean);
99
+ const invalid = requirements.find((requirement) => !REQUIREMENT_SET.has(requirement));
100
+ if (invalid) {
101
+ throw new Error(`Unsupported host doctor requirement: ${invalid}.`);
102
+ }
103
+ return Array.from(new Set(requirements));
104
+ }
105
+ /**
106
+ * Reads a positive integer from CLI values.
107
+ *
108
+ * @param {unknown} value
109
+ * @param {number} fallback
110
+ * @returns {number}
111
+ */
112
+ function parsePositiveInteger(value, fallback) {
113
+ const parsed = typeof value === 'string' ? Number(value) : value;
114
+ return typeof parsed === 'number' && Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
115
+ }
116
+ /**
117
+ * Reads a JSON object if the child artifact exists.
118
+ *
119
+ * @param {string} filePath
120
+ * @returns {Record<string, unknown> | null}
121
+ */
122
+ function readJsonIfPresent(filePath) {
123
+ try {
124
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
125
+ }
126
+ catch {
127
+ return null;
128
+ }
129
+ }
130
+ /**
131
+ * Reads health from a child preflight result or its artifact directory.
132
+ *
133
+ * @param {HostDoctorChildResult} result
134
+ * @returns {Record<string, unknown>}
135
+ */
136
+ function readChildHealth(result) {
137
+ return result.health ?? readJsonIfPresent(path.join(result.runDir, 'health.json')) ?? {};
138
+ }
139
+ /**
140
+ * Reads the first next-action hint from a failed child health artifact.
141
+ *
142
+ * @param {Record<string, unknown>} health
143
+ * @returns {Record<string, string> | null}
144
+ */
145
+ function readChildNextAction(health) {
146
+ const checks = Array.isArray(health.checks) ? health.checks : [];
147
+ for (const check of checks) {
148
+ if (!check || typeof check !== 'object') {
149
+ continue;
150
+ }
151
+ const record = check;
152
+ const metadata = record.metadata;
153
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
154
+ continue;
155
+ }
156
+ const metadataRecord = metadata;
157
+ if (typeof metadataRecord.nextAction === 'string') {
158
+ return {
159
+ nextAction: metadataRecord.nextAction,
160
+ ...(typeof metadataRecord.nextActionCode === 'string' ? { nextActionCode: metadataRecord.nextActionCode } : {}),
161
+ };
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+ /**
167
+ * Builds a scalar ASL health check from a child preflight run.
168
+ *
169
+ * @param {{health: Record<string, unknown>, label: string, name: string, runDir: string}} options
170
+ * @returns {HostDoctorCheck}
171
+ */
172
+ function buildChildRunCheck({ health, label, name, runDir, }) {
173
+ const passed = health.healthStatus === 'passed';
174
+ const nextAction = passed ? null : readChildNextAction(health);
175
+ return {
176
+ code: passed ? `${name}_host_ready` : `${name}_host_unavailable`,
177
+ message: passed
178
+ ? `${label} host preflight passed.`
179
+ : `${label} host preflight failed; inspect ${path.join(runDir, 'agent-summary.md')}.`,
180
+ metadata: {
181
+ childRunDir: runDir,
182
+ ...(nextAction ?? {}),
183
+ },
184
+ name,
185
+ source: 'runner',
186
+ status: passed ? 'passed' : 'failed',
187
+ };
188
+ }
189
+ /**
190
+ * Reads the first failed command-surface check from an availability result.
191
+ *
192
+ * @param {Record<string, unknown>} result
193
+ * @returns {Record<string, unknown> | null}
194
+ */
195
+ function readFailedAvailabilityCheck(result) {
196
+ const checks = Array.isArray(result.checks) ? result.checks : [];
197
+ for (const check of checks) {
198
+ if (!check || typeof check !== 'object' || Array.isArray(check)) {
199
+ continue;
200
+ }
201
+ const record = check;
202
+ if (record.status === 'failed') {
203
+ return record;
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+ /**
209
+ * Builds scalar metadata from a failed command-surface check.
210
+ *
211
+ * @param {{failedCheck: Record<string, unknown>, label: string, name: string, rawPath: string}} options
212
+ * @returns {Record<string, string>}
213
+ */
214
+ function buildAvailabilityFailureMetadata({ failedCheck, label, name, rawPath, }) {
215
+ const failedCheckName = typeof failedCheck.name === 'string' ? failedCheck.name : `${name}_availability`;
216
+ const failedCheckCode = typeof failedCheck.code === 'string' ? failedCheck.code : `${name}_availability_failed`;
217
+ const failedCheckMessage = typeof failedCheck.message === 'string' ? failedCheck.message : `${label} availability check failed.`;
218
+ const stderrPreview = typeof failedCheck.stderrPreview === 'string' ? failedCheck.stderrPreview : '';
219
+ const stdoutPreview = typeof failedCheck.stdoutPreview === 'string' ? failedCheck.stdoutPreview : '';
220
+ const checkMetadata = failedCheck.metadata && typeof failedCheck.metadata === 'object' && !Array.isArray(failedCheck.metadata)
221
+ ? failedCheck.metadata
222
+ : {};
223
+ const classifiedNextActionCode = typeof checkMetadata.nextActionCode === 'string'
224
+ ? checkMetadata.nextActionCode
225
+ : null;
226
+ const classifiedNextAction = typeof checkMetadata.nextAction === 'string'
227
+ ? checkMetadata.nextAction
228
+ : null;
229
+ const failureClass = typeof checkMetadata.failureClass === 'string'
230
+ ? checkMetadata.failureClass
231
+ : null;
232
+ const diagnostic = `${failedCheckMessage}\n${stderrPreview}\n${stdoutPreview}`;
233
+ const hostAccessFailure = /operation not permitted|permission denied|sandbox|daemon|smartsocket|cannot bind/iu.test(diagnostic);
234
+ const timedOut = /timed out|timeout/iu.test(diagnostic);
235
+ const nextActionCode = classifiedNextActionCode ?? (hostAccessFailure
236
+ ? 'rerun_with_host_access'
237
+ : timedOut
238
+ ? `increase_${name}_timeout`
239
+ : `inspect_${name}_availability`);
240
+ const nextAction = classifiedNextAction ?? (hostAccessFailure
241
+ ? `Rerun the host doctor outside the restricted sandbox or grant host/device access before treating ${label} failures as app or scenario regressions.`
242
+ : timedOut
243
+ ? `Confirm ${label} can run without prompts, increase --command-timeout-ms if it is legitimately slow, then rerun the host doctor.`
244
+ : `Inspect ${rawPath}, fix the ${label} command surface, then rerun the host doctor before starting live proof.`);
245
+ return {
246
+ failedCheckCode,
247
+ failedCheckMessage,
248
+ failedCheckName,
249
+ ...(failureClass ? { failureClass } : {}),
250
+ nextAction,
251
+ nextActionCode,
252
+ ...(stderrPreview ? { stderrPreview } : {}),
253
+ ...(stdoutPreview ? { stdoutPreview } : {}),
254
+ };
255
+ }
256
+ /**
257
+ * Builds a scalar ASL health check from a command-surface availability result.
258
+ *
259
+ * @param {{label: string, name: string, rawPath: string, result: Record<string, unknown>}} options
260
+ * @returns {HostDoctorCheck}
261
+ */
262
+ function buildAvailabilityCheck({ label, name, rawPath, result, }) {
263
+ const passed = result.status === 'passed';
264
+ const failedCheck = passed ? null : readFailedAvailabilityCheck(result);
265
+ return {
266
+ code: passed ? `${name}_available` : `${name}_unavailable`,
267
+ message: passed
268
+ ? `${label} command surface is available.`
269
+ : `${label} command surface failed; inspect ${rawPath}.`,
270
+ metadata: {
271
+ rawPath,
272
+ ...(failedCheck
273
+ ? buildAvailabilityFailureMetadata({ failedCheck, label, name, rawPath })
274
+ : {}),
275
+ },
276
+ name,
277
+ source: 'runner',
278
+ status: passed ? 'passed' : 'failed',
279
+ };
280
+ }
281
+ /**
282
+ * Builds a failed health check from an unexpected host doctor exception.
283
+ *
284
+ * @param {{error: unknown, name: string}} options
285
+ * @returns {HostDoctorCheck}
286
+ */
287
+ function buildExceptionCheck({ error, name, }) {
288
+ const message = error instanceof Error ? error.message : String(error);
289
+ return {
290
+ code: `${name}_doctor_exception`,
291
+ message: `${name} preflight threw before writing complete evidence.`,
292
+ metadata: {
293
+ errorMessage: message,
294
+ nextAction: 'Inspect the command configuration and rerun the host doctor with host/device access before starting live proof.',
295
+ nextActionCode: 'rerun_host_doctor',
296
+ },
297
+ name,
298
+ source: 'runner',
299
+ status: 'failed',
300
+ };
301
+ }
302
+ /**
303
+ * Reads a string field from an artifact record.
304
+ *
305
+ * @param {Record<string, unknown>} record
306
+ * @param {string} key
307
+ * @param {string} fallback
308
+ * @returns {string}
309
+ */
310
+ function readStringField(record, key, fallback) {
311
+ const value = record[key];
312
+ return typeof value === 'string' && value.trim() ? value : fallback;
313
+ }
314
+ /**
315
+ * Formats a scalar value as inline markdown code.
316
+ *
317
+ * @param {unknown} value
318
+ * @returns {string}
319
+ */
320
+ function formatCode(value) {
321
+ const text = typeof value === 'string' && value.trim() ? value : 'unknown';
322
+ return `\`${text.replace(/`/gu, '\\`')}\``;
323
+ }
324
+ /**
325
+ * Reads string metadata from a host check.
326
+ *
327
+ * @param {Record<string, unknown>} check
328
+ * @param {string} key
329
+ * @returns {string | null}
330
+ */
331
+ function readCheckMetadataString(check, key) {
332
+ const metadata = check.metadata;
333
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
334
+ return null;
335
+ }
336
+ const value = metadata[key];
337
+ return typeof value === 'string' && value.trim() ? value : null;
338
+ }
339
+ /**
340
+ * Formats one host check for an agent-facing markdown summary.
341
+ *
342
+ * @param {unknown} check
343
+ * @returns {string}
344
+ */
345
+ function formatHostCheckLine(check) {
346
+ if (!check || typeof check !== 'object') {
347
+ return '- unknown_host_check: unknown - No check details were recorded.';
348
+ }
349
+ const record = check;
350
+ const name = readStringField(record, 'name', 'unknown_host_check');
351
+ const status = readStringField(record, 'status', 'unknown');
352
+ const message = readStringField(record, 'message', 'No message was recorded.');
353
+ const nextAction = readCheckMetadataString(record, 'nextAction');
354
+ const nextActionCode = readCheckMetadataString(record, 'nextActionCode');
355
+ const suffix = nextAction
356
+ ? ` Next action${nextActionCode ? ` ${formatCode(nextActionCode)}` : ''}: ${nextAction}`
357
+ : '';
358
+ return `- ${name}: ${status} - ${message}${suffix}`;
359
+ }
360
+ /**
361
+ * Builds a host-specific agent summary for live-proof readiness.
362
+ *
363
+ * @param {{health: Record<string, unknown>, verdict: Record<string, unknown>}} options
364
+ * @returns {string}
365
+ */
366
+ function buildHostDoctorSummary({ health, verdict, }) {
367
+ const runId = readStringField(health, 'runId', readStringField(verdict, 'runId', 'unknown-run'));
368
+ const healthStatus = readStringField(health, 'healthStatus', 'failed');
369
+ const verdictStatus = readStringField(verdict, 'verdictStatus', 'inconclusive');
370
+ const checks = Array.isArray(health.checks) ? health.checks : [];
371
+ const checkLines = checks.length > 0
372
+ ? checks.map(formatHostCheckLine)
373
+ : ['- no_host_checks: unknown - No host checks were recorded.'];
374
+ const gate = healthStatus === 'passed'
375
+ ? 'Host/device preflight passed. Live proof can start with the requested host services.'
376
+ : 'Do not start live proof from this host state. Fix failed host/device checks before treating runtime failures as app or scenario regressions.';
377
+ return [
378
+ '# host doctor',
379
+ '',
380
+ `- Run ID: ${formatCode(runId)}`,
381
+ `- Health: ${healthStatus}`,
382
+ `- Verdict: ${verdictStatus}`,
383
+ '',
384
+ '## gate',
385
+ '',
386
+ gate,
387
+ '',
388
+ '## host checks',
389
+ '',
390
+ ...checkLines,
391
+ '',
392
+ ].join('\n');
393
+ }
394
+ /**
395
+ * Writes one raw JSON file under the doctor run directory.
396
+ *
397
+ * @param {{filePath: string, value: unknown}} options
398
+ * @returns {Promise<string>}
399
+ */
400
+ async function writeRawJson({ filePath, value, }) {
401
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
402
+ await fsp.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
403
+ return filePath;
404
+ }
405
+ /**
406
+ * Runs host/device preflight checks and writes aggregate ASL artifacts.
407
+ *
408
+ * @param {HostDoctorOptions} options
409
+ * @returns {Promise<HostDoctorResult>}
410
+ */
411
+ async function runHostDoctor({ adbPath = 'adb', agentDeviceAvailability = checkAgentDeviceAvailability, agentDevicePath = 'agent-device', agentDeviceRequiredPlatforms = [], androidPackageName = null, androidPreflight = runAndroidAdbPreflight, androidSerial = null, argentAvailability = checkArgentAvailability, argentBaseArgs, argentCommand = 'argent', commandTimeoutMs = 30_000, iosBundleId = null, iosDevice = null, iosPreflight = runIosSimctlCapture, outputDir = path.resolve('artifacts/host-doctor'), requirements = DEFAULT_REQUIREMENTS, runId = createRunId(), xcrunPath = 'xcrun', } = {}) {
412
+ const runDir = path.resolve(outputDir);
413
+ const layout = createArtifactLayout({ outputDir: runDir });
414
+ await fsp.mkdir(layout.raw, { recursive: true });
415
+ const checks = [];
416
+ const raw = {
417
+ requirements,
418
+ runId,
419
+ };
420
+ if (requirements.includes('android')) {
421
+ try {
422
+ const result = await androidPreflight({
423
+ adbPath,
424
+ ...(androidPackageName ? { packageName: androidPackageName } : {}),
425
+ outputDir: path.join(layout.raw, 'android-adb-preflight'),
426
+ ...(androidSerial ? { serial: androidSerial } : {}),
427
+ runId: `${runId}-android-adb`,
428
+ });
429
+ const health = readChildHealth(result);
430
+ raw.android = { healthStatus: health.healthStatus, runDir: result.runDir };
431
+ checks.push(buildChildRunCheck({
432
+ health,
433
+ label: 'Android adb',
434
+ name: 'android_adb',
435
+ runDir: result.runDir,
436
+ }));
437
+ }
438
+ catch (error) {
439
+ checks.push(buildExceptionCheck({ error, name: 'android_adb' }));
440
+ }
441
+ }
442
+ if (requirements.includes('ios')) {
443
+ try {
444
+ const result = await iosPreflight({
445
+ ...(iosBundleId ? { bundleId: iosBundleId } : {}),
446
+ ...(iosDevice ? { device: iosDevice } : {}),
447
+ outputDir: path.join(layout.raw, 'ios-simctl-preflight'),
448
+ runId: `${runId}-ios-simctl`,
449
+ xcrunPath,
450
+ });
451
+ const health = readChildHealth(result);
452
+ raw.ios = { healthStatus: health.healthStatus, runDir: result.runDir };
453
+ checks.push(buildChildRunCheck({
454
+ health,
455
+ label: 'iOS simctl',
456
+ name: 'ios_simctl',
457
+ runDir: result.runDir,
458
+ }));
459
+ }
460
+ catch (error) {
461
+ checks.push(buildExceptionCheck({ error, name: 'ios_simctl' }));
462
+ }
463
+ }
464
+ if (requirements.includes('agent-device')) {
465
+ const rawPath = path.join(layout.raw, 'agent-device-check.json');
466
+ try {
467
+ const requiredPlatforms = agentDeviceRequiredPlatforms.length > 0
468
+ ? agentDeviceRequiredPlatforms
469
+ : requirements.filter((requirement) => requirement === 'android' || requirement === 'ios');
470
+ const result = await agentDeviceAvailability({
471
+ agentDevicePath,
472
+ commandTimeoutMs,
473
+ requiredPlatforms,
474
+ });
475
+ await writeRawJson({ filePath: rawPath, value: result });
476
+ raw.agentDevice = { rawPath, status: result.status };
477
+ checks.push(buildAvailabilityCheck({
478
+ label: 'agent-device',
479
+ name: 'agent_device',
480
+ rawPath,
481
+ result,
482
+ }));
483
+ }
484
+ catch (error) {
485
+ checks.push(buildExceptionCheck({ error, name: 'agent_device' }));
486
+ }
487
+ }
488
+ if (requirements.includes('argent')) {
489
+ const rawPath = path.join(layout.raw, 'argent-check.json');
490
+ try {
491
+ const result = await argentAvailability({
492
+ argentCommand,
493
+ ...(argentBaseArgs ? { baseArgs: argentBaseArgs } : {}),
494
+ commandTimeoutMs,
495
+ });
496
+ await writeRawJson({ filePath: rawPath, value: result });
497
+ raw.argent = { rawPath, status: result.status };
498
+ checks.push(buildAvailabilityCheck({
499
+ label: 'Argent',
500
+ name: 'argent',
501
+ rawPath,
502
+ result,
503
+ }));
504
+ }
505
+ catch (error) {
506
+ checks.push(buildExceptionCheck({ error, name: 'argent' }));
507
+ }
508
+ }
509
+ const healthStatus = checks.every((check) => check.status === 'passed') ? 'passed' : 'failed';
510
+ const health = {
511
+ schemaVersion: '1.0.0',
512
+ scenarioId: 'host-doctor',
513
+ flowId: 'host-doctor',
514
+ runId,
515
+ healthStatus,
516
+ checks,
517
+ };
518
+ const verdict = {
519
+ schemaVersion: '1.0.0',
520
+ scenarioId: 'host-doctor',
521
+ flowId: 'host-doctor',
522
+ runId,
523
+ healthStatus,
524
+ verdictStatus: healthStatus === 'passed' ? 'not_evaluated' : 'inconclusive',
525
+ budgetChecks: [],
526
+ summary: healthStatus === 'passed'
527
+ ? 'Host doctor passed; runtime proof can start with the requested host services.'
528
+ : 'Host doctor failed; fix host/device access before treating live proof failures as app regressions.',
529
+ };
530
+ const agentSummary = buildHostDoctorSummary({ health, verdict });
531
+ await writeJsonArtifact({
532
+ filePath: layout.health,
533
+ value: health,
534
+ schema: SCHEMAS.health,
535
+ label: 'Host doctor health',
536
+ });
537
+ await writeJsonArtifact({
538
+ filePath: layout.verdict,
539
+ value: verdict,
540
+ schema: SCHEMAS.verdict,
541
+ label: 'Host doctor verdict',
542
+ });
543
+ await writeRawJson({
544
+ filePath: path.join(layout.raw, 'host-doctor.json'),
545
+ value: raw,
546
+ });
547
+ await writeTextArtifact({
548
+ filePath: layout.agentSummary,
549
+ content: agentSummary,
550
+ });
551
+ return {
552
+ agentSummary,
553
+ health,
554
+ raw,
555
+ runDir,
556
+ verdict,
557
+ };
558
+ }
559
+ /**
560
+ * Runs the host doctor CLI.
561
+ *
562
+ * @returns {Promise<void>}
563
+ */
564
+ async function main() {
565
+ const argv = process.argv.slice(2);
566
+ if (hasHelpFlag(argv)) {
567
+ usage(process.stdout);
568
+ return;
569
+ }
570
+ loadAslLocalEnv();
571
+ const args = parseArgs(argv);
572
+ const requirements = parseRequirements(readStringArgOrEnv(args.require, ['ASL_HOST_DOCTOR_REQUIRE']));
573
+ const agentDeviceRequiredPlatformsValue = readStringArgOrEnv(args['agent-device-require-platforms'], ['ASL_AGENT_DEVICE_REQUIRED_PLATFORMS']);
574
+ const agentDeviceRequiredPlatforms = agentDeviceRequiredPlatformsValue
575
+ ? parseRequiredPlatforms(agentDeviceRequiredPlatformsValue)
576
+ : [];
577
+ const argentBaseArgsValue = readStringArgOrEnv(args['base-args'], ['ASL_ARGENT_BASE_ARGS']);
578
+ const argentBaseArgs = argentBaseArgsValue
579
+ ? parseBaseArgs(argentBaseArgsValue)
580
+ : null;
581
+ const adbPath = readStringArgOrEnv(args.adb, ['ASL_ANDROID_ADB_BIN']);
582
+ const agentDevicePath = readStringArgOrEnv(args['agent-device'], ['ASL_AGENT_DEVICE_BIN']);
583
+ const androidPackageName = readStringArgOrEnv(args['android-package'], [
584
+ 'ASL_ANDROID_APP_ID',
585
+ 'ASL_EXAMPLE_ANDROID_APP_ID',
586
+ ]);
587
+ const androidSerial = readStringArgOrEnv(args['android-serial'], [
588
+ 'ASL_ANDROID_SERIAL',
589
+ 'ASL_EXAMPLE_ANDROID_SERIAL',
590
+ ]);
591
+ const argentCommand = readStringArgOrEnv(args.argent, ['ASL_ARGENT_BIN']);
592
+ const commandTimeoutMsValue = readStringArgOrEnv(args['command-timeout-ms'], ['ASL_HOST_DOCTOR_COMMAND_TIMEOUT_MS']);
593
+ const iosBundleId = readStringArgOrEnv(args['ios-bundle'], [
594
+ 'ASL_IOS_APP_ID',
595
+ 'ASL_EXAMPLE_IOS_APP_ID',
596
+ ]);
597
+ const iosDevice = readStringArgOrEnv(args['ios-device'], [
598
+ 'ASL_IOS_UDID',
599
+ 'ASL_EXAMPLE_IOS_UDID',
600
+ ]);
601
+ const xcrunPath = readStringArgOrEnv(args.xcrun, ['ASL_XCRUN_PATH', 'ASL_IOS_XCRUN_BIN']);
602
+ const result = await runHostDoctor({
603
+ ...(adbPath ? { adbPath } : {}),
604
+ ...(agentDevicePath ? { agentDevicePath } : {}),
605
+ agentDeviceRequiredPlatforms,
606
+ ...(androidPackageName ? { androidPackageName } : {}),
607
+ ...(androidSerial ? { androidSerial } : {}),
608
+ ...(argentCommand ? { argentCommand } : {}),
609
+ ...(argentBaseArgs ? { argentBaseArgs } : {}),
610
+ commandTimeoutMs: parsePositiveInteger(commandTimeoutMsValue, 30_000),
611
+ ...(iosBundleId ? { iosBundleId } : {}),
612
+ ...(iosDevice ? { iosDevice } : {}),
613
+ ...(typeof args.out === 'string' ? { outputDir: args.out } : {}),
614
+ requirements,
615
+ ...(typeof args['run-id'] === 'string' ? { runId: args['run-id'] } : {}),
616
+ ...(xcrunPath ? { xcrunPath } : {}),
617
+ });
618
+ process.stdout.write(`${result.runDir}\n`);
619
+ if (result.health.healthStatus !== 'passed') {
620
+ process.exitCode = 1;
621
+ }
622
+ }
623
+ if (require.main === module) {
624
+ main().catch((error) => {
625
+ console.error(error instanceof Error ? error.message : String(error));
626
+ process.exitCode = 1;
627
+ });
628
+ }