@vostride/agent-qa-dashboard 0.1.21 → 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,2094 @@
1
+ import { IncomingMessage, ServerResponse, Server } from 'node:http';
2
+ import { RunAttributes, ResolvedWorkspacePaths, ResolvedAppiumExecutable, HookDefinition, AuthStateMetadata } from '@vostride/agent-qa-core';
3
+ import { RunDetailMember, RunDetailStep, RunDetailSubAction, RunDetailRows, RunStatus, RunArtifactKind, RunArtifactStageInput, RunArtifactPayload, RunArtifactFinalizeInput, LiveStreamDeclaration, LiveFrame, LiveFrameEndReason, LiveFrameSubscription, FunctionCaller, RunEventEnvelope, RunStartAcknowledgement, RunSession, RunServiceReadiness, RunsLiveUrlResult, RunServiceDenialBody, MaskFn, LiveViewSubjectRef, LiveUrlVerdict, EditorLiveUrlResult, ConfigWriteIssue, ConfigChange, ConfigPathSegment, ConfigWriteRefusal, PlannerLimits } from '@vostride/agent-qa-schema';
4
+ import { EventEmitter } from 'node:events';
5
+ import { SecretStore, SecretRedactor, ConsoleLogEntry, NetworkLogEntry, StepAnnotation, StepPhaseEvent, StepResult } from '@vostride/agent-qa-runner';
6
+ import { Node } from 'yaml';
7
+
8
+ interface RunOwner {
9
+ readonly pid: number;
10
+ readonly incarnation: string | null;
11
+ readonly host: string;
12
+ }
13
+ interface RunOwnerProbe {
14
+ isAlive(pid: number): boolean;
15
+ incarnationOf(pid: number): string | undefined;
16
+ }
17
+ interface RunOwnership {
18
+ readonly self: RunOwner;
19
+ readonly probe: RunOwnerProbe;
20
+ }
21
+ interface RunOwnershipInput {
22
+ readonly self: Omit<RunOwner, 'host'> & {
23
+ readonly host?: string;
24
+ };
25
+ readonly probe: RunOwnerProbe;
26
+ }
27
+ declare function completeRunOwnership(input: RunOwnershipInput): RunOwnership;
28
+ type RunOwnerState = 'self' | 'alive' | 'gone' | 'unknown';
29
+ declare function classifyRunOwner(owner: RunOwner, ownership: RunOwnership): RunOwnerState;
30
+ declare const orphanSummary: (pid: number) => string;
31
+ declare const ownedElsewhere: (pid: number) => string;
32
+ declare function defaultRunOwnership(): RunOwnership;
33
+
34
+ interface RunRow {
35
+ id: string;
36
+ name: string;
37
+ filePath: string | null;
38
+ status: string;
39
+ priority: number;
40
+ duration: number;
41
+ attributes: RunAttributes;
42
+ environment: string | null;
43
+ metadata: Record<string, unknown> | null;
44
+ startedAt: string | null;
45
+ endedAt: string | null;
46
+ failureSummary: string | null;
47
+ errorLog: string | null;
48
+ memoryLog: string | null;
49
+ testId: string | null;
50
+ suiteId: string | null;
51
+ platform: string;
52
+ testFileContent: string | null;
53
+ modelName: string | null;
54
+ llmProvider: string | null;
55
+ parallel: boolean;
56
+ createdAt: string;
57
+ members?: RunMemberSummary[];
58
+ memberOutcome?: string | null;
59
+ }
60
+ interface TestRef {
61
+ testId?: string;
62
+ filePath?: string;
63
+ }
64
+ interface RunMemberSummary {
65
+ testIndex: number;
66
+ testId: string | null;
67
+ filePath: string | null;
68
+ suiteId: string | null;
69
+ name: string | null;
70
+ outcome: string | null;
71
+ }
72
+ interface RunArtifactRow {
73
+ runId: string;
74
+ kind: RunArtifactKind;
75
+ schemaVersion: number;
76
+ payload: RunArtifactPayload;
77
+ finalizedAt: string | null;
78
+ createdAt: string;
79
+ updatedAt: string;
80
+ }
81
+ /**
82
+ * ONE RUN'S ARTIFACT, AND NOTHING ELSE (405-07, D-02).
83
+ *
84
+ * `children` stood here and is gone. It walked the run row's now-deleted parent link for the child
85
+ * rows of a suite parent — a shape nothing has written since 402-09 put ONE `runs` row per run and moved member
86
+ * identity onto `run_members`. A structurally-always-empty array on a response is worse than an
87
+ * absent one: a reader cannot tell "this run has no children" from "children stopped being a fact
88
+ * this product records", so the page renders an empty section forever and nobody notices.
89
+ */
90
+ interface RunArtifactBundle {
91
+ artifact: RunArtifactRow | null;
92
+ }
93
+ interface ExecutionLogRow {
94
+ id: string;
95
+ runId: string;
96
+ stepId: string | null;
97
+ /**
98
+ * 405 D-01: where in the run tree this hook ran, from `HookFinishedPayload`'s own optional
99
+ * fields. `null` means a RUN-SCOPED hook — a real state, not a missing value.
100
+ */
101
+ testIndex: number | null;
102
+ stepIndex: number | null;
103
+ type: 'hook' | 'appium-script' | 'runjs';
104
+ name: string;
105
+ hookId: string | null;
106
+ phase: 'setup' | 'teardown' | 'inline';
107
+ status: 'passed' | 'failed';
108
+ duration: number;
109
+ stdout: string | null;
110
+ stderr: string | null;
111
+ returnData: unknown | null;
112
+ variables: Record<string, string> | null;
113
+ createdAt: string;
114
+ }
115
+ interface InsertExecutionLogInput {
116
+ id: string;
117
+ runId: string;
118
+ stepId?: string | null;
119
+ /** 405 D-01: the handle and step this hook belongs to; absent means run-scoped. */
120
+ testIndex?: number | null;
121
+ stepIndex?: number | null;
122
+ type: 'hook' | 'appium-script' | 'runjs';
123
+ name: string;
124
+ hookId?: string | null;
125
+ phase: 'setup' | 'teardown' | 'inline';
126
+ status: 'passed' | 'failed';
127
+ duration: number;
128
+ stdout?: string | null;
129
+ stderr?: string | null;
130
+ returnData?: unknown | null;
131
+ variables?: Record<string, string> | null;
132
+ }
133
+ interface RunsFilter {
134
+ statuses?: readonly string[];
135
+ name?: string;
136
+ platform?: string;
137
+ from?: string;
138
+ to?: string;
139
+ attributePredicates?: AttributePredicate[];
140
+ limit?: number;
141
+ offset?: number;
142
+ }
143
+ interface StatsOptions {
144
+ from?: string;
145
+ to?: string;
146
+ attributePredicates?: AttributePredicate[];
147
+ }
148
+ type InsightsBreakdownDimension = 'test' | 'suite' | 'platform';
149
+ interface InsightsBreakdownRow {
150
+ key: string;
151
+ label: string;
152
+ runs: number;
153
+ passRate: number | null;
154
+ flakeRate: number;
155
+ avgDuration: number;
156
+ passed: number;
157
+ failed: number;
158
+ filePath?: string;
159
+ suiteId?: string;
160
+ }
161
+ /**
162
+ * What a run deletion removed (405-07, D-14 · T-405-27).
163
+ *
164
+ * ROWS ONLY. The two path arrays that stood here fed the router's filesystem cleanup, and both the
165
+ * cleanup and the columns it read are gone: refs are the only artifact addressing now, and the run
166
+ * directory under `.agent-qa/runs/` is the run's own record — the event log and the blobs — which
167
+ * the dashboard never owned and must not delete. `deletedRunIds` survives as a one-element list
168
+ * rather than collapsing to a boolean, because it is the shape the response already publishes.
169
+ */
170
+ interface DeleteRunResult {
171
+ deleted: boolean;
172
+ deletedRunIds: string[];
173
+ }
174
+ interface AttributePredicate {
175
+ key: string;
176
+ value: string;
177
+ mode: 'exact' | 'regex';
178
+ }
179
+ interface AttributeKeySuggestion {
180
+ key: string;
181
+ count: number;
182
+ }
183
+ interface AttributeValueSuggestion {
184
+ value: string;
185
+ count: number;
186
+ }
187
+ declare class UnsupportedUserVersionError extends Error {
188
+ readonly version: number;
189
+ readonly schemaVersion: number;
190
+ constructor(version: number, schemaVersion: number);
191
+ }
192
+ declare class RunsDatabaseVersionRefusedError extends Error {
193
+ readonly dbPath: string;
194
+ readonly version: number;
195
+ constructor(dbPath: string, refusal: UnsupportedUserVersionError);
196
+ }
197
+ declare class DashboardDatabase {
198
+ private db;
199
+ readonly runOwnership: RunOwnership;
200
+ readonly dbPath: string;
201
+ private readonly ownStamps;
202
+ constructor({ dbPath, runOwnership, }?: {
203
+ dbPath?: string;
204
+ runOwnership?: RunOwnershipInput;
205
+ });
206
+ private runMigrations;
207
+ insertRun(run: {
208
+ id?: string;
209
+ name: string;
210
+ filePath?: string;
211
+ status: string;
212
+ duration: number;
213
+ attributes?: RunAttributes;
214
+ environment?: string;
215
+ metadata?: Record<string, unknown>;
216
+ startedAt: string;
217
+ endedAt: string;
218
+ failureSummary?: string;
219
+ platform?: string;
220
+ testFileContent?: string;
221
+ modelName?: string;
222
+ llmProvider?: string;
223
+ testId?: string;
224
+ suiteId?: string;
225
+ }): string;
226
+ claimRunningRow(id: string, fields: {
227
+ name?: string;
228
+ filePath?: string;
229
+ attributes?: RunAttributes;
230
+ startedAt: string;
231
+ platform?: string;
232
+ testId?: string;
233
+ suiteId?: string;
234
+ }): boolean;
235
+ updateRun(id: string, updates: {
236
+ name?: string;
237
+ status?: string;
238
+ duration?: number;
239
+ startedAt?: string;
240
+ endedAt?: string;
241
+ failureSummary?: string;
242
+ errorLog?: string;
243
+ memoryLog?: string;
244
+ attributes?: RunAttributes;
245
+ platform?: string;
246
+ testId?: string;
247
+ suiteId?: string;
248
+ }): void;
249
+ updateRunMetadata(id: string, updates: {
250
+ attributes?: RunAttributes;
251
+ metadata?: Record<string, unknown>;
252
+ platform?: string;
253
+ testFileContent?: string;
254
+ modelName?: string;
255
+ llmProvider?: string;
256
+ }): void;
257
+ insertPendingRun(run: {
258
+ id?: string;
259
+ name: string;
260
+ filePath?: string;
261
+ attributes?: RunAttributes;
262
+ priority?: number;
263
+ platform?: string;
264
+ testFileContent?: string;
265
+ modelName?: string;
266
+ llmProvider?: string;
267
+ metadata?: Record<string, unknown>;
268
+ parallel?: boolean;
269
+ }): string;
270
+ getNextPendingRun(): RunRow | undefined;
271
+ getPendingRuns(): RunRow[];
272
+ updateRunStatus(id: string, status: string): boolean;
273
+ /**
274
+ * Write or refresh ONE member row, keyed by the handle `(run_id, test_index)`.
275
+ *
276
+ * AN UPSERT AND NOT TWO WRITES, because the member row is fed by two events at two times:
277
+ * `testStarted` knows the identity fields and `testFinished` knows the outcome — and D-18
278
+ * requires a member whose stream begins with a terminal event to render anyway, so the second
279
+ * one alone has to be able to create the row.
280
+ *
281
+ * ONLY SUPPLIED FIELDS MOVE. `COALESCE(excluded.x, run_members.x)` on every optional column, so
282
+ * a `testFinished` that carries no name cannot blank the name `testStarted` wrote. `attempts` is
283
+ * excluded from the update entirely: it is the D-17 retry ordinal, owned by
284
+ * `incrementRunMemberAttempts`, and an upsert that reset it would erase the fact the header
285
+ * renders.
286
+ */
287
+ upsertRunMember(member: {
288
+ runId: string;
289
+ testIndex: number;
290
+ testId?: string;
291
+ name?: string;
292
+ filePath?: string;
293
+ suiteId?: string;
294
+ suiteName?: string;
295
+ suiteIndex?: number;
296
+ totalSteps?: number;
297
+ outcome?: string;
298
+ durationMs?: number;
299
+ failureSummary?: string;
300
+ retryCount?: number;
301
+ recording?: RunDetailMember['recording'];
302
+ }): void;
303
+ /**
304
+ * Raise ONE member's attempt ordinal (D-17). A no-op when the member row does not exist yet —
305
+ * a retry scheduled before any identity event is a real ordering, and inventing a row here
306
+ * would put a member on the page that nothing has said anything about.
307
+ */
308
+ incrementRunMemberAttempts(runId: string, testIndex: number): void;
309
+ /**
310
+ * Drop ONE HANDLE's projected step and sub-action rows (D-17).
311
+ *
312
+ * SCOPED TO THE HANDLE, which was the whole change from the v1 run-wide step delete this
313
+ * replaced (deleted outright by 405-07's census): that member wiped every step of the RUN on any
314
+ * member's retry, so in a suite the abandoned attempt of test 3 deleted the completed history of
315
+ * tests 0-2. The member row itself survives — its outcome and its attempt count are facts about
316
+ * the member, not about the attempt. So do its setup and teardown hook rows, which run once per
317
+ * member outside the retry loop; only the attempt's `inline` hook rows go (433-41, CR-01).
318
+ */
319
+ deleteMemberProjection(runId: string, testIndex: number): void;
320
+ /**
321
+ * Write ONE finalized step row. `INSERT OR REPLACE` because a re-run of the same handle re-emits
322
+ * the same `(test_index, step_index)` and the LAST attempt is the one the page renders (D-17).
323
+ *
324
+ * The three token columns stay NULL when `tokenUsage` is absent — never 0 (D-05).
325
+ */
326
+ insertRunStep(step: {
327
+ runId: string;
328
+ testIndex: number;
329
+ stepIndex: number;
330
+ stepText?: string;
331
+ cacheStatus: string;
332
+ verifierVerdict: unknown;
333
+ selfHealOccurred: boolean;
334
+ briefDigest: string;
335
+ status?: string;
336
+ durationMs?: number;
337
+ error?: string;
338
+ tokenUsage?: RunDetailStep['tokenUsage'];
339
+ modelUsage?: RunDetailStep['modelUsage'];
340
+ evidence?: RunDetailStep['evidence'];
341
+ variableSnapshot?: RunDetailStep['variableSnapshot'];
342
+ }): void;
343
+ /**
344
+ * Write ONE sub-action row. The four nested payload fields are stringified VERBATIM — this
345
+ * method never reaches inside `plannedAction`, `grounding`, `result` or `artifactRefs`.
346
+ */
347
+ insertRunSubAction(subAction: {
348
+ runId: string;
349
+ testIndex: number;
350
+ stepIndex: number;
351
+ subActionIndex: number;
352
+ plan: string;
353
+ plannedAction: unknown;
354
+ grounding?: unknown;
355
+ result: unknown;
356
+ screenStateHash: string;
357
+ screenStateHashAfter?: string;
358
+ artifactRefs?: readonly unknown[];
359
+ ariaEvidence?: RunDetailSubAction['ariaEvidence'];
360
+ recordingOffsetMs?: RunDetailSubAction['recordingOffsetMs'];
361
+ }): void;
362
+ /**
363
+ * THE run-detail read (D-03). `undefined` when no `runs` row exists — absence is a RESULT here,
364
+ * and turning it into a typed refusal is the `runs.detail` op's job, so the "not found"
365
+ * vocabulary lives in the function layer rather than in this store.
366
+ *
367
+ * A run that exists and projected nothing answers three empty arrays, which is a DIFFERENT fact
368
+ * and stays a different answer.
369
+ */
370
+ readRunDetailRows(runId: string): RunDetailRows | undefined;
371
+ private appendAttributePredicateConditions;
372
+ private applyRegexAttributePredicates;
373
+ private queryRunsWithAttributePredicates;
374
+ private buildTrendsFromRuns;
375
+ getRuns(opts?: RunsFilter): RunRow[];
376
+ private withMemberSummaries;
377
+ readRunOwner(id: string): RunOwner | undefined;
378
+ listRunningRunOwners(): Array<{
379
+ id: string;
380
+ startedAt?: string;
381
+ owner: RunOwner;
382
+ }>;
383
+ finalizeOwnRun(id: string, updates: {
384
+ status: RunStatus;
385
+ endedAt: string;
386
+ duration?: number;
387
+ failureSummary?: string;
388
+ }): boolean;
389
+ closeOwnRun(id: string, updates: {
390
+ status: RunStatus;
391
+ endedAt: string;
392
+ }): boolean;
393
+ isOwnStamp(id: string, owner: RunOwner): boolean;
394
+ private readOwnStamp;
395
+ finalizeOrphanRun(id: string, owner: RunOwner): boolean;
396
+ getRun(id: string): RunRow | undefined;
397
+ /** Read-only repair for old last-member totals. Complete measured member work is cumulative,
398
+ * including parallel members; it is neither recording length nor run wall-clock time.
399
+ * Incomplete legacy projections retain their stored fallback. Bound queries to returned rows,
400
+ * after pagination, with one aggregate per batch rather than a lookup for every list row.
401
+ */
402
+ private withMemberExecutionDuration;
403
+ listRunAttributeKeys(opts?: {
404
+ limit?: number;
405
+ q?: string;
406
+ }): AttributeKeySuggestion[];
407
+ listRunAttributeValues(key: string, opts?: {
408
+ limit?: number;
409
+ q?: string;
410
+ }): AttributeValueSuggestion[];
411
+ /**
412
+ * Delete ONE run and everything the schema hangs off it (405-07, D-02/D-14 · T-405-27).
413
+ *
414
+ * ONE ID, AND THE CASCADE DOES THE REST. This used to open with a recursive `run_tree` CTE that
415
+ * walked the child rows of a suite parent, then hand-delete their step and trace rows in a
416
+ * transaction. Nothing has written a child row since 402-09 — one `runs` row per run, member
417
+ * identity on `run_members` — so the walk could only ever return the one row it was seeded with,
418
+ * and the hand-deletes named tables this schema no longer has. Every table that references a run
419
+ * declares `ON DELETE CASCADE`, so the single statement below is the whole deletion.
420
+ *
421
+ * AND IT TOUCHES NO FILE. The two path arrays it used to collect fed the router's artifact
422
+ * cleanup; both are gone. The run's directory under `.agent-qa/runs/` — its event log and its
423
+ * content-addressed blobs — is the run's own record and is not the dashboard's to remove.
424
+ */
425
+ deleteRun(id: string): DeleteRunResult;
426
+ insertRunArtifact(input: {
427
+ runId: string;
428
+ kind?: RunArtifactKind;
429
+ payload?: RunArtifactStageInput;
430
+ }): RunArtifactRow;
431
+ stageRunArtifact(runId: string, patch: RunArtifactStageInput): RunArtifactRow;
432
+ finalizeRunArtifact(runId: string, updates?: RunArtifactFinalizeInput): RunArtifactRow;
433
+ getRunArtifact(runId: string): RunArtifactRow | null;
434
+ getRunArtifactBundle(runId: string): RunArtifactBundle;
435
+ /**
436
+ * The runs a self-heal happened on (405-07, D-02).
437
+ *
438
+ * READ OFF `run_steps.self_heal_occurred`, WHICH IS THE SURVIVING HEALING FACT. This scanned the
439
+ * v1 wide step table for `status = 'healed'`; that table had no producer left and its readers
440
+ * went with it. `selfHealOccurred` is the bus payload's own boolean, projected verbatim by the
441
+ * subscriber — so the flake rate below is now computed from what the runner actually reported
442
+ * rather than from a status word a retired projection wrote.
443
+ */
444
+ private getHealedRunIds;
445
+ private summarizeAnalyticsRuns;
446
+ private listRunsForInsights;
447
+ getStats(opts?: StatsOptions): {
448
+ totalRuns: number;
449
+ passed: number;
450
+ failed: number;
451
+ flaky: number;
452
+ flakeRate: number;
453
+ avgDuration: number;
454
+ totalTokens: number;
455
+ runs: {
456
+ date: string;
457
+ passed: number;
458
+ failed: number;
459
+ healed: number;
460
+ duration: number;
461
+ }[];
462
+ memory: {
463
+ runs: number;
464
+ added: number;
465
+ confirmed: number;
466
+ deprecated: number;
467
+ deleted: number;
468
+ curatorTokens: number;
469
+ };
470
+ };
471
+ getCostStats(opts?: StatsOptions): {
472
+ runs: {
473
+ runId: string;
474
+ name: string;
475
+ startedAt: string | null;
476
+ promptTokens: number;
477
+ completionTokens: number;
478
+ totalTokens: number;
479
+ }[];
480
+ totals: {
481
+ promptTokens: number;
482
+ completionTokens: number;
483
+ totalTokens: number;
484
+ };
485
+ };
486
+ insertTokenEvent(event: {
487
+ modelName: string;
488
+ promptTokens: number;
489
+ completionTokens: number;
490
+ source: 'test-run' | 'live-editor';
491
+ }): string;
492
+ getTokenEventStats(opts?: {
493
+ from?: string;
494
+ to?: string;
495
+ }): {
496
+ byModel: {
497
+ date: string;
498
+ model: string;
499
+ promptTokens: number;
500
+ completionTokens: number;
501
+ }[];
502
+ bySource: Record<string, {
503
+ promptTokens: number;
504
+ completionTokens: number;
505
+ }>;
506
+ totals: {
507
+ promptTokens: number;
508
+ completionTokens: number;
509
+ };
510
+ testRunUnmeasuredSteps: number;
511
+ };
512
+ /**
513
+ * The window's settled run ids, newest first, capped at `limit`, with their full count, and the
514
+ * count of the window's runs still in flight (433-30, 433-42). A settled run has started and is
515
+ * neither `pending` nor `running`; every other row in the window is in flight. Ids only: the
516
+ * Insights knowledge-change read dispatches each through the agent host.
517
+ */
518
+ listInsightsWindowRunIds(opts: {
519
+ from?: string;
520
+ to?: string;
521
+ limit: number;
522
+ }): {
523
+ runIds: string[];
524
+ total: number;
525
+ inFlight: number;
526
+ };
527
+ getRunsByTest(test: TestRef, opts?: {
528
+ limit?: number;
529
+ offset?: number;
530
+ attributePredicates?: AttributePredicate[];
531
+ }): RunRow[];
532
+ getRunsByTestCount(test: TestRef, opts?: {
533
+ attributePredicates?: AttributePredicate[];
534
+ }): number;
535
+ getTestTrends(test: TestRef, opts?: {
536
+ from?: string;
537
+ attributePredicates?: AttributePredicate[];
538
+ }): {
539
+ daily: {
540
+ date: string;
541
+ passed: number;
542
+ failed: number;
543
+ total: number;
544
+ avgDuration: number;
545
+ }[];
546
+ passRate: number | null;
547
+ totalRuns: number;
548
+ avgDuration: number;
549
+ };
550
+ private withMemberOutcome;
551
+ getRunsBySuiteId(suiteId: string, opts?: {
552
+ limit?: number;
553
+ offset?: number;
554
+ attributePredicates?: AttributePredicate[];
555
+ }): RunRow[];
556
+ getRunsBySuiteIdCount(suiteId: string, opts?: {
557
+ attributePredicates?: AttributePredicate[];
558
+ }): number;
559
+ getSuiteTrends(suiteId: string, opts?: {
560
+ from?: string;
561
+ attributePredicates?: AttributePredicate[];
562
+ }): {
563
+ daily: {
564
+ date: string;
565
+ passed: number;
566
+ failed: number;
567
+ total: number;
568
+ avgDuration: number;
569
+ }[];
570
+ passRate: number | null;
571
+ totalRuns: number;
572
+ avgDuration: number;
573
+ };
574
+ getInsightsBreakdown(dimension: InsightsBreakdownDimension, opts?: StatsOptions & {
575
+ limit?: number;
576
+ }): InsightsBreakdownRow[];
577
+ getFlakyTests(opts?: {
578
+ minRuns?: number;
579
+ limit?: number;
580
+ }): {
581
+ name: string;
582
+ filePath: string | null;
583
+ totalRuns: number;
584
+ passRate: number;
585
+ flakyScore: number;
586
+ }[];
587
+ insertExecutionLog(data: InsertExecutionLogInput): void;
588
+ getExecutionLogs(filter: {
589
+ runId?: string;
590
+ stepId?: string;
591
+ type?: string;
592
+ }): ExecutionLogRow[];
593
+ close(): void;
594
+ private parseRunAttributes;
595
+ private mapRunRow;
596
+ private mapRunArtifactRow;
597
+ }
598
+
599
+ interface ExecuteOptions {
600
+ runId: string;
601
+ args: string[];
602
+ source: string;
603
+ attributes?: RunAttributes;
604
+ timeout?: number;
605
+ /**
606
+ * The Appium lease URL for a mobile run, AS DATA (401-07, D-01/D-12).
607
+ *
608
+ * The bundled Appium spawn is design-of-record and stays exactly where it is; what changed is the
609
+ * DELIVERY. It used to ride `AGENT_QA_APPIUM_URL` in a child's environment, and a child env is
610
+ * something only a child has. The run happens in this process now, so the lease travels as an
611
+ * argument and is handed to the run through an injected resolver.
612
+ */
613
+ appiumUrl?: string;
614
+ }
615
+ /**
616
+ * A run has reached a terminal state (401-07, D-04/D-12).
617
+ *
618
+ * THE TYPED SUCCESSOR TO THE CHILD'S `close` EVENT. The host reconciles a db row still reading
619
+ * `running`, releases the run's Appium lease and frees the queue slot off this — exactly what it did
620
+ * off the process exit — but the status is now the bus's own ground truth and the `details` carry
621
+ * the failure's REASON rather than a scraped tail of stderr.
622
+ *
623
+ * `'run-settled'` is now the WHOLE of this emitter's contract. A second event carried the retired
624
+ * live feed's frames and went with it (405-07, D-19).
625
+ */
626
+ interface RunSettledDetails {
627
+ /**
628
+ * A human-readable reason when the run did not simply finish. 2026-09-30, 432-09 (D-15): a
629
+ * refusal is this plain reason. No remedy list rides the settle; nothing asks the reader to act.
630
+ */
631
+ readonly reason?: string;
632
+ /**
633
+ * The run NEVER BEGAN — the gate or the host refused it (401-18, G-401-16).
634
+ *
635
+ * A FACT, NOT AN INFERENCE. `settle` has always known this — a rejection with no started session
636
+ * is a denial, a refused request field or an input error — and threw the knowledge away, leaving
637
+ * the host's reconciliation to guess from a regex over the diagnostic lines. It guessed
638
+ * `Process exited before test completed`, which is a sentence about a process, for a run where
639
+ * nothing was ever started to exit.
640
+ *
641
+ * DELIBERATELY A BOOLEAN AND NOT A CLASS. Which refusal it was — `fail-closed-denied`, `blocked`,
642
+ * `session-pin-denied`, an unrecognised request field, a plain input error — is not something the
643
+ * reconciliation branches on, and a prose name for it here would be the same defect one layer
644
+ * down. If a class is ever needed it arrives as a closed-set value owned by `schema`.
645
+ *
646
+ * ABSENT ON A RUN THAT STARTED, including one that started and then failed. That asymmetry is the
647
+ * scope guarantee: every other arm of the reconciliation reads exactly what it read before.
648
+ */
649
+ readonly refusedBeforeStart?: true;
650
+ /**
651
+ * The refusal said without naming any surface's affordance (401-18, G-401-16).
652
+ *
653
+ * The gate composed two sentences per decision: the canonical one, which named the session trust
654
+ * flag and landed in the execution-audit row, and the neutral one, which stops at the classification and the outcome.
655
+ * A browser reader was being handed the first, so the record's only account of why a run never
656
+ * happened told them to pass a flag to a command they were not running.
657
+ *
658
+ * READ STRUCTURALLY OFF THE REJECTION, and ABSENT where the
659
+ * rejection carries no neutral form — an input error is not a gate decision and has no second
660
+ * phrasing. The consumer then falls back to the rejection's own message, which for those refusals
661
+ * names no surface's affordance either.
662
+ */
663
+ readonly neutralReason?: string;
664
+ /** Diagnostic lines — a rejection's own message. The typed successor to scraped stderr. */
665
+ readonly output: readonly string[];
666
+ }
667
+ type RunSettledStatus = 'completed' | 'failed' | 'cancelled' | 'timeout';
668
+ /** A trigger's request as the queue would carry it, before anything is recorded (433-34). */
669
+ interface PreflightOptions {
670
+ readonly args: string[];
671
+ readonly source: 'dashboard' | 'suite';
672
+ }
673
+ interface ExecutionBackend extends EventEmitter {
674
+ execute(opts: ExecuteOptions): void;
675
+ /**
676
+ * Ask the run core whether this request may start, before a row is minted or anything enqueued
677
+ * (433-34, G-433-10).
678
+ *
679
+ * The composing host answers with the core's own production rule over the members' declared
680
+ * targets; this package decides nothing about production. A refusal rejects with the core's
681
+ * denial, which carries `neutralReason`. Absent on a backend whose host composes no preflight.
682
+ */
683
+ preflight?(opts: PreflightOptions): Promise<void>;
684
+ kill(runId: string): boolean;
685
+ killAll(): void;
686
+ /**
687
+ * The runs this backend currently has in flight, as `GET /api/execution/active` serves them.
688
+ *
689
+ * `testName` IS ON THE INTERFACE (405-07, D-19). It used to be produced by scanning a per-run
690
+ * event buffer for a `testStarted` frame — a read that only the concrete class could perform,
691
+ * through a member the interface did not declare. It is now recorded on the run handle by the
692
+ * backend's own bus subscription, so the field is part of the contract every implementation
693
+ * answers rather than a property of one implementation's internal storage.
694
+ *
695
+ * ABSENT MEANS NO MEMBER HAS STARTED YET, which is a real state on a run between its start and
696
+ * its first `testStarted`; it is never filled with the run id or a placeholder.
697
+ */
698
+ getActiveExecutions(): {
699
+ runId: string;
700
+ status: string;
701
+ startedAt: string;
702
+ duration: number;
703
+ testName?: string;
704
+ }[];
705
+ }
706
+
707
+ /** The frame-facing slice of a `RunSession`, for an editor session. */
708
+ interface LiveEditorAttachment {
709
+ streamCapabilityOf(handle: number): LiveStreamDeclaration;
710
+ subscribeFrames(handle: number, onFrame: (frame: LiveFrame) => void, onEnd?: (reason: LiveFrameEndReason) => void): LiveFrameSubscription | undefined;
711
+ /** Settles when the editor session is over. The frame socket then says `sessionEnded`. */
712
+ readonly ended: Promise<void>;
713
+ }
714
+ interface LiveEditorSubjects {
715
+ register(sessionId: string, attachment: LiveEditorAttachment): void;
716
+ unregister(sessionId: string): void;
717
+ attach(sessionId: string): LiveEditorAttachment | undefined;
718
+ /** The session's declared stream (its handle 0), or undefined when no such session is held. */
719
+ capabilityOf(sessionId: string): LiveStreamDeclaration | undefined;
720
+ }
721
+ declare function createLiveEditorSubjects(): LiveEditorSubjects;
722
+
723
+ type ModelAdapter = 'openai-responses' | 'anthropic-messages';
724
+ interface DashboardLLMConfig {
725
+ provider: string;
726
+ model: string;
727
+ baseURL?: string;
728
+ providerHeaders?: Record<string, string>;
729
+ apiKey?: string;
730
+ authToken?: string;
731
+ modelAdapter?: ModelAdapter;
732
+ }
733
+
734
+ /**
735
+ * The composed function registry, OPAQUE to this package.
736
+ *
737
+ * Deliberately not structurally described. The registry's real type lives in `agent`, this package
738
+ * may not name it, and a hand-written mirror of its shape here would be a second declaration of
739
+ * somebody else's contract — stale on the day `agent` changes it, and silently so, because nothing
740
+ * would compare the two. What this package does with the value is hand it straight back to the
741
+ * functions below, which is all an opaque handle has to support.
742
+ */
743
+ type AgentRegistryHandle = object;
744
+ /**
745
+ * The whole of what the run-detail and mark routes may ask of the process that started them.
746
+ *
747
+ * A single object rather than loose options, because the members are one decision: they all come
748
+ * from ONE composition, and a server holding an invoke from one host and a registry from another is
749
+ * a state nobody should be able to construct.
750
+ */
751
+ interface AgentHost {
752
+ /** The composed registry every call goes through. Opaque here. */
753
+ readonly registry: AgentRegistryHandle;
754
+ /**
755
+ * The caller this door dispatches as, MINTED by the composition root and never by this package.
756
+ *
757
+ * Attribution and grants both. The composer derives it from the CLI chat door's own mint with
758
+ * only the surface name changed (D-08/Pitfall 9) — a hand-typed scope list here would be the
759
+ * drift vector the parity legs exist to catch, and this package could not type-check one anyway.
760
+ */
761
+ readonly caller: FunctionCaller;
762
+ /**
763
+ * The agent package's `runs.postprocessing` id (404 D-16/D-18).
764
+ *
765
+ * SUPPLIED BY THE COMPOSITION, NEVER SPELLED HERE. The post-processing read route dispatches
766
+ * chip truth through this door, and the op id is the agent package's constant: a string literal
767
+ * in this package would be a second declaration of a capability's name, free to drift from the
768
+ * spec that answers to it. This package also may not import `@vostride/agent-qa-agent`
769
+ * (validate:arch), so there is nowhere here the real constant could come from.
770
+ */
771
+ readonly runsPostProcessingFunctionId: string;
772
+ /**
773
+ * The agent package's `runs.events` id, on the line above's footing (404 D-22).
774
+ *
775
+ * A SECOND ID BECAUSE THE READ ROUTE JOINS TWO FACTS from two different records: the chain's
776
+ * chips live in the durable job store, and the run's POSTURE — who was going to drain it — lives
777
+ * on the `runFinished` line of the run's own event log, beside the last per-stage failure text.
778
+ * Reading the log any other way would mean this package opening a file the function layer owns.
779
+ */
780
+ readonly runsEventsFunctionId: string;
781
+ /**
782
+ * The agent package's `runs.detail` id, on `runsPostProcessingFunctionId`'s exact footing (405
783
+ * D-03).
784
+ *
785
+ * SUPPLIED BY THE COMPOSITION, NEVER SPELLED HERE, for the reason its two neighbours are: the
786
+ * op id is the agent package's constant, a string literal in this package would be a second
787
+ * declaration of a capability's name free to drift from the spec that answers to it, and this
788
+ * package may not import `@vostride/agent-qa-agent` at all (validate:arch) — so there is nowhere
789
+ * here the real constant could come from.
790
+ *
791
+ * WHAT IT REPLACES is worth naming: run detail used to be assembled by reading SQLite from
792
+ * inside the router. Routing it through this id is what makes the same durable read available
793
+ * to the CLI, MCP and chat instead of to the browser alone.
794
+ */
795
+ readonly runsDetailFunctionId: string;
796
+ /** The evidence op identity supplied by the same composition as the detail reader. */
797
+ readonly runsEvidenceFunctionId: string;
798
+ /**
799
+ * The agent package's `runs.artifact` id, on `runsDetailFunctionId`'s exact footing (405 D-13).
800
+ *
801
+ * SUPPLIED BY THE COMPOSITION, NEVER SPELLED HERE, for the reason its four neighbours are — and
802
+ * for one more that is specific to this route: the artifact arm serves BYTES, and the alternative
803
+ * to dispatching them through the function layer is this package constructing the evidence store
804
+ * and reading a file. That would be a private store read no roster row covers and no audit sink
805
+ * sees, on a path whose two inputs both arrive from a URL. Routing it through an id is what makes
806
+ * the containment rule the store's own and the capability one the CLI, MCP and 408's embed reach
807
+ * the same way.
808
+ */
809
+ readonly runsArtifactFunctionId: string;
810
+ /**
811
+ * The agent package's `analysis.get` id, on `runsArtifactFunctionId`'s exact footing (406 D-04).
812
+ *
813
+ * SUPPLIED BY THE COMPOSITION, NEVER SPELLED HERE, for the reason its five neighbours are: the
814
+ * op id is the agent package's constant, a string literal in this package would be a second
815
+ * declaration of a capability's name free to drift from the spec that answers to it, and this
816
+ * package may not import `@vostride/agent-qa-agent` at all (validate:arch) — so there is nowhere
817
+ * here the real constant could come from.
818
+ *
819
+ * WHAT IT SERVES is the failure view: the resolved verdicts and rendered sentences a person
820
+ * reads inside each failed member's group, on BOTH doors through one adapter. `analysis.get` is
821
+ * the single producer of those sentences (389 D-11), so routing the page's read through this id
822
+ * is what keeps the browser, the CLI, chat and 408's embed reading one answer.
823
+ */
824
+ readonly analysisGetFunctionId: string;
825
+ /**
826
+ * The agent package's `runs.knowledge` id, on `analysisGetFunctionId`'s exact footing
827
+ * (2026-08-29, 407-01 D-05).
828
+ *
829
+ * THE SIXTH ID THE RUN-SERVICE ROUTES DISPATCH, supplied by the composition and never spelled
830
+ * here, for the reason its five neighbours are: the op id is the agent package's constant, a
831
+ * string literal in this package would be a second declaration of a capability's name free to
832
+ * drift from the spec that answers to it, and this package may not import
833
+ * `@vostride/agent-qa-agent` at all (validate:arch) — so there is nowhere here the real constant
834
+ * could come from.
835
+ *
836
+ * WHAT IT SERVES is the Knowledge panel: what a run USED (the brief channels served per step,
837
+ * the serve-time trace behind each, procedure replay and hint use, the recorded retrieval rung)
838
+ * and what it LEARNED (curation counts, the record changes attributable to it, canary outcomes).
839
+ * `runs.knowledge` is the single producer of that whole view (407 D-05), so routing the page's
840
+ * read through this id is what keeps the browser, the CLI, MCP and chat reading one answer
841
+ * instead of a page composing it from N per-step calls that can disagree.
842
+ */
843
+ readonly runsKnowledgeFunctionId: string;
844
+ /** 2026-09-08, 419 D-13: the eighth op id arrives as data, on the knowledge id's footing. */
845
+ readonly runsMarkFunctionId: string;
846
+ /** 2026-09-15, 423-21 D-18: the marks-history read id, supplied as data like its neighbours. */
847
+ readonly runsMarksHistoryFunctionId: string;
848
+ /** 2026-09-15, 423-21 D-18: the read-tier mark consequence preview id. */
849
+ readonly runsMarkPreviewFunctionId: string;
850
+ /**
851
+ * `invokeFunction`, bound by the composer. THE one door call.
852
+ *
853
+ * `input` IS `unknown` (404-05). Every op this seam carries has its own strict input schema.
854
+ * Narrowing here would mean this package describing other packages' input contracts — and the
855
+ * real `invokeFunction` has always taken `unknown`, because the op's own schema is what validates
856
+ * it, once, inside the invocation.
857
+ */
858
+ invokeFunction(registry: AgentRegistryHandle, functionId: string, input: unknown, caller: FunctionCaller): Promise<unknown>;
859
+ }
860
+
861
+ /** One viewer's callback. Runs on the EMITTER'S thread — see `publish`. */
862
+ type PostProcessingListener = (envelope: RunEventEnvelope) => void;
863
+ interface PostProcessingRelay {
864
+ /**
865
+ * Hand one chip envelope to whoever is watching THAT run.
866
+ *
867
+ * NEVER THROWS. The caller is the chip emitter, mid-`append`, immediately after the line is on
868
+ * disk: the trail is the record and a viewer's fault is not the emitter's problem (the run bus's
869
+ * own subscriber-fault discipline, applied one layer out). A listener that throws is skipped for
870
+ * this delivery and the remaining listeners still receive it — dropping the rest would let one
871
+ * broken page silence every other page watching the same run.
872
+ */
873
+ publish(envelope: RunEventEnvelope): void;
874
+ /** Watch one run. Returns its own disposer, which is idempotent. */
875
+ subscribe(runId: string, listener: PostProcessingListener): () => void;
876
+ /** How many listeners that run has — so a host can assert a closed stream released its own. */
877
+ listenerCount(runId: string): number;
878
+ }
879
+ declare function createPostProcessingRelay(): PostProcessingRelay;
880
+
881
+ /**
882
+ * What starting a run needs from the composition, as data rather than as a live handle.
883
+ *
884
+ * 2026-10-01 (432-21, D-12): the matched token's `trustScope` and `clientId` are deleted with the
885
+ * token. A start carries the caller's pattern set and nothing about who the caller is.
886
+ */
887
+ interface RunServiceStartInput {
888
+ /** The pattern set the caller asked for; absent means the configured default set. */
889
+ readonly patterns?: readonly string[];
890
+ }
891
+ /**
892
+ * Everything this door reaches the world through (D-02).
893
+ *
894
+ * ALL ARE INJECTED, and the injection is what makes the module mountable: the implementations name
895
+ * `invokeFunction`, `runs.start` and a composed local host, all of it vocabulary this package may
896
+ * not import. `InProcessRunStarter` is the shape precedent one directory over. 2026-10-01 (432-21,
897
+ * D-12): `verifyBearer` is deleted; the same-site guard needs no port.
898
+ */
899
+ interface RunServicePorts {
900
+ /**
901
+ * Start a run and return the acknowledgment.
902
+ *
903
+ * REJECTION IS NOT "the run failed" — a failing run still acknowledges. A rejection is a denial,
904
+ * a refused request field, an input error or a crash.
905
+ */
906
+ invokeRunStart(input: RunServiceStartInput): Promise<RunStartAcknowledgement>;
907
+ /** The started run's session, or nothing if this process never started that run. */
908
+ attach(runId: string): RunSession | undefined;
909
+ /** The measured readiness facts `/ok` reports (D-15). */
910
+ readiness(): RunServiceReadiness;
911
+ /**
912
+ * Mint a live-view URL for one handle of a run (403 D-03).
913
+ *
914
+ * REQUIRED, NOT OPTIONAL, and that is a decision rather than an oversight. An optional member is
915
+ * a member a composition can forget, and the failure mode of forgetting it here is a 404 that
916
+ * reads exactly like "no such route" — which is what a door with no live view is indistinguishable
917
+ * from. A composition that cannot mint supplies a function that REJECTS with a
918
+ * `LiveViewUnavailableError`-named error, which the arm maps to 503 `live_view_unavailable`: an
919
+ * honest host fact, said out loud, once, in the vocabulary the local door already uses.
920
+ *
921
+ * The minted token's own subject (run, handle) and expiry are checked by the live-view validator
922
+ * independently of who asked (D-13). 2026-10-01 (432-21, D-12): the bearer's trust scope this mint
923
+ * used to run under is deleted; the composition supplies the one caller it runs under.
924
+ */
925
+ mintLiveUrl(input: MintLiveUrlInput): Promise<RunsLiveUrlResult>;
926
+ /**
927
+ * The injected agent host the two READ rows dispatch through (405-05, D-12/D-13).
928
+ *
929
+ * OPTIONAL, unlike `mintLiveUrl` above, and the asymmetry is deliberate rather than an
930
+ * oversight. `mintLiveUrl` is required because a composition that forgot it would answer a 404
931
+ * indistinguishable from an unserved path — but the argument runs the other way here: a host
932
+ * genuinely composed without a function registry (a bare mount, this file's own fixtures) has no
933
+ * capability to reach, and the honest answer is the SAME 503 `agent_host_unavailable` the local
934
+ * door already gives for exactly this state. One word, one meaning, both doors.
935
+ *
936
+ * It is a HANDLE and nothing else — this package may not name the agent packages, so what
937
+ * crosses is the seam `chat/host.ts` declares and the op ids the composition supplies as data.
938
+ */
939
+ readonly agentHost?: AgentHost;
940
+ }
941
+ /** What minting one `liveUrl` needs from a request, as data: the subject, and nothing else. */
942
+ interface MintLiveUrlInput {
943
+ readonly runId: string;
944
+ readonly handle: number;
945
+ }
946
+ /** Which handler a table row reaches. A closed union, so a row with no arm is a type error. */
947
+ type RunServiceRouteHandlerName = 'start' | 'events' | 'liveUrl' | 'detail' | 'evidence' | 'artifact' | 'analysis' | 'marks' | 'markPreview' | 'ok';
948
+ interface RunServiceRouteRow {
949
+ readonly method: 'GET' | 'POST';
950
+ /** The literal path, or — for a templated route — its `:param` shape. */
951
+ readonly path: string;
952
+ readonly handler: RunServiceRouteHandlerName;
953
+ /** Present only on a templated route; the matcher uses it instead of `path`. */
954
+ readonly pattern?: RegExp;
955
+ }
956
+ declare const RUN_SERVICE_START_ROUTE = "/run";
957
+ declare const RUN_SERVICE_EVENTS_TEMPLATE = "/run/:runId/events";
958
+ /**
959
+ * The MINT (403 D-03, Q3) — the remote half of the pair whose local half is
960
+ * `GET /api/runs/:runId/live-url`.
961
+ *
962
+ * SAME SHAPE AS THE EVENTS ROUTE ONE LINE UP, deliberately: a caller addresses a run it started by
963
+ * id and asks this door for one thing about it per route. Since 432-21 the two doors differ in
964
+ * nothing but the path — the same op, the same caller shape, the same result and fragment carriage.
965
+ */
966
+ declare const RUN_SERVICE_LIVE_URL_TEMPLATE = "/run/:runId/live-url";
967
+ /**
968
+ * Readiness (D-15) — behind the same-site guard, like every other route on this door.
969
+ *
970
+ * An `/ok` that answered before the guard would tell a page on another origin that this process is
971
+ * an agent-qa run service and that it is up. (Until 432-21 the guard here was the bearer verdict,
972
+ * and the preflight was liveness and early token validation in one round-trip.)
973
+ */
974
+ /**
975
+ * The durable run-detail READ (405-05, D-12 / 402 D-16) — the remote twin of the local door's
976
+ * `GET /api/runs/:id/detail`.
977
+ *
978
+ * IT DISPATCHES THROUGH THE SAME ADAPTER THE LOCAL DOOR CALLS. `serveRunDetailRead` is one
979
+ * function, so the two doors cannot come to disagree about what a run's projection is; a second
980
+ * spelling here would agree on the day it was written and be a wire-contract fork thereafter.
981
+ *
982
+ * THIS ROW IS MASKED, and deliberately. It is a machine wire like `/run/:runId/events` one line up
983
+ * — its consumer (the CLI's own `emitHuman`, 408's embed) is where a person meets the text, and
984
+ * that consumer is the display boundary. The alternative would restore declared secrets onto a
985
+ * machine wire, extending the disclosure surface to every client and every intermediary, which is
986
+ * not what "the browser is never the boundary" (400-06) asks for. 405-10
987
+ * applies the unmask seam to the LOCAL door only and enrols this row on the masked side.
988
+ */
989
+ declare const RUN_SERVICE_DETAIL_TEMPLATE = "/run/:runId/detail";
990
+ declare const RUN_SERVICE_EVIDENCE_TEMPLATE = "/run/:runId/evidence";
991
+ /**
992
+ * The ref-resolving artifact READ (405-05, D-12 / 402 D-16) — the remote twin of the local door's
993
+ * `GET /api/runs/:runId/artifacts/:sha`, and the row 402 D-16 named by anticipation.
994
+ *
995
+ * TWO CAPTURES, `[^/]+` ON BOTH. The runId is decoded and the sha is not: a content address is 64
996
+ * characters from `[0-9a-f]`, none of which percent-encoding can represent differently, so a sha
997
+ * segment that needed decoding is already not one.
998
+ *
999
+ * MASKED, LIKE ITS SIBLING AND MORE SO: these are BYTES. The unmask seam never touches an artifact
1000
+ * body on any door (D-09) — a screenshot is not text and there is nothing in it to restore.
1001
+ */
1002
+ declare const RUN_SERVICE_ARTIFACT_TEMPLATE = "/run/:runId/artifacts/:sha";
1003
+ /**
1004
+ * The run-ANALYSIS read (406-01, D-04) — the remote twin of the local door's
1005
+ * `GET /api/runs/:id/analysis`.
1006
+ *
1007
+ * IT DISPATCHES THROUGH THE SAME ADAPTER THE LOCAL DOOR CALLS, for the reason its detail sibling
1008
+ * does: `serveRunAnalysisRead` is one function, so the two doors cannot come to disagree about
1009
+ * what a run's analysis is.
1010
+ *
1011
+ * THIS ROW IS MASKED, AND FOR THE REASON ITS SIBLING IS. The verdict sentence quotes the failing
1012
+ * step's recorded error, which carries declared placeholders at rest; restoring them onto a
1013
+ * machine wire would hand them to every client and every intermediary, and this row's
1014
+ * consumer — the CLI's own emit, 408's embed — is where a person meets the text. The dispatch
1015
+ * below passes no `boundary`, and the option's `'machine'` default is what keeps it masked.
1016
+ */
1017
+ declare const RUN_SERVICE_ANALYSIS_TEMPLATE = "/run/:runId/analysis";
1018
+ /** 2026-09-15, 423-21 D-18: runs.marks.history and runs.mark.preview behind the same guard. */
1019
+ declare const RUN_SERVICE_MARKS_TEMPLATE = "/run/:runId/marks";
1020
+ declare const RUN_SERVICE_MARK_PREVIEW_TEMPLATE = "/run/:runId/mark-preview";
1021
+ declare const RUN_SERVICE_OK_ROUTE = "/ok";
1022
+ /**
1023
+ * EVERY PATH THIS DOOR ANSWERS, as data the dispatch reads (the chat door's own idiom).
1024
+ *
1025
+ * WHY A TABLE AND NOT THREE ARMS. The same-site guard below is applied to the REQUEST, above this
1026
+ * table, so a route added tomorrow inherits it by construction. The table is what makes that
1027
+ * inheritance AUDITABLE from outside: because dispatch reads it, a route not in it is unreachable,
1028
+ * so a census cell can enumerate this array and drive every row cross-site — and a newly added
1029
+ * route cannot escape that corpus by nobody noticing it.
1030
+ */
1031
+ declare const RUN_SERVICE_ROUTE_TABLE: readonly RunServiceRouteRow[];
1032
+ /** What a matched row carries: the row itself and whatever its pattern captured. */
1033
+ interface RunServiceRouteMatch {
1034
+ readonly row: RunServiceRouteRow;
1035
+ /** The raw, still-ENCODED capture groups. Decoding is the handler's business, and it can throw. */
1036
+ readonly params: readonly string[];
1037
+ }
1038
+ /** Resolve a request to a table row, or to nothing. The dispatch's only route lookup. */
1039
+ declare function matchRunServiceRoute(method: string | undefined, path: string): RunServiceRouteMatch | undefined;
1040
+ /**
1041
+ * Whether a path belongs to this door — the MOUNT's whole question, and method-blind on purpose.
1042
+ *
1043
+ * A host consults this before its own routing so that every request to a service path, including
1044
+ * an `OPTIONS` preflight and a method this table has no row for, is answered by this door rather
1045
+ * than by the host's generic handling. That is what keeps the host's wildcard CORS preflight and
1046
+ * its SPA fallback away from this surface.
1047
+ *
1048
+ * DERIVED FROM THE TABLE, never a second list: the prefixes are read off the rows, so a route
1049
+ * added above is mounted the same commit.
1050
+ */
1051
+ declare function ownsRunServicePath(path: string): boolean;
1052
+ interface RunServiceRouter {
1053
+ (req: IncomingMessage, res: ServerResponse): void;
1054
+ /** Live SSE connections — so a host can assert this door leaks none on teardown. */
1055
+ openStreamCount(): number;
1056
+ }
1057
+ /**
1058
+ * Where a stream request asked to resume from. Three arms, and the count is the contract.
1059
+ *
1060
+ * `start` is "the caller has observed nothing", `after` carries the last seq it DID observe, and
1061
+ * `invalid` is a fact about the caller's own input. There is deliberately no fourth arm meaning
1062
+ * "unparseable, serve them from the beginning": silently restarting a resume at seq 1 would
1063
+ * re-deliver every envelope the caller already has, and silently serving from the floor would hide
1064
+ * a gap — the two answers D-13 refuses by name.
1065
+ */
1066
+ type ResumePoint = {
1067
+ readonly kind: 'start';
1068
+ } | {
1069
+ readonly kind: 'after';
1070
+ readonly seq: number;
1071
+ } | {
1072
+ readonly kind: 'invalid';
1073
+ };
1074
+ /**
1075
+ * The DENIAL this rejection is, if it is one — read structurally, never by class (402-06, T-402-22).
1076
+ *
1077
+ * ON `dashboardNeutralReasonOf`'S EXACT PRECEDENT (`execution/in-process-backend.ts`), and for the
1078
+ * same architectural reason: the rejection is a `RunDeniedError` minted in `agent-local`, and this
1079
+ * package may not import `agent`, `agent-local` or `safety` — `pnpm validate:arch` is the
1080
+ * mechanical proof.
1081
+ *
1082
+ * THE DISCRIMINATOR IS `neutralReason`, DELIBERATELY. Only a run denial carries it: an
1083
+ * unrecognised request field, an input error and "the host was asked to start a run before it was
1084
+ * composed" carry none, and those are exactly the rejections that must stay a 500 with no words on
1085
+ * them. Keying on the class NAME would be a string compare against another package's private
1086
+ * vocabulary.
1087
+ *
1088
+ * 2026-10-01 (432-19, D-15): there is no remedy list to validate or project. The production refusal
1089
+ * never carried one, and the remedy vocabulary left with the execution gate.
1090
+ */
1091
+ declare function runDenialOf(error: unknown): RunServiceDenialBody | undefined;
1092
+ declare function createRunServiceRoutes(ports: RunServicePorts): RunServiceRouter;
1093
+
1094
+ interface StartServerOptions {
1095
+ db: DashboardDatabase;
1096
+ port?: number;
1097
+ /**
1098
+ * The interface this server binds (402-05, D-07 — INVK-03).
1099
+ *
1100
+ * DEFAULTS TO LOOPBACK, and a non-loopback value is REFUSED before anything listens — with no
1101
+ * exemption. This is BREAKING for anyone who relied on the previous `server.listen(port)`, which
1102
+ * bound every interface, and there is no migration flag: the v2 ethos ships no back-compat shim
1103
+ * for a hole. Remote access goes through an authenticating proxy (432 D-13's forward note).
1104
+ */
1105
+ host?: string;
1106
+ uiDir?: string;
1107
+ /**
1108
+ * Where this workspace writes run artifacts (`services.dashboard.artifactsDir`).
1109
+ *
1110
+ * ACCEPTED AND NOT READ BY THIS SERVER SINCE 405-07 (D-14). It used to be resolved into a
1111
+ * screenshots and a videos root that two path-serving routes and the run-deletion sweep read.
1112
+ * Refs are the only artifact addressing now, and the bytes are reached through the
1113
+ * `runs.artifact` op over the evidence store's own layout. The option stays declared because it
1114
+ * is a user-facing config key the RECORDING path still writes under; the dashboard simply no
1115
+ * longer serves or deletes anything by path.
1116
+ */
1117
+ artifactsDir?: string;
1118
+ workspacePaths?: ResolvedWorkspacePaths;
1119
+ configPath?: string;
1120
+ declaredSecretMask: MaskFn;
1121
+ llmConfig?: DashboardLLMConfig & {
1122
+ vision?: {
1123
+ screenshotSize?: number;
1124
+ effectiveResolution?: number;
1125
+ };
1126
+ };
1127
+ authFetch?: typeof globalThis.fetch;
1128
+ /**
1129
+ * The composed agent host the chat door dispatches through (400-01, D-01).
1130
+ *
1131
+ * INJECTED by whoever starts this server — in production `agent-qa dashboard`, which composes the
1132
+ * SAME port set `agent-qa chat` does and hands it over, so the two interactive doors have one
1133
+ * answer to "what can a conversation do" (D-08). Absent leaves the chat routes answering 501 by
1134
+ * name: the door exists, this process was started without a host. See `chat/host.ts`.
1135
+ */
1136
+ agentHost?: AgentHost;
1137
+ /**
1138
+ * The backend every dashboard run executes through (401-07, D-01/D-09/D-10 — INVK-01).
1139
+ *
1140
+ * INJECTED, ON `agentHost`'s OWN IDIOM AND FOR ITS OWN REASON. The implementation that starts a
1141
+ * run names `runs.start`, `invokeFunction` and the composed local host — all of it
1142
+ * `@vostride/agent-qa-agent*` vocabulary this package may not import (validate:arch). So
1143
+ * `agent-qa dashboard` composes it in the process that runs this server and hands it over, which
1144
+ * is what makes the run button one more caller of the op chat already uses rather than a
1145
+ * dashboard carve-out around the gates.
1146
+ *
1147
+ * ABSENT DEGRADES BY NAME, exactly as it did when the CLI binary could not be resolved: the
1148
+ * execution routes answer 503 and the queue's execute handler does nothing. A dashboard started
1149
+ * without a run door is a dashboard that cannot run tests, and it says so.
1150
+ */
1151
+ executionBackend?: ExecutionBackend;
1152
+ /**
1153
+ * The run service's ports (402-01, D-02/D-06 — INVK-03).
1154
+ *
1155
+ * INJECTED, ON `agentHost`'s AND `executionBackend`'s OWN IDIOM AND FOR THEIR OWN REASON. The
1156
+ * implementations name `runs.start`, `invokeFunction` and the composed local host — all of it
1157
+ * vocabulary this package may not import (validate:arch). So `agent-qa dashboard` composes them
1158
+ * in the process that runs this server and hands them over, which is what makes the run service
1159
+ * a mountable module rather than a fourth process.
1160
+ *
1161
+ * ABSENT MEANS THE SERVICE IS NOT MOUNTED, and `/run` and its neighbours reach whatever the
1162
+ * static/SPA handling below does with an unknown path. 2026-10-01 (432-21, D-12): `agent-qa
1163
+ * dashboard` supplies it unconditionally now. It used to mount only when a run-service credential
1164
+ * was declared, and those credentials are deleted; the door is guarded by the same-site check
1165
+ * and the loopback bind like every other door here. Absent is a bare in-process mount's posture.
1166
+ */
1167
+ runService?: RunServicePorts;
1168
+ /**
1169
+ * The run service's readiness predicate, served WITHOUT a token on the local dashboard API
1170
+ * (402-05, D-15 — `GET /api/run-readiness`).
1171
+ *
1172
+ * SEPARATE FROM `runService` ABOVE AND NOT DERIVED FROM IT, because a mount without the service
1173
+ * still has a browser that needs an honest readiness answer rather than a 404. `agent-qa
1174
+ * dashboard` supplies the very same function object it put on `runService.readiness`: one
1175
+ * predicate, two doors. (Until 432-21 the service was absent whenever no credential was declared.)
1176
+ */
1177
+ runServiceReadiness?: () => RunServiceReadiness;
1178
+ /**
1179
+ * The run id → live `RunSession` lookup, as a FIRST-CLASS option (403 D-14 — WINDOWS #264).
1180
+ *
1181
+ * SEPARATE FROM `runService` AND NOT DERIVED FROM IT, on `runServiceReadiness`'s exact argument
1182
+ * one field up. The derivation `runService?.attach` is what made the local run-stream door
1183
+ * answer 503 about runs this very process was executing: until 432-21 `composeRunServicePorts`
1184
+ * ran only when `runServiceTokens` were declared, so the DEFAULT zero-token dashboard composed no
1185
+ * registry at all and the local door had nothing to attach to. `agent-qa dashboard` supplies this
1186
+ * from a registry hoisted above the door.
1187
+ *
1188
+ * The legacy derivation still applies when this is absent, so an in-process caller that only
1189
+ * passes `runService` keeps working byte-for-byte.
1190
+ */
1191
+ runServiceAttach?: (runId: string) => RunSession | undefined;
1192
+ /**
1193
+ * The live-view seam (403 D-03/D-06): the function layer's validator, by reference, and the
1194
+ * mint dispatched through the registry.
1195
+ *
1196
+ * ABSENT MEANS NO LIVE VIEW IS SERVED — the embed route is not registered, the frame socket
1197
+ * refuses the upgrade, and `GET /api/runs/:runId/live-url` answers 503 by name. This server
1198
+ * holds NO signing key and NO token parse of its own; both are the function layer's, handed in.
1199
+ */
1200
+ liveView?: {
1201
+ validate(token: string, expect: {
1202
+ subject: LiveViewSubjectRef;
1203
+ now: number;
1204
+ }): LiveUrlVerdict;
1205
+ mintLiveUrl(runId: string, handle: number): Promise<RunsLiveUrlResult>;
1206
+ /**
1207
+ * The `editor.liveurl` mint, dispatched through the registry (428 D-07). ABSENT MEANS NO
1208
+ * EDITOR VIEW IS MINTED, and `GET /api/live-editor/sessions/:id/live-url` answers 503 by name.
1209
+ */
1210
+ mintEditorLiveUrl?(sessionId: string): Promise<EditorLiveUrlResult>;
1211
+ };
1212
+ /**
1213
+ * The editor-subject registry (428 D-07): the session manager registers each live editor session
1214
+ * here and the frame socket attaches editor subjects through it.
1215
+ *
1216
+ * ABSENT MEANS NO EDITOR SUBJECT IS SERVED ON THE FRAME SOCKET. `agent-qa dashboard` creates one
1217
+ * registry and hands the same object to the live-view composition and to this server.
1218
+ */
1219
+ liveEditorSubjects?: LiveEditorSubjects;
1220
+ /**
1221
+ * The in-process post-processing chip relay (404-05, D-16).
1222
+ *
1223
+ * INJECTED, ON `agentHost`'s OWN IDIOM AND FOR ITS OWN REASON. The PUBLISHER is the CLI's
1224
+ * post-run chip emitter, in this same process, and this package may not import it — so
1225
+ * `agent-qa dashboard` creates one relay, hands `relay.publish` to the run door's bridge as the
1226
+ * emitter's second sink and hands the relay itself here. It has to be the SAME object: a relay
1227
+ * built inside this function would be a registry nothing publishes to, and the stream door would
1228
+ * heartbeat forever at a page whose chips are demonstrably moving.
1229
+ *
1230
+ * ABSENT MEANS NO PUSH CHANNEL, and `GET /api/runs/:id/post-processing/stream` answers 503 by
1231
+ * name. The READ route needs none of this and keeps working: polling is the floor the whole
1232
+ * design rests on, and the stream is the optimisation over it.
1233
+ */
1234
+ postProcessingRelay?: PostProcessingRelay;
1235
+ }
1236
+ declare function startServer(opts: StartServerOptions): Promise<{
1237
+ server: Server;
1238
+ port: number;
1239
+ close: () => void;
1240
+ }>;
1241
+
1242
+ interface AppiumLeaseOwner {
1243
+ runId: string;
1244
+ platform: 'android' | 'ios';
1245
+ }
1246
+ interface AppiumLease extends AppiumLeaseOwner {
1247
+ acquiredAt: string;
1248
+ }
1249
+ declare class AppiumManager {
1250
+ private process;
1251
+ private port;
1252
+ private ready;
1253
+ private startPromise;
1254
+ private managed;
1255
+ private startupTimeoutMs;
1256
+ private pollIntervalMs;
1257
+ private anonymousRefCount;
1258
+ private leases;
1259
+ private logLevel;
1260
+ private appiumResolver;
1261
+ constructor(opts?: {
1262
+ port?: number;
1263
+ startupTimeoutMs?: number;
1264
+ pollIntervalMs?: number;
1265
+ logLevel?: 'normal' | 'debug';
1266
+ appiumResolver?: () => ResolvedAppiumExecutable;
1267
+ });
1268
+ acquire(): Promise<void>;
1269
+ release(): void;
1270
+ acquireLease(owner: AppiumLeaseOwner): Promise<void>;
1271
+ releaseLease(runId: string, reason?: string): boolean;
1272
+ releaseAllLeases(reason?: string): string[];
1273
+ ensureRunning(): Promise<void>;
1274
+ checkStatus(): Promise<boolean>;
1275
+ private startAndWait;
1276
+ private performShutdown;
1277
+ private maybeShutdownAfterRelease;
1278
+ shutdown(): void;
1279
+ getUrl(): string;
1280
+ getPort(): number;
1281
+ isManaged(): boolean;
1282
+ getRefCount(): number;
1283
+ getLeaseCount(): number;
1284
+ getLeases(): AppiumLease[];
1285
+ hasLease(runId: string): boolean;
1286
+ }
1287
+
1288
+ type ApplyChangesOutcome = {
1289
+ kind: 'ok';
1290
+ revision: string;
1291
+ baseRevision: string;
1292
+ priors?: ConfigPriors;
1293
+ } | {
1294
+ kind: 'refused';
1295
+ error: Exclude<ConfigWriteRefusal, 'write_failed'>;
1296
+ issues?: ConfigWriteIssue[];
1297
+ /** The field an `expect` found changed. */
1298
+ path?: ConfigPathSegment[];
1299
+ };
1300
+ type FieldState = {
1301
+ present: false;
1302
+ } | {
1303
+ present: true;
1304
+ value: unknown;
1305
+ };
1306
+ /**
1307
+ * What one change found and left at the field it touched (429-11 D-24). The nodes are the file's
1308
+ * own, so a restore brings back real secrets and comments rather than anything the page holds.
1309
+ */
1310
+ type ConfigPrior = {
1311
+ kind: 'entry';
1312
+ array: ConfigPathSegment[];
1313
+ name: string;
1314
+ index: number;
1315
+ node: Node;
1316
+ } | {
1317
+ kind: 'path';
1318
+ path: ConfigPathSegment[];
1319
+ before: Node | null | undefined;
1320
+ after: FieldState;
1321
+ };
1322
+ type ConfigPriors = readonly ConfigPrior[];
1323
+ /** A whole-document replacement, checked against the bytes the caller read (432-07 D-05). */
1324
+ type ReplaceDocumentOutcome = {
1325
+ kind: 'ok';
1326
+ revision: string;
1327
+ baseRevision: string;
1328
+ } | {
1329
+ kind: 'refused';
1330
+ error: 'file_changed';
1331
+ revision: string;
1332
+ } | {
1333
+ kind: 'refused';
1334
+ error: 'marker_written_back';
1335
+ message: string;
1336
+ };
1337
+ type RestorePriorsOutcome = {
1338
+ kind: 'ok';
1339
+ revision: string;
1340
+ } | {
1341
+ kind: 'refused';
1342
+ error: 'undo_conflict' | 'file_invalid';
1343
+ };
1344
+ type ConfigReadOutcome = {
1345
+ kind: 'ok';
1346
+ config: Record<string, unknown>;
1347
+ raw: string;
1348
+ revision: string;
1349
+ } | {
1350
+ kind: 'absent';
1351
+ } | {
1352
+ kind: 'file_invalid';
1353
+ message: string;
1354
+ revision: string;
1355
+ };
1356
+ /** One line of the config file, as the declared mask shows it. */
1357
+ interface ConfigFileLine {
1358
+ n: number;
1359
+ text: string;
1360
+ }
1361
+ type MaskedConfigOutcome = {
1362
+ kind: 'ok';
1363
+ config: Record<string, unknown>;
1364
+ revision: string;
1365
+ lines: ConfigFileLine[];
1366
+ fileIssues: ConfigWriteIssue[];
1367
+ } | {
1368
+ kind: 'file_invalid';
1369
+ message: string;
1370
+ revision: string;
1371
+ };
1372
+ /** Run `work` in this path's turn. The queue moves on however `work` settles. */
1373
+ declare function serializeConfigWrite<T>(configPath: string, work: () => Promise<T>): Promise<T>;
1374
+ declare class ConfigFileInvalidError extends Error {
1375
+ constructor(message: string);
1376
+ }
1377
+ /**
1378
+ * The config's values for a reader that only needs them (429-32, Pitfall 3): a missing file reads
1379
+ * as empty, and a file that does not parse is an error rather than the part of it yaml recovers.
1380
+ */
1381
+ declare function readConfigValues(manager: ConfigManager): Promise<Record<string, unknown>>;
1382
+ declare class ConfigManager {
1383
+ private configPath;
1384
+ readonly mask: MaskFn;
1385
+ constructor(configPath: string, mask: MaskFn);
1386
+ static addsShownMarker(current: Uint8Array | undefined, next: Uint8Array): boolean;
1387
+ readWithRevision(): Promise<ConfigReadOutcome>;
1388
+ readMasked(): Promise<MaskedConfigOutcome>;
1389
+ private maskSummary;
1390
+ private readParsed;
1391
+ applyChanges(changes: readonly ConfigChange[], options?: {
1392
+ capturePriors?: boolean;
1393
+ }): Promise<ApplyChangesOutcome>;
1394
+ restorePriors(priors: ConfigPriors): Promise<RestorePriorsOutcome>;
1395
+ replaceDocument(expectedRevision: string, content: Uint8Array): Promise<ReplaceDocumentOutcome>;
1396
+ private serialized;
1397
+ private restorePriorsOnce;
1398
+ private applyChangesOnce;
1399
+ private writeAtomic;
1400
+ private writeTarget;
1401
+ }
1402
+
1403
+ type LiveSessionLLMConfig = DashboardLLMConfig & {
1404
+ vision?: {
1405
+ screenshotSize?: number;
1406
+ effectiveResolution?: number;
1407
+ };
1408
+ /** The model entry's own `vision` flag, when the config sets it (439 D-19). */
1409
+ modelVision?: boolean;
1410
+ };
1411
+ interface LiveDraftMetadata {
1412
+ testName: string;
1413
+ testContext?: string;
1414
+ }
1415
+ interface EntityRef {
1416
+ type: 'suite' | 'test';
1417
+ id: string;
1418
+ }
1419
+ interface LiveExecutionLog {
1420
+ id: string;
1421
+ type: 'hook' | 'appium-script' | 'runjs';
1422
+ name: string;
1423
+ hookId?: string | null;
1424
+ phase: 'setup' | 'teardown' | 'inline';
1425
+ status: 'passed' | 'failed';
1426
+ duration: number;
1427
+ stdout: string | null;
1428
+ stderr: string | null;
1429
+ returnData: unknown | null;
1430
+ variables: Record<string, string> | null;
1431
+ createdAt: string;
1432
+ }
1433
+ interface LiveSubActionData {
1434
+ index: number;
1435
+ observation: string;
1436
+ reasoning: string;
1437
+ plannedAction: unknown;
1438
+ result: 'success' | 'failure' | 'no-input' | 'in-progress';
1439
+ error?: string;
1440
+ screenStateBefore: string;
1441
+ screenStateAfter?: string;
1442
+ confidence?: number;
1443
+ verifierReasoning?: string;
1444
+ cached: boolean;
1445
+ tokenUsage?: {
1446
+ promptTokens: number;
1447
+ completionTokens: number;
1448
+ totalTokens: number;
1449
+ };
1450
+ phaseDurations?: {
1451
+ observe?: number;
1452
+ plan?: number;
1453
+ execute?: number;
1454
+ verify?: number;
1455
+ };
1456
+ annotation?: StepAnnotation;
1457
+ screenContextBefore?: string;
1458
+ screenContextAfter?: string;
1459
+ data?: unknown;
1460
+ }
1461
+ interface LiveStepResultPayload {
1462
+ status: 'passed' | 'failed' | 'cancelled';
1463
+ duration?: number;
1464
+ error?: string;
1465
+ capturedVariables?: Record<string, string>;
1466
+ variableSnapshot?: Record<string, {
1467
+ value: string;
1468
+ source: string;
1469
+ }>;
1470
+ originalStepName?: string;
1471
+ consoleLogs?: ConsoleLogEntry[];
1472
+ networkLogs?: NetworkLogEntry[];
1473
+ executionLogs?: LiveExecutionLog[];
1474
+ subActionsData?: LiveSubActionData[];
1475
+ }
1476
+ interface LiveSuiteHookOwner {
1477
+ scope: 'suite';
1478
+ }
1479
+ interface LiveTestOwner {
1480
+ scope: 'test';
1481
+ testExecutionId: string;
1482
+ testIndex: number;
1483
+ testId: string;
1484
+ testName: string;
1485
+ }
1486
+ type LiveHookOwner = LiveSuiteHookOwner | LiveTestOwner;
1487
+ interface LiveHookPayload {
1488
+ executionId: string;
1489
+ hookId: string;
1490
+ hookName: string;
1491
+ phase: 'setup' | 'teardown';
1492
+ owner: LiveHookOwner;
1493
+ status: 'running' | 'passed' | 'failed';
1494
+ duration?: number;
1495
+ stdout?: string | null;
1496
+ stderr?: string | null;
1497
+ variables?: Record<string, string> | null;
1498
+ error?: string;
1499
+ createdAt: string;
1500
+ }
1501
+ interface LiveTestExecutionPayload {
1502
+ testExecutionId: string;
1503
+ testIndex: number;
1504
+ testId: string;
1505
+ testName: string;
1506
+ }
1507
+ interface LiveTestStepPayload extends LiveTestExecutionPayload {
1508
+ stepIndex: number;
1509
+ stepInstruction: string;
1510
+ }
1511
+ interface LiveTestResultPayload {
1512
+ status: 'passed' | 'failed' | 'cancelled';
1513
+ duration: number;
1514
+ error?: string;
1515
+ setupHookExecutions: LiveHookPayload[];
1516
+ stepResults: LiveStepResultPayload[];
1517
+ teardownHookExecutions: LiveHookPayload[];
1518
+ }
1519
+ type ServerMessage = {
1520
+ type: 'session-ready';
1521
+ sessionId: string;
1522
+ platform: string;
1523
+ interactive: boolean;
1524
+ error?: string | null;
1525
+ } | {
1526
+ type: 'hook-start';
1527
+ hook: LiveHookPayload;
1528
+ } | {
1529
+ type: 'hook-complete';
1530
+ hook: LiveHookPayload;
1531
+ } | {
1532
+ type: 'step-phase';
1533
+ phase: string;
1534
+ data?: unknown;
1535
+ } | {
1536
+ type: 'step-complete';
1537
+ result: LiveStepResultPayload;
1538
+ } | {
1539
+ type: 'step-cancelled';
1540
+ } | {
1541
+ type: 'step-error';
1542
+ error: string;
1543
+ } | {
1544
+ type: 'test-start';
1545
+ test: LiveTestExecutionPayload;
1546
+ } | {
1547
+ type: 'test-step-start';
1548
+ step: LiveTestStepPayload;
1549
+ } | {
1550
+ type: 'test-step-phase';
1551
+ step: LiveTestStepPayload;
1552
+ phase: string;
1553
+ data?: unknown;
1554
+ } | {
1555
+ type: 'test-step-complete';
1556
+ step: LiveTestStepPayload;
1557
+ result: LiveStepResultPayload;
1558
+ } | {
1559
+ type: 'test-step-cancelled';
1560
+ step: LiveTestStepPayload;
1561
+ } | {
1562
+ type: 'test-step-error';
1563
+ step: LiveTestStepPayload;
1564
+ error: string;
1565
+ } | {
1566
+ type: 'test-complete';
1567
+ test: LiveTestExecutionPayload;
1568
+ result: LiveTestResultPayload;
1569
+ } | {
1570
+ type: 'test-error';
1571
+ test: LiveTestExecutionPayload;
1572
+ error: string;
1573
+ } | {
1574
+ type: 'session-state';
1575
+ state: SessionState;
1576
+ } | {
1577
+ type: 'session-terminated';
1578
+ } | {
1579
+ type: 'browser-disconnected';
1580
+ } | {
1581
+ type: 'error';
1582
+ message: string;
1583
+ } | {
1584
+ type: 'step-busy';
1585
+ currentStep: string;
1586
+ } | {
1587
+ type: 'navigate-complete';
1588
+ url: string | null;
1589
+ } | {
1590
+ type: 'device-logs';
1591
+ entries: Array<{
1592
+ level: string;
1593
+ message: string;
1594
+ timestamp: number;
1595
+ }>;
1596
+ } | {
1597
+ type: 'aria-tree';
1598
+ tree: string;
1599
+ };
1600
+ interface SessionState {
1601
+ sessionId: string;
1602
+ platform: 'web' | 'android' | 'ios';
1603
+ targetName?: string | null;
1604
+ status: 'idle' | 'executing' | 'disconnected';
1605
+ currentStep: string | null;
1606
+ currentUrl: string | null;
1607
+ stepsExecuted: number;
1608
+ createdAt: number;
1609
+ interactive: boolean;
1610
+ terminalError: string | null;
1611
+ }
1612
+ /**
1613
+ * What the create door found the current config declares about the session's target (432 D-18).
1614
+ *
1615
+ * It can only add a refusal: `initialize` refuses any name here unless the config it reads itself
1616
+ * opts in. There is deliberately no opt-in field, so nothing a caller passes can allow production.
1617
+ */
1618
+ interface LiveProductionFinding {
1619
+ readonly productionTargetNames: readonly string[];
1620
+ }
1621
+ interface LiveSessionConfig {
1622
+ platform: 'web' | 'android' | 'ios';
1623
+ targetName?: string;
1624
+ production?: LiveProductionFinding;
1625
+ /** 436 D-03: resolved at the create door from the project config and the edited entity. */
1626
+ plannerLimits?: PlannerLimits;
1627
+ /** 436 D-03: per executed test, project then suite then the test's own `use`. */
1628
+ plannerLimitsForTest?: (testId: string) => Promise<PlannerLimits>;
1629
+ llmConfig: LiveSessionLLMConfig;
1630
+ authFetch?: typeof globalThis.fetch;
1631
+ envVars?: Record<string, string>;
1632
+ secretStore?: SecretStore;
1633
+ secretRedactor?: SecretRedactor;
1634
+ setupHooks?: string[];
1635
+ teardownHooks?: string[];
1636
+ resolvedHooks?: Map<string, HookDefinition>;
1637
+ hookRegistryError?: string;
1638
+ url?: string;
1639
+ headless?: boolean;
1640
+ useDeviceName?: string;
1641
+ appState?: 'preserve' | 'reset';
1642
+ bundleId?: string;
1643
+ appPackage?: string;
1644
+ appActivity?: string;
1645
+ device?: {
1646
+ name: string;
1647
+ platformVersion?: string;
1648
+ appPath?: string;
1649
+ browserName?: string;
1650
+ appPackage?: string;
1651
+ appActivity?: string;
1652
+ bundleId?: string;
1653
+ udid?: string;
1654
+ avd?: string;
1655
+ appiumUrl?: string;
1656
+ };
1657
+ }
1658
+ /**
1659
+ * The close code the control socket uses for a session this process does not hold (428 P5), so the
1660
+ * page can say the session ended instead of "Disconnected". The dashboard UI spells the same number;
1661
+ * Plan 23 pins the two equal.
1662
+ */
1663
+ declare const LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE = 4404;
1664
+ /**
1665
+ * The close code for a control socket a newer one has replaced (428 WR-01). The session is still
1666
+ * held; this socket no longer drives it, so the page must not reconnect and take it back. The
1667
+ * dashboard UI spells the same number.
1668
+ */
1669
+ declare const LIVE_EDITOR_SUPERSEDED_CLOSE_CODE = 4409;
1670
+
1671
+ /** Why the session let go of a control socket: a newer one replaced it, or the session ended. */
1672
+ type LiveAttachmentRelease = 'superseded' | 'ended';
1673
+ interface LiveSessionDeps {
1674
+ appiumManager?: AppiumManager;
1675
+ configManager?: ConfigManager;
1676
+ configPath?: string;
1677
+ }
1678
+ interface LiveStepExecutionResult extends StepResult {
1679
+ executionLogs?: LiveExecutionLog[];
1680
+ subActionsData?: LiveSubActionData[];
1681
+ }
1682
+ declare class LiveSession {
1683
+ private deps;
1684
+ readonly sessionId: string;
1685
+ status: 'idle' | 'executing' | 'disconnected' | 'terminated';
1686
+ private abortController;
1687
+ stepsExecuted: number;
1688
+ createdAt: number;
1689
+ private adapter;
1690
+ private planner;
1691
+ private verifier;
1692
+ private modelProfiles;
1693
+ private storageStateStore;
1694
+ private model;
1695
+ private variableStore;
1696
+ private previousSteps;
1697
+ private currentStep;
1698
+ private platform;
1699
+ private targetName;
1700
+ private currentUrl;
1701
+ private executing;
1702
+ private idleTimer;
1703
+ private _modelName;
1704
+ private vision;
1705
+ private visionGate;
1706
+ private plannerLimits;
1707
+ private plannerLimitsForTest;
1708
+ private readyForInteraction;
1709
+ private terminalError;
1710
+ private setupHooks;
1711
+ private teardownHooks;
1712
+ private resolvedHooks;
1713
+ private hookRegistryError;
1714
+ private pendingMessages;
1715
+ private messageSink;
1716
+ private hookCaptures;
1717
+ private mobileAppiumLease;
1718
+ private secretStore;
1719
+ private secretRedactor;
1720
+ private liveHub;
1721
+ private attachment;
1722
+ private resolveEnded;
1723
+ private readonly ended;
1724
+ private cleanupRun;
1725
+ private attachmentGeneration;
1726
+ private releaseAttachment;
1727
+ constructor(sessionId: string, deps?: LiveSessionDeps);
1728
+ get modelName(): string;
1729
+ /** What the editor-subject registry holds for this session: its hub, read at call time. */
1730
+ liveAttachment(): LiveEditorAttachment;
1731
+ private openLiveHub;
1732
+ /**
1733
+ * Make a control socket the current one. `release` is how the session lets go of it later: the
1734
+ * previous attachment's is called with `superseded` here (428 WR-01), so one socket drives the
1735
+ * session at a time.
1736
+ */
1737
+ beginAttachment(release?: (reason: LiveAttachmentRelease) => void): number;
1738
+ isCurrentAttachment(generation: number): boolean;
1739
+ attachMessageSink(sink: ((message: ServerMessage) => void) | null): void;
1740
+ /**
1741
+ * A command's reply, for whichever control socket is attached now (428 CR-01). A command can
1742
+ * outlive the socket that issued it, and its outcome belongs to the page, not to that socket:
1743
+ * with none attached it is held and flushed to the next one.
1744
+ */
1745
+ deliver(message: ServerMessage): void;
1746
+ private dispatch;
1747
+ private redact;
1748
+ private resolveSecrets;
1749
+ private emitHookStart;
1750
+ private emitHookComplete;
1751
+ private emitTestStepStart;
1752
+ private emitTestStepPhase;
1753
+ private emitTestStepComplete;
1754
+ private emitTestStepError;
1755
+ private getHookEnvVars;
1756
+ private runLifecycleHooks;
1757
+ executeHookCommand(phase: 'setup' | 'teardown', hookId: string): Promise<LiveExecutionLog>;
1758
+ private releaseMobileAppiumLease;
1759
+ private legacyMobilePlatformConfig;
1760
+ private resolveMobilePlatformConfig;
1761
+ private setupMobileAdapter;
1762
+ initialize(config: LiveSessionConfig): Promise<void>;
1763
+ private resolveStepInstruction;
1764
+ private buildPreparationFailureResult;
1765
+ private getStorageStateStore;
1766
+ private executeStepInternal;
1767
+ executeStepCommand(stepInstruction: string, stepIndex?: number, onPhase?: (event: StepPhaseEvent) => void, draft?: LiveDraftMetadata): Promise<LiveStepExecutionResult>;
1768
+ executeTestCommand(testExecutionId: string, testDraft: {
1769
+ testIndex: number;
1770
+ testId: string;
1771
+ testName: string;
1772
+ testContext?: string;
1773
+ steps: string[];
1774
+ setup: string[];
1775
+ teardown: string[];
1776
+ }, onPhase?: (event: StepPhaseEvent) => void): Promise<LiveTestResultPayload>;
1777
+ private runLifecycleHooksCapturing;
1778
+ captureWebAuthState(name: string, options?: {
1779
+ replace?: boolean;
1780
+ }): Promise<AuthStateMetadata>;
1781
+ cancelStep(): void;
1782
+ navigate(url: string): Promise<string | null>;
1783
+ refreshPage(): Promise<string | null>;
1784
+ goBack(): Promise<string | null>;
1785
+ goForward(): Promise<string | null>;
1786
+ drainDeviceLogs(): Promise<Array<{
1787
+ level: string;
1788
+ message: string;
1789
+ timestamp: number;
1790
+ }>>;
1791
+ getState(): SessionState;
1792
+ getAriaTree(): Promise<string | null>;
1793
+ startIdleTimer(onExpire: () => void): void;
1794
+ clearIdleTimer(): void;
1795
+ /** Idempotent: every later or concurrent call shares the first run, so teardown happens once. */
1796
+ cleanup(): Promise<void>;
1797
+ private runCleanup;
1798
+ }
1799
+
1800
+ interface SessionManagerDeps {
1801
+ appiumManager?: AppiumManager;
1802
+ configManager?: ConfigManager;
1803
+ configPath?: string;
1804
+ liveEditorSubjects?: LiveEditorSubjects;
1805
+ }
1806
+ declare class SessionManager {
1807
+ private deps;
1808
+ private sessions;
1809
+ private counters;
1810
+ constructor(deps?: SessionManagerDeps);
1811
+ createSession(config: LiveSessionConfig, entity?: EntityRef): Promise<{
1812
+ sessionId: string;
1813
+ sessionNumber: number | null;
1814
+ }>;
1815
+ getSession(sessionId: string): LiveSession | undefined;
1816
+ listSessions(): SessionState[];
1817
+ terminateSession(sessionId: string): Promise<boolean>;
1818
+ cleanupAll(): Promise<void>;
1819
+ getCounter(entity: EntityRef): number;
1820
+ get size(): number;
1821
+ }
1822
+
1823
+ interface EnqueueOptions {
1824
+ name: string;
1825
+ filePath?: string;
1826
+ kind?: RunArtifactKind;
1827
+ attributes?: RunAttributes;
1828
+ priority?: number;
1829
+ platform?: string;
1830
+ testFileContent?: string;
1831
+ modelName?: string;
1832
+ llmProvider?: string;
1833
+ metadata?: Record<string, unknown>;
1834
+ parallel?: boolean;
1835
+ }
1836
+ type RunCancelOutcome = {
1837
+ kind: 'cancelled';
1838
+ } | {
1839
+ kind: 'stop-requested';
1840
+ } | {
1841
+ kind: 'not-stoppable-here';
1842
+ } | {
1843
+ kind: 'finished';
1844
+ status: string;
1845
+ } | {
1846
+ kind: 'orphan-finalized';
1847
+ message: string;
1848
+ } | {
1849
+ kind: 'owned-elsewhere';
1850
+ message: string;
1851
+ ownerPid: number;
1852
+ } | {
1853
+ kind: 'not-cancellable';
1854
+ };
1855
+ declare class JobQueue extends EventEmitter {
1856
+ private db;
1857
+ private pollInterval;
1858
+ private isProcessing;
1859
+ private concurrency;
1860
+ private activeCount;
1861
+ private activePlatforms;
1862
+ private activeRuns;
1863
+ private requestStop;
1864
+ private stopped;
1865
+ constructor(opts: {
1866
+ db: DashboardDatabase;
1867
+ concurrency?: number;
1868
+ requestStop?: (runId: string) => boolean;
1869
+ });
1870
+ start(intervalMs?: number): void;
1871
+ stop(): void;
1872
+ enqueue(opts: EnqueueOptions): string;
1873
+ processNext(): void;
1874
+ onSlotFreed(runId: string): void;
1875
+ private dispatchReported;
1876
+ getActiveCount(): number;
1877
+ getConcurrency(): number;
1878
+ cancel(runId: string): RunCancelOutcome;
1879
+ private finishedOrMissing;
1880
+ private findNextEligible;
1881
+ }
1882
+
1883
+ interface TestFileInfo {
1884
+ path: string;
1885
+ name: string;
1886
+ testId: string | null;
1887
+ targetName: string | null;
1888
+ platform: string | null;
1889
+ modified: string;
1890
+ }
1891
+ interface TestValidationError {
1892
+ message: string;
1893
+ line?: number;
1894
+ column?: number;
1895
+ suggestion?: string;
1896
+ }
1897
+ interface TestValidationResult {
1898
+ valid: boolean;
1899
+ errors: TestValidationError[];
1900
+ }
1901
+ declare class TestFileManager {
1902
+ private readonly workspace;
1903
+ constructor(workspace: ResolvedWorkspacePaths);
1904
+ private readonly underLock;
1905
+ list(): Promise<TestFileInfo[]>;
1906
+ read(relativePath: string): Promise<string>;
1907
+ findByTestId(testId: string): Promise<{
1908
+ path: string;
1909
+ content: string;
1910
+ } | null>;
1911
+ write(relativePath: string, content: string): Promise<void>;
1912
+ delete(relativePath: string): Promise<void>;
1913
+ validate(content: string): Promise<TestValidationResult>;
1914
+ private resolveTarget;
1915
+ }
1916
+
1917
+ interface SuiteFileInfo {
1918
+ path: string;
1919
+ suiteId: string | null;
1920
+ name: string;
1921
+ testCount: number;
1922
+ modified: string;
1923
+ platform: string | null;
1924
+ }
1925
+ interface SuiteValidationResult {
1926
+ valid: boolean;
1927
+ errors: {
1928
+ message: string;
1929
+ line?: number;
1930
+ column?: number;
1931
+ suggestion?: string;
1932
+ }[];
1933
+ missingTests?: Array<{
1934
+ index: number;
1935
+ test: string;
1936
+ id: string;
1937
+ }>;
1938
+ }
1939
+ declare class SuiteFileManager {
1940
+ private readonly workspace;
1941
+ private testFileManager;
1942
+ constructor(workspace: ResolvedWorkspacePaths, testFileManager?: TestFileManager);
1943
+ private readonly underLock;
1944
+ list(): Promise<SuiteFileInfo[]>;
1945
+ findBySuiteId(suiteId: string): Promise<{
1946
+ path: string;
1947
+ content: string;
1948
+ } | null>;
1949
+ resolvePath(filePath: string, requireExisting?: boolean): Promise<{
1950
+ storagePath: string;
1951
+ executionPath: string;
1952
+ }>;
1953
+ read(relativePath: string): Promise<string>;
1954
+ write(relativePath: string, content: string): Promise<void>;
1955
+ delete(relativePath: string): Promise<void>;
1956
+ validate(content: string): Promise<SuiteValidationResult>;
1957
+ private resolveTarget;
1958
+ }
1959
+
1960
+ type DashboardRuntimeLLMConfig = DashboardLLMConfig & {
1961
+ vision?: DashboardVisionConfig;
1962
+ modelVision?: boolean;
1963
+ };
1964
+ type DashboardVisionConfig = {
1965
+ screenshotSize?: number;
1966
+ effectiveResolution?: number;
1967
+ };
1968
+ interface RouterDeps {
1969
+ db: DashboardDatabase;
1970
+ workspacePaths?: ResolvedWorkspacePaths;
1971
+ /**
1972
+ * The injected run backend (401-07, D-09).
1973
+ *
1974
+ * TYPED AGAINST THE INTERFACE, AND NOW ACTUALLY SEPARABLE. It always meant to be, and was not:
1975
+ * the retired live-event route reached past the interface for a per-run replay buffer only the
1976
+ * concrete class declared, so the seam had exactly one possible implementation. That route and
1977
+ * that buffer are gone (405-07, D-19); what this router asks of a backend is the interface.
1978
+ */
1979
+ executionBackend?: ExecutionBackend;
1980
+ jobQueue?: JobQueue;
1981
+ testFileManager?: TestFileManager;
1982
+ suiteFileManager?: SuiteFileManager;
1983
+ configManager?: ConfigManager;
1984
+ configPath?: string;
1985
+ declaredSecretMask?: MaskFn;
1986
+ llmConfig?: DashboardRuntimeLLMConfig;
1987
+ authFetch?: typeof globalThis.fetch;
1988
+ sessionManager?: SessionManager;
1989
+ /**
1990
+ * The composed agent host (400-01, D-01). It serves run detail, marks, analysis, knowledge and
1991
+ * post-processing. (The run-confirm pair it also served is deleted, 432-14.)
1992
+ *
1993
+ * INJECTED, and optional because a router built the legacy two-argument way
1994
+ * (`createRouter(db)`) has none. Each arm that needs it then answers by name rather than 404 —
1995
+ * the route exists, this server was started without a host. See `chat/host.ts` for why the
1996
+ * composition happens in the CLI command that starts this server rather than here.
1997
+ */
1998
+ agentHost?: AgentHost;
1999
+ /**
2000
+ * The run service's readiness predicate, served on the LOCAL door (402-05, D-15).
2001
+ *
2002
+ * THE SAME FUNCTION THE RUN SERVICE'S `/ok` SERVES — one readiness spelling behind two doors,
2003
+ * which is the whole of D-15. It is passed in rather than computed here for the reason `agentHost`
2004
+ * is: every fact in it (is a host composed) is known only to the process that composed it, and a
2005
+ * copy made here is how the run service and the dashboard start disagreeing about whether this
2006
+ * process can run anything.
2007
+ *
2008
+ * ABSENT IS THE LEGACY MOUNT, NOT AN UNCONFIGURED SERVICE. `agent-qa dashboard` ALWAYS supplies
2009
+ * this, because the browser preflight needs an honest answer and a 404 is not one. A router built the legacy
2010
+ * two-argument way has none, and that arm answers 503 by name on `agentHost`'s idiom.
2011
+ */
2012
+ runServiceReadiness?: () => RunServiceReadiness;
2013
+ /**
2014
+ * The started run's session, or nothing — the LOCAL door's attach port (402-08, D-01).
2015
+ *
2016
+ * THE SAME PORT THE AUTHED DOOR USES, and `server.ts` supplies it by reading
2017
+ * `runService.attach` rather than by composing a second lookup: one session registry serves both
2018
+ * doors, so a run the remote door can attach to is a run this door can attach to, and there is
2019
+ * no second retention window to disagree about.
2020
+ *
2021
+ * ABSENT MEANS NO RUN SERVICE WAS MOUNTED, which is the same fact as "this process composed no
2022
+ * session registry" — unlike `runServiceReadiness` above, whose absence is a legacy mount rather
2023
+ * than an unconfigured service. The route then answers 503 BY NAME on the execution routes' own
2024
+ * degradation idiom, never a 404 (the route exists) and never an empty stream (which would look
2025
+ * like a run that emitted nothing).
2026
+ */
2027
+ runServiceAttach?: (runId: string) => RunSession | undefined;
2028
+ /**
2029
+ * The `runs.liveurl` mint, DISPATCHED THROUGH THE REGISTRY by the composition root (403 D-03).
2030
+ *
2031
+ * A FUNCTION, not a signer: this router holds no key and mints nothing. What it does is turn one
2032
+ * HTTP shape into one op call and the op's refusals into status codes — which is the whole of a
2033
+ * thin client over the function layer.
2034
+ *
2035
+ * ABSENT MEANS THIS HOST SERVES NO LIVE VIEW, and the route answers 503 BY NAME on
2036
+ * `runServiceReadiness`'s idiom — never a 404 (the route exists) and never a URL to nowhere.
2037
+ */
2038
+ mintLiveUrl?: (runId: string, handle: number) => Promise<RunsLiveUrlResult>;
2039
+ /**
2040
+ * The `editor.liveurl` mint, dispatched through the registry by the composition root (428 D-07).
2041
+ * The editor counterpart of `mintLiveUrl`: a function, never a signer. ABSENT MEANS THIS HOST
2042
+ * MINTS NO EDITOR VIEW, and the route answers 503 by name.
2043
+ */
2044
+ mintEditorLiveUrl?: (sessionId: string) => Promise<EditorLiveUrlResult>;
2045
+ /**
2046
+ * The in-process chip sink the post-processing stream relays from (404-05, D-16).
2047
+ *
2048
+ * INJECTED ON `agentHost`'s OWN IDIOM. The publisher is the CLI's post-run chip emitter — a
2049
+ * package this one may not import — so `agent-qa dashboard` creates the relay, hands
2050
+ * `relay.publish` to the run door's bridge and hands the relay itself here. One object, two
2051
+ * halves of the same process; a relay composed here instead would be a registry the emitter
2052
+ * never reaches, and the stream would heartbeat forever at a page whose chips do move.
2053
+ *
2054
+ * ABSENT MEANS THIS SERVER HAS NO PUSH CHANNEL and the stream arm answers 503 BY NAME — never a
2055
+ * 404 (the route exists) and never an empty stream, which would look like a chain that never
2056
+ * ticked. The READ route is unaffected: polling works with no relay at all.
2057
+ */
2058
+ postProcessingRelay?: PostProcessingRelay;
2059
+ }
2060
+ declare function createRouter(deps: RouterDeps): (req: IncomingMessage, res: ServerResponse) => void;
2061
+ declare function createRouter(db: DashboardDatabase): (req: IncomingMessage, res: ServerResponse) => void;
2062
+
2063
+ declare function readJsonBody<T>(req: IncomingMessage, maxBytes?: number): Promise<T>;
2064
+
2065
+ /**
2066
+ * The request URL, or `undefined` when the request line is not one.
2067
+ *
2068
+ * THE ONE PRODUCER. Its whole contract is that it never throws, which is what lets each caller
2069
+ * answer in its own vocabulary — a 400 body, a JSON envelope, a destroyed socket — instead of
2070
+ * every frame growing its own `try`.
2071
+ */
2072
+ declare function tryParseRequestUrl(req: Pick<IncomingMessage, 'url'>): URL | undefined;
2073
+ /**
2074
+ * Refuse a request whose target could not be parsed, in FIXED BYTES.
2075
+ *
2076
+ * NOTHING FROM THE REQUEST REACHES THIS RESPONSE, and that is deliberate rather than terse. The
2077
+ * refusal exists precisely because the request target is malformed and attacker-chosen; echoing it
2078
+ * would put attacker bytes into a body, and formatting it would give the refusal its own way to
2079
+ * throw. The previous round recorded the same lesson one layer down: a backstop that answers
2080
+ * through the door's own responder can re-enter the fault it was catching.
2081
+ */
2082
+ declare function refuseUnparseableRequest(res: ServerResponse): void;
2083
+ /**
2084
+ * The process-level backstop's answer: a 500 in fixed bytes, or a destroyed socket if it is late.
2085
+ *
2086
+ * THE ERROR IS NOT PRINTED AND NOT SERIALIZED. Phase 395 measured a planted credential leaving this
2087
+ * product through an error's serialization rather than its `message`, so what goes to stderr here
2088
+ * is the error's CONSTRUCTOR NAME and the request METHOD — two values with no attacker- or
2089
+ * secret-derived content — and never the URL, which is attacker-chosen, or the error itself, whose
2090
+ * `cause` chain is exactly the surface that leaked.
2091
+ */
2092
+ declare function backstopServerError(res: ServerResponse, req: Pick<IncomingMessage, 'method'>, error: unknown): void;
2093
+
2094
+ export { createRouter as $, type AgentHost as A, type RunRow as B, ConfigFileInvalidError as C, DashboardDatabase as D, type ExecutionBackend as E, type RunServicePorts as F, type RunServiceRouteHandlerName as G, type RunServiceRouteMatch as H, type RunServiceRouteRow as I, type RunServiceRouter as J, type RunServiceStartInput as K, type LiveEditorAttachment as L, type MintLiveUrlInput as M, type RunSettledDetails as N, type RunSettledStatus as O, type PreflightOptions as P, RunsDatabaseVersionRefusedError as Q, RUN_SERVICE_ANALYSIS_TEMPLATE as R, TestFileManager as S, type TestFileInfo as T, type TestValidationError as U, type TestValidationResult as V, backstopServerError as W, classifyRunOwner as X, completeRunOwnership as Y, createLiveEditorSubjects as Z, createPostProcessingRelay as _, type ExecuteOptions as a, createRunServiceRoutes as a0, defaultRunOwnership as a1, matchRunServiceRoute as a2, orphanSummary as a3, ownedElsewhere as a4, ownsRunServicePath as a5, readConfigValues as a6, readJsonBody as a7, refuseUnparseableRequest as a8, runDenialOf as a9, serializeConfigWrite as aa, startServer as ab, tryParseRequestUrl as ac, type PostProcessingRelay as b, type AgentRegistryHandle as c, AppiumManager as d, ConfigManager as e, LIVE_EDITOR_SUPERSEDED_CLOSE_CODE as f, LIVE_EDITOR_UNKNOWN_SESSION_CLOSE_CODE as g, type LiveEditorSubjects as h, type PostProcessingListener as i, RUN_SERVICE_ARTIFACT_TEMPLATE as j, RUN_SERVICE_DETAIL_TEMPLATE as k, RUN_SERVICE_EVENTS_TEMPLATE as l, RUN_SERVICE_EVIDENCE_TEMPLATE as m, RUN_SERVICE_LIVE_URL_TEMPLATE as n, RUN_SERVICE_MARKS_TEMPLATE as o, RUN_SERVICE_MARK_PREVIEW_TEMPLATE as p, RUN_SERVICE_OK_ROUTE as q, RUN_SERVICE_ROUTE_TABLE as r, RUN_SERVICE_START_ROUTE as s, type ReplaceDocumentOutcome as t, type ResumePoint as u, type RunOwner as v, type RunOwnerProbe as w, type RunOwnerState as x, type RunOwnership as y, type RunOwnershipInput as z };