@vostride/agent-qa-runner 0.0.1 → 0.2.2

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.
@@ -0,0 +1,3903 @@
1
+ import { StepEvidence, RecordingEntry, Action as Action$1, RecordingAnnotationWitness, Platform, ActionsFor as ActionsFor$1, LiveStreamKind, LiveFrame, ScreenState, RecordingContentType, ScopeKey, BriefBlock, ServedRetrievalTraceV1, SearchOptions, IndexEntry, RecordId, ReadOptions, RecordBody, Anchor, CautionBlock, ProcedureBindingAvailability, ProcedureMatch, CanaryOutcome, ElementState, ElementLabels, Ref, ElementInfo, ScreenStateMetadata, CacheKey, ClipboardEvidence, SubmitOutcome, SubmitReason, FillVerdict, InputPath, DownloadEvidence, IdempotentNoopMeta, AssertValueOp, ConditionFileCheck, OperationEvidence, GroundingAttempt, WorkingMemoryEntry, ConditionVerdict, ConditionNeed, CitationSource, InterpolatedVariable, ToolExchangeEvidence, ModelCallRecord, RunEventEnvelope, CompletedRunHandoff, HookStartedPayload, HookFinishedPayload, TestRetryScheduledPayload, DeclaredModelProfile, ModelIdentity, RunId, PostProcessingDisposition, RunStatus, RunArtifactStageInput, RunArtifactFinalizeInput, ExecutionContext, QuarantineAppliedEntry, PlannerLimitsRecord, ServingRecordMetadata, ServingAdmissionDecision, ServedAdmissionSurface, ServedAdmissionEvent, ArtifactRef, EvidenceDocument, RecordingRef, StepFinalizedPayload, ModelUsageObservations, SubActionEvidence, SubActionRecordedPayload, RecordedRetrievalRung, ProcedureBodyPayload, ReadTrace, Surface, SamplingParams, BriefLine, ServedBrief, ServedPromptTurn, ActionResult as ActionResult$1, CautionLine, PlanningFailureEvidence, ReportedValueEvidence, TerminationEvidence, LimitStopEvidence, ConditionVerdictsEvidence, OperationCaptureAvailability, StepLimitUsage, AiJudgmentEvidence, AssertValueEvidence, VerdictSource, StepPlanSource, CacheMissReason, Decision, VerifierVerdict, Viewport, ActionAnnotation, MaskFn, RedactedEnvVar, CacheableStepInput, NonCacheableStepInput, RunEventType, RunEventPayload, LiveFrameEndReason, LiveFrameSubscription, LiveStreamDeclaration, RunSession, PerTestOutcome, MemberTestOutcome, Verdict, RiskActionDescriptor } from '@vostride/agent-qa-schema';
2
+ export { IdempotentNoopMeta, Ref, asRef, readIdempotentNoopMeta } from '@vostride/agent-qa-schema';
3
+ import { LanguageModel, ModelMessage, LanguageModelUsage, ToolSet, SystemModelMessage, Tool } from 'ai';
4
+ import { z } from 'zod';
5
+
6
+ type StepStatus = 'passed' | 'failed' | 'healed' | 'skipped' | 'cancelled';
7
+ type TestStatus = 'passed' | 'failed' | 'skipped' | 'cancelled';
8
+ interface StepAnnotation {
9
+ clickPoint?: {
10
+ x: number;
11
+ y: number;
12
+ };
13
+ boundingBox?: {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ };
19
+ failureHighlight?: {
20
+ x: number;
21
+ y: number;
22
+ width: number;
23
+ height: number;
24
+ };
25
+ type: 'click' | 'fill' | 'tap' | 'hover' | 'scroll' | 'swipe' | 'pinch' | 'navigate' | 'other';
26
+ viewport?: {
27
+ width: number;
28
+ height: number;
29
+ };
30
+ startPoint?: {
31
+ x: number;
32
+ y: number;
33
+ };
34
+ endPoint?: {
35
+ x: number;
36
+ y: number;
37
+ };
38
+ direction?: 'up' | 'down' | 'left' | 'right';
39
+ pinchScale?: 'in' | 'out';
40
+ }
41
+ interface TokenUsage {
42
+ promptTokens: number;
43
+ completionTokens: number;
44
+ totalTokens: number;
45
+ }
46
+ interface SubActionTrace {
47
+ index: number;
48
+ observation: string;
49
+ reasoning: string;
50
+ plannedAction: Action;
51
+ result: 'success' | 'failure' | 'no-input';
52
+ error?: string;
53
+ noInputSent?: {
54
+ currentState: string;
55
+ };
56
+ screenStateBefore: string;
57
+ screenStateAfter?: string;
58
+ screenContextBefore?: string;
59
+ screenContextAfter?: string;
60
+ confidence?: number;
61
+ verifierReasoning?: string;
62
+ cached: boolean;
63
+ tokenUsage?: TokenUsage;
64
+ phaseDurations?: {
65
+ observe?: number;
66
+ plan?: number;
67
+ execute?: number;
68
+ verify?: number;
69
+ };
70
+ screenshotBefore?: Buffer;
71
+ screenshotAfter?: Buffer;
72
+ annotation?: StepAnnotation;
73
+ data?: unknown;
74
+ }
75
+ interface StepTrace {
76
+ observation: string;
77
+ reasoning: string;
78
+ plannedAction: Action;
79
+ result: 'success' | 'failure';
80
+ error?: string;
81
+ screenStateBefore: string;
82
+ screenStateAfter?: string;
83
+ screenContextBefore?: string;
84
+ screenContextAfter?: string;
85
+ confidence?: number;
86
+ verifierReasoning?: string;
87
+ tokenUsage?: TokenUsage;
88
+ phaseDurations?: {
89
+ observe?: number;
90
+ plan?: number;
91
+ execute?: number;
92
+ verify?: number;
93
+ };
94
+ subActions?: SubActionTrace[];
95
+ }
96
+ interface StepPhaseEvent {
97
+ phase: 'observe' | 'plan' | 'execute' | 'verify';
98
+ subActionIndex?: number;
99
+ phaseOrdinal?: number;
100
+ text?: string;
101
+ confidence?: number;
102
+ action?: unknown;
103
+ success?: boolean;
104
+ duration?: number;
105
+ }
106
+ interface AccessibilityViolation {
107
+ ruleId: string;
108
+ impact: 'minor' | 'moderate' | 'serious' | 'critical';
109
+ description: string;
110
+ help: string;
111
+ helpUrl: string;
112
+ nodes: Array<{
113
+ html: string;
114
+ target: string[];
115
+ }>;
116
+ }
117
+ interface ConsoleLogEntry {
118
+ level: string;
119
+ text: string;
120
+ location?: {
121
+ url: string;
122
+ lineNumber: number;
123
+ columnNumber: number;
124
+ };
125
+ timestamp: number;
126
+ }
127
+ interface NetworkLogEntry {
128
+ url: string;
129
+ method: string;
130
+ status: number;
131
+ requestHeaders: Record<string, string>;
132
+ responseHeaders: Record<string, string>;
133
+ body?: string;
134
+ requestBody?: string;
135
+ startTime: number;
136
+ endTime: number;
137
+ timing?: Record<string, number>;
138
+ }
139
+ interface StepResult {
140
+ id?: string;
141
+ name: string;
142
+ status: StepStatus;
143
+ duration: number;
144
+ action?: Action;
145
+ observation?: string;
146
+ error?: string;
147
+ screenshot?: Buffer;
148
+ screenshotBefore?: Buffer;
149
+ trace?: StepTrace;
150
+ /**
151
+ * The §A8 evidence accumulated by the runner loop, produced by construction (D-14).
152
+ * ADDITIVE (Phase 353): the legacy `trace` is projected FROM this, never the reverse
153
+ * (Open Q5). Core's transitional orchestration persists it via the run-artifact path
154
+ * (D-16) until Phase 361 lifts the accumulator into RunSession/CompletedRunHandoff.
155
+ */
156
+ evidence?: StepEvidence;
157
+ retryCount?: number;
158
+ capturedVariables?: Record<string, string>;
159
+ annotation?: StepAnnotation;
160
+ accessibilityViolations?: AccessibilityViolation[];
161
+ consoleLogs?: ConsoleLogEntry[];
162
+ networkLogs?: NetworkLogEntry[];
163
+ variableSnapshot?: Record<string, {
164
+ value: string;
165
+ source: string;
166
+ }>;
167
+ originalStepName?: string;
168
+ }
169
+ interface TestResult {
170
+ runId?: string;
171
+ name: string;
172
+ filePath: string;
173
+ status: TestStatus;
174
+ steps: StepResult[];
175
+ duration: number;
176
+ metadata?: Record<string, unknown>;
177
+ retryCount?: number;
178
+ recording?: RecordingEntry;
179
+ failureSummary?: string;
180
+ /** One detached actual failure, retained only when a subsequent attempt runs. */
181
+ firstFailedAttempt?: {
182
+ /** Zero-based member attempt index, independently of the final retryCount. */
183
+ attempt: number;
184
+ result: Omit<TestResult, 'firstFailedAttempt'>;
185
+ };
186
+ }
187
+
188
+ interface ObserveOptions {
189
+ extractDom?: boolean;
190
+ }
191
+ type Action = Action$1;
192
+ type ActionsFor<P extends Platform = Platform> = ActionsFor$1<P>;
193
+ /**
194
+ * An adapter's LIVE STREAM capability (403 D-05). The adapter owns the producer (a CDP screencast,
195
+ * an MJPEG broadcaster, a WebRTC track, or a snapshot producer over a capture that is safe to run
196
+ * beside a step); the runner owns nothing but this object, so no `Page` and no session handle ever
197
+ * leaves the adapter. Lifecycle is lazy and refcounted by the run's {@link LiveStreamHub}: `start`
198
+ * on the first viewer, `ack` per consumed frame (the producer's flow control), `stop` on the
199
+ * last — idempotent, so a double stop is a no-op rather than a throw.
200
+ *
201
+ * The labelled 1s snapshot producer (`createSnapshotLiveStream`) sits behind this same contract
202
+ * (428 D-11): the composition registers it as the whole preview when an adapter has no continuous
203
+ * stream, and as the fallback the hub starts when a continuous stream fails to start (D-12).
204
+ */
205
+ interface LiveStreamCapability {
206
+ readonly kind: LiveStreamKind;
207
+ start(onFrame: (frame: LiveFrame) => void): Promise<void>;
208
+ ack(frameId: number): Promise<void>;
209
+ stop(): Promise<void>;
210
+ }
211
+ interface RecordingStart {
212
+ zeroMs?: number;
213
+ }
214
+ interface RecordingStop {
215
+ stagingPath: string;
216
+ contentType: RecordingContentType;
217
+ }
218
+ interface RecordingCapability {
219
+ start(): Promise<RecordingStart>;
220
+ stop(): Promise<RecordingStop | undefined>;
221
+ /** Bracket one actual execution; completion withholds unsupported or changed scenes. */
222
+ beginAnnotation?(): (() => RecordingAnnotationWitness | undefined) | undefined;
223
+ }
224
+ interface OperationCaptureSupport {
225
+ download: boolean;
226
+ clipboard: boolean;
227
+ /** Why a capture is unsupported, e.g. the browser name. */
228
+ reason?: string;
229
+ }
230
+ /**
231
+ * 2026-10-08 (436-15, D-19): what the loop hands one dispatch. `remainingStepMs` is set only when
232
+ * a step deadline is armed; adapters that run no post-action wait ignore both.
233
+ */
234
+ interface ExecuteOptions {
235
+ remainingStepMs?: number;
236
+ abortSignal?: AbortSignal;
237
+ }
238
+ interface PlatformAdapter<P extends Platform = Platform> {
239
+ readonly platform: P;
240
+ setup(config: PlatformConfig): Promise<void>;
241
+ cleanup(): Promise<void>;
242
+ observe(options?: ObserveOptions): Promise<ScreenState>;
243
+ execute(action: ActionsFor<P>, options?: ExecuteOptions): Promise<ActionResult>;
244
+ screenshot?(): Promise<Buffer | undefined>;
245
+ /**
246
+ * 403 D-05: the optional live-stream producer for this adapter's current target. ABSENT means
247
+ * "no continuous stream" (non-Chromium, remote farm), not "no preview": the composition's
248
+ * `liveStreamFor` wraps `screenshot()` in the labelled 1s snapshot producer instead, and uses
249
+ * that producer as the fallback when a continuous stream fails to start (428 D-11/D-12). Only an
250
+ * adapter with neither reads `{ kind: 'none' }`. An adapter whose `screenshot()` mutates shared
251
+ * state (the mobile adapters switch the Appium context) must expose its own snapshot
252
+ * `liveStream` over a side-effect-free capture instead, so `screenshot()` is never wrapped.
253
+ */
254
+ readonly liveStream?: LiveStreamCapability;
255
+ readonly recording?: RecordingCapability;
256
+ /**
257
+ * 436 D-23: which operation-specific captures this adapter runs. ABSENT means none; the loop
258
+ * then records both as unavailable, with `reason` or else 'platform'.
259
+ */
260
+ readonly operationCaptureSupport?: OperationCaptureSupport;
261
+ drainConsoleLogs?(level?: string): ConsoleLogEntry[];
262
+ drainNetworkLogs?(urlPattern?: string): NetworkLogEntry[];
263
+ /** 439 D-14: the undrained network log, read without consuming it; the step-end drain still sees it. */
264
+ peekNetworkLogs?(urlPattern?: string): NetworkLogEntry[];
265
+ /**
266
+ * 439 D-04: the bytes of a completed download with this sha256, held in memory and bounded by
267
+ * the adapter; undefined when the adapter holds none (over its cap, evicted, or disposed).
268
+ */
269
+ readDownloadBytes?(sha256: string): Uint8Array | undefined;
270
+ }
271
+ declare function assertNever(value: never): never;
272
+ interface ActionResult {
273
+ recordingAnnotation?: RecordingAnnotationWitness;
274
+ success: boolean;
275
+ error?: string;
276
+ screenshot?: Buffer;
277
+ metadata?: Record<string, unknown>;
278
+ coordinates?: {
279
+ x: number;
280
+ y: number;
281
+ };
282
+ boundingBox?: {
283
+ x: number;
284
+ y: number;
285
+ width: number;
286
+ height: number;
287
+ };
288
+ startCoordinates?: {
289
+ x: number;
290
+ y: number;
291
+ };
292
+ endCoordinates?: {
293
+ x: number;
294
+ y: number;
295
+ };
296
+ data?: unknown;
297
+ }
298
+ interface RuntimeAuthStateConfig {
299
+ version: number;
300
+ kind: 'web';
301
+ targetName: string;
302
+ stateName: string;
303
+ capturedAt: string;
304
+ storageStatePath: string;
305
+ }
306
+ interface PlatformConfig {
307
+ platform: 'web' | 'android' | 'ios';
308
+ browser?: BrowserConfig;
309
+ device?: DeviceConfig;
310
+ bundleId?: string;
311
+ appPackage?: string;
312
+ appActivity?: string;
313
+ deepLinkAppId?: string;
314
+ appState?: 'preserve' | 'reset';
315
+ appPath?: string;
316
+ browserstackApp?: string;
317
+ appiumUrl?: string;
318
+ timeouts?: TimeoutConfig;
319
+ recording?: {
320
+ enabled: boolean;
321
+ videoDir: string;
322
+ videoSize?: {
323
+ width: number;
324
+ height: number;
325
+ };
326
+ };
327
+ verbose?: boolean;
328
+ farmSession?: {
329
+ hostname: string;
330
+ port: number;
331
+ path: string;
332
+ capabilities: Record<string, unknown>;
333
+ };
334
+ logCapture?: {
335
+ console?: boolean;
336
+ network?: boolean;
337
+ };
338
+ authState?: RuntimeAuthStateConfig;
339
+ }
340
+ interface BrowserConfig {
341
+ name: 'chromium' | 'firefox' | 'webkit';
342
+ headless?: boolean;
343
+ viewport?: {
344
+ width: number;
345
+ height: number;
346
+ };
347
+ }
348
+ interface DeviceConfig {
349
+ name: string;
350
+ platform: 'android' | 'ios';
351
+ transport: 'local' | 'browserstack';
352
+ match: Record<string, unknown>;
353
+ }
354
+ interface TimeoutConfig {
355
+ step?: number;
356
+ test?: number;
357
+ navigation?: number;
358
+ }
359
+
360
+ /**
361
+ * The Channel-1 brief and the serve-time trace that explains it, as ONE value (KREV-02 · D-16).
362
+ *
363
+ * The pairing is the guarantee, not a convenience. A reader implementation cannot satisfy this
364
+ * port while producing a brief and no trace, and a consumer cannot receive a brief without also
365
+ * being handed how it was chosen — which is what makes 387-06's why-verb able to answer about the
366
+ * moment of serving rather than about a re-query of today's index.
367
+ */
368
+ interface ServedRunBrief {
369
+ brief: BriefBlock;
370
+ trace: ServedRetrievalTraceV1;
371
+ }
372
+ /** The Channel-3 caution block with its own serve-time trace, on the same one-value contract. */
373
+ interface ServedCautions {
374
+ block: CautionBlock;
375
+ trace: ServedRetrievalTraceV1;
376
+ }
377
+ interface KnowledgeHintReader {
378
+ getRunBrief(scope: ScopeKey, budget: number): ServedRunBrief;
379
+ search(query: string, options: SearchOptions): IndexEntry[];
380
+ read(id: RecordId, options?: ReadOptions): RecordBody | undefined;
381
+ getCautions(anchors: Anchor[], budget: number): ServedCautions;
382
+ /**
383
+ * D-08 port evolution: source-tagged binding NAMES cross here (variables, memory, secrets) so
384
+ * matcher fillability mirrors loop binding; no value ever crosses the reader port in any form.
385
+ * Value binding stays loop-side at replay time. Absent availability is treated as empty, so a
386
+ * referenced-parameter procedure downranks to hint (fail-conservative).
387
+ */
388
+ getProcedure(intent: string, screenState: ScreenState, availability?: ProcedureBindingAvailability): ProcedureMatch | undefined;
389
+ emitCanaryOutcome(payload: CanaryOutcome): void;
390
+ }
391
+
392
+ /**
393
+ * Adapter -> builder input for a single observed node.
394
+ *
395
+ * Structural links are carried as INDICES because refs do not exist yet — the
396
+ * builder mints them in document order (RESEARCH Pitfall #7).
397
+ */
398
+ interface InterimNode {
399
+ /** Schema role vocabulary — adapters map native roles BEFORE the builder. */
400
+ role: string;
401
+ /** Raw accessible name; the builder normalizes it (ElementInfo.name = name ?? ''). */
402
+ name?: string;
403
+ value?: string;
404
+ /** Index of the nearest INCLUDED ancestor in the same nodes array. */
405
+ parentIndex?: number;
406
+ bounds?: {
407
+ x: number;
408
+ y: number;
409
+ width: number;
410
+ height: number;
411
+ };
412
+ /** Optional; the builder derives the geometric center from bounds when absent. */
413
+ center?: {
414
+ x: number;
415
+ y: number;
416
+ };
417
+ /**
418
+ * ONLY real platform signals (D-08 — never fabricated). The builder derives the
419
+ * required booleans from documented defaults when a signal is absent:
420
+ * enabled = state.enabled ?? true (absence of a disabled signal means enabled)
421
+ * focused = state.focused ?? false
422
+ * visible = state.visible ?? true (present in the a11y tree means exposed;
423
+ * adapters set false explicitly for offscreen)
424
+ * Optional bits (selected/checked/editable/clickable) pass through only when the
425
+ * platform supplied them — typed-absent otherwise.
426
+ */
427
+ state?: Partial<ElementState>;
428
+ labels?: ElementLabels;
429
+ /** Raw platform bag, passed through to ElementInfo.attributes (default {}). */
430
+ attributes?: Record<string, string>;
431
+ /** Static text folded in from children that are not nodes; copied to ElementInfo.text. */
432
+ text?: string;
433
+ /** Adapter-private hint (e.g. android class / XCUI type); the builder ignores it. */
434
+ nativeType?: string;
435
+ /** D-10; the builder maps containerIndex -> the minted container Ref. */
436
+ scrollContext?: {
437
+ containerIndex?: number;
438
+ offsetX: number;
439
+ offsetY: number;
440
+ };
441
+ }
442
+ interface BuildScreenStateInput {
443
+ platform: Platform;
444
+ /** Document order — this is the ref-minting order. */
445
+ nodes: InterimNode[];
446
+ viewport: {
447
+ width: number;
448
+ height: number;
449
+ };
450
+ /**
451
+ * EXACT on web; BEST-EFFORT on mobile (D-10) — mobile callers pass a best-effort
452
+ * scroll derived from scrollable-node bounds. The field is precise only on web.
453
+ */
454
+ scroll: {
455
+ offsetX: number;
456
+ offsetY: number;
457
+ contentWidth: number;
458
+ contentHeight: number;
459
+ };
460
+ coordSpace: 'viewport' | 'content' | 'screen';
461
+ /** Caller-supplied — keeps the builder pure and deterministic. */
462
+ timestamp: number;
463
+ url?: string;
464
+ /** Web-only (D-04); lands in metadata.domContext. */
465
+ domContext?: string;
466
+ }
467
+ /**
468
+ * Execute-time resolution stays adapter-private: `index` is the live node's place in the
469
+ * input nodes array. No same-screen position is minted for anything to pick by (438 D-13).
470
+ */
471
+ interface RefBinding {
472
+ index: number;
473
+ }
474
+ interface BuildScreenStateResult {
475
+ /** Schema type — never redefined. */
476
+ state: ScreenState;
477
+ /** Execute-time ref -> live-node resolution; stays adapter-private. */
478
+ binding: Record<Ref, RefBinding>;
479
+ }
480
+ /**
481
+ * The capture stamp the loop applies after compression (D-11). `full` carries the
482
+ * real image dimensions; `textOnly` attaches no image.
483
+ */
484
+ type CaptureStamp = {
485
+ mode: 'full';
486
+ imageWidth: number;
487
+ imageHeight: number;
488
+ } | {
489
+ mode: 'textOnly';
490
+ };
491
+ /**
492
+ * Assemble a schema v2 ScreenState plus an adapter-private ref->node binding from a
493
+ * platform-neutral interim node list. Pure and deterministic: identical input always
494
+ * yields byte-identical output.
495
+ */
496
+ declare function buildScreenState(input: BuildScreenStateInput): BuildScreenStateResult;
497
+ /**
498
+ * Deterministic LLM-facing tree text, derived from the structured elements (D-02) —
499
+ * one grammar for all platforms. Exported so Phase 357's slim/rich dual-projection is
500
+ * a renderer-only change with zero adapter edits.
501
+ *
502
+ * Grammar (pinned; RESEARCH Pitfall #10):
503
+ * [Viewport: {viewportWidth}x{viewportHeight}] [{elements.length} elements]
504
+ * {2 spaces × depth}- {role}{ "name"?} [ref={ref}]{ @(x,y wxh)?}{ [disabled]?}{ [offscreen]?}{ [checked]?}{ [selected]?}{ [editable]?}{ text: <row text as a JSON string>?}
505
+ * depth = length of the parentRef chain (root elements depth 0).
506
+ * A rendered name collapses each line break, with the whitespace around it, to one space, so
507
+ * every element stays on one line; element.name and value keep the raw text.
508
+ * ` text: "…"` appears only on a row container (ROW_CONTAINER_ROLES) whose element.text is
509
+ * non-empty (438 D-02: the planner must see a row to state it); line breaks collapse the same
510
+ * way and the rendered text is capped at 120 characters plus `…`. The row text is a JSON string
511
+ * (JSON.stringify, as quoted() renders names in the refusal line), so a quote or backslash in app
512
+ * text cannot end the string or forge a ref, flag or line (438 gap round, WR-02, T-438-04).
513
+ * element.text keeps the full text the judge and replay compare against.
514
+ */
515
+ declare function renderScreenTree(elements: ElementInfo[], metadata: ScreenStateMetadata): string;
516
+ /**
517
+ * Pure re-stamp of the discriminated captureMode metadata member (D-11). Constructs a
518
+ * fresh metadata object from the base fields and returns a NEW ScreenState — zero `as`
519
+ * casts, zero mutation of the input.
520
+ */
521
+ declare function stampCaptureMode(state: ScreenState, capture: CaptureStamp): ScreenState;
522
+
523
+ /** The element the planner meant beside its ref (D-01); `row` names a repeated label's row. */
524
+ interface StatedTarget {
525
+ role?: string;
526
+ name: string;
527
+ row?: string;
528
+ }
529
+ /** The element lanes an action can carry a stated target on. */
530
+ type StatedTargetLane = 'ref' | 'fromRef' | 'toRef';
531
+ /** The stated target per element lane, kept by the gate for the pre-dispatch check (D-01, D-04). */
532
+ type StatedTargets = Partial<Record<StatedTargetLane, StatedTarget>>;
533
+ type TargetMismatchReason = 'role-kind' | 'name-no-shared-word' | 'name-number' | 'row';
534
+ type StatedTargetJudgement = {
535
+ verdict: 'agrees';
536
+ rowStated: boolean;
537
+ rowKnown: boolean;
538
+ observedRow?: string;
539
+ } | {
540
+ verdict: 'contradicts';
541
+ reasons: TargetMismatchReason[];
542
+ rowStated: boolean;
543
+ observedRow?: string;
544
+ };
545
+ /**
546
+ * Judge a stated target against the element its ref names (D-03). `elements` is the observation
547
+ * the element belongs to; the element's row is read from it (D-02).
548
+ */
549
+ declare function judgeStatedTarget(stated: StatedTarget, element: ElementInfo, elements: readonly ElementInfo[]): StatedTargetJudgement;
550
+
551
+ /**
552
+ * The subset of the action union that may be persisted in a cached plan (D-08). The
553
+ * non-deterministic AI-assertion verbs — `assertAI` (judge lane) and `extractAI`
554
+ * (extraction lane) — are `Exclude`d from the composed `Action` union so a plan carrying
555
+ * one CANNOT typecheck into a cache write. Plan 10's runtime guard at the write site is the
556
+ * narrowing proof; this static boundary is the belt-and-suspenders (the 356
557
+ * `NonCacheableStepInput` type-level idiom, mirrored at the value shape).
558
+ */
559
+ type CacheablePlannedAction = Exclude<Action, {
560
+ type: 'assertAI';
561
+ } | {
562
+ type: 'extractAI';
563
+ }>;
564
+ /**
565
+ * Descriptor-only locator (D-12, 438 D-10/D-11). Identity is the observed role, the exact
566
+ * observed name with spacing tidied (numbers and badges kept) and, when the planner stated a
567
+ * row the element's row holds, those row words. Never a position, a ref, a selector or
568
+ * coordinates: replay re-finds the one element with this identity in a fresh observation.
569
+ */
570
+ interface GroundedLocator {
571
+ role: string;
572
+ name: string;
573
+ row?: string;
574
+ }
575
+ /**
576
+ * The cached plan value. `plannedActions` are stored in sanitized (locator-free,
577
+ * data-free) form AND statically exclude the AI-assertion verbs (D-08); grounding is carried
578
+ * only by `groundedLocator` descriptors so no run-specific ref/selector/coordinate or step
579
+ * `data` value ever persists (D-12).
580
+ */
581
+ interface CachedPlanValue {
582
+ plannedActions: CacheablePlannedAction[];
583
+ groundedLocator: GroundedLocator[];
584
+ verificationOutcome: 'passed' | 'failed';
585
+ }
586
+ /**
587
+ * Redesigned per-key cache port (D-11). Get/set/invalidate over a branded CacheKey
588
+ * — the v1 prefix/index sub-action model is dropped. A storage implementation (Plan
589
+ * 05's FsActionCache) implements this interface; runner stays storage-neutral.
590
+ */
591
+ interface ActionPlanCache {
592
+ get(key: CacheKey): Promise<CachedPlanValue | null>;
593
+ set(key: CacheKey, value: CachedPlanValue): Promise<void>;
594
+ invalidate(key: CacheKey): Promise<void>;
595
+ }
596
+
597
+ /**
598
+ * Project a planned action's target element to a descriptor-only locator: its role, its exact
599
+ * name, and the stated row when the element's known row holds it. Returns null when the action
600
+ * targets no element (e.g. navigate) or the ref no longer resolves.
601
+ */
602
+ declare function toGroundedLocator(action: Action, elements: readonly ElementInfo[], stated?: StatedTarget): GroundedLocator | null;
603
+ interface BuildCachedPlanValueInput {
604
+ plannedActions: readonly Action[];
605
+ elements: readonly ElementInfo[];
606
+ verificationOutcome: 'passed' | 'failed';
607
+ /** The completing decision's stated target per lane; supplies a locator's row words. */
608
+ statedTargets?: StatedTargets;
609
+ }
610
+ /**
611
+ * Assemble a descriptor-only CachedPlanValue. Planned actions are sanitized of every
612
+ * locator binding and step `data` value; grounding is carried solely by the
613
+ * descriptor locators so nothing run-specific is ever persisted (D-12 overfit guard).
614
+ */
615
+ declare function buildCachedPlanValue(input: BuildCachedPlanValueInput): CachedPlanValue;
616
+
617
+ declare const ROW_CONTAINER_ROLES: readonly string[];
618
+ /**
619
+ * Word tokens of a text: digit groups joined, NFKC, lowercased, split on every run that is not a
620
+ * letter or digit.
621
+ */
622
+ declare function wordTokens(text: string | undefined): string[];
623
+ /**
624
+ * The row text of an element in its observation, or undefined when the element has no row
625
+ * container or the container's text is empty (D-02: the row is unknown).
626
+ */
627
+ declare function rowTextOf(element: ElementInfo, elements: readonly ElementInfo[]): string | undefined;
628
+ /** Whether every stated row word is in the row text (equal, or a prefix of a non-number word). */
629
+ declare function rowContains(rowText: string, statedRow: string): boolean;
630
+
631
+ type StrictMatchResult = {
632
+ kind: 'resolved';
633
+ ref: Ref;
634
+ } | {
635
+ kind: 'absent';
636
+ } | {
637
+ kind: 'not-unique';
638
+ count: number;
639
+ };
640
+
641
+ /** The values a readable fill compared: requested and read back (sentinels for a secure field). */
642
+ interface FillRereadMeta {
643
+ expected: string;
644
+ actual: string;
645
+ }
646
+ /** What ended a scroll: the target appeared, the swipe budget ran out, or a boundary signal. */
647
+ type ScrollEndedBy = 'target-visible' | 'max-swipes' | 'boundary';
648
+ /** Outcomes new runs write (436-02, D-24). */
649
+ declare const SCROLL_OUTCOMES_WRITTEN: readonly ["found", "not-found", "unresolved"];
650
+ /** Outcomes only pre-436 runs wrote; still read so persisted metadata stays parseable (D-17). */
651
+ declare const SCROLL_OUTCOMES_READ_ONLY: readonly ["no-change", "cap"];
652
+ /** EXEC-02 scroll outcome block. `endedBy` is absent on pre-436 metadata. */
653
+ interface ScrollUntilVisibleMeta {
654
+ swipes: number;
655
+ reversedOnce: boolean;
656
+ outcome: (typeof SCROLL_OUTCOMES_WRITTEN)[number] | (typeof SCROLL_OUTCOMES_READ_ONLY)[number];
657
+ endedBy?: ScrollEndedBy;
658
+ }
659
+ /** EXEC-03 openApp resolution block. */
660
+ interface OpenAppMeta {
661
+ resolvedId: string;
662
+ matchKind: 'exact' | 'fuzzy' | 'explicit';
663
+ }
664
+
665
+ declare function readFillRereadMeta(actionResult?: ActionResult): FillRereadMeta | undefined;
666
+ declare function readScrollUntilVisibleMeta(actionResult?: MetadataCarrier): ScrollUntilVisibleMeta | undefined;
667
+ declare function readOpenAppMeta(actionResult?: ActionResult): OpenAppMeta | undefined;
668
+ /** EXEC-05 reportValue answer block (D-01/D-03). Keyed `reportedValue`. */
669
+ interface ReportedValueMeta {
670
+ value: string;
671
+ }
672
+ /** EXEC-06 uniqueValue mint block (D-16). Keyed `uniqueValue`. */
673
+ interface UniqueValueMeta {
674
+ value: string;
675
+ prefix: string;
676
+ }
677
+ /** EXEC-07 AI-judgment block (D-05/D-06/D-07/D-09). Keyed `aiJudgment`. */
678
+ interface AiJudgmentMeta {
679
+ verb: 'assertAI' | 'extractAI';
680
+ verdict?: boolean;
681
+ }
682
+ /** EXEC-07 assertValue outcome block (D-10/D-11). Keyed `assertValueOutcome`. */
683
+ interface AssertValueOutcomeMeta {
684
+ source: string;
685
+ op: string;
686
+ passed: boolean;
687
+ expected?: string;
688
+ actual?: string | null;
689
+ }
690
+ /**
691
+ * The minimal structural input the 360 metadata readers narrow: any ActionResult-like carrier of a
692
+ * metadata bag. Broader than `ActionResult` so the seam's loosely-typed sub-action result surfaces
693
+ * (which only guarantee an optional `metadata`) narrow cleanly — the defensive idiom returns
694
+ * undefined on any shape miss regardless.
695
+ */
696
+ type MetadataCarrier = {
697
+ metadata?: Record<string, unknown>;
698
+ };
699
+ declare function readReportedValueMeta(actionResult?: MetadataCarrier): ReportedValueMeta | undefined;
700
+ declare function readUniqueValueMeta(actionResult?: MetadataCarrier): UniqueValueMeta | undefined;
701
+ declare function readAiJudgmentMeta(actionResult?: MetadataCarrier): AiJudgmentMeta | undefined;
702
+ declare function readAssertValueOutcomeMeta(actionResult?: MetadataCarrier): AssertValueOutcomeMeta | undefined;
703
+ /** Typed refusal of a fill/clearText before any text was typed. Keyed `fillRefusal`. */
704
+ interface FillRefusalMeta {
705
+ verb: 'fill' | 'clearText';
706
+ cause: 'not-editable' | 'target-not-found' | 'single-line-field';
707
+ evidence: 'no-editable-signal' | 'no-keyboard-focus' | 'focus-elsewhere' | 'stale-target' | 'text-has-line-breaks';
708
+ }
709
+ /**
710
+ * What happened to a requested submit after a fill (437 D-17). Keyed `fillSubmit`. Pre-437 blocks
711
+ * carry `sent` alone or `{ sent: false, reason: 'multi-line-field' }` and still read.
712
+ */
713
+ interface FillSubmitMeta {
714
+ sent: boolean;
715
+ outcome?: SubmitOutcome;
716
+ reason?: SubmitReason;
717
+ detail?: string;
718
+ }
719
+ /** A shell-routed action refused or failed by the driver (437 D-18). Keyed `shellAction`. */
720
+ interface ShellActionMeta {
721
+ verb: 'keyevent' | 'openQuickSettings';
722
+ outcome: 'unavailable' | 'failed';
723
+ reason?: 'adb-shell-disabled';
724
+ detail?: string;
725
+ }
726
+ declare function readFillRefusalMeta(actionResult?: MetadataCarrier): FillRefusalMeta | undefined;
727
+ /** How a fill or clear's read-back compared with the requested text (437 D-06). Keyed `fillVerdict`. */
728
+ interface FillVerdictMeta {
729
+ verb: 'fill' | 'clearText';
730
+ verdict: FillVerdict;
731
+ path?: InputPath;
732
+ secure?: true;
733
+ }
734
+ declare function readFillVerdictMeta(actionResult?: MetadataCarrier): FillVerdictMeta | undefined;
735
+ declare function readFillSubmitMeta(actionResult?: MetadataCarrier): FillSubmitMeta | undefined;
736
+ declare function readShellActionMeta(actionResult?: MetadataCarrier): ShellActionMeta | undefined;
737
+ declare function readDownloadMeta(actionResult?: MetadataCarrier): DownloadEvidence[] | undefined;
738
+ /** The most masked clipboard text one write keeps; longer text is cut and marked. */
739
+ declare const CLIPBOARD_TEXT_LIMIT = 2000;
740
+ declare const CLIPBOARD_TRUNCATION_MARKER = "\u2026[truncated]";
741
+ type ClipboardWriteMeta = Pick<ClipboardEvidence, 'source' | 'text' | 'readBack'>;
742
+ declare function readClipboardMeta(actionResult?: MetadataCarrier): ClipboardWriteMeta[] | undefined;
743
+
744
+ interface FillVerifyInput {
745
+ expected: string;
746
+ actual: string | undefined;
747
+ secure: boolean;
748
+ verb?: 'fill' | 'clearText';
749
+ }
750
+ interface FillVerifyResult {
751
+ verdict: FillVerdict;
752
+ ok: boolean;
753
+ secure?: true;
754
+ meta?: FillRereadMeta;
755
+ selfCorrectingError?: string;
756
+ }
757
+ declare const FILL_SEPARATORS: ReadonlySet<string>;
758
+ declare function compareFillReread(input: FillVerifyInput): FillVerifyResult;
759
+
760
+ type DesiredState = 'checked' | 'unchecked' | 'selected' | 'unselected';
761
+ interface DesiredStateAction {
762
+ type: string;
763
+ ref?: string;
764
+ desiredState?: DesiredState;
765
+ value?: string;
766
+ }
767
+ declare function desiredStateSatisfied(action: DesiredStateAction, screenState: ScreenState): IdempotentNoopMeta | undefined;
768
+
769
+ type Direction = 'up' | 'down' | 'left' | 'right';
770
+ /** What one swipe proved: the container moved, it is at its extent, or nothing is known. */
771
+ type SwipeSignal = 'moved' | 'at-boundary' | 'unknown';
772
+ /** The target is on screen, definitively not on screen, or cannot be judged. */
773
+ type TargetVisibility = 'visible' | 'hidden' | 'unknown';
774
+ interface ScrollPrimitives {
775
+ /** One platform swipe in `direction`, optionally inside container `ref`. */
776
+ swipeOnce(direction: Direction, ref?: Ref): Promise<SwipeSignal>;
777
+ /** Native search-scroll toward `target`; its `true` is confirmed by the visibility probe. */
778
+ nativeSearchScroll?(target: string, ref?: Ref, direction?: Direction): Promise<boolean>;
779
+ /** Light probe: interim nodes only, NO refMap mint, NO screenshot. Read for visibility only. */
780
+ readInterimTree(): Promise<InterimNode[]>;
781
+ /**
782
+ * Is `target` visible, definitively not on screen, or impossible to judge? When supplied it
783
+ * replaces the interim-tree name match; any other answer counts as unknown.
784
+ */
785
+ probeTargetVisible?(target: string, ref?: Ref): Promise<TargetVisibility>;
786
+ }
787
+ interface UntilVisibleParams {
788
+ target: string;
789
+ direction: Direction;
790
+ maxSwipes: number;
791
+ retryOpposite: boolean;
792
+ ref?: Ref;
793
+ }
794
+ interface ScrollLoopResult {
795
+ meta: ScrollUntilVisibleMeta;
796
+ found: boolean;
797
+ }
798
+ interface ScrollOffsets {
799
+ before: number;
800
+ after: number;
801
+ /** Largest offset the container can reach on the swiped axis. */
802
+ max: number;
803
+ }
804
+ /**
805
+ * The offset rule shared by every surface that can read real scroll offsets (web containers,
806
+ * Android WebView): a changed offset is movement; an unchanged offset at the container's extent
807
+ * in the swiped direction is the boundary; anything else is unknown.
808
+ */
809
+ declare function swipeSignalFromOffsets(direction: Direction, offsets: ScrollOffsets | undefined): SwipeSignal;
810
+ /** A page's scrolling element, read before and after a gesture that is not bound to a container. */
811
+ interface DocumentScrollOffsets {
812
+ x: number;
813
+ y: number;
814
+ maxX: number;
815
+ maxY: number;
816
+ }
817
+ /**
818
+ * The offset rule for a gesture that lands on whatever is under the pointer (web wheel, WebView
819
+ * touch swipe), over every scroller the gesture may have acted on, as aligned before/after arrays
820
+ * (436-18 · D-25). Any member that moved is movement. A boundary needs at least one member with
821
+ * range on the swiped axis and every member with range at its extent; a member short of its
822
+ * extent, no member with range, or missing, empty or misaligned arrays prove nothing.
823
+ */
824
+ declare function scrollerSetSwipeSignal(direction: Direction, before: readonly DocumentScrollOffsets[] | undefined, after: readonly DocumentScrollOffsets[] | undefined): SwipeSignal;
825
+ /**
826
+ * The scroller-set rule over the document scroller alone: a document with no scroll range on the
827
+ * swiped axis proves nothing, because the gesture may have moved a nested scroller.
828
+ */
829
+ declare function documentSwipeSignal(direction: Direction, before: DocumentScrollOffsets | undefined, after: DocumentScrollOffsets | undefined): SwipeSignal;
830
+ declare function runUntilVisibleLoop(p: ScrollPrimitives, params: UntilVisibleParams): Promise<ScrollLoopResult>;
831
+ interface DirectionScrollParams {
832
+ direction: Direction;
833
+ ref?: Ref;
834
+ }
835
+ /** Direction mode is one gesture: no opposite retry, whatever the signal says. */
836
+ declare function runDirectionScroll(p: ScrollPrimitives, params: DirectionScrollParams): Promise<ScrollLoopResult>;
837
+ declare const MAX_TO_END_SWIPES = 20;
838
+ /**
839
+ * toEnd: swipe until a boundary signal or the cap. Reaching the boundary is the goal, so it
840
+ * reports 'found'; the cap without a boundary signal reports 'unresolved'.
841
+ */
842
+ declare function runToEndScroll(p: ScrollPrimitives, direction: Direction, ref?: Ref, cap?: number): Promise<ScrollLoopResult>;
843
+
844
+ interface AppRegistryEntry {
845
+ id: string;
846
+ label?: string;
847
+ }
848
+ interface ScorerOptions {
849
+ fuzzyThreshold?: number;
850
+ fuzzyMargin?: number;
851
+ editDistanceBound?: number;
852
+ candidateCount?: number;
853
+ }
854
+ type AppResolution = {
855
+ kind: 'exact' | 'fuzzy';
856
+ id: string;
857
+ } | {
858
+ kind: 'ambiguous' | 'unknown';
859
+ candidates: {
860
+ id: string;
861
+ label?: string;
862
+ }[];
863
+ };
864
+ /**
865
+ * Minimum score a fuzzy top candidate must clear to be launchable. Internal runner constant;
866
+ * injectable via ScorerOptions for tests only. Do NOT promote to user-facing config or thread
867
+ * through LoopPorts / any config schema (353 K=2 precedent).
868
+ */
869
+ declare const DEFAULT_FUZZY_THRESHOLD = 0.5;
870
+ /**
871
+ * Minimum lead the top fuzzy candidate must hold over the runner-up to launch (else ambiguous).
872
+ * Internal runner constant; injectable for tests only. Do NOT promote to user-facing config.
873
+ */
874
+ declare const DEFAULT_FUZZY_MARGIN = 0.15;
875
+ /**
876
+ * Levenshtein cap: matches beyond this edit distance do not count as edit-rung candidates.
877
+ * Internal runner constant; injectable for tests only. Do NOT promote to user-facing config.
878
+ */
879
+ declare const DEFAULT_EDIT_DISTANCE_BOUND = 3;
880
+ /**
881
+ * How many candidates an ambiguous/unknown resolution surfaces. Internal runner constant;
882
+ * injectable for tests only. Do NOT promote to user-facing config.
883
+ */
884
+ declare const DEFAULT_CANDIDATE_COUNT = 3;
885
+ declare function resolveAppName(name: string, registry: AppRegistryEntry[], opts?: ScorerOptions): AppResolution;
886
+
887
+ /** The closed set of runner-minted (platform-less) verb names dispatched without adapter.execute. */
888
+ declare const MINT_VERBS: readonly ["reportValue", "uniqueValue"];
889
+ type MintVerb = (typeof MINT_VERBS)[number];
890
+ /** Emission params for a `reportValue` mint (mirrors reportValueSchema). */
891
+ interface ReportValueInput {
892
+ value: string;
893
+ }
894
+ /**
895
+ * Mint the D-01/D-03 `reportValue` success result. The answer rides `metadata.reportedValue`
896
+ * (readReportedValueMeta narrows it). NOTE: `value` here is the emission value — the Plan 10 seam
897
+ * masks before recording into StepEvidence.reportedValue (masked-before-record, T-360-01).
898
+ */
899
+ declare function mintReportedValue(input: ReportValueInput): ActionResult;
900
+ /** Emission params for a `uniqueValue` mint (mirrors uniqueValueSchema). */
901
+ interface UniqueValueInput {
902
+ prefix: string;
903
+ }
904
+ /**
905
+ * The injectable suffix generator (353 K=2 determinism, D-16): a pure `() => string` overridden in
906
+ * tests for deterministic output, defaulting to {@link defaultUniqueValueGenerator}.
907
+ */
908
+ type UniqueValueGenerator = () => string;
909
+ /**
910
+ * Mint the D-16 `uniqueValue` success result: `prefix` + a generated suffix, carried on
911
+ * `metadata.uniqueValue` ({ value, prefix }). The generator is injectable for deterministic tests;
912
+ * uniqueness is the semantic, so cache replay re-mints fresh. The Plan 10 seam auto-writes the
913
+ * minted value into run-scoped working memory keyed by the prefix.
914
+ */
915
+ declare function mintUniqueValue(input: UniqueValueInput, generator?: UniqueValueGenerator): ActionResult;
916
+ /**
917
+ * Default suffix source (D-16): timestamp + entropy, base36-encoded, non-empty and distinct across
918
+ * calls. Injectable so deterministic tests pass a fixed generator instead. Randomness/time IS the
919
+ * semantic here (uniqueness), unlike the deterministic app-resolution ladder.
920
+ */
921
+ declare function defaultUniqueValueGenerator(): string;
922
+
923
+ /** Comparator input: the source label, the closed op, the (optional) expected, and the raw actual. */
924
+ interface AssertValueInput {
925
+ source: string;
926
+ op: AssertValueOp;
927
+ /** Absent for the no-expected ops (isNull/isDefined). */
928
+ expected?: string;
929
+ /** The raw value the adapter read. `null` is a read; `undefined` is unread and fails every op. */
930
+ actual?: string | null;
931
+ /** Each network log entry's JSON, for `source: 'network'`. Absent or empty is unread. */
932
+ entries?: string[];
933
+ }
934
+ /** Comparator outcome — projects onto the schema AssertValueEvidence block. */
935
+ interface AssertValueResult {
936
+ source: string;
937
+ op: AssertValueOp;
938
+ expected?: string;
939
+ actual?: string | null;
940
+ passed: boolean;
941
+ }
942
+ /**
943
+ * Apply the closed assertValue op deterministically. Type-aware per the Block A pins: gt/lt coerce
944
+ * numeric; equals/notEquals/contains compare as strings; isNull passes only on a null read. An
945
+ * unread value fails every op (439 D-14). A network read passes only on a matching entry.
946
+ */
947
+ declare function compareAssertValue(input: AssertValueInput): AssertValueResult;
948
+
949
+ /** The most characters of each side `firstDifference` shows. */
950
+ declare const FILE_DIFFERENCE_WINDOW = 80;
951
+ interface SavedFileExpectation {
952
+ name: string;
953
+ /** 'none': the name only; 'exact': the whole text; 'lines': each line whole, in order. */
954
+ match: 'none' | 'exact' | 'lines';
955
+ text: string;
956
+ }
957
+ interface SavedFileComparison extends ConditionFileCheck {
958
+ /** The 1-based file line of the first difference, when the content differs. */
959
+ line?: number;
960
+ }
961
+ /**
962
+ * Compare a held download with the verifier's expectation. The name is exact (case and extension);
963
+ * 'exact' needs the whole text, 'lines' each expected line as a whole file line in order, and an
964
+ * empty expected text matches only a zero-byte file. `bytes` undefined, or bytes that are not valid
965
+ * UTF-8, leave the content 'unverified'.
966
+ */
967
+ declare function compareSavedFile(input: {
968
+ expected: SavedFileExpectation;
969
+ name: string;
970
+ bytes: Uint8Array | undefined;
971
+ mask: (value: string) => string;
972
+ }): SavedFileComparison;
973
+
974
+ /**
975
+ * A complete, valid v2 `ScreenState` with `captureMode: 'textOnly'`. Callers override
976
+ * individual fields; the base is deterministic (fixed refs/bounds/timestamp/scroll).
977
+ */
978
+ declare function makeScreenState(overrides?: Partial<ScreenState>): ScreenState;
979
+ /** The platform-neutral node list + the builder input (minus nodes/platform). */
980
+ interface InterimFixture {
981
+ nodes: InterimNode[];
982
+ buildInput: Omit<BuildScreenStateInput, 'nodes' | 'platform'>;
983
+ }
984
+ type InterimFixtureKind = 'login-screen' | 'mixed-geometry' | 'nested';
985
+ /**
986
+ * Platform-neutral interim nodes plus the matching builder input for a scenario. The
987
+ * conformance cases feed these through `buildScreenState` (per platform) to assert the
988
+ * shared contract holds identically across all three adapters.
989
+ */
990
+ declare function makeInterimNodes(kind: InterimFixtureKind): InterimFixture;
991
+
992
+ type RecordedActionOutcome = 'success' | 'failure' | 'no-input' | 'refused';
993
+ interface RecordedAction {
994
+ /** 1-based position of the action in the step. */
995
+ index: number;
996
+ action: Action;
997
+ /** The element the action's ref named on the screen it acted on. */
998
+ element?: {
999
+ role: string;
1000
+ name: string;
1001
+ };
1002
+ outcome: RecordedActionOutcome;
1003
+ error?: string;
1004
+ operationEvidence?: OperationEvidence;
1005
+ }
1006
+ type RecordedReadKind = 'assertValue' | 'assertAI' | 'extractAI' | 'reportValue';
1007
+ interface RecordedRead {
1008
+ /** The 1-based index of the action that read the value. */
1009
+ index: number;
1010
+ kind: RecordedReadKind;
1011
+ text: string;
1012
+ assert?: {
1013
+ source: string;
1014
+ op: string;
1015
+ expected?: string;
1016
+ passed: boolean;
1017
+ };
1018
+ claim?: string;
1019
+ /** An assertAI read's verdict on its claim. */
1020
+ judged?: boolean;
1021
+ mode?: string;
1022
+ }
1023
+ interface CarriedLine {
1024
+ stepNumber: number;
1025
+ actionIndex: number;
1026
+ text: string;
1027
+ }
1028
+ interface VerifierStepRecord {
1029
+ startScreen: ScreenState;
1030
+ actions: RecordedAction[];
1031
+ reads: RecordedRead[];
1032
+ carried: CarriedLine[];
1033
+ }
1034
+ interface RenderedStepRecord {
1035
+ actions: string;
1036
+ operationEvidence: string;
1037
+ reads: string;
1038
+ carried: string;
1039
+ }
1040
+ declare const RECORD_ACTION_LINE_CAP = 40;
1041
+ declare const RECORD_LINE_CHAR_CAP = 300;
1042
+ declare const RECORD_LIST_CAP = 50;
1043
+ declare class StepRecordAccumulator {
1044
+ private startScreen;
1045
+ private readonly actions;
1046
+ private readonly reads;
1047
+ private readonly carried;
1048
+ /** Keeps the first settled observation of the step; later calls are ignored. */
1049
+ start(screen: ScreenState): void;
1050
+ addAction(entry: Omit<RecordedAction, 'index'>): number;
1051
+ /** A read belongs to the action about to be recorded. */
1052
+ addRead(entry: Omit<RecordedRead, 'index'>): void;
1053
+ addCarried(line: CarriedLine): void;
1054
+ /** The actions recorded so far; unlike `snapshot` it needs no start screen. */
1055
+ recordedActions(): RecordedAction[];
1056
+ snapshot(): VerifierStepRecord;
1057
+ }
1058
+ /** The record as the verifier turn shows it, one string per part. */
1059
+ declare function renderStepRecord(record: VerifierStepRecord): RenderedStepRecord;
1060
+
1061
+ /**
1062
+ * A planned action plus the frozen v45 plan envelope (D-09). Structural mirror of
1063
+ * core's ActionPlanSchema output; the v45 tool registry (this plan, task 3) produces it.
1064
+ */
1065
+ interface ActionPlan {
1066
+ reasoning: string;
1067
+ action: {
1068
+ type: string;
1069
+ [key: string]: unknown;
1070
+ };
1071
+ confidence: number;
1072
+ stepComplete: boolean;
1073
+ stepFailed?: boolean;
1074
+ /**
1075
+ * D-09/GROUND-01 grounding-attempts carriage. When the planning turn grounds the
1076
+ * action in-turn (via the injected groundability callback), the gate's §A8
1077
+ * GroundingAttempt trail rides here so the resolved-descriptor evidence reaches
1078
+ * executeStep (threaded onto StepEvidence in Plan 07). Absent when no grounding
1079
+ * callback is injected or the gate recorded no attempts.
1080
+ */
1081
+ groundingAttempts?: GroundingAttempt[];
1082
+ /** D-01: the element the planner meant per element lane, judged before dispatch. */
1083
+ statedTargets?: StatedTargets;
1084
+ /**
1085
+ * WM-01 write channel (spec §12.15, ratified 2026-07-05). Verbatim facts the planner captured
1086
+ * this turn via the `remember[]` plan-field; the loop hands them to the run-scoped
1087
+ * `WorkingMemoryStore` (Plan 08 wiring). Absent when the planner captured nothing this turn.
1088
+ */
1089
+ remember?: WorkingMemoryEntry[];
1090
+ /**
1091
+ * D-09 bulk-loop completion predicate. On a "do X to all" step the planner declares the positive
1092
+ * completion predicate here and re-states it each turn; the verifier backstops. Absent for
1093
+ * non-bulk steps.
1094
+ */
1095
+ bulkCompletionPredicate?: string;
1096
+ /**
1097
+ * D-04 infeasible terminal outcome (EXEC-05, spec §12.10). The additive plan-field envelope
1098
+ * (358 remember[] precedent) by which a turn declares the step terminally infeasible — the
1099
+ * requested capability/screen does not exist in this build. Closed literal union ('infeasible'
1100
+ * today, extensible later); machine-distinguishable from a plain `stepFailed`. When set, the
1101
+ * loop routes it into a typed StepEvidence.termination block and a distinct verdict outcome
1102
+ * (Plan 10 seam). Absent for an ordinary failure.
1103
+ */
1104
+ failureKind?: 'infeasible';
1105
+ /**
1106
+ * D-04 infeasible reason — required whenever `failureKind` is set (enforced at the
1107
+ * toolCallToActionPlan parse boundary). The human-readable why, recorded verbatim in the
1108
+ * termination evidence block. Absent unless `failureKind` is present.
1109
+ */
1110
+ failureReason?: string;
1111
+ }
1112
+ /** 436 D-08: debits one planner model call before it is made; false refuses the call. */
1113
+ interface PlanningCallGate {
1114
+ tryDebitPlanningCall(): boolean;
1115
+ }
1116
+ interface PlannerConfig {
1117
+ maxActions: number;
1118
+ maxPlanningTurns: number;
1119
+ previousStepCount: number;
1120
+ }
1121
+ interface PlanResult {
1122
+ plan: ActionPlan;
1123
+ tokenUsage?: TokenUsage;
1124
+ /**
1125
+ * Ordered non-action exchanges from this live planning decision (D-01).
1126
+ * Absent for cache-replay-produced results, which have no live planning turn.
1127
+ */
1128
+ exchanges?: ToolExchangeEvidence[];
1129
+ /**
1130
+ * Observed final-response metadata for this live planning decision (D-05/D-08).
1131
+ * Absent when the provider exposes no call record or no live model call occurred.
1132
+ */
1133
+ callRecord?: ModelCallRecord;
1134
+ }
1135
+ /** One condition as the verifier judged it (439 D-02, D-10); the runner decides what it counts for. */
1136
+ interface ConditionJudgment {
1137
+ quote: string;
1138
+ verdict: ConditionVerdict;
1139
+ needs: 'none' | ConditionNeed;
1140
+ controlRef: string;
1141
+ file: {
1142
+ name: string;
1143
+ match: 'none' | 'exact' | 'lines';
1144
+ text: string;
1145
+ };
1146
+ citations: {
1147
+ source: CitationSource;
1148
+ quote: string;
1149
+ }[];
1150
+ reasoning: string;
1151
+ }
1152
+ /** An open dialog as the verifier accounted for it (439 D-16). */
1153
+ interface DialogAccount {
1154
+ name: string;
1155
+ account: 'bears-on-condition' | 'requested' | 'unrelated';
1156
+ reasoning: string;
1157
+ }
1158
+ /**
1159
+ * The verifier's per-condition output. There is no overall success: the runner computes the pass
1160
+ * (439 D-10). `verifierError` is set only by the fallback when the model call failed.
1161
+ */
1162
+ interface VerificationResult {
1163
+ conditions: ConditionJudgment[];
1164
+ dialogs: DialogAccount[];
1165
+ isAppError: boolean;
1166
+ reasoning: string;
1167
+ verifierError?: string;
1168
+ }
1169
+ interface VerifyResult {
1170
+ verification: VerificationResult;
1171
+ tokenUsage?: TokenUsage;
1172
+ }
1173
+ interface StepContext {
1174
+ stepInstruction: string;
1175
+ testName: string;
1176
+ testContext?: string;
1177
+ suiteContext?: string;
1178
+ stepNumber?: number;
1179
+ stepCount?: number;
1180
+ previousSteps: {
1181
+ instruction: string;
1182
+ outcome: string;
1183
+ reasoning?: string;
1184
+ plannedAction?: string;
1185
+ verifierResponse?: string;
1186
+ }[];
1187
+ contextWindow?: number;
1188
+ plannerModel: LanguageModel;
1189
+ verifierModel: LanguageModel;
1190
+ modelId?: string;
1191
+ variables?: Record<string, string>;
1192
+ /**
1193
+ * The `{{env:NAME}}` bindings that interpolated into `stepInstruction` before this step
1194
+ * was planned (PROC-01 · 381 D-03/D-08). DESTINATION: stamped verbatim onto
1195
+ * `StepEvidence.interpolatedVariables` at finalize, where the procedure-extraction emit
1196
+ * fold reads it to classify a trajectory value slot back to its source binding NAME.
1197
+ *
1198
+ * Carried VERBATIM end to end — never masked (unlike `variables` above, which is
1199
+ * secret-redacted for the planner). See the masking prohibition documented on
1200
+ * `StepEvidence.interpolatedVariables`: masking would break the consumer's exact-match
1201
+ * classification for email-valued and credential-named bindings.
1202
+ *
1203
+ * Absent/empty when the step's text interpolated nothing — finalize then stamps nothing.
1204
+ */
1205
+ interpolatedVariables?: readonly InterpolatedVariable[];
1206
+ /**
1207
+ * D-12/D-13 compatibility slot for a directly supplied learned brief. Reader-backed runs use
1208
+ * the structured L1/L2/truncation fields below; buildVolatileTier applies the same hypotheses
1209
+ * frame to either source. Empty/undefined omits the learned section entirely.
1210
+ */
1211
+ knowledgeBrief?: string;
1212
+ /**
1213
+ * BRIF-01 human-authored/sovereign L0 policy material. It is isolated from learned spans so
1214
+ * buildContextTier can keep only this byte-stable material in the cached-prefix region.
1215
+ */
1216
+ knowledgePolicyL0?: string;
1217
+ /** BRIF-01 learned caution summaries rendered under the volatile hypotheses frame. */
1218
+ knowledgeBriefL1?: string;
1219
+ /** BRIF-01 learned procedure/high-trust-fact summaries rendered in the volatile tier. */
1220
+ knowledgeBriefL2?: string;
1221
+ /** BRIF-01 reader-provided clipping signal; empty/undefined means no signal is rendered. */
1222
+ knowledgeBriefTruncation?: string;
1223
+ /**
1224
+ * CR-08 immutable UTC date for planner prompt evidence, captured once per step as
1225
+ * `YYYY-MM-DD`. When absent, direct legacy prompt-builder callers retain the current
1226
+ * UTC-date fallback. Bound values cross this type unchanged; the production capture
1227
+ * boundary validates the closed date form before persistence.
1228
+ */
1229
+ promptDate?: string;
1230
+ /**
1231
+ * D-12/D-13 working-memory channel (structured turn content). The run-scoped
1232
+ * `WorkingMemoryStore` set, injected verbatim as its own turn message by `composeTurnMessages`
1233
+ * — the second v2 channel replacing the retired single memory-context string. Phase 358
1234
+ * SANCTIONED BREAK (D-12/D-13): this was a plain `string` slot; it is now `WorkingMemoryEntry[]`
1235
+ * (no shim, no string overload) so the planner receives the structured verbatim set. Empty
1236
+ * array/undefined ⇒ omitted from the turn entirely (empty-slot omission).
1237
+ */
1238
+ workingMemory?: WorkingMemoryEntry[];
1239
+ /**
1240
+ * D-09 active bulk-completion predicate (per-turn VOLATILE turn content). While a bulk
1241
+ * "do X to all" loop is running, the planner's declared positive predicate is re-stated each
1242
+ * turn with the re-evaluate/cite-positive-evidence instruction. Absent for non-bulk steps.
1243
+ */
1244
+ bulkCompletionPredicate?: string;
1245
+ failureContext?: string;
1246
+ /**
1247
+ * Sub-actions already attempted in this step (multi-action mode), with each outcome and any
1248
+ * returned data. Screen change is not recorded here: outcomes decide (HARN-01).
1249
+ */
1250
+ subActionHistory?: {
1251
+ action: string;
1252
+ reasoning?: string;
1253
+ result: 'success' | 'failure' | 'no-input';
1254
+ error?: string;
1255
+ verifierRejection?: string;
1256
+ data?: string;
1257
+ evidence?: string;
1258
+ }[];
1259
+ plannerConfig?: PlannerConfig;
1260
+ /** 436 D-08: the step's planning-call meter, bound by the loop before each plan() call. */
1261
+ planningCallGate?: PlanningCallGate;
1262
+ screenshot?: Buffer;
1263
+ /**
1264
+ * Vision-gate prompt note for this run (D-11). Non-empty only for text-only
1265
+ * models; injected into the planner prompt when present.
1266
+ */
1267
+ promptNote?: string;
1268
+ /**
1269
+ * D-13 re-run-after-failure context (LOOP-03). A re-run carries everything (full
1270
+ * memory/context, no ablation) plus an explicit run-level flag and the prior
1271
+ * attempt's failure summary, surfaced to the planner as prompt context.
1272
+ */
1273
+ isRerun?: boolean;
1274
+ rerunAttempt?: number;
1275
+ previousFailureSummary?: string;
1276
+ }
1277
+ interface Planner {
1278
+ plan(step: string, screenState: ScreenState, context: StepContext, abortSignal?: AbortSignal): Promise<PlanResult>;
1279
+ }
1280
+ interface VerifierAuthorContext {
1281
+ suiteContext?: string;
1282
+ testContext?: string;
1283
+ stepNumber?: number;
1284
+ stepCount?: number;
1285
+ }
1286
+ interface Verifier {
1287
+ verify(step: string, before: ScreenState, after: ScreenState, action: Action, screenshot?: Buffer, abortSignal?: AbortSignal, promptNote?: string, bulkCompletionPredicate?: string, authorContext?: VerifierAuthorContext, operationEvidence?: OperationEvidence, record?: VerifierStepRecord): Promise<VerifyResult>;
1288
+ /**
1289
+ * D-05 AI judge lane (EXEC-07, spec §9.6/§12.2) — OPTIONAL so every existing Verifier
1290
+ * implementor keeps compiling (D-05: no new port). Plan 09 implements this on LLMVerifier
1291
+ * against the injected model handle; the loop (Plan 10 seam) routes `assertAI` through it
1292
+ * exactly once per attempt (single-model law). A single-shot judgment over the current
1293
+ * observation returning the verdict + rationale. `opts` carries the requested-but-deferred
1294
+ * M3/M4 knobs (consensus/video/effort) so demand is recorded (D-09).
1295
+ */
1296
+ judgeClaim?(claim: string, screen: ScreenState, opts?: Record<string, unknown>): Promise<{
1297
+ verdict: boolean;
1298
+ rationale: string;
1299
+ }>;
1300
+ /**
1301
+ * D-07 AI extraction lane (EXEC-07, spec §9.6) — OPTIONAL, same no-new-port rationale as
1302
+ * `judgeClaim`. Plan 09 implements it on LLMVerifier; the loop routes `extractAI` through it,
1303
+ * writes the extracted fields into run-scoped working memory (`scope:'run'`), and degrades
1304
+ * `scope:'global'` to the same run-scoped write (D-07). Returns the extracted field map.
1305
+ */
1306
+ extractData?(query: string, screen: ScreenState, opts?: Record<string, unknown>): Promise<Record<string, unknown>>;
1307
+ }
1308
+ type AssertionType = 'text-presence' | 'element-visibility' | 'url-match' | 'element-count' | 'ai';
1309
+ interface AssertionInput {
1310
+ type: AssertionType;
1311
+ value: string;
1312
+ expected?: string;
1313
+ }
1314
+ interface AssertionResult {
1315
+ passed: boolean;
1316
+ assertionType: AssertionType;
1317
+ expected: string;
1318
+ actual: string;
1319
+ reasoning: string;
1320
+ }
1321
+ interface Asserter {
1322
+ evaluate(assertion: AssertionInput, screenState: ScreenState): Promise<AssertionResult>;
1323
+ }
1324
+ type ExtractionMethod = 'regex' | 'selector' | 'ai';
1325
+ interface ExtractorInput {
1326
+ method: ExtractionMethod;
1327
+ variableName: string;
1328
+ pattern?: string;
1329
+ selector?: string;
1330
+ description?: string;
1331
+ }
1332
+ interface CaptureResult {
1333
+ success: boolean;
1334
+ variableName: string;
1335
+ value?: string;
1336
+ reasoning: string;
1337
+ }
1338
+ interface VariableExtractor {
1339
+ extract(input: ExtractorInput, screenState: ScreenState): Promise<CaptureResult>;
1340
+ }
1341
+
1342
+ /** A legacy `{ success }` verdict a double may still be written with, or the full result. */
1343
+ type ScriptedVerdict = {
1344
+ success: boolean;
1345
+ reasoning: string;
1346
+ isAppError?: boolean;
1347
+ } | VerificationResult;
1348
+ /** One met condition quoting the whole step, citing the first line of the final screen. */
1349
+ declare function metVerification(options: {
1350
+ step: string;
1351
+ after?: ScreenState;
1352
+ action?: Action;
1353
+ reasoning?: string;
1354
+ }): VerificationResult;
1355
+ /** One not-met condition quoting the whole step, with no citations. */
1356
+ declare function notMetVerification(options: {
1357
+ step: string;
1358
+ reasoning: string;
1359
+ }): VerificationResult;
1360
+ /** A passing or failing scripted verdict in the per-condition shape, with `isAppError` carried. */
1361
+ declare function scriptedVerification(options: {
1362
+ step: string;
1363
+ after?: ScreenState;
1364
+ action?: Action;
1365
+ pass: boolean;
1366
+ reasoning: string;
1367
+ isAppError?: boolean;
1368
+ }): VerificationResult;
1369
+ /** Convert a scripted verdict to the per-condition shape against what `verify` received. */
1370
+ declare function toVerificationResult(verdict: ScriptedVerdict, received: {
1371
+ step: string;
1372
+ after?: ScreenState;
1373
+ action?: Action;
1374
+ }): VerificationResult;
1375
+
1376
+ /**
1377
+ * A single platform's observed input for the shared spec. `buildInput` is the builder
1378
+ * input minus the parts the case controls (`nodes` and `platform`).
1379
+ */
1380
+ interface ConformanceInput {
1381
+ platform: Platform;
1382
+ nodes: InterimNode[];
1383
+ buildInput: Omit<BuildScreenStateInput, 'nodes' | 'platform'>;
1384
+ }
1385
+ /**
1386
+ * One executable contract assertion. `run` receives a factory so cases that need
1387
+ * independent builds (e.g. the determinism check) can materialize the input twice.
1388
+ * Violations throw a plain Error — no assertion library.
1389
+ */
1390
+ interface ScreenConformanceCase {
1391
+ name: string;
1392
+ run(make: () => ConformanceInput): void;
1393
+ }
1394
+ declare const screenConformanceCases: ScreenConformanceCase[];
1395
+
1396
+ type HookRuntime = 'node' | 'bun' | 'python' | 'bash';
1397
+ interface HookDefinition {
1398
+ id: string;
1399
+ name: string;
1400
+ runtime: HookRuntime;
1401
+ file: string;
1402
+ deps: string[];
1403
+ packageFile?: string;
1404
+ timeout: number;
1405
+ network: boolean;
1406
+ }
1407
+ interface HookResult {
1408
+ success: boolean;
1409
+ variables: Record<string, string>;
1410
+ output: string;
1411
+ stdout: string;
1412
+ stderr: string;
1413
+ duration: number;
1414
+ error?: string;
1415
+ }
1416
+ declare const RUNTIME_IMAGE_MAP: Record<HookRuntime, string>;
1417
+
1418
+ /** Structurally compatible with model-owner reports; no provider implementation dependency. */
1419
+ interface StepModelCallReport {
1420
+ inputTokens?: number;
1421
+ outputTokens?: number;
1422
+ cachedInputTokens?: number;
1423
+ respondingModelId?: string;
1424
+ }
1425
+ type StepModelCallEvent = {
1426
+ kind: 'start';
1427
+ } | {
1428
+ kind: 'success';
1429
+ report: StepModelCallReport;
1430
+ } | {
1431
+ kind: 'failure';
1432
+ };
1433
+ interface StepModelObservation {
1434
+ onCall(event: StepModelCallEvent): void;
1435
+ }
1436
+ interface StepModelScopeKey {
1437
+ runId: RunId;
1438
+ testIndex: number;
1439
+ stepIndex: number;
1440
+ }
1441
+ interface StepModelRoleFacts {
1442
+ declared?: ModelIdentity;
1443
+ respondingModelIds: string[];
1444
+ }
1445
+ interface StepModelAttempt {
1446
+ invocation: number;
1447
+ role: 'planner' | 'verifier';
1448
+ kind: StepModelCallEvent['kind'];
1449
+ report?: StepModelCallReport;
1450
+ }
1451
+ interface StepModelSnapshot extends StepModelScopeKey {
1452
+ attempts: StepModelAttempt[];
1453
+ started: number;
1454
+ succeeded: number;
1455
+ failed: number;
1456
+ inputTokens?: number;
1457
+ outputTokens?: number;
1458
+ cachedInputTokens?: number;
1459
+ roles: {
1460
+ planner: StepModelRoleFacts;
1461
+ verifier: StepModelRoleFacts;
1462
+ };
1463
+ }
1464
+ interface StepModelConsumers {
1465
+ planner: Planner;
1466
+ verifier?: Verifier;
1467
+ plannerModel: LanguageModel;
1468
+ verifierModel: LanguageModel;
1469
+ }
1470
+ interface StepModelScope extends StepModelConsumers {
1471
+ readonly key: StepModelScopeKey;
1472
+ snapshot(): StepModelSnapshot;
1473
+ /** Freeze after extraction, before terminal publication. Pending attempts remain uncovered. */
1474
+ finish(): StepModelSnapshot;
1475
+ /** Called only after the final event; retained callbacks cannot affect another step. */
1476
+ dispose(): void;
1477
+ }
1478
+ type StepModelScopeFactory = (key: StepModelScopeKey) => StepModelScope;
1479
+ interface StepModelBinding {
1480
+ /** Resolved handle supplies metadata; it is never mutated or double-wrapped. */
1481
+ model: Exclude<LanguageModel, string>;
1482
+ /**
1483
+ * Bind a fresh observer for ONE SDK invocation. Reuse host conversation/accounting state,
1484
+ * but never a model bound to another observer. Model-owner middleware emits every actual
1485
+ * retry start/terminal; the runner must not synthesize dispatches around generateText.
1486
+ */
1487
+ bind(observation: StepModelObservation): LanguageModel;
1488
+ }
1489
+ interface StepModelScopeOptions {
1490
+ key: StepModelScopeKey;
1491
+ models: {
1492
+ planner: StepModelBinding;
1493
+ verifier: StepModelBinding;
1494
+ };
1495
+ profiles?: {
1496
+ planner: ModelIdentity;
1497
+ verifier?: ModelIdentity;
1498
+ };
1499
+ mask(text: string): string;
1500
+ createConsumers(models: Pick<StepModelConsumers, 'plannerModel' | 'verifierModel'>): Pick<StepModelConsumers, 'planner' | 'verifier'>;
1501
+ }
1502
+ /**
1503
+ * The two 356 A5 config digests the run actually keyed on, computed once at the CLI
1504
+ * composition root and passed in the resolved {@link RunConfig} (D-20). Carried verbatim
1505
+ * into `runStarted`/the handoff so run identity and handoff identity cannot fork.
1506
+ */
1507
+ interface ConfigDigests {
1508
+ suiteConfigDigest: string;
1509
+ testConfigDigest: string;
1510
+ }
1511
+ /**
1512
+ * The run-start baseline health-probe outcome (D-04, RCC-02): ONE GET against the web
1513
+ * target's base URL, executed at the CLI composition root (the runner bans node:* — only
1514
+ * the RESULT crosses this seam). ALL probe failures arrive as values on `error`; a probe
1515
+ * never aborts the run. Consumed by the deterministic root-cause classifier's environment
1516
+ * branch (run/root-cause.ts).
1517
+ */
1518
+ interface BaselineProbeResult {
1519
+ status?: number;
1520
+ elapsedMs: number;
1521
+ error?: string;
1522
+ }
1523
+ /**
1524
+ * The JSONL event-log seam (D-05/D-06). BOTH legs are SYNC and fire-and-forget — the bus
1525
+ * NEVER awaits telemetry (a synchronous one-line append; D-06/D-07). `append` persists one
1526
+ * envelope per emitted event; a throw degrades the bus to memory-only with a single
1527
+ * `runWarning` (D-07). `read(fromSeq)` supplies older-than-window envelopes for deep replay.
1528
+ * The runner-local `FsEventLog` (Plan 07) implements this against `events.jsonl`.
1529
+ */
1530
+ interface EventLogPort {
1531
+ append(envelope: RunEventEnvelope): void;
1532
+ read(fromSeq: number): RunEventEnvelope[];
1533
+ }
1534
+ /**
1535
+ * The per-hook execution options the orchestration threads into the {@link HookExecutorPort}
1536
+ * (D-06). `env` is the accumulated-variables + user-env map the orchestrator merges per hook;
1537
+ * `secretEnv` is the ALREADY-RESOLVED secret name→value map for container injection — the CLI
1538
+ * composition root resolves the SecretStore and the runner never touches the store itself. The
1539
+ * secret VALUES double as the output-variable filter (a hook-emitted variable equal to a secret
1540
+ * value is dropped). `secretRedactor` redacts stdout/stderr/error; `authState` mounts + injects
1541
+ * the active auth state (container-local paths only). Pitfall 3: all sandbox-side
1542
+ * redaction/filtering stays inside the impl — the sink mask is additive.
1543
+ */
1544
+ interface HookExecuteOptions {
1545
+ env: Record<string, string>;
1546
+ secretEnv: Record<string, string>;
1547
+ secretRedactor?: SecretRedactorPort;
1548
+ authState?: RuntimeAuthStateConfig;
1549
+ }
1550
+ /**
1551
+ * The D-06 hook-execution seam: a runner-owned port whose only implementation is the
1552
+ * runner-local `DockerHookExecutor` (the 356/361 port-in-runner/impl-in-runner-local
1553
+ * precedent). `execute` runs ONE hook in the sandboxed container and resolves a
1554
+ * {@link HookResult} — a per-hook timeout resolves a FAILED result with an exit-124-style
1555
+ * error, NEVER a throw (the orchestration relies on a resolved result to synthesize
1556
+ * fail-fast skips). node:child_process / node:fs live behind this port in runner-local;
1557
+ * the runner stays storageNeutral (validate:arch is the arbiter).
1558
+ */
1559
+ interface HookExecutorPort {
1560
+ execute(hook: HookDefinition, opts: HookExecuteOptions): Promise<HookResult>;
1561
+ }
1562
+ /**
1563
+ * The durable handoff seam (D-05/D-17). SYNC single-document persist of the one
1564
+ * `CompletedRunHandoff` at run end; the runner-local `FsHandoffStore` (Plan 07) writes
1565
+ * `handoff.json` beside `events.jsonl`. Failure degrades with a `runWarning`, never gates
1566
+ * the verdict (D-07).
1567
+ */
1568
+ interface HandoffStorePort {
1569
+ save(handoff: CompletedRunHandoff): void;
1570
+ }
1571
+ /**
1572
+ * What the post-verdict step is told about the run whose posture it is deciding (404 D-10).
1573
+ *
1574
+ * The two markers are read off the ASSEMBLED `RunConfig` — the same values `startRun` was handed
1575
+ * — never off ambient state, so a gate-run skip (D-08) is decided from the run's own record.
1576
+ * Absent means absent: `?: true` carriers are conditionally spread, never written as `undefined`.
1577
+ */
1578
+ interface PostVerdictStepInput {
1579
+ readonly runId: RunId;
1580
+ readonly verdict: RunStatus;
1581
+ readonly gateRun?: true;
1582
+ readonly excludedFromOutcomes?: true;
1583
+ }
1584
+ /**
1585
+ * The step's answer: the posture, and — when the consult could not decide — the reason, by name.
1586
+ *
1587
+ * A warning rather than a fourth union arm (D-11). `detached` is the honest "nothing will be
1588
+ * drained from here", and the cause travels on the `runWarning` channel a surface already reads.
1589
+ */
1590
+ interface PostVerdictOutcome {
1591
+ readonly disposition: PostProcessingDisposition;
1592
+ readonly warning?: {
1593
+ readonly code: string;
1594
+ readonly message: string;
1595
+ };
1596
+ }
1597
+ /**
1598
+ * The post-verdict step (404 D-10) — the `HandoffStorePort` idiom applied to the ONE decision the
1599
+ * shell delays `runFinished` for.
1600
+ *
1601
+ * WHAT IT MAY DO: the durable enqueue and ONE writer-lease consult. Milliseconds. The runner
1602
+ * names no queue, no sqlite and no lease vocabulary (validate:arch storageNeutral) — the HOST
1603
+ * supplies the implementation, and the shell only awaits it.
1604
+ *
1605
+ * WHAT IT MAY NOT DO: gate `done`. A throw, a resolution past
1606
+ * {@link POST_VERDICT_STEP_BUDGET_MS}, or an absent port all yield `detached` plus a named
1607
+ * `runWarning`, and `runFinished` still publishes last with `bus.close()` behind it (403-03 D-3).
1608
+ * The step returns a VALUE and never touches the bus: the shell keeps sole ownership of the
1609
+ * terminal sequence.
1610
+ *
1611
+ * THE SHELL HOLDS NO LEASE. Whatever the step acquired stays with the host, which hands it to its
1612
+ * own pump — acquire → pump → release is ONE lease, never re-acquired (RESEARCH Pitfall 3).
1613
+ */
1614
+ interface PostVerdictPort {
1615
+ run(input: PostVerdictStepInput): Promise<PostVerdictOutcome>;
1616
+ }
1617
+ /**
1618
+ * The hard bound the shell races the step against (404 D-10, RESEARCH Pitfall 1).
1619
+ *
1620
+ * `acquireWriterLease` has contention/deadline semantics of its own, and a busy sqlite enqueue can
1621
+ * stall; without a bound here `runFinished`, `done`, the SSE close and the dashboard finalize all
1622
+ * slide behind it. Five seconds is generous for "enqueue + one consult" and still far below any
1623
+ * drain budget — reaching it means the consult is not going to answer, which is `detached`.
1624
+ */
1625
+ declare const POST_VERDICT_STEP_BUDGET_MS = 5000;
1626
+ /**
1627
+ * The D-08 run-artifact persistence seam: a runner-owned port whose only implementation is the
1628
+ * runner-local `FsRunArtifactStore` (the 356/361 port-in-runner/impl-in-runner-local precedent).
1629
+ * BOTH legs are SYNC and fire-and-forget — the shell NEVER awaits artifact persistence, and a
1630
+ * throw degrades with a `runWarning`, never gates the verdict (the event-log/handoff
1631
+ * discipline; D-07). The store writes ONE `artifact.json` into the SAME
1632
+ * `<root>/runs/<slug(runId)>/` family as `events.jsonl`/`handoff.json` (one self-contained run
1633
+ * directory). `stage(document)` persists the config + source snapshot once after `runStarted`;
1634
+ * `finalize(patch)` merges terminal errors + the end timestamp before `runFinished` — matching
1635
+ * the dashboard DB staged/finalize idiom so a crashed run still leaves a staged artifact. Per the
1636
+ * RESEARCH Q5 keep/drop table the FILE artifact keeps config/source/errors only; the top-level
1637
+ * `runtime` section is dropped (derived from events/handoff). An absent port is a silent no-op.
1638
+ */
1639
+ interface RunArtifactStorePort {
1640
+ stage(document: RunArtifactStageInput): void;
1641
+ finalize(patch: RunArtifactFinalizeInput): void;
1642
+ }
1643
+ /**
1644
+ * Injectable timer seam for the heartbeat + run-budget clocks (spec §2.2 liveness / D-03).
1645
+ * `schedule(delayMs, fn)` arms a one-shot and returns a cancel handle; `now()` supplies the
1646
+ * envelope `ts`. The default impl wraps global setTimeout/clearTimeout (unref'd so it never
1647
+ * keeps the process alive); unit pins inject a deterministic fake — no real timers, no watch
1648
+ * mode.
1649
+ */
1650
+ interface RunTimerPort {
1651
+ schedule(delayMs: number, fn: () => void): () => void;
1652
+ now(): number;
1653
+ }
1654
+ /**
1655
+ * One ordered member of the run (D-04). `failFastGroup` scopes today's suite fail-fast: a
1656
+ * failed/timed-out member skips the remaining SAME-group members; ungrouped members never
1657
+ * fail-fast, preserving the single-test path's run-all behavior.
1658
+ *
1659
+ * `testIndex` is optional: the shell owns iteration and derives the authoritative index
1660
+ * from array position (a caller-supplied value is a hint at most). `name` is REQUIRED so
1661
+ * the compiler enforces every descriptor construction site supplies the parsed test name;
1662
+ * handoff assembly copies it verbatim onto `PerTestOutcome.testName`.
1663
+ */
1664
+ interface RunMemberDescriptor {
1665
+ testId: string;
1666
+ testIndex?: number;
1667
+ name: string;
1668
+ failFastGroup?: string;
1669
+ /**
1670
+ * The member test file's WORKSPACE-RELATIVE path — rooted at the config directory,
1671
+ * forward-slashed, no leading `./`. REQUIRED since 381-01 (ABRF-02): stable test identity =
1672
+ * filePath + testName, copied verbatim onto `PerTestOutcome.filePath` by the handoff assembly —
1673
+ * the required field makes the compiler enforce that every descriptor construction site supplies
1674
+ * it.
1675
+ *
1676
+ * 390-08 (CR-02 / D-01): this is IDENTITY, not a file handle. It is deliberately checkout-
1677
+ * independent — the same bytes on every machine — because the committed `quarantine.jsonl` keys
1678
+ * entries on it and matches with `===`, so an absolute path here would produce a ledger that
1679
+ * could only ever apply on the machine that wrote it. Nothing in the run reads bytes off disk
1680
+ * through this field; the CLI mints it via core's `workspaceRelativeIdentityPath` and keeps the
1681
+ * absolute path on the side for every read.
1682
+ */
1683
+ filePath: string;
1684
+ /**
1685
+ * 363 D-01 parity fields (RESEARCH Q1). All OPTIONAL and additive — the shell rides them onto
1686
+ * the widened `testStarted` payload so reporters can render the full v1 console/JUnit/wire
1687
+ * surface from the bus alone. Suite grouping (`suiteId`/`suiteName`/`suiteIndex`/`suiteTotal`)
1688
+ * rides testStarted rather than a suite lifecycle event — the shell stays suite-agnostic and
1689
+ * reporters derive suite boundaries by watching the grouping change. `totalSteps` is the
1690
+ * per-member parsed step count. The CLI populates these at both startRun sites; absent
1691
+ * ⇒ omitted from the payload (no undefined-hole survives JSON round-trip).
1692
+ */
1693
+ suiteId?: string;
1694
+ suiteName?: string;
1695
+ suiteIndex?: number;
1696
+ suiteTotal?: number;
1697
+ totalSteps?: number;
1698
+ /**
1699
+ * G-428-2 (2026-09-26): the raw authored text of each step, in order. It is the same string
1700
+ * member-runner announces at `stepStarted` (`memberStepText`). The shell masks it onto
1701
+ * `testStarted.stepTexts`; callers never pre-mask. Absent ⇒ omitted from the payload.
1702
+ */
1703
+ stepTexts?: readonly string[];
1704
+ /**
1705
+ * THIS MEMBER'S OWN TARGET's run-start health baseline (D-04, 401-19 WR-04).
1706
+ *
1707
+ * PER MEMBER, NOT PER RUN, because a run's members may name different targets and a reading taken
1708
+ * against one of them says nothing about another. It lived on `RunConfig` and was handed to the
1709
+ * classifier for EVERY member, so a 503 observed against member B's target was recorded as member
1710
+ * A's environment failure. The CLI composition root fills this in behind the verdict, probing each
1711
+ * DISTINCT member URL once; absent means no probe applies to this member — the mobile posture,
1712
+ * where the device-session-create failure is the equivalent environment signal, and equally the
1713
+ * posture of a member whose target could not be resolved.
1714
+ */
1715
+ baselineProbe?: BaselineProbeResult;
1716
+ /**
1717
+ * 402-02 (D-09): members declaring the SAME lane share a mutable execution resource and MUST NOT
1718
+ * run concurrently — the bounded member pool runs them strictly one at a time, in index order.
1719
+ * Absent means "this member owns everything it touches" and the pool may schedule it freely.
1720
+ *
1721
+ * This is a PRECONDITION the producer states, never something the shell infers. D-09 requires
1722
+ * each concurrent member its own browser/CDP handle; today's suite path does not satisfy that
1723
+ * (every member of a suite drives the ONE adapter its suite set up, and `createSuiteLifecycle`
1724
+ * admits one active suite at a time), while the direct path does (each direct test constructs
1725
+ * its own adapter). A producer that starts handing each member its own session drops the lane
1726
+ * and gets concurrency; nothing in the shell has to be re-argued for that to be safe.
1727
+ */
1728
+ exclusiveLane?: string;
1729
+ }
1730
+ /**
1731
+ * The run-scoped event sink handed to the executor per member test (D-02). The shell binds
1732
+ * `runId`/`testIndex`/masking; core curries `stepIndex` via `loopEmitFor`. `stepStarted`
1733
+ * fires as a step opens; `loopEmitFor(stepIndex)` yields the {@link LoopEventEmitPort} the
1734
+ * loop emits `stepFinalized`/`subActionRecorded` through.
1735
+ */
1736
+ interface MemberEventSink {
1737
+ stepStarted(input: {
1738
+ stepIndex: number;
1739
+ stepText: string;
1740
+ }): void;
1741
+ loopEmitFor(stepIndex: number): LoopEventEmitPort;
1742
+ /**
1743
+ * 363 D-01 parity legs, each routed through its Plan 01 projection + the sink mask (never a raw
1744
+ * `bus.publish` from the caller — T-363-06). `hookStarted`/`hookFinished` are emitted by the
1745
+ * runner hook orchestration (Plan 04 populates them; `hookFinished` stdout/stderr/error and every
1746
+ * `variables` value are masked at the sink, T-363-05). `testRetryScheduled` is emitted by the
1747
+ * member-runner retry loop (Plan 10) and marks a per-test accumulation reset for subscribers.
1748
+ */
1749
+ hookStarted(input: HookStartedPayload): void;
1750
+ hookFinished(input: HookFinishedPayload): void;
1751
+ testRetryScheduled(input: TestRetryScheduledPayload): void;
1752
+ }
1753
+ /** Immutable member-start decision context used when the shell must synthesize step evidence. */
1754
+ interface MemberEvidenceContext {
1755
+ modelProfile?: DeclaredModelProfile;
1756
+ verifierIdentity?: ModelIdentity;
1757
+ toolSchemaDigest?: string;
1758
+ }
1759
+ /** The per-member execution context threaded into the injected {@link MemberTestExecutor}. */
1760
+ interface MemberTestRunContext {
1761
+ /** Canonical shell identity. Legacy executors without a shell cannot certify scoped usage. */
1762
+ runId?: RunId;
1763
+ abortSignal: AbortSignal;
1764
+ testIndex: number;
1765
+ events: MemberEventSink;
1766
+ setEvidenceContext(context: MemberEvidenceContext): void;
1767
+ /**
1768
+ * CASS-02 run-scoped decision-screenshot byte store (the same instance the shell holds on
1769
+ * {@link RunPorts}). The member threads it onto `LoopPorts.evidenceArtifacts`; absent ⇒ a
1770
+ * memory-only run with no screenshot persistence.
1771
+ */
1772
+ evidenceArtifacts?: EvidenceArtifactStorePort;
1773
+ /**
1774
+ * 403 D-05: declare (or withdraw) this member's live-stream producer. Optional on the SAME
1775
+ * `setEvidenceContext` precedent above — the executor pushes a capability the shell knows
1776
+ * nothing about, and the shell owns the per-run hub that refcounts it. Passing `undefined`
1777
+ * withdraws the declaration (the handle then reads `{ kind: 'none' }`). Absent on the context
1778
+ * ⇒ a composition that predates live view; the executor's optional call is a no-op.
1779
+ * `fallback` is the labelled snapshot producer the hub switches to when the primary fails to
1780
+ * start (428 D-12).
1781
+ */
1782
+ registerLiveStream?(capability: LiveStreamCapability | undefined, fallback?: LiveStreamCapability): void;
1783
+ }
1784
+ /**
1785
+ * The injected per-test execution port (D-01). The CLI composition root wraps core's
1786
+ * `runTest` behind this seam (core depends on runner, so runner can never import core);
1787
+ * Phase 363 dissolves the core side of the port when it migrates hooks/variables/reporters.
1788
+ * A staged migration, not a workaround — this is the exact seam 363 needs.
1789
+ */
1790
+ type MemberTestExecutor = (member: RunMemberDescriptor, ctx: MemberTestRunContext) => Promise<TestResult>;
1791
+ /**
1792
+ * The durable/telemetry/clock seams the run wires behind ports (D-04/D-05). All optional —
1793
+ * an absent leg degrades gracefully (no persistence, memory-only telemetry, default clock).
1794
+ * Grouped under `ports` so the resolved config keeps the run identity fields flat and the
1795
+ * injectable I/O seams together (the Wave-0 `run-shell.test.ts` pins this nesting).
1796
+ */
1797
+ interface RunPorts {
1798
+ handoffStore?: HandoffStorePort;
1799
+ eventLog?: EventLogPort;
1800
+ /**
1801
+ * The D-08 run-artifact store. Staged after `runStarted`, finalized before `runFinished`;
1802
+ * absent ⇒ no artifact.json is written (memory-only run, byte-identical pre-363-06 behavior).
1803
+ */
1804
+ runArtifactStore?: RunArtifactStorePort;
1805
+ /**
1806
+ * The run-budget clock (D-03). The shell arms `runBudgetMs` on this timer; it is NOT
1807
+ * routed into the bus heartbeat (the two clocks stay distinct so firing the injected
1808
+ * budget timer is unambiguous — see the shell's timer note).
1809
+ */
1810
+ timer?: RunTimerPort;
1811
+ /**
1812
+ * The CASS-02 run-scoped decision-screenshot artifact store (Plan 11). The runner-local
1813
+ * `FsEvidenceArtifactStore` is constructed run-scoped at the CLI composition root and injected
1814
+ * here; the shell hands the same instance to every member. Absent ⇒ no screenshot bytes are
1815
+ * persisted (memory-only run, byte-identical pre-Plan-11 behavior).
1816
+ */
1817
+ evidenceArtifacts?: EvidenceArtifactStorePort;
1818
+ /**
1819
+ * Where a VIEWER's fault is reported (403 WR-06): a live-frame subscriber that throws during
1820
+ * delivery is detached by the hub and the fact lands here. NEVER the bus — the run's permanent
1821
+ * transcript must not depend on who happened to be watching (LIVE-03). Same signature as the
1822
+ * hub's own option, so the shell forwards rather than adapts; absent ⇒ reported nowhere.
1823
+ */
1824
+ onSubscriberFault?: (code: string, message: string) => void;
1825
+ /**
1826
+ * The 404 D-10 post-verdict step, awaited between `runArtifactStore.finalize` and
1827
+ * `runFinished`. Absent ⇒ `disposition: 'detached'` with NO warning — a host that composed no
1828
+ * step is not an error, it is a host from which nothing will be drained, and the wire says so.
1829
+ */
1830
+ postVerdict?: PostVerdictPort;
1831
+ }
1832
+ /**
1833
+ * The fully-resolved run configuration `startRun` receives (D-04). The CLI keeps pattern
1834
+ * discovery, parsing, config merge, and target/device/model resolution; `startRun` never
1835
+ * touches fs or providers (validate:arch stays clean) — hence NO fs/provider/discovery-shaped
1836
+ * members here. A future cloud RPC surface reuses this same resolved-config contract.
1837
+ *
1838
+ * `members`/`ports` shapes are pinned by the Wave-0 `run-shell.test.ts` scaffold.
1839
+ */
1840
+ interface RunConfig {
1841
+ runId: RunId;
1842
+ scope: ScopeKey;
1843
+ executionContext: ExecutionContext;
1844
+ configDigests: ConfigDigests;
1845
+ buildOrdinal: number;
1846
+ /**
1847
+ * D-14 §6 gate-run exclusion copied into the durable completed-run handoff. Only literal
1848
+ * true excludes; absence denotes an ordinary counted run and remains absent in JSON.
1849
+ */
1850
+ excludedFromOutcomes?: true;
1851
+ /**
1852
+ * D-10 verify-before-present gate-run marker (Phase 384, 2026-07-28) copied into the
1853
+ * durable completed-run handoff. A DISTINCT axis from `excludedFromOutcomes` above:
1854
+ * C8-ratified, gate runs still feed curation. Only literal true marks a gate run;
1855
+ * absence denotes an ordinary run and remains absent in JSON. 384 threads the carrier
1856
+ * only — the exclusion semantics land in 388/393.
1857
+ *
1858
+ * ═══ 2026-08-26 — Phase 404 D-08 amends "gate runs still feed curation" ═══
1859
+ *
1860
+ * They still feed the FOLD — a gate run's handoff, folded, still emits curation candidates, and
1861
+ * the outcome-side exclusion still keys on `excludedFromOutcomes` alone. What changed is the
1862
+ * QUEUE: the post-verdict step enqueues NOTHING for a run carrying either marker, so no
1863
+ * `analyze-run` row exists for a `write_test` keep-filter probe. The reasons are all about the
1864
+ * gate loop rather than about correctness — no strong-model analysis cost per keep-gate
1865
+ * iteration, no draft evidence reaching curation through the chain, and no post-processing chips
1866
+ * about a run nobody will ever open. The marker still rides the handoff and the payload schema
1867
+ * still carries it, because the exclusion is a decision the enqueue takes and not a fact the
1868
+ * record forgets.
1869
+ */
1870
+ gateRun?: true;
1871
+ members: readonly RunMemberDescriptor[];
1872
+ executor: MemberTestExecutor;
1873
+ /**
1874
+ * The run's redactor over the secrets its producer read at the run's start (441-12, D-06/D-11).
1875
+ * Every bus event, and so `events.jsonl` and the dashboard rows, is masked with it and nothing
1876
+ * else. Absent ⇒ the run's events are published as written.
1877
+ */
1878
+ secretRedactor?: SecretRedactorPort;
1879
+ runBudgetMs?: number;
1880
+ ports?: RunPorts;
1881
+ /**
1882
+ * The D-08 config + source artifact snapshot the CLI composition root assembles (the
1883
+ * `buildRunArtifactContext` document). Staged verbatim into `artifact.json` after `runStarted`
1884
+ * via `ports.runArtifactStore`. Optional — absent ⇒ an empty stage (schema-version-only doc),
1885
+ * so the store leg stays a no-op-shaped write and the memory-only path is unchanged.
1886
+ */
1887
+ runArtifact?: RunArtifactStageInput;
1888
+ /**
1889
+ * Phase 390 D-16: the quarantined-test set, resolved ONCE at run start by the composition layer
1890
+ * (the CLI folds the committed ledger before the first member executes) and threaded in as PLAIN
1891
+ * DATA. This is deliberately NOT a port: the runner gains zero new I/O seams and stays
1892
+ * storageNeutral, and the disposition is deterministic for the whole run, so a mid-run ledger
1893
+ * edit can never retroactively change a running build. Mirrors how `executionContext` is
1894
+ * resolved once at run start.
1895
+ *
1896
+ * Members are matched on `(filePath, name)` — the stable test identity — never `testId`. A match
1897
+ * makes the member's failure skippable in the aggregate rule (`deriveAggregateVerdict`) and
1898
+ * stamps the disposition beside its truthful outcome in the handoff. Absent or empty ⇒
1899
+ * byte-identical pre-390 behavior.
1900
+ */
1901
+ quarantined?: readonly QuarantineAppliedEntry[];
1902
+ /**
1903
+ * 436 D-06: the planner limits the producer resolved for this run and the members whose own
1904
+ * limits differ, as plain data. Copied verbatim onto the handoff; absent stays absent.
1905
+ */
1906
+ plannerLimits?: PlannerLimitsRecord;
1907
+ /**
1908
+ * 402-02 (D-09/D-12): how many members may execute CONCURRENTLY in this run's process. Defaults
1909
+ * to 1 — one worker is a plain sequential loop, byte-identical to the pre-402 shell, so nothing
1910
+ * changes until a config resolves a higher value. Effective concurrency is
1911
+ * `min(members.length, workers)`; values below 1 and non-finite values resolve to 1.
1912
+ *
1913
+ * CONFIG-RESOLVED, NEVER REQUEST-SUPPLIED (D-12). It arrives on the already-resolved RunConfig
1914
+ * the composition root builds, exactly like `runBudgetMs` — `RunsStartInput` carries no
1915
+ * concurrency field, so a remote token-holder cannot raise the concurrency of a target whose
1916
+ * owner tuned it down (the refused-request-fields posture), and unbounded concurrency
1917
+ * (T-402-08) has no reachable input.
1918
+ */
1919
+ workers?: number;
1920
+ }
1921
+
1922
+ /**
1923
+ * The raw storage-state blob (adapter-produced; e.g. a Playwright storageState object). Opaque
1924
+ * to the runner — persisted and returned verbatim, never inspected or masked here (D-14).
1925
+ */
1926
+ type StorageStateBlob = unknown;
1927
+ /**
1928
+ * The evidence-safe reference to a persisted storage state (D-14). This — and ONLY this — is
1929
+ * what may transit into `StepEvidence`/the handoff: the name plus a one-way fingerprint of the
1930
+ * stored blob. The blob itself never leaves the store.
1931
+ */
1932
+ interface StorageStateReference {
1933
+ /** The caller-supplied slot name (as authored; the store slugs it before it becomes a path). */
1934
+ name: string;
1935
+ /** One-way fingerprint of the stored blob (the 343 `fingerprint()` idiom — identity, not the data). */
1936
+ fingerprint: string;
1937
+ }
1938
+ /**
1939
+ * The runner-facing storage-state persistence seam (D-14). A runner-local implementation owns
1940
+ * the `.agent-qa/storage-states/` filesystem writes; the loop injects it beside `cache` and
1941
+ * records only the returned reference.
1942
+ */
1943
+ interface StorageStatePort {
1944
+ /**
1945
+ * Persist the raw storage-state blob under `name` and return the evidence-safe reference.
1946
+ * The name is slugged traversal-safe before it becomes a filename (T-360-03).
1947
+ */
1948
+ save(name: string, state: StorageStateBlob): Promise<StorageStateReference>;
1949
+ /**
1950
+ * Load the raw blob previously saved under `name`, or `undefined` when no such slot exists
1951
+ * (a missing name is never an error).
1952
+ */
1953
+ load(name: string): Promise<StorageStateBlob | undefined>;
1954
+ }
1955
+
1956
+ type RecordAdmission = ServingAdmissionDecision;
1957
+ type ServingExposureSurface = ServedAdmissionSurface;
1958
+ interface ServingExposure {
1959
+ readonly surface: ServingExposureSurface;
1960
+ readonly turnOrdinal: number;
1961
+ }
1962
+ interface RecordLedger {
1963
+ /** Preview without mutation so channel-local and combined-turn checks can commit atomically. */
1964
+ inspect(record: ServingRecordMetadata): RecordAdmission;
1965
+ admit(record: ServingRecordMetadata, exposure?: ServingExposure): RecordAdmission;
1966
+ resetTurn(seedRecords: readonly ServingRecordMetadata[], exposure?: ServingExposure): void;
1967
+ /** Optional-compatible evidence seam; production ledgers provide both methods. */
1968
+ beginStepEvidence?(): void;
1969
+ drainEvidenceEvents?(): ServedAdmissionEvent[];
1970
+ size(): number;
1971
+ spent(): number;
1972
+ }
1973
+ interface ProductionRecordLedger extends RecordLedger {
1974
+ beginStepEvidence(): void;
1975
+ drainEvidenceEvents(): ServedAdmissionEvent[];
1976
+ }
1977
+ /**
1978
+ * One turn-time authority for the global distinct-record and combined dominance walls. Channel 1
1979
+ * seeds exact rendered material; Channel 3 and Channel 2 then charge every exposure against the
1980
+ * same source/cluster maps. Re-emphasizing an id remains free only for the DISTINCT slot — its
1981
+ * repeated bytes still count toward budget and dominance.
1982
+ */
1983
+ declare function createRecordLedger(input: {
1984
+ cap: number;
1985
+ budget: number;
1986
+ seedRecords: readonly ServingRecordMetadata[];
1987
+ }): ProductionRecordLedger;
1988
+
1989
+ /** Compression output — v1's CompressResult shape (Pitfall 9). */
1990
+ interface CompressedScreenshot {
1991
+ buffer: Buffer;
1992
+ imageWidth: number;
1993
+ imageHeight: number;
1994
+ }
1995
+ /**
1996
+ * Screenshot compression + dimension port. The core-side implementation owns
1997
+ * `sharp` (storageNeutral forbids it in runner, Pitfall 9). `compress` covers the
1998
+ * LLM-planner compression call site; `dimensions` covers the action-space-width
1999
+ * metadata fallback done inline via `sharp(screenshot).metadata()` at v1
2000
+ * loop.ts:199-201. The loop applies `stampCaptureMode` itself (runner-owned since 352).
2001
+ */
2002
+ interface ScreenshotCompressor {
2003
+ compress(screenshot: Buffer, options: {
2004
+ effectiveResolution: number;
2005
+ maxBytes?: number;
2006
+ actionSpaceWidth: number;
2007
+ }): Promise<CompressedScreenshot>;
2008
+ dimensions(screenshot: Buffer): Promise<{
2009
+ width: number;
2010
+ height: number;
2011
+ }>;
2012
+ }
2013
+ /**
2014
+ * Structural secret-lookup port. The concrete `SecretStore` (moved into runner in
2015
+ * this plan, task 2) satisfies it by construction; `resolveSecretTemplatesInValue`
2016
+ * consumes it. Named distinctly from the concrete class to avoid a barrel collision.
2017
+ */
2018
+ interface SecretResolver {
2019
+ get(name: string): string | undefined;
2020
+ require(name: string): string;
2021
+ has(name: string): boolean;
2022
+ /**
2023
+ * OPTIONAL name enumeration (PROC-02 · 381 D-08). The loop assembles the procedure
2024
+ * consult's binding-NAME set from the ports already in scope; secret template names are
2025
+ * part of that set so secret-parameterized procedures stay replay-eligible. Names only —
2026
+ * the callback receives the value because the concrete `SecretStore.forEachSecret`
2027
+ * already carries it, but the loop only ever collects the name; no value crosses the
2028
+ * reader port in any form. Absent implementations simply contribute no names.
2029
+ */
2030
+ forEachSecret?(callback: (name: string, value: string) => void): void;
2031
+ }
2032
+ /** Structural secret-redactor port — the concrete `SecretRedactor` satisfies it. */
2033
+ interface SecretRedactorPort {
2034
+ redactString(value: string): string;
2035
+ }
2036
+ /**
2037
+ * Structural run-scoped working-memory port (WM-01 · D-11/D-12). The concrete
2038
+ * `WorkingMemoryStore` (working-memory/store.ts) satisfies it by construction; declared
2039
+ * structurally here (the SecretResolver naming precedent) so the ports module never imports the
2040
+ * concrete class. `capture` writes verbatim entries (registered secrets redacted at capture);
2041
+ * `entries` returns the whole set in capture order; `dispose` empties it at run end.
2042
+ */
2043
+ interface WorkingMemoryPort {
2044
+ capture(entries: readonly WorkingMemoryEntry[], ctx?: {
2045
+ test?: number;
2046
+ step?: number;
2047
+ }): void;
2048
+ entries(): WorkingMemoryEntry[];
2049
+ dispose(): void;
2050
+ }
2051
+ /** Minimal scoped-logger surface the loop uses; core's LogManager satisfies it. */
2052
+ interface LoopScopedLogger {
2053
+ debug(message: string, meta?: Record<string, unknown>): void;
2054
+ info(message: string, meta?: Record<string, unknown>): void;
2055
+ warn(message: string, meta?: Record<string, unknown>): void;
2056
+ error?(message: string, meta?: Record<string, unknown>): void;
2057
+ }
2058
+ interface LoopLogger {
2059
+ createScopedLogger(scope: string): LoopScopedLogger | undefined;
2060
+ }
2061
+ /**
2062
+ * Runner-local structural copy of model's DegradeOptions (D-05/D-07). Runner cannot
2063
+ * import the model package, so the shape is duplicated here; Plan 05's cli conformance
2064
+ * proof asserts model's `DegradeOptions` structurally matches this. Policy-free: the
2065
+ * caller (Phase 355) supplies the concrete budget + pin predicate.
2066
+ */
2067
+ interface RunnerDegradeOptions {
2068
+ budget: number;
2069
+ pin?: (message: ModelMessage, index: number) => boolean;
2070
+ }
2071
+ /**
2072
+ * Runner-local structural copy of model's DegradeOutcome (D-09). `ok:true` carries the
2073
+ * fitted messages; `ok:false` is the structured cannot-fit outcome (system + final +
2074
+ * pinned content alone exceed the budget) — degrade never throws and never violates a pin.
2075
+ */
2076
+ type RunnerDegradeOutcome = {
2077
+ ok: true;
2078
+ messages: ModelMessage[];
2079
+ } | {
2080
+ ok: false;
2081
+ reason: 'pinned-content-exceeds-budget';
2082
+ messages: ModelMessage[];
2083
+ };
2084
+ /**
2085
+ * Provider-neutral token-accounting port (D-05/D-07) — the 354↔355 seam. A types-only
2086
+ * structural duplicate of model's `TokenAccounting` (accounting/types.ts): runner cannot
2087
+ * import model, so the shape is mirrored here and message types are imported from `'ai'`
2088
+ * only (never `@ai-sdk/*`, PROVIDER_ORCHESTRATION_ALLOWED). Policy-free — prompt tokens
2089
+ * only. This plan declares the seam; Phase 355 wires estimate/degrade consumption.
2090
+ */
2091
+ interface TokenAccounting {
2092
+ estimate(messages: ModelMessage[]): number;
2093
+ recordUsage(usage: LanguageModelUsage, messages: ModelMessage[]): void;
2094
+ getCeilings(): {
2095
+ contextWindow: number;
2096
+ maxOutput: number;
2097
+ };
2098
+ degrade(messages: ModelMessage[], options: RunnerDegradeOptions): RunnerDegradeOutcome;
2099
+ }
2100
+ /**
2101
+ * D-02 run-scoped event-emit seam. The loop emits `stepFinalized`/`subActionRecorded` at the
2102
+ * moment they occur (live semantics from day one) — the bus assigns `seq` centrally. Both legs
2103
+ * are fire-and-forget SYNCHRONOUS void: the loop must NEVER await telemetry (D-02/D-07). Follows
2104
+ * the optional-port idiom (masking/workingMemory): structural, optional on {@link LoopPorts},
2105
+ * optional-chained at the emit site, never throw-on-absent (D-05 degrade-when-absent). The
2106
+ * concrete curry lands at the run shell (`MemberEventSink.loopEmitFor`); the port survives 363's
2107
+ * migration unchanged.
2108
+ */
2109
+ interface LoopEventEmitPort {
2110
+ /**
2111
+ * 363 D-01: the additive-optional `extra` carries the StepResult-derived
2112
+ * `{status, durationMs, error}` the reporters need (RESEARCH Q1 parity). Sourced from the
2113
+ * runner `StepResult` at the loop's single terminal `finish()` funnel — NOT fields on the §A8
2114
+ * `StepEvidence` DTO (Pitfall 2). `error` is masked at the sink chokepoint (T-363-05). Absent
2115
+ * ⇒ the base evidence-only payload, source-compatible with pre-363 callers.
2116
+ */
2117
+ stepFinalized(evidence: StepEvidence, extra?: {
2118
+ status?: StepFinalizedPayload['status'];
2119
+ durationMs?: number;
2120
+ error?: string;
2121
+ /**
2122
+ * 405 D-05: the step's measured usage, from the loop's own planner + verifier tally.
2123
+ * Absent when nothing was measured — the loop's `tokenUsage()` already answers
2124
+ * `undefined` in that case, so absence is produced, never fabricated here.
2125
+ */
2126
+ tokenUsage?: StepFinalizedPayload['tokenUsage'];
2127
+ /** Finished member scope, including late extraction; overrides the legacy loop tally. */
2128
+ modelObservations?: ModelUsageObservations;
2129
+ evidence?: StepFinalizedPayload['evidence'];
2130
+ variableSnapshot?: StepFinalizedPayload['variableSnapshot'];
2131
+ }): void;
2132
+ subActionRecorded(evidence: SubActionEvidence, subActionIndex: number,
2133
+ /** Capture-time ARIA state; the projector appends its ref beside the decision screenshot. */
2134
+ extra?: {
2135
+ ariaEvidence?: SubActionRecordedPayload['ariaEvidence'];
2136
+ recordingOffsetMs?: number;
2137
+ }): void;
2138
+ }
2139
+ /**
2140
+ * Run-scoped content-addressed evidence artifact store (CASS-02). Storage-neutral: a
2141
+ * runner-local `FsEvidenceArtifactStore` owns the sha-keyed blob persistence; the runner
2142
+ * declares only the shape. `persistDecisionScreenshot` returns the identity for the exact
2143
+ * byte payload so a decision screenshot becomes addressable by `ArtifactRef.sha`; `getBlob`
2144
+ * serves those bytes back, letting a host adapter satisfy eval's structurally identical
2145
+ * `ArtifactSource` without a runner-to-eval dependency. Both legs are synchronous.
2146
+ */
2147
+ interface PersistedRecording {
2148
+ ref: RecordingRef;
2149
+ byteLength: number;
2150
+ }
2151
+ interface EvidenceArtifactStorePort {
2152
+ persistDecisionScreenshot(bytes: Uint8Array): ArtifactRef;
2153
+ /** Already live-masked JSON only. Optional for existing screenshot-only hosts. */
2154
+ persistEvidenceDocument?(document: Exclude<EvidenceDocument, {
2155
+ kind: 'variables';
2156
+ }>): ArtifactRef;
2157
+ hasBlob?(sha: string): boolean;
2158
+ getBlob(sha: string): Uint8Array | undefined;
2159
+ persistRecording?(stagingPath: string): PersistedRecording;
2160
+ blobSize?(sha: string): number | undefined;
2161
+ readBlobRange?(sha: string, start: number, end: number): Uint8Array | undefined;
2162
+ discardRecording?(stagingPath: string): void;
2163
+ }
2164
+ /**
2165
+ * The member-scoped procedure-flow carry (F-381-51-1 · 381 D-09): the replay position a
2166
+ * completed test step hands to the NEXT test step so a committed multi-step flow replays
2167
+ * exactly one body step per test step. Transports ONLY the consult match, the parsed body
2168
+ * payload, and the cursor — never a pre-resolved ref, never a screen: every carried body
2169
+ * step re-earns dispatch through the full fresh-screen descriptor/guard/page-anchor/
2170
+ * grounding-gate ladder at its own test step (D-10 — a stored action can never dispatch
2171
+ * against a screen its guards did not pass on at that step).
2172
+ */
2173
+ interface ProcedureFlowCarry {
2174
+ match: ProcedureMatch;
2175
+ payload: ProcedureBodyPayload;
2176
+ nextIndex: number;
2177
+ }
2178
+ /** 439 D-04: a completed download from an earlier step of this test, labelled by its producer. */
2179
+ interface CarriedDownload {
2180
+ stepNumber: number;
2181
+ /** 1-based position of the producing action in its step. */
2182
+ actionIndex: number;
2183
+ evidence: DownloadEvidence;
2184
+ }
2185
+ /** 439 D-04: the latest app clipboard write from an earlier step of this test. */
2186
+ interface CarriedClipboard {
2187
+ stepNumber: number;
2188
+ actionIndex: number;
2189
+ evidence: ClipboardEvidence;
2190
+ }
2191
+ interface CarriedArtifacts {
2192
+ downloads: CarriedDownload[];
2193
+ clipboard?: CarriedClipboard;
2194
+ }
2195
+ /**
2196
+ * The runner-owned port bundle the loop executes against — successor to v1's
2197
+ * AgentLoopConfig (core types.ts:66-104), minus the D-10 healing/attempt-limit
2198
+ * fields and minus the v1 cache-key inputs (those move behind the cache port).
2199
+ */
2200
+ interface LoopPorts {
2201
+ recordingZeroMs?: number;
2202
+ adapter: PlatformAdapter;
2203
+ planner: Planner;
2204
+ verifier?: Verifier;
2205
+ /**
2206
+ * Redesigned per-key cache port (D-11): get/set/invalidate over a branded CacheKey,
2207
+ * descriptor-only value. Plan 05's FsActionCache implements it; injected at the Plan 06
2208
+ * CLI composition root. Absent ⇒ every step misses (no cache), never throws (D-05).
2209
+ */
2210
+ cache?: ActionPlanCache;
2211
+ /**
2212
+ * D-05 declared decision context for both model roles. Composition roots pass the
2213
+ * same profile objects to the planner/verifier constructors and this recording lane;
2214
+ * the loop never derives model facts from a model handle. Absent preserves the
2215
+ * provider-neutral degrade-when-absent contract.
2216
+ */
2217
+ modelProfiles?: {
2218
+ planner: DeclaredModelProfile;
2219
+ verifier?: DeclaredModelProfile;
2220
+ };
2221
+ /**
2222
+ * D-16 once-per-member digest of the complete bound tool surface. It remains a
2223
+ * separate optional port because tool attribution is distinct from model identity;
2224
+ * absent callers continue without evidence stamping and never throw.
2225
+ */
2226
+ toolSchemaDigest?: string;
2227
+ compressor?: ScreenshotCompressor;
2228
+ /** D-05 optional read-only knowledge reader — the loop degrades gracefully when absent. */
2229
+ knowledgeReader?: KnowledgeHintReader;
2230
+ /**
2231
+ * 407 D-13/D-17 (2026-08-29) — the HOST's retrieval rung, resolved ONCE at the composition
2232
+ * root by knowledge-local's single `resolveRetrievalRung`, never re-probed per step. It
2233
+ * crosses as the schema type because the runner never imports knowledge-local (validate:arch
2234
+ * enforces that); a promise because the resolver's embedder probe can cost a model load and
2235
+ * overlapping it with run start keeps the probe off the critical path.
2236
+ *
2237
+ * It describes the ladder the machine was on when this run was served — NOT how this brief was
2238
+ * ranked. Absent ⇒ no rung is stamped (D-05 degrade-when-absent); a rejection stamps nothing
2239
+ * and never fails a step; and a rung is never stamped onto a step no reader served (D-16 —
2240
+ * "no knowledge reader bound is not a rung").
2241
+ */
2242
+ knowledgeRetrievalRung?: Promise<RecordedRetrievalRung>;
2243
+ /**
2244
+ * F-381-51-1 · D-09 member-scoped carry cell (the recordLedger shared-mutable-object
2245
+ * precedent): the caller owns ONE cell per test member and threads the SAME object into
2246
+ * every per-step ports literal. The loop TAKES the carry at step start (reading and
2247
+ * immediately emptying the cell) and re-arms it only after the step's verifier accepts a
2248
+ * successful replayed step with body steps remaining — take-then-rearm, the D-11 one-shot
2249
+ * law extended across the step boundary: every other exit (guard miss, execution failure,
2250
+ * verifier rejection, policy denial, step failure, abort, exhaustion) leaves the cell
2251
+ * empty. Loop-side state only: never serialized, never recorded into evidence or the
2252
+ * handoff, never crosses a test boundary. Absent ⇒ no cursor crosses steps (D-05
2253
+ * degrade-when-absent).
2254
+ */
2255
+ procedureFlow?: {
2256
+ carry?: ProcedureFlowCarry;
2257
+ };
2258
+ /**
2259
+ * 439 D-04 member-scoped carry cell, the `procedureFlow` precedent: the caller owns ONE cell per
2260
+ * test member and threads the SAME object into every per-step ports literal. The loop renders its
2261
+ * items into the step record's carried lines at step start, and at the step's end appends the
2262
+ * step's completed downloads and replaces the clipboard with its latest write. Loop-side state
2263
+ * only: never serialized, never recorded into evidence or the handoff, never crosses a test
2264
+ * boundary. Absent ⇒ nothing carries (the live editor).
2265
+ */
2266
+ carriedArtifacts?: CarriedArtifacts;
2267
+ /**
2268
+ * D-02/D-15/D-17 knowledge budgets are computed beside model token accounting at the
2269
+ * composition root and arrive here as plain numbers. The provider-neutral loop never derives
2270
+ * model ceilings or imports token-accounting policy; absent bindings use provisional floors.
2271
+ */
2272
+ knowledgeBudgets?: {
2273
+ brief: number;
2274
+ searchResults: number;
2275
+ caution: number;
2276
+ readBody: number;
2277
+ recordCap: number;
2278
+ };
2279
+ /** D-21 run-fixed build ordinal copied into ServedBrief attribution when a reader is bound. */
2280
+ knowledgeAsOfBuild?: number;
2281
+ /**
2282
+ * D-17 loop-controlled turn ledger. A composition root that registers Channel-2 tools binds
2283
+ * the same object into their closures; the loop owns reset timing and Channel 3 consults it
2284
+ * directly. Absent is legal and creates a step-local ledger for Channel-1 accounting only.
2285
+ */
2286
+ recordLedger?: RecordLedger;
2287
+ /**
2288
+ * D-11 Channel-2 turn reset owned by the memory-tool wrapper. The loop invokes it only after
2289
+ * reseeding the shared distinct-record ledger from the frozen brief, so both ledgers begin the
2290
+ * live planning turn from the same boundary.
2291
+ */
2292
+ beginMemoryTurn?: (scope?: ScopeKey, exposedIds?: readonly RecordId[]) => void;
2293
+ /** Consume the successful direct reads served during the current live planning turn. */
2294
+ drainMemoryReadTraces?: () => ReadTrace[];
2295
+ plannerConfig?: PlannerConfig;
2296
+ /** Wall-clock timeout for a single step (ms). 0 or undefined = no timeout. */
2297
+ stepTimeout?: number;
2298
+ logger?: LoopLogger;
2299
+ logCapture?: {
2300
+ console?: boolean;
2301
+ network?: boolean;
2302
+ };
2303
+ vision?: {
2304
+ screenshotSize?: number;
2305
+ effectiveResolution?: number;
2306
+ };
2307
+ visionGate?: {
2308
+ attachImage: boolean;
2309
+ captureMode: 'full' | 'textOnly';
2310
+ promptNote: string;
2311
+ };
2312
+ /** Pitfall 8 — setVariable executes via this callback, NOT the adapter. */
2313
+ onSetVariable?: (name: string, value: string) => void;
2314
+ /** Pitfall 1 — path resolution stays core-side, injected pre-resolved into the loop. */
2315
+ resolveUploadPath?: (rawPath: string) => string;
2316
+ abortSignal?: AbortSignal;
2317
+ secretStore?: SecretResolver;
2318
+ secretRedactor?: SecretRedactorPort;
2319
+ /**
2320
+ * WM-01 run-scoped working-memory seam (D-11/D-12). The concrete `WorkingMemoryStore` is
2321
+ * created at the CLI composition root and injected here; Phase 361 rebinds this exact seam onto
2322
+ * `RunSession` (the knowledgeReader idiom). Unbound ⇒ the loop runs memory-less: no capture, an
2323
+ * empty served set — it never throws (D-05 degrade-when-absent).
2324
+ */
2325
+ workingMemory?: WorkingMemoryPort;
2326
+ /**
2327
+ * D-14 storage-state persistence seam (Phase 360). The runner-local `FsStorageStateStore` is
2328
+ * constructed run-scoped at the CLI/dashboard composition roots and injected here; the seam
2329
+ * strips the raw blob from the recorded result and records only `{ name, fingerprint }`. Unbound
2330
+ * ⇒ a `storageState` action degrades to a §7.1 persistence-unavailable error (never throws, D-05).
2331
+ */
2332
+ storageState?: StorageStatePort;
2333
+ /**
2334
+ * D-16 injectable unique-value suffix generator (353 K=2 determinism). Absent ⇒ the seam uses
2335
+ * `defaultUniqueValueGenerator` (timestamp + entropy). Tests inject a fixed generator for
2336
+ * deterministic minted values; production leaves it unbound.
2337
+ */
2338
+ uniqueValueGenerator?: UniqueValueGenerator;
2339
+ /**
2340
+ * D-06 optional pre-prompt blob-guard — bounds a single one-shot structured tool-output
2341
+ * value before it lands in the sub-action history / next planning turn. A pure signature
2342
+ * (no `ai`/model types, zero arch risk). Composition roots that depend on model may inject
2343
+ * model's canonical `guardBlob`; the loop degrades to a runner-local fallback when absent.
2344
+ */
2345
+ guardBlob?: (value: unknown) => string | undefined;
2346
+ /**
2347
+ * D-02 run-scoped event-emit seam. Bound by the run shell (per-step curry via
2348
+ * `MemberEventSink.loopEmitFor`) so the loop emits `stepFinalized`/`subActionRecorded` live;
2349
+ * absent ⇒ the loop runs telemetry-less (no emit), never throws (D-05 degrade-when-absent).
2350
+ */
2351
+ events?: LoopEventEmitPort;
2352
+ /**
2353
+ * D-20 real config digests, so the loop can key the cache on what the run actually keyed on
2354
+ * (consumed in Plan 08). Type-imported from the run layer; absent ⇒ the existing key material
2355
+ * is used unchanged.
2356
+ */
2357
+ configDigests?: ConfigDigests;
2358
+ /**
2359
+ * CASS-02 run-scoped decision-screenshot byte store. The shell hands one store instance to
2360
+ * every member; the loop mints an `ArtifactRef` for captured screenshot bytes through it.
2361
+ * Absent ⇒ a legal memory-only / test-harness state (no persistence), never throws (D-05).
2362
+ */
2363
+ evidenceArtifacts?: EvidenceArtifactStorePort;
2364
+ }
2365
+
2366
+ declare function executeStep(stepInstruction: string, ports: LoopPorts, context: StepContext): Promise<StepResult>;
2367
+
2368
+ /**
2369
+ * Capture-location context stamped onto each entry (D-11): the whole run set is served every
2370
+ * turn, so the planner weighs relevance itself — these mark where the entry was captured, they
2371
+ * are not a filter rule.
2372
+ */
2373
+ interface WorkingMemoryProvenanceContext {
2374
+ test?: number;
2375
+ step?: number;
2376
+ }
2377
+ /**
2378
+ * Run-scoped verbatim working memory: a plain Map keyed by entry key, latest-wins on
2379
+ * re-capture (D-12 — one key ⇒ one current value, the OTP-resend reality), whole-set recall in
2380
+ * capture order (D-11 — no filtering, no search). Registered secrets are redacted at capture
2381
+ * (Pitfall 7 boundary 1); every other value stays verbatim.
2382
+ */
2383
+ declare class WorkingMemoryStore {
2384
+ private readonly redactor?;
2385
+ private readonly store;
2386
+ constructor(redactor?: SecretRedactorPort | undefined);
2387
+ /**
2388
+ * Capture verbatim entries. Each item is validated against workingMemoryEntrySchema (closed
2389
+ * enums reject unknown kind/provenance at the boundary, V5), its value redacted for registered
2390
+ * secrets only, and stamped with the capture location. Re-capturing an existing key overwrites
2391
+ * its value (D-12 latest-wins); the key's original capture position is preserved.
2392
+ */
2393
+ capture(entries: readonly WorkingMemoryEntry[], ctx?: WorkingMemoryProvenanceContext): void;
2394
+ /** The whole run-scoped set in capture order (D-11 — no filtering, no search). */
2395
+ entries(): WorkingMemoryEntry[];
2396
+ /** Empty the store at run end (WM-02 disposal; the redacted flush is composed elsewhere). */
2397
+ dispose(): void;
2398
+ }
2399
+
2400
+ type AgentAbortScope = 'test' | 'step';
2401
+ interface AgentTimeoutAbortReason {
2402
+ type: 'agent-qa-timeout';
2403
+ scope: AgentAbortScope;
2404
+ timeoutMs: number;
2405
+ message: string;
2406
+ }
2407
+ declare function createTimeoutAbortReason(scope: AgentAbortScope, timeoutMs: number): AgentTimeoutAbortReason;
2408
+ /**
2409
+ * True iff `message` is the message createTimeoutAbortReason('test', ms) mints for
2410
+ * some ms — the D-16 member-timeout signal. Rejects the 'step'-scope variant (a step
2411
+ * timeout is an ordinary failure) and any arbitrary text.
2412
+ *
2413
+ * WHY match on the message string: the AgentTimeoutAbortReason object does NOT survive
2414
+ * onto TestResult — the only trace that reaches verdict derivation is a failed step
2415
+ * whose `error` equals this minted message (RESEARCH Q3). This matcher is the one place
2416
+ * that knowledge lives, exported beside the sole mint site so the format stays
2417
+ * single-sourced.
2418
+ */
2419
+ declare function isTestTimeoutMessage(message: string): boolean;
2420
+ declare function getTimeoutAbortReason(signal: AbortSignal | undefined): AgentTimeoutAbortReason | undefined;
2421
+ declare function withAbort<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T>;
2422
+
2423
+ type ToolCategory = 'action' | 'file' | 'network';
2424
+ interface ToolDefinition<T extends z.ZodType = z.ZodType> {
2425
+ id: string;
2426
+ type: string;
2427
+ surface: Surface;
2428
+ exposed?: boolean;
2429
+ description: string;
2430
+ category: ToolCategory;
2431
+ schema: T;
2432
+ /** Advisory audit/confirmation hint only; must not gate platform slices. */
2433
+ destructive?: boolean;
2434
+ /** Advisory audit/trace hint only; must not gate platform slices. */
2435
+ readOnly?: boolean;
2436
+ }
2437
+
2438
+ declare const EXPOSURE: Record<Platform, ReadonlySet<Surface>>;
2439
+ declare class ToolRegistry {
2440
+ private tools;
2441
+ register<T extends z.ZodType>(def: ToolDefinition<T>): this;
2442
+ get(id: string): ToolDefinition | undefined;
2443
+ getAll(): ToolDefinition[];
2444
+ getForPlatform(platform: Platform): ToolDefinition[];
2445
+ getSchema(id: string): z.ZodType;
2446
+ generateDocs(platform?: Platform): string;
2447
+ }
2448
+ declare const defaultRegistry: ToolRegistry;
2449
+
2450
+ /**
2451
+ * K — the per-turn non-action tool-call budget (D-06/D-10). Channel 2's designed
2452
+ * search→read sequence needs two calls, and one retry pair must fit in the same turn.
2453
+ * This remains an internal provisional constant, injectable via
2454
+ * ExecutePlanningTurnOptions.budget for tests only. Do NOT promote it to user-facing
2455
+ * config or thread it through LoopPorts / any config schema.
2456
+ */
2457
+ declare const DEFAULT_NON_ACTION_TOOL_BUDGET = 4;
2458
+ /**
2459
+ * A caller-registered non-action tool. `tool` is the `ai` definition merged into the
2460
+ * always-send-all slice; `execute` runs the call and returns a JSON-serializable
2461
+ * result appended to the conversation as a tool message. Zero of these ship in 353;
2462
+ * tests register fakes to exercise the machinery (D-07).
2463
+ */
2464
+ interface NonActionToolHandler {
2465
+ tool: Tool;
2466
+ execute(args: Record<string, unknown>): Promise<unknown> | unknown;
2467
+ }
2468
+ type NonActionToolMap = Record<string, NonActionToolHandler>;
2469
+ /**
2470
+ * An ordered record of one executed non-action tool call within a planning turn
2471
+ * (§B1). Exchanges persist on SubActionEvidence for successful decisions (INST-01,
2472
+ * D-01) and PlanningFailureEvidence for failed turns (D-03).
2473
+ */
2474
+ type ToolExchange = ToolExchangeEvidence;
2475
+ type PlanningTurnFailureReason = 'no_action_tool_called' | 'non_action_tool_budget_exhausted' | 'ungroundable_action' | 'planning_limit_reached';
2476
+ /**
2477
+ * The in-turn groundability verdict for a parsed action (D-09). Returned by the
2478
+ * injected `groundability` callback the caller (LLMPlanner) builds from the Plan-04
2479
+ * grounding gate + the in-scope screenState — the tool-loop stays gate-agnostic and
2480
+ * NEVER imports `grounding/` itself.
2481
+ *
2482
+ * `ok: true` may optionally carry the gate's fully-grounded execution-shape `action`
2483
+ * (ref, never a descriptor) and its §A8 `attempts` trail; when present the turn swaps
2484
+ * them onto the returned plan so the resolved action + evidence ride to executeStep.
2485
+ * When omitted (e.g. a test seam that only signals groundability) the parsed plan is
2486
+ * returned unchanged. `ok: false` carries the §7.1 error text injected back into the
2487
+ * SAME conversation as an error-text tool result for the model's one-shot correction.
2488
+ */
2489
+ type GroundabilityResult = {
2490
+ ok: true;
2491
+ action?: Record<string, unknown>;
2492
+ attempts?: GroundingAttempt[];
2493
+ statedTargets?: StatedTargets;
2494
+ } | {
2495
+ ok: false;
2496
+ message: string;
2497
+ };
2498
+ /** The injected groundability callback: parsed action → groundability verdict (D-09). */
2499
+ type GroundabilityCallback = (plan: ActionPlan) => GroundabilityResult;
2500
+ interface PlanningTurnSuccess {
2501
+ ok: true;
2502
+ plan: ActionPlan;
2503
+ exchanges: ToolExchange[];
2504
+ tokenUsage?: TokenUsage;
2505
+ callRecord?: ModelCallRecord;
2506
+ }
2507
+ interface PlanningTurnFailure {
2508
+ ok: false;
2509
+ reason: PlanningTurnFailureReason;
2510
+ message: string;
2511
+ exchanges: ToolExchange[];
2512
+ tokenUsage?: TokenUsage;
2513
+ callRecord?: ModelCallRecord;
2514
+ }
2515
+ type PlanningTurnResult = PlanningTurnSuccess | PlanningTurnFailure;
2516
+ interface ExecutePlanningTurnOptions {
2517
+ model: LanguageModel;
2518
+ /** Registry backing toolCallToActionPlan (the turn's termination projection). */
2519
+ registry: ToolRegistry;
2520
+ /** The full v45 action slice (buildTools output) — always sent (always-send-all). */
2521
+ actionTools: ToolSet;
2522
+ /**
2523
+ * System prompt — a single string, or the Phase 355 [stable, context, volatile]
2524
+ * tier array the caller assembles (D-15). Passed through to generateText untouched;
2525
+ * the tool-loop applies no clamp/degrade policy of its own.
2526
+ */
2527
+ system: string | Array<SystemModelMessage>;
2528
+ /** The initial user message(s): text and optional image, built by the caller. */
2529
+ messages: ModelMessage[];
2530
+ /** Caller-registered non-action tools. Empty in production (D-07). */
2531
+ nonActionTools?: NonActionToolMap;
2532
+ /** K override for tests (D-06). Defaults to DEFAULT_NON_ACTION_TOOL_BUDGET. */
2533
+ budget?: number;
2534
+ /**
2535
+ * D-09 in-turn grounding feedback. When present, a parsed action call is grounded by
2536
+ * this injected callback after `toolCallToActionPlan` succeeds. A failure appends the
2537
+ * §7.1 error text as an error-text tool result in the SAME conversation with EXACTLY
2538
+ * ONE retry (a turn-local one-shot flag, never a counter — LOOP-02); a second
2539
+ * consecutive grounding failure returns a structured `'ungroundable_action'` failure.
2540
+ * The retry neither consumes nor bypasses the ≤K non-action budget (Pitfall 8). When
2541
+ * absent the turn behaves exactly as the 353 bounded loop (back-compat within phase).
2542
+ */
2543
+ groundability?: GroundabilityCallback;
2544
+ /**
2545
+ * Dynamic output cap (CTXB-04) computed by the caller's clamp helper and passed
2546
+ * through to generateText. `undefined` is a valid no-op passthrough — the tool-loop
2547
+ * computes no clamp of its own (D-15/Pitfall 5).
2548
+ */
2549
+ maxOutputTokens?: number;
2550
+ /**
2551
+ * Optional sampling parameters applied at the model call site (D-10/D-11). Every
2552
+ * field is optional and absence preserves the provider default. `maxOutputTokens`
2553
+ * is deliberately excluded because the output ceiling remains clamp-owned.
2554
+ */
2555
+ sampling?: SamplingParams;
2556
+ /**
2557
+ * 436 D-08: when bound, every model call of the turn is debited before it is made; a refused
2558
+ * debit ends the turn with `planning_limit_reached` and no model call.
2559
+ */
2560
+ planningCallGate?: PlanningCallGate;
2561
+ abortSignal?: AbortSignal;
2562
+ logger?: LoopScopedLogger;
2563
+ }
2564
+ declare function executePlanningTurn(options: ExecutePlanningTurnOptions): Promise<PlanningTurnResult>;
2565
+
2566
+ /**
2567
+ * Structured planning-turn failure used to preserve failed-turn evidence across
2568
+ * the planner's throw-based public contract (D-03).
2569
+ */
2570
+ declare class PlanningTurnError extends Error {
2571
+ readonly reason: PlanningTurnFailureReason;
2572
+ readonly exchanges: ToolExchangeEvidence[];
2573
+ readonly callRecord?: ModelCallRecord;
2574
+ constructor(reason: PlanningTurnFailureReason, message: string, exchanges: ToolExchangeEvidence[], callRecord?: ModelCallRecord);
2575
+ }
2576
+ /**
2577
+ * 436 D-08/D-12: the step's planning-turn limit refused a planner model call. Never a planning
2578
+ * failure; the loop ends the step with its typed limit stop.
2579
+ */
2580
+ declare class PlanningLimitReachedError extends Error {
2581
+ readonly exchanges: ToolExchangeEvidence[];
2582
+ readonly callRecord?: ModelCallRecord;
2583
+ constructor(exchanges: ToolExchangeEvidence[], callRecord?: ModelCallRecord);
2584
+ }
2585
+ declare class LLMPlanner implements Planner {
2586
+ private model;
2587
+ private platform;
2588
+ private logger?;
2589
+ private capabilityNotes?;
2590
+ private accounting;
2591
+ private profile?;
2592
+ private nonActionTools?;
2593
+ constructor(model: LanguageModel, platform: Platform | undefined, logger: LoopScopedLogger | undefined, capabilityNotes: string | undefined, accounting: TokenAccounting, profile?: DeclaredModelProfile, nonActionTools?: NonActionToolMap);
2594
+ private buildSystemTiers;
2595
+ plan(step: string, screenState: ScreenState, context: StepContext, abortSignal?: AbortSignal): Promise<PlanResult>;
2596
+ }
2597
+
2598
+ declare const VerificationResultSchema: z.ZodObject<{
2599
+ conditions: z.ZodArray<z.ZodObject<{
2600
+ quote: z.ZodString;
2601
+ verdict: z.ZodEnum<{
2602
+ unverified: "unverified";
2603
+ met: "met";
2604
+ "not-met": "not-met";
2605
+ inconclusive: "inconclusive";
2606
+ }>;
2607
+ needs: z.ZodEnum<{
2608
+ visual: "visual";
2609
+ none: "none";
2610
+ file: "file";
2611
+ persistence: "persistence";
2612
+ "limit-max": "limit-max";
2613
+ "limit-min": "limit-min";
2614
+ }>;
2615
+ controlRef: z.ZodString;
2616
+ file: z.ZodObject<{
2617
+ name: z.ZodString;
2618
+ match: z.ZodEnum<{
2619
+ exact: "exact";
2620
+ none: "none";
2621
+ lines: "lines";
2622
+ }>;
2623
+ text: z.ZodString;
2624
+ }, z.core.$strip>;
2625
+ citations: z.ZodArray<z.ZodObject<{
2626
+ source: z.ZodEnum<{
2627
+ action: "action";
2628
+ "start-screen": "start-screen";
2629
+ "final-screen": "final-screen";
2630
+ read: "read";
2631
+ carried: "carried";
2632
+ screenshot: "screenshot";
2633
+ }>;
2634
+ quote: z.ZodString;
2635
+ }, z.core.$strip>>;
2636
+ reasoning: z.ZodString;
2637
+ }, z.core.$strip>>;
2638
+ dialogs: z.ZodArray<z.ZodObject<{
2639
+ name: z.ZodString;
2640
+ account: z.ZodEnum<{
2641
+ "bears-on-condition": "bears-on-condition";
2642
+ requested: "requested";
2643
+ unrelated: "unrelated";
2644
+ }>;
2645
+ reasoning: z.ZodString;
2646
+ }, z.core.$strip>>;
2647
+ isAppError: z.ZodBoolean;
2648
+ reasoning: z.ZodString;
2649
+ }, z.core.$strip>;
2650
+ /** Closed judgment schema — a boolean verdict + rationale, never free text (D-06). */
2651
+ declare const AiJudgmentResultSchema: z.ZodObject<{
2652
+ verdict: z.ZodBoolean;
2653
+ rationale: z.ZodString;
2654
+ }, z.core.$strip>;
2655
+ declare class LLMVerifier implements Verifier {
2656
+ private model;
2657
+ private accounting;
2658
+ private profile?;
2659
+ constructor(model: LanguageModel, accounting: TokenAccounting, profile?: DeclaredModelProfile);
2660
+ verify(step: string, before: ScreenState, after: ScreenState, action: Action, screenshot?: Buffer, abortSignal?: AbortSignal, promptNote?: string, bulkCompletionPredicate?: string, authorContext?: VerifierAuthorContext, operationEvidence?: OperationEvidence, record?: VerifierStepRecord): Promise<VerifyResult>;
2661
+ private runVerification;
2662
+ /**
2663
+ * D-05/D-06/D-09 AI judge lane. A SINGLE structured call over the current observation that
2664
+ * returns the verdict + rationale. `opts` carries the requested-but-deferred M3/M4 knobs
2665
+ * (consensus/video/effort) so demand is visible (D-09) — accepted, never honored, never an
2666
+ * error; the loop records them into the AiJudgment evidence block. Empty/overflow model output
2667
+ * degrades to a structured `verdict:false` failure (the verify() never-crash posture) — abort
2668
+ * is the only rethrow.
2669
+ */
2670
+ judgeClaim(claim: string, screen: ScreenState, opts?: Record<string, unknown>): Promise<{
2671
+ verdict: boolean;
2672
+ rationale: string;
2673
+ }>;
2674
+ /**
2675
+ * D-07 AI extraction lane. A SINGLE structured call whose output schema is derived from the
2676
+ * extractAI request's declared field list (each field a string). Returns the extracted field
2677
+ * map; the loop writes it into run-scoped working memory and degrades a `scope:'global'`
2678
+ * request to the same run-scoped write (D-07 — no durable-memory port in M1). Empty/overflow
2679
+ * output degrades to an empty extraction (never a throw) — abort is the only rethrow.
2680
+ */
2681
+ extractData(query: string, screen: ScreenState, opts?: Record<string, unknown>): Promise<Record<string, unknown>>;
2682
+ private runStructured;
2683
+ }
2684
+
2685
+ declare class AssertionEvaluator implements Asserter {
2686
+ evaluate(assertion: AssertionInput, screenState: ScreenState): Promise<AssertionResult>;
2687
+ private evaluateTextPresence;
2688
+ private evaluateElementVisibility;
2689
+ private evaluateUrlMatch;
2690
+ private evaluateElementCount;
2691
+ }
2692
+ declare class LLMAssertionEvaluator implements Asserter {
2693
+ private model;
2694
+ constructor(model: LanguageModel);
2695
+ evaluate(assertion: AssertionInput, screenState: ScreenState): Promise<AssertionResult>;
2696
+ }
2697
+ declare function createAsserter(model?: LanguageModel): Asserter;
2698
+
2699
+ type VariableSource = 'env' | 'inline' | 'suite' | 'cli' | 'capture' | 'hook' | 'step';
2700
+ interface VariableStoreOptions {
2701
+ onWarn?: (message: string) => void;
2702
+ }
2703
+ declare class VariableStore {
2704
+ private storage;
2705
+ private onWarn?;
2706
+ constructor(options?: VariableStoreOptions);
2707
+ set(name: string, value: string, source?: VariableSource): void;
2708
+ get(name: string): string | undefined;
2709
+ has(name: string): boolean;
2710
+ getSource(name: string): VariableSource | undefined;
2711
+ getAll(): Map<string, string>;
2712
+ setAll(vars: Record<string, string>, source?: VariableSource): void;
2713
+ snapshot(): Record<string, {
2714
+ value: string;
2715
+ source: VariableSource;
2716
+ }>;
2717
+ }
2718
+
2719
+ interface InterpolationWithProvenance {
2720
+ text: string;
2721
+ /**
2722
+ * The bindings that actually resolved, one entry per DISTINCT name in first-occurrence
2723
+ * order. Unresolved patterns and the skipped `{{secret:...}}` template contribute
2724
+ * nothing. Values are recorded VERBATIM — see the masking prohibition on
2725
+ * `StepEvidence.interpolatedVariables` (masking here would break the emit fold's
2726
+ * exact-match classification and reintroduce G-381-3).
2727
+ */
2728
+ substitutions: InterpolatedVariable[];
2729
+ }
2730
+ /**
2731
+ * The single replacement implementation (PROC-01 · 381 D-03/D-08). Produces byte-identical
2732
+ * text to the historical `interpolateVariables` while reporting WHICH binding names
2733
+ * produced it, so the name survives the pre-planning interpolation seam into StepEvidence.
2734
+ */
2735
+ declare function interpolateVariablesWithProvenance(template: string, store: VariableStore): InterpolationWithProvenance;
2736
+ declare function interpolateVariables(template: string, store: VariableStore): string;
2737
+ interface UnresolvedTemplate {
2738
+ pattern: string;
2739
+ message: string;
2740
+ }
2741
+ declare function findUnresolvedTemplates(text: string): UnresolvedTemplate[];
2742
+ declare function findBareVariables(text: string): string[];
2743
+
2744
+ declare class ExplicitExtractor implements VariableExtractor {
2745
+ extract(input: ExtractorInput, screenState: ScreenState): Promise<CaptureResult>;
2746
+ private extractByRegex;
2747
+ private extractBySelector;
2748
+ }
2749
+ declare function createExtractor(model?: LanguageModel, method?: string): VariableExtractor;
2750
+
2751
+ declare const ExtractionResultSchema: z.ZodObject<{
2752
+ value: z.ZodString;
2753
+ reasoning: z.ZodString;
2754
+ }, z.core.$strip>;
2755
+ declare class LLMVariableExtractor implements VariableExtractor {
2756
+ private model;
2757
+ constructor(model: LanguageModel);
2758
+ extract(input: ExtractorInput, screenState: ScreenState): Promise<CaptureResult>;
2759
+ }
2760
+
2761
+ type PromptPlatform = 'web' | 'android' | 'ios';
2762
+ /**
2763
+ * v1 token estimate (chars/4). No semantic change (D-04); the TokenAccounting port
2764
+ * lands in Phase 355. Exported here because the prompts module is the token estimate's
2765
+ * sole runner-side consumer.
2766
+ */
2767
+ declare function estimateTokens(text: string): number;
2768
+ declare function buildSystemPrompt(platform?: PromptPlatform): string;
2769
+ /**
2770
+ * CONTEXT tier (D-04): the app-under-test / suite / test framing. Byte-stable per run,
2771
+ * so it sits inside the cached prefix and carries the single Anthropic `cache_control`
2772
+ * ephemeral breakpoint (D-05) — everything at or above this message (stable + context)
2773
+ * is cached once per run at zero incremental cost. A non-Anthropic provider never parses
2774
+ * the `anthropic` namespace, so `providerOptions` is a transparent no-op there.
2775
+ */
2776
+ declare function buildContextTier(context: StepContext): SystemModelMessage;
2777
+ /**
2778
+ * Render the knowledge brief (the volatile-tier ingredient, D-12) framed as hypotheses
2779
+ * to weigh against the live observation — never as commands (spec §5, L3). Returns ''
2780
+ * when the brief slot is unbound/empty so the volatile tier omits the section entirely
2781
+ * (no 'Knowledge brief' header, no dangling blank line).
2782
+ */
2783
+ declare function renderBriefAsHypotheses(brief?: string): string;
2784
+ /**
2785
+ * VOLATILE tier (D-04/D-07): the knowledge brief (empty until M2 binds a
2786
+ * KnowledgeHintReader impl, D-12) plus a DATE-GRANULARITY timestamp. Date precision only
2787
+ * — minute/second precision would bust the KV cache (D-07). Carries NO cache breakpoint;
2788
+ * it sits after the breakpoint that lives on the context tier.
2789
+ */
2790
+ declare function buildVolatileTier(context: StepContext): SystemModelMessage;
2791
+ /**
2792
+ * Per-turn content composer (CTXB-01/CTXB-02 — D-08/D-09/D-12). Decomposes the
2793
+ * per-turn state into a small ordered set of turn messages so 354's degrade() can pin
2794
+ * and proportionally truncate at message granularity rather than shredding one blob.
2795
+ * Order: re-run context → previous steps → sub-action history → failure context →
2796
+ * working memory → latest observation → current instruction (FINAL). Empty slots are
2797
+ * omitted entirely — no blank/degenerate message is pushed (D-12).
2798
+ *
2799
+ * PIN CONTRACT (D-08): the last two messages are the pinned pair — the latest
2800
+ * OBSERVATION (second-to-last) and the CURRENT INSTRUCTION (last, verbatim). The caller
2801
+ * (Plan 05) protects exactly these two via degrade's `pin`; every earlier message is
2802
+ * trim-eligible. All per-turn state lives here and NEVER in the cached system tiers.
2803
+ */
2804
+ declare function composeTurnMessages(context: StepContext, screenState: ScreenState): ModelMessage[];
2805
+ declare function formatAction(action: Action): string;
2806
+ /**
2807
+ * VERIFIER stable tier (CTXB-01 / D-11): the byte-stable verdict-identity block, the verifier's own
2808
+ * cached prefix, identical on every verification call. 439 (D-01, D-05, D-06, D-11, D-18) states
2809
+ * the per-condition contract here: split the step into quoted conditions, judge each from cited
2810
+ * current evidence, and leave the pass to the runner. Arg-less: no per-call inputs; the
2811
+ * screenshot-analysis paragraph rides only a per-call turn that has an image (D-19).
2812
+ */
2813
+ declare function buildVerifierStableTier(): string;
2814
+ /**
2815
+ * VERIFIER per-call turn content (D-11). Only the per-verification-call bytes: the step
2816
+ * goal, the enriched action description, and the before/after screen states (+ the
2817
+ * optional screenshot note and prompt note). The byte-stable verdict-identity/approach
2818
+ * prose now lives in `buildVerifierStableTier()` (the verifier's own cached prefix), so
2819
+ * it is no longer re-emitted per call.
2820
+ */
2821
+ declare function buildVerificationPrompt(step: string, beforeState: ScreenState, afterState: ScreenState, action: Action, hasScreenshot?: boolean, promptNote?: string, bulkCompletionPredicate?: string, authorContext?: VerifierAuthorContext, operationEvidence?: OperationEvidence, record?: VerifierStepRecord): string;
2822
+ declare function buildAssertionPrompt(assertion: AssertionInput, screenState: ScreenState): string;
2823
+ declare function buildExtractionPrompt(input: ExtractorInput, screenState: ScreenState): string;
2824
+
2825
+ declare const CHARS_PER_TOKEN_ESTIMATE = 4;
2826
+ declare const OBSERVATION_LINE_PREFIX = "Observation: ";
2827
+ type OriginStampedEntry = IndexEntry & {
2828
+ origin?: 'app-observed' | 'user';
2829
+ originClass?: 'agent-observed' | 'agent-verified' | 'import' | 'user';
2830
+ };
2831
+ /**
2832
+ * Cap learned summaries using JavaScript String.length (UTF-16 code units), the
2833
+ * BRIF-01 length contract. The ellipsis is part of the 140-code-unit maximum.
2834
+ */
2835
+ declare function renderCautionSummary(summary: string): string;
2836
+ declare function tierForEntry(entry: OriginStampedEntry): string;
2837
+ /** Deterministic provisional cost of the exact text handed to the planner. */
2838
+ declare function estimateRenderedServingCost(text: string): number;
2839
+ interface RenderedCautionLine {
2840
+ summary: string;
2841
+ cost: number;
2842
+ }
2843
+ /** Render and cost Channel 3 through one projection so framing can never be undercharged. */
2844
+ declare function renderCautionLine(entry: IndexEntry): RenderedCautionLine;
2845
+ declare function renderBriefLine(entry: IndexEntry): {
2846
+ line: string;
2847
+ briefLine: BriefLine;
2848
+ cost: number;
2849
+ };
2850
+ declare function renderObservationLine(line: BriefLine): string;
2851
+ declare function assembleBriefTiers(policyText: string, userFactSummaries: readonly string[], renderedLines: readonly {
2852
+ line: string;
2853
+ tier: string;
2854
+ }[]): {
2855
+ L0: string;
2856
+ L1: string;
2857
+ L2: string;
2858
+ };
2859
+
2860
+ declare function makeEmptyServedBrief(scope: ScopeKey, asOfBuild: number): ServedBrief;
2861
+ declare function foldCacheStatus(subActionCacheFlags: boolean[]): StepEvidence['cacheStatus'];
2862
+ declare function collapseAnnotation(action: Action, execResult: ActionResult, viewport: Viewport): ActionAnnotation;
2863
+ interface RecordSubActionInput {
2864
+ /** The planner's reasoning/plan string for this sub-action. */
2865
+ plan: string;
2866
+ plannedAction: Action$1;
2867
+ /**
2868
+ * The FULL observed ScreenState the sub-action was planned against (D-15). Pitfall 3:
2869
+ * v1 stored only the tree string — the forbidden shape. elements + refMap ride along.
2870
+ */
2871
+ stateBefore: ScreenState;
2872
+ stateAfter?: ScreenState;
2873
+ /**
2874
+ * The execution result as the schema ActionResult (screenshot may be present here as
2875
+ * bytes; it is structurally excluded by toRecordedActionResult before storage — the
2876
+ * type-level screenshot-never guarantee, T-353-06). annotation is produced via
2877
+ * collapseAnnotation.
2878
+ */
2879
+ result: ActionResult$1;
2880
+ /** v1 per-sub-action cache flag; folded per step by foldCacheStatus at finalize. */
2881
+ cached?: boolean;
2882
+ /**
2883
+ * The §A8 grounding evidence for this sub-action (GROUND-02): the resolved descriptor
2884
+ * plus the per-rung attempt trail (turn gate on the live path, replay resolution on the
2885
+ * cache path, re-ground heal entries on the D-10 path). Present only when the action
2886
+ * carried an element field; the loop redacts the descriptor BEFORE passing it here so a
2887
+ * pre-redaction descriptor never reaches evidence (T-357-02a, the redact-before-record
2888
+ * ordering). Typed via the schema DTO — no runner-local mirror (353 evidence-by-construction).
2889
+ */
2890
+ grounding?: SubActionEvidence['grounding'];
2891
+ /**
2892
+ * The §A8 decision-screenshot ArtifactRef (CASS-02): the identity of the exact final image
2893
+ * bytes the planner saw for this live decision, minted by the run-scoped artifact store.
2894
+ * Absent on cache-served, text-only, unbound-store, or failed-store paths — never fabricated.
2895
+ */
2896
+ decisionScreenshotRef?: SubActionEvidence['decisionScreenshotRef'];
2897
+ /**
2898
+ * Auxiliary §A8 ArtifactRefs already reserved by the schema DTO. Copied verbatim (order and
2899
+ * identity preserved) so the recorder never drops artifact references present on the input.
2900
+ */
2901
+ artifactRefs?: SubActionEvidence['artifactRefs'];
2902
+ /**
2903
+ * Ordered exchanges from this live planning decision (D-01). The loop masks every
2904
+ * string leaf BEFORE passing them here (T-360-01); the recorder never sees raw values.
2905
+ * Present, possibly empty, for live turns and absent for cache replay.
2906
+ */
2907
+ exchanges?: ToolExchangeEvidence[];
2908
+ /**
2909
+ * Final-response enrichment for this live decision (D-05/D-08). The loop passes it
2910
+ * conditionally; structural absence means no live call record was available.
2911
+ */
2912
+ callRecord?: ModelCallRecord;
2913
+ /** 436 D-22: what the action produced outside the page, already masked by the loop. */
2914
+ operationEvidence?: SubActionEvidence['operationEvidence'];
2915
+ healedDispatch?: SubActionEvidence['healedDispatch'];
2916
+ }
2917
+ declare class EvidenceRecorder {
2918
+ private readonly subActions;
2919
+ private readonly cacheFlags;
2920
+ private servedBrief;
2921
+ /**
2922
+ * KREV-02 · D-13: the serve-time retrieval trace, recorded BESIDE `servedBrief` and never
2923
+ * inside it. `ServedBrief` feeds `computeBriefDigest`, so a trace channel within it would have
2924
+ * rewritten every cassette and acceptance digest in the tree; a sibling field leaves those
2925
+ * bytes untouched, which `evidence-trace.test.ts` proves by digest equality rather than by
2926
+ * asserting that nobody edited the brief.
2927
+ *
2928
+ * Channel 1 is REPLACED on each record (a step replans against the SAME frozen brief, so
2929
+ * appending would duplicate its attributions), while Channel 3 APPENDS — exactly mirroring how
2930
+ * `servedBrief.channel1` and `servedBrief.channel3` themselves behave.
2931
+ */
2932
+ private channel1Trace?;
2933
+ private readonly channel3TraceLines;
2934
+ private readonly channel3Truncated;
2935
+ private selfHeal?;
2936
+ private workingMemory?;
2937
+ private planningFailure?;
2938
+ private reportedValue?;
2939
+ private termination?;
2940
+ private limitStop?;
2941
+ private conditionVerdicts?;
2942
+ private limitUsage?;
2943
+ private operationCapture?;
2944
+ private aiJudgment?;
2945
+ private assertOutcome?;
2946
+ private verdictSource?;
2947
+ private planSource?;
2948
+ private cacheMiss?;
2949
+ /** D-06 run-declared planner context, copied onto every finalized step when bound. */
2950
+ private readonly modelProfile?;
2951
+ /** D-07 verifier identity copied beside every finalized verdict when bound. */
2952
+ private readonly verifierIdentity?;
2953
+ /** D-16 once-per-member bound-surface digest copied onto every finalized step. */
2954
+ private readonly toolSchemaDigest?;
2955
+ /**
2956
+ * 407 D-13: the HOST's retrieval rung at serve time, handed in by the composition root and
2957
+ * copied onto every finalized step. It sits BESIDE `servedBrief` for the same reason
2958
+ * `retrievalTrace` does — `computeBriefDigest` keys cassettes and acceptance evidence on the
2959
+ * brief's bytes, so a rung channel inside the brief would invalidate every brief ever served.
2960
+ * The recorder never resolves it: a run's rung is a fact about serve time, not a live probe.
2961
+ */
2962
+ private readonly retrievalRung?;
2963
+ /**
2964
+ * D-02 single sub-action chokepoint (Plan 361-08). When bound, invoked at the END of
2965
+ * every {@link recordSubAction} append with the recorded evidence and its index — so all
2966
+ * five loop call sites emit `subActionRecorded` through ONE seam, never per-call-site.
2967
+ * Optional-chained: unbound ⇒ the recorder is byte-identical to today (never throws, D-05).
2968
+ */
2969
+ private readonly onSubActionRecorded?;
2970
+ constructor(options?: {
2971
+ servedBrief?: ServedBrief;
2972
+ scope?: ScopeKey;
2973
+ asOfBuild?: number;
2974
+ onSubActionRecorded?: (evidence: SubActionEvidence, index: number) => void;
2975
+ modelProfile?: DeclaredModelProfile;
2976
+ verifierIdentity?: ModelIdentity;
2977
+ toolSchemaDigest?: string;
2978
+ retrievalRung?: RecordedRetrievalRung;
2979
+ });
2980
+ private replaceServedBrief;
2981
+ private losslessProof;
2982
+ /**
2983
+ * Upgrade this production recorder from the proof-less historical shape to a live lossless
2984
+ * proof. The model-visible channel bytes stay unchanged; only the actual serving caps and
2985
+ * empty chronological journals are introduced.
2986
+ */
2987
+ beginLosslessServedBrief(input: {
2988
+ recordCap: number;
2989
+ combinedBudget: number;
2990
+ }): void;
2991
+ /**
2992
+ * Append observations drained from the production admission authority. Ordinals must remain
2993
+ * contiguous across batches; validation occurs before mutation so a bad batch appends nothing.
2994
+ */
2995
+ recordServedAdmissionEvents(events: readonly ServedAdmissionEvent[]): void;
2996
+ /** Append one exact production prompt snapshot and re-address the complete served proof. */
2997
+ recordServedPromptTurn(turn: ServedPromptTurn): void;
2998
+ /** The number of sub-actions recorded so far; the next recordSubAction returns this index. */
2999
+ get subActionCount(): number;
3000
+ /** Append a §A8 SubActionEvidence. Returns its index (for attachStateAfter). */
3001
+ recordSubAction(input: RecordSubActionInput): number;
3002
+ /**
3003
+ * Snapshot the run-scoped working-memory set served to this step's planning turns
3004
+ * (WM-01, §12.15). The loop passes the values ALREADY masked (the loop's `recordMask`, Pitfall
3005
+ * 7 boundary 3) — recorded BESIDE the brief, never inside `servedBrief`.
3006
+ */
3007
+ recordWorkingMemory(entries: WorkingMemoryEntry[]): void;
3008
+ /**
3009
+ * Record the real deterministic Channel-1 brief served to this step. The recording type is the
3010
+ * schema-owned ServedBrief; the digest is always recomputed over the populated channels.
3011
+ * makeEmptyServedBrief remains exclusively the no-reader constructor fallback.
3012
+ */
3013
+ recordServedBrief(input: {
3014
+ scope: ScopeKey;
3015
+ asOfBuild: number;
3016
+ budget: number;
3017
+ block: BriefBlock;
3018
+ /**
3019
+ * D-16's last hop, made structural. The trace is a REQUIRED parameter, so the only way to
3020
+ * persist a served brief without one is to change this signature — which is a compile error
3021
+ * at every call site rather than a silently trace-less run. `makeEmptyServedBrief` stays the
3022
+ * one no-reader constructor fallback and records nothing here, so an absent trace on evidence
3023
+ * means "no reader was bound", never "a reader served and the trace was dropped" (D-15).
3024
+ */
3025
+ trace: ServedRetrievalTraceV1;
3026
+ }): void;
3027
+ /**
3028
+ * Append the exact Channel-3 CautionLines injected into a live replan turn. Refused records
3029
+ * never enter this method; the reserved reader overflow line does because its summary was
3030
+ * actually rendered. Digest recomputation keeps ServedBrief content-addressed after mutation.
3031
+ *
3032
+ * WR-10 (387-13): the LEDGER accumulates on every call, including the zero-injection one. A
3033
+ * turn that injected nothing is not a turn with nothing to explain — it is exactly the turn
3034
+ * "why didn't it mention X" is asked about, and {@link foldRetrievalTrace} carries a dedicated
3035
+ * arm for "no served lines, non-empty truncation ledger" that this method previously made
3036
+ * unreachable by returning before accumulating.
3037
+ *
3038
+ * The surviving `cautions.length === 0` return is a COST guard, not a correctness one, and the
3039
+ * distinction is measured rather than asserted: deleting it outright reddens nothing, because
3040
+ * appending an empty array and recomputing a content-addressed digest reproduces the same
3041
+ * bytes. It stays only to skip a whole-brief rehash on every all-cut turn. D-13's byte-stability
3042
+ * on this path is protected by what is NOT written here — cut candidates are never spoofed into
3043
+ * `channel3` as served lines — and the regression cases pin that outcome, not this branch.
3044
+ */
3045
+ recordServedCautions(cautions: readonly CautionLine[], trace: ServedRetrievalTraceV1): void;
3046
+ /** Append successful Channel-2 direct reads and re-address the complete served brief. */
3047
+ recordServedReadTraces(traces: readonly ReadTrace[]): void;
3048
+ /**
3049
+ * Record the structurally failed planning turn (D-03). Exchanges and message arrive
3050
+ * ALREADY masked by the loop; the recorder remains mask-agnostic.
3051
+ */
3052
+ recordPlanningFailure(evidence: PlanningFailureEvidence): void;
3053
+ /**
3054
+ * Record the D-03 `common_reportValue` answer block. `value` arrives already masked. Surfaced
3055
+ * on StepEvidence.reportedValue so the verdict layer (and Phase 361's handoff) carries the
3056
+ * retrieved answer first-class. reportValue is non-terminal (D-02) — this only records.
3057
+ */
3058
+ recordReportedValue(evidence: ReportedValueEvidence): void;
3059
+ /**
3060
+ * Record the D-04 infeasible termination block — present only when a turn declared the step
3061
+ * infeasible via the `failureKind`/`failureReason` envelope (machine-distinguishable from a
3062
+ * plain `stepFailed`). Sorted keys `["kind","reason"]` are fixed by the DTO.
3063
+ */
3064
+ recordTermination(evidence: TerminationEvidence): void;
3065
+ /**
3066
+ * Record the typed limit stop (436 D-12/D-13). Its text fields arrive already masked; it is
3067
+ * never a planning failure, a termination or a verdict source.
3068
+ */
3069
+ recordLimitStop(evidence: LimitStopEvidence): void;
3070
+ /**
3071
+ * Record the per-condition verdicts (439 D-07). Text fields arrive masked and bounded; a later
3072
+ * judgment of the same step replaces an earlier one.
3073
+ */
3074
+ recordConditionVerdicts(evidence: ConditionVerdictsEvidence): void;
3075
+ /** Record whether the step's platform could capture each operation (436 D-23). */
3076
+ recordOperationCapture(availability: OperationCaptureAvailability): void;
3077
+ /** Record the step's configured limits and usage (436 D-11); finish() calls it on every path. */
3078
+ recordLimitUsage(usage: StepLimitUsage): void;
3079
+ /**
3080
+ * Record the D-05/D-06/D-07/D-09 AI judge block (`assertAI`/`extractAI`): verdict, rationale
3081
+ * (masked), the requested-but-deferred M3/M4 knobs, and the extractAI global-scope degradation
3082
+ * marker. One judgment per attempt (single-model law).
3083
+ */
3084
+ recordAiJudgment(evidence: AiJudgmentEvidence): void;
3085
+ /**
3086
+ * Record the D-10/D-11 structured `common_assertValue` outcome block: the comparator's
3087
+ * deterministic result with `expected`/`actual` already masked. A passing outcome drives the
3088
+ * verifier short-circuit (Plan 10 seam), recorded via {@link recordVerdictSource}.
3089
+ */
3090
+ recordAssertOutcome(evidence: AssertValueEvidence): void;
3091
+ /**
3092
+ * Record which path produced this step's final verification verdict (D-11): the deterministic
3093
+ * assertValue comparator (`'assert-value'`) or the verifier's own model call
3094
+ * (`'verifier-model'`), which also decides every AI-judged conclusion (assertAI or extractAI;
3095
+ * quick 261004-lsa). Set with the final verdict, so a verifier rejection records nothing.
3096
+ */
3097
+ recordVerdictSource(source: VerdictSource): void;
3098
+ /**
3099
+ * Record the typed step plan source (PROC-02 · 381 D-12). `procedure-replay` carries the
3100
+ * guard fold (checksPassed/total + the first miss when the fold broke off); `procedure-hint`
3101
+ * marks a served-without-replay exposure. Recorded BESIDE `cacheStatus` — structurally
3102
+ * distinct from the cached flag so eval/cassette consumers reading cached semantics are
3103
+ * never polluted (Pitfall 4). Last write wins: the loop updates the fold as guard checks
3104
+ * accumulate, and the final state is what finalize emits.
3105
+ */
3106
+ recordPlanSource(source: StepPlanSource): void;
3107
+ /**
3108
+ * Record why a cache key hit did not replay (438 D-11), before the loop invalidates the entry.
3109
+ * The first reason in the step is kept.
3110
+ */
3111
+ recordCacheMiss(miss: {
3112
+ reason: CacheMissReason;
3113
+ }): void;
3114
+ /**
3115
+ * Attach the D-15 stateAfter at the spec-locked boundaries (assert sub-actions, the
3116
+ * verifier boundary, the self-heal boundary) by reuse of an observation the loop
3117
+ * already takes — no extra capture.
3118
+ */
3119
+ attachStateAfter(index: number, state: ScreenState): void;
3120
+ /** Record the §A8 contrastive self-heal pair (D-12): the failed decision and the
3121
+ * recovering decision at the replan-with-failure-context boundary. */
3122
+ recordSelfHeal(failedDecision: Decision, recoveringDecision: Decision): void;
3123
+ /**
3124
+ * Fold the per-channel serve-time attributions into the one persisted trace (D-13/D-14).
3125
+ *
3126
+ * Every value here was computed by the reader at the moment of serving and is copied through
3127
+ * unchanged — no score is re-derived and no rank is recomputed at persistence time. The runner
3128
+ * writes; the reader computed.
3129
+ *
3130
+ * Absent when nothing was served through a bound reader, which is D-15's honest no-trace arm:
3131
+ * an absent trace says "no reader served this step", not "a trace was lost".
3132
+ */
3133
+ private foldRetrievalTrace;
3134
+ /** Finalize the accumulated evidence into a §A8 StepEvidence. cacheStatus is the
3135
+ * per-step fold; servedBrief is always present (empty when no reader bound).
3136
+ *
3137
+ * `options.nonCacheable` threads the D-08 bypass signal (Pitfall 7): when a step was
3138
+ * classified NonCacheableStepInput the loop never touched the cache, so cacheStatus is
3139
+ * `'bypass'` DIRECTLY — orthogonal to the boolean hit/miss fold (a cached sub-action's
3140
+ * flag must not fold to `'hit'` for a bypassed step). */
3141
+ finalize(verdict: VerifierVerdict, stepIndex: number, stepText: string, options?: {
3142
+ nonCacheable?: boolean;
3143
+ /**
3144
+ * The step's `{{env:NAME}}` provenance pairs (PROC-01 · 381 D-03/D-08), stamped only
3145
+ * when non-empty so the absent-by-default shape is preserved. The recorder is
3146
+ * MASK-AGNOSTIC by contract: it records exactly what its caller passes and masks
3147
+ * nothing itself. These pairs in particular must NOT be masked anywhere — see the
3148
+ * masking prohibition on `StepEvidence.interpolatedVariables`.
3149
+ */
3150
+ interpolatedVariables?: readonly InterpolatedVariable[];
3151
+ }): StepEvidence;
3152
+ }
3153
+ declare function projectLegacyTrace(evidence: StepEvidence): SubActionTrace[];
3154
+
3155
+ interface GroundingStrategy {
3156
+ reground(locator: GroundedLocator, elements: readonly ElementInfo[]): StrictMatchResult;
3157
+ }
3158
+ declare const deterministicDescriptorMatcher: GroundingStrategy;
3159
+
3160
+ /**
3161
+ * Stable, non-secret, project-level env-digest salt (Open Question 2 resolution).
3162
+ * It MUST be a fixed versioned constant — a per-run salt would silently change the
3163
+ * env digest every run and disable ALL env-dependent cache hits. Never derive this
3164
+ * per-run and never fold a secret into it.
3165
+ */
3166
+ declare const CACHE_ENV_DIGEST_SALT = "agent-qa/cache/env-digest/v1";
3167
+ interface BuildCacheableStepInputArgs {
3168
+ /** The declared-secret mask (441 D-06); the identity when unbound. */
3169
+ mask?: MaskFn;
3170
+ platform: Platform;
3171
+ suiteConfigDigest: string;
3172
+ testConfigDigest: string;
3173
+ stepText: string;
3174
+ stepIndex: number;
3175
+ testIndex: number;
3176
+ /** Value-free redacted env shapes only — raw env values never reach the digest. */
3177
+ envVars: readonly RedactedEnvVar[];
3178
+ /** Stable project salt; defaults to CACHE_ENV_DIGEST_SALT. Never per-run. */
3179
+ envSalt?: string;
3180
+ screenIdentityHash: string;
3181
+ /** Accepted for caller ergonomics but intentionally never a key component (D-14). */
3182
+ branch?: string;
3183
+ }
3184
+ /**
3185
+ * Assemble the schema kernel's 8-field CacheableStepInput. `mask()` is applied to
3186
+ * stepText BEFORE it enters the tuple (the MASK-FUT-01 floor, D-05); the env digest
3187
+ * folds only the value-free redacted shapes under the stable salt. The caller passes
3188
+ * the result to computeCacheKey to obtain the 16-hex CacheKey / filename (D-06).
3189
+ */
3190
+ declare function buildCacheableStepInput(args: BuildCacheableStepInputArgs): CacheableStepInput;
3191
+
3192
+ interface CacheableClassification {
3193
+ cacheable: true;
3194
+ /** Value-free config-env shapes this step depends on (folded into the digest). */
3195
+ envVars: RedactedEnvVar[];
3196
+ }
3197
+ interface ClassifyStepInput {
3198
+ stepText: string;
3199
+ /** Variable names this step sets — the onSetVariable / setVariable provenance. */
3200
+ envSets?: readonly string[];
3201
+ /** Config-env values this cacheable step reads (value-free shapes). */
3202
+ configEnv?: readonly RedactedEnvVar[];
3203
+ }
3204
+ /**
3205
+ * Classify a step by resolution provenance (D-07). Env/secret read-or-set steps
3206
+ * return NonCacheableStepInput; a config-env-dependent step returns a
3207
+ * CacheableClassification carrying the value-free RedactedEnvVar shapes.
3208
+ */
3209
+ declare function classifyStep(input: ClassifyStepInput): CacheableClassification | NonCacheableStepInput;
3210
+
3211
+ /**
3212
+ * Thrown by `subscribe(fromSeq, …)` when the requested range is neither in the in-memory
3213
+ * window nor recoverable from the (possibly degraded) event log (D-07 — report incomplete,
3214
+ * never silently serve a truncated stream). `earliestAvailableSeq` is the lowest seq the bus
3215
+ * can still replay (the SSE `Last-Event-ID` re-subscribe floor in M5).
3216
+ */
3217
+ declare class ReplayIncompleteError extends Error {
3218
+ readonly earliestAvailableSeq: number;
3219
+ constructor(requestedSeq: number, earliestAvailableSeq: number);
3220
+ }
3221
+ /** A live/replay subscriber. Synchronous — replay-then-live per-subscriber order is total. */
3222
+ type RunEventHandler = (event: RunEventEnvelope) => void;
3223
+ /**
3224
+ * The bus surface. `publish` stamps `seq`/`ts` and returns the sealed envelope (the shell reads
3225
+ * `.seq` for `runFinished.finalSeq`); `subscribe` replays then goes live; `close` stops the
3226
+ * heartbeat (publish-after-close is a programming error).
3227
+ */
3228
+ interface RunEventBus {
3229
+ publish(type: RunEventType, payload: RunEventPayload | Record<string, unknown>): RunEventEnvelope;
3230
+ subscribe(fromSeq: number | undefined, handler: RunEventHandler): () => void;
3231
+ readonly currentSeq: number;
3232
+ close(): void;
3233
+ }
3234
+ interface CreateRunEventBusOptions {
3235
+ runId: string;
3236
+ eventLog?: EventLogPort;
3237
+ timer?: RunTimerPort;
3238
+ heartbeatMs?: number;
3239
+ windowMaxEvents?: number;
3240
+ windowMaxBytes?: number;
3241
+ }
3242
+ declare function createRunEventBus(opts: CreateRunEventBusOptions): RunEventBus;
3243
+
3244
+ /** How long a handle's stream keeps running after its LAST viewer detaches (403 D-07). */
3245
+ declare const LIVE_STREAM_STOP_GRACE_MS = 2000;
3246
+ /** How long the hub waits for every subscriber to ack a frame before acking on their behalf. */
3247
+ declare const LIVE_STREAM_ACK_BOUND_MS = 250;
3248
+ /**
3249
+ * Default timer: the `bus.ts` idiom — unref'd so a pending grace never holds the process open.
3250
+ * Exported so the snapshot producer shares it rather than copying it (428 D-11).
3251
+ */
3252
+ declare function defaultLiveStreamTimer(): RunTimerPort;
3253
+ interface CreateLiveStreamHubOptions {
3254
+ timer?: RunTimerPort;
3255
+ graceMs?: number;
3256
+ ackBoundMs?: number;
3257
+ /**
3258
+ * Facts about the RUN's producer. The shell wires this to the run's durable event log, so what
3259
+ * lands here is replayed to every later reader of the run.
3260
+ */
3261
+ onWarning?: (code: string, message: string) => void;
3262
+ /**
3263
+ * Where a SUBSCRIBER's fault is reported (403 WR-06). Deliberately a DIFFERENT sink than
3264
+ * `onWarning`, and the difference is the whole fix: a viewer that throws — or whose socket died
3265
+ * between a `readyState` check and the write — is a fact about THAT VIEWER, and the run's
3266
+ * permanent record must not depend on who happened to be watching (LIVE-03). Absent by default,
3267
+ * so a host that does not care reports nothing anywhere.
3268
+ */
3269
+ onSubscriberFault?: (code: string, message: string) => void;
3270
+ }
3271
+ /**
3272
+ * The run's frame fan-out. `register` is the PRODUCER side (the member executor hands its adapter's
3273
+ * capability in, and hands `undefined` in when the member finishes); `subscribe`/`capabilityOf` are
3274
+ * the CONSUMER side the `RunSession` exposes.
3275
+ */
3276
+ interface LiveStreamHub {
3277
+ /**
3278
+ * `fallback` is the labelled snapshot producer this handle switches to when `capability` fails
3279
+ * to start (428 D-12). The failure is still recorded; `capabilityOf` keeps the declared kind.
3280
+ */
3281
+ register(handle: number, capability: LiveStreamCapability | undefined, fallback?: LiveStreamCapability): void;
3282
+ subscribe(handle: number, onFrame: (frame: LiveFrame) => void, onEnd?: (reason: LiveFrameEndReason) => void): LiveFrameSubscription | undefined;
3283
+ capabilityOf(handle: number): LiveStreamDeclaration;
3284
+ close(): Promise<void>;
3285
+ }
3286
+ declare function createLiveStreamHub(opts?: CreateLiveStreamHubOptions): LiveStreamHub;
3287
+
3288
+ /**
3289
+ * The three live-preview cadences, side by side: a snapshot every second (this producer), a
3290
+ * keepalive frame after two quiet seconds on a static page (the web adapter's screencast, 428-06),
3291
+ * and the viewer's ~5s freshness window (428-14) that both of them stay inside.
3292
+ */
3293
+ declare const LIVE_SNAPSHOT_INTERVAL_MS = 1000;
3294
+ declare const LIVE_KEEPALIVE_QUIET_MS = 2000;
3295
+ declare function createSnapshotLiveStream(input: {
3296
+ capture: () => Promise<Uint8Array | undefined>;
3297
+ timer?: RunTimerPort;
3298
+ intervalMs?: number;
3299
+ }): LiveStreamCapability;
3300
+ /** What a run or an editor session registers with its hub: a stream, and what replaces it on a failed start. */
3301
+ type LiveStreamRegistration = {
3302
+ capability: LiveStreamCapability;
3303
+ fallback?: LiveStreamCapability;
3304
+ };
3305
+ /**
3306
+ * THE ONE SELECTION RULE (428 D-11/D-12), used by both run paths and the editor.
3307
+ *
3308
+ * The adapter's own `liveStream` always wins. `screenshot()` is wrapped in the snapshot producer
3309
+ * only when the adapter's own capability is not already a snapshot: as the fallback behind a
3310
+ * continuous stream that may fail to start, or as the whole preview when there is no continuous
3311
+ * stream. An adapter whose own stream IS a snapshot (mobile) gets no fallback, which is what keeps
3312
+ * a mobile run off its context-switching `screenshot()`.
3313
+ */
3314
+ declare function liveStreamFor(adapter: Pick<PlatformAdapter, 'liveStream' | 'screenshot'> | undefined, options?: {
3315
+ timer?: RunTimerPort;
3316
+ }): LiveStreamRegistration | undefined;
3317
+ /**
3318
+ * Width and height from a PNG IHDR or a JPEG SOF header, so a snapshot frame has a size before
3319
+ * the viewer decodes it. Anything it cannot read — another format, a truncated header — answers
3320
+ * 0 by 0, and every read is bounded by the byte length.
3321
+ */
3322
+ declare function imageSizeOf(bytes: Uint8Array): {
3323
+ width: number;
3324
+ height: number;
3325
+ };
3326
+
3327
+ /** The explicit handle carries its own capability; no ambient current-step registry exists. */
3328
+ declare function withStepModelCall<T>(model: LanguageModel, call: (model: LanguageModel) => Promise<T>): Promise<{
3329
+ result: T;
3330
+ scoped: boolean;
3331
+ respondingModelId?: string;
3332
+ }>;
3333
+ declare function createStepModelScope(options: StepModelScopeOptions): StepModelScope;
3334
+
3335
+ declare function parseEnvFile(content: string): Record<string, string>;
3336
+ declare function serializeEnvFile(vars: Record<string, string>): string;
3337
+
3338
+ declare const AUTH_STATE_HOOK_JSON_ENV = "AGENT_QA_AUTH_STATE_JSON";
3339
+ declare const AUTH_STATE_HOOK_STORAGE_STATE_PATH_ENV = "AGENT_QA_AUTH_STATE_STORAGE_STATE_PATH";
3340
+ declare const AUTH_STATE_HOOK_WORKSPACE_DIR = ".agent-qa-auth-state";
3341
+ declare const AUTH_STATE_HOOK_STORAGE_STATE_FILENAME = "storage-state.json";
3342
+ declare const AUTH_STATE_HOOK_CONTAINER_STORAGE_STATE_PATH = "/workspace/.agent-qa-auth-state/storage-state.json";
3343
+ declare function isReservedAuthStateHookEnvKey(key: string): boolean;
3344
+ declare function stripReservedAuthStateHookEnv(variables: Record<string, string>): Record<string, string>;
3345
+ declare function buildAuthStateHookEnv(authState: RuntimeAuthStateConfig | undefined, storageStatePath?: string): Record<string, string>;
3346
+
3347
+ interface SecretTemplate {
3348
+ pattern: string;
3349
+ name: string;
3350
+ }
3351
+ interface SecretFileMetadata {
3352
+ count: number;
3353
+ }
3354
+ declare class SecretConfigError extends Error {
3355
+ constructor(message: string);
3356
+ }
3357
+ declare class MissingSecretError extends Error {
3358
+ constructor(name: string);
3359
+ }
3360
+ declare class SecretStore implements SecretResolver {
3361
+ private storage;
3362
+ constructor(secrets?: Record<string, string> | ReadonlyArray<readonly [string, string]>);
3363
+ static empty(): SecretStore;
3364
+ static fromEnvContent(content: string): SecretStore;
3365
+ private set;
3366
+ get(name: string): string | undefined;
3367
+ require(name: string): string;
3368
+ has(name: string): boolean;
3369
+ count(): number;
3370
+ metadata(): SecretFileMetadata;
3371
+ forEachSecret(callback: (name: string, value: string) => void): void;
3372
+ }
3373
+ declare function findSecretTemplates(text: string): SecretTemplate[];
3374
+ declare function interpolateSecretTemplates(template: string, store: SecretStore): string;
3375
+ declare function resolveSecretTemplatesInValue<T>(value: T, store?: SecretResolver): T;
3376
+ declare function redactSecretValue<T>(value: T, redactor?: SecretRedactorPort): T;
3377
+ /**
3378
+ * The cached-plan invalidation predicate (loop cache-poisoning guard): returns true
3379
+ * when a value contains a redacted `[secret:*]` marker. Moved from core loop.ts.
3380
+ */
3381
+ declare function containsRedactedSecretMarker(value: unknown): boolean;
3382
+ declare function literalSecretMarkerRefusal(value: unknown): string | undefined;
3383
+ declare class SecretRedactor implements SecretRedactorPort {
3384
+ private readonly store;
3385
+ private readonly pattern;
3386
+ private readonly valuePattern;
3387
+ private readonly nameByValue;
3388
+ constructor(store: SecretStore);
3389
+ redactString(value: string): string;
3390
+ private maskValues;
3391
+ redactValue(value: unknown): unknown;
3392
+ }
3393
+
3394
+ /** Structural secret source (SecretStore satisfies it) — used to resolve container secretEnv. */
3395
+ interface SecretEnvSource {
3396
+ forEachSecret(callback: (name: string, value: string) => void): void;
3397
+ }
3398
+ /**
3399
+ * Resolve the already-materialized secret name→value map the container is injected with
3400
+ * (D-06). The runner-owned {@link SecretStore} satisfies the structural source; the port only
3401
+ * ever sees the resolved map, never the store itself.
3402
+ */
3403
+ declare function resolveSecretEnv(store?: SecretEnvSource): Record<string, string>;
3404
+ /**
3405
+ * The threaded hook-execution carrier (Phase 363 D-06) — successor to core's
3406
+ * `SandboxRunnerOptions`. Threaded through the run/suite config as `sandboxOptions` and
3407
+ * spread into {@link RunHooksOptions} at each hook site. Carries the injected
3408
+ * {@link HookExecutorPort} (constructed once at the composition root) plus the secret
3409
+ * store / redactor / active auth-state the hook body needs. `dockerBin`/`pullPolicy` are
3410
+ * retained for source-compat with the old carrier (the executor owns those now).
3411
+ */
3412
+ interface SandboxRunnerOptions {
3413
+ executor: HookExecutorPort;
3414
+ dockerBin?: string;
3415
+ pullPolicy?: 'always' | 'if-not-present' | 'never';
3416
+ envVars?: Record<string, string>;
3417
+ secretStore?: SecretStore;
3418
+ secretRedactor?: SecretRedactorPort;
3419
+ authState?: RuntimeAuthStateConfig;
3420
+ }
3421
+ interface HookOrchestrationResult {
3422
+ results: Map<string, HookResult>;
3423
+ variables: Record<string, string>;
3424
+ allPassed: boolean;
3425
+ duration: number;
3426
+ }
3427
+ /**
3428
+ * The narrow lifecycle-emit seam `runHooks` fires through — the Plan 03 sink legs
3429
+ * (`MemberEventSink.hookStarted`/`hookFinished`) satisfy it structurally. Both legs are
3430
+ * fire-and-forget; the orchestration NEVER awaits telemetry. Output detail is emitted
3431
+ * as-is (already sandbox-redacted by the impl); the sink applies the additive run-mask
3432
+ * (T-363-05) — the orchestration does not mask.
3433
+ */
3434
+ interface HookEmitSeam {
3435
+ hookStarted(payload: HookStartedPayload): void;
3436
+ hookFinished(payload: HookFinishedPayload): void;
3437
+ }
3438
+ /** Identity fields locating this hook batch in the run tree (payload provenance). */
3439
+ interface HookRunIdentity {
3440
+ phase: HookStartedPayload['phase'];
3441
+ scope?: HookStartedPayload['scope'];
3442
+ testIndex?: number;
3443
+ stepIndex?: number;
3444
+ }
3445
+ interface RunHooksOptions {
3446
+ /** The injected D-06 executor (runner-local `DockerHookExecutor`). */
3447
+ executor: HookExecutorPort;
3448
+ /** User env + CLI/suite variables the first hook starts from. */
3449
+ env?: Record<string, string>;
3450
+ /** Carrier-compat alias for {@link env} (the old `SandboxRunnerOptions.envVars` field). */
3451
+ envVars?: Record<string, string>;
3452
+ /** Already-resolved secret name→value pairs for container injection (D-06). */
3453
+ secretEnv?: Record<string, string>;
3454
+ /** Runner-owned secret store; the orchestrator resolves {@link secretEnv} from it. */
3455
+ secretStore?: SecretEnvSource;
3456
+ /** Structural secret redactor for stdout/stderr/error redaction. */
3457
+ secretRedactor?: SecretRedactorPort;
3458
+ /** Active auth-state mounted + injected per hook. */
3459
+ authState?: RuntimeAuthStateConfig;
3460
+ /** Tolerated carrier fields (the executor owns docker config now; ignored by orchestration). */
3461
+ dockerBin?: string;
3462
+ pullPolicy?: 'always' | 'if-not-present' | 'never';
3463
+ /** Optional lifecycle emit seam; requires {@link identity} to emit. */
3464
+ emit?: HookEmitSeam;
3465
+ /** Payload provenance; when present alongside {@link emit}, every executed hook emits. */
3466
+ identity?: HookRunIdentity;
3467
+ }
3468
+ declare function runHooks(hooks: HookDefinition[], options: RunHooksOptions): Promise<HookOrchestrationResult>;
3469
+
3470
+ /**
3471
+ * Start a run (D-01). Constructs the session SYNCHRONOUSLY — the bus, `runStarted` at seq 1, the
3472
+ * run-budget arm, the run-level AbortController — then kicks the async member pool whose settled
3473
+ * result feeds `done`. Returns the addressable {@link RunSession} immediately.
3474
+ *
3475
+ * 402-02 (D-09): members execute through a BOUNDED CONCURRENT POOL rather than a `for` loop, but
3476
+ * everything after it is unchanged — the terminal sequence is still ONE code path, run once, after
3477
+ * the pool settles. Concurrency changes which member is in flight when; it does not change who
3478
+ * publishes the verdict, or when `runFinished` is last.
3479
+ */
3480
+ declare function startRun(config: RunConfig): RunSession;
3481
+
3482
+ /**
3483
+ * Build the per-member {@link MemberEventSink} the shell hands to the executor context (D-02).
3484
+ * `testIndex` is bound here; `stepIndex` is curried by `loopEmitFor`. `mask` is the run's mask
3485
+ * (441-12, D-06). All three payload kinds go through the schema projections + mask.
3486
+ */
3487
+ declare function createMemberEventSink(deps: {
3488
+ bus: RunEventBus;
3489
+ mask: MaskFn;
3490
+ testIndex: number;
3491
+ }): MemberEventSink;
3492
+
3493
+ /** A single member-test step — a bare instruction or an object carrying per-step knobs. */
3494
+ interface MemberStepSpec {
3495
+ step: string;
3496
+ timeout?: number;
3497
+ capture?: {
3498
+ variable: string;
3499
+ method?: ExtractionMethod;
3500
+ pattern?: string;
3501
+ selector?: string;
3502
+ description?: string;
3503
+ };
3504
+ }
3505
+ /**
3506
+ * The one derivation of the text a step is announced with at `stepStarted` (G-428-2). Every
3507
+ * producer that states a step's text before it starts (the CLI's `stepTexts` on each member
3508
+ * descriptor) derives it here, so the pending text and the announced text cannot drift.
3509
+ */
3510
+ declare function memberStepText(step: string | MemberStepSpec): string;
3511
+ /** The runner-owned member-test shape (a core-independent subset of TestDefinition). */
3512
+ interface MemberTest {
3513
+ testId: string;
3514
+ name: string;
3515
+ filePath: string;
3516
+ url?: string;
3517
+ context?: string;
3518
+ steps: Array<string | MemberStepSpec>;
3519
+ meta?: {
3520
+ timeout?: number;
3521
+ retries?: number;
3522
+ [key: string]: unknown;
3523
+ };
3524
+ }
3525
+ /** The injected runJS execution handler (Q-2). The CLI owns page.evaluate + platform checks. */
3526
+ type RunJsHandler = (code: string, ctx: {
3527
+ abortSignal: AbortSignal;
3528
+ }) => Promise<{
3529
+ value: string;
3530
+ error?: string;
3531
+ }>;
3532
+ /** The injected appium execution handler (Q-2). The CLI resolves secrets + calls the adapter. */
3533
+ type AppiumScriptHandler = (action: Action, ctx: {
3534
+ abortSignal: AbortSignal;
3535
+ }) => Promise<ActionResult>;
3536
+ /** The injected accessibility-check handler (Q-4). The CLI owns page acquisition + the web module. */
3537
+ type AccessibilityCheckHandler = (opts: {
3538
+ standard?: 'wcag2a' | 'wcag2aa' | 'wcag2aaa';
3539
+ disableRules?: string[];
3540
+ exclude?: string[];
3541
+ }, ctx: {
3542
+ abortSignal: AbortSignal;
3543
+ }) => Promise<AccessibilityViolation[]>;
3544
+ /** The injected failure-screenshot writer. The CLI owns the adapter screenshot capture. */
3545
+ type FailureScreenshotWriter = (ctx: {
3546
+ abortSignal: AbortSignal;
3547
+ }) => Promise<Buffer | undefined>;
3548
+ /**
3549
+ * Everything runMemberTest needs, all injected at the CLI composition root. The sink,
3550
+ * abortSignal, and testIndex are the per-member context the shell binds (so runMemberTest
3551
+ * slots cleanly behind {@link MemberTestExecutor}); the rest are the run-scoped host impls
3552
+ * the CLI already builds for the loop.
3553
+ */
3554
+ interface MemberRunConfig {
3555
+ adapter: PlatformAdapter;
3556
+ planner: Planner;
3557
+ verifier?: Verifier;
3558
+ cache?: ActionPlanCache;
3559
+ modelProfiles?: {
3560
+ planner: DeclaredModelProfile;
3561
+ verifier?: DeclaredModelProfile;
3562
+ };
3563
+ onEvidenceContext?: (context: MemberEvidenceContext) => void;
3564
+ workingMemory?: WorkingMemoryPort;
3565
+ storageState?: StorageStatePort;
3566
+ compressor?: ScreenshotCompressor;
3567
+ knowledgeReader?: KnowledgeHintReader;
3568
+ knowledgeBudgets?: {
3569
+ brief: number;
3570
+ searchResults: number;
3571
+ caution: number;
3572
+ readBody: number;
3573
+ recordCap: number;
3574
+ };
3575
+ knowledgeAsOfBuild?: number;
3576
+ /**
3577
+ * 407 D-13 — the host's retrieval rung, resolved once at the composition root and threaded
3578
+ * verbatim onto every step's LoopPorts. Typed off LoopPorts so there is ONE spelling of the
3579
+ * contract between the run shell and the loop.
3580
+ */
3581
+ knowledgeRetrievalRung?: LoopPorts['knowledgeRetrievalRung'];
3582
+ recordLedger?: LoopPorts['recordLedger'];
3583
+ beginMemoryTurn?: LoopPorts['beginMemoryTurn'];
3584
+ drainMemoryReadTraces?: LoopPorts['drainMemoryReadTraces'];
3585
+ uniqueValueGenerator?: UniqueValueGenerator;
3586
+ guardBlob?: (value: unknown) => string | undefined;
3587
+ configDigests?: ConfigDigests;
3588
+ evidenceArtifacts?: EvidenceArtifactStorePort;
3589
+ plannerModel: LanguageModel;
3590
+ verifierModel: LanguageModel;
3591
+ /** The host closes over resolved model/accounting configuration, never a current-step pointer. */
3592
+ stepModelScope?: {
3593
+ runId: StepModelScopeKey['runId'];
3594
+ create: StepModelScopeFactory;
3595
+ };
3596
+ envFileVars?: Record<string, string>;
3597
+ inlineVars?: Record<string, string>;
3598
+ suiteVars?: Record<string, string>;
3599
+ cliVars?: Record<string, string>;
3600
+ hookSetupVars?: Record<string, string>;
3601
+ hookExecutor?: HookExecutorPort;
3602
+ inlineHookDefs?: Map<string, HookDefinition>;
3603
+ hookEnvVars?: Record<string, string>;
3604
+ hookSecretEnv?: Record<string, string>;
3605
+ hookAuthState?: RuntimeAuthStateConfig;
3606
+ runJs?: RunJsHandler;
3607
+ appiumScript?: AppiumScriptHandler;
3608
+ accessibilityCheck?: AccessibilityCheckHandler;
3609
+ persistFailureScreenshot?: FailureScreenshotWriter;
3610
+ resolveSecrets?: (text: string) => string;
3611
+ secretStore?: SecretResolver;
3612
+ secretRedactor?: SecretRedactorPort;
3613
+ sink: MemberEventSink;
3614
+ testIndex: number;
3615
+ abortSignal: AbortSignal;
3616
+ captureScreenshots?: boolean;
3617
+ recording?: {
3618
+ enabled: boolean;
3619
+ keepFailuresOnly?: boolean;
3620
+ };
3621
+ accessibility?: {
3622
+ enabled: boolean;
3623
+ standard?: 'wcag2a' | 'wcag2aa' | 'wcag2aaa';
3624
+ runAfter?: 'every-step' | 'navigation' | 'test-end';
3625
+ failOnViolation?: boolean;
3626
+ disableRules?: string[];
3627
+ exclude?: string[];
3628
+ };
3629
+ timeouts?: {
3630
+ step?: number;
3631
+ test?: number;
3632
+ navigation?: number;
3633
+ };
3634
+ plannerConfig?: PlannerConfig;
3635
+ logger?: LoopLogger;
3636
+ logCapture?: {
3637
+ console?: boolean;
3638
+ network?: boolean;
3639
+ };
3640
+ vision?: {
3641
+ screenshotSize?: number;
3642
+ effectiveResolution?: number;
3643
+ };
3644
+ visionGate?: {
3645
+ attachImage: boolean;
3646
+ captureMode: 'full' | 'textOnly';
3647
+ promptNote: string;
3648
+ };
3649
+ contextWindow?: number;
3650
+ suiteContext?: string;
3651
+ resolveUploadPath?: (rawPath: string) => string;
3652
+ onWarn?: (message: string) => void;
3653
+ executeStep?: typeof executeStep;
3654
+ extractorFactory?: (model: LanguageModel | undefined, method: string) => VariableExtractor;
3655
+ }
3656
+ /**
3657
+ * Run a member test with the retry loop (D-13). The trigger stays `meta.retries`
3658
+ * (user-config surface unchanged). Attempts run INSIDE the member; `testRetryScheduled`
3659
+ * fires between attempts (a per-test accumulation reset for subscribers); only the FINAL
3660
+ * attempt's TestResult leaves the member carrying `retryCount` (Pitfall 9 — the shell
3661
+ * emits the single authoritative testStarted/testFinished pair per member).
3662
+ */
3663
+ declare function runMemberTest(member: MemberTest, config: MemberRunConfig): Promise<TestResult>;
3664
+
3665
+ declare function isCanonicalHookIdShape(value: string): boolean;
3666
+ interface InlineHookCall {
3667
+ hookId: string;
3668
+ fullMatch: string;
3669
+ }
3670
+ declare function parseHookInline(text: string): InlineHookCall[];
3671
+ declare function stripHookInline(text: string): string;
3672
+ interface RunJSMatch {
3673
+ fullMatch: string;
3674
+ code: string;
3675
+ }
3676
+ declare function parseRunJSInline(text: string): RunJSMatch[];
3677
+ declare function coerceRunJSResult(value: unknown): string;
3678
+ declare function parseAppiumInline(text: string): Action | null;
3679
+
3680
+ declare function generateFailureSummary(steps: StepResult[]): string;
3681
+
3682
+ /**
3683
+ * Fold one completed member {@link TestResult} + its derived {@link MemberTestOutcome} into a
3684
+ * {@link PerTestOutcome}. Steps are built from `result.steps`: a step with §A8 evidence carries it
3685
+ * (with `stepIndex` OVERRIDDEN to its array position); an evidence-less step gets a synthesized
3686
+ * minimal StepEvidence. `testIndex` is the shell-owned array position.
3687
+ *
3688
+ * `rootCause` is the shell-classified §A9 verdict (373-07): attached ONLY when defined so clean
3689
+ * passes/skipped/cancelled members stay rootCause-free in the persisted JSON (the D-01 presence
3690
+ * rule — the key is absent, never undefined-valued).
3691
+ *
3692
+ * `deps.quarantined` is the 390 D-14 disposition, stamped BESIDE `outcome` under the same
3693
+ * presence rule. It never alters `outcome`: a quarantined failing member reads `'failed'` here, and
3694
+ * the licence to ignore that failure lives entirely in the aggregate rule and in this marker.
3695
+ */
3696
+ declare function assemblePerTestOutcome(member: RunMemberDescriptor, result: TestResult, outcome: MemberTestOutcome, deps: {
3697
+ testIndex: number;
3698
+ scope: ScopeKey;
3699
+ buildOrdinal: number;
3700
+ mask: MaskFn;
3701
+ modelProfile?: DeclaredModelProfile;
3702
+ verifierIdentity?: ModelIdentity;
3703
+ toolSchemaDigest?: string;
3704
+ quarantined?: true;
3705
+ }, rootCause?: Verdict, origin?: {
3706
+ runId: string;
3707
+ outcome: 'failed' | 'timeout';
3708
+ rootCause?: Verdict;
3709
+ }): PerTestOutcome;
3710
+ /**
3711
+ * The shell-produced skip outcome (D-15): a member the shell never invoked (run aborted, or a
3712
+ * poisoned same-group member). `outcome: 'skipped'`, no steps — neutral in the aggregate fold.
3713
+ *
3714
+ * `quarantined` is still stamped when the ledger licensed this member (390 D-14): the disposition
3715
+ * describes the TEST's standing, not the outcome it happened to reach, so a reader of the handoff
3716
+ * sees the same truth whether the member ran or was skipped past.
3717
+ */
3718
+ declare function skippedPerTestOutcome(member: RunMemberDescriptor, testIndex: number, quarantined?: true): PerTestOutcome;
3719
+ /**
3720
+ * Assemble the one {@link CompletedRunHandoff} a run persists (D-17). Every header field is copied
3721
+ * verbatim FROM the resolved {@link RunConfig} (D-04/D-20 — never re-derived); `aggregateVerdict`
3722
+ * is the execution verdict; `tests` are the assembled per-member outcomes in run order.
3723
+ *
3724
+ * 390 D-16: `quarantineApplied` records the ledger entries this run was operating under, copied
3725
+ * from the same resolved config the disposition markers were matched against — so the run's own
3726
+ * receipt answers "which suppressions were licensed here, and until when" without re-reading a
3727
+ * ledger that may since have changed. An empty set stays ABSENT, keeping every unquarantined run's
3728
+ * handoff bytes identical to pre-390.
3729
+ */
3730
+ declare function assembleHandoff(config: RunConfig, outcomes: PerTestOutcome[], aggregateVerdict: RunStatus, timing: {
3731
+ startedAt: number;
3732
+ endedAt: number;
3733
+ }): CompletedRunHandoff;
3734
+
3735
+ /**
3736
+ * Derive the member-level outcome for one completed test (D-16). Pure total function.
3737
+ *
3738
+ * - status `skipped` ⇒ `'skipped'` (member-neutral; passed through)
3739
+ * - status `cancelled` ⇒ `'cancelled'`
3740
+ * - status `passed` ⇒ `'healed'` when ANY step carries selfHeal evidence, else `'passed'`.
3741
+ * The live heal signal is StepEvidence.selfHeal *presence* — a step-status read would
3742
+ * never fire (RESEARCH Pitfall 5), so we never inspect a step status string here.
3743
+ * - status `failed` ⇒ `'timeout'` when ANY failed step's `error` is the test-deadline
3744
+ * message (isTestTimeoutMessage), else `'failed'`.
3745
+ *
3746
+ * Core's TestStatus and the executor port stay untouched by this derivation (D-16).
3747
+ */
3748
+ declare function deriveMemberOutcome(result: TestResult): MemberTestOutcome;
3749
+ /**
3750
+ * Fold member outcomes into the aggregate run verdict — the ratified spec §2.1 ordered
3751
+ * rule with the D-15 neutral-skipped clause. EVALUATED IN ORDER:
3752
+ *
3753
+ * 1. `runCancelled` ⇒ `'cancelled'` (a whole-run cancel wins over everything)
3754
+ * 2. `runBudgetExpired` ⇒ `'timeout'` (reserved for the whole-run wall-clock budget;
3755
+ * a member 'timeout' is NOT this — it falls to the failed branch below)
3756
+ * 3. any member `'failed'` or `'timeout'` ⇒ `'failed'`
3757
+ * 4. any member `'cancelled'` (none failed) ⇒ `'cancelled'`
3758
+ * 5. any member `'healed'` (none failed) ⇒ `'healed'`
3759
+ * 6. else ⇒ `'passed'`
3760
+ *
3761
+ * `'skipped'` members are neutral throughout — an all-skipped (or empty) run is
3762
+ * vacuously `'passed'` (D-15, the reconciled clause; matches every test runner).
3763
+ *
3764
+ * PHASE 390 D-14 — the quarantine skip is EXPLICIT IN THE RULE, right here in step 3 and nowhere
3765
+ * else. `quarantined[i] === true` marks a member whose failure the committed ledger has licensed:
3766
+ * step 3 passes over it, so a run whose only failures are quarantined aggregates `'passed'` and
3767
+ * exits 0 through the untouched {@link mapVerdictToExitCode}. Three things this deliberately does
3768
+ * NOT do: it does not rewrite the member outcome (`'failed'` stays `'failed'` — the projection
3769
+ * window and the auto-exit keep seeing reality), it does not touch any other branch (a sibling
3770
+ * `'cancelled'`/`'healed'` still governs, and the two run-level branches still win outright), and
3771
+ * it does not widen either closed union. `quarantined` absent — or shorter than `outcomes` — is
3772
+ * byte-identical pre-390 behavior, which is why every existing call site compiles unchanged.
3773
+ *
3774
+ * The skip covers `'timeout'` members as well as `'failed'` ones: a quarantined flaky test that
3775
+ * times out is the same A3 phenomenon, and a rule that suppressed one but not the other would make
3776
+ * relief depend on how the flake happened to manifest that build.
3777
+ */
3778
+ declare function deriveAggregateVerdict(input: {
3779
+ runCancelled: boolean;
3780
+ runBudgetExpired: boolean;
3781
+ outcomes: readonly MemberTestOutcome[];
3782
+ quarantined?: readonly boolean[];
3783
+ }): RunStatus;
3784
+ /**
3785
+ * Map an aggregate verdict to a process exit code (D-14). Pure and pinned:
3786
+ * passed/healed ⇒ 0 (healed is ratified as a pass — the annotation lives in output and
3787
+ * the handoff, never the exit code), failed/timeout ⇒ 1, cancelled ⇒ 130 (existing SIGINT
3788
+ * convention). Exit 2 stays reserved for config/setup errors and is NOT produced here.
3789
+ */
3790
+ declare function mapVerdictToExitCode(verdict: RunStatus): number;
3791
+
3792
+ /** The four classes the deterministic classifier can mint (RCC-02). */
3793
+ type DeterministicRootCauseClass = 'timeout' | 'environment' | 'flake' | 'unknown';
3794
+ /** The fixed unknown-class rationale template (D-02). */
3795
+ declare const UNKNOWN_SIGNAL_RATIONALE = "no deterministic signal matched";
3796
+ interface RootCauseInput {
3797
+ /** The member's final-attempt TestResult (carries retryCount — the flake signal source). */
3798
+ result: TestResult;
3799
+ /** The derived member outcome (deriveMemberOutcome) — the timeout + presence-rule signal. */
3800
+ outcome: MemberTestOutcome;
3801
+ /** Shell-derived member index; becomes Verdict.perTest. */
3802
+ testIndex: number;
3803
+ /** Run-start baseline probe outcome (D-04, web targets; threaded by Plan 07). */
3804
+ baselineProbe?: {
3805
+ status?: number;
3806
+ elapsedMs: number;
3807
+ error?: string;
3808
+ };
3809
+ /**
3810
+ * The run's mask (439 D-08). An unproven step's reason carries the planner's own words, which
3811
+ * the step error holds unmasked, so its rationale passes through this before it is recorded.
3812
+ */
3813
+ mask?: (text: string) => string;
3814
+ }
3815
+ /**
3816
+ * The D-02 faithful VerdictBase constructor. Pure and total: every field of the LLM-shaped
3817
+ * base is filled deterministically — confidence keys on the class ('unknown' ⇒ 0, else 1),
3818
+ * the unknown rationale is always the fixed template regardless of the signal argument.
3819
+ */
3820
+ declare function deterministicVerdict(cls: DeterministicRootCauseClass, signal: string, citedSteps: number[], perTest: number): Verdict;
3821
+ /**
3822
+ * Classify one member's root cause from deterministic signals only (RCC-02). Returns
3823
+ * undefined when the presence rule says no rootCause (skipped/cancelled/clean pass).
3824
+ * See the module header for the documented, pinned branch order.
3825
+ */
3826
+ declare function classifyRootCause(input: RootCauseInput): Verdict | undefined;
3827
+
3828
+ declare function createMemoryTools(deps: {
3829
+ reader: KnowledgeHintReader;
3830
+ scope: ScopeKey;
3831
+ asOfBuild: number;
3832
+ searchResultBudget: number;
3833
+ readBodyBudget: number;
3834
+ recordLedger: RecordLedger;
3835
+ estimateTokens: (text: string) => number;
3836
+ onReadTrace?: (trace: ReadTrace) => void;
3837
+ }): {
3838
+ tools: NonActionToolMap;
3839
+ beginTurn(scope?: ScopeKey, exposedIds?: readonly RecordId[], turnOrdinal?: number): void;
3840
+ drainReadTraces(): ReadTrace[];
3841
+ };
3842
+
3843
+ declare function buildTools(registry: ToolRegistry, options: {
3844
+ platform: Platform;
3845
+ categories?: ToolCategory[];
3846
+ }): Record<string, Tool<any, any>>;
3847
+ declare function toolCallToActionPlan(toolId: string, args: Record<string, unknown>, registry: ToolRegistry): {
3848
+ action: {
3849
+ type: string;
3850
+ };
3851
+ failureReason?: string | undefined;
3852
+ failureKind?: "infeasible" | undefined;
3853
+ bulkCompletionPredicate?: string | undefined;
3854
+ remember?: {
3855
+ key: string;
3856
+ value: string;
3857
+ kind: "code" | "filename" | "extracted-value" | "progress-marker";
3858
+ provenance: "observation" | "toolResult" | "plannerInference";
3859
+ testIndex?: number | undefined;
3860
+ stepIndex?: number | undefined;
3861
+ }[] | undefined;
3862
+ reasoning: string;
3863
+ confidence: number;
3864
+ stepComplete: boolean;
3865
+ stepFailed: boolean;
3866
+ doesActionAdvanceUsTowardsGoal: boolean;
3867
+ };
3868
+
3869
+ /**
3870
+ * Project a resolved Action onto the schema-owned {@link RiskActionDescriptor} (D-08). An
3871
+ * unrecognized verb projects `recognized: false` — the classifier's fail-safe floor maps it to
3872
+ * `restricted` → deny. `command`/`declaredTier` are deliberately omitted: no shell-wrapper actions
3873
+ * exist in-test and `declaredTier` stays the registry's future channel.
3874
+ */
3875
+ declare function projectRiskAction(action: {
3876
+ type: string;
3877
+ }): RiskActionDescriptor;
3878
+
3879
+ declare function normalizePointerActionForStep(action: Action, stepInstruction: string): Action;
3880
+
3881
+ /**
3882
+ * Converts "Meta" tokens in keyboard combo strings to "Control" on non-Mac platforms.
3883
+ *
3884
+ * Users write combos like "Meta+k" and we route them to Cmd+K on Mac, Ctrl+K on
3885
+ * Windows/Linux. Uses split/join (not regex) to avoid edge-case bugs with adjacent
3886
+ * tokens like "Shift+Meta+Meta+T".
3887
+ *
3888
+ * @param keys - Array of key/combo strings (e.g., ["Meta+k", "Enter"])
3889
+ * @param options.enabled - When false, keys are returned unchanged
3890
+ * @param options.isMac - When true, keys are returned unchanged (Meta is native)
3891
+ * @returns New array with Meta tokens replaced by Control (when applicable)
3892
+ */
3893
+ declare function convertKeysForPlatform(keys: string[], options: {
3894
+ enabled: boolean;
3895
+ isMac: boolean;
3896
+ }): string[];
3897
+ /**
3898
+ * Returns true when running on macOS. Reads process.platform at call time so
3899
+ * tests can override it via Object.defineProperty(process, 'platform', ...).
3900
+ */
3901
+ declare function isMacPlatform(): boolean;
3902
+
3903
+ export { AUTH_STATE_HOOK_CONTAINER_STORAGE_STATE_PATH, AUTH_STATE_HOOK_JSON_ENV, AUTH_STATE_HOOK_STORAGE_STATE_FILENAME, AUTH_STATE_HOOK_STORAGE_STATE_PATH_ENV, AUTH_STATE_HOOK_WORKSPACE_DIR, type AccessibilityCheckHandler, type AccessibilityViolation, type Action, type ActionPlan, type ActionPlanCache, type ActionResult, type ActionsFor, type AgentAbortScope, type AgentTimeoutAbortReason, type AiJudgmentMeta, AiJudgmentResultSchema, type AppRegistryEntry, type AppResolution, type AppiumScriptHandler, type AssertValueInput, type AssertValueOutcomeMeta, type AssertValueResult, type Asserter, AssertionEvaluator, type AssertionInput, type AssertionResult, type AssertionType, type BaselineProbeResult, type BrowserConfig, type BuildCacheableStepInputArgs, type BuildCachedPlanValueInput, type BuildScreenStateInput, type BuildScreenStateResult, CACHE_ENV_DIGEST_SALT, CHARS_PER_TOKEN_ESTIMATE, CLIPBOARD_TEXT_LIMIT, CLIPBOARD_TRUNCATION_MARKER, type CacheableClassification, type CacheablePlannedAction, type CachedPlanValue, type CaptureResult, type CaptureStamp, type CarriedArtifacts, type CarriedClipboard, type CarriedDownload, type CarriedLine, type ClassifyStepInput, type ClipboardWriteMeta, type CompressedScreenshot, type ConditionJudgment, type ConfigDigests, type ConformanceInput, type ConsoleLogEntry, type CreateLiveStreamHubOptions, type CreateRunEventBusOptions, DEFAULT_CANDIDATE_COUNT, DEFAULT_EDIT_DISTANCE_BOUND, DEFAULT_FUZZY_MARGIN, DEFAULT_FUZZY_THRESHOLD, DEFAULT_NON_ACTION_TOOL_BUDGET, type DesiredState, type DesiredStateAction, type DeterministicRootCauseClass, type DeviceConfig, type DialogAccount, type Direction, type DirectionScrollParams, type DocumentScrollOffsets, EXPOSURE, type EventLogPort, type EvidenceArtifactStorePort, EvidenceRecorder, type ExecuteOptions, type ExecutePlanningTurnOptions, ExplicitExtractor, type ExtractionMethod, ExtractionResultSchema, type ExtractorInput, FILE_DIFFERENCE_WINDOW, FILL_SEPARATORS, type FailureScreenshotWriter, type FillRefusalMeta, type FillRereadMeta, type FillSubmitMeta, type FillVerdictMeta, type FillVerifyInput, type FillVerifyResult, type GroundedLocator, type GroundingStrategy, type HandoffStorePort, type HookDefinition, type HookEmitSeam, type HookExecuteOptions, type HookExecutorPort, type HookOrchestrationResult, type HookResult, type HookRunIdentity, type HookRuntime, type InlineHookCall, type InterimFixture, type InterimFixtureKind, type InterimNode, type InterpolationWithProvenance, type KnowledgeHintReader, LIVE_KEEPALIVE_QUIET_MS, LIVE_SNAPSHOT_INTERVAL_MS, LIVE_STREAM_ACK_BOUND_MS, LIVE_STREAM_STOP_GRACE_MS, LLMAssertionEvaluator, LLMPlanner, LLMVariableExtractor, LLMVerifier, type LiveStreamCapability, type LiveStreamHub, type LiveStreamRegistration, type LoopEventEmitPort, type LoopLogger, type LoopPorts, type LoopScopedLogger, MAX_TO_END_SWIPES, MINT_VERBS, type MemberEventSink, type MemberEvidenceContext, type MemberRunConfig, type MemberStepSpec, type MemberTest, type MemberTestExecutor, type MemberTestRunContext, type MintVerb, MissingSecretError, type NetworkLogEntry, type NonActionToolHandler, type NonActionToolMap, OBSERVATION_LINE_PREFIX, type ObserveOptions, type OpenAppMeta, type OperationCaptureSupport, POST_VERDICT_STEP_BUDGET_MS, type PersistedRecording, type PlanResult, type Planner, type PlannerConfig, type PlanningCallGate, PlanningLimitReachedError, PlanningTurnError, type PlanningTurnFailure, type PlanningTurnFailureReason, type PlanningTurnResult, type PlanningTurnSuccess, type PlatformAdapter, type PlatformConfig, type PostVerdictOutcome, type PostVerdictPort, type PostVerdictStepInput, RECORD_ACTION_LINE_CAP, RECORD_LINE_CHAR_CAP, RECORD_LIST_CAP, ROW_CONTAINER_ROLES, RUNTIME_IMAGE_MAP, type RecordAdmission, type RecordLedger, type RecordSubActionInput, type RecordedAction, type RecordedActionOutcome, type RecordedRead, type RecordedReadKind, type RecordingCapability, type RecordingStart, type RecordingStop, type RefBinding, type RenderedCautionLine, type RenderedStepRecord, ReplayIncompleteError, type ReportValueInput, type ReportedValueMeta, type RootCauseInput, type RunArtifactStorePort, type RunConfig, type RunEventBus, type RunHooksOptions, type RunJSMatch, type RunJsHandler, type RunMemberDescriptor, type RunPorts, type RunTimerPort, type RunnerDegradeOptions, type RunnerDegradeOutcome, type RuntimeAuthStateConfig, SCROLL_OUTCOMES_READ_ONLY, SCROLL_OUTCOMES_WRITTEN, type SandboxRunnerOptions, type SavedFileComparison, type SavedFileExpectation, type ScorerOptions, type ScreenConformanceCase, type ScreenshotCompressor, type ScriptedVerdict, type ScrollEndedBy, type ScrollLoopResult, type ScrollOffsets, type ScrollPrimitives, type ScrollUntilVisibleMeta, SecretConfigError, type SecretFileMetadata, SecretRedactor, type SecretRedactorPort, type SecretResolver, SecretStore, type SecretTemplate, type ServedCautions, type ServedRunBrief, type ShellActionMeta, type StatedTarget, type StatedTargets, type StepAnnotation, type StepContext, type StepModelAttempt, type StepModelBinding, type StepModelCallEvent, type StepModelCallReport, type StepModelConsumers, type StepModelObservation, type StepModelRoleFacts, type StepModelScope, type StepModelScopeFactory, type StepModelScopeKey, type StepModelScopeOptions, type StepModelSnapshot, type StepPhaseEvent, StepRecordAccumulator, type StepResult, type StepStatus, type StepTrace, type StorageStateBlob, type StorageStatePort, type StorageStateReference, type SubActionTrace, type SwipeSignal, type TargetVisibility, type TestResult, type TestStatus, type TimeoutConfig, type TokenAccounting, type TokenUsage, type ToolCategory, type ToolDefinition, type ToolExchange, ToolRegistry, UNKNOWN_SIGNAL_RATIONALE, type UniqueValueGenerator, type UniqueValueInput, type UniqueValueMeta, type UntilVisibleParams, type VariableExtractor, type VariableSource, VariableStore, type VariableStoreOptions, type VerificationResult, VerificationResultSchema, type Verifier, type VerifierStepRecord, type VerifyResult, type WorkingMemoryPort, type WorkingMemoryProvenanceContext, WorkingMemoryStore, assembleBriefTiers, assembleHandoff, assemblePerTestOutcome, assertNever, buildAssertionPrompt, buildAuthStateHookEnv, buildCacheableStepInput, buildCachedPlanValue, buildContextTier, buildExtractionPrompt, buildScreenState, buildSystemPrompt, buildTools, buildVerificationPrompt, buildVerifierStableTier, buildVolatileTier, classifyRootCause, classifyStep, coerceRunJSResult, collapseAnnotation, compareAssertValue, compareFillReread, compareSavedFile, composeTurnMessages, containsRedactedSecretMarker, convertKeysForPlatform, createAsserter, createExtractor, createLiveStreamHub, createMemberEventSink, createMemoryTools, createRecordLedger, createRunEventBus, createSnapshotLiveStream, createStepModelScope, createTimeoutAbortReason, defaultLiveStreamTimer, defaultRegistry, defaultUniqueValueGenerator, deriveAggregateVerdict, deriveMemberOutcome, desiredStateSatisfied, deterministicDescriptorMatcher, deterministicVerdict, documentSwipeSignal, estimateRenderedServingCost, estimateTokens, executePlanningTurn, executeStep, findBareVariables, findSecretTemplates, findUnresolvedTemplates, foldCacheStatus, formatAction, generateFailureSummary, getTimeoutAbortReason, imageSizeOf, interpolateSecretTemplates, interpolateVariables, interpolateVariablesWithProvenance, isCanonicalHookIdShape, isMacPlatform, isReservedAuthStateHookEnvKey, isTestTimeoutMessage, judgeStatedTarget, literalSecretMarkerRefusal, liveStreamFor, makeEmptyServedBrief, makeInterimNodes, makeScreenState, mapVerdictToExitCode, memberStepText, metVerification, mintReportedValue, mintUniqueValue, normalizePointerActionForStep, notMetVerification, parseAppiumInline, parseEnvFile, parseHookInline, parseRunJSInline, projectLegacyTrace, projectRiskAction, readAiJudgmentMeta, readAssertValueOutcomeMeta, readClipboardMeta, readDownloadMeta, readFillRefusalMeta, readFillRereadMeta, readFillSubmitMeta, readFillVerdictMeta, readOpenAppMeta, readReportedValueMeta, readScrollUntilVisibleMeta, readShellActionMeta, readUniqueValueMeta, redactSecretValue, renderBriefAsHypotheses, renderBriefLine, renderCautionLine, renderCautionSummary, renderObservationLine, renderScreenTree, renderStepRecord, resolveAppName, resolveSecretEnv, resolveSecretTemplatesInValue, rowContains, rowTextOf, runDirectionScroll, runHooks, runMemberTest, runToEndScroll, runUntilVisibleLoop, screenConformanceCases, scriptedVerification, scrollerSetSwipeSignal, serializeEnvFile, skippedPerTestOutcome, stampCaptureMode, startRun, stripHookInline, stripReservedAuthStateHookEnv, swipeSignalFromOffsets, tierForEntry, toGroundedLocator, toVerificationResult, toolCallToActionPlan, withAbort, withStepModelCall, wordTokens };