agent-scenario-loop 0.1.2 → 0.1.4

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 (87) hide show
  1. package/README.md +9 -9
  2. package/app/profile-session.ts +352 -12
  3. package/dist/core/agent-summary.d.ts +3 -2
  4. package/dist/core/agent-summary.js +44 -2
  5. package/dist/core/artifact-contract.d.ts +28 -8
  6. package/dist/core/artifact-contract.js +676 -26
  7. package/dist/core/comparison.d.ts +57 -3
  8. package/dist/core/comparison.js +113 -1
  9. package/dist/core/planner.d.ts +32 -1
  10. package/dist/core/planner.js +144 -0
  11. package/dist/core/run-index.d.ts +4 -0
  12. package/dist/core/run-index.js +55 -1
  13. package/dist/core/schema-validator.d.ts +2 -0
  14. package/dist/core/schema-validator.js +2 -0
  15. package/dist/runner/android-adb-driver.d.ts +7 -2
  16. package/dist/runner/android-adb-driver.js +7 -1
  17. package/dist/runner/android-adb.d.ts +40 -5
  18. package/dist/runner/android-adb.js +1046 -664
  19. package/dist/runner/compare-latest.d.ts +8 -4
  20. package/dist/runner/compare-latest.js +24 -5
  21. package/dist/runner/example-android-live.d.ts +10 -1
  22. package/dist/runner/example-android-live.js +55 -0
  23. package/dist/runner/example-ios-live.d.ts +10 -1
  24. package/dist/runner/example-ios-live.js +55 -0
  25. package/dist/runner/ios-simctl.d.ts +6 -0
  26. package/dist/runner/ios-simctl.js +7 -0
  27. package/dist/runner/live-comparison.d.ts +2 -2
  28. package/dist/runner/live-comparison.js +2 -1
  29. package/dist/runner/live-proof-summary.d.ts +5 -4
  30. package/dist/runner/live-proof-summary.js +12 -2
  31. package/dist/runner/live-proof.d.ts +3 -2
  32. package/dist/runner/live-proof.js +9 -2
  33. package/dist/runner/profile-android.d.ts +16 -1
  34. package/dist/runner/profile-android.js +364 -26
  35. package/dist/runner/profile-ios.d.ts +13 -2
  36. package/dist/runner/profile-ios.js +341 -19
  37. package/dist/runner/profile-mobile.d.ts +39 -3
  38. package/dist/runner/profile-mobile.js +1054 -42
  39. package/dist/runner/validate-project.js +3 -0
  40. package/dist/scripts/consumer-rehearsal.d.ts +119 -0
  41. package/dist/scripts/consumer-rehearsal.js +757 -0
  42. package/dist/scripts/downstream-local-package-gate.d.ts +2 -0
  43. package/dist/scripts/downstream-local-package-gate.js +264 -0
  44. package/dist/scripts/package-smoke.d.ts +96 -0
  45. package/dist/scripts/package-smoke.js +2282 -0
  46. package/dist/scripts/release-readiness.d.ts +2 -0
  47. package/dist/scripts/release-readiness.js +520 -0
  48. package/docs/adapters.md +7 -1
  49. package/docs/api.md +2 -2
  50. package/docs/architecture.md +90 -0
  51. package/docs/authoring.md +39 -3
  52. package/docs/concepts.md +3 -24
  53. package/docs/consumer-rehearsal.md +31 -1
  54. package/docs/contracts.md +45 -101
  55. package/docs/external-adapter-protocol.md +219 -0
  56. package/docs/live-proofs.md +86 -3
  57. package/docs/principles.md +9 -15
  58. package/examples/mobile-app/README.md +12 -0
  59. package/examples/mobile-app/runner-manifests/evidence-provider.json +3 -3
  60. package/examples/mobile-app/runner-manifests/primary-runner.json +1 -0
  61. package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +25 -0
  62. package/examples/runners/README.md +4 -3
  63. package/examples/runners/adb-android.json +1 -0
  64. package/examples/runners/agent-device-android.json +1 -0
  65. package/examples/runners/agent-device-ios.json +1 -0
  66. package/examples/runners/argent-android.json +1 -0
  67. package/examples/runners/argent-ios.json +1 -0
  68. package/examples/runners/axe-accessibility-provider.json +2 -2
  69. package/examples/runners/script-accessibility-provider.json +2 -2
  70. package/examples/runners/script-memory-provider.json +2 -2
  71. package/examples/runners/script-network-provider.json +2 -2
  72. package/examples/runners/script-profiler-provider.json +2 -2
  73. package/examples/runners/xcodebuildmcp-ios.json +1 -0
  74. package/package.json +12 -3
  75. package/schemas/causal-run.schema.json +85 -2
  76. package/schemas/comparison.schema.json +130 -2
  77. package/schemas/external-adapter-message.schema.json +693 -0
  78. package/schemas/health.schema.json +72 -0
  79. package/schemas/live-proof-set.schema.json +1 -1
  80. package/schemas/live-proof.schema.json +14 -6
  81. package/schemas/manifest.schema.json +515 -4
  82. package/schemas/profiler.schema.json +243 -0
  83. package/schemas/runner-capabilities.schema.json +28 -2
  84. package/schemas/scenario.schema.json +34 -2
  85. package/templates/evidence-provider.json +3 -3
  86. package/templates/primary-runner.json +1 -0
  87. package/templates/scripts/asl-capture-profiler-provider.mjs +20 -0
