agent-scenario-loop 0.1.2 → 0.1.3

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 (63) hide show
  1. package/README.md +9 -9
  2. package/app/profile-session.ts +98 -4
  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 +22 -4
  6. package/dist/core/artifact-contract.js +512 -11
  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 +1 -0
  14. package/dist/core/schema-validator.js +1 -0
  15. package/dist/runner/compare-latest.d.ts +8 -4
  16. package/dist/runner/compare-latest.js +24 -5
  17. package/dist/runner/example-android-live.d.ts +10 -1
  18. package/dist/runner/example-android-live.js +55 -0
  19. package/dist/runner/example-ios-live.d.ts +10 -1
  20. package/dist/runner/example-ios-live.js +55 -0
  21. package/dist/runner/ios-simctl.d.ts +5 -0
  22. package/dist/runner/ios-simctl.js +6 -0
  23. package/dist/runner/live-comparison.d.ts +2 -2
  24. package/dist/runner/live-comparison.js +2 -1
  25. package/dist/runner/live-proof-summary.d.ts +5 -4
  26. package/dist/runner/live-proof-summary.js +12 -2
  27. package/dist/runner/live-proof.d.ts +3 -2
  28. package/dist/runner/live-proof.js +9 -2
  29. package/dist/runner/profile-android.d.ts +5 -0
  30. package/dist/runner/profile-android.js +148 -24
  31. package/dist/runner/profile-ios.d.ts +11 -1
  32. package/dist/runner/profile-ios.js +128 -9
  33. package/dist/runner/profile-mobile.d.ts +8 -0
  34. package/dist/runner/profile-mobile.js +267 -28
  35. package/docs/adapters.md +4 -0
  36. package/docs/architecture.md +90 -0
  37. package/docs/authoring.md +5 -1
  38. package/docs/concepts.md +3 -24
  39. package/docs/consumer-rehearsal.md +4 -0
  40. package/docs/contracts.md +30 -100
  41. package/docs/external-adapter-protocol.md +219 -0
  42. package/docs/live-proofs.md +83 -2
  43. package/docs/principles.md +9 -15
  44. package/examples/mobile-app/README.md +12 -0
  45. package/examples/mobile-app/runner-manifests/primary-runner.json +1 -0
  46. package/examples/runners/README.md +1 -0
  47. package/examples/runners/adb-android.json +1 -0
  48. package/examples/runners/agent-device-android.json +1 -0
  49. package/examples/runners/agent-device-ios.json +1 -0
  50. package/examples/runners/argent-android.json +1 -0
  51. package/examples/runners/argent-ios.json +1 -0
  52. package/examples/runners/xcodebuildmcp-ios.json +1 -0
  53. package/package.json +2 -1
  54. package/schemas/causal-run.schema.json +85 -2
  55. package/schemas/comparison.schema.json +130 -2
  56. package/schemas/external-adapter-message.schema.json +693 -0
  57. package/schemas/health.schema.json +72 -0
  58. package/schemas/live-proof-set.schema.json +1 -1
  59. package/schemas/live-proof.schema.json +14 -6
  60. package/schemas/manifest.schema.json +442 -1
  61. package/schemas/runner-capabilities.schema.json +20 -0
  62. package/schemas/scenario.schema.json +16 -0
  63. package/templates/primary-runner.json +1 -0
@@ -26,6 +26,21 @@ const PROFILE_SESSION_CAPTURE_BOOTSTRAP_MS = 1000;
26
26
  const PROFILE_SESSION_CAPTURE_MAX_MS = 120000;
27
27
  const DEFAULT_ANDROID_PROFILE_SESSION_STORAGE_KEY = 'agent-scenario-loop.profile-session.1';
28
28
  const DEFAULT_ANDROID_PROFILE_COMMAND_STORAGE_KEY = 'agent-scenario-loop.profile-commands.1';
