@reticlehq/server 3.3.0 → 3.5.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/LICENSE-ENTERPRISE +1 -1
- package/README.md +3 -3
- package/SKILL.md +12 -14
- package/dist/chromium-launch-options.d.ts +4 -3
- package/dist/chromium-launch-options.js +6 -2
- package/dist/command/cli/adhoc-suite.d.ts +22 -0
- package/dist/command/cli/adhoc-suite.js +90 -19
- package/dist/command/cli/adhoc-verdict.d.ts +35 -0
- package/dist/command/cli/adhoc-verdict.js +90 -3
- package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
- package/dist/command/cli/answers/gate-hook-message.js +6 -0
- package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
- package/dist/command/cli/auth/cloud-keystore.js +0 -3
- package/dist/command/cli/auth/cloud-session.d.ts +6 -2
- package/dist/command/cli/auth/cloud-session.js +2 -0
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +7 -5
- package/dist/command/cli/cli-flow-commands.d.ts +3 -6
- package/dist/command/cli/cli-flow-commands.js +67 -4
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +24 -4
- package/dist/command/cli/cli-update-commands.js +8 -1
- package/dist/command/cli/cli-verify.d.ts +24 -3
- package/dist/command/cli/cli-verify.js +54 -11
- package/dist/command/cli/cloud-cli.js +64 -2
- package/dist/command/cli/cloud-kit.js +3 -5
- package/dist/command/cli/cloud-login.d.ts +1 -1
- package/dist/command/cli/cloud-login.js +27 -8
- package/dist/command/cli/demo/demo-page.js +6 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
- package/dist/command/cli/launch/cli-launch.d.ts +8 -0
- package/dist/command/cli/launch/cli-launch.js +21 -9
- package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
- package/dist/command/cli/launch/sdk-version-match.js +206 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
- package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
- package/dist/command/cli/ports/resolve/cli-port.js +125 -15
- package/dist/command/cli/setup-mcp-cli.js +21 -13
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +21 -7
- package/dist/command/cli.js +79 -11
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
- package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
- package/dist/command/daemon/daemon-resolve.d.ts +9 -0
- package/dist/command/daemon/daemon-resolve.js +1 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
- package/dist/command/dev/stale-issue-guard.d.ts +3 -4
- package/dist/command/dev/stale-issue-guard.js +8 -8
- package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
- package/dist/command/setup/bringup/connect-lease.js +38 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
- package/dist/command/setup/bringup/dev-server-files.js +131 -0
- package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
- package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
- package/dist/command/setup/bringup/dev-server-wait.js +1 -1
- package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
- package/dist/command/setup/bringup/ensure-daemon.js +135 -16
- package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
- package/dist/command/setup/bringup/static-page-server.js +151 -0
- package/dist/command/setup/desktop-shape.d.ts +4 -0
- package/dist/command/setup/desktop-shape.js +18 -2
- package/dist/command/setup/init/init-port.d.ts +15 -0
- package/dist/command/setup/init/init-port.js +44 -0
- package/dist/command/setup/init/init-runtime.d.ts +1 -1
- package/dist/command/setup/init/init-runtime.js +91 -9
- package/dist/command/setup/node-effects.d.ts +27 -14
- package/dist/command/setup/node-effects.js +86 -45
- package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
- package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
- package/dist/command/setup/probe/page-probe.d.ts +13 -2
- package/dist/command/setup/probe/page-probe.js +18 -3
- package/dist/command/setup/run-setup.d.ts +28 -1
- package/dist/command/setup/run-setup.js +103 -15
- package/dist/command/setup/session-pick.d.ts +13 -0
- package/dist/command/setup/session-pick.js +20 -0
- package/dist/command/setup/setup-command.d.ts +2 -14
- package/dist/command/setup/setup-command.js +105 -31
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +17 -8
- package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
- package/dist/command/setup/terminal/wait-progress.js +17 -0
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- package/dist/features/crawl/crawl-empty.js +1 -1
- package/dist/features/crawl/crawl-tools.d.ts +6 -0
- package/dist/features/crawl/crawl-tools.js +57 -0
- package/dist/features/crawl/crawl.js +171 -157
- package/dist/features/exhaust/app-coverage.d.ts +33 -0
- package/dist/features/exhaust/app-coverage.js +31 -0
- package/dist/features/exhaust/code-coverage.d.ts +47 -0
- package/dist/features/exhaust/code-coverage.js +77 -0
- package/dist/features/exhaust/exhaust-run.d.ts +33 -0
- package/dist/features/exhaust/exhaust-run.js +82 -0
- package/dist/features/exhaust/explorer.d.ts +103 -0
- package/dist/features/exhaust/explorer.js +195 -0
- package/dist/features/exhaust/failure-branches.d.ts +33 -0
- package/dist/features/exhaust/failure-branches.js +92 -0
- package/dist/features/exhaust/ledger.d.ts +202 -0
- package/dist/features/exhaust/ledger.js +252 -0
- package/dist/features/exhaust/session-fold.d.ts +31 -0
- package/dist/features/exhaust/session-fold.js +79 -0
- package/dist/features/exhaust/session-port.d.ts +36 -0
- package/dist/features/exhaust/session-port.js +102 -0
- package/dist/features/harness/drive-report.d.ts +0 -9
- package/dist/features/harness/drive-report.js +18 -0
- package/dist/features/harness/driver.js +4 -7
- package/dist/features/harness/fill-values.d.ts +10 -0
- package/dist/features/harness/fill-values.js +50 -11
- package/dist/features/harness/jev-driver.js +30 -13
- package/dist/features/harness/openai-driver.d.ts +12 -0
- package/dist/features/harness/openai-driver.js +21 -12
- package/dist/features/harness/platform-config.js +5 -6
- package/dist/features/license/license.d.ts +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +56 -19
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- package/dist/judgement/domain/domain-model.js +3 -1
- package/dist/judgement/oracles/self-instrument.js +4 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
- package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
- package/dist/judgement/runs/verification-sync.d.ts +3 -1
- package/dist/judgement/runs/verification-sync.js +1 -1
- package/dist/language/flows/change/affected.d.ts +9 -0
- package/dist/language/flows/change/affected.js +35 -0
- package/dist/language/flows/change/gate.d.ts +20 -0
- package/dist/language/flows/change/gate.js +12 -1
- package/dist/language/flows/flow-author.d.ts +7 -0
- package/dist/language/flows/flow-author.js +19 -0
- package/dist/language/flows/flow-expect-grammar.js +1 -1
- package/dist/language/flows/flow-journey.d.ts +54 -0
- package/dist/language/flows/flow-journey.js +119 -0
- package/dist/language/flows/flow-replay-run.d.ts +2 -3
- package/dist/language/flows/flow-replay-run.js +13 -47
- package/dist/language/flows/flow-replay.d.ts +22 -8
- package/dist/language/flows/flow-replay.js +55 -9
- package/dist/language/flows/flow-result.d.ts +2 -0
- package/dist/language/flows/flow-result.js +25 -0
- package/dist/language/flows/flow-tools.js +17 -5
- package/dist/language/flows/flows.d.ts +10 -1
- package/dist/language/flows/flows.js +90 -14
- package/dist/language/flows/recording/replay-halt.js +3 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
- package/dist/language/flows/recording/tape/recordings.js +55 -1
- package/dist/language/flows/replay.d.ts +19 -0
- package/dist/language/flows/replay.js +42 -6
- package/dist/language/flows/verify-change-tools.js +5 -2
- package/dist/launch-chromium.d.ts +80 -0
- package/dist/launch-chromium.js +218 -0
- package/dist/memory/cloud/cloud-config.d.ts +11 -2
- package/dist/memory/cloud/cloud-config.js +47 -7
- package/dist/memory/cloud/cloud-sync.d.ts +4 -1
- package/dist/memory/cloud/cloud-sync.js +6 -9
- package/dist/memory/cloud/harness-offer.js +5 -6
- package/dist/memory/cloud/harness-switch.js +5 -6
- package/dist/memory/cloud/sync-cycle.d.ts +77 -3
- package/dist/memory/cloud/sync-cycle.js +325 -55
- package/dist/memory/cloud/sync-daemon.js +24 -20
- package/dist/memory/cloud/sync-disk.js +16 -2
- package/dist/memory/intent/intent-shard.d.ts +10 -32
- package/dist/memory/intent/intent-shard.js +0 -10
- package/dist/memory/intent/intent-tools.js +8 -3
- package/dist/memory/journal/drive-flow.d.ts +18 -13
- package/dist/memory/journal/drive-flow.js +45 -37
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
- package/dist/memory/journal/session-end.d.ts +9 -1
- package/dist/memory/journal/session-end.js +24 -12
- package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
- package/dist/memory/project/artifact-root-resolver.js +46 -19
- package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
- package/dist/memory/project/dir/reticle-dir.js +1 -0
- package/dist/memory/project/project-tools.js +3 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
- package/dist/portal/bridge/auth-failure-reason.js +4 -2
- package/dist/portal/bridge/bridge.d.ts +12 -3
- package/dist/portal/bridge/bridge.js +49 -3
- package/dist/portal/input/js-coverage.d.ts +25 -0
- package/dist/portal/input/js-coverage.js +30 -0
- package/dist/portal/input/real-input.d.ts +11 -0
- package/dist/portal/input/real-input.js +29 -4
- package/dist/portal/input/viewport-tools.js +16 -6
- package/dist/portal/pool/playwright-launcher.d.ts +6 -0
- package/dist/portal/pool/playwright-launcher.js +20 -13
- package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
- package/dist/portal/session/dev-server/dev-command.js +30 -12
- package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
- package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
- package/dist/portal/session/dev-server/route-status-probe.js +57 -0
- package/dist/portal/session/facts/observed-state.d.ts +11 -0
- package/dist/portal/session/facts/observed-state.js +34 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
- package/dist/portal/session/no-session-diagnosis.js +62 -5
- package/dist/portal/session/no-session-next-action.d.ts +13 -0
- package/dist/portal/session/no-session-next-action.js +48 -9
- package/dist/portal/session/no-session-watch.d.ts +18 -0
- package/dist/portal/session/no-session-watch.js +88 -2
- package/dist/portal/session/session-health.js +3 -2
- package/dist/portal/session/session-manager.d.ts +20 -6
- package/dist/portal/session/session-manager.js +19 -3
- package/dist/portal/session/session-reaper.d.ts +28 -0
- package/dist/portal/session/session-reaper.js +49 -1
- package/dist/portal/session/session.d.ts +10 -8
- package/dist/portal/session/session.js +24 -32
- package/dist/surface/http-server.d.ts +3 -2
- package/dist/surface/http-server.js +8 -5
- package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
- package/dist/surface/mcp/mcp-proxy.js +2 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- package/dist/surface/mcp/server-instructions.js +9 -7
- package/dist/surface/tools/act/after-match.d.ts +17 -0
- package/dist/surface/tools/act/after-match.js +30 -0
- package/dist/surface/tools/act/durable.d.ts +69 -0
- package/dist/surface/tools/act/durable.js +139 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
- package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
- package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
- package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
- package/dist/surface/tools/act/proved-controls.d.ts +9 -0
- package/dist/surface/tools/act/proved-controls.js +11 -0
- package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
- package/dist/surface/tools/act/sequence-grade.js +32 -1
- package/dist/surface/tools/act-capsule.d.ts +7 -0
- package/dist/surface/tools/act-capsule.js +46 -1
- package/dist/surface/tools/act-sequence-tool.js +25 -8
- package/dist/surface/tools/act-tools.js +28 -52
- package/dist/surface/tools/assert/assert-grade.js +12 -1
- package/dist/surface/tools/assert/assert-verdict.js +2 -2
- package/dist/surface/tools/browser-tools.js +14 -2
- package/dist/surface/tools/coverage-tools.js +27 -1
- package/dist/surface/tools/dynamic-tools.js +29 -2
- package/dist/surface/tools/error-recovery.js +5 -6
- package/dist/surface/tools/feedback-invite.js +6 -4
- package/dist/surface/tools/harness-explore.d.ts +3 -2
- package/dist/surface/tools/harness-explore.js +12 -7
- package/dist/surface/tools/invoke-tool.js +12 -8
- package/dist/surface/tools/lease-tools.js +15 -6
- package/dist/surface/tools/live-call-text.js +4 -1
- package/dist/surface/tools/observe-tools.js +9 -1
- package/dist/surface/tools/tool-kit.d.ts +10 -0
- package/dist/surface/tools/tools.js +4 -0
- package/dist/telemetry/argument-shape.d.ts +0 -2
- package/dist/telemetry/argument-shape.js +1 -5
- package/dist/telemetry/error-class.d.ts +2 -3
- package/dist/telemetry/error-class.js +2 -3
- package/dist/telemetry/harness-drive.d.ts +0 -2
- package/dist/telemetry/harness-drive.js +0 -4
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- package/dist/wire-journal.d.ts +3 -0
- package/dist/wire-journal.js +4 -0
- package/dist/workspace-packages.d.ts +0 -2
- package/dist/workspace-packages.js +1 -9
- package/docs/README.md +4 -2
- package/docs/actions.mdx +3 -4
- package/docs/agent-cheatsheet.md +4 -4
- package/docs/architecture.md +6 -6
- package/docs/best-practices.mdx +1 -1
- package/docs/capabilities.mdx +32 -1
- package/docs/cli/cloud.mdx +20 -8
- package/docs/cli/doctor.mdx +4 -0
- package/docs/cli/feedback.mdx +2 -2
- package/docs/cli/gate.mdx +9 -4
- package/docs/cli/update.mdx +10 -0
- package/docs/cli.mdx +22 -7
- package/docs/desktop.mdx +14 -10
- package/docs/environment.mdx +87 -0
- package/docs/faq.mdx +18 -16
- package/docs/first-drive.md +5 -13
- package/docs/flows.md +21 -2
- package/docs/frameworks.mdx +29 -10
- package/docs/getting-started.md +15 -5
- package/docs/http-transport.md +6 -3
- package/docs/human-control.md +2 -2
- package/docs/index.mdx +16 -6
- package/docs/install-agentic.mdx +2 -2
- package/docs/install-manual.mdx +1 -1
- package/docs/integration-patterns.md +3 -3
- package/docs/multi-agent-testing.md +1 -1
- package/docs/packages/babel-plugin.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/react.mdx +2 -2
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +4 -2
- package/docs/packages.mdx +9 -9
- package/docs/predicates.mdx +50 -5
- package/docs/quickstart.mdx +12 -10
- package/docs/recipes.mdx +2 -2
- package/docs/state-management.mdx +2 -2
- package/docs/system-map.md +1 -1
- package/docs/telemetry.md +1 -1
- package/docs/token-efficiency.md +3 -3
- package/docs/tools/act-and-wait.mdx +1 -0
- package/docs/tools/act-sequence.mdx +1 -1
- package/docs/tools/act.mdx +1 -1
- package/docs/tools/console.mdx +1 -1
- package/docs/tools/inspect.mdx +1 -1
- package/docs/tools/network.mdx +1 -1
- package/docs/tools/query.mdx +1 -1
- package/docs/tools/sessions.mdx +1 -1
- package/docs/tools/snapshot.mdx +1 -1
- package/docs/tools/state.mdx +1 -1
- package/docs/tools/wait-for.mdx +1 -1
- package/docs/usage.md +7 -7
- package/docs/use-cases.mdx +157 -0
- package/docs/vs-screenshots.mdx +3 -3
- package/docs/what-is-recorded.md +2 -2
- package/docs/what-is-reticle.mdx +17 -20
- package/docs/why-reticle.mdx +14 -21
- package/package.json +10 -9
- package/dist/judgement/capsule/minimize.d.ts +0 -12
- package/dist/judgement/capsule/minimize.js +0 -22
- package/dist/judgement/phenomena/phenomena.d.ts +0 -27
- package/dist/judgement/phenomena/phenomena.js +0 -101
- package/dist/language/flows/flow-report.d.ts +0 -22
- package/dist/language/flows/flow-report.js +0 -106
|
@@ -34,30 +34,57 @@ function daemonSitsInAProject(daemonRoot) {
|
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Every project this machine knows the directory of: discovered `.reticle.json` files first, then
|
|
39
|
+
* the user-level registry. Read on each call, since `init` can run in another terminal while the
|
|
40
|
+
* daemon is up.
|
|
41
|
+
*/
|
|
42
|
+
function knownProjectCandidates() {
|
|
43
|
+
let registry = emptyProjectRegistry();
|
|
44
|
+
try {
|
|
45
|
+
const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
|
|
46
|
+
registry = existsSync(path)
|
|
47
|
+
? parseProjectRegistry(JSON.parse(readFileSync(path, 'utf8')))
|
|
48
|
+
: registry;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// A cache that cannot be read is an empty cache, never an error: the daemon still resolves
|
|
52
|
+
// through discovery, and falls back to its own root exactly as it did before this existed.
|
|
53
|
+
}
|
|
54
|
+
let discovery = { found: [], searched: [] };
|
|
55
|
+
try {
|
|
56
|
+
discovery = discoverProjectConfigs(process.cwd());
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Same reasoning: a diagnostic search that throws must not take a tool call with it.
|
|
60
|
+
}
|
|
61
|
+
return projectCandidatesFrom(discovery, registry);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The directory of the project a page announced, or undefined when this machine does not know it.
|
|
65
|
+
*
|
|
66
|
+
* A daemon is shared: started from `$HOME`, a monorepo root, or another project's MCP client, its
|
|
67
|
+
* cwd is often not the app that sent the HELLO. The version-skew remedy read `package.json` from
|
|
68
|
+
* cwd anyway, so it fell back to the generic sensor package and told a Next.js project to install
|
|
69
|
+
* `@reticlehq/browser` (#1135). This is the same lookup the artifact root uses, keyed by the page's
|
|
70
|
+
* own project id.
|
|
71
|
+
*/
|
|
72
|
+
export function projectDirectoryFor(projectId) {
|
|
73
|
+
if (projectId === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
const directories = new Set(knownProjectCandidates()
|
|
76
|
+
.filter((candidate) => candidate.projectId === projectId)
|
|
77
|
+
.map((candidate) => candidate.directory));
|
|
78
|
+
// Two checkouts declaring one id are refused, as the artifact root refuses them: naming one of
|
|
79
|
+
// them would read the wrong app's package.json and prescribe its packages to the other.
|
|
80
|
+
return 1 === directories.size ? [...directories][0] : undefined;
|
|
81
|
+
}
|
|
37
82
|
export function artifactRootResolver(daemonRoot) {
|
|
38
83
|
const daemonIsProject = daemonSitsInAProject(daemonRoot);
|
|
39
84
|
return (projectId, origin) => {
|
|
40
|
-
let registry = emptyProjectRegistry();
|
|
41
|
-
try {
|
|
42
|
-
const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
|
|
43
|
-
registry = existsSync(path)
|
|
44
|
-
? parseProjectRegistry(JSON.parse(readFileSync(path, 'utf8')))
|
|
45
|
-
: registry;
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// A cache that cannot be read is an empty cache, never an error: the daemon still resolves
|
|
49
|
-
// through discovery, and falls back to its own root exactly as it did before this existed.
|
|
50
|
-
}
|
|
51
|
-
let discovery = { found: [], searched: [] };
|
|
52
|
-
try {
|
|
53
|
-
discovery = discoverProjectConfigs(process.cwd());
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
// Same reasoning: a diagnostic search that throws must not take a tool call with it.
|
|
57
|
-
}
|
|
58
85
|
const resolved = resolveArtifactRoot({
|
|
59
86
|
projectId,
|
|
60
|
-
candidates:
|
|
87
|
+
candidates: knownProjectCandidates(),
|
|
61
88
|
daemonRoot,
|
|
62
89
|
});
|
|
63
90
|
if (resolved.reason === ArtifactRootReason.MATCHED_PROJECT)
|
|
@@ -33,6 +33,8 @@ export interface ReticleDirPaths {
|
|
|
33
33
|
capsules: string;
|
|
34
34
|
/**.../.reticle/flake.json (per-flow flake ledger) */
|
|
35
35
|
flake: string;
|
|
36
|
+
/**.../.reticle/coverage.json (app-wide coverage ledger, all six levels) */
|
|
37
|
+
coverage: string;
|
|
36
38
|
/**.../.reticle/assertion-tiers.json (last-passing assertion tiers; anti-reward-hacking baseline) */
|
|
37
39
|
tiers: string;
|
|
38
40
|
}
|
|
@@ -18,6 +18,7 @@ export function reticleDirPaths(root) {
|
|
|
18
18
|
ambient: join(root, ReticleDir.AMBIENT_FILE),
|
|
19
19
|
capsules: join(root, ReticleDir.CAPSULES_SUBDIR),
|
|
20
20
|
flake: join(root, ReticleDir.FLAKE_FILE),
|
|
21
|
+
coverage: join(root, ReticleDir.COVERAGE_FILE),
|
|
21
22
|
tiers: join(root, ReticleDir.TIERS_FILE),
|
|
22
23
|
};
|
|
23
24
|
}
|
|
@@ -38,7 +38,9 @@ function numericDelta(before, after) {
|
|
|
38
38
|
* "what's broken vs before" from the cloud. Absent creds / unreachable → undefined (agent stays local).
|
|
39
39
|
*/
|
|
40
40
|
async function cloudRegression(deps, sessionId) {
|
|
41
|
-
|
|
41
|
+
// The same resolver every caller uses (stored key for a linked repo, else the exported one);
|
|
42
|
+
// the environment alone only for an embedder that wires no linked-credential port.
|
|
43
|
+
const config = await (deps.linkedCloud?.() ?? Promise.resolve(resolveCloudConfig(process.env)));
|
|
42
44
|
if (null === config)
|
|
43
45
|
return undefined;
|
|
44
46
|
let projectId;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* A daemon left running by one project answers the next project's app and rejects it on token, and
|
|
5
5
|
* the SDK printed `bridge refused the connection: authentication failed`. That sends someone to
|
|
6
6
|
* check a token which is perfectly correct — it is simply a DIFFERENT project's token. The two cases
|
|
7
|
-
* need opposite fixes: "your token is wrong" versus "that daemon is not yours,
|
|
7
|
+
* need opposite fixes: "your token is wrong" versus "that daemon is not yours, use your own port".
|
|
8
8
|
*
|
|
9
9
|
* The discriminator is EVIDENCE, not derivation. The daemon and the SDK derive project ids by
|
|
10
10
|
* different schemes (the SDK from its Vite root, the daemon from its own cwd), so comparing those
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* A daemon left running by one project answers the next project's app and rejects it on token, and
|
|
5
5
|
* the SDK printed `bridge refused the connection: authentication failed`. That sends someone to
|
|
6
6
|
* check a token which is perfectly correct — it is simply a DIFFERENT project's token. The two cases
|
|
7
|
-
* need opposite fixes: "your token is wrong" versus "that daemon is not yours,
|
|
7
|
+
* need opposite fixes: "your token is wrong" versus "that daemon is not yours, use your own port".
|
|
8
8
|
*
|
|
9
9
|
* The discriminator is EVIDENCE, not derivation. The daemon and the SDK derive project ids by
|
|
10
10
|
* different schemes (the SDK from its Vite root, the daemon from its own cwd), so comparing those
|
|
@@ -62,7 +62,9 @@ export function authFailureReason(servedProjects, helloProject, helloToken) {
|
|
|
62
62
|
if (helloProject !== undefined &&
|
|
63
63
|
1 === servedProjects.size &&
|
|
64
64
|
!servedProjects.has(helloProject)) {
|
|
65
|
-
|
|
65
|
+
// Not "stop it": that daemon is serving somebody else's app. This project needs its own port,
|
|
66
|
+
// which the CLI and the build plugins both read from `.reticle.json`.
|
|
67
|
+
const reason = `${DIFFERENT_PROJECT_PREFIX} — set a free "port" in .reticle.json, then restart the dev server`;
|
|
66
68
|
return Buffer.byteLength(reason, 'utf8') <= MAX_REASON_BYTES ? reason : PLAIN;
|
|
67
69
|
}
|
|
68
70
|
if (helloToken === undefined || 0 === helloToken.length)
|
|
@@ -38,6 +38,14 @@ export declare const WS_CLOSE_REASON: {
|
|
|
38
38
|
* refusal on this path: nothing about the app is wrong and retrying is the correct response.
|
|
39
39
|
*/
|
|
40
40
|
readonly HANDSHAKE_POOL_FULL: string;
|
|
41
|
+
/**
|
|
42
|
+
* A page's HELLO failed the wire schema for a reason other than its protocol number — the SDK and
|
|
43
|
+
* this daemon are different releases whose contracts no longer agree. The socket is still closed
|
|
44
|
+
* with the short `invalid message` the SDK already prints; this is the sentence the no-session
|
|
45
|
+
* diagnosis records, because "never dialled the bridge" was what `init` said about a page this
|
|
46
|
+
* daemon had just turned away. Same `cause — fix` shape as the protocol-skew reasons.
|
|
47
|
+
*/
|
|
48
|
+
readonly INVALID_HELLO: string;
|
|
41
49
|
};
|
|
42
50
|
interface BridgeOptions {
|
|
43
51
|
port: number;
|
|
@@ -58,10 +66,11 @@ interface BridgeOptions {
|
|
|
58
66
|
*/
|
|
59
67
|
server?: http.Server;
|
|
60
68
|
/**
|
|
61
|
-
* The SDK-upgrade sentence to attach when a HELLO is skewed
|
|
62
|
-
*
|
|
69
|
+
* The SDK-upgrade sentence to attach when a HELLO is skewed, for the project that HELLO named.
|
|
70
|
+
* Injected so the daemon can name that project's packages and package manager; tests that omit
|
|
71
|
+
* it get the no-project fallback.
|
|
63
72
|
*/
|
|
64
|
-
sdkFix?: () => string;
|
|
73
|
+
sdkFix?: (projectId?: string) => string;
|
|
65
74
|
}
|
|
66
75
|
/**
|
|
67
76
|
* The browser-facing half of the relay: a localhost WebSocket server. Each connection
|
|
@@ -69,6 +69,16 @@ export const WS_CLOSE_REASON = {
|
|
|
69
69
|
'this dial never got to identify itself. Nothing is wrong with the app; the pool drains on its ' +
|
|
70
70
|
'own and reloading the page reconnects. If it persists, something else on this machine is ' +
|
|
71
71
|
'opening sockets to this port.',
|
|
72
|
+
/**
|
|
73
|
+
* A page's HELLO failed the wire schema for a reason other than its protocol number — the SDK and
|
|
74
|
+
* this daemon are different releases whose contracts no longer agree. The socket is still closed
|
|
75
|
+
* with the short `invalid message` the SDK already prints; this is the sentence the no-session
|
|
76
|
+
* diagnosis records, because "never dialled the bridge" was what `init` said about a page this
|
|
77
|
+
* daemon had just turned away. Same `cause — fix` shape as the protocol-skew reasons.
|
|
78
|
+
*/
|
|
79
|
+
INVALID_HELLO: "invalid message: the page's hello does not match this daemon's wire contract — put " +
|
|
80
|
+
'@reticlehq/browser, the build plugin and @reticlehq/server on one release, run `reticle stop`, ' +
|
|
81
|
+
'then restart the dev server',
|
|
72
82
|
};
|
|
73
83
|
/**
|
|
74
84
|
* A dial turned away at the ORIGIN gate, in words the reader can act on.
|
|
@@ -179,6 +189,25 @@ function helloProtocolMismatch(text) {
|
|
|
179
189
|
}
|
|
180
190
|
return null;
|
|
181
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* What a HELLO says about the page it came from, read loosely enough to survive the schema failing.
|
|
194
|
+
* Null when the message is not a hello at all — only an SDK sends one, so only then is a refusal
|
|
195
|
+
* evidence about an app.
|
|
196
|
+
*/
|
|
197
|
+
function peekHelloPage(text) {
|
|
198
|
+
try {
|
|
199
|
+
const json = JSON.parse(text);
|
|
200
|
+
if (json.kind !== MessageKind.HELLO)
|
|
201
|
+
return null;
|
|
202
|
+
return {
|
|
203
|
+
...('string' === typeof json.url ? { url: json.url } : {}),
|
|
204
|
+
...('string' === typeof json.projectId ? { projectId: json.projectId } : {}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
182
211
|
/** Normalize ws RawData (string | Buffer | Buffer[] | ArrayBuffer) into a UTF-8 string. */
|
|
183
212
|
function rawToString(raw) {
|
|
184
213
|
if ('string' === typeof raw)
|
|
@@ -436,10 +465,19 @@ export class Bridge {
|
|
|
436
465
|
// Told to the AGENT too, not only to the SDK that is about to stop retrying. Without this
|
|
437
466
|
// the next tool call answers "no browser session connected", which reads as "no app is
|
|
438
467
|
// running" — so an SDK too old to connect is invisible.
|
|
439
|
-
this.sessions.noteClosure(reason, this.#clock());
|
|
468
|
+
this.sessions.noteClosure(reason, this.#clock(), peekHelloPage(text) ?? {});
|
|
440
469
|
socket.close(WS_CLOSE.PROTOCOL_MISMATCH[0], reason);
|
|
441
470
|
return;
|
|
442
471
|
}
|
|
472
|
+
// Recorded only for a HELLO on a socket that has none yet: that is a page being turned
|
|
473
|
+
// away, and it used to leave no trace, so `init` reported "never dialled the bridge" about
|
|
474
|
+
// a page this daemon had just refused. A malformed frame on a live session is a different
|
|
475
|
+
// failure and says nothing about whether the app can connect.
|
|
476
|
+
const refused = undefined === session ? peekHelloPage(text) : null;
|
|
477
|
+
if (null !== refused) {
|
|
478
|
+
log('hello_invalid', { ...refused });
|
|
479
|
+
this.sessions.noteClosure(WS_CLOSE_REASON.INVALID_HELLO, this.#clock(), refused);
|
|
480
|
+
}
|
|
443
481
|
socket.close(...WS_CLOSE.INVALID_MESSAGE);
|
|
444
482
|
return;
|
|
445
483
|
}
|
|
@@ -498,7 +536,10 @@ export class Bridge {
|
|
|
498
536
|
presented: parsed.token !== undefined && 0 < parsed.token.length,
|
|
499
537
|
tokenSource: this.#tokenSource,
|
|
500
538
|
});
|
|
501
|
-
this.sessions.noteClosure(reason, this.#clock()
|
|
539
|
+
this.sessions.noteClosure(reason, this.#clock(), {
|
|
540
|
+
url: parsed.url,
|
|
541
|
+
...(parsed.projectId === undefined ? {} : { projectId: parsed.projectId }),
|
|
542
|
+
});
|
|
502
543
|
socket.close(WS_CLOSE.AUTH_FAILED[0], reason);
|
|
503
544
|
return;
|
|
504
545
|
}
|
|
@@ -567,6 +608,9 @@ export class Bridge {
|
|
|
567
608
|
// The daemon is the single judge of skew, and HELLO is where the page announces itself.
|
|
568
609
|
// Reported on the session (reticle_sessions) AND queued for the next tool result, because an
|
|
569
610
|
// agent driving a flow never calls reticle_sessions and would never learn.
|
|
611
|
+
// Read only if the page IS skewed: the fix reads the project's package.json and lockfile,
|
|
612
|
+
// and a compatible page (nearly every HELLO) should not pay for that on connect.
|
|
613
|
+
const fixForThisProject = () => this.#sdkFix(parsed.projectId);
|
|
570
614
|
const skew = describeSkew({
|
|
571
615
|
what: 'the page',
|
|
572
616
|
version: parsed.sdkVersion,
|
|
@@ -577,7 +621,9 @@ export class Bridge {
|
|
|
577
621
|
...(parsed.contractParts === undefined
|
|
578
622
|
? {}
|
|
579
623
|
: { contractParts: parsed.contractParts }),
|
|
580
|
-
fix
|
|
624
|
+
get fix() {
|
|
625
|
+
return fixForThisProject();
|
|
626
|
+
},
|
|
581
627
|
}, {
|
|
582
628
|
version: SERVER_VERSION,
|
|
583
629
|
contract: CONTRACT_FINGERPRINT,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
/**
|
|
3
|
+
* V8 JS coverage on a page Reticle drives — the raw half of "what of the app ever ran".
|
|
4
|
+
*
|
|
5
|
+
* Started once per page with `resetOnNavigation: false`, so a reload keeps counting instead of
|
|
6
|
+
* starting over. A take stops and immediately restarts: the counts it returns are since the last
|
|
7
|
+
* take, and the reader folds them as a union (see features/exhaust/code-coverage.ts).
|
|
8
|
+
*
|
|
9
|
+
* Chromium only. Anything else — or a page that closed — answers false/undefined rather than
|
|
10
|
+
* throwing: coverage is an addition to a drive, never a reason for one to fail.
|
|
11
|
+
*/
|
|
12
|
+
/** One script's coverage — the shape `stopJSCoverage` returns, narrowed to what is read. */
|
|
13
|
+
export interface ScriptCoverage {
|
|
14
|
+
url: string;
|
|
15
|
+
functions: {
|
|
16
|
+
functionName: string;
|
|
17
|
+
ranges: {
|
|
18
|
+
startOffset: number;
|
|
19
|
+
endOffset: number;
|
|
20
|
+
count: number;
|
|
21
|
+
}[];
|
|
22
|
+
}[];
|
|
23
|
+
}
|
|
24
|
+
export declare function startJsCoverage(page: Page, covering: WeakSet<Page>): Promise<boolean>;
|
|
25
|
+
export declare function takeJsCoverage(page: Page, covering: WeakSet<Page>): Promise<ScriptCoverage[] | undefined>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export async function startJsCoverage(page, covering) {
|
|
2
|
+
if (covering.has(page))
|
|
3
|
+
return true;
|
|
4
|
+
try {
|
|
5
|
+
await page.coverage.startJSCoverage({ resetOnNavigation: false });
|
|
6
|
+
covering.add(page);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function takeJsCoverage(page, covering) {
|
|
14
|
+
// Nothing collects until something asks: collection slows every script on the page. The first
|
|
15
|
+
// take starts it and has nothing to report yet.
|
|
16
|
+
if (!covering.has(page)) {
|
|
17
|
+
await startJsCoverage(page, covering);
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const entries = await page.coverage.stopJSCoverage();
|
|
22
|
+
covering.delete(page);
|
|
23
|
+
await startJsCoverage(page, covering);
|
|
24
|
+
return entries;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
covering.delete(page);
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Node-only. Playwright is loaded via DYNAMIC `import('playwright')` so non-CDP users never
|
|
10
10
|
* pay for it; the type-only import is elided by `tsc`, so the build stays green without it.
|
|
11
11
|
*/
|
|
12
|
+
import { type ScriptCoverage } from './js-coverage.js';
|
|
12
13
|
import type { Browser, Page } from 'playwright';
|
|
13
14
|
import { ActionType, DriveErrorCode } from '@reticlehq/core';
|
|
14
15
|
import { type MockRule } from './network-mock.js';
|
|
@@ -77,6 +78,14 @@ export interface RealInputProvider {
|
|
|
77
78
|
* Optional: a provider with no owned browser simply omits it.
|
|
78
79
|
*/
|
|
79
80
|
setMocks?(sessionUrl: string, rules: MockRule[]): Promise<boolean>;
|
|
81
|
+
/**
|
|
82
|
+
* What of the app's own code ran since the last take (V8 coverage, Chromium only), or undefined
|
|
83
|
+
* when no driven page matches or collection had not started. The first call starts collecting,
|
|
84
|
+
* because collection slows every script on the page and a slower page can let a console error land
|
|
85
|
+
* after a clean-console check passed. Optional: a provider with no owned browser cannot see the
|
|
86
|
+
* engine's counters.
|
|
87
|
+
*/
|
|
88
|
+
takeCodeCoverage?(sessionUrl: string): Promise<ScriptCoverage[] | undefined>;
|
|
80
89
|
/**
|
|
81
90
|
* Pin the correlated page's viewport to fixed pixel dimensions so a screenshot baseline is
|
|
82
91
|
* reproducible across machines (the missing piece of CI-stable visual regression, alongside masks
|
|
@@ -144,6 +153,7 @@ interface CdpProviderOptions {
|
|
|
144
153
|
export declare class CdpRealInputProvider implements RealInputProvider {
|
|
145
154
|
#private;
|
|
146
155
|
constructor(options: CdpProviderOptions);
|
|
156
|
+
takeCodeCoverage(sessionUrl: string): Promise<ScriptCoverage[] | undefined>;
|
|
147
157
|
isAvailableFor(sessionUrl: string): Promise<boolean>;
|
|
148
158
|
perform(sessionUrl: string, action: ActionType, box: ElementBox, args: RealInputArgs): Promise<RealInputResult>;
|
|
149
159
|
/** PNG of the correlated page, or undefined if none matches. */
|
|
@@ -236,6 +246,7 @@ export declare class LaunchedRealInputProvider implements OwnedRealInputProvider
|
|
|
236
246
|
width: number;
|
|
237
247
|
height: number;
|
|
238
248
|
}): Promise<boolean>;
|
|
249
|
+
takeCodeCoverage(_sessionUrl: string): Promise<ScriptCoverage[] | undefined>;
|
|
239
250
|
/** Apply network-mock rules to the owned page; false before navigate / after dispose. */
|
|
240
251
|
setMocks(_sessionUrl: string, rules: MockRule[]): Promise<boolean>;
|
|
241
252
|
/** Close the owned browser once. Idempotent and safe before navigate. */
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional CDP/Playwright real-input mode.
|
|
3
|
+
*
|
|
4
|
+
* Synthetic `dispatchEvent` cannot drive native hover/pointer state (an onMouseEnter never
|
|
5
|
+
* fires, hit-testing never runs). When a CDP endpoint is configured, this module connects a
|
|
6
|
+
* Playwright `Browser` over CDP and drives REAL pointer/keyboard input against the element box
|
|
7
|
+
* the SDK resolves (viewport CSS px from getBoundingClientRect).
|
|
8
|
+
*
|
|
9
|
+
* Node-only. Playwright is loaded via DYNAMIC `import('playwright')` so non-CDP users never
|
|
10
|
+
* pay for it; the type-only import is elided by `tsc`, so the build stays green without it.
|
|
11
|
+
*/
|
|
12
|
+
import { takeJsCoverage } from './js-coverage.js';
|
|
1
13
|
import { stampedDriveUrl } from './drive-url-stamp.js';
|
|
2
|
-
import {
|
|
3
|
-
import { gotoOptions } from '../pool/playwright-launcher.js';
|
|
14
|
+
import { launchChromium } from '../../launch-chromium.js';
|
|
15
|
+
import { chromiumLaunchHint, gotoOptions } from '../pool/playwright-launcher.js';
|
|
4
16
|
import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
|
|
5
17
|
import { getSessionMetrics } from '../../telemetry/session-metrics.js';
|
|
6
18
|
import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
|
|
@@ -131,6 +143,8 @@ export class CdpRealInputProvider {
|
|
|
131
143
|
/** Pages already listening. #pageFor resolves on EVERY call, so without this each action would add
|
|
132
144
|
* another listener and every response would be emitted once per action taken so far. */
|
|
133
145
|
#listening = new WeakSet();
|
|
146
|
+
/** Pages collecting V8 coverage — see js-coverage.ts. */
|
|
147
|
+
#covering = new WeakSet();
|
|
134
148
|
#browser;
|
|
135
149
|
constructor(options) {
|
|
136
150
|
this.#cdpUrl = options.cdpUrl;
|
|
@@ -176,6 +190,10 @@ export class CdpRealInputProvider {
|
|
|
176
190
|
this.#listen(page);
|
|
177
191
|
return page;
|
|
178
192
|
}
|
|
193
|
+
async takeCodeCoverage(sessionUrl) {
|
|
194
|
+
const page = await this.#pageFor(sessionUrl);
|
|
195
|
+
return page === undefined ? undefined : takeJsCoverage(page, this.#covering);
|
|
196
|
+
}
|
|
179
197
|
async isAvailableFor(sessionUrl) {
|
|
180
198
|
try {
|
|
181
199
|
return (await this.#pageFor(sessionUrl)) !== undefined;
|
|
@@ -273,13 +291,14 @@ export const launchedChromium = async (headless) => {
|
|
|
273
291
|
}
|
|
274
292
|
const settle = getSessionMetrics().recordConnectAttempt(BrowserLaunchKind.LAUNCHED);
|
|
275
293
|
try {
|
|
276
|
-
const browser = await mod.chromium
|
|
294
|
+
const browser = await launchChromium(mod.chromium, headless);
|
|
277
295
|
settle();
|
|
278
296
|
return browser;
|
|
279
297
|
}
|
|
280
298
|
catch (e) {
|
|
281
299
|
settle(classifyConnectFailure(e));
|
|
282
|
-
|
|
300
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
301
|
+
throw new DriveError(DriveErrorCode.LAUNCH_FAILED, chromiumLaunchHint(msg) ?? msg);
|
|
283
302
|
}
|
|
284
303
|
};
|
|
285
304
|
/**
|
|
@@ -296,6 +315,8 @@ export class LaunchedRealInputProvider {
|
|
|
296
315
|
#onNetworkDetail;
|
|
297
316
|
#browser;
|
|
298
317
|
#page;
|
|
318
|
+
/** Pages collecting V8 coverage — see js-coverage.ts. */
|
|
319
|
+
#covering = new WeakSet();
|
|
299
320
|
constructor(options) {
|
|
300
321
|
this.#driveUrl = options.driveUrl;
|
|
301
322
|
this.#headless = options.headless;
|
|
@@ -442,6 +463,10 @@ export class LaunchedRealInputProvider {
|
|
|
442
463
|
await page.setViewportSize({ width: size.width, height: size.height });
|
|
443
464
|
return true;
|
|
444
465
|
}
|
|
466
|
+
takeCodeCoverage(_sessionUrl) {
|
|
467
|
+
const page = this.#livePage();
|
|
468
|
+
return page === undefined ? Promise.resolve(undefined) : takeJsCoverage(page, this.#covering);
|
|
469
|
+
}
|
|
445
470
|
/** Apply network-mock rules to the owned page; false before navigate / after dispose. */
|
|
446
471
|
async setMocks(_sessionUrl, rules) {
|
|
447
472
|
const page = this.#livePage();
|
|
@@ -34,8 +34,8 @@ export const VIEWPORT_TOOLS = [
|
|
|
34
34
|
recommendation: z.string().optional(),
|
|
35
35
|
},
|
|
36
36
|
handler: async (deps, args) => {
|
|
37
|
-
const width = clampDim(args['width']);
|
|
38
|
-
const height = clampDim(args['height']);
|
|
37
|
+
const width = clampDim('width', args['width']);
|
|
38
|
+
const height = clampDim('height', args['height']);
|
|
39
39
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
40
40
|
const provider = viewportProvider(deps);
|
|
41
41
|
if (provider !== undefined) {
|
|
@@ -65,8 +65,18 @@ export const VIEWPORT_TOOLS = [
|
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
67
|
];
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Clamp a requested dimension into [MIN_DIM, MAX_DIM]; a missing value falls back to MIN_DIM.
|
|
70
|
+
*
|
|
71
|
+
* A value that is PRESENT but not a finite number is refused, not read as missing: a client that
|
|
72
|
+
* sent `"1440"` meant 1440, and answering with a 64px page it never asked for made every later
|
|
73
|
+
* snapshot, click and screenshot on that page read as broken (#1118).
|
|
74
|
+
*/
|
|
75
|
+
function clampDim(key, value) {
|
|
76
|
+
if (value === undefined)
|
|
77
|
+
return MIN_DIM;
|
|
78
|
+
if ('number' !== typeof value || !Number.isFinite(value)) {
|
|
79
|
+
throw new Error(`${ReticleTool.VIEWPORT}: \`${key}\` must be a number of CSS px, got ${JSON.stringify(value)} — NOT applied`);
|
|
80
|
+
}
|
|
81
|
+
return Math.max(MIN_DIM, Math.min(Math.round(value), MAX_DIM));
|
|
72
82
|
}
|
|
@@ -25,6 +25,12 @@ export declare function gotoOptions(timeoutMs: number | undefined): {
|
|
|
25
25
|
export declare function playwrightLauncher(opts?: {
|
|
26
26
|
headless?: boolean;
|
|
27
27
|
}): Launcher;
|
|
28
|
+
/**
|
|
29
|
+
* Turn Playwright's raw launch error into the one command that fixes it, or undefined when it is
|
|
30
|
+
* not a missing-browser failure. Shared by the pooled and the drive launch, so `reticle verify` on
|
|
31
|
+
* a fresh machine gets the same advice as a lease.
|
|
32
|
+
*/
|
|
33
|
+
export declare function chromiumLaunchHint(msg: string): string | undefined;
|
|
28
34
|
/**
|
|
29
35
|
* Resolve the pool's concurrency cap. An explicit RETICLE_MAX_CONTEXTS wins (clamped to
|
|
30
36
|
* [1, hard ceiling]); otherwise scale with the machine but never above the CPU-derived ceiling.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Headless by default — the pool exists for fault-tolerant headless multi-agent testing.
|
|
7
7
|
*/
|
|
8
8
|
import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
|
|
9
|
-
import {
|
|
9
|
+
import { launchChromium } from '../../launch-chromium.js';
|
|
10
10
|
import { getSessionMetrics } from '../../telemetry/session-metrics.js';
|
|
11
11
|
import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
|
|
12
12
|
import { chromiumInstallCommand, chromiumInstallDepsCommand, bundledPlaywrightVersion, } from '../../command/cli/doctor/browser/chromium-hint.js';
|
|
@@ -105,27 +105,34 @@ export function playwrightLauncher(opts = {}) {
|
|
|
105
105
|
// number that actually costs memory and the one that explains a slow machine.
|
|
106
106
|
const settle = getSessionMetrics().recordConnectAttempt(BrowserLaunchKind.POOLED);
|
|
107
107
|
try {
|
|
108
|
-
const browser = wrapBrowser(await chromium
|
|
108
|
+
const browser = wrapBrowser(await launchChromium(chromium, headless));
|
|
109
109
|
settle();
|
|
110
110
|
return browser;
|
|
111
111
|
}
|
|
112
112
|
catch (err) {
|
|
113
113
|
settle(classifyConnectFailure(err));
|
|
114
|
-
// Turn Playwright's raw "Executable doesn't exist" into the one command that fixes it.
|
|
115
114
|
const msg = err instanceof Error ? err.message : String(err);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// missing binary and send the user through a fix that changes nothing.
|
|
119
|
-
if (/host system is missing dependencies/i.test(msg)) {
|
|
120
|
-
throw new Error(CHROMIUM_MISSING_DEPS_HINT);
|
|
121
|
-
}
|
|
122
|
-
if (/executable doesn.?t exist|playwright install|browsertype\.launch/i.test(msg)) {
|
|
123
|
-
throw new Error(CHROMIUM_MISSING_HINT);
|
|
124
|
-
}
|
|
125
|
-
throw err;
|
|
115
|
+
const hint = chromiumLaunchHint(msg);
|
|
116
|
+
throw hint !== undefined ? new Error(hint) : err;
|
|
126
117
|
}
|
|
127
118
|
};
|
|
128
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Turn Playwright's raw launch error into the one command that fixes it, or undefined when it is
|
|
122
|
+
* not a missing-browser failure. Shared by the pooled and the drive launch, so `reticle verify` on
|
|
123
|
+
* a fresh machine gets the same advice as a lease.
|
|
124
|
+
*/
|
|
125
|
+
export function chromiumLaunchHint(msg) {
|
|
126
|
+
// Checked FIRST: Playwright's host-requirement validation error also contains
|
|
127
|
+
// "browserType.launch", so the generic check below would otherwise misdiagnose it as a
|
|
128
|
+
// missing binary and send the user through a fix that changes nothing.
|
|
129
|
+
if (/host system is missing dependencies/i.test(msg))
|
|
130
|
+
return CHROMIUM_MISSING_DEPS_HINT;
|
|
131
|
+
if (/executable doesn.?t exist|playwright install|browsertype\.launch/i.test(msg)) {
|
|
132
|
+
return CHROMIUM_MISSING_HINT;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
129
136
|
const MAX_CONTEXTS_CEILING = 8;
|
|
130
137
|
const MAX_CONTEXTS_FLOOR = 1;
|
|
131
138
|
/**
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
export interface DevCommand {
|
|
13
13
|
/** The literal shell command, e.g. `pnpm run dev`. */
|
|
14
14
|
command: string;
|
|
15
|
-
/** The package.json script it runs. */
|
|
16
|
-
script
|
|
15
|
+
/** The package.json script it runs. Absent when the command runs a CLI directly (`npx tauri dev`). */
|
|
16
|
+
script?: string;
|
|
17
17
|
/** The port the script PINS, when it pins one. Absent means the tool picks — never a guess. */
|
|
18
18
|
port?: number;
|
|
19
19
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { readFileSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
+
import { desktopLaunch } from '@reticlehq/init';
|
|
13
14
|
/**
|
|
14
15
|
* Script names checked, in order.
|
|
15
16
|
*
|
|
@@ -47,23 +48,26 @@ function pinnedPort(script) {
|
|
|
47
48
|
const port = Number.parseInt(digits, 10);
|
|
48
49
|
return Number.isSafeInteger(port) ? port : undefined;
|
|
49
50
|
}
|
|
50
|
-
/** The
|
|
51
|
-
function
|
|
51
|
+
/** The parsed package.json, or undefined for anything unreadable/unparseable. */
|
|
52
|
+
function readManifest(raw) {
|
|
52
53
|
if (raw === undefined)
|
|
53
|
-
return
|
|
54
|
+
return undefined;
|
|
54
55
|
try {
|
|
55
|
-
|
|
56
|
-
if (null === parsed || 'object' !== typeof parsed)
|
|
57
|
-
return {};
|
|
58
|
-
const scripts = parsed.scripts;
|
|
59
|
-
if (null === scripts || 'object' !== typeof scripts)
|
|
60
|
-
return {};
|
|
61
|
-
return scripts;
|
|
56
|
+
return JSON.parse(raw);
|
|
62
57
|
}
|
|
63
58
|
catch {
|
|
64
|
-
return
|
|
59
|
+
return undefined;
|
|
65
60
|
}
|
|
66
61
|
}
|
|
62
|
+
/** The scripts block of a parsed package.json, or an empty one. */
|
|
63
|
+
function scriptsOf(manifest) {
|
|
64
|
+
if (null === manifest || 'object' !== typeof manifest)
|
|
65
|
+
return {};
|
|
66
|
+
const scripts = manifest.scripts;
|
|
67
|
+
if (null === scripts || 'object' !== typeof scripts)
|
|
68
|
+
return {};
|
|
69
|
+
return scripts;
|
|
70
|
+
}
|
|
67
71
|
/**
|
|
68
72
|
* The dev command for the project in `directory`, or undefined when there is not one to be sure of.
|
|
69
73
|
*
|
|
@@ -71,9 +75,23 @@ function readScripts(raw) {
|
|
|
71
75
|
* default disk reader.
|
|
72
76
|
*/
|
|
73
77
|
export function detectDevCommand(directory, read = readTextFile) {
|
|
74
|
-
const
|
|
78
|
+
const manifest = readManifest(read(join(directory, PACKAGE_JSON)));
|
|
79
|
+
const scripts = scriptsOf(manifest);
|
|
75
80
|
const manager = LOCKFILES.find(({ file }) => read(join(directory, file)) !== undefined)?.manager ??
|
|
76
81
|
DEFAULT_PACKAGE_MANAGER;
|
|
82
|
+
// A desktop shell's `dev` is its renderer's dev server, which no window ever opens onto. init's
|
|
83
|
+
// rule decides which launcher it is, so the command the daemon hands an agent and the one init
|
|
84
|
+
// spawns are the same.
|
|
85
|
+
const desktop = desktopLaunch(manifest);
|
|
86
|
+
if (desktop !== undefined) {
|
|
87
|
+
if ('command' in desktop)
|
|
88
|
+
return { command: desktop.command };
|
|
89
|
+
const command = `${manager} run ${desktop.script}`;
|
|
90
|
+
return {
|
|
91
|
+
command: 0 < desktop.args.length ? `${command} ${desktop.args}` : command,
|
|
92
|
+
script: desktop.script,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
77
95
|
for (const script of DEV_SCRIPTS) {
|
|
78
96
|
const body = scripts[script];
|
|
79
97
|
if ('string' !== typeof body || 0 === body.trim().length)
|