@@ -8,8 +8,13 @@ type AndroidProfileOptions = {
8
8
  };
9
9
  type AndroidAdbProfileCommand = {
10
10
  command: string;
11
+ commandId?: string;
11
12
  label?: string;
13
+ queueId?: string;
14
+ sequence?: number;
15
+ waitForMilestone?: string;
12
16
  waitMs?: number;
17
+ waitTimeoutMs?: number;
13
18
  };
14
19
  type AndroidAdbDriverStep = import('./android-adb').AndroidAdbDriverStep;
15
20
  /**
@@ -30,6 +35,16 @@ declare function resolveProfileSessionCaptureWaitMs({ args, profileSessionEnable
30
35
  profileSessionEnabled: boolean;
31
36
  scenario: Record<string, any>;
32
37
  }): number;
38
+ /**
39
+ * Derives a bounded logcat tail large enough to keep command-session evidence.
40
+ *
41
+ * @param {{commands: AndroidAdbProfileCommand[], profileSessionEnabled: boolean}} options
42
+ * @returns {number}
43
+ */
44
+ declare function deriveProfileSessionLogcatLines({ commands, profileSessionEnabled, }: {
45
+ commands: AndroidAdbProfileCommand[];
46
+ profileSessionEnabled: boolean;
47
+ }): number;
33
48
  /**
34
49
  * Expands normalized scenario evidence steps into Android adb driver actions.
35
50
  *
@@ -79,4 +94,4 @@ declare function runProfileAndroid(args: import('./profile-mobile').CliArgs, opt
79
94
  * @returns {Promise<void>}
80
95
  */
81
96
  declare function main(): Promise<void>;
82
- export { deriveProfileSessionCaptureWaitMs, main, parseArgs, resolveAndroidAdbProfileCommands, resolveAndroidAdbDriverSteps, resolveProfileSessionCaptureWaitMs, readAndroidAdbVideoCapturePath, validateAndroidAdbDriverSteps, runProfileAndroid, summarizeFailedAndroidChecks, usage, };
97
+ export { deriveProfileSessionCaptureWaitMs, deriveProfileSessionLogcatLines, main, parseArgs, resolveAndroidAdbProfileCommands, resolveAndroidAdbDriverSteps, resolveProfileSessionCaptureWaitMs, readAndroidAdbVideoCapturePath, validateAndroidAdbDriverSteps, runProfileAndroid, summarizeFailedAndroidChecks, usage, };
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.usage = exports.parseArgs = void 0;
5
5
  exports.deriveProfileSessionCaptureWaitMs = deriveProfileSessionCaptureWaitMs;
6
+ exports.deriveProfileSessionLogcatLines = deriveProfileSessionLogcatLines;
6
7
  exports.main = main;
7
8
  exports.resolveAndroidAdbProfileCommands = resolveAndroidAdbProfileCommands;
8
9
  exports.resolveAndroidAdbDriverSteps = resolveAndroidAdbDriverSteps;
@@ -24,8 +25,27 @@ const { runAgentDeviceCapture } = require('./agent-device');
24
25
  const { loadAslLocalEnv, readStringArgOrEnv } = require('./local-env');
25
26
  const PROFILE_SESSION_CAPTURE_BOOTSTRAP_MS = 1000;
26
27
  const PROFILE_SESSION_CAPTURE_MAX_MS = 120000;
28
+ const PROFILE_SESSION_LOGCAT_MIN_LINES = 1000;
29
+ const PROFILE_SESSION_LOGCAT_MAX_LINES = 20000;
30
+ const PROFILE_SESSION_LOGCAT_LINES_PER_COMMAND = 300;
27
31
  const DEFAULT_ANDROID_PROFILE_SESSION_STORAGE_KEY = 'agent-scenario-loop.profile-session.1';