29
+ const MANIFEST_LIFECYCLE_PHASES = new Set([
30
+ 'cold-launch',
31
+ 'warm-launch',
32
+ 'hot-launch',
33
+ 'resume',
34
+ 'foreground',
35
+ 'background',
36
+ 'force-stop',
37
+ 'process-death',
38
+ 'scene-recreation',
39
+ 'activity-recreation',
40
+ 'os-reclaim',
41
+ 'reboot',
42
+ 'relaunch',
43
+ ]);
29
44
  /**
30
45
  * Reads and parses a JSON object from disk.
31
46
  *
@@ -54,6 +69,22 @@ function isEnabled(value) {
54
69
  function readPositiveInteger(value, fallback) {
55
70
  return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback;
56
71
  }
72
+ /**
73
+ * Resolves the lifecycle phase this runner is prepared to assert.
74
+ *
75
+ * @param {import('./profile-mobile').CliArgs} args
76
+ * @returns {string}
77
+ */
78
+ function resolveManifestLifecyclePhase(args) {
79
+ const lifecyclePhase = readScalarArg(args['lifecycle-phase']);
80
+ if (lifecyclePhase === undefined) {
81
+ return 'cold-launch';
82
+ }
83
+ if (typeof lifecyclePhase !== 'string' || !MANIFEST_LIFECYCLE_PHASES.has(lifecyclePhase)) {
84
+ throw new Error(`Unsupported --lifecycle-phase "${String(lifecyclePhase)}". Expected one of ${Array.from(MANIFEST_LIFECYCLE_PHASES).join(', ')}.`);
85
+ }
86
+ return lifecyclePhase;
87
+ }
57
88
  /**
58
89
  * Reads the number of scenario iterations that can emit app-owned truth events.
59
90
  *
@@ -119,7 +150,7 @@ function resolveAndroidPackageName({ args, config, }) {
119
150
  * @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
120
151
  * @returns {string}
121
152
  */
122
- function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
153
+ function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitTimeoutMs, }) {
123
154
  const scheme = typeof config.app?.profileSessionScheme === 'string'
124
155
  ? config.app.profileSessionScheme
125
156
  : typeof config.app?.scheme === 'string'
@@ -128,6 +159,21 @@ function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
128
159
  const params = new URLSearchParams({ runId, scenario });
129
160
  if (action === 'command' && command) {
130
161
  params.set('command', command);
162
+ if (commandId) {
163
+ params.set('commandId', commandId);
164
+ }
165
+ if (typeof sequence === 'number') {
166
+ params.set('sequence', String(sequence));
167
+ }
168
+ if (queueId) {
169
+ params.set('queueId', queueId);
170
+ }
171
+ if (waitForMilestone) {
172
+ params.set('waitForMilestone', waitForMilestone);
173
+ }
174
+ if (typeof waitTimeoutMs === 'number') {
175
+ params.set('waitTimeoutMs', String(waitTimeoutMs));
176
+ }
131
177
  }
132
178
  return `${scheme}://profile-session/${action}?${params.toString()}`;
133
179
  }
@@ -138,6 +184,23 @@ function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
138
184
  * @returns {import('./android-adb').AndroidAsyncStorageWrite[]}
139
185
  */
