harnery 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.js +3 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +9 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +113 -12
- package/dist/commands/browse.d.ts +164 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +263 -51
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +52 -0
- package/dist/commands/disk.d.ts +6 -0
- package/dist/commands/disk.d.ts.map +1 -0
- package/dist/commands/disk.js +80 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +48 -1
- package/dist/commands/files.d.ts +16 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -2
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +43 -9
- package/dist/commands/rm.d.ts +4 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +87 -0
- package/dist/commands/servers.d.ts +7 -0
- package/dist/commands/servers.d.ts.map +1 -0
- package/dist/commands/servers.js +394 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +82 -2
- package/dist/core/adapters/profiles.d.ts +9 -0
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +22 -1
- package/dist/core/adapters/types.d.ts +9 -0
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/agents/cli.js +13 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +2 -0
- package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +60 -0
- package/dist/core/artifacts/delivery-card.js +9 -2
- package/dist/core/artifacts/index.d.ts +140 -4
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +522 -49
- package/dist/core/config.d.ts +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +128 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +2 -2
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +16 -0
- package/dist/core/events/v3/projection.js +9 -1
- package/dist/core/hooks/adapter/events.d.ts +17 -0
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +5 -0
- package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
- package/dist/core/hooks/adapter/wiring.d.ts +37 -7
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +62 -4
- package/dist/core/hooks/cli.js +45 -6
- package/dist/core/hooks/run-markers.d.ts +37 -0
- package/dist/core/hooks/run-markers.d.ts.map +1 -0
- package/dist/core/hooks/run-markers.js +109 -0
- package/dist/core/servers/index.d.ts +211 -0
- package/dist/core/servers/index.d.ts.map +1 -0
- package/dist/core/servers/index.js +560 -0
- package/dist/core/servers/net.d.ts +58 -0
- package/dist/core/servers/net.d.ts.map +1 -0
- package/dist/core/servers/net.js +270 -0
- package/dist/core/servers/tunnels.d.ts +14 -0
- package/dist/core/servers/tunnels.d.ts.map +1 -0
- package/dist/core/servers/tunnels.js +33 -0
- package/dist/core/storage/builtins.js +1 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +30 -1
- package/dist/core/workflow/sandbox-projection.d.ts +13 -2
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
- package/dist/core/workflow/sandbox-projection.js +23 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +6 -1
- package/dist/core/workflow/spawn-codex.d.ts +2 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +20 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +6 -1
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +4 -1
- package/dist/core/workflow/types.d.ts +47 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/worker-access.d.ts +92 -0
- package/dist/core/workflow/worker-access.d.ts.map +1 -0
- package/dist/core/workflow/worker-access.js +450 -0
- package/dist/lib/browser/batch-steps.d.ts +18 -0
- package/dist/lib/browser/batch-steps.d.ts.map +1 -0
- package/dist/lib/browser/batch-steps.js +82 -0
- package/dist/lib/browser/collect/collector.d.ts +99 -0
- package/dist/lib/browser/collect/collector.d.ts.map +1 -0
- package/dist/lib/browser/collect/collector.js +202 -0
- package/dist/lib/browser/collect/fields.d.ts +31 -0
- package/dist/lib/browser/collect/fields.d.ts.map +1 -0
- package/dist/lib/browser/collect/fields.js +75 -0
- package/dist/lib/browser/collect/index.d.ts +6 -0
- package/dist/lib/browser/collect/index.d.ts.map +1 -0
- package/dist/lib/browser/collect/index.js +5 -0
- package/dist/lib/browser/collect/page-driver.d.ts +14 -0
- package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
- package/dist/lib/browser/collect/page-driver.js +306 -0
- package/dist/lib/browser/collect/presets.d.ts +52 -0
- package/dist/lib/browser/collect/presets.d.ts.map +1 -0
- package/dist/lib/browser/collect/presets.js +176 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
- package/dist/lib/browser/collect/scroll-plan.js +118 -0
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.d.ts +2 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +13 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +1 -0
- package/dist/lib/browser/windows-chrome.d.ts +59 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
- package/dist/lib/browser/windows-chrome.js +179 -0
- package/dist/lib/claude-desktop-share.d.ts +136 -0
- package/dist/lib/claude-desktop-share.d.ts.map +1 -0
- package/dist/lib/claude-desktop-share.js +394 -0
- package/dist/lib/disk-usage.d.ts +50 -0
- package/dist/lib/disk-usage.d.ts.map +1 -0
- package/dist/lib/disk-usage.js +303 -0
- package/dist/lib/docs-lint.d.ts +6 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +8 -3
- package/dist/lib/guarded-remove.d.ts +34 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -0
- package/dist/lib/guarded-remove.js +310 -0
- package/dist/lib/instructions/templates.js +8 -8
- package/dist/lib/tunnel/error-page.d.ts +1 -1
- package/dist/lib/tunnel/error-page.d.ts.map +1 -1
- package/dist/lib/tunnel/error-page.js +19 -8
- package/dist/lib/tunnel/gate.js +20 -1
- package/dist/lib/tunnel/path-scope.d.ts +22 -0
- package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
- package/dist/lib/tunnel/path-scope.js +62 -0
- package/dist/lib/tunnel/state.d.ts +5 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +17 -3
- package/package.json +6 -1
- package/schemas/config.schema.json +91 -0
- package/src/commander.ts +19 -0
- package/src/commands/agents.ts +14 -0
- package/src/commands/artifacts.ts +171 -19
- package/src/commands/browse.ts +400 -57
- package/src/commands/claude-desktop.ts +61 -0
- package/src/commands/disk.ts +114 -0
- package/src/commands/doctor.ts +52 -1
- package/src/commands/files.ts +93 -2
- package/src/commands/init.ts +55 -5
- package/src/commands/rm.ts +115 -0
- package/src/commands/servers.ts +506 -0
- package/src/commands/tunnel.ts +102 -2
- package/src/core/adapters/profiles.ts +22 -1
- package/src/core/adapters/types.ts +10 -0
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/render/session-context.ts +1 -0
- package/src/core/agents/state/live-coordination-view.ts +73 -0
- package/src/core/artifacts/delivery-card.ts +10 -2
- package/src/core/artifacts/index.ts +697 -44
- package/src/core/config.ts +183 -4
- package/src/core/diagnostics/bundle.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +16 -0
- package/src/core/events/v3/projection.ts +11 -1
- package/src/core/hooks/adapter/events.ts +19 -0
- package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
- package/src/core/hooks/adapter/wiring.ts +116 -8
- package/src/core/hooks/cli.ts +47 -6
- package/src/core/hooks/run-markers.ts +132 -0
- package/src/core/servers/index.ts +712 -0
- package/src/core/servers/net.ts +283 -0
- package/src/core/servers/tunnels.ts +39 -0
- package/src/core/storage/builtins.ts +1 -0
- package/src/core/workflow/engine.ts +38 -1
- package/src/core/workflow/sandbox-projection.ts +38 -2
- package/src/core/workflow/spawn-claude.ts +5 -1
- package/src/core/workflow/spawn-codex.ts +18 -1
- package/src/core/workflow/spawn-cursor.ts +5 -1
- package/src/core/workflow/spawn-opencode.ts +4 -1
- package/src/core/workflow/types.ts +62 -2
- package/src/core/workflow/worker-access.ts +525 -0
- package/src/lib/browser/batch-steps.ts +79 -0
- package/src/lib/browser/collect/collector.ts +306 -0
- package/src/lib/browser/collect/fields.ts +87 -0
- package/src/lib/browser/collect/index.ts +47 -0
- package/src/lib/browser/collect/page-driver.ts +327 -0
- package/src/lib/browser/collect/presets.ts +237 -0
- package/src/lib/browser/collect/scroll-plan.ts +179 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/page-review-judge.ts +16 -0
- package/src/lib/browser/qa-run-contracts.ts +11 -0
- package/src/lib/browser/qa-run.ts +1 -0
- package/src/lib/browser/windows-chrome.ts +227 -0
- package/src/lib/claude-desktop-share.ts +511 -0
- package/src/lib/disk-usage.ts +347 -0
- package/src/lib/docs-lint.ts +8 -3
- package/src/lib/guarded-remove.ts +376 -0
- package/src/lib/instructions/templates.ts +8 -8
- package/src/lib/tunnel/error-page.ts +25 -9
- package/src/lib/tunnel/gate.ts +22 -1
- package/src/lib/tunnel/path-scope.ts +73 -0
- package/src/lib/tunnel/state.ts +20 -3
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeded randomness and the human-like scroll schedule for `browse --collect`.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is pure: given a seed and a pacing policy, the plan for each
|
|
5
|
+
* step (wheel notches, pauses, reading pauses, small scroll-backs) is fully
|
|
6
|
+
* reproducible, which is what the unit tests rely on.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A random source returning floats in [0, 1). */
|
|
10
|
+
export type Rng = () => number;
|
|
11
|
+
|
|
12
|
+
/** mulberry32: a small, fast, well-distributed 32-bit seeded generator. */
|
|
13
|
+
export function createRng(seed: number): Rng {
|
|
14
|
+
let a = seed >>> 0;
|
|
15
|
+
return () => {
|
|
16
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
17
|
+
let t = a;
|
|
18
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
19
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
20
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A fresh 32-bit seed for runs that did not pass one. */
|
|
25
|
+
export function randomSeed(): number {
|
|
26
|
+
return Math.floor(Math.random() * 0x1_0000_0000) >>> 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Range {
|
|
30
|
+
min: number;
|
|
31
|
+
max: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function uniform(rng: Rng, range: Range): number {
|
|
35
|
+
return range.min + (range.max - range.min) * rng();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ScrollPacing {
|
|
39
|
+
/** Scroll distance per step, as a fraction of the viewport height. */
|
|
40
|
+
step: Range;
|
|
41
|
+
/** Pause after each step, in ms. */
|
|
42
|
+
pauseMs: Range;
|
|
43
|
+
/** Probability that a step is followed by a longer reading pause instead. */
|
|
44
|
+
readChance: number;
|
|
45
|
+
/** Reading pause, in ms. */
|
|
46
|
+
readPauseMs: Range;
|
|
47
|
+
/** Probability that a step starts with a small scroll back up. */
|
|
48
|
+
backChance: number;
|
|
49
|
+
/** Scroll-back distance, as a fraction of the viewport height. */
|
|
50
|
+
back: Range;
|
|
51
|
+
/** Size of one wheel notch, in px. */
|
|
52
|
+
tickPx: Range;
|
|
53
|
+
/** Gap between wheel notches, in ms. */
|
|
54
|
+
tickGapMs: Range;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const DEFAULT_SCROLL_PACING: ScrollPacing = {
|
|
58
|
+
step: { min: 0.6, max: 1.1 },
|
|
59
|
+
pauseMs: { min: 1_200, max: 3_500 },
|
|
60
|
+
readChance: 0.12,
|
|
61
|
+
readPauseMs: { min: 4_000, max: 9_000 },
|
|
62
|
+
backChance: 0.08,
|
|
63
|
+
back: { min: 0.1, max: 0.3 },
|
|
64
|
+
tickPx: { min: 80, max: 140 },
|
|
65
|
+
tickGapMs: { min: 12, max: 45 },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Upper bounds that keep every pause and step bounded. */
|
|
69
|
+
export const PACING_LIMITS = {
|
|
70
|
+
maxStepFraction: 3,
|
|
71
|
+
maxPauseMs: 120_000,
|
|
72
|
+
} as const;
|
|
73
|
+
|
|
74
|
+
export interface WheelTick {
|
|
75
|
+
/** Positive scrolls down, negative scrolls up. */
|
|
76
|
+
deltaY: number;
|
|
77
|
+
/** Wait after this notch, in ms. */
|
|
78
|
+
gapMs: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ScrollStepPlan {
|
|
82
|
+
/** Optional scroll-back notches run before the main step (negative deltas). */
|
|
83
|
+
back: WheelTick[];
|
|
84
|
+
/** Pause after the scroll-back, in ms (0 when there is no scroll-back). */
|
|
85
|
+
backPauseMs: number;
|
|
86
|
+
/** Main downward notches. */
|
|
87
|
+
ticks: WheelTick[];
|
|
88
|
+
/** Total downward distance, in px. */
|
|
89
|
+
deltaY: number;
|
|
90
|
+
/** Pause after the main step, in ms. */
|
|
91
|
+
pauseMs: number;
|
|
92
|
+
/** True when the pause is a longer reading pause. */
|
|
93
|
+
reading: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Split a distance into wheel notches the way a mouse wheel reports them. */
|
|
97
|
+
export function wheelTicks(rng: Rng, pacing: ScrollPacing, distance: number): WheelTick[] {
|
|
98
|
+
const sign = distance < 0 ? -1 : 1;
|
|
99
|
+
let remaining = Math.round(Math.abs(distance));
|
|
100
|
+
const ticks: WheelTick[] = [];
|
|
101
|
+
while (remaining > 0) {
|
|
102
|
+
const size = Math.min(remaining, Math.max(1, Math.round(uniform(rng, pacing.tickPx))));
|
|
103
|
+
remaining -= size;
|
|
104
|
+
ticks.push({ deltaY: sign * size, gapMs: Math.round(uniform(rng, pacing.tickGapMs)) });
|
|
105
|
+
}
|
|
106
|
+
return ticks;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function planScrollStep(
|
|
110
|
+
rng: Rng,
|
|
111
|
+
pacing: ScrollPacing,
|
|
112
|
+
viewportHeight: number,
|
|
113
|
+
): ScrollStepPlan {
|
|
114
|
+
const height = Math.max(1, viewportHeight);
|
|
115
|
+
let back: WheelTick[] = [];
|
|
116
|
+
let backPauseMs = 0;
|
|
117
|
+
if (rng() < pacing.backChance) {
|
|
118
|
+
back = wheelTicks(rng, pacing, -uniform(rng, pacing.back) * height);
|
|
119
|
+
backPauseMs = Math.round(uniform(rng, { min: 400, max: 1_100 }));
|
|
120
|
+
}
|
|
121
|
+
const deltaY = Math.max(1, Math.round(uniform(rng, pacing.step) * height));
|
|
122
|
+
const ticks = wheelTicks(rng, pacing, deltaY);
|
|
123
|
+
const reading = rng() < pacing.readChance;
|
|
124
|
+
const pauseMs = Math.round(uniform(rng, reading ? pacing.readPauseMs : pacing.pauseMs));
|
|
125
|
+
return { back, backPauseMs, ticks, deltaY, pauseMs, reading };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse `min-max` (or a single value meaning min = max). Throws a message that
|
|
130
|
+
* names the flag when the value is malformed or out of bounds.
|
|
131
|
+
*/
|
|
132
|
+
export function parseRange(raw: string, flag: string, bounds: { min: number; max: number }): Range {
|
|
133
|
+
const text = raw.trim();
|
|
134
|
+
const m = /^(\d+(?:\.\d+)?)(?:\s*-\s*(\d+(?:\.\d+)?))?$/.exec(text);
|
|
135
|
+
if (!m) throw new Error(`${flag} must be <min>-<max> or a single number (got "${raw}").`);
|
|
136
|
+
const min = Number(m[1]);
|
|
137
|
+
const max = m[2] === undefined ? min : Number(m[2]);
|
|
138
|
+
if (min > max) throw new Error(`${flag}: min (${min}) must not exceed max (${max}).`);
|
|
139
|
+
if (min < bounds.min || max > bounds.max) {
|
|
140
|
+
throw new Error(`${flag} must stay within ${bounds.min}-${bounds.max} (got "${raw}").`);
|
|
141
|
+
}
|
|
142
|
+
return { min, max };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function parseProbability(raw: string, flag: string): number {
|
|
146
|
+
const n = Number(raw);
|
|
147
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
148
|
+
throw new Error(`${flag} must be a number from 0 to 1 (got "${raw}").`);
|
|
149
|
+
}
|
|
150
|
+
return n;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Machine-wide pacing defaults from the environment, in the same style as the
|
|
155
|
+
* page-load pace gate (`HARNERY_PACE_*`):
|
|
156
|
+
*
|
|
157
|
+
* - `HARNERY_COLLECT_PAUSE_MS`: pause after each step, `min-max` in ms.
|
|
158
|
+
* - `HARNERY_COLLECT_STEP`: step size as a viewport fraction, `min-max`.
|
|
159
|
+
*
|
|
160
|
+
* Command flags override these.
|
|
161
|
+
*/
|
|
162
|
+
export function scrollPacingFromEnv(env: NodeJS.ProcessEnv = process.env): ScrollPacing {
|
|
163
|
+
const pacing: ScrollPacing = { ...DEFAULT_SCROLL_PACING };
|
|
164
|
+
const pause = env.HARNERY_COLLECT_PAUSE_MS?.trim();
|
|
165
|
+
if (pause) {
|
|
166
|
+
pacing.pauseMs = parseRange(pause, "HARNERY_COLLECT_PAUSE_MS", {
|
|
167
|
+
min: 0,
|
|
168
|
+
max: PACING_LIMITS.maxPauseMs,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const step = env.HARNERY_COLLECT_STEP?.trim();
|
|
172
|
+
if (step) {
|
|
173
|
+
pacing.step = parseRange(step, "HARNERY_COLLECT_STEP", {
|
|
174
|
+
min: 0.05,
|
|
175
|
+
max: PACING_LIMITS.maxStepFraction,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return pacing;
|
|
179
|
+
}
|
package/src/lib/browser/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "./page-review-pack.js";
|
|
26
26
|
import type { QaContext } from "./qa-plan.js";
|
|
27
27
|
import type { CritiqueReusePlan } from "./qa-reuse.js";
|
|
28
|
+
import type { QaRunCritiqueTileTiming } from "./qa-run-contracts.js";
|
|
28
29
|
import { loadQaSnapshot, type QaSnapshotStoreOptions } from "./qa-snapshot.js";
|
|
29
30
|
|
|
30
31
|
export interface JudgePageReviewPackOptions {
|
|
@@ -62,6 +63,7 @@ export interface JudgedContext extends PageReviewCritiqueRecord {
|
|
|
62
63
|
export interface JudgePageReviewPackResult {
|
|
63
64
|
contexts: JudgedContext[];
|
|
64
65
|
pool: { concurrency: number; wall_time_ms: number; provider: string };
|
|
66
|
+
tile_timings: QaRunCritiqueTileTiming[];
|
|
65
67
|
tiles_total: number;
|
|
66
68
|
tiles_reviewed: number;
|
|
67
69
|
tiles_reused: number;
|
|
@@ -261,6 +263,7 @@ export async function judgePageReviewPack(
|
|
|
261
263
|
|
|
262
264
|
let meta: Record<string, unknown> | undefined;
|
|
263
265
|
let concurrency = 0;
|
|
266
|
+
const tileTimings: Array<QaRunCritiqueTileTiming | undefined> = new Array(work.length);
|
|
264
267
|
if (provider && work.length > 0) {
|
|
265
268
|
concurrency = Math.max(
|
|
266
269
|
1,
|
|
@@ -277,12 +280,15 @@ export async function judgePageReviewPack(
|
|
|
277
280
|
const entry = perContext[item.contextIndex];
|
|
278
281
|
if (!entry) continue;
|
|
279
282
|
entry.judged.add(item.tile.id);
|
|
283
|
+
const tileStarted = performance.now();
|
|
284
|
+
let tileOutcome: QaRunCritiqueTileTiming["outcome"] = "passed";
|
|
280
285
|
try {
|
|
281
286
|
const found = await provider({ url: item.url, rubric: item.rubric, tile: item.tile });
|
|
282
287
|
for (const finding of found) {
|
|
283
288
|
entry.findings.push({ ...finding, tile: item.tile.index, tile_id: item.tile.id });
|
|
284
289
|
}
|
|
285
290
|
} catch (err: unknown) {
|
|
291
|
+
tileOutcome = "failed";
|
|
286
292
|
entry.findings.push({
|
|
287
293
|
tile: item.tile.index,
|
|
288
294
|
tile_id: item.tile.id,
|
|
@@ -290,6 +296,13 @@ export async function judgePageReviewPack(
|
|
|
290
296
|
category: "provider-error",
|
|
291
297
|
description: `critique provider failed on ${item.tile.label}: ${err instanceof Error ? err.message : String(err)}`,
|
|
292
298
|
});
|
|
299
|
+
} finally {
|
|
300
|
+
tileTimings[i] = {
|
|
301
|
+
context_id: entry.record.id,
|
|
302
|
+
tile_id: item.tile.id,
|
|
303
|
+
duration_ms: Math.max(0, Math.round(performance.now() - tileStarted)),
|
|
304
|
+
outcome: tileOutcome,
|
|
305
|
+
};
|
|
293
306
|
}
|
|
294
307
|
}
|
|
295
308
|
};
|
|
@@ -360,6 +373,9 @@ export async function judgePageReviewPack(
|
|
|
360
373
|
return {
|
|
361
374
|
contexts,
|
|
362
375
|
pool: { concurrency, wall_time_ms: Date.now() - started, provider: label },
|
|
376
|
+
tile_timings: tileTimings.filter(
|
|
377
|
+
(timing): timing is QaRunCritiqueTileTiming => timing !== undefined,
|
|
378
|
+
),
|
|
363
379
|
tiles_total: tilesTotal,
|
|
364
380
|
tiles_reviewed: tilesReviewed,
|
|
365
381
|
tiles_reused: tilesReused,
|
|
@@ -145,6 +145,15 @@ export interface QaRunCritiqueLatency {
|
|
|
145
145
|
p95: number;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/** Wall time for one tile's complete vision call, including provider retries
|
|
149
|
+
* and failover. Reused and unjudged tiles have no call and no timing entry. */
|
|
150
|
+
export interface QaRunCritiqueTileTiming {
|
|
151
|
+
context_id: string;
|
|
152
|
+
tile_id: string;
|
|
153
|
+
duration_ms: number;
|
|
154
|
+
outcome: "passed" | "failed";
|
|
155
|
+
}
|
|
156
|
+
|
|
148
157
|
/** What share of the page the critique tiles covered, lifted from the browse
|
|
149
158
|
* envelope. `capped` means the tiler dropped bands past its per-context
|
|
150
159
|
* maximum; in signoff mode that is a blocker, in review mode a flag. Across
|
|
@@ -188,6 +197,8 @@ export interface QaRunCritiquePool {
|
|
|
188
197
|
wall_time_ms: number;
|
|
189
198
|
provider: string;
|
|
190
199
|
latency_ms?: Record<string, QaRunCritiqueLatency>;
|
|
200
|
+
/** One entry per attempted tile, in context and tile order. */
|
|
201
|
+
tile_timings?: QaRunCritiqueTileTiming[];
|
|
191
202
|
}
|
|
192
203
|
|
|
193
204
|
/** Where the run's page review pack lives: the on-disk evidence an agent can
|
|
@@ -1397,6 +1397,7 @@ export async function runQaMatrix(options: QaRunMatrixOptions): Promise<QaRunRes
|
|
|
1397
1397
|
wall_time_ms: judged.pool.wall_time_ms,
|
|
1398
1398
|
provider: judged.pool.provider,
|
|
1399
1399
|
...(latency ? { latency_ms: latency } : {}),
|
|
1400
|
+
tile_timings: judged.tile_timings,
|
|
1400
1401
|
};
|
|
1401
1402
|
for (const row of judged.contexts) {
|
|
1402
1403
|
judgedById.set(row.context_id, row);
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { Cookie } from "../cookies/client.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sign-in through the Windows host's Google Chrome when running under WSL.
|
|
9
|
+
*
|
|
10
|
+
* Some sign-in flows (X is the reported case) reject every browser running
|
|
11
|
+
* inside WSL, including a plain Linux Google Chrome with nothing attached:
|
|
12
|
+
* the sign-in request comes back "429 Too Many Requests" while the same
|
|
13
|
+
* account signs in from the Windows desktop browser without trouble. The
|
|
14
|
+
* Windows Chrome is the client those flows accept, so `browse --login
|
|
15
|
+
* --plain` under WSL opens it on a dedicated Windows-side profile. The
|
|
16
|
+
* person signs in there with nothing attached. Afterwards the profile is
|
|
17
|
+
* reopened headless for a moment, its cookies for the sign-in site are read
|
|
18
|
+
* through the DevTools protocol, and they are merged into the shared cookie
|
|
19
|
+
* store that later `browse` runs attach. The Windows profile cannot be shared
|
|
20
|
+
* with Linux Chromium directly because Windows encrypts its cookie database
|
|
21
|
+
* with keys only Windows can read.
|
|
22
|
+
*
|
|
23
|
+
* Under WSL2's default NAT networking the Windows loopback port is not
|
|
24
|
+
* reachable from Linux, so the DevTools conversation runs inside a short
|
|
25
|
+
* PowerShell script on the Windows side and prints the result to stdout.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const WINDOWS_CHROME_CANDIDATES = [
|
|
29
|
+
"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
|
|
30
|
+
"/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** Linux path of the Windows host's Google Chrome, or `undefined`. */
|
|
34
|
+
export function windowsChromePath(
|
|
35
|
+
exists: (path: string) => boolean = existsSync,
|
|
36
|
+
localAppDataLinux?: string,
|
|
37
|
+
): string | undefined {
|
|
38
|
+
const candidates = [...WINDOWS_CHROME_CANDIDATES];
|
|
39
|
+
if (localAppDataLinux)
|
|
40
|
+
candidates.push(join(localAppDataLinux, "Google", "Chrome", "Application", "chrome.exe"));
|
|
41
|
+
return candidates.find((candidate) => exists(candidate));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `%LOCALAPPDATA%` as a Windows path, read through cmd.exe interop. */
|
|
45
|
+
export function windowsLocalAppData(): string | undefined {
|
|
46
|
+
const result = spawnSync("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
timeout: 10_000,
|
|
49
|
+
});
|
|
50
|
+
const value = (result.stdout ?? "").replace(/\r/g, "").trim();
|
|
51
|
+
return result.status === 0 && /^[A-Za-z]:\\/.test(value) ? value : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Windows-side profile used only for `--login --plain` sign-ins under WSL. */
|
|
55
|
+
export function windowsPlainProfile(localAppData: string): string {
|
|
56
|
+
return `${localAppData}\\harnery\\plain-login-profile`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The registrable domain to harvest for a sign-in URL: the last two labels of
|
|
61
|
+
* the host (`x.com` from `https://x.com/i/flow/login`). Cookies are kept when
|
|
62
|
+
* their domain equals it or is a subdomain of it, so a sign-in that also sets
|
|
63
|
+
* cookies for an identity provider does not copy those into the store.
|
|
64
|
+
*/
|
|
65
|
+
export function harvestDomain(url: string): string {
|
|
66
|
+
const host = new URL(url).hostname.toLowerCase();
|
|
67
|
+
const labels = host.split(".").filter(Boolean);
|
|
68
|
+
return labels.length <= 2 ? host : labels.slice(-2).join(".");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function cookieInDomain(cookieDomain: string, domain: string): boolean {
|
|
72
|
+
const bare = cookieDomain.replace(/^\./, "").toLowerCase();
|
|
73
|
+
return bare === domain || bare.endsWith(`.${domain}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Shape `Storage.getCookies` returns; extra fields are ignored. */
|
|
77
|
+
interface CdpCookie {
|
|
78
|
+
name: string;
|
|
79
|
+
value: string;
|
|
80
|
+
domain: string;
|
|
81
|
+
path: string;
|
|
82
|
+
expires: number;
|
|
83
|
+
size?: number;
|
|
84
|
+
httpOnly: boolean;
|
|
85
|
+
secure: boolean;
|
|
86
|
+
session?: boolean;
|
|
87
|
+
sameSite?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Keep the sign-in site's cookies and normalize them to the store's shape. */
|
|
91
|
+
export function cookiesForDomain(cdpCookies: CdpCookie[], domain: string): Cookie[] {
|
|
92
|
+
return cdpCookies
|
|
93
|
+
.filter((cookie) => cookieInDomain(cookie.domain, domain))
|
|
94
|
+
.map((cookie) => ({
|
|
95
|
+
name: cookie.name,
|
|
96
|
+
value: cookie.value,
|
|
97
|
+
domain: cookie.domain,
|
|
98
|
+
path: cookie.path || "/",
|
|
99
|
+
expires: typeof cookie.expires === "number" ? cookie.expires : -1,
|
|
100
|
+
...(cookie.size === undefined ? {} : { size: cookie.size }),
|
|
101
|
+
httpOnly: Boolean(cookie.httpOnly),
|
|
102
|
+
secure: Boolean(cookie.secure),
|
|
103
|
+
...(cookie.session === undefined ? {} : { session: cookie.session }),
|
|
104
|
+
...(cookie.sameSite ? { sameSite: cookie.sameSite } : {}),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* PowerShell that does the whole read on the Windows side: start Chrome
|
|
110
|
+
* headless on the profile, wait for the DevToolsActivePort file it writes,
|
|
111
|
+
* read every cookie with `Storage.getCookies`, write the raw response to
|
|
112
|
+
* stdout, and close the browser. WSL never touches the profile folder, which
|
|
113
|
+
* it may not be able to list. Every wait carries a deadline.
|
|
114
|
+
*/
|
|
115
|
+
export function harvestScript(): string {
|
|
116
|
+
return [
|
|
117
|
+
"param([string]$Chrome, [string]$Profile, [int]$StartupSeconds = 30)",
|
|
118
|
+
"$ErrorActionPreference = 'Stop'",
|
|
119
|
+
"$portFile = Join-Path $Profile 'DevToolsActivePort'",
|
|
120
|
+
"if (Test-Path $portFile) { Remove-Item $portFile -Force }",
|
|
121
|
+
"$args = @('--headless=new', '--no-first-run', '--no-default-browser-check', '--remote-debugging-port=0', \"--user-data-dir=$Profile\", 'about:blank')",
|
|
122
|
+
"$proc = Start-Process -FilePath $Chrome -ArgumentList $args -PassThru -WindowStyle Hidden",
|
|
123
|
+
"$deadline = (Get-Date).AddSeconds($StartupSeconds)",
|
|
124
|
+
"while (-not (Test-Path $portFile)) { if ((Get-Date) -gt $deadline) { throw 'Windows Chrome did not open its DevTools port in time.' }; if ($proc.HasExited) { throw 'Windows Chrome exited before opening its DevTools port.' }; Start-Sleep -Milliseconds 250 }",
|
|
125
|
+
"Start-Sleep -Milliseconds 200",
|
|
126
|
+
"$lines = Get-Content $portFile",
|
|
127
|
+
"$port = [int]$lines[0]; $path = $lines[1]",
|
|
128
|
+
"$ws = New-Object System.Net.WebSockets.ClientWebSocket",
|
|
129
|
+
"$cts = New-Object System.Threading.CancellationTokenSource 20000",
|
|
130
|
+
'$ws.ConnectAsync([Uri]("ws://127.0.0.1:$port$path"), $cts.Token).Wait()',
|
|
131
|
+
"function Send($obj) { $b = [Text.Encoding]::UTF8.GetBytes(($obj | ConvertTo-Json -Compress)); $ws.SendAsync([ArraySegment[byte]]$b, 'Text', $true, $cts.Token).Wait() }",
|
|
132
|
+
"function Recv() { $buf = New-Object byte[] 65536; $ms = New-Object IO.MemoryStream; do { $r = $ws.ReceiveAsync([ArraySegment[byte]]$buf, $cts.Token).Result; $ms.Write($buf, 0, $r.Count) } while (-not $r.EndOfMessage); [Text.Encoding]::UTF8.GetString($ms.ToArray()) }",
|
|
133
|
+
"Send @{ id = 1; method = 'Storage.getCookies' }",
|
|
134
|
+
"$out = $null",
|
|
135
|
+
"while ($null -eq $out) { $m = Recv; if ($m -match '\"id\":1[,}]') { $out = $m } }",
|
|
136
|
+
"[Console]::Out.Write($out)",
|
|
137
|
+
"try { Send @{ id = 2; method = 'Browser.close' } } catch {}",
|
|
138
|
+
"if (-not $proc.WaitForExit(10000)) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue }",
|
|
139
|
+
].join("\r\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Reopen the Windows profile headless, read its cookies for `domain`, and
|
|
144
|
+
* close it. Returns the cookies in the store's shape.
|
|
145
|
+
*/
|
|
146
|
+
export function harvestWindowsChromeCookies(input: {
|
|
147
|
+
chromeWindows: string;
|
|
148
|
+
profileWindows: string;
|
|
149
|
+
domain: string;
|
|
150
|
+
startupTimeoutSeconds?: number;
|
|
151
|
+
}): Cookie[] {
|
|
152
|
+
const scriptDir = join(tmpdir(), "harnery-windows-chrome");
|
|
153
|
+
mkdirSync(scriptDir, { recursive: true });
|
|
154
|
+
const scriptLinux = join(scriptDir, `harvest-cookies-${process.pid}.ps1`);
|
|
155
|
+
writeFileSync(scriptLinux, harvestScript(), { mode: 0o600 });
|
|
156
|
+
try {
|
|
157
|
+
const scriptWindows = spawnSync("wslpath", ["-w", scriptLinux], {
|
|
158
|
+
encoding: "utf8",
|
|
159
|
+
}).stdout.trim();
|
|
160
|
+
const result = spawnSync(
|
|
161
|
+
"powershell.exe",
|
|
162
|
+
[
|
|
163
|
+
"-NoProfile",
|
|
164
|
+
"-ExecutionPolicy",
|
|
165
|
+
"Bypass",
|
|
166
|
+
"-File",
|
|
167
|
+
scriptWindows,
|
|
168
|
+
"-Chrome",
|
|
169
|
+
input.chromeWindows,
|
|
170
|
+
"-Profile",
|
|
171
|
+
input.profileWindows,
|
|
172
|
+
"-StartupSeconds",
|
|
173
|
+
String(input.startupTimeoutSeconds ?? 30),
|
|
174
|
+
],
|
|
175
|
+
{ encoding: "utf8", timeout: 90_000, maxBuffer: 64 * 1024 * 1024 },
|
|
176
|
+
);
|
|
177
|
+
if (result.status !== 0) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Reading cookies from Windows Chrome failed: ${(result.stderr || result.stdout || "no output").trim().slice(0, 400)}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const parsed = JSON.parse(result.stdout) as {
|
|
183
|
+
result?: { cookies?: CdpCookie[] };
|
|
184
|
+
error?: { message?: string };
|
|
185
|
+
};
|
|
186
|
+
if (!parsed.result?.cookies)
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Windows Chrome returned no cookie list: ${parsed.error?.message ?? "unknown response"}`,
|
|
189
|
+
);
|
|
190
|
+
return cookiesForDomain(parsed.result.cookies, input.domain);
|
|
191
|
+
} finally {
|
|
192
|
+
rmSync(scriptLinux, { force: true });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Close any Chrome still running on `profileWindows`, for when the person ends
|
|
198
|
+
* the sign-in from the terminal while the window is open. The profile must be
|
|
199
|
+
* closed before its cookies can be read. Closing the window first lets Chrome
|
|
200
|
+
* write recent cookies to disk; a forced stop is the fallback after 15 s.
|
|
201
|
+
*/
|
|
202
|
+
export function stopWindowsChromeProfile(profileWindows: string): void {
|
|
203
|
+
const escaped = profileWindows.replace(/'/g, "''");
|
|
204
|
+
const select = `Get-CimInstance Win32_Process -Filter "Name='chrome.exe'" | Where-Object { $_.CommandLine -like '*${escaped}*' }`;
|
|
205
|
+
spawnSync(
|
|
206
|
+
"powershell.exe",
|
|
207
|
+
[
|
|
208
|
+
"-NoProfile",
|
|
209
|
+
"-Command",
|
|
210
|
+
[
|
|
211
|
+
`$ids = @(${select} | ForEach-Object { $_.ProcessId })`,
|
|
212
|
+
"foreach ($id in $ids) { $p = Get-Process -Id $id -ErrorAction SilentlyContinue; if ($p -and $p.MainWindowHandle -ne 0) { [void]$p.CloseMainWindow() } }",
|
|
213
|
+
"$deadline = (Get-Date).AddSeconds(15)",
|
|
214
|
+
`while ((Get-Date) -lt $deadline -and @(${select}).Count -gt 0) { Start-Sleep -Milliseconds 250 }`,
|
|
215
|
+
`${select} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }`,
|
|
216
|
+
].join("; "),
|
|
217
|
+
],
|
|
218
|
+
{ encoding: "utf8", timeout: 45_000 },
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Windows form of a `/mnt/<drive>/...` path. */
|
|
223
|
+
export function wslPathToWindows(linuxPath: string): string {
|
|
224
|
+
const match = /^\/mnt\/([a-z])\/(.*)$/.exec(linuxPath);
|
|
225
|
+
if (!match) throw new Error(`Not a /mnt/<drive> path: ${linuxPath}`);
|
|
226
|
+
return `${(match[1] as string).toUpperCase()}:\\${(match[2] ?? "").replace(/\//g, "\\")}`;
|
|
227
|
+
}
|