28
32
  const DEFAULT_ANDROID_PROFILE_COMMAND_STORAGE_KEY = 'agent-scenario-loop.profile-commands.1';
33
+ const DEFAULT_ANDROID_DEV_CLIENT_READY_PATTERN = 'Running "main"';
34
+ const MANIFEST_LIFECYCLE_PHASES = new Set([
35
+ 'cold-launch',
36
+ 'warm-launch',
37
+ 'hot-launch',
38
+ 'resume',
39
+ 'foreground',
40
+ 'background',
41
+ 'force-stop',
42
+ 'process-death',
43
+ 'scene-recreation',
44
+ 'activity-recreation',
45
+ 'os-reclaim',
46
+ 'reboot',
47
+ 'relaunch',
48
+ ]);
29
49
  /**
30
50
  * Reads and parses a JSON object from disk.
31
51
  *
@@ -54,6 +74,22 @@ function isEnabled(value) {
54
74
  function readPositiveInteger(value, fallback) {
55
75
  return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback;
56
76
  }
77
+ /**
78
+ * Resolves the lifecycle phase this runner is prepared to assert.
79
+ *
80
+ * @param {import('./profile-mobile').CliArgs} args
81
+ * @returns {string}
82
+ */
83
+ function resolveManifestLifecyclePhase(args) {
84
+ const lifecyclePhase = readScalarArg(args['lifecycle-phase']);
85
+ if (lifecyclePhase === undefined) {
86
+ return 'cold-launch';
87
+ }
88
+ if (typeof lifecyclePhase !== 'string' || !MANIFEST_LIFECYCLE_PHASES.has(lifecyclePhase)) {
89
+ throw new Error(`Unsupported --lifecycle-phase "${String(lifecyclePhase)}". Expected one of ${Array.from(MANIFEST_LIFECYCLE_PHASES).join(', ')}.`);
90
+ }
91
+ return lifecyclePhase;
92
+ }
57
93
  /**
58
94
  * Reads the number of scenario iterations that can emit app-owned truth events.
59
95
  *
@@ -119,7 +155,7 @@ function resolveAndroidPackageName({ args, config, }) {
119
155
  * @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
120
156
  * @returns {string}
121
157
  */
122
- function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
158
+ function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitMs, waitTimeoutMs, }) {
123
159
  const scheme = typeof config.app?.profileSessionScheme === 'string'
124
160
  ? config.app.profileSessionScheme
125
161
  : typeof config.app?.scheme === 'string'
@@ -128,6 +164,24 @@ function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
128
164
  const params = new URLSearchParams({ runId, scenario });
129
165
  if (action === 'command' && command) {
130
166
  params.set('command', command);
167
+ if (commandId) {
168
+ params.set('commandId', commandId);
169
+ }
170
+ if (typeof sequence === 'number') {
171
+ params.set('sequence', String(sequence));
172
+ }
173
+ if (queueId) {
174
+ params.set('queueId', queueId);
175
+ }
176
+ if (waitForMilestone) {
177
+ params.set('waitForMilestone', waitForMilestone);
178
+ }
179
+ if (typeof waitMs === 'number') {
180
+ params.set('waitMs', String(waitMs));
181
+ }
182
+ if (typeof waitTimeoutMs === 'number') {
183
+ params.set('waitTimeoutMs', String(waitTimeoutMs));
184
+ }
131
185
  }
132
186
  return `${scheme}://profile-session/${action}?${params.toString()}`;
133
187
  }
@@ -138,6 +192,23 @@ function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
138
192
  * @returns {import('./android-adb').AndroidAsyncStorageWrite[]}
139
193
  */