140
186
  function buildProfileSessionStorageWrites({ commands, commandStorageKey, commandWaitMs, runId, scenario, sessionStorageKey, }) {
187
+ const timestampBase = Date.now();
188
+ const storedCommands = commands.map((profileCommand, index) => {
189
+ const timestamp = timestampBase + index + 1;
190
+ return {
191
+ id: `${timestamp}-${scenario}-${profileCommand.command}`,
192
+ scenario,
193
+ runId,
194
+ command: profileCommand.command,
195
+ ...(typeof profileCommand.commandId === 'string' ? { commandId: profileCommand.commandId } : {}),
196
+ ...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
197
+ ...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
198
+ ...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
199
+ ...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
200
+ timestamp,
201
+ };
202
+ });
203
+ const commandWaitMsTotal = commands.reduce((total, profileCommand) => (total + (typeof profileCommand.waitMs === 'number' && profileCommand.waitMs > 0 ? profileCommand.waitMs : 0)), 0);
141
204
  return [
142
205
  {
143
206
  clearKeys: [commandStorageKey],
@@ -151,21 +214,14 @@ function buildProfileSessionStorageWrites({ commands, commandStorageKey, command
151
214
  }).replace(`"${ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER}"`, ANDROID_DEVICE_EPOCH_MS_PLACEHOLDER),
152
215
  waitMs: commandWaitMs,
153
216
  },
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
- }),
217
+ ...(storedCommands.length > 0
218
+ ? [{
219
+ key: commandStorageKey,
220
+ label: 'profile-command-queue',
221
+ value: JSON.stringify(storedCommands),
222
+ ...(commandWaitMsTotal > 0 ? { waitMs: commandWaitMsTotal } : {}),
223
+ }]
224
+ : []),
169
225
  ];
170
226
  }
