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.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/app/profile-session.ts +812 -0
- package/core/config-template.json +41 -0
- package/dist/core/agent-summary.d.ts +15 -0
- package/dist/core/agent-summary.js +177 -0
- package/dist/core/artifact-contract.d.ts +151 -0
- package/dist/core/artifact-contract.js +897 -0
- package/dist/core/artifact-layout.d.ts +56 -0
- package/dist/core/artifact-layout.js +61 -0
- package/dist/core/artifact-writer.d.ts +44 -0
- package/dist/core/artifact-writer.js +55 -0
- package/dist/core/comparison.d.ts +133 -0
- package/dist/core/comparison.js +294 -0
- package/dist/core/evidence-interpreter.d.ts +28 -0
- package/dist/core/evidence-interpreter.js +69 -0
- package/dist/core/execution-plan.d.ts +44 -0
- package/dist/core/execution-plan.js +95 -0
- package/dist/core/planner.d.ts +132 -0
- package/dist/core/planner.js +812 -0
- package/dist/core/ports.d.ts +198 -0
- package/dist/core/ports.js +146 -0
- package/dist/core/run-index.d.ts +62 -0
- package/dist/core/run-index.js +143 -0
- package/dist/core/schema-validator.d.ts +86 -0
- package/dist/core/schema-validator.js +407 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +27 -0
- package/dist/runner/agent-device-driver.d.ts +126 -0
- package/dist/runner/agent-device-driver.js +168 -0
- package/dist/runner/agent-device.d.ts +295 -0
- package/dist/runner/agent-device.js +1271 -0
- package/dist/runner/android-adb-driver.d.ts +175 -0
- package/dist/runner/android-adb-driver.js +399 -0
- package/dist/runner/android-adb.d.ts +254 -0
- package/dist/runner/android-adb.js +1618 -0
- package/dist/runner/argent-driver.d.ts +183 -0
- package/dist/runner/argent-driver.js +297 -0
- package/dist/runner/argent.d.ts +349 -0
- package/dist/runner/argent.js +1211 -0
- package/dist/runner/check-plan.d.ts +45 -0
- package/dist/runner/check-plan.js +210 -0
- package/dist/runner/cli.d.ts +20 -0
- package/dist/runner/cli.js +23 -0
- package/dist/runner/compare-latest.d.ts +99 -0
- package/dist/runner/compare-latest.js +233 -0
- package/dist/runner/compare.d.ts +58 -0
- package/dist/runner/compare.js +157 -0
- package/dist/runner/demo-loop.d.ts +45 -0
- package/dist/runner/demo-loop.js +170 -0
- package/dist/runner/example-android-live.d.ts +137 -0
- package/dist/runner/example-android-live.js +454 -0
- package/dist/runner/example-ios-live.d.ts +137 -0
- package/dist/runner/example-ios-live.js +471 -0
- package/dist/runner/host-doctor.d.ts +131 -0
- package/dist/runner/host-doctor.js +628 -0
- package/dist/runner/init-project.d.ts +88 -0
- package/dist/runner/init-project.js +263 -0
- package/dist/runner/ios-simctl-driver.d.ts +69 -0
- package/dist/runner/ios-simctl-driver.js +97 -0
- package/dist/runner/ios-simctl.d.ts +254 -0
- package/dist/runner/ios-simctl.js +1415 -0
- package/dist/runner/live-android.d.ts +137 -0
- package/dist/runner/live-android.js +539 -0
- package/dist/runner/live-comparison.d.ts +67 -0
- package/dist/runner/live-comparison.js +147 -0
- package/dist/runner/live-ios.d.ts +137 -0
- package/dist/runner/live-ios.js +460 -0
- package/dist/runner/live-proof-summary.d.ts +263 -0
- package/dist/runner/live-proof-summary.js +465 -0
- package/dist/runner/live-proof.d.ts +467 -0
- package/dist/runner/live-proof.js +920 -0
- package/dist/runner/local-env.d.ts +64 -0
- package/dist/runner/local-env.js +155 -0
- package/dist/runner/profile-android.d.ts +82 -0
- package/dist/runner/profile-android.js +671 -0
- package/dist/runner/profile-ios.d.ts +108 -0
- package/dist/runner/profile-ios.js +532 -0
- package/dist/runner/profile-mobile.d.ts +254 -0
- package/dist/runner/profile-mobile.js +1307 -0
- package/dist/runner/validate-project.d.ts +273 -0
- package/dist/runner/validate-project.js +1501 -0
- package/docs/adapters.md +145 -0
- package/docs/api.md +94 -0
- package/docs/authoring.md +196 -0
- package/docs/concepts.md +136 -0
- package/docs/consumer-rehearsal.md +115 -0
- package/docs/contracts.md +267 -0
- package/docs/live-proofs.md +270 -0
- package/docs/principles.md +46 -0
- package/examples/event-logs/app-startup-baseline.log +4 -0
- package/examples/event-logs/app-startup-current.log +4 -0
- package/examples/minimal-app/README.md +70 -0
- package/examples/mobile-app/README.md +302 -0
- package/examples/mobile-app/app.json +22 -0
- package/examples/mobile-app/asl/package-scripts.json +32 -0
- package/examples/mobile-app/asl.config.json +37 -0
- package/examples/mobile-app/event-logs/android-app-startup.log +4 -0
- package/examples/mobile-app/event-logs/android-open-close-cycle.log +12 -0
- package/examples/mobile-app/event-logs/android-scroll-settle.log +12 -0
- package/examples/mobile-app/event-logs/app-startup.log +4 -0
- package/examples/mobile-app/event-logs/open-close-cycle.log +12 -0
- package/examples/mobile-app/event-logs/scroll-settle.log +12 -0
- package/examples/mobile-app/index.ts +20 -0
- package/examples/mobile-app/metro.config.js +20 -0
- package/examples/mobile-app/package.json +62 -0
- package/examples/mobile-app/patches/expo-modules-jsi@56.0.10.patch +19 -0
- package/examples/mobile-app/plugins/with-ios-build-compat.js +271 -0
- package/examples/mobile-app/pnpm-lock.yaml +4440 -0
- package/examples/mobile-app/runner-manifests/evidence-provider.json +79 -0
- package/examples/mobile-app/runner-manifests/primary-runner.json +19 -0
- package/examples/mobile-app/scenarios/android/app-startup-video.json +73 -0
- package/examples/mobile-app/scenarios/android/app-startup.json +44 -0
- package/examples/mobile-app/scenarios/android/open-close-cycle.json +54 -0
- package/examples/mobile-app/scenarios/android/scroll-settle.json +49 -0
- package/examples/mobile-app/scenarios/ios/app-startup.json +44 -0
- package/examples/mobile-app/scenarios/ios/open-close-cycle.json +54 -0
- package/examples/mobile-app/scenarios/ios/scroll-settle.json +49 -0
- package/examples/mobile-app/scenarios/mobile/app-startup.json +91 -0
- package/examples/mobile-app/scenarios/mobile/open-close-cycle.json +160 -0
- package/examples/mobile-app/scenarios/mobile/scroll-settle.json +148 -0
- package/examples/mobile-app/scripts/asl-capture-accessibility-provider.mjs +112 -0
- package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +127 -0
- package/examples/mobile-app/src/devtools/profile-session.ts +7 -0
- package/examples/mobile-app/src/example-screen.tsx +322 -0
- package/examples/mobile-app/tsconfig.json +16 -0
- package/examples/mobile-app/tsconfig.typecheck.json +13 -0
- package/examples/runners/README.md +44 -0
- package/examples/runners/adb-android.json +25 -0
- package/examples/runners/agent-device-android.json +27 -0
- package/examples/runners/agent-device-ios.json +27 -0
- package/examples/runners/argent-android.json +32 -0
- package/examples/runners/argent-ios.json +32 -0
- package/examples/runners/argent-react-profiler-provider.json +15 -0
- package/examples/runners/axe-accessibility-provider.json +24 -0
- package/examples/runners/manual-log-ingest.json +9 -0
- package/examples/runners/rozenite-profiler-provider.json +9 -0
- package/examples/runners/script-accessibility-provider.json +24 -0
- package/examples/runners/script-memory-provider.json +24 -0
- package/examples/runners/script-network-provider.json +24 -0
- package/examples/runners/script-profiler-provider.json +30 -0
- package/examples/runners/xcodebuildmcp-ios.json +29 -0
- package/examples/scenarios/ios/app-startup.json +28 -0
- package/examples/scenarios/ios/open-close-cycle.json +35 -0
- package/examples/scenarios/mobile/app-startup.json +72 -0
- package/examples/scenarios/mobile/media-open-close.json +141 -0
- package/examples/scenarios/mobile/open-close-cycle.json +135 -0
- package/examples/scenarios/mobile/scroll-settle.json +106 -0
- package/package.json +240 -0
- package/schemas/budget-verdict.schema.json +115 -0
- package/schemas/causal-run.schema.json +279 -0
- package/schemas/comparison.schema.json +196 -0
- package/schemas/health.schema.json +108 -0
- package/schemas/live-proof-set.schema.json +195 -0
- package/schemas/live-proof.schema.json +413 -0
- package/schemas/manifest.schema.json +204 -0
- package/schemas/metrics.schema.json +137 -0
- package/schemas/project-validation.schema.json +343 -0
- package/schemas/runner-capabilities.schema.json +217 -0
- package/schemas/scenario.schema.json +400 -0
- package/schemas/verdict.schema.json +88 -0
- package/templates/evidence-provider.json +83 -0
- package/templates/gitignore-snippet +9 -0
- package/templates/integration-readme.md +125 -0
- package/templates/mobile-scenario.json +133 -0
- package/templates/package-scripts.json +32 -0
- package/templates/primary-runner.json +19 -0
- package/templates/project.config.json +37 -0
- package/templates/scripts/asl-capture-accessibility-provider.mjs +112 -0
- 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
|
+
}
|