140
194
  function buildProfileSessionStorageWrites({ commands, commandStorageKey, commandWaitMs, runId, scenario, sessionStorageKey, }) {
195
+ const storedCommands = commands.map((profileCommand, index) => {
196
+ const timestampPlaceholder = `${ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER}+${index + 1}`;
197
+ return {
198
+ id: `${scenario}-${index + 1}-${profileCommand.command}`,
199
+ scenario,
200
+ runId,
201
+ command: profileCommand.command,
202
+ ...(typeof profileCommand.commandId === 'string' ? { commandId: profileCommand.commandId } : {}),
203
+ ...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
204
+ ...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
205
+ ...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
206
+ ...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
207
+ ...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
208
+ timestamp: timestampPlaceholder,
209
+ };
210
+ });
211
+ const commandWaitMsTotal = commands.reduce((total, profileCommand) => (total + (typeof profileCommand.waitMs === 'number' && profileCommand.waitMs > 0 ? profileCommand.waitMs : 0)), 0);
141
212
  return [
142
213
  {
143
214
  clearKeys: [commandStorageKey],
@@ -151,21 +222,14 @@ function buildProfileSessionStorageWrites({ commands, commandStorageKey, command
151
222
  }).replace(`"${ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER}"`, ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER),
152
223
  waitMs: commandWaitMs,
153
224
  },
154
- ...commands.map((profileCommand, index) => {
155
- const timestamp = Date.now() + index + 1;
156
- return {
157
- key: commandStorageKey,
158
- label: profileCommand.label ?? `profile-command-${index + 1}`,
159
- value: JSON.stringify([{
160
- id: `${timestamp}-${scenario}-${profileCommand.command}`,
161
- scenario,
162
- runId,
163
- command: profileCommand.command,
164
- timestamp,
165
- }]),
166
- ...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
167
- };
168
- }),
225
+ ...(storedCommands.length > 0
226
+ ? [{
227
+ key: commandStorageKey,
228
+ label: 'profile-command-queue',
229
+ value: JSON.stringify(storedCommands),
230
+ ...(commandWaitMsTotal > 0 ? { waitMs: commandWaitMsTotal } : {}),
231
+ }]
232
+ : []),
169
233
  ];
170
234
  }