171
227
  /**
@@ -271,14 +327,30 @@ function appendCaptureArg({ args, value, }) {
271
327
  function resolveExecutionPlanProfileCommands(scenario) {
272
328
  const executionPlan = buildScenarioExecutionPlan(scenario);
273
329
  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),
280
- }));
281
- return Array.from({ length: repeat }).flatMap(() => commands);
330
+ const commands = [];
331
+ for (const [index, step] of executionPlan.steps.entries()) {
332
+ if (step.portMethod !== 'executeStep' || typeof step.command !== 'string') {
333
+ continue;
334
+ }
335
+ const nextStep = executionPlan.steps[index + 1];
336
+ commands.push({
337
+ command: step.command,
338
+ commandId: step.id,
339
+ label: step.id,
340
+ queueId: scenario.id ?? scenario.name,
341
+ waitMs: readStepWaitMs(step),
342
+ ...(nextStep?.portMethod === 'waitForTruthEvent' && typeof nextStep.milestone === 'string'
343
+ ? {
344
+ waitForMilestone: nextStep.milestone,
345
+ waitTimeoutMs: readPositiveInteger(nextStep.timeoutMs, 0),
346
+ }
347
+ : {}),
348
+ });
349
+ }
350
+ return Array.from({ length: repeat }).flatMap((_, iteration) => commands.map((command, commandIndex) => ({
351
+ ...command,
352
+ sequence: (iteration * commands.length) + commandIndex + 1,
353
+ })));
282
354
  }
283
355
  /**
284
356
  * Expands normalized scenario evidence steps into Android adb driver actions.
@@ -392,7 +464,16 @@ function resolveAndroidAdbProfileCommands(scenario) {
392
464
  }
393
465
  commands.push({
394
466
  command: command.command,
467
+ commandId: typeof command.id === 'string'
468
+ ? command.id
469
+ : typeof command.commandId === 'string'
470
+ ? command.commandId
471
+ : typeof command.label === 'string'
472
+ ? command.label
473
+ : command.command,
395
474
  ...(typeof command.label === 'string' ? { label: command.label } : {}),
475
+ queueId: scenario.id ?? scenario.name,
476
+ sequence: commands.length + 1,
396
477
  waitMs: readPositiveInteger(command.waitMs, 0),
397
478
  });
398
479
  }
@@ -459,6 +540,7 @@ function appendAgentDeviceCaptureArgs({ args, capture, }) {
459
540
  async function runProfileAndroid(args, options = {}) {
460
541
  if (!isEnabled(args['adb-capture']) && !isEnabled(args['agent-device-capture'])) {
461
542
  return runProfileMobile(args, {
543
+ commandTransport: typeof args.events === 'string' ? 'fixture-log-ingest' : 'adb-artifacts',
462
544
  ...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
463
545
  defaultDriver: 'adb-logcat',
464
546
  ...(typeof args['adb-artifacts'] === 'string' ? { interactionDriver: 'adb-logcat' } : {}),
@@ -530,9 +612,14 @@ async function runProfileAndroid(args, options = {}) {
530
612
  url: buildProfileSessionUrl({
531
613
  action: 'command',
532
614
  command: profileCommand.command,
615
+ ...(typeof profileCommand.commandId === 'string' ? { commandId: profileCommand.commandId } : {}),
533
616
  config,
534
617
  runId,
535
618
  scenario: scenarioName,
619
+ ...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
620
+ ...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
621
+ ...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
622
+ ...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
536
623
  }),
537
624
  waitMs: profileCommand.waitMs,
538
625
  })),
@@ -635,9 +722,46 @@ async function runProfileAndroid(args, options = {}) {
635
722
  const profileArgs = agentDeviceCapture
636
723
  ? appendAgentDeviceCaptureArgs({ args: baseProfileArgs, capture: agentDeviceCapture })
637
724
  : baseProfileArgs;
725
+ const lifecyclePhase = resolveManifestLifecyclePhase(args);
726
+ const environmentSource = agentDeviceCapture ? 'agent-device' : 'adb';
727
+ const lifecycleArtifact = adbCapture ? 'raw/adb-logcat.txt' : 'raw/interaction.log';
638
728
  return runProfileMobile(profileArgs, {
729
+ commandTransport: profileSessionStorageEnabled
730
+ ? 'profile-session-storage'
731
+ : profileSessionEnabled
732
+ ? 'profile-session-deeplink'
733
+ : agentDeviceCapture
734
+ ? 'agent-device'
735
+ : 'adb-capture',
639
736
  ...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
640
737
  defaultDriver: 'adb-logcat',
738
+ environmentPostconditions: {
739
+ appState: {
740
+ value: 'foreground',
741
+ evidence: 'asserted',
742
+ source: environmentSource,
743
+ artifact: lifecycleArtifact,
744
+ },
745
+ lifecyclePhase: {
746
+ value: 'foreground',
747
+ evidence: 'asserted',
748
+ source: environmentSource,
749
+ artifact: lifecycleArtifact,
750
+ },
751
+ },
752
+ environmentPreconditions: {
753
+ foregroundState: {
754
+ value: 'controlled-by-runner',
755
+ evidence: 'asserted',
756
+ source: environmentSource,
757
+ },
758
+ lifecyclePhase: {
759
+ value: lifecyclePhase,
760
+ evidence: 'asserted',
761
+ source: environmentSource,
762
+ artifact: lifecycleArtifact,
763
+ },
764
+ },
641
765
  interactionDriver: agentDeviceCapture ? 'agent-device' : 'adb-logcat',
642
766
  platform: 'android',
643
767
  });
@@ -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,12 +49,17 @@ 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, 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
+ waitTimeoutMs?: number;
53
63
  }): string;
54
64
  /**
55
65
  * Derives a storage-backed profile capture window from scenario waits and cycles.
@@ -34,6 +34,21 @@ const DEFAULT_IOS_PROFILE_COMMAND_STORAGE_KEY = 'agent-scenario-loop.profile-com
34
34
  const DEFAULT_IOS_PROFILE_EVENT_STORAGE_KEY = 'agent-scenario-loop.profile-events.1';
35
35
  const DEFAULT_IOS_PROFILE_SIGNAL_STORAGE_KEY = 'agent-scenario-loop.profile-signals.1';
36
36
  const DEFAULT_IOS_PROFILE_SESSION_ENTRIES_STORAGE_KEY = 'agent-scenario-loop.profile-session-entries.1';
37
+ const MANIFEST_LIFECYCLE_PHASES = new Set([
38
+ 'cold-launch',
39
+ 'warm-launch',
40
+ 'hot-launch',
41
+ 'resume',
42
+ 'foreground',
43
+ 'background',
44
+ 'force-stop',
45
+ 'process-death',
46
+ 'scene-recreation',
47
+ 'activity-recreation',
48
+ 'os-reclaim',
49
+ 'reboot',
50
+ 'relaunch',
51
+ ]);
37
52
  /**
38
53
  * Reads and parses a JSON object from disk.
39
54
  *
@@ -63,6 +78,22 @@ function readPositiveInteger(value, fallback) {
63
78
  const parsed = typeof value === 'string' ? Number(value) : value;
64
79
  return typeof parsed === 'number' && Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
65
80
  }
81
+ /**
82
+ * Resolves the lifecycle phase this runner is prepared to assert.
83
+ *
84
+ * @param {import('./profile-mobile').CliArgs} args
85
+ * @returns {string}
86
+ */
87
+ function resolveManifestLifecyclePhase(args) {
88
+ const lifecyclePhase = readScalarArg(args['lifecycle-phase']);
89
+ if (lifecyclePhase === undefined) {
90
+ return 'cold-launch';
91
+ }
92
+ if (typeof lifecyclePhase !== 'string' || !MANIFEST_LIFECYCLE_PHASES.has(lifecyclePhase)) {
93
+ throw new Error(`Unsupported --lifecycle-phase "${String(lifecyclePhase)}". Expected one of ${Array.from(MANIFEST_LIFECYCLE_PHASES).join(', ')}.`);
94
+ }
95
+ return lifecyclePhase;
96
+ }
66
97
  /**
67
98
  * Reads the number of scenario iterations that can emit app-owned truth events.
68
99
  *
@@ -140,7 +171,7 @@ function resolveIosConflictingBundleIds(config) {
140
171
  * @param {{config: Record<string, unknown>, action: 'start' | 'command', scenario: string, runId: string, command?: string}} options
141
172
  * @returns {string}
142
173
  */
