@reticlehq/server 2.12.0 → 2.13.1
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/SKILL.md +67 -109
- package/dist/bridge/auth-failure-reason.d.ts +6 -2
- package/dist/bridge/auth-failure-reason.js +15 -8
- package/dist/bridge/bridge.d.ts +18 -1
- package/dist/bridge/bridge.js +247 -164
- package/dist/bridge/pairing-token.d.ts +5 -0
- package/dist/bridge/pairing-token.js +28 -0
- package/dist/bridge/protocol-skew.d.ts +37 -0
- package/dist/bridge/protocol-skew.js +45 -0
- package/dist/capsule/capsule-store.d.ts +6 -6
- package/dist/capsule/causal-summary.js +7 -2
- package/dist/cli/cli-doctor.js +31 -1
- package/dist/cli/cli-launch.d.ts +11 -1
- package/dist/cli/cli-launch.js +59 -10
- package/dist/cli/cli-parse.d.ts +15 -1
- package/dist/cli/cli-parse.js +212 -8
- package/dist/cli/cli-verify.d.ts +1 -0
- package/dist/cli/cli-verify.js +3 -2
- package/dist/cli/cloud-cli.js +428 -259
- package/dist/cli/cloud-keystore.d.ts +37 -0
- package/dist/cli/cloud-keystore.js +97 -0
- package/dist/cli/cloud-kit.d.ts +37 -0
- package/dist/cli/cloud-kit.js +121 -0
- package/dist/cli/cloud-login.d.ts +17 -0
- package/dist/cli/cloud-login.js +183 -0
- package/dist/cli/cloud-session.d.ts +48 -0
- package/dist/cli/cloud-session.js +53 -0
- package/dist/cli/config-discovery.js +1 -0
- package/dist/cli/daemon-start-options.d.ts +32 -0
- package/dist/cli/daemon-start-options.js +38 -0
- package/dist/cli/device-flow.d.ts +76 -0
- package/dist/cli/device-flow.js +56 -0
- package/dist/cli/doctor-rows.d.ts +9 -0
- package/dist/cli/doctor-rows.js +9 -0
- package/dist/cli/drive-attach.d.ts +13 -0
- package/dist/cli/drive-attach.js +18 -0
- package/dist/cli/drive-command.js +7 -3
- package/dist/cli/mcp-command.js +51 -20
- package/dist/cli/project-name.d.ts +30 -0
- package/dist/cli/project-name.js +44 -0
- package/dist/cli/status-next-action.d.ts +14 -0
- package/dist/cli/status-next-action.js +4 -4
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +134 -80
- package/dist/cloud/cloud-config.d.ts +22 -1
- package/dist/cloud/cloud-config.js +64 -5
- package/dist/cloud/cloud-sync.d.ts +5 -5
- package/dist/cloud/cloud-sync.js +33 -2
- package/dist/cloud/sync-cycle.d.ts +110 -0
- package/dist/cloud/sync-cycle.js +270 -0
- package/dist/cloud/sync-daemon.d.ts +69 -0
- package/dist/cloud/sync-daemon.js +260 -0
- package/dist/cloud/sync-disk.d.ts +9 -0
- package/dist/cloud/sync-disk.js +191 -0
- package/dist/cloud/sync-hash.d.ts +2 -0
- package/dist/cloud/sync-hash.js +35 -0
- package/dist/crawl/crawl-tools.js +5 -3
- package/dist/crawl/crawl.d.ts +3 -0
- package/dist/crawl/crawl.js +5 -0
- package/dist/daemon/daemon-resolve.d.ts +43 -0
- package/dist/daemon/daemon-resolve.js +69 -0
- package/dist/daemon/dev-servers.d.ts +15 -0
- package/dist/daemon/dev-servers.js +68 -0
- package/dist/daemon/wake-decision.d.ts +32 -0
- package/dist/daemon/wake-decision.js +38 -0
- package/dist/domain/domain-tools.js +2 -2
- package/dist/events/contradictions.d.ts +18 -0
- package/dist/events/contradictions.js +134 -21
- package/dist/events/declared.d.ts +7 -4
- package/dist/events/declared.js +15 -6
- package/dist/events/echo-mismatch.js +64 -14
- package/dist/events/event-filters.d.ts +8 -0
- package/dist/events/event-filters.js +19 -2
- package/dist/events/predicate-eval.js +53 -5
- package/dist/events/predicate-precheck.d.ts +8 -0
- package/dist/events/predicate-precheck.js +41 -0
- package/dist/events/predicate-route.d.ts +14 -0
- package/dist/events/predicate-route.js +36 -8
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +37 -5
- package/dist/events/split-text-miss.d.ts +30 -0
- package/dist/events/split-text-miss.js +44 -0
- package/dist/flows/flake.d.ts +6 -6
- package/dist/flows/flow-memory-consult.d.ts +55 -0
- package/dist/flows/flow-memory-consult.js +38 -0
- package/dist/flows/flow-replay-run.d.ts +18 -7
- package/dist/flows/flow-replay-run.js +239 -22
- package/dist/flows/flow-replay.js +11 -2
- package/dist/flows/flow-step-runners.d.ts +8 -0
- package/dist/flows/flow-step-runners.js +8 -2
- package/dist/flows/flow-store-for-session.d.ts +31 -0
- package/dist/flows/flow-store-for-session.js +33 -0
- package/dist/flows/flow-tools.js +58 -32
- package/dist/flows/flows.d.ts +8 -0
- package/dist/flows/flows.js +468 -2
- package/dist/flows/heal-run.js +5 -2
- package/dist/flows/predicate-to-expect.d.ts +17 -12
- package/dist/flows/predicate-to-expect.js +30 -12
- package/dist/flows/recordings.d.ts +13 -1
- package/dist/flows/recordings.js +17 -2
- package/dist/flows/replay.d.ts +22 -2
- package/dist/flows/replay.js +28 -2
- package/dist/flows/verify-change-tools.js +5 -2
- package/dist/honesty/blind-spots.d.ts +55 -2
- package/dist/honesty/blind-spots.js +63 -3
- package/dist/honesty/honesty.d.ts +24 -0
- package/dist/honesty/honesty.js +25 -1
- package/dist/honesty/instrumentation-gaps.d.ts +9 -0
- package/dist/honesty/instrumentation-gaps.js +29 -2
- package/dist/honesty/verified.d.ts +2 -1
- package/dist/honesty/verified.js +16 -1
- package/dist/http-server.js +12 -0
- package/dist/impact/impact-recorder.d.ts +31 -8
- package/dist/impact/impact-recorder.js +104 -21
- package/dist/impact/impact-store.d.ts +15 -1
- package/dist/impact/impact-store.js +43 -2
- package/dist/index.js +113 -7
- package/dist/init/agent-rules.d.ts +13 -6
- package/dist/init/agent-rules.js +23 -12
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +28 -0
- package/dist/init/capabilities.d.ts +4 -1
- package/dist/init/capabilities.js +22 -2
- package/dist/init/confirm.d.ts +31 -7
- package/dist/init/confirm.js +48 -8
- package/dist/init/cra.d.ts +20 -4
- package/dist/init/cra.js +57 -17
- package/dist/init/csp-check.js +17 -2
- package/dist/init/csp-doctor.d.ts +8 -1
- package/dist/init/csp-doctor.js +8 -3
- package/dist/init/dev-script.d.ts +3 -1
- package/dist/init/dev-script.js +8 -5
- package/dist/init/format-generated.d.ts +18 -0
- package/dist/init/format-generated.js +39 -0
- package/dist/init/install-hint.d.ts +9 -0
- package/dist/init/install-hint.js +29 -0
- package/dist/init/mcp-clients.d.ts +7 -0
- package/dist/init/mcp-clients.js +7 -0
- package/dist/init/mcp.d.ts +1 -1
- package/dist/init/mcp.js +1 -1
- package/dist/init/node-io.js +11 -1
- package/dist/init/plan-framework.js +81 -45
- package/dist/init/plan.d.ts +20 -7
- package/dist/init/plan.js +20 -45
- package/dist/init/preflight.d.ts +28 -0
- package/dist/init/preflight.js +38 -0
- package/dist/init/run.d.ts +32 -9
- package/dist/init/run.js +141 -119
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/snippets.js +41 -25
- package/dist/init/vite-config.d.ts +1 -1
- package/dist/init/vite-config.js +39 -25
- package/dist/init/workspace-apps.d.ts +17 -10
- package/dist/init/workspace-apps.js +110 -0
- package/dist/input/network-mock-tools.js +3 -6
- package/dist/input/real-input.js +25 -5
- package/dist/input/scroll-tools.js +4 -6
- package/dist/input/viewport-tools.js +5 -4
- package/dist/intent/inline-intent.d.ts +10 -19
- package/dist/intent/inline-intent.js +91 -1
- package/dist/intent/intent-shard-store.d.ts +72 -0
- package/dist/intent/intent-shard-store.js +203 -0
- package/dist/intent/intent-shard.d.ts +411 -0
- package/dist/intent/intent-shard.js +176 -0
- package/dist/intent/intent-store.d.ts +12 -0
- package/dist/intent/intent-store.js +21 -0
- package/dist/intent/intent-subject.d.ts +45 -0
- package/dist/intent/intent-subject.js +177 -0
- package/dist/intent/intent-tools.js +65 -2
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +15 -0
- package/dist/license/license-env.d.ts +29 -0
- package/dist/license/license-env.js +148 -0
- package/dist/loopback-agent.d.ts +31 -0
- package/dist/loopback-agent.js +57 -0
- package/dist/mcp/attach-memory.d.ts +18 -2
- package/dist/mcp/attach-memory.js +50 -7
- package/dist/mcp/client-identity.d.ts +31 -0
- package/dist/mcp/client-identity.js +51 -0
- package/dist/mcp/mcp-post-transport.d.ts +26 -0
- package/dist/mcp/mcp-post-transport.js +109 -0
- package/dist/mcp/mcp-proxy.d.ts +2 -1
- package/dist/mcp/mcp-proxy.js +52 -62
- package/dist/mcp/mcp.js +7 -1
- package/dist/mcp/proxy-backoff.d.ts +11 -0
- package/dist/mcp/proxy-backoff.js +13 -0
- package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
- package/dist/mcp/proxy-daemon-probe.js +7 -1
- package/dist/mcp/proxy-handshake.d.ts +23 -0
- package/dist/mcp/proxy-handshake.js +31 -0
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +12 -0
- package/dist/memory/memory-tools.d.ts +2 -0
- package/dist/memory/memory-tools.js +82 -0
- package/dist/memory/project-memory.d.ts +34 -0
- package/dist/memory/project-memory.js +96 -0
- package/dist/oracles/propose-consequences.js +12 -3
- package/dist/pool/browser-pool.d.ts +63 -4
- package/dist/pool/browser-pool.js +113 -4
- package/dist/pool/playwright-launcher.js +9 -0
- package/dist/project/project-tools.js +3 -3
- package/dist/project/reticle-dir.d.ts +15 -2
- package/dist/project/reticle-dir.js +21 -4
- package/dist/runs/run-store.d.ts +10 -0
- package/dist/runs/run-store.js +10 -0
- package/dist/runs/verification-sync.d.ts +0 -11
- package/dist/runs/verification-sync.js +22 -1
- package/dist/runs/verify-server.d.ts +8 -1
- package/dist/runs/verify-server.js +14 -2
- package/dist/session/command-timeout.d.ts +5 -2
- package/dist/session/command-timeout.js +5 -4
- package/dist/session/lease-visibility.d.ts +67 -0
- package/dist/session/lease-visibility.js +76 -0
- package/dist/session/no-session-diagnosis.d.ts +23 -0
- package/dist/session/no-session-diagnosis.js +58 -17
- package/dist/session/no-session-next-action.d.ts +7 -0
- package/dist/session/no-session-next-action.js +42 -3
- package/dist/session/no-session-watch.d.ts +10 -6
- package/dist/session/no-session-watch.js +51 -19
- package/dist/session/pending-commands.d.ts +11 -1
- package/dist/session/pending-commands.js +12 -2
- package/dist/session/resolve-within.d.ts +34 -0
- package/dist/session/resolve-within.js +53 -0
- package/dist/session/session-info.d.ts +18 -0
- package/dist/session/session-info.js +11 -0
- package/dist/session/session-manager.d.ts +14 -0
- package/dist/session/session-manager.js +61 -3
- package/dist/session/session-mirror.d.ts +16 -0
- package/dist/session/session-mirror.js +22 -0
- package/dist/session/session-recommendation.d.ts +19 -5
- package/dist/session/session-recommendation.js +25 -9
- package/dist/session/session-successor.js +36 -1
- package/dist/session/session-tools.js +2 -2
- package/dist/session/session.d.ts +22 -2
- package/dist/session/session.js +87 -7
- package/dist/session/wall-clock.d.ts +12 -0
- package/dist/session/wall-clock.js +14 -0
- package/dist/setup/agent-configs.d.ts +96 -0
- package/dist/setup/agent-configs.js +333 -0
- package/dist/setup/agent-io.d.ts +9 -0
- package/dist/setup/agent-io.js +16 -0
- package/dist/setup/agent-writer.d.ts +51 -0
- package/dist/setup/agent-writer.js +115 -0
- package/dist/setup/approval-migration.d.ts +47 -0
- package/dist/setup/approval-migration.js +68 -0
- package/dist/setup/auto-approve.d.ts +85 -0
- package/dist/setup/auto-approve.js +150 -0
- package/dist/setup/bridge-port.d.ts +15 -0
- package/dist/setup/bridge-port.js +24 -0
- package/dist/setup/desktop-shape.d.ts +57 -0
- package/dist/setup/desktop-shape.js +63 -0
- package/dist/setup/dev-server-wait.d.ts +68 -0
- package/dist/setup/dev-server-wait.js +67 -0
- package/dist/setup/drive-agent.d.ts +71 -0
- package/dist/setup/drive-agent.js +171 -0
- package/dist/setup/drive-plan.d.ts +68 -0
- package/dist/setup/drive-plan.js +130 -0
- package/dist/setup/ensure-daemon.d.ts +29 -0
- package/dist/setup/ensure-daemon.js +58 -0
- package/dist/setup/init-runtime.d.ts +47 -0
- package/dist/setup/init-runtime.js +160 -0
- package/dist/setup/interrupt.d.ts +25 -0
- package/dist/setup/interrupt.js +40 -0
- package/dist/setup/license-key.d.ts +39 -0
- package/dist/setup/license-key.js +72 -0
- package/dist/setup/listeners.d.ts +37 -0
- package/dist/setup/listeners.js +85 -0
- package/dist/setup/node-effects.d.ts +60 -0
- package/dist/setup/node-effects.js +210 -0
- package/dist/setup/page-probe.d.ts +39 -0
- package/dist/setup/page-probe.js +49 -0
- package/dist/setup/relaunch.d.ts +36 -0
- package/dist/setup/relaunch.js +57 -0
- package/dist/setup/remaining-steps.d.ts +19 -0
- package/dist/setup/remaining-steps.js +31 -0
- package/dist/setup/run-setup.d.ts +99 -0
- package/dist/setup/run-setup.js +203 -0
- package/dist/setup/session-pick.d.ts +47 -0
- package/dist/setup/session-pick.js +51 -0
- package/dist/setup/setup-command.d.ts +45 -0
- package/dist/setup/setup-command.js +199 -0
- package/dist/setup/setup-options.d.ts +74 -0
- package/dist/setup/setup-options.js +67 -0
- package/dist/setup/transcripts.d.ts +16 -0
- package/dist/setup/transcripts.js +43 -0
- package/dist/status-payload.d.ts +14 -1
- package/dist/status-payload.js +23 -1
- package/dist/telemetry/daemon-telemetry.d.ts +8 -1
- package/dist/telemetry/daemon-telemetry.js +52 -5
- package/dist/telemetry/error-class.js +1 -1
- package/dist/telemetry/feedback-context.d.ts +9 -1
- package/dist/telemetry/feedback-context.js +60 -29
- package/dist/telemetry/feedback-local.d.ts +15 -0
- package/dist/telemetry/feedback-local.js +56 -0
- package/dist/telemetry/feedback.d.ts +11 -0
- package/dist/telemetry/feedback.js +19 -6
- package/dist/telemetry/init-telemetry.d.ts +2 -0
- package/dist/telemetry/init-telemetry.js +2 -0
- package/dist/telemetry/install-source.d.ts +14 -0
- package/dist/telemetry/install-source.js +30 -0
- package/dist/telemetry/license-activation.d.ts +7 -1
- package/dist/telemetry/license-activation.js +9 -5
- package/dist/telemetry/mcp-connection.js +7 -0
- package/dist/telemetry/project-profile.d.ts +12 -1
- package/dist/telemetry/project-profile.js +11 -6
- package/dist/telemetry/proxy-telemetry.d.ts +3 -0
- package/dist/telemetry/proxy-telemetry.js +21 -0
- package/dist/telemetry/session-metrics.d.ts +12 -0
- package/dist/telemetry/session-metrics.js +35 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +3 -1
- package/dist/telemetry/tool-refused.js +34 -2
- package/dist/tools/act-capsule.d.ts +10 -0
- package/dist/tools/act-capsule.js +8 -2
- package/dist/tools/act-danger.d.ts +2 -1
- package/dist/tools/act-danger.js +11 -3
- package/dist/tools/act-observation.d.ts +41 -0
- package/dist/tools/act-observation.js +36 -0
- package/dist/tools/act-preflight.d.ts +20 -0
- package/dist/tools/act-preflight.js +98 -0
- package/dist/tools/act-sequence-retry.d.ts +45 -0
- package/dist/tools/act-sequence-retry.js +58 -0
- package/dist/tools/act-sequence-tool.d.ts +13 -0
- package/dist/tools/act-sequence-tool.js +135 -0
- package/dist/tools/act-target.d.ts +14 -0
- package/dist/tools/act-target.js +47 -0
- package/dist/tools/act-tools.d.ts +21 -1
- package/dist/tools/act-tools.js +100 -213
- package/dist/tools/assert-verdict.js +22 -16
- package/dist/tools/browser-tools.js +6 -2
- package/dist/tools/dynamic-tools.js +24 -5
- package/dist/tools/error-recovery.d.ts +1 -0
- package/dist/tools/error-recovery.js +8 -0
- package/dist/tools/invoke-tool.js +59 -9
- package/dist/tools/lease-tools.d.ts +30 -4
- package/dist/tools/lease-tools.js +169 -5
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +22 -0
- package/dist/tools/numeric-bounds.d.ts +100 -0
- package/dist/tools/numeric-bounds.js +125 -0
- package/dist/tools/observe-tools.js +47 -40
- package/dist/tools/read-tools.js +28 -7
- package/dist/tools/real-input-attempt.d.ts +10 -0
- package/dist/tools/real-input-attempt.js +59 -1
- package/dist/tools/reconcile-tools.js +4 -4
- package/dist/tools/tool-kit.d.ts +12 -0
- package/dist/tools/tool-kit.js +6 -1
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tools-helpers.d.ts +19 -0
- package/dist/tools/tools-helpers.js +46 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +56 -8
- package/dist/update/reticle-deps.d.ts +9 -0
- package/dist/update/reticle-deps.js +27 -0
- package/dist/update/sdk-sync.d.ts +1 -2
- package/dist/update/sdk-sync.js +1 -21
- package/dist/update/update-nudge.d.ts +18 -0
- package/dist/update/update-nudge.js +20 -0
- package/dist/version/peer-announce.js +4 -2
- package/dist/version/sdk-fix.d.ts +47 -0
- package/dist/version/sdk-fix.js +127 -0
- package/dist/version/version-nudge.d.ts +14 -0
- package/dist/version/version-nudge.js +21 -0
- package/dist/version/version-skew.d.ts +38 -3
- package/dist/version/version-skew.js +83 -7
- package/dist/visual/visual-store.d.ts +6 -6
- package/dist/visual/visual-store.js +18 -16
- package/dist/visual/visual-tools.js +57 -12
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/serve.mdx +4 -2
- package/docs/cli.mdx +7 -3
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +1 -1
- package/docs/getting-started.md +19 -3
- package/docs/index.mdx +9 -6
- package/docs/install-agentic.mdx +38 -12
- package/docs/install-manual.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/skill-file.mdx +3 -3
- package/docs/telemetry-contract.md +108 -4
- package/docs/tools-act-sequence.mdx +16 -0
- package/docs/usage.md +1 -1
- package/package.json +4 -3
- package/dist/init/cursor.d.ts +0 -24
- package/dist/init/cursor.js +0 -80
|
@@ -23,6 +23,20 @@ interface VersionSkewNudge {
|
|
|
23
23
|
export declare function noteVersionSkew(pair: SkewPair, action: string): void;
|
|
24
24
|
/** The next undelivered skew, once. */
|
|
25
25
|
export declare function takeVersionSkew(): VersionSkewNudge | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Splice pending skew onto a tool-error payload.
|
|
28
|
+
*
|
|
29
|
+
* Successful calls already carry `version_skew` via `runTool`. Thrown errors never reached that
|
|
30
|
+
* splice — they became a Playwright string plus `FEEDBACK_ASK`, so a caller debugs a CDP timeout
|
|
31
|
+
* instead of the mismatched pair that caused it (#618). When the error was unrecognized, the
|
|
32
|
+
* feedback ask is replaced: the skew IS the next move, and inviting a bug report about it is
|
|
33
|
+
* backwards. A recognized recovery is kept; the envelope still rides along.
|
|
34
|
+
*/
|
|
35
|
+
export declare function takeVersionSkewOnto(payload: {
|
|
36
|
+
error: string;
|
|
37
|
+
recovery?: string;
|
|
38
|
+
feedback?: string;
|
|
39
|
+
}): Record<string, unknown>;
|
|
26
40
|
/** Tests only — drop the module state so each case starts clean. */
|
|
27
41
|
export declare function resetVersionSkew(): void;
|
|
28
42
|
export {};
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* result, then goes quiet until the skew CHANGES. Told, not nagged: a banner on every call is one an
|
|
12
12
|
* agent learns to skip, and it would cost those tokens on every call for the rest of the session.
|
|
13
13
|
*/
|
|
14
|
+
import { EnvelopeKey } from '../tools/tool-kit.js';
|
|
14
15
|
/** Pending per pair — the SDK and the daemon are different upgrades and each is worth saying once. */
|
|
15
16
|
const pending = new Map();
|
|
16
17
|
/** The last message delivered for each pair, so the same skew is never repeated. */
|
|
@@ -33,6 +34,26 @@ export function takeVersionSkew() {
|
|
|
33
34
|
}
|
|
34
35
|
return undefined;
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Splice pending skew onto a tool-error payload.
|
|
39
|
+
*
|
|
40
|
+
* Successful calls already carry `version_skew` via `runTool`. Thrown errors never reached that
|
|
41
|
+
* splice — they became a Playwright string plus `FEEDBACK_ASK`, so a caller debugs a CDP timeout
|
|
42
|
+
* instead of the mismatched pair that caused it (#618). When the error was unrecognized, the
|
|
43
|
+
* feedback ask is replaced: the skew IS the next move, and inviting a bug report about it is
|
|
44
|
+
* backwards. A recognized recovery is kept; the envelope still rides along.
|
|
45
|
+
*/
|
|
46
|
+
export function takeVersionSkewOnto(payload) {
|
|
47
|
+
const skew = takeVersionSkew();
|
|
48
|
+
if (skew === undefined)
|
|
49
|
+
return payload;
|
|
50
|
+
const { feedback, ...rest } = payload;
|
|
51
|
+
return {
|
|
52
|
+
...rest,
|
|
53
|
+
...(feedback !== undefined && rest.recovery === undefined ? { recovery: skew.action } : {}),
|
|
54
|
+
[EnvelopeKey.VERSION_SKEW]: skew,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
36
57
|
/** Tests only — drop the module state so each case starts clean. */
|
|
37
58
|
export function resetVersionSkew() {
|
|
38
59
|
pending.clear();
|
|
@@ -59,8 +59,43 @@ interface SelfIdentity {
|
|
|
59
59
|
* inventing a warning would be as dishonest as staying silent about a real one.
|
|
60
60
|
*/
|
|
61
61
|
export declare function describeSkew(peer: PeerIdentity, self: SelfIdentity): string | undefined;
|
|
62
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* The fix line for a page whose SDK disagrees with this daemon, with no project to read.
|
|
64
|
+
*
|
|
65
|
+
* Names the framework-neutral sensor and plain npm, because those are the answers that are never
|
|
66
|
+
* actively wrong. When a project directory IS available, `resolveSdkFix` / `sdkFixForDirectory`
|
|
67
|
+
* in sdk-fix.ts read the real packages and package manager — this used to hardcode
|
|
68
|
+
* `@reticlehq/react` and told Vue projects to install a React package (#618).
|
|
69
|
+
*/
|
|
63
70
|
export declare function sdkFix(daemonVersion: string): string;
|
|
64
|
-
/**
|
|
65
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The fix line for another Reticle process that disagrees with the daemon it attached to.
|
|
73
|
+
*
|
|
74
|
+
* Branches on DIRECTION, which the old unconditional `reticle stop` did not (#618). Restarting the
|
|
75
|
+
* daemon converges the pair only when the daemon is the OLDER half. When the daemon is newer — the
|
|
76
|
+
* common case, because the MCP registration is deliberately unpinned and so resolves fresh while a
|
|
77
|
+
* long-lived daemon does not — `reticle stop` starts another newer daemon and leaves the same two
|
|
78
|
+
* contract hashes with the roles unchanged. Nothing the agent can do converges that, so say so and
|
|
79
|
+
* ask for the one thing that does, exactly as the SDK branch already does when the page is stale.
|
|
80
|
+
*
|
|
81
|
+
* Both versions are named by ROLE, not by which side is calling: this is read from both ends — a
|
|
82
|
+
* CLI judging the daemon it attached to, and the daemon judging an agent that announced itself —
|
|
83
|
+
* and the advice depends on which piece is behind, not on who noticed.
|
|
84
|
+
*/
|
|
85
|
+
export declare function daemonFix(daemonVersion: string | undefined, agentVersion: string | undefined): string;
|
|
86
|
+
/**
|
|
87
|
+
* Playwright wording when a CDP call cannot bind to the page.
|
|
88
|
+
*
|
|
89
|
+
* Under SDK/daemon version skew this text is a LIE: the page is still connected (DOM tools work),
|
|
90
|
+
* and the agent spends turns re-acquiring leases and asking a human to sign into a fresh profile
|
|
91
|
+
* (#688). Matched only so we can replace it with the skew sentence already on the session.
|
|
92
|
+
*/
|
|
93
|
+
export declare const PLAYWRIGHT_CLOSED_ERROR: RegExp;
|
|
94
|
+
/** True when `error` is Playwright's closed-target class — not proof the tab is gone. */
|
|
95
|
+
export declare function isPlaywrightClosedError(error: unknown): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Replace a closed-target throw with the session's skew sentence when we already know the pair is
|
|
98
|
+
* mismatched. Undefined when there is no skew to name, or the throw is something else.
|
|
99
|
+
*/
|
|
100
|
+
export declare function rewriteClosedAsSkew(error: unknown, skew: string | undefined): Error | undefined;
|
|
66
101
|
export {};
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* the wire changes and stays put when internals do. Versions still ride along, because they are what
|
|
24
24
|
* a human acts on.
|
|
25
25
|
*/
|
|
26
|
+
import { resolveSdkFix } from './sdk-fix.js';
|
|
26
27
|
/** Which pair disagreed. Named so the nudge can report each independently. */
|
|
27
28
|
export const SkewPair = {
|
|
28
29
|
/** The SDK in the page vs this daemon. */
|
|
@@ -60,12 +61,87 @@ export function describeSkew(peer, self) {
|
|
|
60
61
|
`(${peer.contract} vs ${self.contract}) — so this is a real mismatch, not a harmless patch ` +
|
|
61
62
|
`difference. Tools will behave in ways neither side reports. ${peer.fix}`);
|
|
62
63
|
}
|
|
63
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* The fix line for a page whose SDK disagrees with this daemon, with no project to read.
|
|
66
|
+
*
|
|
67
|
+
* Names the framework-neutral sensor and plain npm, because those are the answers that are never
|
|
68
|
+
* actively wrong. When a project directory IS available, `resolveSdkFix` / `sdkFixForDirectory`
|
|
69
|
+
* in sdk-fix.ts read the real packages and package manager — this used to hardcode
|
|
70
|
+
* `@reticlehq/react` and told Vue projects to install a React package (#618).
|
|
71
|
+
*/
|
|
64
72
|
export function sdkFix(daemonVersion) {
|
|
65
|
-
return (
|
|
66
|
-
|
|
73
|
+
return resolveSdkFix(daemonVersion);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Compare two of our own version strings. Numeric dotted compare, prerelease tags ignored: both
|
|
77
|
+
* sides are Reticle package versions, so this never sees a range or build metadata. Undefined when
|
|
78
|
+
* either side does not parse, so the caller can decline to guess a direction.
|
|
79
|
+
*/
|
|
80
|
+
function compareVersions(a, b) {
|
|
81
|
+
const parts = (v) => {
|
|
82
|
+
const head = v.split('-')[0];
|
|
83
|
+
if (head === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
const nums = head.split('.').map((n) => parseInt(n, 10));
|
|
86
|
+
return nums.some(isNaN) ? undefined : nums;
|
|
87
|
+
};
|
|
88
|
+
if (a === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
const left = parts(a);
|
|
91
|
+
const right = parts(b);
|
|
92
|
+
if (left === undefined || right === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
|
95
|
+
const diff = (left[i] ?? 0) - (right[i] ?? 0);
|
|
96
|
+
if (0 !== diff)
|
|
97
|
+
return diff;
|
|
98
|
+
}
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The fix line for another Reticle process that disagrees with the daemon it attached to.
|
|
103
|
+
*
|
|
104
|
+
* Branches on DIRECTION, which the old unconditional `reticle stop` did not (#618). Restarting the
|
|
105
|
+
* daemon converges the pair only when the daemon is the OLDER half. When the daemon is newer — the
|
|
106
|
+
* common case, because the MCP registration is deliberately unpinned and so resolves fresh while a
|
|
107
|
+
* long-lived daemon does not — `reticle stop` starts another newer daemon and leaves the same two
|
|
108
|
+
* contract hashes with the roles unchanged. Nothing the agent can do converges that, so say so and
|
|
109
|
+
* ask for the one thing that does, exactly as the SDK branch already does when the page is stale.
|
|
110
|
+
*
|
|
111
|
+
* Both versions are named by ROLE, not by which side is calling: this is read from both ends — a
|
|
112
|
+
* CLI judging the daemon it attached to, and the daemon judging an agent that announced itself —
|
|
113
|
+
* and the advice depends on which piece is behind, not on who noticed.
|
|
114
|
+
*/
|
|
115
|
+
export function daemonFix(daemonVersion, agentVersion) {
|
|
116
|
+
const order = agentVersion === undefined ? undefined : compareVersions(daemonVersion, agentVersion);
|
|
117
|
+
if (order !== undefined && order > 0) {
|
|
118
|
+
return ('The daemon is NEWER than this process, so `reticle stop` will not converge them — it would ' +
|
|
119
|
+
'replace it with another newer daemon and leave the same mismatch. Tell the human to restart ' +
|
|
120
|
+
'the agent so it spawns a current MCP server (or update the package it spawns), then retry.');
|
|
121
|
+
}
|
|
122
|
+
return ('A daemon outlives the agents attached to it, so it keeps serving its OWN code until it restarts ' +
|
|
123
|
+
'— anything fixed in the newer package is simply absent. Run `reticle stop` and retry to replace ' +
|
|
124
|
+
'it (other agents on that daemon will need to reconnect).');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Playwright wording when a CDP call cannot bind to the page.
|
|
128
|
+
*
|
|
129
|
+
* Under SDK/daemon version skew this text is a LIE: the page is still connected (DOM tools work),
|
|
130
|
+
* and the agent spends turns re-acquiring leases and asking a human to sign into a fresh profile
|
|
131
|
+
* (#688). Matched only so we can replace it with the skew sentence already on the session.
|
|
132
|
+
*/
|
|
133
|
+
export const PLAYWRIGHT_CLOSED_ERROR = /target (?:page|context|browser) has been closed|browser has been closed|browser has disconnected/i;
|
|
134
|
+
/** True when `error` is Playwright's closed-target class — not proof the tab is gone. */
|
|
135
|
+
export function isPlaywrightClosedError(error) {
|
|
136
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
137
|
+
return PLAYWRIGHT_CLOSED_ERROR.test(message);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Replace a closed-target throw with the session's skew sentence when we already know the pair is
|
|
141
|
+
* mismatched. Undefined when there is no skew to name, or the throw is something else.
|
|
142
|
+
*/
|
|
143
|
+
export function rewriteClosedAsSkew(error, skew) {
|
|
144
|
+
if (skew === undefined || !isPlaywrightClosedError(error))
|
|
145
|
+
return undefined;
|
|
146
|
+
return new Error(skew);
|
|
67
147
|
}
|
|
68
|
-
/** The fix line for another Reticle process that disagrees with the daemon it attached to. */
|
|
69
|
-
export const DAEMON_FIX = 'A daemon outlives the agents attached to it, so it keeps serving its OWN code until it restarts ' +
|
|
70
|
-
'— anything fixed in the newer package is simply absent. Run `reticle stop` and retry to replace ' +
|
|
71
|
-
'it (other agents on that daemon will need to reconnect).';
|
|
@@ -8,15 +8,15 @@ export declare class VisualStore {
|
|
|
8
8
|
constructor(fs: FileSystemPort, root: string);
|
|
9
9
|
/** The absolute baseline path for `name` (for echoing back to the agent). Guards the name so a
|
|
10
10
|
* crafted value can never echo a path outside .reticle/visual/ to a future caller that does IO. */
|
|
11
|
-
baselinePath(name: string): string;
|
|
11
|
+
baselinePath(name: string, runtime?: string): string;
|
|
12
12
|
/** The absolute overlay-diff path for `name`. Same name guard as baselinePath. */
|
|
13
|
-
diffPath(name: string): string;
|
|
13
|
+
diffPath(name: string, runtime?: string): string;
|
|
14
14
|
/** True iff a valid name and a baseline PNG exists on disk. */
|
|
15
|
-
hasBaseline(name: string): Promise<boolean>;
|
|
15
|
+
hasBaseline(name: string, runtime?: string): Promise<boolean>;
|
|
16
16
|
/** Write a PNG baseline (auto-creating .reticle/visual/). Returns its path; throws on a bad name. */
|
|
17
|
-
saveBaseline(name: string, png: Uint8Array): Promise<string>;
|
|
17
|
+
saveBaseline(name: string, png: Uint8Array, runtime?: string): Promise<string>;
|
|
18
18
|
/** Read a PNG baseline, or undefined if absent / invalid name (never throws on missing). */
|
|
19
|
-
readBaseline(name: string): Promise<Uint8Array | undefined>;
|
|
19
|
+
readBaseline(name: string, runtime?: string): Promise<Uint8Array | undefined>;
|
|
20
20
|
/** Write the overlay diff PNG for `name` (auto-creating .reticle/visual/). Returns its path. */
|
|
21
|
-
saveDiff(name: string, png: Uint8Array): Promise<string>;
|
|
21
|
+
saveDiff(name: string, png: Uint8Array, runtime?: string): Promise<string>;
|
|
22
22
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isValidFlowName, visualDiffPath, visualDir, visualPath } from '../project/reticle-dir.js';
|
|
2
2
|
/**
|
|
3
3
|
* On-disk PNG baselines + diffs under .reticle/visual/. Binary sibling of FlowStore — same
|
|
4
4
|
* name guard (rejects path traversal before any join) and injected FileSystemPort, but bytes not text.
|
|
@@ -12,39 +12,40 @@ export class VisualStore {
|
|
|
12
12
|
}
|
|
13
13
|
/** The absolute baseline path for `name` (for echoing back to the agent). Guards the name so a
|
|
14
14
|
* crafted value can never echo a path outside .reticle/visual/ to a future caller that does IO. */
|
|
15
|
-
baselinePath(name) {
|
|
15
|
+
baselinePath(name, runtime) {
|
|
16
16
|
if (!isValidFlowName(name))
|
|
17
17
|
throw new Error(`invalid visual baseline name: ${name}`);
|
|
18
|
-
return visualPath(this.#root, name);
|
|
18
|
+
return visualPath(this.#root, name, runtime);
|
|
19
19
|
}
|
|
20
20
|
/** The absolute overlay-diff path for `name`. Same name guard as baselinePath. */
|
|
21
|
-
diffPath(name) {
|
|
21
|
+
diffPath(name, runtime) {
|
|
22
22
|
if (!isValidFlowName(name))
|
|
23
23
|
throw new Error(`invalid visual diff name: ${name}`);
|
|
24
|
-
return visualDiffPath(this.#root, name);
|
|
24
|
+
return visualDiffPath(this.#root, name, runtime);
|
|
25
25
|
}
|
|
26
26
|
/** True iff a valid name and a baseline PNG exists on disk. */
|
|
27
|
-
async hasBaseline(name) {
|
|
27
|
+
async hasBaseline(name, runtime) {
|
|
28
28
|
if (!isValidFlowName(name))
|
|
29
29
|
return false;
|
|
30
|
-
return this.#fs.exists(visualPath(this.#root, name));
|
|
30
|
+
return this.#fs.exists(visualPath(this.#root, name, runtime));
|
|
31
31
|
}
|
|
32
32
|
/** Write a PNG baseline (auto-creating .reticle/visual/). Returns its path; throws on a bad name. */
|
|
33
|
-
async saveBaseline(name, png) {
|
|
33
|
+
async saveBaseline(name, png, runtime) {
|
|
34
34
|
if (!isValidFlowName(name))
|
|
35
35
|
throw new Error(`invalid visual baseline name: ${name}`);
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// The runtime's own directory, not the shared one — see visualDir.
|
|
37
|
+
await this.#fs.mkdir(visualDir(this.#root, runtime));
|
|
38
|
+
const path = visualPath(this.#root, name, runtime);
|
|
38
39
|
await this.#fs.writeFileBytes(path, png);
|
|
39
40
|
return path;
|
|
40
41
|
}
|
|
41
42
|
/** Read a PNG baseline, or undefined if absent / invalid name (never throws on missing). */
|
|
42
|
-
async readBaseline(name) {
|
|
43
|
-
if (!isValidFlowName(name) || !(await this.#fs.exists(visualPath(this.#root, name)))) {
|
|
43
|
+
async readBaseline(name, runtime) {
|
|
44
|
+
if (!isValidFlowName(name) || !(await this.#fs.exists(visualPath(this.#root, name, runtime)))) {
|
|
44
45
|
return undefined;
|
|
45
46
|
}
|
|
46
47
|
try {
|
|
47
|
-
return await this.#fs.readFileBytes(visualPath(this.#root, name));
|
|
48
|
+
return await this.#fs.readFileBytes(visualPath(this.#root, name, runtime));
|
|
48
49
|
}
|
|
49
50
|
catch (error) {
|
|
50
51
|
if (this.#fs.isNotFound(error))
|
|
@@ -53,11 +54,12 @@ export class VisualStore {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
/** Write the overlay diff PNG for `name` (auto-creating .reticle/visual/). Returns its path. */
|
|
56
|
-
async saveDiff(name, png) {
|
|
57
|
+
async saveDiff(name, png, runtime) {
|
|
57
58
|
if (!isValidFlowName(name))
|
|
58
59
|
throw new Error(`invalid visual baseline name: ${name}`);
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
// Beside its baseline, or a diff lands in a directory the baseline is not in.
|
|
61
|
+
await this.#fs.mkdir(visualDir(this.#root, runtime));
|
|
62
|
+
const path = visualDiffPath(this.#root, name, runtime);
|
|
61
63
|
await this.#fs.writeFileBytes(path, png);
|
|
62
64
|
return path;
|
|
63
65
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { ReticleCommand, RETICLE_CAPTURE_FILE_PREFIX, VISUAL_NO_PROVIDER_RECOMMENDATION, VisualReason, } from '@reticlehq/core';
|
|
2
|
+
import { AppRuntime, ReticleCommand, RETICLE_CAPTURE_FILE_PREFIX, VISUAL_NO_PROVIDER_RECOMMENDATION, VisualReason, } from '@reticlehq/core';
|
|
3
3
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
4
4
|
import { sessionIdShape } from '../tools/tool-kit.js';
|
|
5
|
+
import { ratioSchema } from '../tools/numeric-bounds.js';
|
|
5
6
|
import { asNumber, asRecord, asString } from '../tools/tools-helpers.js';
|
|
6
7
|
import { diffPng } from './visual-diff.js';
|
|
7
8
|
import { VisualStore } from './visual-store.js';
|
|
@@ -148,19 +149,51 @@ function isCompletePng(bytes) {
|
|
|
148
149
|
* `full-page-unsupported` — a viewport image returned as though it were the whole scroll height is a
|
|
149
150
|
* baseline that says nothing about the content below the fold, and a later diff would call it green.
|
|
150
151
|
*/
|
|
152
|
+
/**
|
|
153
|
+
* Which runtime this session is, for scoping a visual artifact.
|
|
154
|
+
*
|
|
155
|
+
* Undefined when the session cannot be resolved or its SDK never reported one — and undefined means
|
|
156
|
+
* the flat legacy path, so a baseline captured before any of this keeps matching.
|
|
157
|
+
*/
|
|
158
|
+
function runtimeOf(deps, sessionId) {
|
|
159
|
+
try {
|
|
160
|
+
return deps.sessions.resolve(sessionId).runtime;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
151
166
|
async function capture(deps, sessionId, args) {
|
|
152
167
|
const provider = screenshotProvider(deps);
|
|
153
168
|
if (provider !== undefined) {
|
|
154
169
|
const session = deps.sessions.resolve(sessionId);
|
|
155
170
|
const png = await provider.screenshot(session.url, await buildOpts(deps, sessionId, args));
|
|
171
|
+
// A driven browser renders the session's URL in a BROWSER — so the pixels are web even when the
|
|
172
|
+
// session named is a desktop window. Scoping those under the desktop runtime would corrupt that
|
|
173
|
+
// runtime's baseline with a picture of a different renderer.
|
|
156
174
|
if (png !== undefined)
|
|
157
|
-
return { png };
|
|
175
|
+
return { png, runtime: AppRuntime.WEB };
|
|
158
176
|
}
|
|
177
|
+
// The window's own backing store, via the Electron/Tauri adapter — the only route whose pixels
|
|
178
|
+
// really are the desktop app.
|
|
159
179
|
const desktop = await desktopCapture(deps, sessionId, true === args['fullPage']);
|
|
160
180
|
if (desktop.png !== undefined)
|
|
161
|
-
return { png: desktop.png };
|
|
181
|
+
return { png: desktop.png, runtime: runtimeOf(deps, sessionId) };
|
|
162
182
|
if (desktop.reason !== undefined)
|
|
163
183
|
return { reason: desktop.reason };
|
|
184
|
+
/*
|
|
185
|
+
* A LEASED page is a real browser page, so the pixels were always there — the visual tools simply
|
|
186
|
+
* had no route to them. Without this, `reticle_screenshot` answered "no provider" for every context
|
|
187
|
+
* an agent can actually acquire, which made visual regression impossible on exactly the isolated
|
|
188
|
+
* contexts the pool exists to hand out. Tried last: a CDP provider, when there is one, is driving
|
|
189
|
+
* the page the caller means, and a lease is the fallback rather than a competitor.
|
|
190
|
+
*/
|
|
191
|
+
const leased = sessionId === undefined
|
|
192
|
+
? undefined
|
|
193
|
+
: await deps.pool?.screenshotLease(sessionId, { fullPage: true === args['fullPage'] });
|
|
194
|
+
// A leased page is a real browser page, whatever the session is.
|
|
195
|
+
if (leased !== undefined)
|
|
196
|
+
return { png: leased, runtime: AppRuntime.WEB };
|
|
164
197
|
return {
|
|
165
198
|
reason: provider === undefined ? VisualReason.NO_PROVIDER : VisualReason.CAPTURE_FAILED,
|
|
166
199
|
};
|
|
@@ -215,13 +248,16 @@ export const VISUAL_TOOLS = [
|
|
|
215
248
|
},
|
|
216
249
|
handler: async (deps, args) => {
|
|
217
250
|
const sessionId = asString(args['sessionId']);
|
|
218
|
-
const { png, reason } = await capture(deps, sessionId, args);
|
|
251
|
+
const { png, reason, runtime } = await capture(deps, sessionId, args);
|
|
219
252
|
if (png === undefined) {
|
|
220
253
|
return reason === VisualReason.NO_PROVIDER ? noProvider : { ok: false, reason };
|
|
221
254
|
}
|
|
222
255
|
const name = asString(args['name']) ?? 'default';
|
|
223
256
|
const store = new VisualStore(deps.fs, deps.reticleRoot);
|
|
224
|
-
|
|
257
|
+
// Scoped to the runtime that produced it: an Electron window, a Tauri webview and a browser
|
|
258
|
+
// tab do not render the same url the same way, and one shared baseline makes every
|
|
259
|
+
// cross-runtime diff wrong. See visualDir.
|
|
260
|
+
const path = await store.saveBaseline(name, png, runtime);
|
|
225
261
|
return { ok: true, saved: true, name, path, bytes: png.length };
|
|
226
262
|
},
|
|
227
263
|
},
|
|
@@ -237,8 +273,10 @@ export const VISUAL_TOOLS = [
|
|
|
237
273
|
ref: z.string().optional(),
|
|
238
274
|
clip: rectShape.optional(),
|
|
239
275
|
masks: z.array(rectShape).optional(),
|
|
240
|
-
maxRatio:
|
|
241
|
-
threshold:
|
|
276
|
+
maxRatio: ratioSchema.optional(),
|
|
277
|
+
threshold: ratioSchema
|
|
278
|
+
.optional()
|
|
279
|
+
.describe('Pixel difference threshold (0–1). Default: 0.01.'),
|
|
242
280
|
...sessionIdShape,
|
|
243
281
|
},
|
|
244
282
|
outputSchema: {
|
|
@@ -255,14 +293,21 @@ export const VISUAL_TOOLS = [
|
|
|
255
293
|
handler: async (deps, args) => {
|
|
256
294
|
const baseline = asString(args['baseline']) ?? '';
|
|
257
295
|
const store = new VisualStore(deps.fs, deps.reticleRoot);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
296
|
+
// Capture FIRST, then fetch the baseline for the runtime that produced these pixels. Reading it
|
|
297
|
+
// by the session's runtime looked equivalent and is not: the route decides the renderer, so a
|
|
298
|
+
// desktop session captured through a driven browser must be compared against the WEB baseline,
|
|
299
|
+
// not the desktop one. Comparing across renderers is a confident wrong diff in one direction
|
|
300
|
+
// and a silently overwritten baseline in the other.
|
|
261
301
|
const sessionId = asString(args['sessionId']);
|
|
262
|
-
const { png: current, reason } = await capture(deps, sessionId, args);
|
|
302
|
+
const { png: current, reason, runtime } = await capture(deps, sessionId, args);
|
|
263
303
|
if (current === undefined) {
|
|
264
304
|
return reason === VisualReason.NO_PROVIDER ? noProvider : { ok: false, reason };
|
|
265
305
|
}
|
|
306
|
+
// A baseline from another runtime is not missing by accident — it is a different rendering, and
|
|
307
|
+
// "no baseline for this one" is the honest answer.
|
|
308
|
+
const baselineBytes = await store.readBaseline(baseline, runtime);
|
|
309
|
+
if (baselineBytes === undefined)
|
|
310
|
+
return { ok: false, reason: VisualReason.BASELINE_MISSING };
|
|
266
311
|
const masks = rectsFrom(args['masks']);
|
|
267
312
|
const threshold = asNumber(args['threshold']);
|
|
268
313
|
const maxRatio = asNumber(args['maxRatio']);
|
|
@@ -276,7 +321,7 @@ export const VISUAL_TOOLS = [
|
|
|
276
321
|
if (diffBytes === undefined) {
|
|
277
322
|
return { ok: false, ...verdict, reason: VisualReason.DIMENSION_MISMATCH };
|
|
278
323
|
}
|
|
279
|
-
const diffPath = await store.saveDiff(baseline, diffBytes);
|
|
324
|
+
const diffPath = await store.saveDiff(baseline, diffBytes, runtime);
|
|
280
325
|
return { ok: true, ...verdict, diffPath };
|
|
281
326
|
},
|
|
282
327
|
},
|
package/docs/cli/doctor.mdx
CHANGED
|
@@ -51,6 +51,7 @@ When nothing has connected, the `sessions` line fails and the daemon's own diagn
|
|
|
51
51
|
| `chromium` | Whether the exact Chromium revision the bundled Playwright wants is on disk. The most common silent failure |
|
|
52
52
|
| `daemon` | Whether the port holds a Reticle daemon, a foreign process (named where possible), or nothing. Adds a `version` line when the daemon's version or contract fingerprint disagrees with the CLI's |
|
|
53
53
|
| `sessions` | Whether any page has actually connected. Zero is a FAILING check rather than a neutral note, because it is the state the setup most often stalls in: wired correctly, daemon up, nothing dialled in. When it is zero the daemon's own no-session diagnosis prints below the checklist |
|
|
54
|
+
| `agent link` | Whether an MCP client has listed Reticle's tools on this port **and actually called one**. Every other row here checks a component; this one checks the hop between your agent and the daemon. It is possible for the SDK to be injected, a session to be live, and your client to show every tool enabled, while no request has ever crossed from the agent. That state passes every other check and is the one people lose hours to |
|
|
54
55
|
| `bridge port` | The port your app must dial, spelled out because mixing it up with the dev server port is the most common setup failure |
|
|
55
56
|
| `port check` | Only when your project's configured port disagrees with the one the daemon is on. Prints the mismatch rather than leaving you to spot it across two lines |
|
|
56
57
|
| `sibling` | Only when a well-known Reticle port other than this daemon's has a listener. An observation, not a diagnosis: that listener may or may not be related |
|
package/docs/cli/serve.mdx
CHANGED
|
@@ -21,8 +21,10 @@ Most people never type this. Your agent runs [`reticle mcp`](/cli/mcp), which st
|
|
|
21
21
|
| `--headless` | boolean | `true` for `serve` | Hide the browser |
|
|
22
22
|
| `--headed` | boolean | `false` | Show it. `serve` owns the pooled browser behind leases, which is batch work, so it hides by default |
|
|
23
23
|
| `--http` | boolean | `false` | Also expose the HTTP verify endpoint |
|
|
24
|
-
| `--http-port N` | number | unset | Port for that endpoint |
|
|
25
|
-
| `--http-token T` | string | unset | Bearer token the endpoint requires |
|
|
24
|
+
| `--http-port N` | number | unset (`7331`) | Port for that endpoint. Requires `--http`. |
|
|
25
|
+
| `--http-token T` | string | unset | Bearer token the endpoint requires. Requires `--http`. |
|
|
26
|
+
|
|
27
|
+
The HTTP flags are honoured or refused, never silently dropped: against an already-running daemon that does not serve the requested verify port, `serve --http` exits 1 and names both ports. A running daemon keeps the flags it was started with, so `reticle stop`, then serve again. A verify port that cannot be bound fails the daemon start with a `reticle_daemon_start_failed` line naming that port.
|
|
26
28
|
|
|
27
29
|
## What it prints
|
|
28
30
|
|
package/docs/cli.mdx
CHANGED
|
@@ -4,7 +4,7 @@ description: Every reticle command. What it does, what it prints, and what it ex
|
|
|
4
4
|
icon: terminal
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
The `reticle` CLI
|
|
7
|
+
The `reticle` CLI installs Reticle into a project and proves the install works (`init`), runs and diagnoses the daemon your app connects to (`serve`, `status`, `doctor`, `kill`, `restart`), and gates a build on verification in CI (`verify`, `affected`, `gate`). This page is the index; each command has its own page with real captured output and measured exit codes.
|
|
8
8
|
|
|
9
9
|
The CLI is mostly for you, not your agent. Your agent talks MCP; you use these to set things up, work out why nothing connected, and wire verification into CI.
|
|
10
10
|
|
|
@@ -32,12 +32,16 @@ npx @reticlehq/server <command>
|
|
|
32
32
|
|
|
33
33
|
### `reticle init`
|
|
34
34
|
|
|
35
|
-
Wires Reticle into the project in the current directory.
|
|
35
|
+
Wires Reticle into the project in the current directory, then boots the app, proves a session connected and drives one flow to a verdict. It exits non-zero if no verdict was produced, and prints what is left to do.
|
|
36
36
|
|
|
37
37
|
```
|
|
38
|
-
reticle init [--
|
|
38
|
+
reticle init [--flow "<what to drive>"] [--env KEY=VALUE]... [--app <dir>] [--license <key>]
|
|
39
|
+
[--json] [--files-only] [--no-open] [--no-drive] [--dry-run]
|
|
40
|
+
[--port N] [--no-mcp] [--no-install] [--timeout S] [--drive-model M]
|
|
39
41
|
```
|
|
40
42
|
|
|
43
|
+
`--flow`, `--env` and `--app` are the answers the command cannot work out for itself: which journey proves what you care about, what the app needs to reach a usable state, and which app in a monorepo. `--files-only` is what `init` did before it learned to boot the app: it still registers the MCP server and pre-approves Reticle's tools, so it doubles as the upgrade path for an existing install.
|
|
44
|
+
|
|
41
45
|
<Card title="Full walkthrough with real output" icon="wand-magic-sparkles" href="/install-agentic">
|
|
42
46
|
What it writes, and how to read its four status marks.
|
|
43
47
|
</Card>
|
package/docs/desktop.mdx
CHANGED
package/docs/docs.json
CHANGED
package/docs/getting-started.md
CHANGED
|
@@ -4,7 +4,7 @@ description: 'Zero to your agent verifying your real app, step by step, with wor
|
|
|
4
4
|
icon: rocket
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
**To get started with Reticle: run `npx @reticlehq/server init` in your project root,
|
|
7
|
+
**To get started with Reticle: run `npx @reticlehq/server init --flow "<the journey worth proving>"` in your project root.** That one command wires your project, starts your dev server, opens the app, waits for it to connect, and drives that flow to a verdict. You do not restart anything and you do not drive it yourself. Reticle is a verification layer that embeds a dev-only SDK in your running web app so an AI coding agent can prove a change works instead of guessing. It needs Node 20 or newer, an app you run locally, and an agent that speaks MCP.
|
|
8
8
|
|
|
9
9
|
> **Looking for the fast path?** [Quickstart](/quickstart) gets you to a real verdict in five minutes, and every response on it was captured live. [Agentic install](/install-agentic) and [Manual install](/install-manual) cover setup in detail, per agent and per framework.
|
|
10
10
|
>
|
|
@@ -76,7 +76,19 @@ It detects your framework, package manager, and React version, then:
|
|
|
76
76
|
|
|
77
77
|
The bridge + MCP server is a single process that serves all your projects, so it's registered at **user scope**, not in a per-project `.mcp.json`. Only the SDK (the `reticle()` plugin / connect call) is added per project.
|
|
78
78
|
|
|
79
|
-
Re-running is safe
|
|
79
|
+
Re-running is safe: already-registered and already-patched steps are skipped, and on a wired project it goes straight to proving the app still works. Preview without writing via `npx @reticlehq/server init --dry-run`.
|
|
80
|
+
|
|
81
|
+
`init` does not stop at writing files. It starts your dev server (restarting one whose bundle predates the config edit), opens the app, waits for a session to connect from inside it, and drives one flow to a verdict, which it saves so later checks are a single call with no model involved. It exits non-zero if no verdict was produced and prints what is left to do.
|
|
82
|
+
|
|
83
|
+
Three flags carry what the command cannot work out for itself:
|
|
84
|
+
|
|
85
|
+
| Flag | What only you know |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `--flow "<what>"` | Which journey proves the thing you care about. It can list the buttons on your page; it cannot know that checkout matters and the theme toggle does not. |
|
|
88
|
+
| `--env KEY=VALUE` | What your app needs to reach a usable state: the key from `.env.example`, the mock backend, the variable that skips an auth wall. Repeatable. |
|
|
89
|
+
| `--app <dir>` | Which app in a monorepo. It finds the servable ones; only you know which you are working in. |
|
|
90
|
+
|
|
91
|
+
The rest are dials: `--license <key>` (writes it to `.env` and keeps `.env` out of git), `--json` (one object for an agent to read), `--files-only` (write, register, pre-approve, and stop, which is what `init` did before it learned to boot the app, and what an existing install re-runs to pick up new wiring), `--no-open`, `--no-drive`, `--dry-run`, `--port N`, `--no-mcp`, `--no-install`.
|
|
80
92
|
|
|
81
93
|
Then restart your dev server and skip to [Step 4](#step-4-run-it-and-verify-the-connection). The manual steps below explain what `init` sets up, if you prefer to wire it yourself.
|
|
82
94
|
|
|
@@ -310,6 +322,8 @@ export default defineConfig({
|
|
|
310
322
|
|
|
311
323
|
## Step 4: Run it and verify the connection
|
|
312
324
|
|
|
325
|
+
> `reticle init` does this for you: it starts the dev server, opens the app and waits for the session. Read on only if you are wiring by hand, or if it told you it could not.
|
|
326
|
+
|
|
313
327
|
1. Start your app's dev server as usual (`npm run dev`).
|
|
314
328
|
2. Open it in the browser (the SDK connects when the page loads).
|
|
315
329
|
3. In your agent, ask it to confirm the connection:
|
|
@@ -328,6 +342,8 @@ If the list is empty, see [Troubleshooting](#troubleshooting).
|
|
|
328
342
|
|
|
329
343
|
## Step 5: Your first verification
|
|
330
344
|
|
|
345
|
+
> `reticle init --flow "<what>"` drives this for you and saves the flow. This section is what it does, for when you want to do it yourself.
|
|
346
|
+
|
|
331
347
|
Now just talk to your agent in plain language. For example:
|
|
332
348
|
|
|
333
349
|
> "Add a 'Refresh' button to the header that re-fetches the dashboard data, then use Reticle to verify clicking it fires `GET /api/dashboard` and shows no console errors."
|
|
@@ -490,7 +506,7 @@ pnpm add @reticlehq/next
|
|
|
490
506
|
|
|
491
507
|
### What exactly does `reticle init` change in my project?
|
|
492
508
|
|
|
493
|
-
Four
|
|
509
|
+
Four files, and none of them are mysterious: a `.reticle.json` project config, your build config (the `reticle()` Vite plugin, or `withReticle` in `next.config`), a dev-only capabilities file at `src/reticle-dev.ts` (or `app/reticle-dev.tsx` on Next.js), and your agent's rule files. It then starts your dev server, opens the app and drives one flow, none of which changes your source: the dev server is left running for you afterwards, and the only file the drive may edit is the capabilities file, and only when your app registers none. It also registers the MCP server globally, which is a once-per-machine step rather than a per-project one. Run `npx @reticlehq/server init --dry-run` first to see the exact plan before anything is written.
|
|
494
510
|
|
|
495
511
|
### Do I have to re-register the MCP server for every project?
|
|
496
512
|
|
package/docs/index.mdx
CHANGED
|
@@ -14,7 +14,7 @@ It answers the only question that matters: did the change actually work? On Reac
|
|
|
14
14
|
|
|
15
15
|
## Install it: paste this into your coding agent
|
|
16
16
|
|
|
17
|
-
You do not install Reticle by hand. Copy the block below into Claude Code, Cursor, Copilot, Codex, Windsurf, OpenCode, or any MCP agent, and let
|
|
17
|
+
You do not install Reticle by hand, and neither does your agent: it runs one command that does the whole thing. Copy the block below into Claude Code, Cursor, Copilot, Codex, Windsurf, OpenCode, or any MCP agent, and let it wire its own workspace. It ends with the only definition of "installed" that counts: one real flow driven, one real verdict returned.
|
|
18
18
|
|
|
19
19
|
```text
|
|
20
20
|
Reticle is the proof layer for AI coding agents. It runs a dev-only SDK inside my
|
|
@@ -30,12 +30,15 @@ silent 500.
|
|
|
30
30
|
|
|
31
31
|
Install it now:
|
|
32
32
|
|
|
33
|
-
RETICLE_INSTALL_SOURCE=docs_site npx @reticlehq/server@latest init
|
|
33
|
+
RETICLE_INSTALL_SOURCE=docs_site npx @reticlehq/server@latest init \
|
|
34
|
+
--flow "<pick the journey that matters in my app and name it here>"
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
That one command wires the project, starts the dev server, opens the app, waits
|
|
37
|
+
for it to connect and drives that flow to a verdict. Do not walk it by hand: read
|
|
38
|
+
what it prints, do anything it says is outstanding, and re-run it, which is safe.
|
|
39
|
+
It exits non-zero until a verdict exists, and that, not a config file, is what
|
|
40
|
+
"installed" means. Only if it could not run at all, follow
|
|
41
|
+
https://docs.reticle.sh/install-manual.md.
|
|
39
42
|
|
|
40
43
|
Afterwards, use it whenever you change any user-facing behaviour.
|
|
41
44
|
```
|