171
235
  /**
@@ -218,6 +282,32 @@ function resolveProfileSessionCaptureWaitMs({ args, profileSessionEnabled, scena
218
282
  }
219
283
  return profileSessionEnabled ? deriveProfileSessionCaptureWaitMs(scenario) : 0;
220
284
  }
285
+ /**
286
+ * Derives a bounded logcat tail large enough to keep command-session evidence.
287
+ *
288
+ * @param {{commands: AndroidAdbProfileCommand[], profileSessionEnabled: boolean}} options
289
+ * @returns {number}
290
+ */
291
+ function deriveProfileSessionLogcatLines({ commands, profileSessionEnabled, }) {
292
+ if (!profileSessionEnabled || commands.length === 0) {
293
+ return PROFILE_SESSION_LOGCAT_MIN_LINES;
294
+ }
295
+ const derivedLines = PROFILE_SESSION_LOGCAT_MIN_LINES + (commands.length * PROFILE_SESSION_LOGCAT_LINES_PER_COMMAND);
296
+ return Math.min(Math.max(derivedLines, PROFILE_SESSION_LOGCAT_MIN_LINES), PROFILE_SESSION_LOGCAT_MAX_LINES);
297
+ }
298
+ /**
299
+ * Resolves Android logcat capture lines, keeping explicit CLI input authoritative.
300
+ *
301
+ * @param {{args: import('./profile-mobile').CliArgs, commands: AndroidAdbProfileCommand[], profileSessionEnabled: boolean}} options
302
+ * @returns {number}
303
+ */
304
+ function resolveProfileSessionLogcatLines({ args, commands, profileSessionEnabled, }) {
305
+ const explicitLogcatLines = readScalarArg(args['logcat-lines']);
306
+ if (explicitLogcatLines !== undefined) {
307
+ return parsePositiveInteger(explicitLogcatLines, PROFILE_SESSION_LOGCAT_MIN_LINES);
308
+ }
309
+ return deriveProfileSessionLogcatLines({ commands, profileSessionEnabled });
310
+ }
221
311
  /**
222
312
  * Reads Android adb adapter metadata from a normalized scenario step.
223
313
  *
@@ -271,14 +361,196 @@ function appendCaptureArg({ args, value, }) {
271
361
  function resolveExecutionPlanProfileCommands(scenario) {
272
362
  const executionPlan = buildScenarioExecutionPlan(scenario);
273
363
  const repeat = readPositiveInteger(scenario.defaultIterations, readPositiveInteger(scenario.cycles?.iterations, 1));
274
- const commands = executionPlan.steps
275
- .filter((step) => step.portMethod === 'executeStep' && typeof step.command === 'string')
276
- .map((step) => ({
277
- command: step.command,
278
- label: step.id,
279
- waitMs: readStepWaitMs(step),
364
+ const commands = [];
365
+ for (const [index, step] of executionPlan.steps.entries()) {
366
+ if (step.portMethod !== 'executeStep' || typeof step.command !== 'string') {
367
+ continue;
368
+ }
369
+ const nextStep = executionPlan.steps[index + 1];
370
+ commands.push({
371
+ command: step.command,
372
+ commandId: step.id,
373
+ label: step.id,
374
+ queueId: scenario.id ?? scenario.name,
375
+ waitMs: readStepWaitMs(step),
376
+ ...(nextStep?.portMethod === 'waitForTruthEvent' && typeof nextStep.milestone === 'string'
377
+ ? {
378
+ waitForMilestone: resolveMilestoneEventName(scenario, nextStep.milestone),
379
+ waitTimeoutMs: readPositiveInteger(nextStep.timeoutMs, 0),
380
+ }
381
+ : {}),
382
+ });
383
+ }
384
+ return expandProfileCommandCycles(scenario, commands, repeat);
385
+ }
386
+ /**
387
+ * Returns true when a command is part of the setup prefix that establishes app readiness before repeated cycle work.
388
+ *
389
+ * @param {Record<string, unknown>} scenario
390
+ * @param {AndroidAdbProfileCommand} command
391
+ * @returns {boolean}
392
+ */
393
+ function isReadinessSetupProfileCommand(scenario, command) {
394
+ if (typeof command.waitForMilestone !== 'string') {
395
+ return false;
396
+ }
397
+ const readyEvent = resolveScenarioReadinessEvent(scenario);
398
+ return typeof readyEvent === 'string' && command.waitForMilestone === readyEvent;
399
+ }
400
+ /**
401
+ * Reads a string id list from scenario cycles metadata.
402
+ *
403
+ * @param {unknown} value
404
+ * @returns {Set<string>}
405
+ */
406
+ function readCycleStepIdSet(value) {
407
+ return new Set(Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : []);
408
+ }
409
+ /**
410
+ * Resolves the milestone ids that represent measured cycle boundaries.
411
+ *
412
+ * @param {Record<string, unknown>} scenario
413
+ * @returns {Set<string>}
414
+ */
415
+ function resolveMeasuredCycleMilestoneEvents(scenario) {
416
+ const milestones = new Set();
417
+ for (const budget of Array.isArray(scenario.budgets) ? scenario.budgets : []) {
418
+ if (!budget || typeof budget !== 'object' || budget.source !== 'milestone') {
419
+ continue;
420
+ }
421
+ if (typeof budget.fromMilestone === 'string') {
422
+ milestones.add(resolveMilestoneEventName(scenario, budget.fromMilestone));
423
+ }
424
+ if (typeof budget.toMilestone === 'string') {
425
+ milestones.add(resolveMilestoneEventName(scenario, budget.toMilestone));
426
+ }
427
+ }
428
+ return milestones;
429
+ }
430
+ /**
431
+ * Resolves how many leading commands are setup-only before repeated cycle work.
432
+ *
433
+ * @param {Record<string, unknown>} scenario
434
+ * @param {AndroidAdbProfileCommand[]} commands
435
+ * @returns {number}
436
+ */
437
+ function resolveSetupCommandCount(scenario, commands) {
438
+ const explicitSetupStepIds = readCycleStepIdSet(scenario.cycles?.setupStepIds);
439
+ if (explicitSetupStepIds.size > 0) {
440
+ let count = 0;
441
+ for (const command of commands) {
442
+ if (!command.commandId || !explicitSetupStepIds.has(command.commandId)) {
443
+ break;
444
+ }
445
+ count += 1;
446
+ }
447
+ return count;
448
+ }
449
+ const explicitBodyStepIds = readCycleStepIdSet(scenario.cycles?.bodyStepIds);
450
+ if (explicitBodyStepIds.size > 0) {
451
+ const firstBodyIndex = commands.findIndex((command) => (typeof command.commandId === 'string' && explicitBodyStepIds.has(command.commandId)));
452
+ return firstBodyIndex > 0 ? firstBodyIndex : 0;
453
+ }
454
+ let readinessSetupCommandCount = 0;
455
+ for (const command of commands) {
456
+ if (!isReadinessSetupProfileCommand(scenario, command)) {
457
+ break;
458
+ }
459
+ readinessSetupCommandCount += 1;
460
+ }
461
+ if (readinessSetupCommandCount > 0) {
462
+ return readinessSetupCommandCount;
463
+ }
464
+ const measuredMilestones = resolveMeasuredCycleMilestoneEvents(scenario);
465
+ if (measuredMilestones.size === 0) {
466
+ return 0;
467
+ }
468
+ const firstMeasuredCommandIndex = commands.findIndex((command) => (typeof command.waitForMilestone === 'string' && measuredMilestones.has(command.waitForMilestone)));
469
+ return firstMeasuredCommandIndex > 0 ? firstMeasuredCommandIndex : 0;
470
+ }
471
+ /**
472
+ * Expands commands so setup/readiness commands execute once while cycle-body commands repeat.
473
+ *
474
+ * @param {Record<string, unknown>} scenario
475
+ * @param {AndroidAdbProfileCommand[]} commands
476
+ * @param {number} repeat
477
+ * @returns {AndroidAdbProfileCommand[]}
478
+ */
479
+ function expandProfileCommandCycles(scenario, commands, repeat) {
480
+ const setupCommandCount = resolveSetupCommandCount(scenario, commands);
481
+ const setupCommands = commands.slice(0, setupCommandCount);
482
+ const cycleCommands = commands.slice(setupCommandCount);
483
+ const expandedCommands = cycleCommands.length === 0
484
+ ? setupCommands
485
+ : [
486
+ ...setupCommands,
487
+ ...Array.from({ length: repeat }).flatMap(() => cycleCommands),
488
+ ];
489
+ return expandedCommands.map((command, index) => ({
490
+ ...command,
491
+ sequence: index + 1,
280
492
  }));
281
- return Array.from({ length: repeat }).flatMap(() => commands);
493
+ }
494
+ /**
495
+ * Resolves a portable milestone id to the app truth event that releases command sequencing.
496
+ *
497
+ * @param {Record<string, unknown>} scenario
498
+ * @param {string} milestone
499
+ * @returns {string}
500
+ */
501
+ function resolveMilestoneEventName(scenario, milestone) {
502
+ const milestoneEntry = Array.isArray(scenario.milestones)
503
+ ? scenario.milestones.find((entry) => entry?.id === milestone)
504
+ : undefined;
505
+ if (typeof milestoneEntry?.event === 'string' && milestoneEntry.event.length > 0) {
506
+ return milestoneEntry.event;
507
+ }
508
+ const metricEvent = scenario.metricEvents?.[milestone];
509
+ return typeof metricEvent === 'string' && metricEvent.length > 0 ? metricEvent : milestone;
510
+ }
511
+ /**
512
+ * Resolves the scenario truth event that represents initial app readiness.
513
+ *
514
+ * @param {Record<string, unknown>} scenario
515
+ * @returns {string | null}
516
+ */
517
+ function resolveScenarioReadinessEvent(scenario) {
518
+ const explicitReadyEvent = scenario.truthEvents?.ready?.event;
519
+ if (typeof explicitReadyEvent === 'string' && explicitReadyEvent.length > 0) {
520
+ return explicitReadyEvent;
521
+ }
522
+ const milestoneEntry = Array.isArray(scenario.milestones)
523
+ ? scenario.milestones.find((entry) => (String(entry?.event ?? '').includes('ready')))
524
+ : undefined;
525
+ return typeof milestoneEntry?.event === 'string' && milestoneEntry.event.length > 0
526
+ ? milestoneEntry.event
527
+ : null;
528
+ }
529
+ /**
530
+ * Applies wait gates from the normalized execution plan to platform-declared commands.
531
+ *
532
+ * @param {Record<string, unknown>} scenario
533
+ * @param {AndroidAdbProfileCommand[]} commands
534
+ * @returns {AndroidAdbProfileCommand[]}
535
+ */
536
+ function applyExecutionPlanCommandGates(scenario, commands) {
537
+ const planCommands = resolveExecutionPlanProfileCommands(scenario);
538
+ if (planCommands.length === 0) {
539
+ return commands;
540
+ }
541
+ return commands.map((command, index) => {
542
+ const planCommand = planCommands[index];
543
+ if (!planCommand || typeof planCommand.waitForMilestone !== 'string' || typeof command.waitForMilestone === 'string') {
544
+ return command;
545
+ }
546
+ return {
547
+ ...command,
548
+ waitForMilestone: planCommand.waitForMilestone,
549
+ ...(typeof command.waitTimeoutMs === 'number'
550
+ ? {}
551
+ : { waitTimeoutMs: readPositiveInteger(planCommand.waitTimeoutMs, 0) }),
552
+ };
553
+ });
282
554
  }