143
- function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
174
+ function buildProfileSessionUrl({ action, command, commandId, config, queueId, runId, scenario, sequence, waitForMilestone, waitTimeoutMs, }) {
144
175
  const scheme = typeof config.app?.profileSessionScheme === 'string'
145
176
  ? config.app.profileSessionScheme
146
177
  : typeof config.app?.scheme === 'string'
@@ -149,6 +180,21 @@ function buildProfileSessionUrl({ action, command, config, runId, scenario, }) {
149
180
  const params = new URLSearchParams({ runId, scenario });
150
181
  if (action === 'command' && command) {
151
182
  params.set('command', command);
183
+ if (commandId) {
184
+ params.set('commandId', commandId);
185
+ }
186
+ if (typeof sequence === 'number') {
187
+ params.set('sequence', String(sequence));
188
+ }
189
+ if (queueId) {
190
+ params.set('queueId', queueId);
191
+ }
192
+ if (waitForMilestone) {
193
+ params.set('waitForMilestone', waitForMilestone);
194
+ }
195
+ if (typeof waitTimeoutMs === 'number') {
196
+ params.set('waitTimeoutMs', String(waitTimeoutMs));
197
+ }
152
198
  }
153
199
  return `${scheme}://profile-session/${action}?${params.toString()}`;
154
200
  }
@@ -211,14 +257,30 @@ function resolveProfileSessionCaptureWaitMs({ args, profileSessionEnabled, scena
211
257
  function resolveExecutionPlanProfileCommands(scenario) {
212
258
  const executionPlan = buildScenarioExecutionPlan(scenario);
213
259
  const repeat = readPositiveInteger(scenario.defaultIterations, readPositiveInteger(scenario.cycles?.iterations, 1));
214
- const commands = executionPlan.steps
215
- .filter((step) => step.portMethod === 'executeStep' && typeof step.command === 'string')
216
- .map((step) => ({
217
- command: step.command,
218
- label: step.id,
219
- waitMs: readStepWaitMs(step),
220
- }));
221
- return Array.from({ length: repeat }).flatMap(() => commands);
260
+ const commands = [];
261
+ for (const [index, step] of executionPlan.steps.entries()) {
262
+ if (step.portMethod !== 'executeStep' || typeof step.command !== 'string') {
263
+ continue;
264
+ }
265
+ const nextStep = executionPlan.steps[index + 1];
266
+ commands.push({
267
+ command: step.command,
268
+ commandId: step.id,
269
+ label: step.id,
270
+ queueId: scenario.id ?? scenario.name,
271
+ waitMs: readStepWaitMs(step),
272
+ ...(nextStep?.portMethod === 'waitForTruthEvent' && typeof nextStep.milestone === 'string'
273
+ ? {
274
+ waitForMilestone: nextStep.milestone,
275
+ waitTimeoutMs: readPositiveInteger(nextStep.timeoutMs, 0),
276
+ }
277
+ : {}),
278
+ });
279
+ }
280
+ return Array.from({ length: repeat }).flatMap((_, iteration) => commands.map((command, commandIndex) => ({
281
+ ...command,
282
+ sequence: (iteration * commands.length) + commandIndex + 1,
283
+ })));
222
284
  }
223
285
  /**
224
286
  * Expands scenario-declared iOS commands for a simctl capture profile session.
@@ -240,7 +302,16 @@ function resolveIosSimctlProfileCommands(scenario) {
240
302
  }
241
303
  commands.push({
242
304
  command: command.command,
305
+ commandId: typeof command.id === 'string'
306
+ ? command.id
307
+ : typeof command.commandId === 'string'
308
+ ? command.commandId
309
+ : typeof command.label === 'string'
310
+ ? command.label
311
+ : command.command,
243
312
  ...(typeof command.label === 'string' ? { label: command.label } : {}),
313
+ queueId: scenario.id ?? scenario.name,
314
+ sequence: commands.length + 1,
244
315
  waitMs: readPositiveInteger(command.waitMs, 0),
245
316
  });
246
317
  }
@@ -327,6 +398,7 @@ function appendAgentDeviceCaptureArgs({ args, capture, }) {
327
398
  async function runProfileIos(args, options = {}) {
328
399
  if (!isEnabled(args['simctl-capture']) && !isEnabled(args['agent-device-capture'])) {
329
400
  return runProfileMobile(args, {
401
+ commandTransport: typeof args.events === 'string' ? 'fixture-log-ingest' : 'simctl-artifacts',
330
402
  ...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
331
403
  defaultDriver: 'ios-simctl',
332
404
  ...(typeof args['simctl-artifacts'] === 'string' ? { interactionDriver: 'ios-simctl' } : {}),
@@ -397,9 +469,14 @@ async function runProfileIos(args, options = {}) {
397
469
  url: buildProfileSessionUrl({
398
470
  action: 'command',
399
471
  command: profileCommand.command,
472
+ ...(typeof profileCommand.commandId === 'string' ? { commandId: profileCommand.commandId } : {}),
400
473
  config,
401
474
  runId,
402
475
  scenario: scenarioName,
476
+ ...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
477
+ ...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
478
+ ...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
479
+ ...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
403
480
  }),
404
481
  waitMs: profileCommand.waitMs,
405
482
  })),
@@ -431,8 +508,13 @@ async function runProfileIos(args, options = {}) {
431
508
  profileSessionStorage: {
432
509
  commands: profileSessionCommands.map((profileCommand, index) => ({
433
510
  command: profileCommand.command,
511
+ ...(typeof profileCommand.commandId === 'string' ? { commandId: profileCommand.commandId } : {}),
434
512
  id: `ios-storage-command-${index + 1}`,
435
513
  ...(typeof profileCommand.label === 'string' ? { label: profileCommand.label } : {}),
514
+ ...(typeof profileCommand.queueId === 'string' ? { queueId: profileCommand.queueId } : {}),
515
+ ...(typeof profileCommand.sequence === 'number' ? { sequence: profileCommand.sequence } : {}),
516
+ ...(typeof profileCommand.waitForMilestone === 'string' ? { waitForMilestone: profileCommand.waitForMilestone } : {}),
517
+ ...(typeof profileCommand.waitTimeoutMs === 'number' ? { waitTimeoutMs: profileCommand.waitTimeoutMs } : {}),
436
518
  })),
437
519
  runId,
438
520
  scenario: scenarioName,
@@ -496,9 +578,46 @@ async function runProfileIos(args, options = {}) {
496
578
  const profileArgs = agentDeviceCapture
497
579
  ? appendAgentDeviceCaptureArgs({ args: baseProfileArgs, capture: agentDeviceCapture })
498
580
  : baseProfileArgs;
581
+ const lifecyclePhase = resolveManifestLifecyclePhase(args);
582
+ const environmentSource = agentDeviceCapture ? 'agent-device' : 'simctl';
583
+ const lifecycleArtifact = simctlCapture ? 'raw/ios-simctl-log.txt' : 'raw/interaction.log';
499
584
  return runProfileMobile(profileArgs, {
585
+ commandTransport: agentDeviceCapture
586
+ ? 'agent-device'
587
+ : profileSessionEnabled && !profileSessionStorageEnabled
588
+ ? 'profile-session-deeplink'
589
+ : profileSessionEnabled
590
+ ? 'profile-session-storage'
591
+ : 'simctl-capture',
500
592
  ...(options.comparisonLane ? { comparisonLane: options.comparisonLane } : {}),
501
593
  defaultDriver: 'ios-simctl',
594
+ environmentPostconditions: {
595
+ appState: {
596
+ value: 'foreground',
597
+ evidence: 'asserted',
598
+ source: environmentSource,
599
+ artifact: lifecycleArtifact,
600
+ },
601
+ lifecyclePhase: {
602
+ value: 'foreground',
603
+ evidence: 'asserted',
604
+ source: environmentSource,
605
+ artifact: lifecycleArtifact,
606
+ },
607
+ },
608
+ environmentPreconditions: {
609
+ foregroundState: {
610
+ value: 'controlled-by-runner',
611
+ evidence: 'asserted',
612
+ source: environmentSource,
613
+ },
614
+ lifecyclePhase: {
615
+ value: lifecyclePhase,
616
+ evidence: 'asserted',
617
+ source: environmentSource,
618
+ artifact: lifecycleArtifact,
619
+ },
620
+ },
502
621
  interactionDriver: agentDeviceCapture ? 'agent-device' : 'ios-simctl',
503
622
  platform: 'ios',
504
623
  });
@@ -21,10 +21,14 @@ type ProfileRunResult = {
21
21
  };
22
22
  type ProfilePlatform = 'android' | 'ios';
23
23
  type ProfileMobileOptions = {
24
+ commandTransport?: string;
24
25
  comparisonLane?: string;
25
26
  defaultDriver: string;
27
+ environmentPostconditions?: Record<string, unknown>;
28
+ environmentPreconditions?: Record<string, unknown>;
26
29
  interactionDriver?: string;
27
30
  platform: ProfilePlatform;
31
+ provenanceCohort?: Record<string, unknown>;
28
32
  };
29
33
  type CaptureEvidenceKind = 'screenshot' | 'uiTree' | 'video';
30
34
  type ProviderEvidenceKind = 'accessibility' | 'logs' | 'profiler';
@@ -33,13 +37,17 @@ type EvidenceChannel = 'capture' | 'provider' | 'signal';
33
37
  type EvidenceKind = CaptureEvidenceKind | ProviderEvidenceKind | SignalEvidenceKind;
34
38
  type EvidenceAttachment = {
35
39
  channel: EvidenceChannel;
40
+ completenessStatus: 'complete';
41
+ corruptionStatus: 'valid';
36
42
  destinationPath: string;
37
43
  kind: EvidenceKind;
38
44
  manifestPath: string;
45
+ redactionStatus: 'not-redacted';
39
46
  sha256: string;
40
47
  sourcePath: string;
41
48
  sourceFileName: string;
42
49
  sizeBytes: number;
50
+ transformations: readonly ['copied'];
43
51
  };
44
52
  type EvidenceAttachmentInput = {
45
53
  channel: EvidenceChannel;