283
555
  /**
284
556
  * Expands normalized scenario evidence steps into Android adb driver actions.
@@ -392,12 +664,21 @@ function resolveAndroidAdbProfileCommands(scenario) {
392
664
  }
393
665
  commands.push({
394
666
  command: command.command,
667
+ commandId: typeof command.id === 'string'
668
+ ? command.id
669
+ : typeof command.commandId === 'string'
670
+ ? command.commandId
671
+ : typeof command.label === 'string'
672
+ ? command.label
673
+ : command.command,
395
674
  ...(typeof command.label === 'string' ? { label: command.label } : {}),
675
+ queueId: scenario.id ?? scenario.name,
676
+ sequence: commands.length + 1,
396
677
  waitMs: readPositiveInteger(command.waitMs, 0),
397
678
  });
398
679
  }
399
680
  }
400
- return commands;
681
+ return applyExecutionPlanCommandGates(scenario, commands);
401
682
  }
402
683
  /**
403
684
  * Summarizes failed adb capture checks for CLI errors.
@@ -459,6 +740,7 @@ function appendAgentDeviceCaptureArgs({ args, capture, }) {
459
740
  async function runProfileAndroid(args, options = {}) {
460
741
  if (!isEnabled(args['adb-capture']) && !isEnabled(args['agent-device-capture'])) {
461
742
  return runProfileMobile(args, {
743
+ commandTransport: typeof args.events === 'string' ? 'fixture-log-ingest' : 'adb-artifacts',
462
744
  ...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
463
745
  defaultDriver: 'adb-logcat',
464
746
  ...(typeof args['adb-artifacts'] === 'string' ? { interactionDriver: 'adb-logcat' } : {}),
@@ -491,10 +773,14 @@ async function runProfileAndroid(args, options = {}) {
491
773
  'ASL_ANDROID_DEV_CLIENT_WAIT_MS',
492
774
  'ASL_EXAMPLE_ANDROID_DEV_CLIENT_WAIT_MS',
493
775
  ]), 1000);
494
- const androidDevClientReadyPattern = readStringArgOrEnv(args['android-dev-client-ready-pattern'], [
776
+ const configuredAndroidDevClientReadyPattern = readStringArgOrEnv(args['android-dev-client-ready-pattern'], [
495
777
  'ASL_ANDROID_DEV_CLIENT_READY_PATTERN',
496
778
  'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_PATTERN',
497
779
  ]);
780
+ const androidDevClientReadyPattern = configuredAndroidDevClientReadyPattern
781
+ ?? (androidDevClientUrl && profileSessionEnabled && profileSessionStorageEnabled
782
+ ? DEFAULT_ANDROID_DEV_CLIENT_READY_PATTERN
783
+ : undefined);
498
784
  const androidDevClientReadyQuietMs = parsePositiveInteger(readStringArgOrEnv(args['android-dev-client-ready-quiet-ms'], [
499
785
  'ASL_ANDROID_DEV_CLIENT_READY_QUIET_MS',
500
786
  'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_QUIET_MS',
@@ -503,6 +789,10 @@ async function runProfileAndroid(args, options = {}) {
503
789
  'ASL_ANDROID_DEV_CLIENT_READY_TIMEOUT_MS',
504
790
  'ASL_EXAMPLE_ANDROID_DEV_CLIENT_READY_TIMEOUT_MS',
505
791
  ]), 60000);
792
+ const adbCommandTimeoutMs = parsePositiveInteger(readStringArgOrEnv(args['adb-command-timeout-ms'], [
793
+ 'ASL_ANDROID_ADB_COMMAND_TIMEOUT_MS',
794
+ 'ASL_EXAMPLE_ANDROID_ADB_COMMAND_TIMEOUT_MS',
795
+ ]), 30000);
506
796
  const scenarioName = typeof scenario.name === 'string' ? scenario.name : path.basename(args.scenario, '.json');
507
797
  const driverSteps = adbCaptureEnabled ? resolveAndroidAdbDriverSteps(scenario) : [];
508
798
  if (adbCaptureEnabled) {
@@ -530,9 +820,15 @@ async function runProfileAndroid(args, options = {}) {
530
820
  url: buildProfileSessionUrl({
531
821
  action: 'command',
532
822
  command: profileCommand.command,
823
+ ...(typeof profileCommand.commandId === 'string' ? { commandId: profileCommand.commandId } : {}),
533
824
  config,
534
825
  runId,
535
826
  scenario: scenarioName,
827
+ ...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
828
+ ...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
829
+ ...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
830
+ ...(typeof profileCommand.waitMs === 'number' ? { waitMs: profileCommand.waitMs } : {}),
831
+ ...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
536
832
  }),
537
833
  waitMs: profileCommand.waitMs,
538
834
  })),
@@ -565,13 +861,18 @@ async function runProfileAndroid(args, options = {}) {
565
861
  ...(typeof args.adb === 'string' ? { adbPath: args.adb } : {}),
566
862
  captureLogcat: true,
567
863
  clearLogcat: isEnabled(args['clear-logcat']),
864
+ commandTimeoutMs: adbCommandTimeoutMs,
568
865
  deepLinks: profileSessionDeepLinks,
569
866
  ...(options.delay ? { delay: options.delay } : {}),
570
867
  ...(options.executor ? { executor: options.executor } : {}),
571
868
  driverSteps,
572
869
  launch: isEnabled(args.launch),
573
870
  launchWaitMs: parsePositiveInteger(readScalarArg(args['launch-wait-ms']), 0),
574
- logcatLines: parsePositiveInteger(readScalarArg(args['logcat-lines']), 1000),
871
+ logcatLines: resolveProfileSessionLogcatLines({
872
+ args,
873
+ commands: profileSessionCommands,
874
+ profileSessionEnabled,
875
+ }),
575
876
  outputDir: resolveAdbCaptureOutputDir({ args, runId }),
576
877
  packageName: resolveAndroidPackageName({ args, config }),
577
878
  ...(typeof args['react-native-debug-host'] === 'string'
@@ -635,9 +936,46 @@ async function runProfileAndroid(args, options = {}) {
635
936
  const profileArgs = agentDeviceCapture
636
937
  ? appendAgentDeviceCaptureArgs({ args: baseProfileArgs, capture: agentDeviceCapture })
637
938
  : baseProfileArgs;
939
+ const lifecyclePhase = resolveManifestLifecyclePhase(args);
940
+ const environmentSource = agentDeviceCapture ? 'agent-device' : 'adb';
941
+ const copiedAdbLogArtifact = adbCapture ? 'raw/adb-logcat.txt' : undefined;
638
942
  return runProfileMobile(profileArgs, {
943
+ commandTransport: profileSessionStorageEnabled
944
+ ? 'profile-session-storage'
945
+ : profileSessionEnabled
946
+ ? 'profile-session-deeplink'
947
+ : agentDeviceCapture
948
+ ? 'agent-device'
949
+ : 'adb-capture',
639
950
  ...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
640
951
  defaultDriver: 'adb-logcat',
952
+ environmentPostconditions: {
953
+ appState: {
954
+ value: 'foreground',
955
+ evidence: 'asserted',
956
+ source: environmentSource,
957
+ ...(copiedAdbLogArtifact ? { artifact: copiedAdbLogArtifact } : {}),
958
+ },
959
+ lifecyclePhase: {
960
+ value: 'foreground',
961
+ evidence: 'asserted',
962
+ source: environmentSource,
963
+ ...(copiedAdbLogArtifact ? { artifact: copiedAdbLogArtifact } : {}),
964
+ },
965
+ },
966
+ environmentPreconditions: {
967
+ foregroundState: {
968
+ value: 'controlled-by-runner',
969
+ evidence: 'asserted',
970
+ source: environmentSource,
971
+ },
972
+ lifecyclePhase: {
973
+ value: lifecyclePhase,
974
+ evidence: 'asserted',
975
+ source: environmentSource,
976
+ ...(copiedAdbLogArtifact ? { artifact: copiedAdbLogArtifact } : {}),
977
+ },
978
+ },
641
979
  interactionDriver: agentDeviceCapture ? 'agent-device' : 'adb-logcat',
642
980
  platform: 'android',
643
981
  });
@@ -8,8 +8,13 @@ type IosProfileOptions = {
8
8
  };
9
9
  type IosSimctlProfileCommand = {
10
10
  command: string;
11
+ commandId?: string;
11
12
  label?: string;
13
+ queueId?: string;
14
+ sequence?: number;
15
+ waitForMilestone?: string;
12
16
  waitMs?: number;
17
+ waitTimeoutMs?: number;
13
18
  };
14
19
  /**
15
20
  * Resolves the simctl capture output directory for a profile run.
@@ -44,15 +49,21 @@ declare function resolveIosConflictingBundleIds(config: Record<string, any>): st
44
49
  * @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
45
50
  * @returns {string}
46
51
  */
47
- declare function buildProfileSessionUrl({ action, command, config, runId, scenario, }: {
52
+ declare function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitMs, waitTimeoutMs, }: {
48
53
  action: 'start' | 'command';
49
54
  command?: string;
55
+ commandId?: string;
50
56
  config: Record<string, any>;
57
+ queueId?: string;
51
58
  runId: string;
52
59
  scenario: string;
60
+ sequence?: number;
61
+ waitForMilestone?: string;
62
+ waitMs?: number;
63
+ waitTimeoutMs?: number;
53
64
  }): string;
54
65
  /**
55
- * Derives a storage-backed profile capture window from scenario waits and cycles.
66
+ * Derives a storage-backed profile capture window from scenario waits, command gates, and cycles.
56
67
  *
57
68
  * @param {Record<string, unknown>} scenario
58
69
  * @returns {number}